import { test, expect, mock } from 'claude-code/testing' import type { Engine, MockClock, Plugin } from 'claude-code/testing' import type { On, TurnStepInput } from 'claude-code' const FABLE = 'claude-fable-5-1' const OPUS = 'claude-opus-5-5' const SONNET = 'claude-sonnet-5-5' const HAIKU = 'claude-haiku-4-5-20251001' type Rec = Record type Asked = { text: string; options: string[]; header: string } const HOME = '/home/t' const HOME_FILE = `${HOME}/.claude/model-router.json` const ROOT = '/work/app' const REAL_ROOT = '/real/work/app' const REMOTE = 'https://github.com/acme/app.git' const KEY = 'github.com/acme/app' const REJECT = '' // an answer the dialog never gets: headless // A Change answered to the end: model, effort (no scope yet). /feat is on // reflect (best, high); implement is the first work/medium phase, apply // the work/low one, judge the only big one (opus has one effort: skipped). const TO_IMPLEMENT = ['Change', 'sonnet (work)', 'medium'] const TO_APPLY = ['Change', 'sonnet (work)', 'low'] const TO_JUDGE = ['Change', 'opus (big)'] const SCOPE_ASK = 3 // index of the scope question after a Change chain /** What the bottom `fs`, `env`, `session.repo` and dialog hooks serve. */ type World = { files: Map // 'routing' (routing.json) or a full path reads: string[] // every path fs.read got written: string[] // every path fs.write got asked: Asked[] // the dialogs, in order answers: string[] // queued dialog answers (REJECT = headless) gate: Promise | undefined // holds a dialog open until it settles writeGate: Promise | undefined // holds an fs.write open failWrite: boolean // fs.write rejects onAsk: (() => void) | undefined // called when a dialog opens onWrite: (() => void) | undefined // called when an fs.write starts repo: { root: string; remote: string | null } | null | 'fail' env: string | undefined // HOME as env.get answers it (undefined = unset) kinds: Map // per-path stat kind override } type Fixture = { ask?: boolean // routing.json `ask` (default false: no dialog) edit?: (file: Rec) => void // tweaks the inline routing.json routing?: 'absent' | 'broken' home?: Rec | 'broken' // ~/.claude/model-router.json answers?: string[] repo?: World['repo'] noHome?: boolean // HOME unset: env.get answers undefined } /** * The phases as shipped and the rows the tests use; the real table is * locked by the census. */ const baseRouting = (): Rec => ({ phases: { plan: { tier: 'best', effort: 'xhigh' }, reflect: { tier: 'best', effort: 'high' }, orchestrate: { tier: 'best', effort: 'medium' }, escalate: { tier: 'best', effort: 'max' }, judge: { tier: 'big', effort: 'xhigh' }, implement: { tier: 'work', effort: 'medium' }, write: { tier: 'work', effort: 'high' }, verify: { tier: 'work', effort: 'xhigh' }, explore: { tier: 'work', effort: 'medium' }, apply: { tier: 'work', effort: 'low' }, mechanical: { tier: 'cheap', effort: 'low' }, }, skills: { feat: 'reflect', status: 'mechanical', gitflow: 'implement', 'using-git-worktrees': 'mechanical', }, agents: { Explore: 'explore', 'plan-challenger': 'judge', feater: 'implement', verifier: 'verify', }, projects: {}, confirmed: {}, changed: {}, ask: false, }) function newWorld(fx: Fixture = {}): World { const file = baseRouting() file.ask = fx.ask ?? false fx.edit?.(file) const files = new Map() if (fx.routing === 'broken') files.set('routing', '{nope') else if (fx.routing !== 'absent') { files.set('routing', JSON.stringify(file, null, 2)) } if (fx.home !== undefined) { files.set(HOME_FILE, fx.home === 'broken' ? '{nope' : JSON.stringify(fx.home)) } return { files, reads: [], written: [], asked: [], answers: [...(fx.answers ?? [])], gate: undefined, writeGate: undefined, failWrite: false, onAsk: undefined, onWrite: undefined, repo: fx.repo === undefined ? { root: ROOT, remote: REMOTE } : fx.repo, env: fx.noHome ? undefined : HOME, kinds: new Map(), } } const slot = (path: string): string => { if (path.endsWith('/routing.json')) return 'routing' // the engine resolves a relative path against its cwd: `undefined/...` // (a HOME read as unset and not guarded) comes back absolute const rel = /\/(undefined\/\.claude\/.*)$/.exec(path) return rel?.[1] ?? path } /** routing.json as the world holds it now. */ const routingOf = (w: World): Rec => JSON.parse(w.files.get('routing') ?? '{}') as Rec /** * A path-aware fs, HOME, project root and repo: the kit has none (an fs call * no hook answers is refused, never sent to the disk: see "writes: kit"). */ function installWorld(on: On, w: World): void { on('env.get', () => ({ value: w.env })) on('fs.exists', (_a, e) => ({ value: w.files.has(slot(e.path)) || e.path === ROOT, })) on('fs.stat', (_a, e) => { const text = w.files.get(slot(e.path)) if (text === undefined && e.path !== ROOT) throw new Error('ENOENT') return { value: { kind: w.kinds.get(e.path) ?? (text === undefined ? 'dir' as const : 'file' as const), size: text?.length ?? 0, mtimeMs: 0, isLink: false, ...(e.resolve ? { realPath: REAL_ROOT } : {}), }, } }) on('fs.read', (_a, e) => { w.reads.push(e.path) const text = w.files.get(slot(e.path)) if (text === undefined) throw new Error('ENOENT') return { value: text } }) installWrite(on, w) on('session.root', () => ({ value: ROOT })) on('session.repo', () => { if (w.repo === 'fail') throw new Error('no repo') return { value: w.repo === null ? null : { ...w.repo, internal: false, name: null, } } }) on('classic.CwdChanged', () => ({})) installDialog(on, w) } /** * The world's fs.write. Loud: a write anywhere but the routing.json the mod * read fails the test (never a project tree, never the machine override). */ function installWrite(on: On, w: World): void { on('fs.write', async (_a, e) => { expect(slot(e.path) === 'routing' && w.reads.includes(e.path), `the world never served ${e.path}`).toBe(true) w.written.push(e.path) w.onWrite?.() await w.writeGate if (w.failWrite) throw new Error('EACCES') w.files.set(slot(e.path), e.text) return { value: undefined } }) } /** The AskUserQuestion tool beneath `$.ui.ask`: records, then answers. */ function installDialog(on: On, w: World): void { on('tool.call', { tool: 'AskUserQuestion' }, async (_a, e) => { const q = (e as unknown as { questions: Rec[] }).questions[0] ?? {} const options = (q.options as { label: string }[] | undefined) ?? [] w.asked.push({ text: String(q.question), options: options.map(o => o.label), header: String(q.header), }) w.onAsk?.() await w.gate const answer = w.answers.shift() ?? REJECT if (answer === REJECT) return { deny: 'no one to ask' } return { result: { questions: e.questions, answers: { [String(q.question)]: answer }, } } as never }) } /** * Fires session.start so the mod loads its config and registers /route. * The bottom hooks the router reads are in place before the first `$` call: * the session model, the classic failure and switch events, a mock clock, * a small context (`tokens`; null = usage unanswered, size unknown), and * the `world` (fs, repo, dialog) serving an inline routing.json. */ async function boot( $: Engine, on: On, model: string = FABLE, tokens: number | null = 1000, world: World = newWorld(), ): Promise { const clock = mock.clock(on) on('session.model', () => ({ value: model })) on('classic.StopFailure', () => ({})) on('classic.PostModelSwitch', () => ({})) on('skill.prompt', ($, e) => ({ text: e.text })) on('agent.offer', () => ({ isOffered: true })) installWorld(on, world) if (tokens !== null) usageOf(on, tokens) on('session.start', ($, e) => ({ cwd: e.cwd })) await $.session.start({ cwd: '/tmp', surface: null, isInteractive: false }) return clock } /** Runs `/route ` as the user would type it. */ async function route($: Engine, args: string): Promise { const out = await $.command.run({ command: 'route', args, origin: { kind: 'composer' }, presentation: { isFullscreen: false, columns: 80 }, }) return out.text ?? '' } /** The `main:` line of `show`: the phases listing holds every id and level. */ function mainLine(text: string): string { return text.split('\n').find(l => l.startsWith('main:')) ?? '' } const spawnInput = (model?: string) => ({ tool_use_id: 't1', prompt: 'x', description: 'd', subagentType: 'Explore', provider: { plugin: 'engine', tier: 'core' as const }, parentModel: 'claude-fable-5-1', background: false, fork: false, ...(model === undefined ? {} : { model }), }) test('route tool with phase orchestrate sets medium on main', async ( $, on) => { await boot($, on) const out = await $.tool.call({ tool: 'mcp__model-router__route', phase: 'orchestrate', }) expect(out).toHaveProperty('result') const line = mainLine(await route($, 'show')) expect(line).toContain('model orchestrate') expect(line).toContain('effort medium') }) test('/route clear drops the route', async ($, on) => { await boot($, on) await $.tool.call({ tool: 'mcp__model-router__route', phase: 'plan' }) expect(mainLine(await route($, 'show'))).toContain('model plan') await route($, 'clear') expect(mainLine(await route($, 'show'))).toContain('session defaults') }) test('/route bogus names the phases', async ($, on) => { await boot($, on) const text = await route($, 'bogus') expect(text).toContain('unknown') for (const phase of ['plan', 'judge', 'explore', 'mechanical']) { expect(text).toContain(phase) } }) test('ultrathink in a prompt sets a max floor on main', async ($, on) => { on('prompt.submit', ($, e) => ({ text: e.text })) await boot($, on) await $.prompt.submit({ text: 'ultrathink please', wait: false, origin: { kind: 'composer' }, }) expect(mainLine(await route($, 'show'))).toContain('floor max') }) test('/route model: alias resolved, id passed, typo refused', async ( $, on) => { await boot($, on) const alias = await route($, 'model=sonnet') expect(mainLine(alias)).toContain('asked claude-sonnet-5-5, keeps ') const id = await route($, 'model=claude-x-9') expect(mainLine(id)).toContain('claude-x-9') expect(await route($, 'model=sonet')).toContain('unknown') }) test('Explore spawn gets the sonnet id; an explicit model wins', async ( $, on) => { const seen: (string | undefined)[] = [] on('agent.spawn', ($, e) => { seen.push(e.model) return { model: e.model ?? e.parentModel, agentId: 'a1' } }) await boot($, on) await $.agent.spawn(spawnInput()) await $.agent.spawn(spawnInput('opus')) expect(seen).toEqual(['claude-sonnet-5-5', 'opus']) }) test('a sticky /route wins over a model-declared route', async ($, on) => { await boot($, on) await route($, 'judge') const out = await $.tool.call({ tool: 'mcp__model-router__route', phase: 'mechanical', }) expect(JSON.stringify(out)).toContain('sticky') expect(mainLine(await route($, 'show'))).toContain('user judge') }) test('/route off makes the route tool a no-op', async ($, on) => { await boot($, on) await route($, 'off') const out = await $.tool.call({ tool: 'mcp__model-router__route', phase: 'plan', }) expect(JSON.stringify(out)).toContain('off') expect(mainLine(await route($, 'show'))).toContain('session defaults') }) type Seen = { model: string; effort: unknown } /** A bottom turn.step hook that records what reaches the model. */ function recordSteps(on: On, seen: Seen[]): void { on('turn.step', async function* ($, e) { seen.push({ model: e.model, effort: e.effort }) return { turnId: e.turnId, index: e.index, answer: '', toolUses: [], stopReason: 'end_turn' as const, usage: null, } }) } const stepInput = (agentId?: string) => ({ turnId: 'u1', index: 0, model: 'claude-fable-5-1', effort: 'low' as const, messageCount: 1, ...(agentId === undefined ? {} : { agentId }), }) /** Streams one turn.step to its end; the hooks run as the chunks flow. */ async function runStep( $: Engine, input: TurnStepInput, ): Promise { const stream = $.turn.step(input) for await (const _chunk of stream) { // chunks are not under test } await stream.result } test('the main step carries the routed effort, model untouched', async ( $, on) => { const seen: Seen[] = [] recordSteps(on, seen) await boot($, on) await $.tool.call({ tool: 'mcp__model-router__route', phase: 'plan' }) await runStep($, stepInput()) expect(seen).toEqual([{ model: 'claude-fable-5-1', effort: 'xhigh' }]) }) test('a tabled agent steps at its table effort', async ($, on) => { const seen: Seen[] = [] recordSteps(on, seen) on('agent.spawn', ($, e) => ({ model: e.model ?? e.parentModel, agentId: 'a1', })) await boot($, on) await $.agent.spawn(spawnInput()) await runStep($, stepInput('a1')) expect(seen).toEqual([{ model: 'claude-fable-5-1', effort: 'medium' }]) }) test('/route from a non-composer origin is refused, state kept', async ( $, on) => { await boot($, on) await route($, 'judge') const out = await $.command.run({ command: 'route', args: 'mechanical', origin: { kind: 'plugin', name: 'x' }, presentation: { isFullscreen: false, columns: 80 }, }) expect(out.text).toContain('user-only') expect(mainLine(await route($, 'show'))).toContain('user judge') }) test('an in-agent route sets effort only, the model stays', async ($, on) => { const seen: Seen[] = [] recordSteps(on, seen) await boot($, on) await $.tool.call({ tool: 'mcp__model-router__route', phase: 'judge', agentId: 'a1', }) await runStep($, { ...stepInput('a1'), model: 'claude-sonnet-5-5' }) expect(seen).toEqual([{ model: 'claude-sonnet-5-5', effort: 'xhigh' }]) }) test('a rule only scans the first 4096 chars of a prompt', async ($, on) => { on('prompt.submit', ($, e) => ({ text: e.text })) await boot($, on) await $.prompt.submit({ text: 'x'.repeat(5000) + ' ultrathink', wait: false, origin: { kind: 'composer' }, }) expect(mainLine(await route($, 'show'))).toContain('session defaults') }) // ---- user effort floor ------------------------------------------------- const ROUTE_TOOL = 'mcp__model-router__route' /** A main (or agent) step as the engine would make it: engine effort high. */ const highStep = (agentId?: string): TurnStepInput => ({ ...stepInput(agentId), effort: 'high' as const, }) /** Types `ultrathink` in the composer, idle or over a running turn. */ async function ultrathink($: Engine, turnId?: string): Promise { await $.prompt.submit({ text: 'ultrathink please', wait: turnId !== undefined, origin: { kind: 'composer' }, ...(turnId === undefined ? {} : { turnId }), }) } /** Ends a main turn: the floor's life is bounded by this event. */ async function endTurn($: Engine): Promise { await $.turn.complete({ turnId: 'u1', answer: '', durationMs: 1, isAborted: false, reason: 'answer', }) } /** One main step; returns the effort that reached the bottom hook. */ async function stepEffort($: Engine, seen: Seen[]): Promise { await runStep($, highStep()) return seen[seen.length - 1]?.effort } /** Boots with a bottom step recorder and prompt/turn hooks in place. */ async function bootFloor($: Engine, on: On): Promise { const seen: Seen[] = [] recordSteps(on, seen) on('prompt.submit', ($, e) => ({ text: e.text })) on('turn.complete', () => ({ text: '' })) on('tool.call', { tool: 'Skill' }, () => ({ result: { success: true, commandName: 'other' }, })) on('agent.spawn', ($, e) => ({ model: e.model ?? e.parentModel, agentId: 'a1', })) await boot($, on) return seen } test('floor: ultrathink survives a model route', async ($, on) => { const seen = await bootFloor($, on) await ultrathink($) await $.tool.call({ tool: ROUTE_TOOL, phase: 'orchestrate' }) expect(await stepEffort($, seen)).toBe('max') }) test('floor: survives a skill load; an unrowed skill keeps the route', async ( $, on) => { const seen = await bootFloor($, on) await ultrathink($) await $.tool.call({ tool: ROUTE_TOOL, phase: 'orchestrate' }) await $.tool.call({ tool: 'Skill', skill: 'other' }) expect(await stepEffort($, seen)).toBe('max') expect(mainLine(await route($, 'show'))).toContain('model orchestrate') }) test('floor: lifts a lower sticky, then ends with the turn', async ( $, on) => { const seen = await bootFloor($, on) await route($, 'effort=low') await ultrathink($) expect(await stepEffort($, seen)).toBe('max') await endTurn($) expect(await stepEffort($, seen)).toBe('low') }) test('floor: main only, an agent step is unaffected', async ($, on) => { const seen = await bootFloor($, on) await $.agent.spawn(spawnInput()) await ultrathink($) await runStep($, highStep('a1')) expect(seen[0]?.effort).toBe('medium') }) test('floor: /route clear removes it', async ($, on) => { const seen = await bootFloor($, on) await ultrathink($) expect(mainLine(await route($, 'show'))).toContain('floor max') await route($, 'clear') expect(await stepEffort($, seen)).toBe('high') expect(mainLine(await route($, 'show'))).not.toContain('floor') }) test('floor: a mid-turn prompt applies now and is kept for the next', async ( $, on) => { const seen = await bootFloor($, on) await ultrathink($, 'u1') expect(await stepEffort($, seen)).toBe('max') await endTurn($) expect(await stepEffort($, seen)).toBe('max') await endTurn($) expect(await stepEffort($, seen)).toBe('high') }) test('floor: the route answer names the floor over a sticky', async ( $, on) => { await bootFloor($, on) await route($, 'effort=low') await ultrathink($) const out = await $.tool.call({ tool: ROUTE_TOOL, phase: 'plan' }) const text = JSON.stringify(out) expect(text).toContain('user floor max (prompt rule escalate)') expect(text).toContain('/route clear') }) // `enabled: false` in ~/.claude/model-router.json has its own layers test; // this one covers the shared off path through `/route off`. test('floor: /route off keeps the floor from routing', async ($, on) => { const seen = await bootFloor($, on) await ultrathink($) await route($, 'off') expect(await stepEffort($, seen)).toBe('high') expect(mainLine(await route($, 'show'))).not.toContain('floor') }) test('per axis: a model-only sticky keeps the turn route effort', async ( $, on) => { const seen = await bootFloor($, on) await route($, 'model=sonnet') await $.tool.call({ tool: ROUTE_TOOL, phase: 'orchestrate' }) expect(await stepEffort($, seen)).toBe('medium') }) // The config-driven `offConfig` is re-applied in the same rebuild; only the // session-only off is reached here (the config-driven one has its own test). test('session.end rebuild drops a session-only /route off', async ( $, on) => { on('session.end', ($, e) => ({ sessionId: e.sessionId })) await boot($, on) await route($, 'off') await $.session.end({ reason: 'clear', sessionId: 's1', resume: { id: 's1' }, }) expect(await route($, 'show')).toContain('router: on') }) // The override breaks after the first load: the previous cfg must stay. test('reload with an unreadable override keeps the previous config', async ( $, on) => { const world = newWorld({ home: {} }) await boot($, on, FABLE, 1000, world) await route($, 'switch on') world.files.set(HOME_FILE, '{nope') const out = await route($, 'reload') expect(out).toContain('switch: on') expect(await route($, 'show')).toContain('switch: on') }) // ---- tiers, breaker, derived phases ------------------------------------- type Rig = { seen: Seen[]; clock: MockClock; specs: (string | undefined)[] } type Failure = 'rate_limit' | 'overloaded' | 'invalid_request' /** Bottom hooks for a routed run, then the boot; `specs` records spawns. */ async function bootRig( $: Engine, on: On, model: string = FABLE, tokens: number | null = 1000, world: World = newWorld(), ): Promise { const seen: Seen[] = [] const specs: (string | undefined)[] = [] recordSteps(on, seen) on('prompt.submit', ($, e) => ({ text: e.text })) on('turn.complete', () => ({ text: '' })) on('agent.spawn', ($, e) => { specs.push(e.model) return { model: e.model ?? e.parentModel, agentId: 'a1' } }) on('tool.call', { tool: 'Agent' }, () => ({ result: { status: 'async_launched' as const, agentId: 'a1', description: 'd', prompt: 'p', outputFile: '/tmp/o', }, })) const clock = await boot($, on, model, tokens, world) return { seen, clock, specs } } /** One main step on `model`; returns what reached the bottom hook. */ async function stepOn($: Engine, rig: Rig, model: string): Promise { await runStep($, { ...highStep(), model }) return rig.seen[rig.seen.length - 1] ?? { model: '', effort: undefined } } const failWith = ($: Engine, error: Failure, agentId?: string) => $.classic.StopFailure({ error, ...(agentId === undefined ? {} : { agent_id: agentId }), }) const switchTo = ( $: Engine, from: string, to: string, source: 'command' | 'auto', ) => $.classic.PostModelSwitch({ from_model: from, to_model: to, requested_model: null, source, context_tokens: 0, prompt_cache_warm: false, cache_ttl: '5m', estimated_cache_write_usd: 0, pricing: 'catalog', }) const planRoute = ($: Engine) => $.tool.call({ tool: ROUTE_TOOL, phase: 'plan' }) /** A bottom `session.usage` answering a context of `tokens`. */ function usageOf(on: On, tokens: number): void { on('session.usage', () => ({ value: { startedAt: 0, context: { window: 1000000, tokens }, rateLimits: [], }, })) } test('tier: a plan route upgrades a haiku session to fable xhigh', async ( $, on) => { const rig = await bootRig($, on, HAIKU) await planRoute($) expect(await stepOn($, rig, HAIKU)).toEqual({ model: FABLE, effort: 'xhigh', }) }) test('tier: show prints the resolved id of each phase and a down line', async ( $, on) => { await bootRig($, on) const text = await route($, 'show') expect(text).toContain('plan=best→claude-fable-5-1/xhigh') expect(text).toContain('judge=big→claude-opus-5-5/xhigh') expect(text).toContain('down: none') }) test('tier: an upgrade is skipped above the context cap', async ($, on) => { const rig = await bootRig($, on, SONNET, 300000) await planRoute($) expect(await stepOn($, rig, SONNET)).toEqual({ model: SONNET, effort: 'xhigh', }) }) test('cap: unknown usage blocks the upgrade', async ($, on) => { const rig = await bootRig($, on, HAIKU, null) await planRoute($) expect((await stepOn($, rig, HAIKU)).model).toBe(HAIKU) }) test('cap: a down model leaves for a better wanted only under the cap', async ( $, on) => { const rig = await bootRig($, on, OPUS, 300000) await planRoute($) await stepOn($, rig, OPUS) await failWith($, 'rate_limit') expect((await stepOn($, rig, OPUS)).model).toBe(SONNET) }) test('breaker: model_not_found on a non-table id marks nothing', async ( $, on) => { const rig = await bootRig($, on) await planRoute($) await stepOn($, rig, 'claude-zz-9') await $.classic.StopFailure({ error: 'model_not_found' }) expect(await route($, 'show')).toContain('down: none') }) test('downgrade: mechanical on fable keeps the model, switch off', async ( $, on) => { const rig = await bootRig($, on) await $.tool.call({ tool: ROUTE_TOOL, phase: 'mechanical' }) expect(await stepOn($, rig, FABLE)).toEqual({ model: FABLE, effort: 'low' }) }) test('downgrade: the switch on moves main to the cheap tier', async ( $, on) => { const rig = await bootRig($, on) await route($, 'switch on') await $.tool.call({ tool: ROUTE_TOOL, phase: 'mechanical' }) const sent = await stepOn($, rig, FABLE) expect(sent.model).toBe(HAIKU) expect(sent.effort).toBeUndefined() }) test('fallback: a rate limit on fable moves plan to opus xhigh', async ( $, on) => { const rig = await bootRig($, on) await planRoute($) expect((await stepOn($, rig, FABLE)).model).toBe(FABLE) await failWith($, 'rate_limit') expect(await stepOn($, rig, FABLE)).toEqual({ model: OPUS, effort: 'xhigh' }) expect(await route($, 'show')).toContain('claude-fable-5-1 for 15 min') await route($, 'reload') expect((await stepOn($, rig, FABLE)).model).toBe(FABLE) }) test('breaker: an invalid_request or a turn error never marks down', async ( $, on) => { const rig = await bootRig($, on) await planRoute($) await stepOn($, rig, FABLE) await failWith($, 'invalid_request') await $.turn.complete({ turnId: 'u1', answer: '', durationMs: 1, isAborted: false, reason: 'error', }) expect((await stepOn($, rig, FABLE)).model).toBe(FABLE) expect(await route($, 'show')).toContain('down: none') }) test('breaker: the hold lapses with the clock, a second one doubles', async ( $, on) => { const rig = await bootRig($, on) await planRoute($) await stepOn($, rig, FABLE) await failWith($, 'overloaded') await rig.clock.advance(15 * 60000) expect(await route($, 'show')).toContain('down: none') await endTurn($) await planRoute($) expect((await stepOn($, rig, FABLE)).model).toBe(FABLE) await failWith($, 'overloaded') expect(await route($, 'show')).toContain('claude-fable-5-1 for 30 min') }) test('breaker: a /model command clears the mark of its target', async ( $, on) => { const rig = await bootRig($, on) await planRoute($) await stepOn($, rig, FABLE) await failWith($, 'rate_limit') expect(await route($, 'show')).toContain('claude-fable-5-1 for') await switchTo($, OPUS, FABLE, 'command') expect(await route($, 'show')).toContain('down: none') expect((await stepOn($, rig, FABLE)).model).toBe(FABLE) }) test('breaker: model_not_found holds until the reload', async ($, on) => { const rig = await bootRig($, on) await planRoute($) await stepOn($, rig, FABLE) await $.classic.StopFailure({ error: 'model_not_found' }) await rig.clock.advance(24 * 3600000) expect(await route($, 'show')).toContain('until reload (model_not_found)') }) test('engine fallback: an auto switch marks the model it left, once', async ( $, on) => { const rig = await bootRig($, on) await switchTo($, FABLE, OPUS, 'auto') const first = await route($, 'show') expect(first).toContain('claude-fable-5-1 for 15 min (engine fallback)') await planRoute($) expect((await stepOn($, rig, OPUS)).model).toBe(OPUS) await rig.clock.advance(60000) await switchTo($, FABLE, OPUS, 'auto') expect(await route($, 'show')).toContain('claude-fable-5-1 for 14 min') }) test('unknown: a session model absent from the table is never switched', async ( $, on) => { const rig = await bootRig($, on) await planRoute($) expect((await stepOn($, rig, 'claude-zz-9')).model).toBe('claude-zz-9') }) test('spawn: Explore goes to opus while sonnet is down', async ($, on) => { const rig = await bootRig($, on) await $.agent.spawn(spawnInput()) await failWith($, 'overloaded', 'a1') await $.agent.spawn(spawnInput()) expect(rig.specs).toEqual([SONNET, OPUS]) }) /** Types a plain prompt, idle, in the composer. */ const typed = ($: Engine, text: string) => $.prompt.submit({ text, wait: false, origin: { kind: 'composer' }, }) const dispatch = ($: Engine) => $.tool.call({ tool: 'Agent', description: 'd', prompt: 'p' }) const agentEnds = ($: Engine) => $.turn.complete({ turnId: 'a1', agentId: 'a1', answer: '', durationMs: 1, isAborted: false, reason: 'answer', }) test('derived: a dispatch pushes orchestrate, the end pops the prompt', async ( $, on) => { await bootRig($, on) await typed($, 'planifie la migration') expect(mainLine(await route($, 'show'))).toContain('prompt plan') await dispatch($) expect(mainLine(await route($, 'show'))).toContain('derived orchestrate') await agentEnds($) expect(mainLine(await route($, 'show'))).toContain('prompt plan') }) test('derived: a route declared after the dispatch survives the pop', async ( $, on) => { await bootRig($, on) await typed($, 'planifie la migration') await dispatch($) await $.tool.call({ tool: ROUTE_TOOL, phase: 'reflect' }) await agentEnds($) expect(mainLine(await route($, 'show'))).toContain('model reflect') }) test('default rule: planifie sets plan, a later route overrides', async ( $, on) => { const rig = await bootRig($, on, HAIKU) await typed($, 'planifie la migration') expect(await stepOn($, rig, HAIKU)).toEqual({ model: FABLE, effort: 'xhigh' }) await $.tool.call({ tool: ROUTE_TOOL, phase: 'implement' }) const line = mainLine(await route($, 'show')) expect(line).toContain('model implement') expect(line).not.toContain('prompt') }) test('default rule: a typed slash command gets no default rule', async ( $, on) => { await bootRig($, on) await typed($, '/analyze pourquoi ça plante') expect(mainLine(await route($, 'show'))).toContain('session defaults') }) test('per axis: a model-less sticky never hides a turn route tier', async ( $, on) => { const rig = await bootRig($, on, HAIKU) await route($, 'effort=low') await planRoute($) expect(await stepOn($, rig, HAIKU)).toEqual({ model: FABLE, effort: 'low' }) }) // ---- texts follow the decision, from the turn's model ------------------ test('text: effort-only route on a down model says fallback', async ( $, on) => { const rig = await bootRig($, on) await stepOn($, rig, FABLE) await failWith($, 'rate_limit') const out = await $.tool.call({ tool: ROUTE_TOOL, effort: 'low' }) expect(JSON.stringify(out)).toContain(`model ${OPUS} (fallback)`) }) test('text: /route effort= on a down model names the fallback', async ( $, on) => { const rig = await bootRig($, on) await stepOn($, rig, FABLE) await failWith($, 'rate_limit') await route($, 'effort=low') expect(mainLine(await route($, 'show'))).toContain(`${OPUS} (fallback)`) }) test('text: show names the session model after the turn ended', async ( $, on) => { const rig = await bootRig($, on, HAIKU) await planRoute($) expect((await stepOn($, rig, HAIKU)).model).toBe(FABLE) await endTurn($) await route($, 'mechanical') const line = mainLine(await route($, 'show')) expect(line).toContain(`model ${HAIKU}`) expect(line).not.toContain(FABLE) }) test('text: show reflects a /model command to opus', async ($, on) => { const rig = await bootRig($, on) await stepOn($, rig, FABLE) await switchTo($, FABLE, OPUS, 'command') await route($, 'mechanical') const line = mainLine(await route($, 'show')) expect(line).toContain(OPUS) expect(line).not.toContain(FABLE) }) test('text: an unknown session model prints "session model"', async ( $, on) => { await bootRig($, on, 'mystery-model') await route($, 'plan') expect(mainLine(await route($, 'show'))).toContain('keeps session model') }) test('text: show names the model a floor upgrade moves to', async ( $, on) => { await bootRig($, on, HAIKU) await $.prompt.submit({ text: 'ultrathink please', wait: false, origin: { kind: 'composer' }, }) const line = mainLine(await route($, 'show')) expect(line).toContain(`${FABLE} (upgrade)`) }) // ---- wave 2: skill rows, run slot, agent rows --------------------------- const skillPrompt = ($: Engine, skill: string) => $.skill.prompt({ skill, text: 'x' }) const loadSkill = ($: Engine, skill: string, agentId?: string) => $.tool.call({ tool: 'Skill', skill, ...(agentId === undefined ? {} : { agentId }), }) /** bootRig plus a bottom Skill tool, so a model-loaded skill goes through. */ async function bootRun( $: Engine, on: On, model: string = FABLE, world: World = newWorld(), ) { on('tool.call', { tool: 'Skill' }, () => ({ result: { success: true, commandName: 'bottom' }, })) return bootRig($, on, model, 1000, world) } const spawnOf = (agent: string, extra: object = {}) => ({ ...spawnInput(), subagentType: agent, ...extra }) const offerOf = ( $: Engine, agent: string, source: string, description = 'd', ) => $.agent.offer({ agent, description, source, provider: { plugin: 'engine', tier: 'core' }, }) /** * A ui.log recorder. The kit refuses a bottom hook for a void event, so the * hook sits on top and swallows the missing implementation below it. */ function recordLogs(on: On): string[] { const lines: string[] = [] on('ui.log', async ($, e, next) => { lines.push(e.text) try { await next(e) } catch { // nothing implements ui.log below the recorder in the kit } }) return lines } test('typed /feat routes main to its row, best tier', async ($, on) => { await bootRun($, on) await typed($, '/feat add a thing') await skillPrompt($, 'feat') const line = mainLine(await route($, 'show')) expect(line).toContain('skill reflect') expect(line).toContain('effort high') expect(line).toContain('[tier best]') }) test('typed slash: a foreign origin arms nothing, fallback refused', async ( $, on) => { await bootRun($, on) await $.prompt.submit({ text: '/feat x', wait: false, origin: { kind: 'channel', server: 'slack' }, }) await skillPrompt($, 'feat') expect(mainLine(await route($, 'show'))).toContain('session defaults') }) test('typed slash: a preload in a live sub-agent leaves main alone', async ( $, on) => { await bootRun($, on) await typed($, 'hello') await $.agent.spawn(spawnOf('feater')) await skillPrompt($, 'feat') expect(mainLine(await route($, 'show'))).toContain('session defaults') }) test('typed slash: no live loop and an allowed origin still routes', async ( $, on) => { await bootRun($, on) await typed($, 'hello') await skillPrompt($, 'feat') expect(mainLine(await route($, 'show'))).toContain('skill reflect') }) test('typed slash: an sdk origin arms the marker', async ($, on) => { await bootRun($, on) await $.prompt.submit({ text: '/status', wait: false, origin: { kind: 'sdk' }, }) await $.agent.spawn(spawnOf('feater')) await skillPrompt($, 'status') expect(mainLine(await route($, 'show'))).toContain('skill mechanical') }) test('typed slash: a mid-turn /status waits for the turn end', async ( $, on) => { await bootRun($, on) await $.prompt.submit({ text: '/status', wait: true, origin: { kind: 'composer' }, turnId: 'u1', }) await skillPrompt($, 'status') expect(mainLine(await route($, 'show'))).toContain('session defaults') await endTurn($) await skillPrompt($, 'status') expect(mainLine(await route($, 'show'))).toContain('skill mechanical') }) test('skill without a row keeps the route; a rowed one replaces it', async ( $, on) => { await bootRun($, on) await $.tool.call({ tool: ROUTE_TOOL, phase: 'plan' }) await loadSkill($, 'find-docs') expect(mainLine(await route($, 'show'))).toContain('model plan') await loadSkill($, 'gitflow') expect(mainLine(await route($, 'show'))).toContain('skill implement') }) test('run slot: survives route calls and the turn end', async ($, on) => { await bootRun($, on) await loadSkill($, 'feat') await $.tool.call({ tool: ROUTE_TOOL, phase: 'orchestrate' }) await endTurn($) expect(mainLine(await route($, 'show'))).toContain('run reflect') await $.tool.call({ tool: ROUTE_TOOL, phase: 'plan' }) await endTurn($) expect(mainLine(await route($, 'show'))).toContain('run reflect') }) test('run slot: a model-loaded helper skill keeps it', async ($, on) => { await bootRun($, on) await loadSkill($, 'feat') await loadSkill($, 'using-git-worktrees') expect(mainLine(await route($, 'show'))).toContain('skill mechanical') await endTurn($) expect(mainLine(await route($, 'show'))).toContain('run reflect') }) test('run slot: a typed non-best skill drops it', async ($, on) => { await bootRun($, on) await loadSkill($, 'feat') await endTurn($) await typed($, '/status') await skillPrompt($, 'status') await endTurn($) expect(mainLine(await route($, 'show'))).toContain('session defaults') }) test('run slot: /route clear and /route off drop it', async ($, on) => { await bootRun($, on) await loadSkill($, 'feat') expect(await route($, 'clear')).toContain('run slot dropped') await endTurn($) expect(mainLine(await route($, 'show'))).toContain('session defaults') await loadSkill($, 'feat') await route($, 'off') await route($, 'on') await endTurn($) expect(mainLine(await route($, 'show'))).toContain('session defaults') }) test('run slot: a user /model switch drops it, an auto one does not', async ( $, on) => { await bootRun($, on) await loadSkill($, 'feat') await endTurn($) await switchTo($, FABLE, OPUS, 'auto') expect(mainLine(await route($, 'show'))).toContain('run reflect') await switchTo($, FABLE, OPUS, 'command') expect(mainLine(await route($, 'show'))).toContain('session defaults') }) test('run slot: a Skill call inside a sub-agent never touches it', async ( $, on) => { await bootRun($, on) await loadSkill($, 'feat', 'a1') await endTurn($) expect(mainLine(await route($, 'show'))).toContain('session defaults') }) test('run slot: route(clear) clears the turn and names the run', async ( $, on) => { await bootRun($, on) await loadSkill($, 'feat') const out = await $.tool.call({ tool: ROUTE_TOOL, clear: true }) expect(JSON.stringify(out)).toContain('run reflect still holds') }) test('route answer names the id even with a floor in force', async ( $, on) => { await bootRun($, on) await ultrathink($) const out = await $.tool.call({ tool: ROUTE_TOOL, phase: 'plan' }) const text = JSON.stringify(out) expect(text).toContain(`model ${FABLE}`) expect(text).toContain('user floor max') }) test('agent row: feater spawns on sonnet and steps at medium', async ( $, on) => { const rig = await bootRun($, on) const out = await $.agent.spawn(spawnOf('feater')) expect(rig.specs).toEqual([SONNET]) expect(out.model).toBe(SONNET) await runStep($, { ...highStep('a1'), model: SONNET }) expect(rig.seen[rig.seen.length - 1]).toEqual({ model: SONNET, effort: 'medium', }) }) /** Spawn feater, load a skill inside it, return the effort of its step. */ async function effortAfterSkill($: Engine, on: On, skill: string) { const rig = await bootRun($, on) await $.agent.spawn(spawnOf('feater')) await loadSkill($, skill, 'a1') await runStep($, { ...highStep('a1'), model: SONNET }) return rig.seen[rig.seen.length - 1].effort } test('agent skill: an unrowed one leaves the loop effort', async ($, on) => { expect(await effortAfterSkill($, on, 'find-docs')).toBe('medium') }) test('agent skill: a rowed one writes the loop effort', async ($, on) => { expect(await effortAfterSkill($, on, 'status')).toBe('low') }) test('agent row: judge moves up to fable when opus is down', async ( $, on) => { const rig = await bootRun($, on) await switchTo($, OPUS, SONNET, 'auto') await $.agent.spawn(spawnOf('plan-challenger')) expect(rig.specs).toEqual([FABLE]) }) test('agent row: opus and fable down, no write and one log line', async ( $, on) => { const logs = recordLogs(on) const rig = await bootRun($, on) await switchTo($, OPUS, SONNET, 'auto') await switchTo($, FABLE, SONNET, 'auto') await $.agent.spawn(spawnOf('plan-challenger')) await $.agent.spawn(spawnOf('plan-challenger')) expect(rig.specs).toEqual([undefined, undefined]) const down = logs.filter(l => l.includes('frontmatter model kept')) expect(down).toEqual([ 'model-router: plan-challenger tier big down, frontmatter model kept', ]) }) test('agent row: explicit model, explicit effort and fork win', async ( $, on) => { const rig = await bootRun($, on) await $.agent.spawn(spawnOf('feater', { model: 'opus' })) await $.agent.spawn(spawnOf('feater', { fork: true })) expect(rig.specs).toEqual(['opus', undefined]) await $.tool.call({ tool: 'Agent', description: 'd', prompt: 'p', effort: 'low', tool_use_id: 't1', }) await $.agent.spawn(spawnOf('feater')) await runStep($, { ...highStep('a1'), model: SONNET }) expect(rig.seen[rig.seen.length - 1]?.effort).toBe('high') }) test('agent row: a project definition keeps its own model', async ($, on) => { const rig = await bootRun($, on) await offerOf($, 'verifier', 'projectSettings') await offerOf($, 'feater', 'userSettings') await $.agent.spawn(spawnOf('verifier')) await $.agent.spawn(spawnOf('feater')) expect(rig.specs).toEqual([undefined, SONNET]) }) test('agent row: an override null drops the row', async ($, on) => { const world = newWorld({ home: { agents: { verifier: null } } }) const rig = await bootRun($, on, FABLE, world) await $.agent.spawn(spawnOf('verifier')) await $.agent.spawn(spawnOf('feater')) expect(rig.specs).toEqual([undefined, SONNET]) }) test('show lists the write and apply phases', async ($, on) => { await bootRun($, on) const text = await route($, 'show') expect(text).toContain('apply=work→claude-sonnet-5-5/low') expect(text).toContain('write=work→claude-sonnet-5-5/high') }) // ---- wave 2: typed-slash origins, spawning counter, spawn exclusions ---- /** A promise the test settles by hand, to hold a hook in flight. */ function deferred() { let release: () => void = () => {} const gate = new Promise(r => { release = r }) return { gate, release } } test('typed slash: an in-flight spawn refuses the fallback', async ( $, on) => { const hold = deferred() const entered = deferred() on('prompt.submit', ($, e) => ({ text: e.text })) on('agent.spawn', async ($, e) => { entered.release() await hold.gate return { model: e.model ?? e.parentModel, agentId: 'a1' } }) await boot($, on) await typed($, 'hello') const spawned = $.agent.spawn(spawnOf('feater')) await entered.gate await skillPrompt($, 'feat') expect(mainLine(await route($, 'show'))).toContain('session defaults') hold.release() await spawned }) test('typed slash: a bridge origin arms the marker', async ($, on) => { await bootRun($, on) await $.prompt.submit({ text: '/feat x', wait: false, origin: { kind: 'bridge' }, }) await $.agent.spawn(spawnOf('feater')) await skillPrompt($, 'feat') expect(mainLine(await route($, 'show'))).toContain('skill reflect') }) test('typed slash: a promoted mid-turn marker routes past a live loop', async ( $, on) => { await bootRun($, on) await $.agent.spawn(spawnOf('feater')) await $.prompt.submit({ text: '/status', wait: true, origin: { kind: 'composer' }, turnId: 'u1', }) await endTurn($) await skillPrompt($, 'status') expect(mainLine(await route($, 'show'))).toContain('skill mechanical') }) test('agent row: any provider plugin applies, a local definition not', async ( $, on) => { const rig = await bootRun($, on) const provider = { plugin: 'some-plugin', tier: 'core' as const } await $.agent.spawn(spawnOf('feater', { provider })) await offerOf($, 'verifier', 'localSettings') await $.agent.spawn(spawnOf('verifier', { provider })) expect(rig.specs).toEqual([SONNET, undefined]) }) test('agent row: a workflow spawn is untouched, model and effort', async ( $, on) => { const rig = await bootRun($, on) await $.agent.spawn(spawnOf('feater', { workflow: {} })) expect(rig.specs).toEqual([undefined]) await runStep($, { ...highStep('a1'), model: SONNET }) expect(rig.seen[rig.seen.length - 1]?.effort).toBe('high') }) test('typed slash: the marker binds to its skill name only', async ( $, on) => { await bootRun($, on) await typed($, '/status') await $.agent.spawn(spawnOf('feater')) await skillPrompt($, 'feat') expect(mainLine(await route($, 'show'))).toContain('session defaults') }) test('typed slash: the marker is one-shot', async ($, on) => { await bootRun($, on) await typed($, '/status') await $.agent.spawn(spawnOf('feater')) await skillPrompt($, 'status') expect(mainLine(await route($, 'show'))).toContain('skill mechanical') await route($, 'clear') await skillPrompt($, 'status') expect(mainLine(await route($, 'show'))).toContain('session defaults') }) test('skill.prompt nested in a Skill call leaves the run slot', async ( $, on) => { on('tool.call', { tool: 'Skill' }, async (_api, e) => { await skillPrompt($, 'status') return { result: { success: true, commandName: String(e.skill) } } }) await bootRig($, on, FABLE) await typed($, '/status') await loadSkill($, 'feat') await endTurn($) expect(mainLine(await route($, 'show'))).toContain('run reflect') }) /** Submits a prompt from `kind` over a running turn (mid-turn). */ const queued = ($: Engine, kind: string, text: string) => $.prompt.submit({ text, wait: true, origin: { kind }, turnId: 'u1', }) test('idle fallback: a mid-turn foreign prompt never arms it', async ( $, on) => { await bootRun($, on) await queued($, 'channel', 'hello') await endTurn($) await skillPrompt($, 'status') expect(mainLine(await route($, 'show'))).toContain('session defaults') }) test('idle fallback: a mid-turn composer prompt promotes the allowance', async ( $, on) => { await bootRun($, on) await $.prompt.submit({ text: 'hello', wait: false, origin: { kind: 'channel' }, }) await queued($, 'composer', 'hello') await endTurn($) await skillPrompt($, 'status') expect(mainLine(await route($, 'show'))).toContain('skill mechanical') }) test('spawning: a finished spawn no longer counts as a live loop', async ( $, on) => { await bootRun($, on) await $.agent.spawn(spawnOf('feater')) await agentEnds($) await typed($, 'hello') await skillPrompt($, 'status') expect(mainLine(await route($, 'show'))).toContain('skill mechanical') }) // ---- wave 3-A: routing.json layers --------------------------------------- /** A ui.toast recorder, same shape as recordLogs. */ function recordToasts(on: On): string[] { const lines: string[] = [] on('ui.toast', async ($, e, next) => { lines.push(e.text) try { await next(e) } catch { // nothing implements ui.toast below the recorder in the kit } }) return lines } const projectRows = (agents: Rec, key: string = KEY) => (file: Rec) => { file.projects = { [key]: { agents } } } /** The model a `feater` spawn reaches the bottom hook with. */ async function featerModel($: Engine, rig: Rig): Promise { await $.agent.spawn(spawnOf('feater')) return rig.specs[rig.specs.length - 1] } test('layers: routing.json rows route a spawn', async ($, on) => { const rig = await bootRun($, on) expect(await featerModel($, rig)).toBe(SONNET) }) test('layers: a projects[key] row beats the routing.json row', async ( $, on) => { const w = newWorld({ edit: projectRows({ feater: 'judge' }) }) const rig = await bootRun($, on, FABLE, w) expect(await featerModel($, rig)).toBe(OPUS) }) test('layers: another repo key leaves the routing.json row', async ( $, on) => { const w = newWorld({ edit: projectRows({ feater: 'judge' }, 'other/repo') }) const rig = await bootRun($, on, FABLE, w) expect(await featerModel($, rig)).toBe(SONNET) }) test('layers: the machine override beats the project row', async ( $, on) => { const w = newWorld({ edit: projectRows({ feater: 'judge' }), home: { agents: { feater: 'mechanical' } }, }) const rig = await bootRun($, on, FABLE, w) expect(await featerModel($, rig)).toBe(HAIKU) }) test('layers: a .claude/model-router.json in the project is never read', async ( $, on) => { const w = newWorld() const tree = `${ROOT}/.claude/model-router.json` w.files.set(tree, '{"agents":{"feater":"judge"}}') const rig = await bootRun($, on, FABLE, w) expect(await featerModel($, rig)).toBe(SONNET) expect(w.reads).not.toContain(tree) }) test('layers: the machine override\'s ask:false is honored', async ( $, on) => { const w = newWorld({ ask: true, home: { ask: false } }) await bootRun($, on, FABLE, w) await typed($, '/feat x') await skillPrompt($, 'feat') expect(w.asked).toEqual([]) }) test('layers: a row on a phase the table lacks is ignored, logged', async ( $, on) => { const logs = recordLogs(on) const w = newWorld({ edit: f => { (f.agents as Rec).feater = 'ghost' }, }) const rig = await bootRun($, on, FABLE, w) expect(await featerModel($, rig)).toBeUndefined() expect(logs.join('\n')).toContain('config agents.feater ignored') }) test('layers: an invalid routing.json phase falls back by name', async ( $, on) => { const logs = recordLogs(on) const w = newWorld({ edit: f => { f.phases = { reflect: { tier: 'nope' } } }, }) await bootRun($, on, FABLE, w) const text = await route($, 'show') expect(text).toContain('reflect=best→claude-fable-5-1/high') expect(logs.join('\n')).toContain('config phases.reflect ignored') }) test('layers: a valid routing.json phase replaces the default', async ( $, on) => { const w = newWorld({ edit: f => { f.phases = { reflect: { tier: 'work', effort: 'low' } } }, }) await bootRun($, on, FABLE, w) const text = await route($, 'show') expect(text).toContain('reflect=work→claude-sonnet-5-5/low') }) test('layers: a routing.json phase gone missing falls back, logged', async ( $, on) => { const logs = recordLogs(on) const w = newWorld({ edit: f => { delete (f.phases as Rec).reflect } }) await bootRun($, on, FABLE, w) expect(await route($, 'show')).toContain('reflect=best→claude-fable-5-1/high') expect(logs.filter(l => l.includes('lacks phases'))).toEqual([ 'model-router: routing.json lacks phases reflect; code defaults kept', ]) }) test('layers: a row may name a phase only the override defines', async ( $, on) => { const logs = recordLogs(on) const w = newWorld({ edit: f => { ;(f.skills as Rec).feat = 'custom' projectRows({ feater: 'custom' })(f) }, home: { phases: { custom: { tier: 'cheap', effort: 'low' } } }, }) const rig = await bootRun($, on, FABLE, w) await typeFeat($) expect(await mainNow($)).toContain('skill custom') expect(await featerModel($, rig)).toBe(HAIKU) expect(logs.join('\n')).not.toContain('ignored') }) test('layers: show names routing.json as the config', async ($, on) => { await bootRun($, on) expect(await route($, 'show')).toContain('config: routing.json') }) test('layers: first load skips a broken override, keeps routing.json', async ( $, on) => { const logs = recordLogs(on) const w = newWorld({ home: 'broken' }) const rig = await bootRun($, on, FABLE, w) expect(await featerModel($, rig)).toBe(SONNET) expect(logs.join('\n')).toContain('model-router.json unreadable') }) test('layers: a later broken override keeps the whole previous config', async ( $, on) => { const w = newWorld({ home: { agents: { feater: 'mechanical' } } }) const rig = await bootRun($, on, FABLE, w) w.files.set(HOME_FILE, '{nope') expect(await route($, 'reload')).toContain('config reloaded') expect(await featerModel($, rig)).toBe(HAIKU) }) test('layers: a routing.json gone missing keeps the previous config', async ( $, on) => { const w = newWorld() const rig = await bootRun($, on, FABLE, w) w.files.delete('routing') await route($, 'reload') expect(await featerModel($, rig)).toBe(SONNET) }) test('layers: routing.json unreadable at start means defaults, no rows', async ( $, on) => { const logs = recordLogs(on) const w = newWorld({ routing: 'broken', ask: true }) const rig = await bootRun($, on, FABLE, w) expect(await featerModel($, rig)).toBeUndefined() expect(await route($, 'show')).toContain('plan=best→claude-fable-5-1/xhigh') const said = logs.filter(l => l.includes('routing.json unreadable')) expect(said).toHaveLength(1) }) test('layers: routing.json missing at start logs once, no rows', async ( $, on) => { const logs = recordLogs(on) const w = newWorld({ routing: 'absent' }) const rig = await bootRun($, on, FABLE, w) expect(await featerModel($, rig)).toBeUndefined() expect(logs.filter(l => l.includes('routing.json missing'))).toHaveLength(1) }) test('layers: session toggles survive a rebuild', async ($, on) => { await bootRun($, on) await route($, 'switch on') await route($, 'verbose on') const out = await route($, 'reload') expect(out).toContain('switch: on') expect(out).toContain('verbose: on') }) test('layers: /clear keeps the config, the next prompt re-reads it', async ( $, on) => { on('session.end', ($, e) => ({ sessionId: e.sessionId })) const w = newWorld() const rig = await bootRun($, on, FABLE, w) await clearOf($) const before = w.reads.length expect(await featerModel($, rig)).toBe(SONNET) expect(w.reads.length).toBe(before) const file = routingOf(w) projectRows({ feater: 'judge' })(file) w.files.set('routing', JSON.stringify(file)) await typed($, 'hello') expect(await featerModel($, rig)).toBe(OPUS) }) test('layers: /clear swaps nothing when the re-read fails', async ($, on) => { on('session.end', ($, e) => ({ sessionId: e.sessionId })) const w = newWorld() const rig = await bootRun($, on, FABLE, w) await clearOf($) w.files.delete('routing') await typed($, 'hello') expect(await featerModel($, rig)).toBe(SONNET) }) test('layers: a state that never saw session.start loads on first use', async ( $, on) => { const w = newWorld() installWorld(on, w) on('agent.offer', () => ({ isOffered: true })) const specs: (string | undefined)[] = [] on('agent.spawn', ($, e) => { specs.push(e.model) return { model: e.model ?? e.parentModel, agentId: 'a1' } }) await $.agent.spawn(spawnOf('feater')) expect(specs).toEqual([SONNET]) }) test('layers: a cwd change re-reads the layers for the new repo', async ( $, on) => { const w = newWorld({ edit: projectRows({ feater: 'judge' }, 'github.com/o/two'), }) const rig = await bootRun($, on, FABLE, w) expect(await featerModel($, rig)).toBe(SONNET) w.repo = { root: '/work/two', remote: 'git@GitHub.com:o/two.git' } await $.classic.CwdChanged({ old_cwd: ROOT, new_cwd: '/work/two' }) expect(await featerModel($, rig)).toBe(OPUS) }) test('layers: no remote gives no project key, no local: string', async ( $, on) => { const w = asks([...TO_IMPLEMENT, 'This project only'], { repo: { root: ROOT, remote: null } }) await bootRun($, on, FABLE, w) await typeFeat($) expect(w.asked[SCOPE_ASK]?.options).toEqual(['Everywhere', 'Later']) expect(w.written).toEqual([]) expect(w.files.get('routing')).not.toContain('local:') expect(w.files.get('routing')).not.toContain(REAL_ROOT) }) test('layers: a failing repo read skips only the project layer', async ( $, on) => { const w = newWorld({ edit: projectRows({ feater: 'judge' }), repo: 'fail' }) const rig = await bootRun($, on, FABLE, w) expect(await featerModel($, rig)).toBe(SONNET) }) test('layers: a remote with credentials keys without them', async ($, on) => { const w = newWorld({ edit: projectRows({ feater: 'judge' }), repo: { root: ROOT, remote: 'https://me:tok@GitHub.com/acme/app/' }, }) const rig = await bootRun($, on, FABLE, w) expect(await featerModel($, rig)).toBe(OPUS) }) /** The project keys "This project only" stores for a remote. */ async function keyedBy($: Engine, on: On, remote: string) { const w = asks([...TO_IMPLEMENT, 'This project only'], { repo: { root: ROOT, remote } }) await bootRun($, on, FABLE, w) await typeFeat($) return { w, keys: Object.keys(routingOf(w).projects as Rec) } } for (const bad of [ 'https://user:p@ss@Host.com/a/b.git', 'https://user:pa/ss@host/a/b.git', ]) { test(`layers: credentials never become a key: ${bad}`, async ($, on) => { const { w, keys } = await keyedBy($, on, bad) expect(keys).toEqual([]) expect(w.asked[SCOPE_ASK]?.options).toEqual(['Everywhere', 'Later']) expect(w.files.get('routing')).not.toContain('ss@') }) } /** Remotes each rejected by exactly one guard of normalizeRemote. */ for (const [guard, bad] of [ ['path @', 'https://user:12/ss@host/a/b.git'], ['empty host', 'file:///srv/git/app.git'], ['host [@:]', 'https://[::1]/a/b'], ['non-URL', 'https:host/a/b'], ['non-URL', 'weird remote'], ] as const) { test(`layers: ${guard} guard rejects ${bad}`, async ($, on) => { const { w, keys } = await keyedBy($, on, bad) expect(keys).toEqual([]) expect(w.asked[SCOPE_ASK]?.options).toEqual(['Everywhere', 'Later']) expect(w.files.get('routing')).not.toContain('/srv/') expect(w.files.get('routing')).not.toContain('ss@') }) } for (const [remote, key] of [ ['https://tok@host/a/b', 'host/a/b'], ['git@github.com:acme/app.git', KEY], [REMOTE, KEY], ] as const) { test(`layers: ${remote} keys as ${key}`, async ($, on) => { expect((await keyedBy($, on, remote)).keys).toEqual([key]) }) } // ---- wave 3-A: first-use dialog ------------------------------------------- /** A world with dialogs on and the answers queued. */ const asks = (answers: string[], fx: Fixture = {}) => newWorld({ ask: true, answers, ...fx }) /** The user types /feat; the engine then loads the skill's prompt. */ async function typeFeat($: Engine): Promise { await typed($, '/feat add a thing') await skillPrompt($, 'feat') } const mainNow = async ($: Engine) => mainLine(await route($, 'show')) const confirmedOf = (w: World): Rec => routingOf(w).confirmed as Rec test('T1: a typed skill asks once, with the real id and effort', async ( $, on) => { const w = asks(['Later']) await bootRun($, on, FABLE, w) await typeFeat($) expect(w.asked).toHaveLength(1) const first = w.asked[0] as Asked expect(first.text).toContain('/feat') expect(first.text).toContain('claude-fable-5-1 at high') expect(first.options).toEqual(['Later', 'Keep', 'Change']) expect(first.header).toBe('model-router') }) test('T1: the question follows the model main will really run on', async ( $, on) => { const w = asks(['Later']) await bootRun($, on, SONNET, w) await typeFeat($) expect(w.asked[0]?.text).toContain('claude-fable-5-1 at high') }) test('T1: Keep writes the row and its phase, then the skill never asks', async ( $, on) => { const w = asks(['Keep']) await bootRun($, on, FABLE, w) await typeFeat($) expect(confirmedOf(w)).toEqual({ skills: { feat: 'reflect' }, phases: { reflect: 'reflect' }, }) await typeFeat($) expect(w.asked).toHaveLength(1) }) test('T1: Keep leaves the route as it was', async ($, on) => { await bootRun($, on, FABLE, asks(['Keep'])) await typeFeat($) expect(await mainNow($)).toContain('skill reflect') }) test('T1: a Change then Everywhere rewrites the row, routes at once', async ( $, on) => { const w = asks([...TO_IMPLEMENT, 'Everywhere']) await bootRun($, on, FABLE, w) await typeFeat($) const file = routingOf(w) expect((file.skills as Rec).feat).toBe('implement') expect((file.changed as Rec).skills).toEqual({ feat: { from: 'reflect', to: 'implement' }, }) expect(confirmedOf(w).skills).toEqual({ feat: 'implement' }) expect(await mainNow($)).toContain('skill implement') }) test('T1: the scope question names the two scopes', async ($, on) => { const w = asks([...TO_IMPLEMENT, 'Later']) await bootRun($, on, FABLE, w) await typeFeat($) expect(w.asked[SCOPE_ASK]?.options).toEqual(['Everywhere', 'This project only']) }) test('T1: This project only writes the exception, not the row', async ( $, on) => { const w = asks([...TO_IMPLEMENT, 'This project only']) await bootRun($, on, FABLE, w) await typeFeat($) const file = routingOf(w) expect((file.skills as Rec).feat).toBe('reflect') expect(file.projects).toEqual({ [KEY]: { skills: { feat: 'implement' } } }) expect(file.changed).toEqual({}) expect(confirmedOf(w).skills).toEqual({ feat: 'reflect' }) expect(await mainNow($)).toContain('skill implement') }) test('T1: after a project exception no other project asks', async ($, on) => { const w = asks([...TO_IMPLEMENT, 'This project only']) await bootRun($, on, FABLE, w) await typeFeat($) w.repo = { root: '/work/two', remote: 'https://github.com/acme/two.git' } await $.classic.CwdChanged({ old_cwd: ROOT, new_cwd: '/work/two' }) await typeFeat($) expect(w.asked).toHaveLength(SCOPE_ASK + 1) expect(await mainNow($)).toContain('skill reflect') }) /** /clear: a new conversation, the config kept and re-read at the prompt. */ function clearOf($: Engine) { return $.session.end({ reason: 'clear', sessionId: 's1', resume: { id: 's1' }, }) } test('T1: a second change keeps the first shipped phase in changed', async ( $, on) => { on('session.end', ($, e) => ({ sessionId: e.sessionId })) const w = asks([...TO_IMPLEMENT, 'Everywhere', ...TO_APPLY, 'Everywhere']) await bootRun($, on, FABLE, w) await typeFeat($) const file = routingOf(w) ;(file.confirmed as Rec).skills = {} w.files.set('routing', JSON.stringify(file)) await clearOf($) await typeFeat($) expect(((routingOf(w).changed as Rec).skills as Rec).feat).toEqual({ from: 'reflect', to: 'apply', }) }) test('T1: Later applies the default, writes nothing, asks no more', async ( $, on) => { const w = asks(['Later']) await bootRun($, on, FABLE, w) await typeFeat($) await typeFeat($) expect(w.written).toEqual([]) expect(w.asked).toHaveLength(1) expect(await mainNow($)).toContain('skill reflect') }) test('T1: a dismissed dialog is a Later', async ($, on) => { const w = asks([REJECT]) await bootRun($, on, FABLE, w) await typeFeat($) await typeFeat($) expect(w.written).toEqual([]) expect(w.asked).toHaveLength(1) expect(await mainNow($)).toContain('skill reflect') }) test('T1: an answer that is no label is a Later and says so', async ( $, on) => { const toasts = recordToasts(on) const w = asks(['banana']) await bootRun($, on, FABLE, w) await typeFeat($) expect(w.written).toEqual([]) expect(toasts).toContain('answer not recognised, default kept') expect(await mainNow($)).toContain('skill reflect') }) test('T1: a scope answer that is not a label is a Later', async ($, on) => { const w = asks([...TO_IMPLEMENT, 'whenever']) await bootRun($, on, FABLE, w) await typeFeat($) expect(w.written).toEqual([]) expect(await mainNow($)).toContain('skill reflect') }) test('T1: without a repo key only Everywhere is offered', async ($, on) => { const w = asks([...TO_IMPLEMENT, 'This project only'], { repo: 'fail' }) await bootRun($, on, FABLE, w) await typeFeat($) expect(w.asked[SCOPE_ASK]?.options).toEqual(['Everywhere', 'Later']) expect(w.written).toEqual([]) }) test('T1: the model\'s own Skill call never asks', async ($, on) => { const w = asks(['Keep']) await bootRun($, on, FABLE, w) await loadSkill($, 'feat') expect(w.asked).toEqual([]) }) test('T1: a skill without a row never asks', async ($, on) => { const w = asks(['Keep']) await bootRun($, on, FABLE, w) await typed($, '/other') await skillPrompt($, 'other') expect(w.asked).toEqual([]) }) test('T1: a project row counts as decided', async ($, on) => { const w = asks(['Keep'], { edit: f => { f.projects = { [KEY]: { skills: { feat: 'plan' } } } }, }) await bootRun($, on, FABLE, w) await typeFeat($) expect(w.asked).toEqual([]) expect(await mainNow($)).toContain('skill plan') }) test('T1: a machine override row counts as decided', async ($, on) => { const w = asks(['Keep'], { home: { skills: { feat: 'plan' } } }) await bootRun($, on, FABLE, w) await typeFeat($) expect(w.asked).toEqual([]) }) test('T1: another session\'s decision is seen before the dialog', async ( $, on) => { const w = asks(['Keep']) await bootRun($, on, FABLE, w) const file = routingOf(w) file.confirmed = { skills: { feat: 'reflect' } } w.files.set('routing', JSON.stringify(file)) await typeFeat($) expect(w.asked).toEqual([]) }) test('T1: `ask` turned off elsewhere is seen before the dialog', async ( $, on) => { const w = asks(['Keep']) await bootRun($, on, FABLE, w) const file = routingOf(w) file.ask = false w.files.set('routing', JSON.stringify(file)) await typeFeat($) expect(w.asked).toEqual([]) }) test('T1: a row edited by hand after a Keep asks again', async ($, on) => { on('session.end', ($, e) => ({ sessionId: e.sessionId })) const w = asks(['Keep', 'Later']) await bootRun($, on, FABLE, w) await typeFeat($) const file = routingOf(w) ;(file.skills as Rec).feat = 'plan' w.files.set('routing', JSON.stringify(file)) await clearOf($) await typeFeat($) expect(w.asked).toHaveLength(2) }) test('T1: ask false asks nothing', async ($, on) => { const w = newWorld({ answers: ['Keep'] }) await bootRun($, on, FABLE, w) await typeFeat($) expect(w.asked).toEqual([]) }) test('T1: the router off asks nothing', async ($, on) => { const w = asks(['Keep']) await bootRun($, on, FABLE, w) await route($, 'off') await typeFeat($) expect(w.asked).toEqual([]) }) test('T1: routing.json unreadable at the dialog asks nothing', async ( $, on) => { const w = asks(['Keep']) await bootRun($, on, FABLE, w) w.files.delete('routing') await typeFeat($) expect(w.asked).toEqual([]) expect(await mainNow($)).toContain('skill reflect') }) test('T3: routing.json unreadable at the dialog asks nothing', async ( $, on) => { const w = asks(['Keep'], { routing: 'broken' }) await bootRun($, on, FABLE, w) await $.tool.call({ tool: ROUTE_TOOL, phase: 'orchestrate' }) expect(w.asked).toEqual([]) }) test('T1: a failed write applies the default, key left unasked', async ( $, on) => { const logs = recordLogs(on) const w = asks(['Keep', 'Later']) await bootRun($, on, FABLE, w) w.failWrite = true await typeFeat($) expect(await mainNow($)).toContain('skill reflect') expect(logs.join('\n')).toContain('first-use dialog failed') w.failWrite = false await typeFeat($) expect(w.asked).toHaveLength(2) }) const failures = (logs: string[]): string[] => logs.filter(l => l.includes('first-use dialog failed')) test('T1: two failed writes log the failure once', async ($, on) => { const logs = recordLogs(on) const w = asks(['Keep', 'Keep']) await bootRun($, on, FABLE, w) w.failWrite = true await typeFeat($) await typeFeat($) expect(w.asked).toHaveLength(2) expect(failures(logs)).toHaveLength(1) }) test('T1: a rebuild failing after the write leaves the key unasked', async ( $, on) => { const logs = recordLogs(on) const w = asks([...TO_IMPLEMENT, 'Everywhere']) await bootRun($, on, FABLE, w) w.onWrite = () => { w.files.set(HOME_FILE, '{nope') } await typeFeat($) expect((routingOf(w).skills as Rec).feat).toBe('implement') expect(await mainNow($)).toContain('skill reflect') const pending = (await route($, 'pending')).split('\n') expect(pending[1]).toBe('asked this session: none') expect(failures(logs)).toHaveLength(1) expect(failures(logs)[0]).toContain('rebuild failed after the write') }) test('T1: a Keep toasts that routing.json changed', async ($, on) => { const toasts = recordToasts(on) await bootRun($, on, FABLE, asks(['Keep'])) await typeFeat($) expect(toasts.join('\n')).toContain('routing.json updated: commit it') }) test('writes: only routing.json, 2-space, keys in the documented order', async ( $, on) => { const w = asks(['Keep']) await bootRun($, on, FABLE, w) await typeFeat($) expect(w.written).toHaveLength(1) expect(w.written[0]).toMatch(/\/mods\/model-router\/routing\.json$/) expect(w.files.get('routing')).toMatch(/^\{\n {2}"phases"/) expect(Object.keys(routingOf(w))).toEqual([ 'phases', 'skills', 'agents', 'projects', 'confirmed', 'changed', 'ask', ]) }) test('writes: a missing routing.json is never created', async ($, on) => { const w = newWorld({ routing: 'absent' }) await bootRun($, on, FABLE, w) const out = await route($, 'ask off') expect(out).toContain('missing or unreadable') expect(w.written).toEqual([]) }) test('writes: two writers on one file both land', async ($, on) => { const w = asks(['Keep']) await bootRun($, on, FABLE, w) const held = deferred() const seen = deferred() w.writeGate = held.gate w.onWrite = seen.release const first = typeFeat($) await seen.gate w.onWrite = undefined const second = route($, 'ask off') await new Promise(r => setTimeout(r, 20)) // let a second writer start held.release() await first await second expect(routingOf(w).ask).toBe(false) expect(confirmedOf(w).skills).toEqual({ feat: 'reflect' }) }) test('writes: the route tool never writes', async ($, on) => { const w = newWorld() await bootRun($, on, FABLE, w) await $.tool.call({ tool: ROUTE_TOOL, phase: 'plan' }) await $.tool.call({ tool: ROUTE_TOOL, effort: 'low' }) expect(w.written).toEqual([]) }) /** A Change chain, then `scope`: the world after the typed /feat. */ async function changeFeat($: Engine, on: On, scope: string) { const w = asks([...TO_IMPLEMENT, scope]) await bootRun($, on, FABLE, w) await typeFeat($) return w } test('writes: an Everywhere change is one write', async ($, on) => { const w = await changeFeat($, on, 'Everywhere') expect(w.written).toHaveLength(1) const file = routingOf(w) expect((file.skills as Rec).feat).toBe('implement') expect((file.changed as Rec).skills).toEqual({ feat: { from: 'reflect', to: 'implement' }, }) expect(confirmedOf(w).skills).toEqual({ feat: 'implement' }) }) test('writes: a This project only change is one write', async ($, on) => { const w = await changeFeat($, on, 'This project only') expect(w.written).toHaveLength(1) const file = routingOf(w) expect(file.projects).toEqual({ [KEY]: { skills: { feat: 'implement' } } }) expect(confirmedOf(w).skills).toEqual({ feat: 'reflect' }) }) /** Rebuilds the fixture in reverse key order: `ask` first, `phases` last. */ function reverseKeys(file: Rec): void { const copy = { ...file } for (const key of Object.keys(copy)) delete file[key] for (const key of Object.keys(copy).reverse()) file[key] = copy[key] } test('writes: an out-of-order routing.json is written back in order', async ( $, on) => { const w = asks(['Keep'], { edit: reverseKeys }) expect(Object.keys(routingOf(w))).toEqual([ 'ask', 'changed', 'confirmed', 'projects', 'agents', 'skills', 'phases', ]) await bootRun($, on, FABLE, w) await typeFeat($) expect(w.written).toHaveLength(1) expect(Object.keys(routingOf(w))).toEqual([ 'phases', 'skills', 'agents', 'projects', 'confirmed', 'changed', 'ask', ]) }) test('writes: an output over the cap is refused: a Later', async ($, on) => { const toasts = recordToasts(on) const pad = (f: Rec) => { f.pad = 'x'.repeat(65536 - JSON.stringify(f, null, 2).length - 60) } const w = asks(['Keep'], { edit: pad }) await bootRun($, on, FABLE, w) await typeFeat($) expect(toasts.join('\n')).toContain( 'routing.json would exceed 65536 bytes: nothing saved') expect(w.written).toEqual([]) }) const NOTHING_SAVED = 'routing.json is missing or unreadable: nothing saved' test('writes: routing.json gone at the save: a Later, kept asked', async ( $, on) => { const toasts = recordToasts(on) const w = asks(['Keep', 'Keep']) await bootRun($, on, FABLE, w) const saved = w.files.get('routing') ?? '' w.onAsk = () => { w.files.delete('routing') } await typeFeat($) expect(toasts).toContain(NOTHING_SAVED) expect(w.written).toEqual([]) expect(w.files.has('routing')).toBe(false) expect(await mainNow($)).toContain('skill reflect') w.onAsk = undefined w.files.set('routing', saved) await typeFeat($) expect(w.asked).toHaveLength(1) const pending = (await route($, 'pending')).split('\n') expect(pending[1]).toBe('asked this session: skills:feat') }) // The kit has no disk: an fs.write no hook answers, or whose mock throws // (the engine skips a failing hook), is refused by the kit's bottom. The // probe aims at /home/t, where nothing can be created: a real write would // fail there with a disk error, not this one. const REFUSED = 'no implementation for fs.write' /** An inline plugin: `/probe ` says what `$.fs.write` did. */ const PROBE: Plugin = { name: 'fs-probe', register: on => { on('command.run', { command: 'probe' }, async ($, e) => { try { await $.fs.write(e.args, 'x') return { text: 'written' } } catch (err) { return { text: String(err) } } }) }, } const probe = async ($: Engine): Promise => (await $.command.run({ command: 'probe', args: `${HOME}/never-written.json`, origin: { kind: 'composer' }, presentation: { isFullscreen: false, columns: 80 }, })).text ?? '' test('writes: kit: an fs.write no hook answers is refused', { plugins: [PROBE], }, async $ => { expect(await probe($)).toContain(REFUSED) }) test('writes: kit: a throwing fs.write mock lands on the same refusal', { plugins: [PROBE], }, async ($, on) => { on('fs.write', () => { throw new Error('EACCES') }) expect(await probe($)).toContain(REFUSED) }) // ---- wave 3-A: T2 agent spawns, T3 route tool ---------------------------- test('T2: a rowed spawn asks with the model and effort it will get', async ( $, on) => { const w = asks(['Later']) await bootRun($, on, FABLE, w) await $.agent.spawn(spawnOf('feater')) const q = w.asked[0] as Asked expect(q.text).toContain('First dispatch of feater') expect(q.text).toContain('claude-sonnet-5-5 at medium') expect(q.options).toEqual(['Later', 'Keep', 'Change']) }) test('T2: the question carries an explicit effort param', async ($, on) => { const w = asks(['Later']) await bootRun($, on, FABLE, w) await $.tool.call({ tool: 'Agent', description: 'd', prompt: 'p', effort: 'low', tool_use_id: 't1', }) await $.agent.spawn(spawnOf('feater')) expect(w.asked[0]?.text).toContain('claude-sonnet-5-5 at low') }) test('T2: Keep writes the agent row and the spawn goes on', async ($, on) => { const w = asks(['Keep']) const rig = await bootRun($, on, FABLE, w) await $.agent.spawn(spawnOf('feater')) expect(confirmedOf(w)).toEqual({ agents: { feater: 'implement' }, phases: { implement: 'implement' }, }) expect(rig.specs).toEqual([SONNET]) await $.agent.spawn(spawnOf('feater')) expect(w.asked).toHaveLength(1) }) test('T2: a change routes this very spawn with the new row', async ( $, on) => { const w = asks([...TO_JUDGE, 'Everywhere']) const rig = await bootRun($, on, FABLE, w) await $.agent.spawn(spawnOf('feater')) expect(rig.specs).toEqual([OPUS]) }) test('T2: an explicit model param never asks', async ($, on) => { const w = asks(['Keep']) await bootRun($, on, FABLE, w) await $.agent.spawn(spawnOf('feater', { model: 'opus' })) expect(w.asked).toEqual([]) }) test('T2: a spawn inside an agent never asks', async ($, on) => { const w = asks(['Keep']) const rig = await bootRun($, on, FABLE, w) await $.agent.spawn(spawnOf('feater', { parentAgentId: 'a9' })) expect(w.asked).toEqual([]) expect(rig.specs).toEqual([SONNET]) }) test('T2: a project-defined agent never asks', async ($, on) => { const w = asks(['Keep']) await bootRun($, on, FABLE, w) await offerOf($, 'verifier', 'projectSettings') await $.agent.spawn(spawnOf('verifier')) expect(w.asked).toEqual([]) }) test('T2: an agent without a row never asks', async ($, on) => { const w = asks(['Keep']) await bootRun($, on, FABLE, w) await $.agent.spawn(spawnOf('stranger')) expect(w.asked).toEqual([]) }) test('T2: a parallel spawn of the same agent is routed unasked', async ( $, on) => { const w = asks(['Keep']) const rig = await bootRun($, on, FABLE, w) const held = deferred() const open = deferred() w.gate = held.gate w.onAsk = open.release const owner = $.agent.spawn(spawnOf('feater')) await open.gate await $.agent.spawn(spawnOf('feater')) expect(rig.specs).toEqual([SONNET]) held.release() await owner expect(w.asked).toHaveLength(1) expect(rig.specs).toEqual([SONNET, SONNET]) }) test('T2: a parallel spawn of another agent is routed unasked', async ( $, on) => { const w = asks(['Keep']) const rig = await bootRun($, on, FABLE, w) const held = deferred() const open = deferred() w.gate = held.gate w.onAsk = open.release const owner = $.agent.spawn(spawnOf('feater')) await open.gate await $.agent.spawn(spawnOf('plan-challenger')) expect(rig.specs).toEqual([OPUS]) held.release() await owner expect(w.asked).toHaveLength(1) }) test('T2: the unasked agent is asked at its next use', async ($, on) => { const w = asks(['Keep', 'Keep']) const rig = await bootRun($, on, FABLE, w) const held = deferred() const open = deferred() w.gate = held.gate w.onAsk = open.release const owner = $.agent.spawn(spawnOf('feater')) await open.gate await $.agent.spawn(spawnOf('plan-challenger')) held.release() await owner w.gate = undefined await $.agent.spawn(spawnOf('plan-challenger')) expect(w.asked).toHaveLength(2) expect(rig.specs).toEqual([OPUS, SONNET, OPUS]) }) test('T3: a phase declared through the route tool asks Keep or Later', async ( $, on) => { const w = asks(['Later']) await bootRun($, on, FABLE, w) await $.tool.call({ tool: ROUTE_TOOL, phase: 'orchestrate' }) const q = w.asked[0] as Asked expect(q.text).toContain('First use of orchestrate on the main loop') expect(q.text).toContain('claude-fable-5-1 at medium') expect(q.options).toEqual(['Later', 'Keep']) }) test('T3: Keep endorses the phase in routing.json', async ($, on) => { const w = asks(['Keep']) await bootRun($, on, FABLE, w) await $.tool.call({ tool: ROUTE_TOOL, phase: 'orchestrate' }) expect(confirmedOf(w)).toEqual({ phases: { orchestrate: 'orchestrate' } }) await $.tool.call({ tool: ROUTE_TOOL, phase: 'orchestrate' }) expect(w.asked).toHaveLength(1) }) test('T3: no change is offered, a phase name answer is a Later', async ( $, on) => { const w = asks(['plan']) await bootRun($, on, FABLE, w) await $.tool.call({ tool: ROUTE_TOOL, phase: 'orchestrate' }) expect(w.written).toEqual([]) expect(await mainNow($)).toContain('model orchestrate') }) test('T3: a phase endorsed by a skill Keep is not asked again', async ( $, on) => { const w = asks(['Keep']) await bootRun($, on, FABLE, w) await typeFeat($) await $.tool.call({ tool: ROUTE_TOOL, phase: 'reflect' }) expect(w.asked).toHaveLength(1) }) test('T3: an effort-only call never asks', async ($, on) => { const w = asks(['Keep']) await bootRun($, on, FABLE, w) await $.tool.call({ tool: ROUTE_TOOL, effort: 'low' }) expect(w.asked).toEqual([]) }) test('T3: a route call inside an agent never asks', async ($, on) => { const w = asks(['Keep']) await bootRun($, on, FABLE, w) await $.tool.call({ tool: ROUTE_TOOL, phase: 'judge', agentId: 'a1' }) expect(w.asked).toEqual([]) }) test('a step never opens a dialog', async ($, on) => { const w = asks(['Keep']) const rig = await bootRig($, on, FABLE, 1000, w) await runStep($, highStep()) expect(w.asked).toEqual([]) expect(rig.seen).toHaveLength(1) }) // ---- wave 3-A: /route pending, ask, reload ------------------------------- test('/route pending lists what is not confirmed, asked first', async ( $, on) => { const w = asks(['Later']) await bootRun($, on, FABLE, w) await typeFeat($) const lines = (await route($, 'pending')).split('\n') expect(lines[0]).toBe('pending confirmation: 19') expect(lines[1]).toBe('asked this session: skills:feat') expect(lines[2]).toContain('agents:feater') expect(lines[2]).toContain('phases:plan') expect(lines[2]).not.toContain('skills:feat') }) test('/route pending drops a confirmed row', async ($, on) => { const w = asks(['Keep']) await bootRun($, on, FABLE, w) await typeFeat($) const text = await route($, 'pending') expect(text).toContain('pending confirmation: 17') expect(text).not.toContain('phases:reflect') }) test('/route ask off writes the flag and silences the dialog', async ( $, on) => { const w = asks(['Keep']) await bootRun($, on, FABLE, w) expect(await route($, 'ask off')).toContain('ask off') expect(routingOf(w).ask).toBe(false) await typeFeat($) expect(w.asked).toEqual([]) }) test('/route ask on turns the dialog back on', async ($, on) => { const w = newWorld({ answers: ['Later'] }) await bootRun($, on, FABLE, w) await route($, 'ask on') expect(routingOf(w).ask).toBe(true) await typeFeat($) expect(w.asked).toHaveLength(1) }) test('/route ask names a machine override that wins', async ($, on) => { const w = newWorld({ home: { ask: false } }) await bootRun($, on, FABLE, w) expect(await route($, 'ask on')).toContain('sets ask off and wins') }) test('/route ask wants on or off', async ($, on) => { const w = newWorld() await bootRun($, on, FABLE, w) expect(await route($, 'ask maybe')).toContain('usage: /route ask on|off') expect(w.written).toEqual([]) }) test('/route ask refuses a user-less origin like the rest of /route', async ( $, on) => { const w = newWorld() await bootRun($, on, FABLE, w) const out = await $.command.run({ command: 'route', args: 'ask off', origin: { kind: 'plugin', name: 'x' }, presentation: { isFullscreen: false, columns: 80 }, }) expect(out.text).toContain('user-only') expect(w.written).toEqual([]) }) test('/route reload re-reads routing.json and the override', async ( $, on) => { const w = newWorld() const rig = await bootRun($, on, FABLE, w) expect(await featerModel($, rig)).toBe(SONNET) const file = routingOf(w) ;(file.agents as Rec).feater = 'judge' w.files.set('routing', JSON.stringify(file)) w.files.set(HOME_FILE, '{"agents":{"verifier":"mechanical"}}') await route($, 'reload') expect(await featerModel($, rig)).toBe(OPUS) await $.agent.spawn(spawnOf('verifier')) expect(rig.specs[rig.specs.length - 1]).toBe(HAIKU) }) test('layers: enabled:false in the override survives /clear', async ( $, on) => { on('session.end', ($, e) => ({ sessionId: e.sessionId })) const w = newWorld({ home: { enabled: false } }) const rig = await bootRun($, on, FABLE, w) expect(await route($, 'show')).toContain('router: off (config)') await clearOf($) expect(await route($, 'show')).toContain('router: off (config)') expect(await featerModel($, rig)).toBeUndefined() }) // ---- wave 3-B: a dialog with context, model then effort on a change ------ const skillPath = (name: string) => `${HOME}/.claude/skills/${name}/SKILL.md` const skillMd = (front: string, body = '# body') => `---\nname: feat\n${front}\nallowed-tools: Read\n---\n${body}\n` const ROUTED = 'Routed to reflect: claude-fable-5-1 at high. OK?' /** A typed /feat with `md` served as its SKILL.md; the world after. */ async function featWith( $: Engine, on: On, md: string | undefined, fx: Fixture = {}, ) { const w = asks(['Later'], fx) if (md !== undefined) w.files.set(skillPath('feat'), md) await bootRun($, on, FABLE, w) await typeFeat($) return w } /** Edits one phase of the inline routing.json. */ const setPhase = (phase: string, extra: Rec) => (file: Rec) => { const phases = file.phases as Record phases[phase] = { ...phases[phase], ...extra } } for (const [form, front, desc] of [ ['plain', 'description: Builds a thing. Then more words.', 'Builds a thing'], ['literal block', 'description: |\n Line one\n runs on. Second.', 'Line one runs on'], ['folded block', 'description: >-\n Folded one\n two. Next.', 'Folded one two'], ['single-quoted', "description: 'It''s here. More.'", "It's here"], ['double-quoted', 'description: "Say \\"hi\\" now. More."', 'Say "hi" now'], ] as const) { test(`text: a ${form} SKILL.md description leads the skill question`, async ( $, on) => { const w = await featWith($, on, skillMd(front)) expect(w.asked[0]?.text).toBe(`First route for /feat — ${desc}. ${ROUTED}`) }) } test('text: a skill without a SKILL.md is named alone', async ($, on) => { const w = await featWith($, on, undefined) expect(w.asked[0]?.text).toBe(`First route for /feat. ${ROUTED}`) }) test('text: a name outside the allowlist reads no file', async ($, on) => { const w = asks(['Later'], { edit: f => { (f.skills as Rec)['../x'] = 'reflect' }, }) const md = skillMd('description: Escaped. Bad.') w.files.set(`${HOME}/.claude/skills/../x/SKILL.md`, md) w.files.set(`${HOME}/.claude/x/SKILL.md`, md) // the folded spelling await bootRun($, on, FABLE, w) await typed($, 'hello') await skillPrompt($, '../x') expect(w.asked[0]?.text).toBe(`First route for /../x. ${ROUTED}`) expect(w.reads.filter(p => p.includes('SKILL.md'))).toEqual([]) }) test('text: a SKILL.md that is not a regular file is not read', async ( $, on) => { const w = asks(['Later']) w.files.set(skillPath('feat'), skillMd('description: Hidden. No.')) w.kinds.set(skillPath('feat'), 'dir') await bootRun($, on, FABLE, w) await typeFeat($) expect(w.asked[0]?.text).toBe(`First route for /feat. ${ROUTED}`) expect(w.reads).not.toContain(skillPath('feat')) }) test('text: a SKILL.md over the size cap is named alone', async ($, on) => { const big = skillMd('description: Too big. No.', 'x'.repeat(70000)) const w = await featWith($, on, big) expect(w.asked[0]?.text).toBe(`First route for /feat. ${ROUTED}`) }) test('text: HOME unset names the skill alone, reads nothing', async ( $, on) => { const w = asks(['Later'], { noHome: true }) w.files.set(skillPath('feat'), skillMd('description: Hidden. No.')) // served where an unguarded `${undefined}/.claude/...` would land for (const root of ['undefined', '']) { w.files.set(`${root}/.claude/skills/feat/SKILL.md`, skillMd('description: Unguarded. No.')) } await bootRun($, on, FABLE, w) await typeFeat($) expect(w.asked[0]?.text).toBe(`First route for /feat. ${ROUTED}`) expect(w.reads.filter(p => p.includes('SKILL.md'))).toEqual([]) }) test('text: an empty HOME never reads from the root', async ($, on) => { const w = asks(['Later']) w.env = '' w.files.set('/.claude/skills/feat/SKILL.md', skillMd('description: Hidden. No.')) await bootRun($, on, FABLE, w) await typeFeat($) expect(w.asked[0]?.text).toBe(`First route for /feat. ${ROUTED}`) expect(w.reads.filter(p => p.includes('SKILL.md'))).toEqual([]) }) test('text: a long description with control chars is cut and cleaned', async ( $, on) => { const raw = 'word \u0007‮
'.repeat(100) const w = await featWith($, on, skillMd(`description: ${raw}`)) const text = w.asked[0]?.text ?? '' const desc = text.slice(text.indexOf(' — ') + 3, text.indexOf('. Routed')) expect([...desc]).toHaveLength(140) expect(desc.endsWith('…')).toBe(true) expect(text).not.toMatch(/[\p{Cc}\p{Cf}]/u) }) test('text: an agent question carries the offered description', async ( $, on) => { const w = asks(['Later']) await bootRun($, on, FABLE, w) await offerOf($, 'feater', 'userSettings', 'Writes the code.\nMore text.') await $.agent.spawn(spawnOf('feater')) expect(w.asked[0]?.text).toBe('First dispatch of feater — Writes the code. ' + 'Routed to implement: claude-sonnet-5-5 at medium. OK?') }) test('text: an agent description is cleaned like a skill one', async ( $, on) => { const w = asks(['Later']) await bootRun($, on, FABLE, w) await offerOf($, 'feater', 'userSettings', 'a‮b\u0007c'.repeat(100)) await $.agent.spawn(spawnOf('feater')) const text = w.asked[0]?.text ?? '' expect(text).not.toMatch(/[\p{Cc}\p{Cf}]/u) expect(text.indexOf('. Routed') - text.indexOf(' — ') - 3).toBe(140) }) test('text: the phase about shows in the skill question', async ($, on) => { const w = await featWith($, on, undefined, { edit: setPhase('reflect', { about: 'deep thinking' }) }) expect(w.asked[0]?.text).toBe('First route for /feat. Routed to reflect ' + '(deep thinking): claude-fable-5-1 at high. OK?') }) test('text: the about is cleaned before it enters a question', async ( $, on) => { const w = await featWith($, on, undefined, { edit: setPhase('reflect', { about: 'a‮b\nc' }) }) expect(w.asked[0]?.text).toContain('Routed to reflect (ab c):') }) test('text: an astral description is cut by code point, not unit', async ( $, on) => { const w = asks(['Later']) await bootRun($, on, FABLE, w) await offerOf($, 'feater', 'userSettings', '\u{1F600}'.repeat(200)) await $.agent.spawn(spawnOf('feater')) const text = w.asked[0]?.text ?? '' const desc = text.slice(text.indexOf(' — ') + 3, text.indexOf('. Routed')) expect([...desc]).toHaveLength(140) expect(desc.endsWith('…')).toBe(true) expect(desc.isWellFormed()).toBe(true) }) test('text: an agent question carries the phase about', async ($, on) => { const w = asks(['Later'], { edit: setPhase('implement', { about: 'writing code' }), }) await bootRun($, on, FABLE, w) await $.agent.spawn(spawnOf('feater')) expect(w.asked[0]?.text).toContain('Routed to implement (writing code):') }) test('change: the effort question lists four labels at most', async ( $, on) => { const w = asks(['Change', 'sonnet (work)', 'Later'], { edit: f => { (f.phases as Rec).deep = { tier: 'work', effort: 'max' } }, }) await bootRun($, on, FABLE, w) await $.agent.spawn(spawnOf('feater')) const q = w.asked.find(a => a.text.startsWith('Which effort')) expect(q?.options).toEqual(['low', 'medium', 'high', 'xhigh']) }) for (const [what, about] of [ ['an overlong', 'x'.repeat(121)], ['a non-string', 5], ] as const) { test(`about: ${what} value is dropped and logged`, async ($, on) => { const logs = recordLogs(on) const w = await featWith($, on, undefined, { edit: setPhase('reflect', { about }) }) expect(w.asked[0]?.text).toBe(`First route for /feat. ${ROUTED}`) expect(logs.join('\n')).toContain('phases.reflect.about ignored') }) } test('text: a phase question names its about and its consumers', async ( $, on) => { const w = asks(['Later'], { edit: setPhase('orchestrate', { about: 'dispatch work' }), }) await bootRun($, on, FABLE, w) await $.tool.call({ tool: ROUTE_TOOL, phase: 'orchestrate' }) expect(w.asked[0]?.text).toBe('First use of orchestrate on the main loop — ' + 'dispatch work; used by 0 rows, the derived route of dispatches: ' + 'claude-fable-5-1 at medium. OK?') }) test('text: a phase question counts rows and prompt rules', async ($, on) => { const w = asks(['Later']) await bootRun($, on, FABLE, w) await $.tool.call({ tool: ROUTE_TOOL, phase: 'reflect' }) expect(w.asked[0]?.text).toBe('First use of reflect on the main loop — ' + 'used by 1 row, 1 prompt rule: claude-fable-5-1 at high. OK?') }) test('text: a phase question names its floor rule', async ($, on) => { const w = asks(['Later']) await bootRun($, on, FABLE, w) await $.tool.call({ tool: ROUTE_TOOL, phase: 'escalate' }) expect(w.asked[0]?.text).toBe('First use of escalate on the main loop — ' + 'used by 0 rows, 1 floor rule: claude-fable-5-1 at max. OK?') }) test('T3: a Change answer changes nothing and is said', async ($, on) => { const toasts = recordToasts(on) const w = asks(['Change']) await bootRun($, on, FABLE, w) await $.tool.call({ tool: ROUTE_TOOL, phase: 'orchestrate' }) expect(w.asked).toHaveLength(1) expect(w.written).toEqual([]) expect(toasts).toContain('answer not recognised, default kept') }) test('change: the model question offers the four tiered aliases', async ( $, on) => { const w = asks(['Change', 'Later']) await bootRun($, on, FABLE, w) await typeFeat($) expect(w.asked[1]?.text).toBe('Which model for feat?') expect(w.asked[1]?.options).toEqual([ 'fable (best)', 'opus (big)', 'sonnet (work)', 'haiku (cheap)', ]) }) for (const [label, alias, efforts] of [ ['fable (best)', 'fable', ['medium', 'high', 'xhigh', 'max']], ['sonnet (work)', 'sonnet', ['low', 'medium', 'high', 'xhigh']], ] as const) { test(`change: ${alias} offers the efforts of its phases, ascending`, async ( $, on) => { const w = asks(['Change', label, 'Later']) await bootRun($, on, FABLE, w) await typeFeat($) expect(w.asked[2]?.text).toBe(`Which effort on ${alias}?`) expect(w.asked[2]?.options).toEqual(efforts) }) } for (const [label, phase] of [ ['opus (big)', 'judge'], ['haiku (cheap)', 'mechanical'], ] as const) { test(`change: ${label} has one effort, the question is skipped`, async ( $, on) => { const w = asks(['Change', label, 'Later']) await bootRun($, on, FABLE, w) await typeFeat($) expect(w.asked).toHaveLength(3) expect(w.asked[2]?.text).toContain(`Use ${phase} for feat`) expect(w.asked[2]?.options).toEqual(['Everywhere', 'This project only']) }) } test('change: an alias no phase uses is a Later and says so', async ( $, on) => { const toasts = recordToasts(on) const w = asks(['Change', 'haiku (cheap)'], { home: { phases: { mechanical: { tier: 'work', effort: 'low' } } }, }) await bootRun($, on, FABLE, w) await typeFeat($) expect(toasts).toContain('no phase uses haiku') expect(w.asked).toHaveLength(2) expect(w.written).toEqual([]) }) test('change: a dismissed model question is a silent Later', async ( $, on) => { const toasts = recordToasts(on) const w = asks(['Change', REJECT]) await bootRun($, on, FABLE, w) await typeFeat($) expect(toasts).toEqual([]) expect(w.written).toEqual([]) }) for (const [step, answers] of [ ['the first', ['whatever']], ['the model', ['Change', 'mistral']], ['the effort', ['Change', 'sonnet (work)', 'ludicrous']], ] as const) { test(`change: another answer at ${step} question is a Later, said`, async ( $, on) => { const toasts = recordToasts(on) const w = asks([...answers]) await bootRun($, on, FABLE, w) await typeFeat($) expect(toasts).toContain('answer not recognised, default kept') expect(w.asked).toHaveLength(answers.length) expect(w.written).toEqual([]) expect(toasts.join('\n')).not.toContain('mistral') }) } test('change: a Keep on a main row says nothing about a new route', async ( $, on) => { const toasts = recordToasts(on) await bootRun($, on, FABLE, asks(['Keep'])) await typeFeat($) expect(toasts.join('\n')).not.toContain('now runs') }) test('change: a downgrade names the real route and the switch', async ( $, on) => { const toasts = recordToasts(on) const w = asks([...TO_IMPLEMENT, 'Everywhere']) await bootRun($, on, FABLE, w) await typeFeat($) expect((routingOf(w).skills as Rec).feat).toBe('implement') expect(toasts).toContain('/feat now runs claude-fable-5-1 at medium, ' + 'main moves up only: /route switch on allows a downgrade') }) test('change: with the switch on the toast carries no hint', async ( $, on) => { const toasts = recordToasts(on) await bootRun($, on, FABLE, asks([...TO_IMPLEMENT, 'Everywhere'])) await route($, 'switch on') await typeFeat($) expect(toasts).toContain('/feat now runs claude-sonnet-5-5 at medium') }) test('change: an upgrade toast carries no hint', async ($, on) => { const toasts = recordToasts(on) const w = asks(['Change', 'fable (best)', 'xhigh', 'Everywhere']) await bootRun($, on, SONNET, w) await typeFeat($) expect(toasts).toContain('/feat now runs claude-fable-5-1 at xhigh') }) test('change: an upgrade the config holds back carries no hint', async ( $, on) => { const toasts = recordToasts(on) const w = asks(['Change', 'fable (best)', 'xhigh', 'Everywhere'], { home: { mainUpgrade: false } }) await bootRun($, on, SONNET, w) await typeFeat($) expect(toasts).toContain('/feat now runs claude-sonnet-5-5 at xhigh') }) test('change: a project scope stores the derived phase, keeps the row', async ( $, on) => { const w = asks([...TO_APPLY, 'This project only']) await bootRun($, on, FABLE, w) await typeFeat($) const file = routingOf(w) expect((file.skills as Rec).feat).toBe('reflect') expect(file.projects).toEqual({ [KEY]: { skills: { feat: 'apply' } } }) }) test('change: a security auditor moves to opus at xhigh, at once', async ( $, on) => { const w = asks([...TO_JUDGE, 'Everywhere'], { edit: f => { (f.agents as Rec)['security-auditor'] = 'verify' }, }) const rig = await bootRun($, on, FABLE, w) await $.agent.spawn(spawnOf('security-auditor')) expect(w.asked[0]?.text).toContain('claude-sonnet-5-5 at xhigh') expect(rig.specs).toEqual([OPUS]) expect((routingOf(w).agents as Rec)['security-auditor']).toBe('judge') await runStep($, { ...highStep('a1'), model: OPUS }) expect(rig.seen[rig.seen.length - 1]?.effort).toBe('xhigh') }) test('change: an agent picks sonnet then high, the write phase', async ( $, on) => { const w = asks(['Change', 'sonnet (work)', 'high', 'Everywhere']) const rig = await bootRun($, on, FABLE, w) await $.agent.spawn(spawnOf('feater')) expect((routingOf(w).agents as Rec).feater).toBe('write') expect(rig.specs).toEqual([SONNET]) }) test('change: the current phase wins a tie, and counts as a Keep', async ( $, on) => { const w = asks(['Change', 'sonnet (work)', 'medium']) await bootRun($, on, FABLE, w) await $.agent.spawn(spawnOf('Explore')) expect(w.asked).toHaveLength(3) expect(routingOf(w).changed).toEqual({}) expect(confirmedOf(w).agents).toEqual({ Explore: 'explore' }) })