import { describe, expect, it } from "vite-plus/test"; import { normalizeClaudeTurnTokenUsage } from "./ClaudeTurnTokenUsage.ts"; import { accumulateCodexTurnTokenUsage, completeCodexTurnTokenUsage, getCodexTurnAccumulator, makeCodexTurnTokenUsageState, } from "./CodexTurnTokenUsage.ts"; const breakdown = (input: number, cached: number, output: number) => ({ totalTokens: input + output, inputTokens: input, cachedInputTokens: cached, outputTokens: output, reasoningOutputTokens: 0, }); const update = (total: ReturnType, last = total) => ({ total, last, modelContextWindow: 200_000, }); describe("billable native turn usage", () => { it("adds each Codex response once and excludes the prior resumed history", () => { const state = makeCodexTurnTokenUsageState(); state.activeTurnId = "turn"; const first = update(breakdown(1010, 505, 102), breakdown(10, 5, 2)); accumulateCodexTurnTokenUsage(state, "turn", first); accumulateCodexTurnTokenUsage(state, "turn", first); accumulateCodexTurnTokenUsage( state, "turn", update(breakdown(1030, 515, 108), breakdown(20, 10, 6)), ); expect(completeCodexTurnTokenUsage(state, "turn", true)).toEqual({ usageStatus: "complete", usageScope: "main_agent", hasSubagents: false, inputTokens: 30, cachedInputTokens: 15, outputTokens: 8, reasoningTokens: 0, }); }); it("keeps previous responses when compaction resets Codex cumulative counters", () => { const state = makeCodexTurnTokenUsageState(); state.activeTurnId = "turn"; accumulateCodexTurnTokenUsage(state, "turn", update(breakdown(100, 40, 20))); accumulateCodexTurnTokenUsage(state, "turn", update(breakdown(20, 5, 3))); getCodexTurnAccumulator(state, "turn").hasSubagents = true; expect(completeCodexTurnTokenUsage(state, "turn", false)).toMatchObject({ usageStatus: "partial", hasSubagents: true, inputTokens: 120, cachedInputTokens: 45, outputTokens: 23, }); }); it("does not bill late Codex updates against the next active turn", () => { const state = makeCodexTurnTokenUsageState(); state.activeTurnId = "next"; accumulateCodexTurnTokenUsage(state, "previous", update(breakdown(100, 10, 20))); accumulateCodexTurnTokenUsage( state, "next", update(breakdown(104, 12, 21), breakdown(4, 2, 1)), ); expect(completeCodexTurnTokenUsage(state, "next", true)).toMatchObject({ inputTokens: 4, outputTokens: 1, }); expect(completeCodexTurnTokenUsage(state, "missing", false).usageStatus).toBe("unavailable"); }); it("counts Claude cached input inside total input and bounds thinking output", () => { expect( normalizeClaudeTurnTokenUsage( { subtype: "success", usage: { input_tokens: 100, cache_read_input_tokens: 40, cache_creation_input_tokens: 10, output_tokens: 20, output_tokens_details: { thinking_tokens: 25 }, }, }, true, "completed", ), ).toEqual({ usageStatus: "complete", usageScope: "main_agent", hasSubagents: true, inputTokens: 150, cachedInputTokens: 40, cacheCreationTokens: 10, outputTokens: 20, reasoningTokens: 20, }); }); it("does not turn a zeroed Claude crash result into a measured zero-cost turn", () => { expect( normalizeClaudeTurnTokenUsage( { subtype: "error_during_execution", usage: { input_tokens: 0, output_tokens: 0, }, }, false, "failed", ).usageStatus, ).toBe("unavailable"); expect( normalizeClaudeTurnTokenUsage( { subtype: "success", usage: { input_tokens: 4, output_tokens: 2, }, }, false, "interrupted", ), ).toMatchObject({ usageStatus: "partial", inputTokens: 4, outputTokens: 2 }); }); });