Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
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.

commit-status-required-checks.test.tsBlame165 lines · 1 contributor
91b054eccanty labs1/**
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
18import { describe, expect, test, mock, afterAll } from "bun:test";
19import { commitStatuses, gateRuns, workflowRuns } from "../db/schema";
20
21const _real_db = await import("../db");
22
23const REPO_ID = "22222222-2222-2222-2222-222222222222";
24const SHA = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
25
26type 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
38let _statusRows: FakeStatusRow[] = [];
39
40function 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
56let _lastFrom: any = null;
57
58const _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
87const _fakeDb = {
88 db: {
89 select: () => _selectChain,
90 },
91 getDb: () => _fakeDb.db,
92};
93
94mock.module("../db", () => ({ ..._real_db, ..._fakeDb }));
95
96afterAll(() => {
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.
103const { passingCheckNames } = await import("../lib/branch-protection");
104
105describe("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});