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 | /**
* Block E6/J8 — commit_statuses feed into required-checks enforcement.
*
* `passingCheckNames()` in `src/lib/branch-protection.ts` must treat a
* `commit_statuses` row (external CI signal, see src/lib/commit-statuses.ts)
* with state="success" as a passing check, matched by its `context`. Only
* the LATEST row per (repositoryId, commitSha, context) — by `createdAt` —
* counts, so a stale success followed by a newer failure must NOT count as
* passing, and vice versa.
*
* We stub `../db` (same pattern as mcp-write.test.ts / repo-access.test.ts):
* a fake `db.select()` dispatches on table identity (compared by reference
* against the real schema table objects) so gate_runs/workflow_runs queries
* come back empty and commit_statuses queries return our seeded fixture
* rows, without ever touching Neon.
*/
import { describe, expect, test, mock, afterAll } from "bun:test";
import { commitStatuses, gateRuns, workflowRuns } from "../db/schema";
const _real_db = await import("../db");
const REPO_ID = "22222222-2222-2222-2222-222222222222";
const SHA = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
type FakeStatusRow = {
repositoryId: string;
commitSha: string;
state: string;
context: string;
description: string | null;
targetUrl: string | null;
creatorId: string | null;
createdAt: Date;
updatedAt: Date;
};
let _statusRows: FakeStatusRow[] = [];
function statusRow(overrides: Partial<FakeStatusRow>): FakeStatusRow {
const now = new Date();
return {
repositoryId: REPO_ID,
commitSha: SHA,
state: "success",
context: "ci/external",
description: null,
targetUrl: null,
creatorId: null,
createdAt: now,
updatedAt: now,
...overrides,
};
}
let _lastFrom: any = null;
const _selectChain: any = {
from: (t: any) => {
_lastFrom = t;
return _selectChain;
},
innerJoin: () => _selectChain,
where: () => _selectChain,
orderBy: () => _selectChain,
limit: async () => {
// gate_runs / workflow_runs paths always end in `.limit(200)` — return
// no rows so only the commit_statuses signal under test is exercised.
if (_lastFrom === gateRuns) return [];
if (_lastFrom === workflowRuns) return [];
return [];
},
// commit_statuses (via listStatuses) ends in `.orderBy(...)` with no
// `.limit()` call — the caller awaits the chain directly.
then: (resolve: (v: any) => void) => {
if (_lastFrom === commitStatuses) {
return resolve(
_statusRows.filter(
(r) => r.repositoryId === REPO_ID && r.commitSha === SHA
)
);
}
return resolve([]);
},
};
const _fakeDb = {
db: {
select: () => _selectChain,
},
getDb: () => _fakeDb.db,
};
mock.module("../db", () => ({ ..._real_db, ..._fakeDb }));
afterAll(() => {
_statusRows = [];
mock.module("../db", () => _real_db);
});
// Import AFTER mock.module so branch-protection (and the commit-statuses
// module it now imports) resolve against the stub.
const { passingCheckNames } = await import("../lib/branch-protection");
describe("passingCheckNames — commit_statuses integration", () => {
test("a success status's context is included", async () => {
_statusRows = [statusRow({ context: "ci/success-ctx", state: "success" })];
const names = await passingCheckNames(REPO_ID, SHA);
expect(names).toContain("ci/success-ctx");
});
test("a failure status's context is NOT included", async () => {
_statusRows = [statusRow({ context: "ci/failure-ctx", state: "failure" })];
const names = await passingCheckNames(REPO_ID, SHA);
expect(names).not.toContain("ci/failure-ctx");
});
test("a pending status's context is NOT included", async () => {
_statusRows = [statusRow({ context: "ci/pending-ctx", state: "pending" })];
const names = await passingCheckNames(REPO_ID, SHA);
expect(names).not.toContain("ci/pending-ctx");
});
test("older failure then newer success → context IS included", async () => {
const older = new Date(Date.now() - 60_000);
const newer = new Date();
_statusRows = [
statusRow({
context: "ci/flaky-ctx",
state: "failure",
createdAt: older,
updatedAt: older,
}),
statusRow({
context: "ci/flaky-ctx",
state: "success",
createdAt: newer,
updatedAt: newer,
}),
];
const names = await passingCheckNames(REPO_ID, SHA);
expect(names).toContain("ci/flaky-ctx");
});
test("older success then newer failure → context is NOT included", async () => {
const older = new Date(Date.now() - 60_000);
const newer = new Date();
_statusRows = [
statusRow({
context: "ci/regressed-ctx",
state: "success",
createdAt: older,
updatedAt: older,
}),
statusRow({
context: "ci/regressed-ctx",
state: "failure",
createdAt: newer,
updatedAt: newer,
}),
];
const names = await passingCheckNames(REPO_ID, SHA);
expect(names).not.toContain("ci/regressed-ctx");
});
});
|