CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
Blame · Line-by-line history
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.
| a79a9ed | 1 | /** |
| 2 | * Block E6 — Required status checks matrix tests. | |
| 3 | * | |
| 4 | * Covers the pure protection-evaluator path (no DB) + route auth smoke. | |
| 5 | */ | |
| 6 | ||
| 7 | import { describe, it, expect } from "bun:test"; | |
| 8 | import app from "../app"; | |
| 9 | import { evaluateProtection } from "../lib/branch-protection"; | |
| 10 | import type { BranchProtection } from "../db/schema"; | |
| 11 | ||
| 12 | function rule(overrides: Partial<BranchProtection> = {}): BranchProtection { | |
| 13 | return { | |
| 14 | id: "rule-1", | |
| 15 | repositoryId: "repo-1", | |
| 16 | pattern: "main", | |
| 17 | requirePullRequest: true, | |
| 18 | requireGreenGates: true, | |
| 19 | requireAiApproval: false, | |
| 20 | requireHumanReview: false, | |
| 21 | requiredApprovals: 0, | |
| 22 | allowForcePush: false, | |
| 23 | allowDeletion: false, | |
| 24 | createdAt: new Date(), | |
| 25 | updatedAt: new Date(), | |
| 26 | ...overrides, | |
| 27 | } as BranchProtection; | |
| 28 | } | |
| 29 | ||
| 30 | describe("evaluateProtection — required checks matrix", () => { | |
| 31 | const green = { | |
| 32 | aiApproved: true, | |
| 33 | humanApprovalCount: 1, | |
| 34 | gateResultGreen: true, | |
| 35 | hasFailedGates: false, | |
| 36 | }; | |
| 37 | ||
| 38 | it("allows when no required checks are configured", () => { | |
| 39 | const d = evaluateProtection(rule(), green, []); | |
| 40 | expect(d.allowed).toBe(true); | |
| 41 | expect(d.reasons.length).toBe(0); | |
| 42 | expect(d.missingChecks).toBeUndefined(); | |
| 43 | }); | |
| 44 | ||
| 45 | it("allows when all required checks are in passingCheckNames", () => { | |
| 46 | const d = evaluateProtection( | |
| 47 | rule(), | |
| 48 | { ...green, passingCheckNames: ["GateTest", "AI Review", "CI"] }, | |
| 49 | ["GateTest", "AI Review"] | |
| 50 | ); | |
| 51 | expect(d.allowed).toBe(true); | |
| 52 | expect(d.missingChecks).toBeUndefined(); | |
| 53 | }); | |
| 54 | ||
| 55 | it("blocks when a required check is missing", () => { | |
| 56 | const d = evaluateProtection( | |
| 57 | rule(), | |
| 58 | { ...green, passingCheckNames: ["GateTest"] }, | |
| 59 | ["GateTest", "AI Review"] | |
| 60 | ); | |
| 61 | expect(d.allowed).toBe(false); | |
| 62 | expect(d.missingChecks).toEqual(["AI Review"]); | |
| 63 | expect(d.reasons.join(" ")).toContain("AI Review"); | |
| 64 | }); | |
| 65 | ||
| 66 | it("blocks when passingCheckNames is empty but checks are required", () => { | |
| 67 | const d = evaluateProtection( | |
| 68 | rule(), | |
| 69 | { ...green, passingCheckNames: [] }, | |
| 70 | ["GateTest"] | |
| 71 | ); | |
| 72 | expect(d.allowed).toBe(false); | |
| 73 | expect(d.missingChecks).toEqual(["GateTest"]); | |
| 74 | }); | |
| 75 | ||
| 76 | it("still reports other failures alongside missing checks", () => { | |
| 77 | const d = evaluateProtection( | |
| 78 | rule({ requireAiApproval: true, requiredApprovals: 2 }), | |
| 79 | { | |
| 80 | aiApproved: false, | |
| 81 | humanApprovalCount: 0, | |
| 82 | gateResultGreen: true, | |
| 83 | hasFailedGates: false, | |
| 84 | passingCheckNames: [], | |
| 85 | }, | |
| 86 | ["CI"] | |
| 87 | ); | |
| 88 | expect(d.allowed).toBe(false); | |
| 89 | // 3 reasons: AI approval, required approvals, missing check | |
| 90 | expect(d.reasons.length).toBeGreaterThanOrEqual(3); | |
| 91 | expect(d.missingChecks).toEqual(["CI"]); | |
| 92 | }); | |
| 93 | }); | |
| 94 | ||
| 95 | describe("required-checks — route smoke", () => { | |
| 96 | it("GET protection/:id/checks without auth → 302 /login", async () => { | |
| 97 | const res = await app.request( | |
| 98 | "/any/repo/gates/protection/abc/checks" | |
| 99 | ); | |
| 100 | expect(res.status).toBe(302); | |
| 101 | const loc = res.headers.get("location") || ""; | |
| 102 | expect(loc.startsWith("/login")).toBe(true); | |
| 103 | }); | |
| 104 | ||
| 105 | it("POST protection/:id/checks without auth → 302 /login", async () => { | |
| 106 | const res = await app.request( | |
| 107 | "/any/repo/gates/protection/abc/checks", | |
| 108 | { | |
| 109 | method: "POST", | |
| 110 | body: new URLSearchParams({ checkName: "CI" }), | |
| 111 | headers: { "content-type": "application/x-www-form-urlencoded" }, | |
| 112 | } | |
| 113 | ); | |
| 114 | expect(res.status).toBe(302); | |
| 115 | const loc = res.headers.get("location") || ""; | |
| 116 | expect(loc.startsWith("/login")).toBe(true); | |
| 117 | }); | |
| 118 | ||
| 119 | it("POST protection/:id/checks/:cid/delete without auth → 302 /login", async () => { | |
| 120 | const res = await app.request( | |
| 121 | "/any/repo/gates/protection/abc/checks/xyz/delete", | |
| 122 | { method: "POST" } | |
| 123 | ); | |
| 124 | expect(res.status).toBe(302); | |
| 125 | const loc = res.headers.get("location") || ""; | |
| 126 | expect(loc.startsWith("/login")).toBe(true); | |
| 127 | }); | |
| 128 | }); |