From 0e4a89f56918f5257ce7220c6b2c4d447bf3cd50 Mon Sep 17 00:00:00 2001 From: luvs01 Date: Sat, 8 Aug 2026 19:47:21 +0900 Subject: [PATCH] fix: invert stale contract scan gate --- scripts/openai-provider-option-final-gates.ts | 6 +++++- tests/openai-provider-option-tooling.test.ts | 15 ++++++++++++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/scripts/openai-provider-option-final-gates.ts b/scripts/openai-provider-option-final-gates.ts index 303f9b348c..54d77a40c2 100644 --- a/scripts/openai-provider-option-final-gates.ts +++ b/scripts/openai-provider-option-final-gates.ts @@ -7,6 +7,7 @@ export interface GateSpec { command: string[]; cwd?: string; env?: Record; + successExitCodes?: number[]; } export interface GateResult { @@ -31,7 +32,9 @@ export async function runGateSequence(plan: GateSpec[], deps: GateDeps): Promise const lines = ["schemaVersion=1", "verdict=PASS"]; for (const [index, gate] of plan.entries()) { const result = await deps.run(gate); - if (result.exitCode !== 0) throw new Error(`gate failed: ${gate.name} (${result.exitCode})`); + if (!(gate.successExitCodes ?? [0]).includes(result.exitCode)) { + throw new Error(`gate failed: ${gate.name} (${result.exitCode})`); + } lines.push(summaryLine(index, gate, result)); } const summary = lines.join("\n") + "\n"; @@ -87,6 +90,7 @@ export function finalGatePlan(root: string, evidenceDir: string, unitRoot = dirn "src", "gui/src", "tests", "scripts", "README.md", "readme/README.ko.md", "readme/README.zh-CN.md", "structure", "docs-site/src/content/docs"], cwd: root, + successExitCodes: [1], }, { name: "scoped-diff-check", diff --git a/tests/openai-provider-option-tooling.test.ts b/tests/openai-provider-option-tooling.test.ts index d01cef954d..4566fb9519 100644 --- a/tests/openai-provider-option-tooling.test.ts +++ b/tests/openai-provider-option-tooling.test.ts @@ -3,7 +3,7 @@ import { chmodSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "nod import { tmpdir } from "node:os"; import { join } from "node:path"; import { evidenceDenyFindings, scanEvidence } from "../scripts/openai-provider-option-evidence-scan"; -import { runGateSequence, type GateResult, type GateSpec } from "../scripts/openai-provider-option-final-gates"; +import { finalGatePlan, runGateSequence, type GateResult, type GateSpec } from "../scripts/openai-provider-option-final-gates"; import { evaluateLivePolicy, type LiveOutcome } from "../scripts/openai-hardening-live-policy"; import { buildSanitizedRuntimeEnv } from "../scripts/openai-hardening-runtime-env"; import { buildUnixCodexShim } from "../src/codex/shim"; @@ -159,6 +159,19 @@ describe("OpenAI provider-option evidence scanner", () => { }); describe("OpenAI provider-option final gate runner", () => { + test("passes the stale-contract scan only when ripgrep finds no matches", async () => { + const gate = finalGatePlan("/repo", "/repo/evidence").find(candidate => candidate.name === "stale-contract-scan")!; + const deps = (exitCode: number) => ({ + run: async () => ({ exitCode, output: "" }), + writeSummary: () => {}, + scan: () => [], + }); + + await expect(runGateSequence([gate], deps(0))).rejects.toThrow("gate failed: stale-contract-scan (0)"); + await expect(runGateSequence([gate], deps(1))).resolves.toContain("verdict=PASS"); + await expect(runGateSequence([gate], deps(2))).rejects.toThrow("gate failed: stale-contract-scan (2)"); + }); + test("runs once in order, writes one sanitized summary, then scans", async () => { const order: string[] = []; const writes: string[] = [];