From 61d3031007472ad8a9ab3f872f978c3c6a06a540 Mon Sep 17 00:00:00 2001 From: Mikael Hugo Date: Thu, 14 May 2026 18:08:29 +0200 Subject: [PATCH] test(uok): fail-closed contract for triage-apply gate emission MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the missing test case that confirms the fail-closed semantics the parallel worker shipped in slice 3a: when the trace writer cannot persist a UOK gate record (e.g. .sf/traces is unwritable), runTriageApply MUST abort before any subagent runs and surface the emission failure as the run error. This pins down the contract codex Q5 noted as soft: enrichment failures are debug-only, but PRIMARY gate emission for the apply flow is hard-required. Without observable gates, an apply that mutates the ledger has no audit trail — refusing is the right call. Test asserts: trace-dir write failure → ok=false, error contains "UOK gate emission failed for trusted-agent-source-gate", and the mocked agentRunner was never invoked. Suite: 1682/1682. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/tests/headless-triage-uok-gates.test.ts | 30 ++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/src/tests/headless-triage-uok-gates.test.ts b/src/tests/headless-triage-uok-gates.test.ts index 2c352c49f..de4687ae2 100644 --- a/src/tests/headless-triage-uok-gates.test.ts +++ b/src/tests/headless-triage-uok-gates.test.ts @@ -8,7 +8,14 @@ * surface=headless, runControl=supervised, permissionProfile=high, traceId= * the flowId. The outcome reflects the decision (pass/fail/manual-attention). */ -import { mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync } from "node:fs"; +import { + mkdirSync, + mkdtempSync, + readFileSync, + readdirSync, + rmSync, + writeFileSync, +} from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, beforeEach, describe, expect, test } from "vitest"; @@ -179,4 +186,25 @@ describe("runTriageApply emits gate_run trace events with schema-v2 metadata", ( // Review gate must NOT fire because plan validation blocked the flow. expect(reviewEvents).toHaveLength(0); }); + + test("trace_write_failure_fails_closed_before_agents_run", async () => { + const project = makeProject(); + writeFileSync(join(project, ".sf", "traces"), "not a directory"); + let calls = 0; + + const result = await runTriageApply(project, "triage prompt", { + candidateCount: 1, + allowUntrustedRunner: true, + agentRunner: async () => { + calls += 1; + return { ok: true, output: deciderPlan, exitCode: 0 }; + }, + }); + + expect(result.ok).toBe(false); + expect(result.error).toContain( + "UOK gate emission failed for trusted-agent-source-gate", + ); + expect(calls).toBe(0); + }); });