1import { describe, expect, it } from "@effect/vitest";
2
3import { initialCodexScanState, parseCodexLine } from "./codexUsage.ts";
4
5describe("parseCodexLine", () => {
6 const sessionMeta = JSON.stringify({
7 type: "session_meta",
8 timestamp: "2026-08-01T05:17:41.289Z",
9 payload: { type: "session_meta", id: "019fbbc1-b12c-7360-a685-28c181f0025f" },
10 });
11 const turnContext = JSON.stringify({
12 type: "turn_context",
13 timestamp: "2026-08-01T05:17:42.694Z",
14 payload: { type: "turn_context", model: "gpt-5.6-sol" },
15 });
16 const tokenCount = (inputTokens: number, cached: number, output: number, reasoning: number) =>
17 JSON.stringify({
18 type: "event_msg",
19 timestamp: "2026-08-01T05:17:49.919Z",
20 payload: {
21 type: "token_count",
22 info: {
23 last_token_usage: {
24 input_tokens: inputTokens,
25 cached_input_tokens: cached,
26 cache_write_input_tokens: 0,
27 output_tokens: output,
28 reasoning_output_tokens: reasoning,
29 },
30 },
31 },
32 });
33
34 it("attributes usage to the model from the preceding turn context", () => {
35 const state = initialCodexScanState();
36 parseCodexLine(sessionMeta, state);
37 parseCodexLine(turnContext, state);
38 const record = parseCodexLine(tokenCount(19239, 11008, 299, 116), state);
39
40 expect(record?.provider).toBe("codex");
41 expect(record?.model).toBe("gpt-5.6-sol");
42 expect(record?.sessionId).toBe("019fbbc1-b12c-7360-a685-28c181f0025f");
43 // Codex reports input_tokens inclusive of the cached portion.
44 expect(record?.totals.uncachedInputTokens).toBe(19239 - 11008);
45 expect(record?.totals.cachedInputTokens).toBe(11008);
46 expect(record?.totals.reasoningTokens).toBe(116);
47 });
48
49 it("skips a repeated token_count so deltas are not double counted", () => {
50 const state = initialCodexScanState();
51 parseCodexLine(turnContext, state);
52 const first = parseCodexLine(tokenCount(100, 0, 10, 0), state);
53 const repeat = parseCodexLine(tokenCount(100, 0, 10, 0), state);
54
55 expect(first).not.toBeNull();
56 expect(repeat).toBeNull();
57 });
58
59 it("drops usage that arrives before any model is known", () => {
60 const state = initialCodexScanState();
61 expect(parseCodexLine(tokenCount(100, 0, 10, 0), state)).toBeNull();
62 });
63
64 it("does not let a pre-model event poison the duplicate signature", () => {
65 // A token_count before its turn_context is dropped; the identical event
66 // re-emitted once the model is known must still be counted.
67 const state = initialCodexScanState();
68 expect(parseCodexLine(tokenCount(100, 0, 10, 0), state)).toBeNull();
69 parseCodexLine(turnContext, state);
70 expect(parseCodexLine(tokenCount(100, 0, 10, 0), state)).not.toBeNull();
71 });
72
73 it("carries the service tier from the latest thread settings", () => {
74 const settings = (thread_settings: Record<string, unknown>) =>
75 JSON.stringify({
76 type: "event_msg",
77 timestamp: "2026-08-01T05:17:42.000Z",
78 payload: { type: "thread_settings_applied", thread_settings },
79 });
80 const state = initialCodexScanState();
81 parseCodexLine(turnContext, state);
82 const speedAfter = (line: string, output: number) => {
83 parseCodexLine(line, state);
84 return parseCodexLine(tokenCount(100, 0, output, 0), state)?.speed;
85 };
86
87 expect(parseCodexLine(tokenCount(100, 0, 1, 0), state)?.speed).toBe("standard");
88 expect(speedAfter(settings({ service_tier: "ultrafast" }), 2)).toBe("ultrafast");
89 expect(speedAfter(settings({ service_tier: "priority" }), 3)).toBe("fast");
90 // Codex omits the field when no tier was requested.
91 expect(speedAfter(settings({ model: "gpt-6-astra" }), 4)).toBe("standard");
92 });
93
94 // A forked/subagent rollout opens with the parent's history copied in and
95 // every line re-stamped to the fork instant, then the ancestors' session
96 // metas. Counting those again multiplied usage ~1.85x on real data (#5758).
97 describe("forked rollouts", () => {
98 const meta = (overrides: {
99 id: string;
100 timestamp: string;
101 forkedFromId?: string;
102 spawnParentId?: string;
103 }) =>
104 JSON.stringify({
105 type: "session_meta",
106 timestamp: overrides.timestamp,
107 payload: {
108 type: "session_meta",
109 id: overrides.id,
110 ...(overrides.forkedFromId === undefined
111 ? {}
112 : { forked_from_id: overrides.forkedFromId }),
113 ...(overrides.spawnParentId === undefined
114 ? {}
115 : {
116 source: {
117 subagent: { thread_spawn: { parent_thread_id: overrides.spawnParentId } },
118 },
119 }),
120 },
121 });
122 const stamped = (timestamp: string, line: string) => {
123 const parsed = JSON.parse(line) as { timestamp: string };
124 parsed.timestamp = timestamp;
125 return JSON.stringify(parsed);
126 };
127
128 it("keeps the child session id over copied ancestor metas", () => {
129 const state = initialCodexScanState();
130 parseCodexLine(meta({ id: "child", timestamp: "2026-08-01T05:00:00.000Z" }), state);
131 parseCodexLine(meta({ id: "parent", timestamp: "2026-08-01T05:00:00.000Z" }), state);
132 parseCodexLine(turnContext, state);
133 const record = parseCodexLine(tokenCount(100, 0, 10, 0), state);
134
135 expect(record?.sessionId).toBe("child");
136 });
137
138 it("drops the re-stamped copied burst and keeps the first real event", () => {
139 const state = initialCodexScanState();
140 const forkInstant = "2026-08-01T05:00:00.000Z";
141 parseCodexLine(meta({ id: "child", timestamp: forkInstant, forkedFromId: "parent" }), state);
142 parseCodexLine(meta({ id: "parent", timestamp: forkInstant }), state);
143 parseCodexLine(stamped(forkInstant, turnContext), state);
144
145 // Copied history: written in one burst at the fork instant.
146 expect(
147 parseCodexLine(stamped("2026-08-01T05:00:00.001Z", tokenCount(100, 0, 10, 0)), state),
148 ).toBeNull();
149 expect(
150 parseCodexLine(stamped("2026-08-01T05:00:00.002Z", tokenCount(200, 0, 20, 0)), state),
151 ).toBeNull();
152
153 // The child's first genuine turn lands seconds later and must count.
154 const real = parseCodexLine(
155 stamped("2026-08-01T05:00:06.000Z", tokenCount(300, 0, 30, 0)),
156 state,
157 );
158 expect(real).not.toBeNull();
159 expect(real?.totals.outputTokens).toBe(30);
160
161 // Suppression never restarts, even for closely spaced later events.
162 const next = parseCodexLine(
163 stamped("2026-08-01T05:00:06.100Z", tokenCount(400, 0, 40, 0)),
164 state,
165 );
166 expect(next).not.toBeNull();
167 });
168
169 it("recognizes subagent spawns without forked_from_id", () => {
170 const state = initialCodexScanState();
171 const spawnInstant = "2026-08-01T05:00:00.000Z";
172 parseCodexLine(
173 meta({ id: "child", timestamp: spawnInstant, spawnParentId: "parent" }),
174 state,
175 );
176 parseCodexLine(stamped(spawnInstant, turnContext), state);
177 expect(
178 parseCodexLine(stamped("2026-08-01T05:00:00.001Z", tokenCount(100, 0, 10, 0)), state),
179 ).toBeNull();
180 });
181
182 it("does not suppress anything in a rollout that is not a fork", () => {
183 const state = initialCodexScanState();
184 parseCodexLine(meta({ id: "root", timestamp: "2026-08-01T05:00:00.000Z" }), state);
185 parseCodexLine(stamped("2026-08-01T05:00:00.100Z", turnContext), state);
186 const record = parseCodexLine(
187 stamped("2026-08-01T05:00:00.200Z", tokenCount(100, 0, 10, 0)),
188 state,
189 );
190 expect(record).not.toBeNull();
191 });
192 });
193});