()
| 950 | // answers. It lives here rather than in a test file on purpose — a new |
| 951 | // scripts/agent-eval/*.mjs scores into the self-query eval fixture's corpus. |
| 952 | function selftest() { |
| 953 | const { writeFileSync, mkdtempSync } = require0('fs'); |
| 954 | const { join } = require0('path'); |
| 955 | const { tmpdir } = require0('os'); |
| 956 | const dir = mkdtempSync(join(tmpdir(), 'cg-occ-')); |
| 957 | let n = 0, failures = 0; |
| 958 | const check = (name, got, want, tol) => { |
| 959 | n++; |
| 960 | const ok = Math.abs(got - want) <= tol; |
| 961 | if (!ok) failures++; |
| 962 | console.log(`${ok ? ' ok ' : ' FAIL'} ${name}: got ${Math.round(got)}, want ${want} ±${tol}`); |
| 963 | }; |
| 964 | // Builders for the event shapes Claude Code actually emits. |
| 965 | const req = (ctx, id, blocks) => blocks.map((b) => JSON.stringify({ |
| 966 | type: 'assistant', |
| 967 | message: { id, content: [b], usage: { input_tokens: ctx, cache_read_input_tokens: 0, cache_creation_input_tokens: 0, output_tokens: 2 } }, |
| 968 | })); |
| 969 | const use = (id, name, input = {}) => ({ type: 'tool_use', id, name, input }); |
| 970 | const res = (id, chars) => JSON.stringify({ |
| 971 | type: 'user', message: { content: [{ type: 'tool_result', tool_use_id: id, content: [{ type: 'text', text: 'x'.repeat(chars) }] }] }, |
| 972 | }); |
| 973 | const done = () => JSON.stringify({ type: 'result', subtype: 'success', duration_ms: 1000, total_cost_usd: 0.1, usage: {} }); |
| 974 | const write = (name, lines) => { const f = join(dir, name); writeFileSync(f, lines.join('\n') + '\n'); return f; }; |
| 975 | |
| 976 | // 1. Attribution: ratio 2.5 chars/tok, two families, no shedding. |
| 977 | // 10,000 explore chars over a 4,000-tok gap; 5,000 Read chars over 2,000. |
| 978 | let f = write('basic.jsonl', [ |
| 979 | ...req(10000, 'm1', [use('t1', 'mcp__codegraph__codegraph_explore')]), |
| 980 | res('t1', 10000), |
| 981 | ...req(14000, 'm2', [use('t2', 'Read')]), |
| 982 | res('t2', 5000), |
| 983 | ...req(16000, 'm3', [{ type: 'text', text: 'done' }]), |
| 984 | done(), |
| 985 | ]); |
| 986 | let o = parseSession([f]).occupancy; |
| 987 | check('chars/token', o.charsPerToken * 1000, 2500, 30); |
| 988 | check('codegraph residual', o.residual.codegraph, 4000, 60); |
| 989 | check('Read residual', o.residual.read, 2000, 40); |
| 990 | check('file-access residual', o.residualFileAccess, 2000, 40); |
| 991 | check('final context', o.ctxFinal, 16000, 0); |
| 992 | check('fixed base', o.ctxBase, 10000, 0); |
| 993 | check('nothing evicted', o.evicted, 0, 1); |
| 994 | |
| 995 | // 2. Dedupe: thinking + tool_use are two events sharing one id and one usage. |
| 996 | // Counting usage per event would report 5 requests instead of 3. |
| 997 | f = write('dupe.jsonl', [ |
| 998 | ...req(10000, 'm1', [{ type: 'thinking', thinking: '' }, use('t1', 'mcp__codegraph__codegraph_explore')]), |
| 999 | res('t1', 10000), |
| 1000 | ...req(14000, 'm2', [{ type: 'thinking', thinking: '' }, use('t2', 'Read')]), |
| 1001 | res('t2', 5000), |
| 1002 | ...req(16000, 'm3', [{ type: 'text', text: 'done' }]), |
| 1003 | done(), |
| 1004 | ]); |
| 1005 | let s = parseSession([f]); |
| 1006 | check('turns deduped by message.id', s.turns, 3, 0); |
| 1007 | check('codegraph residual (deduped)', s.occupancy.residual.codegraph, 4000, 60); |
| 1008 | |
| 1009 | // 3. Compaction: the boundary clears everything resident before it. |
no test coverage detected