CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
Blame · Line-by-line history
gate-gatetest-availability.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.
| 6930df0 | 1 | /** |
| 2 | * runGateTestScan availability handling. | |
| 3 | * | |
| 4 | * A GateTest outage or misconfiguration (non-2xx response, network error) | |
| 5 | * must not hard-block merges platform-wide — that makes every merge | |
| 6 | * hostage to a third-party service's uptime. Only an actual scan verdict | |
| 7 | * (a 200 response with a pass/fail result) should block. This mirrors the | |
| 8 | * "not configured" path, which already skips rather than fails. | |
| 9 | * | |
| 10 | * Regression coverage for the 2026-07-15 incident: gatetest.ai returned | |
| 11 | * 503 (missing GLUECRON_EMITTER_SECRET on its side) and every merge on | |
| 12 | * every repo failed with "GateTest returned 503: ...". | |
| 13 | */ | |
| 14 | ||
| 15 | import { afterEach, beforeEach, describe, expect, it } from "bun:test"; | |
| 16 | import { runGateTestScan } from "../lib/gate"; | |
| 17 | import { config } from "../lib/config"; | |
| 18 | ||
| 19 | const origFetch = globalThis.fetch; | |
| 20 | const origUrl = process.env.GATETEST_URL; | |
| 21 | ||
| 22 | function mockFetch(responder: () => Response | Promise<Response>): void { | |
| 23 | // @ts-expect-error — override global fetch for the test | |
| 24 | globalThis.fetch = async (): Promise<Response> => responder(); | |
| 25 | } | |
| 26 | ||
| 27 | beforeEach(() => { | |
| 28 | process.env.GATETEST_URL = "https://gatetest.example/api/events/push"; | |
| 29 | }); | |
| 30 | ||
| 31 | afterEach(() => { | |
| 32 | globalThis.fetch = origFetch; | |
| 33 | if (origUrl === undefined) delete process.env.GATETEST_URL; | |
| 34 | else process.env.GATETEST_URL = origUrl; | |
| 35 | }); | |
| 36 | ||
| 37 | describe("runGateTestScan — availability vs verdict", () => { | |
| 38 | it("skips (does not block) when GateTest is not configured", async () => { | |
| 39 | delete process.env.GATETEST_URL; | |
| 40 | const result = await runGateTestScan("o", "r", "refs/heads/main", "a".repeat(40)); | |
| 41 | expect(result.skipped).toBe(true); | |
| 42 | expect(result.passed).toBe(true); | |
| 43 | }); | |
| 44 | ||
| 45 | it("skips (does not block) on a non-2xx response — e.g. GateTest itself is down", async () => { | |
| 46 | mockFetch(() => new Response(JSON.stringify({ error: "GLUECRON_EMITTER_SECRET is not set" }), { status: 503 })); | |
| 47 | const result = await runGateTestScan("o", "r", "refs/heads/main", "a".repeat(40)); | |
| 48 | expect(result.skipped).toBe(true); | |
| 49 | expect(result.passed).toBe(true); | |
| 50 | expect(result.details).toContain("503"); | |
| 51 | }); | |
| 52 | ||
| 53 | it("skips (does not block) when the request throws — network error / timeout", async () => { | |
| 54 | mockFetch(() => { | |
| 55 | throw new Error("ECONNREFUSED"); | |
| 56 | }); | |
| 57 | const result = await runGateTestScan("o", "r", "refs/heads/main", "a".repeat(40)); | |
| 58 | expect(result.skipped).toBe(true); | |
| 59 | expect(result.passed).toBe(true); | |
| 60 | expect(result.details).toContain("ECONNREFUSED"); | |
| 61 | }); | |
| 62 | ||
| 63 | it("blocks on a genuine 200 scan-fail verdict", async () => { | |
| 64 | mockFetch(() => new Response(JSON.stringify({ passed: false, summary: "2 vulnerabilities found" }), { status: 200 })); | |
| 65 | const result = await runGateTestScan("o", "r", "refs/heads/main", "a".repeat(40)); | |
| 66 | expect(result.skipped).toBeFalsy(); | |
| 67 | expect(result.passed).toBe(false); | |
| 68 | expect(result.details).toBe("2 vulnerabilities found"); | |
| 69 | }); | |
| 70 | ||
| 71 | it("passes on a genuine 200 scan-pass verdict", async () => { | |
| 72 | mockFetch(() => new Response(JSON.stringify({ passed: true, summary: "All checks passed" }), { status: 200 })); | |
| 73 | const result = await runGateTestScan("o", "r", "refs/heads/main", "a".repeat(40)); | |
| 74 | expect(result.skipped).toBeFalsy(); | |
| 75 | expect(result.passed).toBe(true); | |
| 76 | }); | |
| 77 | }); | |
| 78 | ||
| 79 | // Keep config import referenced — sanity that config.gatetestUrl reflects env for this test file. | |
| 80 | describe("config.gatetestUrl reflects GATETEST_URL for this suite", () => { | |
| 81 | it("resolves to the mocked value while set", () => { | |
| 82 | expect(config.gatetestUrl).toBe("https://gatetest.example/api/events/push"); | |
| 83 | }); | |
| 84 | }); |