From 7aaff2e4e4dd5312154dd5e738b98d597a4638e3 Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Wed, 11 Nov 2026 13:22:30 +0800 Subject: [PATCH] fix(cli): retain Evidence receipts on format refusal (#9943) --- src/cli/cliResultOutput.ts | 44 ++++++++++--------- tests/boundary/cli/formattingRecovery.test.ts | 35 +++++++++++++++ 2 files changed, 59 insertions(+), 20 deletions(-) diff --git a/src/cli/cliResultOutput.ts b/src/cli/cliResultOutput.ts index 4ea5c5fe87..9109c66d9e 100644 --- a/src/cli/cliResultOutput.ts +++ b/src/cli/cliResultOutput.ts @@ -556,28 +556,32 @@ export const createCliResultOutput = ( const selected = selection.value; const refuseUnrepresentableText = async (): Promise => { error = projectAnalysisError( - new AnalysisInputError( - metadata.command, - { - userMessage: - format === "md" - ? "Markdown cannot preserve NUL or unpaired UTF-16 surrogates in rendered output." - : "TOON cannot preserve unpaired UTF-16 surrogates in UTF-8 output.", - remediationAction: - "Use --format json or --format yaml to retain the exact code units.", - ...(completed?.originalFailure === undefined - ? {} - : { - diagnostics: { original_failure: completed.originalFailure }, - }), - }, - [ + retainCliPublicationOnError( + new AnalysisInputError( + metadata.command, { - path: ["--format"], - reason: "invalid_value", - message: "The selected format cannot preserve this value.", + userMessage: + format === "md" + ? "Markdown cannot preserve NUL or unpaired UTF-16 surrogates in rendered output." + : "TOON cannot preserve unpaired UTF-16 surrogates in UTF-8 output.", + remediationAction: + "Use --format json or --format yaml to retain the exact code units.", + ...(completed?.originalFailure === undefined + ? {} + : { + diagnostics: { + original_failure: completed.originalFailure, + }, + }), }, - ], + [ + { + path: ["--format"], + reason: "invalid_value", + message: "The selected format cannot preserve this value.", + }, + ], + ), ), ); const document = options.fullOutput diff --git a/tests/boundary/cli/formattingRecovery.test.ts b/tests/boundary/cli/formattingRecovery.test.ts index 8b8c4a81c5..7b49433b1b 100644 --- a/tests/boundary/cli/formattingRecovery.test.ts +++ b/tests/boundary/cli/formattingRecovery.test.ts @@ -72,6 +72,41 @@ it("retains a publication receipt when formatting fails after the invocation con expect(Buffer.concat(chunks).toString("utf8")).toContain("/tmp/result.json"); }); +it.each([ + { format: "md", value: "\u0000", window: false }, + { format: "toon", value: "\ud800", window: false }, + { format: "toon", value: "\ud800", window: true }, +] as const)( + "retains the committed receipt when $format cannot encode output (token window: $window)", + async ({ format, value, window }) => { + const chunks: Buffer[] = []; + const receipt = { path: "/authored/retained.json", bytes: 123 }; + const output = createCliResultOutput( + parseCliOutputArguments([ + "--format", + format, + ...(window ? ["--token-limit", "1000"] : []), + ]), + new Writable({ + write(chunk: Buffer, _encoding, callback) { + chunks.push(chunk); + callback(); + }, + }), + ); + if (output === undefined) throw new Error("Missing output owner"); + await withCliEvidencePublication(async () => { + recordCliEvidencePublication(receipt); + expect(await output.write({ value }, { ...metadata, format })).toBe(true); + }); + expect(output.error).toMatchObject({ + code: "invalid_request", + details: { diagnostics: { evidence_bundle: receipt } }, + }); + expect(Buffer.concat(chunks).toString("utf8")).toContain(receipt.path); + }, +); + describe("CLI formatting stage admission", () => { it("preserves native CTA sentinel output", async () => { const replies: string[] = [];