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
538 lines
25 KiB
TypeScript
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')
|
|
})
|