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