Blame · Line-by-line history
pr-workflow-sync.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.
| 91b054e | 1 | /** |
| 2 | * Tests for src/lib/pr-workflow-sync.ts — the fix for `on: pull_request` | |
| 3 | * workflow triggers being silently dead. Before this, `enqueueRun()` was | |
| 4 | * never called with `event: "pull_request"` anywhere in the codebase, so a | |
| 5 | * workflow declaring `on: pull_request` was parsed, stored in the | |
| 6 | * `workflows` table (by push-workflow-sync.ts), and never actually run. | |
| 7 | * | |
| 8 | * Both the workflow lookup and enqueueRun are passed in via the injectable | |
| 9 | * `deps` param (same DI rationale as codeowners.test.ts's | |
| 10 | * requiredOwnersApproved tests) — no mock.module() on ../db or | |
| 11 | * ../lib/workflow-runner, both of which are imported by dozens of unrelated | |
| 12 | * test files and would leak a global mock across the whole `bun test` run. | |
| 13 | */ | |
| 14 | ||
| 15 | import { beforeEach, describe, expect, it } from "bun:test"; | |
| 16 | import { enqueuePullRequestWorkflows } from "../lib/pr-workflow-sync"; | |
| 17 | ||
| 18 | let _workflowRows: Array<{ id: string; onEvents: string }> = []; | |
| 19 | let _selectShouldThrow = false; | |
| 20 | ||
| 21 | let _enqueueCalls: any[] = []; | |
| 22 | let _enqueueShouldThrow = false; | |
| 23 | ||
| 24 | const fakeDeps = { | |
| 25 | loadWorkflows: async (_repositoryId: string) => { | |
| 26 | if (_selectShouldThrow) throw new Error("db boom"); | |
| 27 | return _workflowRows; | |
| 28 | }, | |
| 29 | enqueueRun: async (opts: any) => { | |
| 30 | if (_enqueueShouldThrow) throw new Error("enqueue boom"); | |
| 31 | _enqueueCalls.push(opts); | |
| 32 | return "run-id-1"; | |
| 33 | }, | |
| 34 | } as any; | |
| 35 | ||
| 36 | beforeEach(() => { | |
| 37 | _workflowRows = []; | |
| 38 | _selectShouldThrow = false; | |
| 39 | _enqueueCalls = []; | |
| 40 | _enqueueShouldThrow = false; | |
| 41 | }); | |
| 42 | ||
| 43 | function baseOpts(overrides: Partial<Parameters<typeof enqueuePullRequestWorkflows>[0]> = {}) { | |
| 44 | return { | |
| 45 | repositoryId: "repo-1", | |
| 46 | headBranch: "feature/foo", | |
| 47 | headSha: "b".repeat(40), | |
| 48 | triggeredBy: "user-1", | |
| 49 | ...overrides, | |
| 50 | }; | |
| 51 | } | |
| 52 | ||
| 53 | describe("enqueuePullRequestWorkflows", () => { | |
| 54 | it("returns zero enqueued when the repo has no workflows", async () => { | |
| 55 | _workflowRows = []; | |
| 56 | const result = await enqueuePullRequestWorkflows(baseOpts(), fakeDeps); | |
| 57 | expect(result).toEqual({ enqueued: 0, errors: [] }); | |
| 58 | expect(_enqueueCalls).toHaveLength(0); | |
| 59 | }); | |
| 60 | ||
| 61 | it("enqueues a workflow whose on: includes pull_request", async () => { | |
| 62 | _workflowRows = [{ id: "wf-1", onEvents: JSON.stringify(["pull_request"]) }]; | |
| 63 | const result = await enqueuePullRequestWorkflows(baseOpts(), fakeDeps); | |
| 64 | ||
| 65 | expect(result.enqueued).toBe(1); | |
| 66 | expect(result.errors).toEqual([]); | |
| 67 | expect(_enqueueCalls[0]).toMatchObject({ | |
| 68 | workflowId: "wf-1", | |
| 69 | repositoryId: "repo-1", | |
| 70 | event: "pull_request", | |
| 71 | ref: "feature/foo", | |
| 72 | commitSha: "b".repeat(40), | |
| 73 | triggeredBy: "user-1", | |
| 74 | }); | |
| 75 | }); | |
| 76 | ||
| 77 | it("does NOT enqueue a workflow with only an on:push trigger", async () => { | |
| 78 | _workflowRows = [{ id: "wf-2", onEvents: JSON.stringify(["push"]) }]; | |
| 79 | const result = await enqueuePullRequestWorkflows(baseOpts(), fakeDeps); | |
| 80 | ||
| 81 | expect(result.enqueued).toBe(0); | |
| 82 | expect(_enqueueCalls).toHaveLength(0); | |
| 83 | }); | |
| 84 | ||
| 85 | it("enqueues only the matching workflow out of several", async () => { | |
| 86 | _workflowRows = [ | |
| 87 | { id: "wf-push", onEvents: JSON.stringify(["push"]) }, | |
| 88 | { id: "wf-pr", onEvents: JSON.stringify(["push", "pull_request"]) }, | |
| 89 | { id: "wf-dispatch", onEvents: JSON.stringify(["workflow_dispatch"]) }, | |
| 90 | ]; | |
| 91 | const result = await enqueuePullRequestWorkflows(baseOpts(), fakeDeps); | |
| 92 | ||
| 93 | expect(result.enqueued).toBe(1); | |
| 94 | expect(_enqueueCalls[0].workflowId).toBe("wf-pr"); | |
| 95 | }); | |
| 96 | ||
| 97 | it("records a per-row error but keeps going when onEvents is malformed JSON", async () => { | |
| 98 | _workflowRows = [ | |
| 99 | { id: "wf-broken", onEvents: "not json" }, | |
| 100 | { id: "wf-good", onEvents: JSON.stringify(["pull_request"]) }, | |
| 101 | ]; | |
| 102 | const result = await enqueuePullRequestWorkflows(baseOpts(), fakeDeps); | |
| 103 | ||
| 104 | expect(result.enqueued).toBe(1); | |
| 105 | expect(_enqueueCalls[0].workflowId).toBe("wf-good"); | |
| 106 | expect(result.errors.some((e) => e.includes("wf-broken"))).toBe(true); | |
| 107 | }); | |
| 108 | ||
| 109 | it("records an error but does not throw when enqueueRun fails", async () => { | |
| 110 | _workflowRows = [{ id: "wf-1", onEvents: JSON.stringify(["pull_request"]) }]; | |
| 111 | _enqueueShouldThrow = true; | |
| 112 | ||
| 113 | const result = await enqueuePullRequestWorkflows(baseOpts(), fakeDeps); | |
| 114 | ||
| 115 | expect(result.enqueued).toBe(0); | |
| 116 | expect(result.errors.length).toBe(1); | |
| 117 | expect(result.errors[0]).toContain("enqueue"); | |
| 118 | }); | |
| 119 | ||
| 120 | it("returns a query error but does not throw when the DB select fails", async () => { | |
| 121 | _selectShouldThrow = true; | |
| 122 | const result = await enqueuePullRequestWorkflows(baseOpts(), fakeDeps); | |
| 123 | ||
| 124 | expect(result.enqueued).toBe(0); | |
| 125 | expect(result.errors.length).toBe(1); | |
| 126 | expect(result.errors[0]).toContain("query"); | |
| 127 | }); | |
| 128 | }); |