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.
179 lines
8.5 KiB
JavaScript
179 lines
8.5 KiB
JavaScript
// 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);
|
||
});
|