/* * THE TRAINING SET, AND THE FOUR WAYS A TRAINING SET SILENTLY RUINS A MODEL. * * Every test here is one of them: * * - a credential leaves with the data, because redaction was going to be a later step; * - the label comes from the agent's own report, so the model learns to declare success; * - the evaluation split is cut by TURN, so turns from the same job sit on both sides and every * measurement afterwards flatters the model; * - one loud role drowns the rest, and the model becomes a notification sweeper. * * None of these fail loudly. They all produce a model that looks fine and is not. */ import { describe, it, expect } from 'vitest'; import { build, turnsOf, toJsonl, scrubText, scrubValue } from '../src/traceset.js'; const job = (over = {}) => ({ id: 'j-1', role: 'research.web', profile: 'google', goal: 'Find three suppliers', createdAt: '2026-09-01T10:00:00.000Z', endedAt: '2026-09-01T10:10:00.000Z', status: 'idle', report: 'Found three suppliers.', error: '', proposals: [], leads: [], steps: [ { n: 1, kind: 'you', text: 'Find three suppliers' }, { n: 2, kind: 'tool', tool: 'google', args: { query: 'suppliers' } }, { n: 3, kind: 'read', text: 'google "suppliers" — 8 results', url: 'https://www.google.com/search?q=suppliers' }, { n: 4, kind: 'tool', tool: 'open', args: { url: 'https://example.com' } }, { n: 5, kind: 'read', text: 'Example — contact us at sales@example.com or +32 496 31 12 12' }, { n: 6, kind: 'tool', tool: 'finish', args: { summary: 'done' } }, ], ...over, }); describe('nothing sensitive leaves with the data', () => { it('redacts the things actually found in this corpus', () => { /* 533 of 2,222 job files contain an email address; one holds a Search Console token and one a Facebook CSRF pair. Those are the real contents, so those are the tests. */ expect(scrubText('write to sales@example.com')).toBe('write to '); expect(scrubText('ya29.a0AfB_veryLongTokenValue')).toBe(''); expect(scrubText('key gb_42c3f0aabbccdd')).toBe('key '); expect(scrubText('Authorization: Bearer abcdefghijklmnop')).toContain('Bearer '); expect(scrubText('call +32 496 31 12 12 now')).toBe('call now'); expect(scrubText('{"fb_dtsg":"NAcOabcdef123"}')).toBe('{"fb_dtsg":""}'); }); it('drops identity and plumbing fields rather than carrying them along', () => { const v = scrubValue({ owner: 'someone@x.com', companyId: 'c1', sessionId: 's1', gscToken: 'abc', keep: 'yes' }); expect(v).toEqual({ keep: 'yes' }); }); it('redacts by key name too, because a token does not arrive under a helpful label', () => { expect(scrubValue({ apiKey: 'zzzz', password: 'hunter2', cookie: 'a=b' })) .toEqual({ apiKey: '', password: '', cookie: '' }); }); it('scrubs inside a tool call\'s arguments, where a person pastes things', () => { const t = turnsOf(job({ steps: [ { n: 1, kind: 'tool', tool: 'type', args: { text: 'my email is carla@example.com' } }, ], })); expect(t[0].action.args.text).toBe('my email is '); }); it('does not recurse forever on a self-referential record', () => { const a = { name: 'x' }; a.self = a; expect(() => scrubValue(a)).not.toThrow(); }); }); describe('a turn is a decision, not a page dump', () => { it('pairs what was seen with the tool that was called next', () => { const t = turnsOf(job()); expect(t).toHaveLength(3); expect(t[0].action.tool).toBe('google'); expect(t[0].observed).toEqual([]); // nothing seen before the first call expect(t[1].action.tool).toBe('open'); expect(t[1].observed[0].text).toContain('8 results'); // it saw the search result first }); it('caps the observation text, so the set teaches acting and not reading', () => { const long = 'x'.repeat(5000); const t = turnsOf(job({ steps: [ { n: 1, kind: 'read', text: long }, { n: 2, kind: 'tool', tool: 'open', args: {} }, ] }), { maxObs: 100 }); expect(t[0].observed[0].text.length).toBe(100); }); it('keeps only the recent history, because a fortieth step does not need the first', () => { const steps = []; for (let i = 1; i <= 20; i++) steps.push({ n: i, kind: 'read', text: `obs ${i}` }); steps.push({ n: 21, kind: 'tool', tool: 'open', args: {} }); const t = turnsOf(job({ steps }), { maxHistory: 4 }); expect(t[0].observed).toHaveLength(4); expect(t[0].observed[3].text).toBe('obs 20'); }); it('produces nothing from a job with no goal', () => { expect(turnsOf(job({ goal: '' }))).toEqual([]); }); }); describe('the label comes from verifiers, never from the report', () => { it('a glowing report with a failed check is bronze and is left out', () => { /* It claimed a file. None appeared. The report says otherwise, and the report does not win. */ const bad = job({ report: 'Exported the video successfully.', steps: [ { n: 1, kind: 'tool', tool: 'download_url', args: { url: 'https://x/v.mp4' } }, { n: 2, kind: 'read', text: 'saved' }, ], }); const out = build([bad], { files: () => [] }); expect(out.manifest.tiers.bronze).toBe(1); expect(out.manifest.claimsCaught.fileWasProduced).toBe(1); expect(out.train).toHaveLength(0); }); it('an approved act is gold, and gold is kept', () => { const good = job({ proposals: [{ pid: 'p1', state: 'approved' }] }); const out = build([good], {}); expect(out.manifest.tiers.gold).toBe(1); expect(out.train.length + out.eval.length).toBeGreaterThan(0); }); it('counts what the verifiers caught, because that is the number worth watching', () => { const lied = job({ steps: [{ n: 1, kind: 'tool', tool: 'type', args: { text: 'a sentence that never lands' } }, { n: 2, kind: 'read', text: 'a completely different page' }], }); expect(build([lied], {}).manifest.claimsCaught.typedTextLanded).toBe(1); }); }); describe('a click by number needs the list the number came from', () => { /* * Every run recorded before look and open began carrying their marks drew a numbered list and * threw it away. The turn that survives reads: given a page you cannot see, emit 19. The * rebuild of 2026-09-23 had 3,073 of these and 12 that were learnable. */ const clicked = (marks) => job({ proposals: [{ pid: 'p1', state: 'approved' }], steps: [ { n: 1, kind: 'you', text: 'Find three suppliers' }, { n: 2, kind: 'tool', tool: 'look', args: {} }, { n: 3, kind: 'look', text: 'the page', ...(marks ? { marks } : {}) }, { n: 4, kind: 'tool', tool: 'click', args: { index: 19 } }, { n: 5, kind: 'read', text: 'the listing' }, ], }); it('throws away an index whose list was never recorded', () => { const out = build([clicked(null)], {}); const kept = [...out.train, ...out.eval]; expect(kept.some((x) => x.action.tool === 'click')).toBe(false); expect(out.manifest.droppedTurns['an index with no list to read it from']).toBeGreaterThan(0); }); it('keeps the same turn once the list travels with it', () => { const out = build([clicked('[19] Website laten maken | incl. hosting')], {}); const kept = [...out.train, ...out.eval]; expect(kept.some((x) => x.action.tool === 'click' && x.action.args.index === 19)).toBe(true); }); it('leaves calls that name what they want alone, once the page they came from is on record', () => { /* A url carries its own subject - but only if the read it was chosen from is in the record. The fixture's reads carry content here; without it the same turn is a blind decision. */ const sighted = job({ proposals: [{ pid: 'p1', state: 'approved' }] }); for (const s of sighted.steps) if (s.kind === 'read') s.content = 'Example Ltd - sales@example.com - +32 496 31 12 12 - https://example.com/contact'; const out = build([sighted], {}); const kept = [...out.train, ...out.eval]; expect(kept.some((x) => x.action.tool === 'open')).toBe(true); }); }); describe('a decision needs the page it was made from', () => { /* * The finding behind both failed rounds. 58% of a training slice followed a read whose content * was never recorded - "read the page (14592 characters)" and nothing else - and those turns' * answers were dig 284, open 215, run_script 213, finish 114: the exact tools that scored zero. * Both rounds collapsed onto look. Same rule as the index with no list, one level up. */ const blind = () => job({ proposals: [{ pid: 'p1', state: 'approved' }], steps: [ { n: 1, kind: 'you', text: 'Find three suppliers' }, { n: 2, kind: 'tool', tool: 'open', args: { url: 'https://example.com' } }, { n: 3, kind: 'read', text: 'read the page (14592 characters)' }, { n: 4, kind: 'tool', tool: 'open', args: { url: 'https://example.com/suppliers' } }, { n: 5, kind: 'read', text: 'read the page (2210 characters)' }, ], }); it('drops a decision that followed a read whose content was never recorded', () => { const out = build([blind()], {}); const kept = [...out.train, ...out.eval].filter((x) => x.action.tool === 'open' && x.action.args.url.endsWith('/suppliers')); expect(kept).toHaveLength(0); expect(out.manifest.droppedTurns['a decision with no page to read it from']).toBeGreaterThan(0); }); it('keeps the same decision once the page travels with it', () => { const j = blind(); j.steps[2].content = 'Example Ltd. Suppliers: https://example.com/suppliers. Contact: sales@example.com'; const out = build([j], {}); const kept = [...out.train, ...out.eval].filter((x) => x.action.tool === 'open' && x.action.args.url.endsWith('/suppliers')); expect(kept).toHaveLength(1); expect(kept[0].observed[kept[0].observed.length - 1].content).toMatch(/suppliers/); }); it('never drops the opening move, which is decided from the goal alone', () => { /* What do you reach for when you know nothing yet - the most informative turn there is, and one that by definition has no page to be blind to. */ const out = build([blind()], {}); const first = [...out.train, ...out.eval].filter((x) => x.action.tool === 'open' && x.action.args.url === 'https://example.com'); expect(first).toHaveLength(1); }); }); describe('the exam does not pay for what training spent', () => { /* * The per-tool cap counter was shared between the training pass and the evaluation pass, and * training is built first. Measured on the real set of 2026-09-23: `open` is 15% of training * and the most common action there is, and the eval split held ZERO of it — 0 of 3,057 turns, * 1,518 dropped to that rule, 30 tools on the exam against 49 in training. The model was never * scored on the thing it does most. */ it('still scores a tool that training used its whole quota of', () => { const jobs = []; for (let i = 0; i < 40; i++) { jobs.push(job({ id: `j-${i}`, proposals: [{ pid: `p${i}`, state: 'approved' }], goal: `open a lot of pages ${i}`, steps: [ { n: 1, kind: 'you', text: `open a lot of pages ${i}` }, { n: 2, kind: 'tool', tool: 'open', args: { url: `https://a${i}.example` } }, { n: 3, kind: 'read', text: `page ${i}` }, { n: 4, kind: 'tool', tool: 'open', args: { url: `https://b${i}.example` } }, { n: 5, kind: 'read', text: `other ${i}` }, ], })); } /* A cap low enough that the training pass exhausts it outright. */ const out = build(jobs, {}, { toolCap: 2 }); expect(out.train.filter((x) => x.action.tool === 'open').length).toBe(2); expect(out.eval.length).toBeGreaterThan(0); expect(out.eval.some((x) => x.action.tool === 'open')).toBe(true); }); }); describe('the evaluation split is cut by job, before any turn exists', () => { it('never puts two turns from the same job on both sides', () => { const jobs = []; for (let i = 0; i < 60; i++) jobs.push(job({ id: `j-${i}`, goal: `find suppliers batch ${i}`, proposals: [{ state: 'approved' }] })); const out = build(jobs, {}, { evalFraction: 0.3 }); const trainJobs = new Set(out.train.map((t) => t.jobId)); const evalJobs = new Set(out.eval.map((t) => t.jobId)); for (const id of evalJobs) expect(trainJobs.has(id)).toBe(false); }); it('is deterministic, so a rebuild does not quietly move the exam', () => { const jobs = []; for (let i = 0; i < 40; i++) jobs.push(job({ id: `j-${i}`, goal: `find suppliers batch ${i}`, proposals: [{ state: 'approved' }] })); const a = build(jobs, {}, { evalFraction: 0.25 }).eval.map((t) => t.jobId); const b = build(jobs, {}, { evalFraction: 0.25 }).eval.map((t) => t.jobId); expect(a).toEqual(b); }); it('actually holds some back', () => { /* Distinct goals, because real jobs have them — and the dedupe key includes the goal, so a fixture of byte-identical jobs collapses to one turn and measures nothing. */ const jobs = []; for (let i = 0; i < 80; i++) jobs.push(job({ id: `j-${i}`, goal: `find suppliers batch ${i}`, proposals: [{ state: 'approved' }] })); const out = build(jobs, {}, { evalFraction: 0.2 }); expect(out.eval.length).toBeGreaterThan(0); expect(out.train.length).toBeGreaterThan(out.eval.length); }); }); describe('no single role is allowed to drown the rest', () => { it('caps per role, because 292 notification sweeps against 84 screenshots is not a balance', () => { const jobs = []; for (let i = 0; i < 200; i++) jobs.push(job({ id: `loud-${i}`, goal: `sweep notifications ${i}`, role: 'facebook-notification-watch', proposals: [{ state: 'approved' }] })); for (let i = 0; i < 20; i++) jobs.push(job({ id: `quiet-${i}`, goal: `take a shot ${i}`, role: 'learn.shot', proposals: [{ state: 'approved' }] })); const out = build(jobs, {}, { perRoleCap: 30, evalFraction: 0 }); const byRole = {}; out.train.forEach((t) => { byRole[t.role] = (byRole[t.role] || 0) + 1; }); /* Turns, not jobs — but the loud role must no longer be ten times the quiet one. */ expect(out.manifest.roles['facebook-notification-watch']).toBeGreaterThan(30); const loudJobs = new Set(out.train.filter((t) => t.role === 'facebook-notification-watch').map((t) => t.jobId)); expect(loudJobs.size).toBe(30); }); }); describe('the output is what a trainer eats', () => { it('is one JSON object per line, with the tool call as the assistant turn', () => { const out = build([job({ proposals: [{ state: 'approved' }] })], {}, { evalFraction: 0 }); const lines = toJsonl(out.train).split('\n'); expect(lines.length).toBe(out.train.length); const first = JSON.parse(lines[0]); expect(first.messages).toHaveLength(3); expect(first.messages[2].role).toBe('assistant'); expect(JSON.parse(first.messages[2].content).tool).toBe('google'); expect(first.meta.tier).toBe('gold'); }); it('carries the goal and the role into the prompt, since that is what a specialist needs', () => { const out = build([job({ proposals: [{ state: 'approved' }] })], {}, { evalFraction: 0 }); const first = JSON.parse(toJsonl(out.train).split('\n')[0]); expect(first.messages[0].content).toContain('research.web'); expect(first.messages[1].content).toContain('Find three suppliers'); }); it('survives an empty corpus without pretending it built something', () => { const out = build([], {}); expect(out.train).toEqual([]); expect(out.manifest.jobsSeen).toBe(0); }); }); describe('a verdict per step', () => { /* A wrong click inside a gold run used to be labelled gold. The record says otherwise right after the call, and that is the label. */ const step = (kind, text, extra = {}) => ({ kind, text, at: 'x', ...extra }); /* A report, so the run is silver rather than void - a void run never reaches the filters. */ const job = () => ({ id: 'j-sv', goal: 'Find the price', role: 'general', profile: 'default', status: 'done', report: 'The price is 12 euro.', steps: [ step('you', 'Find the price'), step('tool', 'open(https://a.org)', { tool: 'open', args: { url: 'https://a.org' } }), step('open', 'https://a.org'), step('tool', 'run_script()', { tool: 'run_script', args: { code: 'location.href=1' } }), step('blocked', 'run_script refused: a script may not navigate — use open'), step('tool', 'look()', { tool: 'look', args: {} }), step('look', 'A — 3 things to click', { marks: '[1] a\n[2] b\n[3] c' }), step('tool', 'choose_option(2)', { tool: 'choose_option', args: { index: 2, value: 'x' } }), step('blocked', 'choose_option [2]: no option matches'), step('tool', 'click(1)', { tool: 'click', args: { index: 1 } }), step('click', 'clicked [1]'), step('tool', 'finish()', { tool: 'finish', args: { summary: 'done' } }), step('done', 'done'), ] }); it('marks the refused calls wrong and leaves the rest unknown', () => { const turns = turnsOf(job()); expect(turns.map((t) => `${t.action.tool}:${t.step}`)).toEqual(['open:unknown', 'run_script:wrong', 'look:unknown', 'choose_option:wrong', 'click:unknown', 'finish:unknown']); expect(turns[1].stepWhy).toBe('the call was refused'); }); it('keeps a wrong step out of the training set and puts it on the reject pile, marked', () => { const out = build([job()], { files: () => [], recordings: () => [], runs: () => null }, { evalFraction: 0 }); const trainTools = out.train.map((t) => t.action.tool); expect(trainTools).not.toContain('run_script'); expect(trainTools).not.toContain('choose_option'); expect(out.manifest.droppedTurns['a step the record shows was wrong']).toBe(2); const wrong = out.reject.filter((t) => (t.failed || []).some((f) => /^step:/.test(f))); expect(wrong.map((t) => t.action.tool).sort()).toEqual(['choose_option', 'run_script']); expect(out.manifest.wrongSteps).toBe(2); const line = JSON.parse(toJsonl(out.train).split('\n')[0]); expect(line.meta.step).toBe('unknown'); }); });