import { describe, expect, it } from 'vitest' import { decodeCodex } from '../../src/providers/codex/index.js' import type { CodexDecodeState } from '../../src/providers/codex/index.js' import type { DecodeContext } from '../../src/contracts.js' const context: DecodeContext = { privacyKey: 'k', providerId: 'codex', sourceRef: 'ref' } function sessionMeta(opts: { session_id: string; model?: string; forked_from_id?: string; timestamp?: string; cwd?: string }) { return JSON.stringify({ type: 'session_meta', timestamp: opts.timestamp ?? '2026-04-14T10:00:00Z', payload: { cwd: opts.cwd ?? '/Users/t/p', originator: 'codex-cli', session_id: opts.session_id, model: opts.model ?? 'gpt-5.3-codex', ...(opts.forked_from_id ? { forked_from_id: opts.forked_from_id } : {}), }, }) } function userMessage(text: string, timestamp: string) { return JSON.stringify({ type: 'response_item', timestamp, payload: { type: 'message', role: 'user', content: [{ type: 'input_text', text }] } }) } function assistantMessage(text: string, timestamp: string) { return JSON.stringify({ type: 'response_item', timestamp, payload: { type: 'message', role: 'assistant', content: [{ type: 'output_text', text }] } }) } function functionCall(name: string, timestamp: string) { return JSON.stringify({ type: 'response_item', timestamp, payload: { type: 'function_call', name } }) } function tokenCount(opts: { timestamp: string; last?: { input?: number; cached?: number; output?: number; reasoning?: number }; total?: { input?: number; cached?: number; output?: number; reasoning?: number; total?: number }; noInfo?: boolean }) { const info = opts.noInfo ? undefined : { last_token_usage: opts.last ? { input_tokens: opts.last.input ?? 0, cached_input_tokens: opts.last.cached ?? 0, output_tokens: opts.last.output ?? 0, reasoning_output_tokens: opts.last.reasoning ?? 0, total_tokens: (opts.last.input ?? 0) + (opts.last.output ?? 0) } : undefined, total_token_usage: opts.total ? { input_tokens: opts.total.input ?? 0, cached_input_tokens: opts.total.cached ?? 0, output_tokens: opts.total.output ?? 0, reasoning_output_tokens: opts.total.reasoning ?? 0, total_tokens: opts.total.total ?? 0 } : undefined, } return JSON.stringify({ type: 'event_msg', timestamp: opts.timestamp, payload: { type: 'token_count', ...(opts.noInfo ? {} : { info }) } }) } // A corpus spanning THREE sessions, including a forked session that replays the // parent's history and a session whose token_count carries no info (char-estimate // branch). Session boundaries (session_meta) exercise the per-session reset while // the fork boundary exercises cross-session dedup memory in the threaded state. const CORPUS: string[] = [ // Session A (parent): 1100 tokens of real work across two turns. sessionMeta({ session_id: 'sess-parent', timestamp: '2026-04-14T10:00:00Z' }), userMessage('fix the parser', '2026-04-14T10:00:01Z'), functionCall('exec_command', '2026-04-14T10:00:02Z'), tokenCount({ timestamp: '2026-04-14T10:00:03Z', last: { input: 700 }, total: { input: 700, total: 700 } }), userMessage('now the tests', '2026-04-14T10:00:04Z'), functionCall('read_file', '2026-04-14T10:00:05Z'), tokenCount({ timestamp: '2026-04-14T10:00:06Z', last: { input: 400 }, total: { input: 1100, total: 1100 } }), // Session B (fork of A): replays A's two events past the 5s cutoff, then +400. sessionMeta({ session_id: 'sess-fork', forked_from_id: 'sess-parent', timestamp: '2026-04-14T10:05:00Z' }), tokenCount({ timestamp: '2026-04-14T10:05:20Z', last: { input: 700 }, total: { input: 700, total: 700 } }), tokenCount({ timestamp: '2026-04-14T10:05:21Z', last: { input: 400 }, total: { input: 1100, total: 1100 } }), functionCall('spawn_agent', '2026-04-14T10:05:22Z'), tokenCount({ timestamp: '2026-04-14T10:05:23Z', last: { input: 400 }, total: { input: 1500, total: 1500 } }), // Session C: char-estimate branch (token_count with no info). sessionMeta({ session_id: 'sess-est', model: 'gpt-5.5', timestamp: '2026-04-14T11:00:00Z' }), userMessage('hello there estimate branch', '2026-04-14T11:00:01Z'), assistantMessage('a fairly long assistant reply that should estimate some output tokens here', '2026-04-14T11:00:02Z'), tokenCount({ timestamp: '2026-04-14T11:00:03Z', noInfo: true }), ] const FORK_BOUNDARY_INDEX = 7 // the fork's session_meta const MID_PARENT_INDEX = 4 // between A's two turns function decodeCold(records: string[]) { return decodeCodex({ records, context }).calls } function decodeTwoPass(records: string[], split: number) { const first = decodeCodex({ records: records.slice(0, split), context }) // Serialize state to JSON and back between passes (the resume invariant). const serialized: CodexDecodeState = JSON.parse(JSON.stringify(first.state)) const second = decodeCodex({ records: records.slice(split), context, state: serialized }) return [...first.calls, ...second.calls] } describe('codex decoder — round-trip resume invariant', () => { it('cold single pass produces the expected number of calls', () => { const cold = decodeCold(CORPUS) // A: 2 real turns; fork: replays dropped, 1 genuine; C: 1 estimate = 4. expect(cold).toHaveLength(4) // The fork's genuine event survives (input 400 at cumulative 1500). const tokensTotal = cold.reduce((n, c) => n + c.inputTokens + c.outputTokens + c.cachedInputTokens + c.reasoningTokens, 0) // A: 700 + 400; fork: +400; C: estimate input+output. expect(tokensTotal).toBeGreaterThan(1500) }) it('two-pass split at EVERY index deep-equals the cold pass', () => { const cold = decodeCold(CORPUS) for (let split = 0; split <= CORPUS.length; split++) { const twoPass = decodeTwoPass(CORPUS, split) expect(twoPass, `split at index ${split}`).toEqual(cold) } }) it('mid-session split (between two turns of the same session) matches cold', () => { expect(decodeTwoPass(CORPUS, MID_PARENT_INDEX)).toEqual(decodeCold(CORPUS)) }) it('split across the forked-session boundary matches cold (dedup memory threads)', () => { expect(decodeTwoPass(CORPUS, FORK_BOUNDARY_INDEX)).toEqual(decodeCold(CORPUS)) }) it('serialized state is plain JSON (no Map/Set/undefined-only loss)', () => { const { state } = decodeCodex({ records: CORPUS.slice(0, FORK_BOUNDARY_INDEX), context }) const round = JSON.parse(JSON.stringify(state)) as CodexDecodeState // seenKeys survives as an array carrying the parent's dedup fingerprints. expect(Array.isArray(round.seenKeys)).toBe(true) expect(round.seenKeys.length).toBeGreaterThan(0) // prevCumulativeTotal is a JSON-safe null|number. expect(round.prevCumulativeTotal === null || typeof round.prevCumulativeTotal === 'number').toBe(true) }) it('a fork replay is dropped only because seenKeys threaded across the split', () => { // Splitting exactly at the fork boundary, WITHOUT threading state, would let // the fork replay its parent's events (double count). With threaded state the // replay collides and drops — proving the dedup memory is in the state. const threaded = decodeTwoPass(CORPUS, FORK_BOUNDARY_INDEX) const parentAndForkFresh = [ ...decodeCodex({ records: CORPUS.slice(0, FORK_BOUNDARY_INDEX), context }).calls, // second pass with NO state: the fork can't see the parent's keys. ...decodeCodex({ records: CORPUS.slice(FORK_BOUNDARY_INDEX), context }).calls, ] expect(parentAndForkFresh.length).toBeGreaterThan(threaded.length) }) })