CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
Blame · Line-by-line history
commit-status-required-checks.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 | * Block E6/J8 — commit_statuses feed into required-checks enforcement. | |
| 3 | * | |
| 4 | * `passingCheckNames()` in `src/lib/branch-protection.ts` must treat a | |
| 5 | * `commit_statuses` row (external CI signal, see src/lib/commit-statuses.ts) | |
| 6 | * with state="success" as a passing check, matched by its `context`. Only | |
| 7 | * the LATEST row per (repositoryId, commitSha, context) — by `createdAt` — | |
| 8 | * counts, so a stale success followed by a newer failure must NOT count as | |
| 9 | * passing, and vice versa. | |
| 10 | * | |
| 11 | * We stub `../db` (same pattern as mcp-write.test.ts / repo-access.test.ts): | |
| 12 | * a fake `db.select()` dispatches on table identity (compared by reference | |
| 13 | * against the real schema table objects) so gate_runs/workflow_runs queries | |
| 14 | * come back empty and commit_statuses queries return our seeded fixture | |
| 15 | * rows, without ever touching Neon. | |
| 16 | */ | |
| 17 | ||
| 18 | import { describe, expect, test, mock, afterAll } from "bun:test"; | |
| 19 | import { commitStatuses, gateRuns, workflowRuns } from "../db/schema"; | |
| 20 | ||
| 21 | const _real_db = await import("../db"); | |
| 22 | ||
| 23 | const REPO_ID = "22222222-2222-2222-2222-222222222222"; | |
| 24 | const SHA = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"; | |
| 25 | ||
| 26 | type FakeStatusRow = { | |
| 27 | repositoryId: string; | |
| 28 | commitSha: string; | |
| 29 | state: string; | |
| 30 | context: string; | |
| 31 | description: string | null; | |
| 32 | targetUrl: string | null; | |
| 33 | creatorId: string | null; | |
| 34 | createdAt: Date; | |
| 35 | updatedAt: Date; | |
| 36 | }; | |
| 37 | ||
| 38 | let _statusRows: FakeStatusRow[] = []; | |
| 39 | ||
| 40 | function statusRow(overrides: Partial<FakeStatusRow>): FakeStatusRow { | |
| 41 | const now = new Date(); | |
| 42 | return { | |
| 43 | repositoryId: REPO_ID, | |
| 44 | commitSha: SHA, | |
| 45 | state: "success", | |
| 46 | context: "ci/external", | |
| 47 | description: null, | |
| 48 | targetUrl: null, | |
| 49 | creatorId: null, | |
| 50 | createdAt: now, | |
| 51 | updatedAt: now, | |
| 52 | ...overrides, | |
| 53 | }; | |
| 54 | } | |
| 55 | ||
| 56 | let _lastFrom: any = null; | |
| 57 | ||
| 58 | const _selectChain: any = { | |
| 59 | from: (t: any) => { | |
| 60 | _lastFrom = t; | |
| 61 | return _selectChain; | |
| 62 | }, | |
| 63 | innerJoin: () => _selectChain, | |
| 64 | where: () => _selectChain, | |
| 65 | orderBy: () => _selectChain, | |
| 66 | limit: async () => { | |
| 67 | // gate_runs / workflow_runs paths always end in `.limit(200)` — return | |
| 68 | // no rows so only the commit_statuses signal under test is exercised. | |
| 69 | if (_lastFrom === gateRuns) return []; | |
| 70 | if (_lastFrom === workflowRuns) return []; | |
| 71 | return []; | |
| 72 | }, | |
| 73 | // commit_statuses (via listStatuses) ends in `.orderBy(...)` with no | |
| 74 | // `.limit()` call — the caller awaits the chain directly. | |
| 75 | then: (resolve: (v: any) => void) => { | |
| 76 | if (_lastFrom === commitStatuses) { | |
| 77 | return resolve( | |
| 78 | _statusRows.filter( | |
| 79 | (r) => r.repositoryId === REPO_ID && r.commitSha === SHA | |
| 80 | ) | |
| 81 | ); | |
| 82 | } | |
| 83 | return resolve([]); | |
| 84 | }, | |
| 85 | }; | |
| 86 | ||
| 87 | const _fakeDb = { | |
| 88 | db: { | |
| 89 | select: () => _selectChain, | |
| 90 | }, | |
| 91 | getDb: () => _fakeDb.db, | |
| 92 | }; | |
| 93 | ||
| 94 | mock.module("../db", () => ({ ..._real_db, ..._fakeDb })); | |
| 95 | ||
| 96 | afterAll(() => { | |
| 97 | _statusRows = []; | |
| 98 | mock.module("../db", () => _real_db); | |
| 99 | }); | |
| 100 | ||
| 101 | // Import AFTER mock.module so branch-protection (and the commit-statuses | |
| 102 | // module it now imports) resolve against the stub. | |
| 103 | const { passingCheckNames } = await import("../lib/branch-protection"); | |
| 104 | ||
| 105 | describe("passingCheckNames — commit_statuses integration", () => { | |
| 106 | test("a success status's context is included", async () => { | |
| 107 | _statusRows = [statusRow({ context: "ci/success-ctx", state: "success" })]; | |
| 108 | const names = await passingCheckNames(REPO_ID, SHA); | |
| 109 | expect(names).toContain("ci/success-ctx"); | |
| 110 | }); | |
| 111 | ||
| 112 | test("a failure status's context is NOT included", async () => { | |
| 113 | _statusRows = [statusRow({ context: "ci/failure-ctx", state: "failure" })]; | |
| 114 | const names = await passingCheckNames(REPO_ID, SHA); | |
| 115 | expect(names).not.toContain("ci/failure-ctx"); | |
| 116 | }); | |
| 117 | ||
| 118 | test("a pending status's context is NOT included", async () => { | |
| 119 | _statusRows = [statusRow({ context: "ci/pending-ctx", state: "pending" })]; | |
| 120 | const names = await passingCheckNames(REPO_ID, SHA); | |
| 121 | expect(names).not.toContain("ci/pending-ctx"); | |
| 122 | }); | |
| 123 | ||
| 124 | test("older failure then newer success → context IS included", async () => { | |
| 125 | const older = new Date(Date.now() - 60_000); | |
| 126 | const newer = new Date(); | |
| 127 | _statusRows = [ | |
| 128 | statusRow({ | |
| 129 | context: "ci/flaky-ctx", | |
| 130 | state: "failure", | |
| 131 | createdAt: older, | |
| 132 | updatedAt: older, | |
| 133 | }), | |
| 134 | statusRow({ | |
| 135 | context: "ci/flaky-ctx", | |
| 136 | state: "success", | |
| 137 | createdAt: newer, | |
| 138 | updatedAt: newer, | |
| 139 | }), | |
| 140 | ]; | |
| 141 | const names = await passingCheckNames(REPO_ID, SHA); | |
| 142 | expect(names).toContain("ci/flaky-ctx"); | |
| 143 | }); | |
| 144 | ||
| 145 | test("older success then newer failure → context is NOT included", async () => { | |
| 146 | const older = new Date(Date.now() - 60_000); | |
| 147 | const newer = new Date(); | |
| 148 | _statusRows = [ | |
| 149 | statusRow({ | |
| 150 | context: "ci/regressed-ctx", | |
| 151 | state: "success", | |
| 152 | createdAt: older, | |
| 153 | updatedAt: older, | |
| 154 | }), | |
| 155 | statusRow({ | |
| 156 | context: "ci/regressed-ctx", | |
| 157 | state: "failure", | |
| 158 | createdAt: newer, | |
| 159 | updatedAt: newer, | |
| 160 | }), | |
| 161 | ]; | |
| 162 | const names = await passingCheckNames(REPO_ID, SHA); | |
| 163 | expect(names).not.toContain("ci/regressed-ctx"); | |
| 164 | }); | |
| 165 | }); |