Files
model-router/tests/router.test.ts
T
adminandClaude Opus 5.5 05f9663129 Fix TOOL-02: read verifier verdicts with prose around them
The verdict reader matched from the first { to the last } in the whole
reply, so a fenced JSON verdict followed by prose containing a } failed
to parse, counted as FAIL and triggered a rewrite (SAFETY-129, T192).

- Read a ```json fenced block first, then the first balanced {...} that
  parses and has a PASS or FAIL verdict. Braces inside strings are
  ignored.
- An unreadable reply is re-asked once for the JSON only. Still
  unreadable: NOT VERIFIED, "verdict unreadable", handed back to the
  manager with the start of the first reply. No fail counted, no
  resume, no rewrite.
- 10 new tests; each new rule was also broken on purpose and caught.

Version 0.4.5.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MdqPWvuDpbpRvDEdURoAwo
2026-11-07 00:45:56 +00:00

538 lines
25 KiB
TypeScript

import type { On } from 'claude-code'
import { expect, mock, test, type Engine } from 'claude-code/testing'
import {
decide,
heuristic,
onFail,
parseBrief,
parseVerdict,
rewriteTier,
skipReason,
validTimeout,
verifierFor,
} from '../hooks/router'
const DEV = '/home/homelab/projects/goodwe/clones/SAFETY-95-dev'
const REVIEW = '/home/homelab/projects/goodwe/clones/SAFETY-95-review'
const TEST = '/home/homelab/projects/goodwe/clones/TEST-23-test'
// ---------- the brief (C2) ----------
test('brief: clone path, ticket, role and base branch are read', async () => {
const b = parseBrief(`Work in ${DEV}/ only. Base branch: release/1.0.\nExpected: ...`)
expect(b).toEqual({ clone: DEV, ticket: 'SAFETY-95', role: 'dev', baseBranch: 'release/1.0' })
expect(parseBrief(`Clone: \`${TEST}\``).role).toBe('test')
expect(parseBrief('PR against `release/1.1`, no clone given').baseBranch).toBe('release/1.1')
expect(parseBrief('just look around').clone).toBe(undefined)
})
test('fix 1: tickets with extra parts parse, the role is the last part', async () => {
const p = '/home/homelab/projects/goodwe/clones/OPT-11-P4-dev'
expect(parseBrief(`Work in ${p}/ only.`)).toMatchObject({ clone: p, ticket: 'OPT-11-P4', role: 'dev' })
expect(parseBrief('Clone: /x/clones/OPT-11-P4-R2-test/').ticket).toBe('OPT-11-P4-R2')
expect(parseBrief('Clone: /x/clones/OPT-11-P4-R2-test/').role).toBe('test')
})
test('fix 2: the Clone: line decides, whatever the brief mentions first', async () => {
const brief = `Verify the work of ${DEV}/ on the rig.\nClone: ${TEST}/\nBase branch: release/1.0.`
expect(parseBrief(brief)).toMatchObject({ clone: TEST, role: 'test', baseBranch: 'release/1.0' })
// an explicit role wins over the path
expect(parseBrief(`Clone: \`/x/clones/OPT-11-P4\` (Role: review)`)).toMatchObject({ clone: '/x/clones/OPT-11-P4', role: 'review' })
// (Role: ...) wins over the role in the path
expect(parseBrief(`Clone: ${DEV}/ (Role: review)`).role).toBe('review')
// a list item or quote marker in front of the line is fine
expect(parseBrief(`- Clone: ${DEV}`).role).toBe('dev')
// "Clone:" inside a sentence is not the line, even at a line's end
expect(parseBrief(`Results are in the dev agent's Clone: ${DEV}\nRun the rig in ${TEST}/`).isAmbiguous).toBe(true)
})
test('fix 2: several clones and no Clone: line gives no role, and routes to opus', async () => {
const b = parseBrief(`Compare ${DEV}/ with ${TEST}/.`)
expect(b.role).toBe(undefined)
expect(b.isAmbiguous).toBe(true)
expect(decide({ prompt: 'x', respectExplicit: false, isAmbiguous: true, named: 'haiku' })).toMatchObject({ kind: 'fixed', tier: 'opus' })
expect(decide({ prompt: 'x', respectExplicit: false, isAmbiguous: true, named: 'sonnet' })).toMatchObject({ tier: 'opus' })
expect(decide({ prompt: 'x', respectExplicit: false, isAmbiguous: true, named: 'fable' })).toMatchObject({ tier: 'fable' })
// the same path twice is one clone, not ambiguous
expect(parseBrief(`Work in ${DEV}/. Commit in ${DEV}.`).role).toBe('dev')
})
// ---------- who picks the writer (C4) ----------
test('C4: review and test agents keep the model they are given', async () => {
for (const role of ['review', 'test'] as const)
for (const named of ['haiku', 'sonnet', 'opus'] as const)
expect(decide({ role, named, prompt: 'x', respectExplicit: false })).toEqual({
kind: 'fixed',
tier: named,
reason: `named model kept for ${role} agent`,
})
})
test('C4: unnamed review and test agents run on opus', async () => {
expect(decide({ role: 'test', prompt: 'run the rig', respectExplicit: false })).toMatchObject({ tier: 'opus' })
expect(decide({ role: 'review', prompt: 'review', respectExplicit: false })).toMatchObject({ tier: 'opus' })
})
test('C4: dev agents are routed even when opus is named, and that is logged', async () => {
const d = decide({ role: 'dev', named: 'opus', prompt: 'Expected: clamp to 2000 W', respectExplicit: true })
expect(d.kind).toBe('classify')
expect(d.opusRequested).toBe(true)
expect(decide({ role: 'dev', named: 'sonnet', prompt: 'x', respectExplicit: true }).opusRequested).toBe(false)
})
test('C4: respectExplicit only applies to agents without a role', async () => {
expect(decide({ named: 'haiku', prompt: 'x', respectExplicit: true })).toMatchObject({ kind: 'fixed', tier: 'haiku' })
expect(decide({ named: 'haiku', prompt: 'x', respectExplicit: false }).kind).toBe('classify')
})
test('C4: open design goes to opus, a decided brief is left to the classifier', async () => {
expect(heuristic(undefined, 'MAINT-13: decide how the staleness guard should recover', 'dev')?.tier).toBe('opus')
expect(heuristic(undefined, 'In the asyncio control loop, clamp the integrator; Expected: test_clamp passes', 'dev')).toBe(undefined)
})
test('C4: narrowed light patterns: "docs" or "run tests" in a brief never mean haiku', async () => {
expect(heuristic(undefined, 'Update the docs and run the tests after changing the setpoint logic')).toBe(undefined)
expect(heuristic(undefined, 'Find where the MQTT discovery payload is built')?.tier).toBe('haiku')
// a dev brief never takes the light path, however light it reads
expect(heuristic(undefined, 'Find where the MQTT discovery payload is built', 'dev')).toBe(undefined)
expect(heuristic('Explore', 'anything', 'dev')).toBe(undefined)
})
// ---------- what gets verified (C1, C5) ----------
test('only finished dev rounds below opus with a clone are verified', async () => {
expect(skipReason({ role: 'dev', clone: DEV, tier: 'sonnet' }, 'answer')).toBe(undefined)
expect(skipReason({ role: 'dev', clone: DEV, tier: 'haiku' }, 'answer')).toBe(undefined)
expect(skipReason({ role: 'dev', clone: DEV, tier: 'opus' }, 'answer')).toBe('opus-unverified')
expect(skipReason({ role: 'dev', tier: 'sonnet' }, 'answer')).toBe('no-clone')
expect(skipReason({ role: 'test', clone: TEST, tier: 'sonnet' }, 'answer')).toBe('test-agent')
expect(skipReason({ role: 'review', clone: REVIEW, tier: 'sonnet' }, 'answer')).toBe('review-agent')
expect(skipReason({ role: 'dev', clone: DEV, tier: 'sonnet' }, 'error')).toBe('agent-ended')
expect(skipReason({ role: 'dev', clone: DEV, tier: 'sonnet' }, 'aborted')).toBe('agent-ended')
})
test('the verifier is opus below opus, and never the writer', async () => {
expect(verifierFor('haiku')).toBe('opus')
expect(verifierFor('sonnet')).toBe('opus')
for (const w of ['haiku', 'sonnet', 'opus', 'fable'] as const) expect(verifierFor(w)).not.toBe(w)
})
test('C5: first fail resumes, second starts a stronger agent, then hand back', async () => {
expect(onFail(1, 'sonnet', 2, false)).toEqual({ kind: 'resume' })
expect(onFail(2, 'sonnet', 2, false)).toEqual({ kind: 'new-agent', tier: 'opus' })
expect(onFail(3, 'opus', 2, false)).toEqual({ kind: 'hand-back' })
expect(onFail(2, 'opus', 2, false)).toEqual({ kind: 'hand-back' }) // nothing above opus
expect(onFail(2, 'opus', 2, true)).toEqual({ kind: 'new-agent', tier: 'fable' })
expect(onFail(1, 'sonnet', 0, false)).toEqual({ kind: 'hand-back' })
expect(rewriteTier('haiku', false)).toBe('sonnet')
})
// ---------- reading the verdict (TOOL-02) ----------
// The SAFETY-129 reply shape: a fenced block, then prose that contains a `}`.
const SAFETY_129 =
'Checked the diff against the brief.\n\n```json\n{"verdict": "PASS", "issues": [], "rewrite_brief": ""}\n```\n\n' +
'Notes: the guard keeps `if (stale) { hold() }` as before, and the new test covers it.'
test('TOOL-02: fenced JSON followed by prose with a } reads as PASS', async () => {
expect(parseVerdict(SAFETY_129)).toEqual({ isReadable: true, pass: true, issues: [], brief: '' })
})
test('TOOL-02: the fenced answer wins over an earlier object in the prose', async () => {
const r = parseVerdict('Last round said {"verdict":"FAIL","issues":["x"]}. Fixed now:\n```json\n{"verdict":"PASS","issues":[]}\n```')
expect(r).toMatchObject({ isReadable: true, pass: true })
})
test('TOOL-02: bare JSON reads, with or without prose around it', async () => {
expect(parseVerdict('{"verdict":"PASS","issues":[]}')).toMatchObject({ isReadable: true, pass: true })
expect(parseVerdict('Verdict below. {"verdict": "pass"} That is all {really}.')).toMatchObject({ isReadable: true, pass: true })
})
test('TOOL-02: FAIL keeps its issues and brief', async () => {
const r = parseVerdict('```json\n{"verdict":"FAIL","issues":["loop.py:40 no clamp"],"rewrite_brief":"clamp it"}\n```')
expect(r).toEqual({ isReadable: true, pass: false, issues: ['loop.py:40 no clamp'], brief: 'clamp it' })
})
test('TOOL-02: braces inside strings do not end the object', async () => {
const r = parseVerdict('{"verdict":"FAIL","issues":["handler } has a stray brace","quote \\" and }"]} trailing {x}')
expect(r).toMatchObject({ isReadable: true, pass: false })
if (r.isReadable) expect(r.issues).toEqual(['handler } has a stray brace', 'quote " and }'])
})
test('TOOL-02: the first object with a verdict wins; other objects are skipped', async () => {
const r = parseVerdict('Config seen: {"limit": 2000}. Verdict: {"verdict":"FAIL","issues":["x"]} and later {"verdict":"PASS"}')
expect(r).toMatchObject({ isReadable: true, pass: false })
})
test('TOOL-02: a fence without a verdict falls through to the text', async () => {
const r = parseVerdict('```json\n{"limit": 2000}\n```\nResult: {"verdict":"PASS"}')
expect(r).toMatchObject({ isReadable: true, pass: true })
})
test('TOOL-02: garbage, prose and odd verdicts are unreadable, never a FAIL', async () => {
for (const t of ['looks fine', 'verdict: pass', '{"verdict":"MAYBE"}', '{"issues":[]}', '{broken json', '']) {
const r = parseVerdict(t)
expect(r.isReadable).toBe(false)
}
const long = parseVerdict('x'.repeat(500))
expect(long.isReadable ? '' : long.preview).toBe('x'.repeat(300))
})
// ---------- the hooks, with the engine mocked beneath ----------
type World = {
spawned: { model?: string; prompt: string }[]
verifier: string[]
verdicts: string[]
told: string[]
diffs: string[]
diffBases: string[]
testCwd: string[]
testArgv: string[][]
/** How the next test run ends: exit code (124 = timed out), or a throw. */
testEnds: (number | 'throw')[]
/** Whether `timeout` fired on each test run (it then says so on stderr). */
testFired: boolean[]
/** The timeout line arrives in two stderr pieces. */
testSplitLine?: boolean
/** A test itself prints the timeout line, on stdout. */
testLineOnStdout?: boolean
snap: number
}
const USAGE = { input_tokens: 1, output_tokens: 1, cache_creation_input_tokens: 0, cache_read_input_tokens: 0 }
const ok = (stdout: string, exitCode = 0) => ({
value: { exitCode, stdout, stderr: '', isStdoutTruncated: false, isStderrTruncated: false },
})
function world(on: On, w: World) {
mock.store(on)
const clock = mock.clock(on)
on('ui.status', async () => ({ value: undefined }))
on('ui.toast', async () => ({ value: undefined }))
on('model.complete', async (_$, e) => {
if (e.system?.startsWith('You route')) return { value: { isAnswered: true as const, text: 'sonnet', usage: USAGE } }
w.verifier.push(e.model)
return { value: { isAnswered: true as const, text: w.verdicts.shift() ?? '{"verdict":"PASS"}', usage: USAGE } }
})
on('process.run', async (_$, e) => {
const a = e.argv.join(' ')
const cwd = String(e.init?.cwd ?? '')
if (cwd !== DEV) return ok('', 128) // not a git repo: the session folder
if (a === 'git merge-base origin/release/1.0 HEAD') return ok('mergebase\n')
if (a === 'git stash create') return ok(`snap${++w.snap}\n`)
if (a.startsWith('git diff ')) {
w.diffBases.push(e.argv[2] ?? '')
return ok(w.diffs.shift() ?? '')
}
return ok('')
})
on('process.spawn', async function* (_$, e) {
w.testCwd.push(String(e.cwd ?? ''))
w.testArgv.push([...e.argv])
const end = w.testEnds.shift() ?? 0
const fired = w.testFired.shift() ?? false
if (end === 'throw') throw new Error('docker: not found')
yield { stream: 'stdout' as const, text: end === 0 ? '67 passed\n' : 'test_modes_p4 ... still running\n' }
if (fired) yield { stream: 'stderr' as const, text: "timeout: sending signal TERM to command 'sh'\n" }
if (w.testSplitLine) {
yield { stream: 'stderr' as const, text: 'timeout: sending sig' }
yield { stream: 'stderr' as const, text: "nal TERM to command 'sh'\n" }
}
if (w.testLineOnStdout) yield { stream: 'stdout' as const, text: 'assert "timeout: sending signal" in log\n' }
return { value: { code: end, signal: null } }
})
on('agent.spawn', async (_$, e) => {
w.spawned.push({ model: e.model, prompt: e.prompt })
return { model: e.model ?? 'inherit', agentId: `agent-${w.spawned.length}` }
})
on('turn.complete', async (_$, e) => ({ text: e.answer }))
on('prompt.submit', async (_$, e) => {
w.told.push(e.text)
return { text: e.text }
})
return clock
}
const fresh = (): World => ({ spawned: [], verifier: [], verdicts: [], told: [], diffs: [], diffBases: [], testCwd: [], testArgv: [], testEnds: [], testFired: [], snap: 0 })
async function finish($: Engine, clock: { advance: (ms: number) => Promise<void> }, agentId: string) {
await ($.turn as unknown as { complete: (e: unknown) => Promise<unknown> }).complete({ answer: 'done', durationMs: 1, isAborted: false, turnId: 't', agentId, reason: 'answer' })
await clock.advance(1)
}
const devBrief = (extra = '') => `SAFETY-95: clamp the integrator. Work in ${DEV}/. Base branch: release/1.0.\nExpected: test_clamp passes.${extra}`
test('acceptance 1+2: a background dev agent is verified in its clone, each round on its own diff', { options: { testCommand: 'pytest -q' } }, async ($, on) => {
const w = fresh()
w.diffs = ['diff --git a/loop.py', 'diff --git a/loop.py (round 2)']
w.verdicts = ['{"verdict":"FAIL","issues":["loop.py:40 no clamp on negative"]}', '{"verdict":"PASS"}']
const clock = world(on, w)
const r = await $.agent.spawn({ prompt: devBrief(), description: 'clamp', model: 'opus' } as never)
expect(w.spawned[0]?.model).toBe('sonnet') // routed despite the named opus
await finish($, clock, r.agentId as string)
expect(w.diffBases[0]).toBe('mergebase') // first round: from the merge base, in the clone
expect(w.testCwd).toEqual([DEV])
expect(w.verifier).toEqual(['opus'])
expect(w.told[0]).toContain('FAIL')
// round 2: the resumed agent finishes again (same agentId)
await finish($, clock, r.agentId as string)
expect(w.diffBases[1]).toBe('snap1') // only what this round added
expect(w.told[1]).toContain('PASS')
})
test('acceptance 3: review and test agents keep a named opus; dev briefs below opus are verified by opus', async ($, on) => {
const w = fresh()
const clock = world(on, w)
await $.agent.spawn({ prompt: `Review PR. Clone ${REVIEW}/`, description: 'review', model: 'opus' } as never)
await $.agent.spawn({ prompt: `Run the rig test. Clone ${TEST}/`, description: 'rig', model: 'opus' } as never)
const t = await $.agent.spawn({ prompt: `Run the rig test. Clone ${TEST}/`, description: 'rig2' } as never)
expect(w.spawned.map(s => s.model)).toEqual(['opus', 'opus', 'opus'])
await finish($, clock, t.agentId as string)
expect(w.verifier).toEqual([]) // test agents are not verified
expect(w.told).toEqual([])
})
test('acceptance 4: the first FAIL asks to resume the same agent', async ($, on) => {
const w = fresh()
w.diffs = ['diff']
w.verdicts = ['{"verdict":"FAIL","issues":["x"]}']
const clock = world(on, w)
const r = await $.agent.spawn({ prompt: devBrief(), description: 'clamp' } as never)
await finish($, clock, r.agentId as string)
expect(w.told[0]).toContain(`Resume agent ${r.agentId} with SendMessage`)
expect(w.told[0]).not.toContain('[router-rewrite')
})
test('C5: the second FAIL starts a stronger agent in the same clone', async ($, on) => {
const w = fresh()
w.diffs = ['d1', 'd2']
w.verdicts = ['{"verdict":"FAIL","issues":["x"]}', '{"verdict":"FAIL","issues":["y"]}']
const clock = world(on, w)
const r = await $.agent.spawn({ prompt: devBrief(), description: 'clamp' } as never)
await finish($, clock, r.agentId as string)
await finish($, clock, r.agentId as string)
const id = w.told[1]?.match(/\[router-rewrite (T\d+)\]/)?.[1]
expect(id).toBeDefined()
await $.agent.spawn({ prompt: `[router-rewrite ${id}]`, description: 'rewrite' } as never)
expect(w.spawned[1]?.model).toBe('opus')
expect(w.spawned[1]?.prompt).toContain(`Their work is in the clone ${DEV}: continue from it`)
expect(w.spawned[1]?.prompt).toContain('- y')
})
test('C2: a dev brief with no clone is never diffed in the session folder', async ($, on) => {
const w = fresh()
const clock = world(on, w)
const r = await $.agent.spawn({ prompt: 'Clamp the integrator in clones/ somewhere. Expected: ...', description: 'x' } as never)
await finish($, clock, r.agentId as string)
expect(w.diffBases).toEqual([])
expect(w.verifier).toEqual([])
})
test('fix 2 end to end: a test brief that mentions the dev clone first keeps its model and is not verified', async ($, on) => {
const w = fresh()
const clock = world(on, w)
const r = await $.agent.spawn({
prompt: `Run the OPT-11 P4 rig test against the work in ${DEV}/.\nClone: /home/homelab/projects/goodwe/clones/OPT-11-P4-test/`,
description: 'rig',
model: 'opus',
} as never)
expect(w.spawned[0]?.model).toBe('opus')
await finish($, clock, r.agentId as string)
expect(w.verifier).toEqual([])
expect(w.diffBases).toEqual([])
})
test('fix 2 end to end: two clones and no Clone: line run on opus, unverified', async ($, on) => {
const w = fresh()
const clock = world(on, w)
const r = await $.agent.spawn({ prompt: `Compare ${DEV}/ with ${TEST}/ and report.`, description: 'cmp', model: 'haiku' } as never)
expect(w.spawned[0]?.model).toBe('opus')
await finish($, clock, r.agentId as string)
expect(w.verifier).toEqual([])
})
test('a brief without a role is routed but never tracked or verified', async ($, on) => {
const w = fresh()
const clock = world(on, w)
const r = await $.agent.spawn({ prompt: 'Find where the MQTT discovery payload is built', description: 'look' } as never)
expect(w.spawned[0]?.model).toBe('haiku')
await finish($, clock, r.agentId as string)
expect(w.verifier).toEqual([])
expect(w.told).toEqual([])
})
// ---------- the test run ----------
test('tests stream with a real time limit, not the ten-minute command cap', { options: { testCommand: 'make verifier-tests', testTimeoutSec: 1800 } }, async ($, on) => {
const w = fresh()
w.diffs = ['diff']
const clock = world(on, w)
const r = await $.agent.spawn({ prompt: devBrief(), description: 'clamp' } as never)
await finish($, clock, r.agentId as string)
expect(w.testArgv[0]).toEqual(['timeout', '--verbose', '--kill-after=30', '1800', 'sh', '-c', 'make verifier-tests'])
expect(w.testCwd).toEqual([DEV])
expect(w.verifier).toEqual(['opus'])
})
test('a test timeout is NOT VERIFIED, never a FAIL: no verifier call, no resume, no fail counted', { options: { testCommand: 'make t', testTimeoutSec: 60 } }, async ($, on) => {
const w = fresh()
w.diffs = ['diff', 'diff 2']
w.testEnds = [124, 0]
w.testFired = [true, false]
w.verdicts = ['{"verdict":"FAIL","issues":["x"]}']
const clock = world(on, w)
const r = await $.agent.spawn({ prompt: devBrief(), description: 'clamp' } as never)
await finish($, clock, r.agentId as string)
expect(w.verifier).toEqual([])
expect(w.told[0]).toContain('NOT VERIFIED - the tests did not finish within 60 s')
expect(w.told[0]).not.toContain('Resume agent')
// the next round's FAIL is the first one counted: it resumes, it does not rewrite
await finish($, clock, r.agentId as string)
expect(w.told[1]).toContain('Resume agent')
})
test('a kill after the grace period also counts as a timeout', { options: { testCommand: 'make t' } }, async ($, on) => {
const w = fresh()
w.diffs = ['diff']
w.testEnds = [137]
w.testFired = [true]
const clock = world(on, w)
const r = await $.agent.spawn({ prompt: devBrief(), description: 'clamp' } as never)
await finish($, clock, r.agentId as string)
expect(w.verifier).toEqual([])
expect(w.told[0]).toContain('did not finish')
})
test('a test command that cannot run (exit 127) is NOT VERIFIED, not a FAIL', { options: { testCommand: 'make t' } }, async ($, on) => {
const w = fresh()
w.diffs = ['diff']
w.testEnds = [127]
const clock = world(on, w)
const r = await $.agent.spawn({ prompt: devBrief(), description: 'clamp' } as never)
await finish($, clock, r.agentId as string)
expect(w.verifier).toEqual([])
expect(w.told[0]).toContain('could not run')
})
test('a test command that cannot run (a start failure) is NOT VERIFIED, not a FAIL', { options: { testCommand: 'make t' } }, async ($, on) => {
const w = fresh()
w.diffs = ['diff']
w.testEnds = ['throw' as const]
const clock = world(on, w)
const r = await $.agent.spawn({ prompt: devBrief(), description: 'clamp' } as never)
await finish($, clock, r.agentId as string)
expect(w.verifier).toEqual([])
expect(w.told[0]).toContain('could not run')
})
test('failing tests are evidence: the verifier sees them and decides', { options: { testCommand: 'make t' } }, async ($, on) => {
const w = fresh()
w.diffs = ['diff']
w.testEnds = [1]
const clock = world(on, w)
const r = await $.agent.spawn({ prompt: devBrief(), description: 'clamp' } as never)
await finish($, clock, r.agentId as string)
expect(w.verifier).toEqual(['opus'])
})
test('an early 137 (out of memory) is a failed run for the verifier, not a timeout', { options: { testCommand: 'make t', testTimeoutSec: 1800 } }, async ($, on) => {
const w = fresh()
w.diffs = ['diff']
w.testEnds = [137]
w.testFired = [false]
const clock = world(on, w)
const r = await $.agent.spawn({ prompt: devBrief(), description: 'clamp' } as never)
await finish($, clock, r.agentId as string)
expect(w.verifier).toEqual(['opus'])
expect(w.told[0] ?? '').not.toContain('NOT VERIFIED')
})
test('an invalid testTimeoutSec falls back to 1800', async () => {
for (const bad of [0, -5, Number.NaN, 'abc', undefined, null]) expect(validTimeout(bad)).toBe(1800)
expect(validTimeout(900)).toBe(900)
expect(validTimeout('600')).toBe(600)
})
test('a command that exits 124 by itself is a failed run, not a timeout', { options: { testCommand: 'make t' } }, async ($, on) => {
const w = fresh()
w.diffs = ['diff']
w.testEnds = [124]
w.testFired = [false]
const clock = world(on, w)
const r = await $.agent.spawn({ prompt: devBrief(), description: 'clamp' } as never)
await finish($, clock, r.agentId as string)
expect(w.verifier).toEqual(['opus'])
})
test('the timeout line still counts when it arrives split across pieces', { options: { testCommand: 'make t', testTimeoutSec: 60 } }, async ($, on) => {
const w = fresh()
w.diffs = ['diff']
w.testEnds = [124]
w.testSplitLine = true
const clock = world(on, w)
const r = await $.agent.spawn({ prompt: devBrief(), description: 'clamp' } as never)
await finish($, clock, r.agentId as string)
expect(w.verifier).toEqual([])
expect(w.told[0]).toContain('did not finish')
})
test('the timeout line on stdout (a test printing it) is not a fired timeout', { options: { testCommand: 'make t' } }, async ($, on) => {
const w = fresh()
w.diffs = ['diff']
w.testEnds = [1]
w.testLineOnStdout = true
const clock = world(on, w)
const r = await $.agent.spawn({ prompt: devBrief(), description: 'clamp' } as never)
await finish($, clock, r.agentId as string)
expect(w.verifier).toEqual(['opus'])
})
// ---------- TOOL-02 end to end ----------
test('TOOL-02: the SAFETY-129 reply passes the round, no rewrite', async ($, on) => {
const w = fresh()
w.diffs = ['diff']
w.verdicts = [SAFETY_129]
const clock = world(on, w)
const r = await $.agent.spawn({ prompt: devBrief(), description: 'clamp' } as never)
await finish($, clock, r.agentId as string)
expect(w.verifier).toEqual(['opus'])
expect(w.told[0]).toContain('PASS from verifier opus')
})
test('TOOL-02: an unreadable reply is asked again once, and a readable answer then counts', async ($, on) => {
const w = fresh()
w.diffs = ['diff']
w.verdicts = ['I think it is fine overall.', '{"verdict":"FAIL","issues":["x"]}']
const clock = world(on, w)
const r = await $.agent.spawn({ prompt: devBrief(), description: 'clamp' } as never)
await finish($, clock, r.agentId as string)
expect(w.verifier).toEqual(['opus', 'opus'])
expect(w.told[0]).toContain('Resume agent')
})
test('TOOL-02: unreadable twice is handed back, no fail counted, no rewrite', async ($, on) => {
const w = fresh()
w.diffs = ['diff', 'diff 2']
w.verdicts = ['Looks good to me.', 'Still prose, sorry.', '{"verdict":"FAIL","issues":["y"]}']
const clock = world(on, w)
const r = await $.agent.spawn({ prompt: devBrief(), description: 'clamp' } as never)
await finish($, clock, r.agentId as string)
expect(w.verifier).toEqual(['opus', 'opus'])
expect(w.told[0]).toContain('NOT VERIFIED - verdict unreadable')
expect(w.told[0]).toContain('Start of its reply: Looks good to me.')
expect(w.told[0]).not.toContain('[router-rewrite')
expect(w.told[0]).not.toContain('Resume agent')
// the next round's FAIL is the first one counted: it resumes, it does not rewrite
await finish($, clock, r.agentId as string)
expect(w.told[1]).toContain('Resume agent')
expect(w.told[1]).not.toContain('[router-rewrite')
})