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 | /**
* runGateTestScan availability handling.
*
* A GateTest outage or misconfiguration (non-2xx response, network error)
* must not hard-block merges platform-wide — that makes every merge
* hostage to a third-party service's uptime. Only an actual scan verdict
* (a 200 response with a pass/fail result) should block. This mirrors the
* "not configured" path, which already skips rather than fails.
*
* Regression coverage for the 2026-07-15 incident: gatetest.ai returned
* 503 (missing GLUECRON_EMITTER_SECRET on its side) and every merge on
* every repo failed with "GateTest returned 503: ...".
*/
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
import { runGateTestScan } from "../lib/gate";
import { config } from "../lib/config";
const origFetch = globalThis.fetch;
const origUrl = process.env.GATETEST_URL;
function mockFetch(responder: () => Response | Promise<Response>): void {
// @ts-expect-error — override global fetch for the test
globalThis.fetch = async (): Promise<Response> => responder();
}
beforeEach(() => {
process.env.GATETEST_URL = "https://gatetest.example/api/events/push";
});
afterEach(() => {
globalThis.fetch = origFetch;
if (origUrl === undefined) delete process.env.GATETEST_URL;
else process.env.GATETEST_URL = origUrl;
});
describe("runGateTestScan — availability vs verdict", () => {
it("skips (does not block) when GateTest is not configured", async () => {
delete process.env.GATETEST_URL;
const result = await runGateTestScan("o", "r", "refs/heads/main", "a".repeat(40));
expect(result.skipped).toBe(true);
expect(result.passed).toBe(true);
});
it("skips (does not block) on a non-2xx response — e.g. GateTest itself is down", async () => {
mockFetch(() => new Response(JSON.stringify({ error: "GLUECRON_EMITTER_SECRET is not set" }), { status: 503 }));
const result = await runGateTestScan("o", "r", "refs/heads/main", "a".repeat(40));
expect(result.skipped).toBe(true);
expect(result.passed).toBe(true);
expect(result.details).toContain("503");
});
it("skips (does not block) when the request throws — network error / timeout", async () => {
mockFetch(() => {
throw new Error("ECONNREFUSED");
});
const result = await runGateTestScan("o", "r", "refs/heads/main", "a".repeat(40));
expect(result.skipped).toBe(true);
expect(result.passed).toBe(true);
expect(result.details).toContain("ECONNREFUSED");
});
it("blocks on a genuine 200 scan-fail verdict", async () => {
mockFetch(() => new Response(JSON.stringify({ passed: false, summary: "2 vulnerabilities found" }), { status: 200 }));
const result = await runGateTestScan("o", "r", "refs/heads/main", "a".repeat(40));
expect(result.skipped).toBeFalsy();
expect(result.passed).toBe(false);
expect(result.details).toBe("2 vulnerabilities found");
});
it("passes on a genuine 200 scan-pass verdict", async () => {
mockFetch(() => new Response(JSON.stringify({ passed: true, summary: "All checks passed" }), { status: 200 }));
const result = await runGateTestScan("o", "r", "refs/heads/main", "a".repeat(40));
expect(result.skipped).toBeFalsy();
expect(result.passed).toBe(true);
});
});
// Keep config import referenced — sanity that config.gatetestUrl reflects env for this test file.
describe("config.gatetestUrl reflects GATETEST_URL for this suite", () => {
it("resolves to the mocked value while set", () => {
expect(config.gatetestUrl).toBe("https://gatetest.example/api/events/push");
});
});
|