fix(model-router): close the accepted residuals of waves 1-4

Kill switch fails closed: a present-but-unreadable override at first load
or a non-boolean enabled turns the router off (logged); /route reload says
when the previous config was kept. Prompt rules with nested quantifiers
are dropped; a config path that is not a regular file reads as failed;
caught errors log kind + code only (errWord); rejected keys are masked in
logs (keyWord), alias keys match KEY_NAME, prototype names are never keys;
offers capped at 256; a models alias without a fallback list is ranked at
the end of the default chain; canonical matches a table id at a '-'
boundary only; logCall/noteMain cannot unroute a step; route-tool failure
text no longer claims nothing was routed.

Repo side: mods suite probes the 'claude plugin test' usage line, doctor
bounds plugin list to 60 s, update-all skips @skills-dir mods.
Kit 431/431, full make test green.
This commit is contained in:
bchanot
2026-10-11 23:39:01 +02:00
parent 37ec8e332a
commit a3fbb9ae3f
7 changed files with 169 additions and 56 deletions
+41 -6
View File
@@ -1581,13 +1581,47 @@ test('layers: show names routing.json as the config', async ($, on) => {
expect(await route($, 'show')).toContain('config: routing.json')
})
test('layers: first load skips a broken override, keeps routing.json', async (
test('layers: a broken override at first load turns the router off', async (
$, on) => {
const logs = recordLogs(on)
const w = newWorld({ home: 'broken' })
const rig = await bootRun($, on, FABLE, w)
expect(await featerModel($, rig)).toBe(SONNET)
await bootRun($, on, FABLE, w)
expect(logs.join('\n')).toContain('model-router.json unreadable')
expect(logs.join('\n')).toContain('router off (fail closed)')
expect(await route($, 'show')).toContain('router: off (config)')
})
test('layers: an override that is not a regular file fails closed', async (
$, on) => {
const logs = recordLogs(on)
const w = newWorld({ home: { verbose: true } })
w.kinds.set(HOME_FILE, 'other')
await bootRun($, on, FABLE, w)
expect(logs.join('\n')).toContain('model-router.json is not a regular file')
expect(await route($, 'show')).toContain('router: off (config)')
})
test('layers: prototype and malformed keys are dropped, named safely', async (
$, on) => {
const logs = recordLogs(on)
const w = newWorld()
w.files.set(HOME_FILE, '{"models":{"__proto__":"claude-x-1",' +
'"constructor":"claude-x-1","bad key\\n":"claude-x-1","mini":"claude-x-1"}}')
await bootRun($, on, FABLE, w)
expect(logs).toContain('model-router: config models.constructor ignored')
expect(logs.filter(l => l === 'model-router: config models.<key> ignored'))
.toHaveLength(2) // __proto__ and the key with a newline, both masked
expect(logs).toContain('model-router: config fallback lacks mini; appended')
const shown = await route($, 'nope')
expect(shown).toContain('mini')
expect(shown).not.toContain('constructor')
})
test('layers: a nested-quantifier prompt rule is dropped', async ($, on) => {
const logs = recordLogs(on)
const home = { prompt: [{ pattern: '(a+)+$', phase: 'plan' }] }
await bootRun($, on, FABLE, newWorld({ home }))
expect(logs).toContain('model-router: config prompt rule ignored')
})
test('layers: a later broken override keeps the whole previous config', async (
@@ -1595,7 +1629,7 @@ test('layers: a later broken override keeps the whole previous config', async (
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 route($, 'reload')).toContain('previous config kept')
expect(await featerModel($, rig)).toBe(HAIKU)
})
@@ -2094,7 +2128,7 @@ test('T1: a rebuild failing after the write keeps the key asked', async (
const pending = (await route($, 'pending')).split('\n')
expect(pending[1]).toBe('asked this session: skills:feat')
expect(failures(logs)).toHaveLength(1)
expect(failures(logs)[0]).toContain('rebuild failed after the write')
expect(failures(logs)[0]).toContain('first-use dialog failed (Error)')
})
test('T1: a Keep toasts that routing.json changed', async ($, on) => {
@@ -3776,7 +3810,8 @@ test('logs: switch warnings keep their exact texts', async ($, on) => {
await bootRun($, on, FABLE, newWorld({ home }))
expect(logs).toContain('config "verbose" is not a boolean; ignored')
expect(logs).toContain(
'model-router: config "enabled" is not a boolean; ignored')
'model-router: config "enabled" is not a boolean; router off (fail closed)')
expect(await route($, 'show')).toContain('router: off (config)')
})
for (const bad of ['ON', 1, null]) {