From b8a42fd91a8305ca6a142771124240bd185cc061 Mon Sep 17 00:00:00 2001 From: Dotta Date: Mon, 28 Sep 2026 16:45:37 -0500 Subject: [PATCH] test(runner): preserve prepared OpenCode failure diagnostics Retain primary and cleanup errors together without relaxing durable close checks. Include only bounded process facts and cover cleanup-only failures and credential-safe diagnostics. Co-Authored-By: Paperclip --- .../src/live/runnerd-codex-transport.test.ts | 105 +++++++++++++++--- 1 file changed, 90 insertions(+), 15 deletions(-) diff --git a/packages/paperclip-runner/src/live/runnerd-codex-transport.test.ts b/packages/paperclip-runner/src/live/runnerd-codex-transport.test.ts index aba6ede9097..bad027bc29b 100644 --- a/packages/paperclip-runner/src/live/runnerd-codex-transport.test.ts +++ b/packages/paperclip-runner/src/live/runnerd-codex-transport.test.ts @@ -7379,6 +7379,79 @@ it("admits only exact live Pi turn controls", () => { expect(() => parseAcpxTurnControlCapabilities({ steering: true, queuedFollowUp: false }, agent)).toThrow("cannot advertise"); } }); + +async function withPreparedOpenCodeCleanup(input: { + run: () => Promise; + closeSession: () => Promise; + closeTransport: () => Promise; + removeRoot: () => Promise; + evidence: () => Pick< + ReturnType["evidence"]>, + "runnerPid" | "runnerExited" | "runnerExitCode" | "runnerSignal" | "diagnostics" + >; +}): Promise { + const failures: { stage: string; error: unknown }[] = []; + for (const [stage, operation] of [ + ["run", input.run], + ["session.close", input.closeSession], + ["transport.close", input.closeTransport], + ["removeRoot", input.removeRoot], + ] as const) { + try { + await operation(); + } catch (error) { + failures.push({ stage, error }); + } + } + if (failures.length === 0) return; + const evidence = input.evidence(); + // Never dump stderr, environment values, prompts, paths, or arbitrary + // diagnostic text. Keep only bounded process/settlement facts alongside + // the original errors, so a finally failure cannot hide the primary one. + const summary = { + failedStages: failures.map(({ stage }) => stage), + runnerStarted: evidence.runnerPid !== null, + runnerExited: evidence.runnerExited, + runnerExitCode: evidence.runnerExitCode, + runnerSignalled: evidence.runnerSignal !== null, + diagnosticCount: evidence.diagnostics.length, + suspensionRejected: evidence.diagnostics.includes( + "runner did not prove durable suspension before checkpoint", + ), + }; + throw new AggregateError( + failures.map(({ error }) => error), + `Prepared OpenCode fixture failed: ${JSON.stringify(summary)}`, + { cause: failures[0]!.error }, + ); +} + +it.each([true, false])("preserves prepared OpenCode cleanup errors (primary failure: %s)", async (failRun) => { + const primary = new Error("prepared input assertion failed"); + const cleanup = new NativeSessionCloseUnrecoverableError(); + const calls: string[] = []; + const evidence = { + runnerPid: 123, + runnerExited: true, + runnerExitCode: 1, + runnerSignal: null, + diagnostics: ["Bearer fixture-secret", "runner did not prove durable suspension before checkpoint"], + }; + const failure = await withPreparedOpenCodeCleanup({ + run: async () => { calls.push("run"); if (failRun) throw primary; }, + closeSession: async () => { calls.push("session.close"); throw cleanup; }, + closeTransport: async () => { calls.push("transport.close"); }, + removeRoot: async () => { calls.push("removeRoot"); }, + evidence: () => evidence, + }).catch((error: unknown) => error); + expect(failure).toBeInstanceOf(AggregateError); + expect((failure as AggregateError).errors).toEqual(failRun ? [primary, cleanup] : [cleanup]); + expect((failure as AggregateError).cause).toBe(failRun ? primary : cleanup); + expect(calls).toEqual(["run", "session.close", "transport.close", "removeRoot"]); + expect((failure as Error).message).toContain('"suspensionRejected":true'); + expect((failure as Error).message).not.toContain("fixture-secret"); +}); + it("preserves prepared input through runnerd and the real OpenCode proxy boundary", async () => { const root = await mkdtemp(join(tmpdir(), "runnerd-prepared-opencode-")); // The qualified launch boundary unlinks its executable after exec. Use a @@ -7432,19 +7505,21 @@ it("preserves prepared input through runnerd and the real OpenCode proxy boundar task: { prompt: "Keep this request unchanged." }, completionContract: { revision: "prepared-v1", criteria: task.completionContract.criteria }, }); - try { - session = await driver.openSession({ runId: "prepared-opencode", normalizedSessionId: "prepared-opencode", workingDirectory: root }); - await session.startTurn({ message: { role: "user", text: prepared } }); - for await (const event of session.events()) { - if (event.eventType === "turn.completed") break; - } - const sessionRoots = (await readdir(runtime, { withFileTypes: true })).filter((entry) => entry.isDirectory()); - expect(sessionRoots).toHaveLength(1); - const requests = (await readFile(join(runtime, sessionRoots[0]!.name, "data/fake-prompt-requests.ndjson"), "utf8")).trim().split("\n").map((line) => JSON.parse(line)); - expect(requests.map((request) => request.parts)).toEqual([[{ type: "text", text: prepared }]]); - } finally { - await session?.close(); - await bundle.transport.close(); - await rm(root, { recursive: true, force: true }); - } + await withPreparedOpenCodeCleanup({ + run: async () => { + session = await driver.openSession({ runId: "prepared-opencode", normalizedSessionId: "prepared-opencode", workingDirectory: root }); + await session.startTurn({ message: { role: "user", text: prepared } }); + for await (const event of session.events()) { + if (event.eventType === "turn.completed") break; + } + const sessionRoots = (await readdir(runtime, { withFileTypes: true })).filter((entry) => entry.isDirectory()); + expect(sessionRoots).toHaveLength(1); + const requests = (await readFile(join(runtime, sessionRoots[0]!.name, "data/fake-prompt-requests.ndjson"), "utf8")).trim().split("\n").map((line) => JSON.parse(line)); + expect(requests.map((request) => request.parts)).toEqual([[{ type: "text", text: prepared }]]); + }, + closeSession: async () => { await session?.close(); }, + closeTransport: () => bundle.transport.close(), + removeRoot: () => rm(root, { recursive: true, force: true }), + evidence: () => bundle.evidence(), + }); }, 30_000);