From a02202aba2aea8a7289500ca3eb45acfa8778a0b Mon Sep 17 00:00:00 2001 From: Saim Shafique Date: Tue, 27 Oct 2026 01:07:15 +0500 Subject: [PATCH] fix(cli): count the full-output envelope for TOON/YAML token counts With --token-count --full-output, TOON and YAML results were delegated to Incur before the envelope-aware count branch, so Incur counted only the selected data (13) instead of the emitted full document (31). Keep that combination on the REA output boundary so the count covers the envelope and metadata, matching the docs/cli.md contract. Bare counts stay delegated to Incur and are unchanged. Fixes #7271 Supersedes #7278 Co-authored-by: Saim Shafique <128062316+sx4im@users.noreply.github.com> Co-Authored-By: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com> --- src/cli/cliResultOutput.ts | 7 +- .../cli/cliFullOutputTokenCount.test.ts | 65 +++++++++++++++++++ 2 files changed, 70 insertions(+), 2 deletions(-) create mode 100644 tests/boundary/cli/cliFullOutputTokenCount.test.ts 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. +});