CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
Blame · Line-by-line history
workflow-conditionals.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-conditionals.ts (Agent 4, Sprint 1). | |
| 3 | * | |
| 4 | * Pure evaluator — no external state, no eval(), no DB. We exercise the | |
| 5 | * grammar corners: precedence, literals, context lookups, the small set of | |
| 6 | * built-in helper functions (success/failure/always/contains/startsWith), | |
| 7 | * and the `${{ ... }}` wrapper strip. | |
| 8 | */ | |
| 9 | ||
| 10 | import { describe, it, expect } from "bun:test"; | |
| 11 | import { evaluateIf } from "../lib/workflow-conditionals"; | |
| 12 | ||
| 13 | describe("workflow-conditionals — evaluateIf", () => { | |
| 14 | it("undefined / null / empty expression evaluates to true", () => { | |
| 15 | expect(evaluateIf(undefined, {})).toEqual({ ok: true, value: true }); | |
| 16 | expect(evaluateIf(null, {})).toEqual({ ok: true, value: true }); | |
| 17 | expect(evaluateIf("", {})).toEqual({ ok: true, value: true }); | |
| 18 | expect(evaluateIf(" ", {})).toEqual({ ok: true, value: true }); | |
| 19 | }); | |
| 20 | ||
| 21 | it("literal 'true' and 'false' evaluate correctly", () => { | |
| 22 | expect(evaluateIf("true", {})).toEqual({ ok: true, value: true }); | |
| 23 | expect(evaluateIf("false", {})).toEqual({ ok: true, value: false }); | |
| 24 | }); | |
| 25 | ||
| 26 | it("context lookup with env.FOO == 'bar' returns true when matched", () => { | |
| 27 | const r = evaluateIf("env.FOO == 'bar'", { env: { FOO: "bar" } }); | |
| 28 | expect(r).toEqual({ ok: true, value: true }); | |
| 29 | }); | |
| 30 | ||
| 31 | it("missing context key resolves to null and is falsy", () => { | |
| 32 | const r = evaluateIf("env.DOES_NOT_EXIST", { env: {} }); | |
| 33 | expect(r).toEqual({ ok: true, value: false }); | |
| 34 | }); | |
| 35 | ||
| 36 | it("negation `!success()` is true when job status is 'failure'", () => { | |
| 37 | const r = evaluateIf("!success()", { job: { status: "failure" } }); | |
| 38 | expect(r).toEqual({ ok: true, value: true }); | |
| 39 | }); | |
| 40 | ||
| 41 | it("&& binds tighter than || (precedence check)", () => { | |
| 42 | // true && false || true -> (true && false) || true -> true | |
| 43 | const r1 = evaluateIf("true && false || true", {}); | |
| 44 | expect(r1).toEqual({ ok: true, value: true }); | |
| 45 | // false || true && false -> false || (true && false) -> false | |
| 46 | const r2 = evaluateIf("false || true && false", {}); | |
| 47 | expect(r2).toEqual({ ok: true, value: false }); | |
| 48 | // Mixed with equality: env.a == 'x' && env.b == 'y' || env.c == 'z' | |
| 49 | const r3 = evaluateIf( | |
| 50 | "env.a == 'x' && env.b == 'y' || env.c == 'z'", | |
| 51 | { env: { a: "x", b: "y", c: "no" } } | |
| 52 | ); | |
| 53 | expect(r3).toEqual({ ok: true, value: true }); | |
| 54 | }); | |
| 55 | ||
| 56 | it("contains('abcdef', 'cd') returns true", () => { | |
| 57 | const r = evaluateIf("contains('abcdef', 'cd')", {}); | |
| 58 | expect(r).toEqual({ ok: true, value: true }); | |
| 59 | const miss = evaluateIf("contains('abcdef', 'zz')", {}); | |
| 60 | expect(miss).toEqual({ ok: true, value: false }); | |
| 61 | }); | |
| 62 | ||
| 63 | it("startsWith('refs/heads/main', 'refs/heads/') returns true", () => { | |
| 64 | const r = evaluateIf("startsWith('refs/heads/main', 'refs/heads/')", {}); | |
| 65 | expect(r).toEqual({ ok: true, value: true }); | |
| 66 | }); | |
| 67 | ||
| 68 | it("always() returns true even when job.status == 'failure'", () => { | |
| 69 | const r = evaluateIf("always()", { job: { status: "failure" } }); | |
| 70 | expect(r).toEqual({ ok: true, value: true }); | |
| 71 | }); | |
| 72 | ||
| 73 | it("success() is true when job.status is unset (default running/ok)", () => { | |
| 74 | const r = evaluateIf("success()", {}); | |
| 75 | expect(r).toEqual({ ok: true, value: true }); | |
| 76 | }); | |
| 77 | ||
| 78 | it("failure() is true only when job.status == 'failure'", () => { | |
| 79 | expect(evaluateIf("failure()", { job: { status: "failure" } })).toEqual({ | |
| 80 | ok: true, | |
| 81 | value: true, | |
| 82 | }); | |
| 83 | expect(evaluateIf("failure()", { job: { status: "success" } })).toEqual({ | |
| 84 | ok: true, | |
| 85 | value: false, | |
| 86 | }); | |
| 87 | expect(evaluateIf("failure()", {})).toEqual({ ok: true, value: false }); | |
| 88 | }); | |
| 89 | ||
| 90 | it("strips a surrounding ${{ ... }} wrapper before evaluating", () => { | |
| 91 | const r = evaluateIf("${{ env.FOO == 'bar' }}", { env: { FOO: "bar" } }); | |
| 92 | expect(r).toEqual({ ok: true, value: true }); | |
| 93 | }); | |
| 94 | ||
| 95 | it("malformed expressions return {ok:false, error}", () => { | |
| 96 | const r = evaluateIf("== == ==", {}); | |
| 97 | expect(r.ok).toBe(false); | |
| 98 | if (!r.ok) expect(r.error.length).toBeGreaterThan(0); | |
| 99 | }); | |
| 100 | }); |