voyage/tests/lib/token-usage.test.mjs

142 lines
6.7 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,
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);
});