import { describe, expect, it } from "@effect/vitest"; import { initialCodexScanState, parseClaudeLine, parseCodexLine, totalTokens,} from "./usageTranscripts.ts"; /** Shaped after a real Claude Code assistant record. */function claudeLine(overrides: { messageId: string; contentType: string; model?: string; outputTokens?: number;}): string { return JSON.stringify({ type: "assistant", timestamp: "2026-08-07T04:05:13.944Z", sessionId: "5a128faa-8253-489e-b935-6c08e8e670c0", cwd: "/home/theo/project", message: { id: overrides.messageId, role: "assistant", model: overrides.model ?? "claude-fable-5", content: [{ type: overrides.contentType }], usage: { input_tokens: 2, cache_creation_input_tokens: 66818, cache_read_input_tokens: 1000, output_tokens: overrides.outputTokens ?? 286, }, }, });} describe("parseClaudeLine", () => { it("extracts token totals and a dedupe key", () => { const record = parseClaudeLine(claudeLine({ messageId: "msg_1", contentType: "text" })); expect(record).not.toBeNull(); expect(record?.provider).toBe("claude"); expect(record?.model).toBe("claude-fable-5"); expect(record?.totals).toEqual({ uncachedInputTokens: 2, cachedInputTokens: 1000, cacheCreationTokens: 66818, outputTokens: 286, reasoningTokens: 0, }); expect(record?.dedupeKey).toBe("msg_1:"); }); it("gives every content block of one message the same dedupe key", () => { // T3 Code writes one record per content block, each repeating the parent // message's full usage. Summing them would overcount ~2.4x on real data. const text = parseClaudeLine(claudeLine({ messageId: "msg_2", contentType: "text" })); const toolUse = parseClaudeLine(claudeLine({ messageId: "msg_2", contentType: "tool_use" })); expect(text?.dedupeKey).toBe(toolUse?.dedupeKey); expect(text?.totals).toEqual(toolUse?.totals); }); it("ignores records that are not assistant messages", () => { expect(parseClaudeLine(JSON.stringify({ type: "user", message: {} }))).toBeNull(); expect(parseClaudeLine("not json")).toBeNull(); });}); describe("parseCodexLine", () => { const sessionMeta = JSON.stringify({ type: "session_meta", timestamp: "2026-08-01T05:17:41.289Z", payload: { type: "session_meta", id: "019fbbc1-b12c-7360-a685-28c181f0025f" }, }); const turnContext = JSON.stringify({ type: "turn_context", timestamp: "2026-08-01T05:17:42.694Z", payload: { type: "turn_context", model: "gpt-5.6-sol" }, }); const tokenCount = (inputTokens: number, cached: number, output: number, reasoning: number) => JSON.stringify({ type: "event_msg", timestamp: "2026-08-01T05:17:49.919Z", payload: { type: "token_count", info: { last_token_usage: { input_tokens: inputTokens, cached_input_tokens: cached, cache_write_input_tokens: 0, output_tokens: output, reasoning_output_tokens: reasoning, }, }, }, }); it("attributes usage to the model from the preceding turn context", () => { const state = initialCodexScanState(); parseCodexLine(sessionMeta, state); parseCodexLine(turnContext, state); const record = parseCodexLine(tokenCount(19239, 11008, 299, 116), state); expect(record?.provider).toBe("codex"); expect(record?.model).toBe("gpt-5.6-sol"); expect(record?.sessionId).toBe("019fbbc1-b12c-7360-a685-28c181f0025f"); // Codex reports input_tokens inclusive of the cached portion. expect(record?.totals.uncachedInputTokens).toBe(19239 - 11008); expect(record?.totals.cachedInputTokens).toBe(11008); expect(record?.totals.reasoningTokens).toBe(116); }); it("skips a repeated token_count so deltas are not double counted", () => { const state = initialCodexScanState(); parseCodexLine(turnContext, state); const first = parseCodexLine(tokenCount(100, 0, 10, 0), state); const repeat = parseCodexLine(tokenCount(100, 0, 10, 0), state); expect(first).not.toBeNull(); expect(repeat).toBeNull(); }); it("drops usage that arrives before any model is known", () => { const state = initialCodexScanState(); expect(parseCodexLine(tokenCount(100, 0, 10, 0), state)).toBeNull(); }); it("does not let a pre-model event poison the duplicate signature", () => { // A token_count before its turn_context is dropped; the identical event // re-emitted once the model is known must still be counted. const state = initialCodexScanState(); expect(parseCodexLine(tokenCount(100, 0, 10, 0), state)).toBeNull(); parseCodexLine(turnContext, state); expect(parseCodexLine(tokenCount(100, 0, 10, 0), state)).not.toBeNull(); });}); describe("totalTokens", () => { it("does not add reasoning on top of output", () => { expect( totalTokens({ uncachedInputTokens: 10, cachedInputTokens: 20, cacheCreationTokens: 30, outputTokens: 40, reasoningTokens: 25, }), ).toBe(100); });});
import { describe, expect, it } from "@effect/vitest";
import {
initialCodexScanState,
parseClaudeLine,
parseCodexLine,
totalTokens,
} from "./usageTranscripts.ts";
/** Shaped after a real Claude Code assistant record. */
function claudeLine(overrides: {
messageId: string;
contentType: string;
model?: string;
outputTokens?: number;
}): string {
return JSON.stringify({
type: "assistant",
timestamp: "2026-08-07T04:05:13.944Z",
sessionId: "5a128faa-8253-489e-b935-6c08e8e670c0",
cwd: "/home/theo/project",
message: {
id: overrides.messageId,
role: "assistant",
model: overrides.model ?? "claude-fable-5",
content: [{ type: overrides.contentType }],
usage: {
input_tokens: 2,
cache_creation_input_tokens: 66818,
cache_read_input_tokens: 1000,
output_tokens: overrides.outputTokens ?? 286,
},
},
});
}
describe("parseClaudeLine", () => {
it("extracts token totals and a dedupe key", () => {
const record = parseClaudeLine(claudeLine({ messageId: "msg_1", contentType: "text" }));
expect(record).not.toBeNull();
expect(record?.provider).toBe("claude");
expect(record?.model).toBe("claude-fable-5");
expect(record?.totals).toEqual({
uncachedInputTokens: 2,
cachedInputTokens: 1000,
cacheCreationTokens: 66818,
outputTokens: 286,
reasoningTokens: 0,
});
expect(record?.dedupeKey).toBe("msg_1:");
});
it("gives every content block of one message the same dedupe key", () => {
// T3 Code writes one record per content block, each repeating the parent
// message's full usage. Summing them would overcount ~2.4x on real data.
const text = parseClaudeLine(claudeLine({ messageId: "msg_2", contentType: "text" }));
const toolUse = parseClaudeLine(claudeLine({ messageId: "msg_2", contentType: "tool_use" }));
expect(text?.dedupeKey).toBe(toolUse?.dedupeKey);
expect(text?.totals).toEqual(toolUse?.totals);
});
it("ignores records that are not assistant messages", () => {
expect(parseClaudeLine(JSON.stringify({ type: "user", message: {} }))).toBeNull();
expect(parseClaudeLine("not json")).toBeNull();
});
});
describe("parseCodexLine", () => {
const sessionMeta = JSON.stringify({
type: "session_meta",
timestamp: "2026-08-01T05:17:41.289Z",
payload: { type: "session_meta", id: "019fbbc1-b12c-7360-a685-28c181f0025f" },
});
const turnContext = JSON.stringify({
type: "turn_context",
timestamp: "2026-08-01T05:17:42.694Z",
payload: { type: "turn_context", model: "gpt-5.6-sol" },
});
const tokenCount = (inputTokens: number, cached: number, output: number, reasoning: number) =>
JSON.stringify({
type: "event_msg",
timestamp: "2026-08-01T05:17:49.919Z",
payload: {
type: "token_count",
info: {
last_token_usage: {
input_tokens: inputTokens,
cached_input_tokens: cached,
cache_write_input_tokens: 0,
output_tokens: output,
reasoning_output_tokens: reasoning,
},
},
},
});
it("attributes usage to the model from the preceding turn context", () => {
const state = initialCodexScanState();
parseCodexLine(sessionMeta, state);
parseCodexLine(turnContext, state);
const record = parseCodexLine(tokenCount(19239, 11008, 299, 116), state);
expect(record?.provider).toBe("codex");
expect(record?.model).toBe("gpt-5.6-sol");
expect(record?.sessionId).toBe("019fbbc1-b12c-7360-a685-28c181f0025f");
// Codex reports input_tokens inclusive of the cached portion.
expect(record?.totals.uncachedInputTokens).toBe(19239 - 11008);
expect(record?.totals.cachedInputTokens).toBe(11008);
expect(record?.totals.reasoningTokens).toBe(116);
});
it("skips a repeated token_count so deltas are not double counted", () => {
const state = initialCodexScanState();
parseCodexLine(turnContext, state);
const first = parseCodexLine(tokenCount(100, 0, 10, 0), state);
const repeat = parseCodexLine(tokenCount(100, 0, 10, 0), state);
expect(first).not.toBeNull();
expect(repeat).toBeNull();
});
it("drops usage that arrives before any model is known", () => {
const state = initialCodexScanState();
expect(parseCodexLine(tokenCount(100, 0, 10, 0), state)).toBeNull();
});
it("does not let a pre-model event poison the duplicate signature", () => {
// A token_count before its turn_context is dropped; the identical event
// re-emitted once the model is known must still be counted.
const state = initialCodexScanState();
expect(parseCodexLine(tokenCount(100, 0, 10, 0), state)).toBeNull();
parseCodexLine(turnContext, state);
expect(parseCodexLine(tokenCount(100, 0, 10, 0), state)).not.toBeNull();
});
});
describe("totalTokens", () => {
it("does not add reasoning on top of output", () => {
expect(
totalTokens({
uncachedInputTokens: 10,
cachedInputTokens: 20,
cacheCreationTokens: 30,
outputTokens: 40,
reasoningTokens: 25,
}),
).toBe(100);
});
});