voyage/tests/lib/token-usage.test.mjs
Kjell Tore Guttormsen 68c9bef38f test(observability): close 2 MAJOR test gaps from SKAL-2 review
F1 cache-analyzer regression guard (SC5): pin percentile (wall_time_ms_p50/p90)
and time-range (oldest/newest_event_iso) — 2 of 3 'unchanged' categories were
previously un-asserted on the mixed-input fixture.

F2 lastMainChainModel: add direct coverage — last-wins across 2 distinct
main-chain models, sidechain exclusion (even when the sidechain is the last
record), and model-absent → null propagating to deriveCost refuse-to-estimate.

804 -> 807 tests (805 pass / 0 fail / 2 skipped). No test pins the test count.
2026-06-26 17:14:03 +02:00

179 lines
8.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// tests/lib/token-usage.test.mjs
// SKAL-2 Step 1 — pure token-usage parser + cache-aware cost derivation.
//
// Hermetic: no LLM, network, time, or randomness. Fixture transcript exercises
// the dedup-by-requestId (streaming-placeholder) and isSidechain-exclusion
// paths that were verified against a real local transcript at execute time.
// Idiom: tests/lib/coordinator-contract.test.mjs (pure-module node:test style).
import { test } from 'node:test';
import { strict as assert } from 'node:assert';
import { readFileSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import {
parseTranscriptUsage,
deriveCost,
buildRecord,
upsertSessionRecord,
lastMainChainModel,
PRICE_TABLE,
PRICE_TABLE_VERSION,
} from '../../lib/stats/token-usage.mjs';
const HERE = dirname(fileURLToPath(import.meta.url));
const FIXTURE = join(HERE, '..', 'fixtures', 'token-usage', 'sample-transcript.jsonl');
// ---- parseTranscriptUsage --------------------------------------------------
test('parseTranscriptUsage — sums main-chain final-per-requestId; excludes sidechain + placeholder dup', () => {
const text = readFileSync(FIXTURE, 'utf-8');
const totals = parseTranscriptUsage(text);
// req_A: placeholder {1,1,0,0} then real {1000,200,400,5000} → LAST wins.
// req_B: appears twice identically → dedup → counted once {2000,300,600,1000}.
// req_SIDE (isSidechain:true, all 9999) → EXCLUDED. user/system/malformed → skipped.
assert.deepEqual(totals, {
tokens_input: 3000, // 1000 + 2000 (NOT 1 — placeholder dropped)
tokens_output: 500, // 200 + 300
tokens_cache_creation: 1000, // 400 + 600
tokens_cache_read: 6000, // 5000 + 1000
});
// Sidechain 9999s must not leak into any field.
assert.ok(totals.tokens_input < 9999, 'sidechain tokens leaked into total');
});
test('parseTranscriptUsage — empty / nullish input → zeroed totals', () => {
const zero = { tokens_input: 0, tokens_output: 0, tokens_cache_creation: 0, tokens_cache_read: 0 };
assert.deepEqual(parseTranscriptUsage(''), zero);
assert.deepEqual(parseTranscriptUsage(null), zero);
assert.deepEqual(parseTranscriptUsage(undefined), zero);
});
test('parseTranscriptUsage — single-pass O(n) shape on a 500-line transcript', () => {
// 500 distinct-requestId main-chain assistant records, each input_tokens:10.
const lines = [];
for (let i = 0; i < 500; i++) {
lines.push(JSON.stringify({
type: 'assistant',
isSidechain: false,
requestId: `req_${i}`,
message: { model: 'claude-opus-4-8', usage: { input_tokens: 10, output_tokens: 0, cache_creation_input_tokens: 0, cache_read_input_tokens: 0 } },
}));
}
const totals = parseTranscriptUsage(lines.join('\n'));
assert.equal(totals.tokens_input, 5000); // 500 × 10
});
// ---- deriveCost ------------------------------------------------------------
test('deriveCost — hand-computed value for a known model (cache-aware)', () => {
const totals = { tokens_input: 3000, tokens_output: 500, tokens_cache_creation: 1000, tokens_cache_read: 6000 };
const { cost_usd, is_estimate } = deriveCost(totals, 'claude-opus-4-8');
// (3000×5 + 500×25 + 1000×6.25 + 6000×0.5) / 1e6
// = (15000 + 12500 + 6250 + 3000) / 1e6 = 36750 / 1e6 = 0.03675
assert.equal(cost_usd, 0.03675);
assert.equal(is_estimate, false);
});
test('deriveCost — refuse-to-estimate for an unknown model', () => {
const totals = { tokens_input: 3000, tokens_output: 500, tokens_cache_creation: 1000, tokens_cache_read: 6000 };
const out = deriveCost(totals, 'claude-unknown-9');
assert.equal(out.cost_usd, null);
assert.equal(out.is_estimate, true);
});
test('PRICE_TABLE — frozen, opus-4-8 carries all five rate fields', () => {
assert.ok(Object.isFrozen(PRICE_TABLE), 'PRICE_TABLE must be frozen (drift-pin)');
const p = PRICE_TABLE['claude-opus-4-8'];
for (const k of ['input', 'output', 'cache_read', 'cache_write_5m', 'cache_write_1h']) {
assert.equal(typeof p[k], 'number', `missing rate field ${k}`);
}
});
// ---- buildRecord -----------------------------------------------------------
test('buildRecord — stamps scope:main-context + price_table_version + flat numerics', () => {
const totals = { tokens_input: 3000, tokens_output: 500, tokens_cache_creation: 1000, tokens_cache_read: 6000 };
const rec = buildRecord({ sessionId: 'S1', model: 'claude-opus-4-8', totals, now: '2026-06-26T12:00:00Z' });
assert.equal(rec.scope, 'main-context');
assert.equal(rec.price_table_version, PRICE_TABLE_VERSION);
assert.equal(rec.ts, '2026-06-26T12:00:00Z');
assert.equal(rec.session_id, 'S1');
assert.equal(rec.model, 'claude-opus-4-8');
assert.equal(rec.tokens_input, 3000);
assert.equal(rec.cost_usd, 0.03675);
assert.equal(rec.is_estimate, false);
});
test('buildRecord — unknown model → cost_usd null + is_estimate true (honesty contract)', () => {
const rec = buildRecord({ sessionId: 'S9', model: 'mystery', totals: { tokens_input: 1 }, now: 't' });
assert.equal(rec.cost_usd, null);
assert.equal(rec.is_estimate, true);
});
// ---- upsertSessionRecord ---------------------------------------------------
test('upsertSessionRecord — REPLACES a same-session line (not append)', () => {
const existing =
JSON.stringify({ session_id: 'S1', tokens_input: 1, scope: 'main-context' }) + '\n' +
JSON.stringify({ session_id: 'S2', tokens_input: 2, scope: 'main-context' }) + '\n';
const rec = buildRecord({ sessionId: 'S1', model: 'claude-opus-4-8', totals: { tokens_input: 999 }, now: 't' });
const out = upsertSessionRecord(existing, rec);
const recs = out.trim().split('\n').map(JSON.parse);
assert.equal(recs.length, 2, 'replace must NOT add a line');
const s1 = recs.find(r => r.session_id === 'S1');
assert.equal(s1.tokens_input, 999, 'S1 must carry the new totals');
assert.ok(recs.some(r => r.session_id === 'S2'), 'S2 must be preserved');
});
test('upsertSessionRecord — APPENDS a new-session line', () => {
const existing = JSON.stringify({ session_id: 'S1', tokens_input: 1 }) + '\n';
const rec = buildRecord({ sessionId: 'S3', model: 'claude-opus-4-8', totals: { tokens_input: 7 }, now: 't' });
const out = upsertSessionRecord(existing, rec);
const recs = out.trim().split('\n').map(JSON.parse);
assert.equal(recs.length, 2, 'new session must append');
assert.ok(recs.some(r => r.session_id === 'S3' && r.tokens_input === 7));
});
test('upsertSessionRecord — empty file → single record, trailing newline', () => {
const rec = buildRecord({ sessionId: 'S1', model: 'claude-opus-4-8', totals: { tokens_input: 5 }, now: 't' });
const out = upsertSessionRecord('', rec);
assert.ok(out.endsWith('\n'));
assert.deepEqual(out.trim().split('\n').map(JSON.parse).length, 1);
});
// ---- lastMainChainModel ----------------------------------------------------
test('lastMainChainModel — last main-chain model wins; sidechain excluded even when last', () => {
// Two distinct main-chain models (opus then sonnet) → sonnet (last) wins.
// A sidechain record sits AFTER sonnet: if exclusion broke it would win;
// if last-wins broke, opus (first) would win. Correct answer pins both.
const text = [
JSON.stringify({ type: 'assistant', isSidechain: false, message: { model: 'claude-opus-4-8', usage: {} } }),
JSON.stringify({ type: 'assistant', isSidechain: false, message: { model: 'claude-sonnet-4-6', usage: {} } }),
JSON.stringify({ type: 'assistant', isSidechain: true, message: { model: 'claude-sidechain-ZZZ', usage: {} } }),
JSON.stringify({ type: 'user', message: { content: 'ignored' } }),
].join('\n');
assert.equal(lastMainChainModel(text), 'claude-sonnet-4-6');
});
test('lastMainChainModel — no main-chain model → null → deriveCost refuses to estimate', () => {
// Only a sidechain model, a user line, and a malformed line: no main-chain model.
const text = [
JSON.stringify({ type: 'assistant', isSidechain: true, message: { model: 'claude-opus-4-8', usage: {} } }),
JSON.stringify({ type: 'user', message: { content: 'hi' } }),
'not json',
].join('\n');
const model = lastMainChainModel(text);
assert.equal(model, null);
// null model must propagate to refuse-to-estimate (honesty contract).
const { cost_usd, is_estimate } = deriveCost({ tokens_input: 1000 }, model);
assert.equal(cost_usd, null);
assert.equal(is_estimate, true);
});
test('lastMainChainModel — empty / nullish input → null', () => {
assert.equal(lastMainChainModel(''), null);
assert.equal(lastMainChainModel(null), null);
assert.equal(lastMainChainModel(undefined), null);
});