diff --git a/src/cli/cliResultOutput.ts b/src/cli/cliResultOutput.ts index 7eb5ee5cf0..d8712b1eff 100644 --- a/src/cli/cliResultOutput.ts +++ b/src/cli/cliResultOutput.ts @@ -632,7 +632,9 @@ export const createCliResultOutput = ( format !== "json" && format !== "jsonl"; // Delegate other human formats to Incur. Markdown stays here so admission - // and emitted table text use the same safe formatter. + // and emitted table text use the same safe formatter. A full-output token + // count stays here too: Incur counts only the selected data, so delegating + // it would report the bare result instead of the emitted envelope. if ( !metadata.forceOutput && format !== "md" && @@ -640,7 +642,8 @@ export const createCliResultOutput = ( format !== "jsonl" && operationFailure === undefined && !ownsFilteredOutput && - !ownsSuccessfulTokenWindow + !ownsSuccessfulTokenWindow && + !(options.tokenCount && options.fullOutput) ) return false; if (format === "jsonl") { diff --git a/tests/boundary/cli/cliFullOutputTokenCount.test.ts b/tests/boundary/cli/cliFullOutputTokenCount.test.ts new file mode 100644 index 0000000000..6fd2628d30 --- /dev/null +++ b/tests/boundary/cli/cliFullOutputTokenCount.test.ts @@ -0,0 +1,65 @@ +import { Writable } from "node:stream"; + +import { Formatter } from "incur"; +import { estimateTokenCount } from "tokenx"; +import { describe, expect, it } from "vitest"; + +import { parseCliOutputArguments } from "../../../src/cliOutput.js"; +import { createCliResultOutput } from "../../../src/cli/cliResultOutput.js"; + +const countFor = async ( + value: unknown, + format: "toon" | "yaml", +): Promise<{ readonly handled: boolean; readonly text: string }> => { + const chunks: Buffer[] = []; + const destination = new Writable({ + write(chunk: Buffer, _encoding, callback) { + chunks.push(chunk); + callback(); + }, + }); + const output = createCliResultOutput( + parseCliOutputArguments([ + "fixture", + "--format", + format, + "--full-output", + "--token-count", + ]), + destination, + ); + if (output === undefined) throw new Error("Missing output surface"); + const handled = await output.write(value, { + command: "fixture", + duration: "0ms", + format, + }); + return { handled, text: Buffer.concat(chunks).toString("utf8") }; +}; + +describe("full-output token counts for delegated formats", () => { + it.each(["toon", "yaml"] as const)( + "counts the emitted envelope, not just the selected data (%s)", + async (format) => { + const value = { imported: 0, unknowns_added: 0, total: 0 }; + const counted = await countFor(value, format); + expect(counted.handled).toBe(true); + expect(counted.text).toBe( + `${estimateTokenCount( + Formatter.format( + { + ok: true, + data: value, + meta: { command: "fixture", duration: "0ms" }, + }, + format, + ), + )}\n`, + ); + }, + ); + + // Bare counts stay delegated to Incur and are covered by the CLI replay + // (bare toon/yaml report the selected result before and after this fix); + // this boundary owns only the full-output envelope count. +});