CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 | /**
* Unit tests for src/lib/workflow-parser-ext.ts (Agent 3, Sprint 1).
*
* FIXME (Agent 3): At the time this test file was authored the
* `workflow-parser-ext.ts` module had not yet landed on disk. The tests
* below are written to the documented contract, but the whole suite guards
* the import via a dynamic `require` so that `bun test` does not fail
* cold-start if Agent 3 is still in flight. Once the module exists, these
* tests will begin running automatically — no edit required.
*
* Contract under test:
* parseExtended(yaml: string):
* | { ok: true; workflow: ExtendedWorkflow }
* | { ok: false; error: string }
*
* Extended workflow adds (vs base ParsedWorkflow):
* - dispatchInputs (from on.workflow_dispatch.inputs)
* - job.needs (string[], normalised from scalar-or-array)
* - job.strategy.matrix.axes
* - step.if (raw expression string)
* - step.uses + step.with
* - warnings[] for malformed extension fields that don't kill the parse
*/
import { describe, it, expect } from "bun:test";
// Guarded dynamic import — if Agent 3's module isn't present yet we skip.
let parseExtended: ((yaml: string) => any) | null = null;
try {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const mod = require("../lib/workflow-parser-ext");
parseExtended = mod.parseExtended ?? null;
} catch {
parseExtended = null;
}
const d = parseExtended ? describe : describe.skip;
d("workflow-parser-ext — parseExtended", () => {
it("parses a bare workflow with no extension fields populated", () => {
const res = parseExtended!(`name: bare
on: [push]
jobs:
build:
runs-on: default
steps:
- run: echo hi
`);
expect(res.ok).toBe(true);
if (!res.ok) return;
// Base fields still present.
expect(res.workflow.name).toBe("bare");
expect(Array.isArray(res.workflow.on)).toBe(true);
expect(res.workflow.jobs.length).toBe(1);
// Extension fields should be absent / empty.
expect(res.workflow.dispatchInputs).toBeFalsy();
const job = res.workflow.jobs[0];
expect(job.needs === undefined || (Array.isArray(job.needs) && job.needs.length === 0)).toBe(true);
expect(job.strategy === undefined || job.strategy === null).toBe(true);
});
it("captures workflow_dispatch inputs with a choice type", () => {
const res = parseExtended!(`name: dispatch
on:
workflow_dispatch:
inputs:
environment:
type: choice
options: [staging, production]
jobs:
deploy:
steps:
- run: echo deploy
`);
expect(res.ok).toBe(true);
if (!res.ok) return;
expect(res.workflow.on).toContain("workflow_dispatch");
expect(res.workflow.dispatchInputs).toBeDefined();
// dispatchInputs is typically keyed by input name.
const inputs = res.workflow.dispatchInputs!;
expect(inputs.environment).toBeDefined();
expect(inputs.environment.type).toBe("choice");
expect(inputs.environment.options).toContain("staging");
expect(inputs.environment.options).toContain("production");
});
it("normalises a scalar `needs:` into a single-element array", () => {
const res = parseExtended!(`name: needs-scalar
on: [push]
jobs:
build:
steps:
- run: echo build
deploy:
needs: build
steps:
- run: echo deploy
`);
expect(res.ok).toBe(true);
if (!res.ok) return;
const deploy = res.workflow.jobs.find((j: any) => j.name === "deploy");
expect(deploy).toBeDefined();
expect(deploy.needs).toEqual(["build"]);
});
it("passes through an array `needs:` unchanged", () => {
const res = parseExtended!(`name: needs-array
on: [push]
jobs:
a:
steps:
- run: echo a
b:
steps:
- run: echo b
deploy:
needs: [a, b]
steps:
- run: echo deploy
`);
expect(res.ok).toBe(true);
if (!res.ok) return;
const deploy = res.workflow.jobs.find((j: any) => j.name === "deploy");
expect(deploy.needs).toEqual(["a", "b"]);
});
it("captures job.strategy.matrix.axes from a matrix block", () => {
const res = parseExtended!(`name: matrix
on: [push]
jobs:
test:
strategy:
matrix:
node: [16, 18]
os: [ubuntu]
steps:
- run: bun test
`);
expect(res.ok).toBe(true);
if (!res.ok) return;
const job = res.workflow.jobs[0];
expect(job.strategy).toBeDefined();
expect(job.strategy.matrix).toBeDefined();
expect(job.strategy.matrix.axes).toBeDefined();
expect(job.strategy.matrix.axes.node).toEqual([16, 18]);
expect(job.strategy.matrix.axes.os).toEqual(["ubuntu"]);
});
it("captures step-level `if:` as the raw expression string", () => {
const res = parseExtended!(`name: iffy
on: [push]
jobs:
test:
steps:
- if: success()
run: echo only-on-success
`);
expect(res.ok).toBe(true);
if (!res.ok) return;
const step = res.workflow.jobs[0].steps[0];
expect(step.if).toBe("success()");
});
it("captures step `uses:` and `with:` blocks verbatim", () => {
const res = parseExtended!(`name: uses
on: [push]
jobs:
scan:
steps:
- uses: gluecron/gatetest@v1
with:
url: 'x'
`);
expect(res.ok).toBe(true);
if (!res.ok) return;
const step = res.workflow.jobs[0].steps[0];
expect(step.uses).toBe("gluecron/gatetest@v1");
expect(step.with).toBeDefined();
expect(step.with.url).toBe("x");
});
it("emits warnings[] when an extension field is malformed but base parse succeeds", () => {
// Matrix whose axes value is a scalar (not an array) — an extension
// error. Base workflow must still parse.
const res = parseExtended!(`name: malformed
on: [push]
jobs:
test:
strategy:
matrix:
node: not-an-array
steps:
- run: echo hi
`);
expect(res.ok).toBe(true);
if (!res.ok) return;
expect(Array.isArray(res.workflow.warnings)).toBe(true);
expect(res.workflow.warnings.length).toBeGreaterThan(0);
});
});
|