fix(tui): make mutating slash paths native and lifecycle-safe

Route /browser, /reload-mcp, /rollback, /stop, /fast, and /busy through direct TUI RPC handlers so state changes hit the live gateway session instead of slash-worker fallback. Add TUI session finalize/reset parity hooks (memory commit + plugin boundaries) and parity matrix tests to keep mutating commands off fallback.
This commit is contained in:
Brooklyn Nicholson 2026-04-27 12:20:08 -05:00
parent d5a89283b7
commit a4cb3ef66c
7 changed files with 594 additions and 11 deletions

View file

@ -0,0 +1,88 @@
import { readFileSync } from 'node:fs'
import { dirname, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import { describe, expect, it } from 'vitest'
import { SLASH_COMMANDS } from '../app/slash/registry.js'
type CommandRoute = 'fallback' | 'local' | 'native'
const NATIVE_MUTATING_COMMANDS = new Set([
'browser',
'busy',
'fast',
'reload-mcp',
'rollback',
'stop'
])
const MUTATING_COMMANDS = [
'background',
'branch',
'browser',
'busy',
'clear',
'compress',
'fast',
'model',
'new',
'personality',
'queue',
'reasoning',
'reload-mcp',
'retry',
'rollback',
'steer',
'stop',
'title',
'tools',
'undo',
'verbose',
'voice',
'yolo'
] as const
const loadCommandRegistryNames = (): string[] => {
const here = dirname(fileURLToPath(import.meta.url))
const source = readFileSync(resolve(here, '../../../hermes_cli/commands.py'), 'utf8')
const names = [...source.matchAll(/CommandDef\("([^"]+)"/g)].map(match => match[1]!)
return [...new Set(names)]
}
const LOCAL_COMMAND_NAMES = new Set(
SLASH_COMMANDS.flatMap(command => [command.name, ...(command.aliases ?? [])].map(name => name.toLowerCase()))
)
const classifyRoute = (name: string): CommandRoute => {
const normalized = name.toLowerCase()
if (NATIVE_MUTATING_COMMANDS.has(normalized)) {
return 'native'
}
if (LOCAL_COMMAND_NAMES.has(normalized)) {
return 'local'
}
return 'fallback'
}
describe('slash parity matrix', () => {
it('classifies each command registry command as local/native/fallback', () => {
const routes = Object.fromEntries(loadCommandRegistryNames().map(name => [name, classifyRoute(name)]))
expect(routes['model']).toBe('local')
expect(routes['browser']).toBe('native')
expect(routes['reload-mcp']).toBe('native')
expect(routes['rollback']).toBe('native')
expect(routes['stop']).toBe('native')
})
it('keeps every mutating command off slash-worker fallback', () => {
const routes = Object.fromEntries(loadCommandRegistryNames().map(name => [name, classifyRoute(name)]))
for (const name of MUTATING_COMMANDS) {
expect(routes[name], `missing command in registry: ${name}`).toBeDefined()
expect(routes[name], `mutating command must not fallback: ${name}`).not.toBe('fallback')
}
})
})