// tests/lib/cache-analyzer.test.mjs // SKAL-2 Step 3 — token/cost aggregation in cache-analyzer's summarize(). // Also the first test coverage for cache-analyzer (previously zero-coverage): // includes a regression guard that the pre-existing duration/event-name fields // still compute correctly on mixed input. // // Hermetic: no LLM, network, time, or randomness. Idiom: // tests/lib/token-usage.test.mjs (pure-module node:test style). import { test } from 'node:test'; import { strict as assert } from 'node:assert'; import { summarize } from '../../lib/stats/cache-analyzer.mjs'; const jsonl = (objs) => objs.map(o => JSON.stringify(o)); // ---- token/cost aggregation ------------------------------------------------ test('summarize — two token-bearing records → summed totals + cost', () => { const s = summarize(jsonl([ { ts: '2026-06-26T12:00:00Z', session_id: 'S1', scope: 'main-context', tokens_input: 1000, tokens_output: 200, tokens_cache_creation: 400, tokens_cache_read: 5000, cost_usd: 0.03675 }, { ts: '2026-06-26T13:00:00Z', session_id: 'S2', scope: 'main-context', tokens_input: 2000, tokens_output: 300, tokens_cache_creation: 600, tokens_cache_read: 1000, cost_usd: 0.05 }, ])); assert.equal(s.total_tokens_input, 3000); assert.equal(s.total_tokens_output, 500); assert.equal(s.total_tokens_cache_creation, 1000); assert.equal(s.total_tokens_cache_read, 6000); assert.equal(s.sessions_with_tokens, 2); assert.ok(Math.abs(s.total_cost_usd - 0.08675) < 1e-9, `cost ${s.total_cost_usd}`); }); test('summarize — cost_usd:null counts the session but is excluded from total_cost_usd', () => { const s = summarize(jsonl([ { session_id: 'S1', tokens_input: 100, cost_usd: 0.01 }, { session_id: 'S2', tokens_input: 200, cost_usd: null, is_estimate: true }, ])); assert.equal(s.sessions_with_tokens, 2, 'null-cost session must still be counted'); assert.equal(s.total_tokens_input, 300); assert.ok(Math.abs(s.total_cost_usd - 0.01) < 1e-9, `cost ${s.total_cost_usd}`); }); test('summarize — no token records → token totals stay zero (additive default)', () => { const s = summarize(jsonl([ { event: 'main-merge-gate', duration_ms: 100 }, ])); assert.equal(s.total_tokens_input, 0); assert.equal(s.total_cost_usd, 0); assert.equal(s.sessions_with_tokens, 0); }); // ---- regression guard: pre-existing fields still computed ------------------ test('summarize — existing duration/event-name fields still correct on mixed input', () => { const s = summarize(jsonl([ { event: 'main-merge-gate', duration_ms: 100, ts: '2026-06-26T10:00:00Z' }, { event: 'main-merge-approved', duration_ms: 200, ts: '2026-06-26T11:00:00Z' }, { session_id: 'S1', tokens_input: 1000, cost_usd: 0.04, ts: '2026-06-26T12:00:00Z' }, ])); // Pre-existing behavior (regression guard) assert.equal(s.total_events, 3); assert.equal(s.events_with_duration, 2); // Percentiles: durations [100,200] → floor(2·0.5)=floor(2·0.9)=idx 1 → both 200. assert.equal(s.wall_time_ms_p50, 200); assert.equal(s.wall_time_ms_p90, 200); assert.equal(s.wall_time_ms_max, 200); assert.deepEqual(s.unique_event_names, ['main-merge-approved', 'main-merge-gate']); // Time range spans all ts-bearing lines (incl. the token-only record at 12:00). assert.equal(s.oldest_event_iso, '2026-06-26T10:00:00.000Z'); assert.equal(s.newest_event_iso, '2026-06-26T12:00:00.000Z'); // New behavior coexists assert.equal(s.total_tokens_input, 1000); assert.equal(s.sessions_with_tokens, 1); assert.ok(Math.abs(s.total_cost_usd - 0.04) < 1e-9); }); test('summarize — malformed/empty lines tolerated alongside token records', () => { const s = summarize([ '', 'not json', JSON.stringify({ session_id: 'S1', tokens_input: 50, cost_usd: 0.001 }), ]); assert.equal(s.total_events, 1); assert.equal(s.sessions_with_tokens, 1); assert.equal(s.total_tokens_input, 50); });