Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
Blame · Line-by-line history

workflows.test.ts

Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.

workflows.test.tsBlame186 lines · 1 contributor
eafe8c6Claude1/**
2 * Tests for Block C1 — Actions-equivalent workflow runner.
3 *
4 * Covers the pure-function parser + route-level unauthed guards. The
5 * shell-executor itself is exercised by higher-level integration tests
6 * once a real test DB is wired — for now we only verify that the
7 * exported surface exists and the route shell is correct.
8 */
9
10import { describe, it, expect } from "bun:test";
11import app from "../app";
12import { parseWorkflow } from "../lib/workflow-parser";
13
14describe("workflow parser (C1)", () => {
15 it("parses a minimal workflow", () => {
16 const result = parseWorkflow(`name: CI
17on: [push]
18jobs:
19 test:
20 runs-on: default
21 steps:
22 - run: echo hello
23`);
24 expect(result.ok).toBe(true);
25 if (!result.ok) return;
26 expect(result.workflow.name).toBe("CI");
27 expect(result.workflow.on).toContain("push");
28 expect(result.workflow.jobs).toHaveLength(1);
29 expect(result.workflow.jobs[0].name).toBe("test");
30 expect(result.workflow.jobs[0].steps).toHaveLength(1);
31 expect(result.workflow.jobs[0].steps[0].run).toBe("echo hello");
32 });
33
34 it("handles scalar 'on' trigger", () => {
35 const result = parseWorkflow(`name: scalar
36on: push
37jobs:
38 test:
39 steps:
40 - run: pwd
41`);
42 expect(result.ok).toBe(true);
43 if (!result.ok) return;
44 expect(result.workflow.on).toEqual(["push"]);
45 });
46
47 it("handles list 'on' triggers", () => {
48 const result = parseWorkflow(`name: multi
49on: [push, pull_request]
50jobs:
51 a:
52 steps:
53 - run: true
54`);
55 expect(result.ok).toBe(true);
56 if (!result.ok) return;
57 expect(result.workflow.on).toContain("push");
58 expect(result.workflow.on).toContain("pull_request");
59 });
60
61 it("auto-names steps that only have a run field", () => {
62 const result = parseWorkflow(`name: n
63on: [push]
64jobs:
65 test:
66 steps:
67 - run: echo x
68`);
69 expect(result.ok).toBe(true);
70 if (!result.ok) return;
71 expect(result.workflow.jobs[0].steps[0].name).toBeTruthy();
72 });
73
74 it("preserves explicit step names", () => {
75 const result = parseWorkflow(`name: n
76on: [push]
77jobs:
78 test:
79 steps:
80 - name: Install
81 run: bun install
82 - name: Test
83 run: bun test
84`);
85 expect(result.ok).toBe(true);
86 if (!result.ok) return;
87 const names = result.workflow.jobs[0].steps.map((s) => s.name);
88 expect(names).toContain("Install");
89 expect(names).toContain("Test");
90 });
91
92 it("defaults runs-on to 'default' when omitted", () => {
93 const result = parseWorkflow(`name: n
94on: [push]
95jobs:
96 test:
97 steps:
98 - run: true
99`);
100 expect(result.ok).toBe(true);
101 if (!result.ok) return;
102 expect(result.workflow.jobs[0].runsOn).toBe("default");
103 });
104
105 it("rejects workflows with no 'on' trigger", () => {
106 const result = parseWorkflow(`name: bad
107jobs:
108 test:
109 steps:
110 - run: true
111`);
112 expect(result.ok).toBe(false);
113 if (result.ok) return;
114 expect(result.error.toLowerCase()).toContain("on");
115 });
116
117 it("rejects workflows with no jobs", () => {
118 const result = parseWorkflow(`name: bad
119on: [push]
120`);
121 expect(result.ok).toBe(false);
122 });
123
124 it("rejects jobs with no steps", () => {
125 const result = parseWorkflow(`name: bad
126on: [push]
127jobs:
128 test:
129 runs-on: default
130`);
131 expect(result.ok).toBe(false);
132 });
133
134 it("rejects steps without a 'run' command", () => {
135 const result = parseWorkflow(`name: bad
136on: [push]
137jobs:
138 test:
139 steps:
140 - name: no-op
141`);
142 expect(result.ok).toBe(false);
143 });
144
145 it("returns a default name when 'name' is missing", () => {
146 const result = parseWorkflow(`on: [push]
147jobs:
148 test:
149 steps:
150 - run: true
151`);
152 expect(result.ok).toBe(true);
153 if (!result.ok) return;
154 expect(typeof result.workflow.name).toBe("string");
155 expect(result.workflow.name.length).toBeGreaterThan(0);
156 });
157
158 it("never throws on malformed input", () => {
159 const inputs = ["", " ", "not:\nyaml\n-\n:", "{]}", "jobs: oh no"];
160 for (const i of inputs) {
161 expect(() => parseWorkflow(i)).not.toThrow();
162 }
163 });
164});
165
166describe("workflow routes (C1) — unauthed behaviour", () => {
167 it("POST /:owner/:repo/actions/:workflowId/run requires auth", async () => {
168 const res = await app.request("/alice/project/actions/abc/run", {
169 method: "POST",
170 headers: { "content-type": "application/x-www-form-urlencoded" },
171 body: "",
172 });
173 // Either a redirect to /login (repo exists, auth required), or 404
174 // (repo doesn't exist in DB-less tests), or 503 on DB failure.
175 expect([301, 302, 303, 307, 404, 503]).toContain(res.status);
176 });
177
178 it("POST /:owner/:repo/actions/runs/:id/cancel requires auth", async () => {
179 const res = await app.request("/alice/project/actions/runs/xyz/cancel", {
180 method: "POST",
181 headers: { "content-type": "application/x-www-form-urlencoded" },
182 body: "",
183 });
184 expect([301, 302, 303, 307, 404, 503]).toContain(res.status);
185 });
186});