diff --git a/src/github/app.ts b/src/github/app.ts index acd1708ecf..f512d7bcdd 100644 --- a/src/github/app.ts +++ b/src/github/app.ts @@ -4,7 +4,7 @@ import { makeInstallationOctokit } from "./client"; import { maintainerControlPanelUrl } from "./footer"; import type { AgentActionMode } from "../settings/agent-execution"; import { signRs256Jwt } from "../utils/crypto"; -import { evaluateGateCheck, formatCheckRunOutput, formatGateCheckOutput, type CheckRunAnnotationContext, type CheckRunOutput, type GateCheckConclusion, type GateCheckPolicy } from "../rules/advisory"; +import { evaluateGateCheck, formatCheckRunOutput, formatGateCheckOutput, type CheckRunAnnotationContext, type CheckRunOutput, type GateCheckConclusion, type GateCheckEvaluation, type GateCheckPolicy } from "../rules/advisory"; type CheckRunResponse = { id: number; @@ -159,10 +159,14 @@ export async function createOrUpdateGateCheckRun( repoFullName: string, advisory: Advisory, policy: GateCheckPolicy = {}, - options: { checkRunId?: number | undefined } = {}, + options: { checkRunId?: number | undefined; gate?: GateCheckEvaluation | undefined } = {}, mode: AgentActionMode = "live", ): Promise { - const gate = evaluateGateCheck(advisory, policy); + // Prefer the AUTHORITATIVE pre-computed evaluation when the caller has one (#5 / audit): the surface/content + // lane can OVERRIDE the generic verdict (surface_lane_reject → failure, surface_lane_manual → action_required), + // and re-deriving here via evaluateGateCheck would discard that override — publishing a GREEN check while the + // PR is actually auto-closed/held. Callers without a surface lane omit `gate` and re-derive as before (identical). + const gate = options.gate ?? evaluateGateCheck(advisory, policy); return createOrUpdateNamedCheckRun(env, installationId, repoFullName, advisory, { name: GITTENSORY_GATE_CHECK_NAME, status: "completed", diff --git a/src/queue/processors.ts b/src/queue/processors.ts index 505b0d829d..547e378e7e 100644 --- a/src/queue/processors.ts +++ b/src/queue/processors.ts @@ -2720,6 +2720,10 @@ async function maybePublishPrPublicSurface( gatePolicy, { checkRunId: pendingGateCheckRunId, + // #5 (audit): publish the AUTHORITATIVE surface-lane-merged verdict so the check-run conclusion matches + // the disposition; without this the check re-derives the generic verdict and shows green on a surface- + // lane reject/manual PR that is actually auto-closed/held. Undefined (gate off) ⇒ re-derive (identical). + gate: gateEvaluation, }, mode, ); diff --git a/test/unit/github-app.test.ts b/test/unit/github-app.test.ts index 5b95c2d2d6..95d3fa7dd8 100644 --- a/test/unit/github-app.test.ts +++ b/test/unit/github-app.test.ts @@ -378,6 +378,38 @@ describe("GitHub check runs", () => { }); }); + it("publishes the precomputed authoritative gate (surface-lane override) instead of re-deriving (#5)", async () => { + const privateKey = await generatePrivateKeyPem(); + let capturedBody: { conclusion?: string; output?: { title?: string; text?: string } } = {}; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/commits/")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/check-runs")) { + capturedBody = JSON.parse(String(init?.body)) as typeof capturedBody; + return Response.json({ id: 91 }, { status: 201 }); + } + return new Response("not found", { status: 404 }); + }); + + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: privateKey }); + // The advisory is CLEAN (re-deriving via evaluateGateCheck would publish "success"), but the surface lane + // REJECTED the PR. The published check must reflect the authoritative override, not the generic re-derivation. + const surfaceGate = { + enabled: true, + conclusion: "failure" as const, + title: "Metagraphed surface review", + summary: "Surface payload rejected.", + blockers: [{ code: "surface_lane_reject", title: "Surface rejected", severity: "critical" as const, detail: "Registry payload failed validation." }], + warnings: [], + }; + const result = await createOrUpdateGateCheckRun(env, 123, "JSONbored/gittensory", gateAdvisory("surface-sha"), {}, { gate: surfaceGate }); + + expect(result).toEqual({ kind: "published", id: 91 }); + expect(capturedBody.conclusion).toBe("failure"); // the surface override, NOT the clean re-derivation + expect(capturedBody.output?.title).toBe("Metagraphed surface review"); + }); + it("updates an existing pending Gate check without adding a conclusion", async () => { const privateKey = await generatePrivateKeyPem(); let capturedBody: { status?: string; conclusion?: string } = {};