Files
claude/mods/model-router/hooks/register.test.ts
T
bchanot 604a6c4411 feat(model-router): wave 3-B — first-use dialog with context, model then effort on change
The dialog now explains what it asks about: the skill's description (from
its SKILL.md frontmatter, five YAML forms, first sentence, cleaned), the
agent's description (from agent.offer), the phase's new 'about' line in
routing.json, and the real model id and effort the next step runs on.
Options Later / Keep / Change; Change asks the model (fable, opus, sonnet,
haiku with their tier role), then the effort among those the phases of
that model offer, then the scope; the pair maps to an existing phase (rows
stay phase names; the row's current phase wins a tie; same phase = Keep).
A main-loop phase (T3) is Later / Keep only. A main-row change toasts the
real decision and the /route switch hint when the pick is a downgrade.
Descriptions pass a hardened read (name allowlist, stat kind, size cap)
and clean(). Kit suite 190 → 232; W3-A dialog tests migrated.

Contract .claude/tasks/contracts/2026-10-11-model-router-w3b-dialog-1240.md,
plan r3: 3 lenses (2 BLOCKERs: inline rows, phase edits) + 1 confirmation,
feater + 2 rounds, GATE 0 MET, verifier CONFORME (3rd pass), security PASS.
2026-10-11 14:10:24 +02:00

2959 lines
96 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<string, unknown>
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 = '<rejected>' // 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<string, string> // '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<void> | undefined // holds a dialog open until it settles
writeGate: Promise<void> | 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<string, 'file' | 'dir' | 'other'> // 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<string, string>()
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<MockClock> {
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 <args>` as the user would type it. */
async function route($: Engine, args: string): Promise<string> {
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<void> {
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<void> {
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<void> {
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<unknown> {
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<Seen[]> {
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<Rig> {
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<Seen> {
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<void>(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<string | undefined> {
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<void> {
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 <path>` 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<string> => (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<string, Rec>
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' })
})