/** * Integration tests for TUI command parsing + registry (P8-019) * * Covers: * - parseSlashCommand() + commandRegistry.find() round-trip for all aliases * - /help, /stop, /cost, /status resolve to 'local' execution * - Unknown commands return null from find() * - Alias resolution: /h → help, /m → model, /n → new, etc. * - filterCommands prefix filtering */ import { describe, it, expect, beforeEach } from 'vitest'; import { parseSlashCommand } from './parse.js'; import { CommandRegistry } from './registry.js'; import type { CommandDef } from '@mosaicstack/types'; // ─── Parse + Registry Round-trip ───────────────────────────────────────────── describe('parseSlashCommand + CommandRegistry — integration', () => { let registry: CommandRegistry; // Gateway-style commands to simulate a live manifest const gatewayCommands: CommandDef[] = [ { name: 'model', description: 'Switch the active model', aliases: ['m'], args: [{ name: 'model-name', type: 'string', optional: false, description: 'Model name' }], scope: 'core', execution: 'socket', available: true, }, { name: 'thinking', description: 'Set thinking level', aliases: ['t'], args: [ { name: 'level', type: 'enum', optional: false, values: ['none', 'low', 'medium', 'high', 'auto'], description: 'Thinking level', }, ], scope: 'core', execution: 'socket', available: true, }, { name: 'new', description: 'Start a new conversation', aliases: ['n'], scope: 'core', execution: 'socket', available: true, }, { name: 'agent', description: 'Switch or list available agents', aliases: ['a'], args: [{ name: 'args', type: 'string', optional: true, description: 'list or ' }], scope: 'agent', execution: 'socket', available: true, }, { name: 'preferences', description: 'View or set user preferences', aliases: ['pref'], args: [ { name: 'action', type: 'enum', optional: true, values: ['show', 'set', 'reset'], description: 'Action', }, ], scope: 'core', execution: 'rest', available: true, }, { name: 'gc', description: 'Trigger garbage collection sweep', aliases: [], scope: 'core', execution: 'socket', available: true, }, { name: 'mission', description: 'View or set active mission', aliases: [], args: [{ name: 'args', type: 'string', optional: true, description: 'status | set ' }], scope: 'agent', execution: 'socket', available: true, }, ]; beforeEach(() => { registry = new CommandRegistry(); registry.updateManifest({ version: 1, commands: gatewayCommands, skills: [] }); }); // ── parseSlashCommand tests ── it('returns null for non-slash input', () => { expect(parseSlashCommand('hello world')).toBeNull(); expect(parseSlashCommand('')).toBeNull(); expect(parseSlashCommand('model')).toBeNull(); }); it('parses "/model claude-3-opus" → command=model args=claude-3-opus', () => { const parsed = parseSlashCommand('/model claude-3-opus'); expect(parsed).not.toBeNull(); expect(parsed!.command).toBe('model'); expect(parsed!.args).toBe('claude-3-opus'); expect(parsed!.raw).toBe('/model claude-3-opus'); }); it('parses "/gc" with no args → command=gc args=null', () => { const parsed = parseSlashCommand('/gc'); expect(parsed).not.toBeNull(); expect(parsed!.command).toBe('gc'); expect(parsed!.args).toBeNull(); }); it('parses "/system you are a helpful assistant" → args contains full text', () => { const parsed = parseSlashCommand('/system you are a helpful assistant'); expect(parsed!.command).toBe('system'); expect(parsed!.args).toBe('you are a helpful assistant'); }); it('parses "/help" → command=help args=null', () => { const parsed = parseSlashCommand('/help'); expect(parsed!.command).toBe('help'); expect(parsed!.args).toBeNull(); }); // ── Round-trip: parse then find ── it('round-trip: /m → resolves to "model" command via alias', () => { const parsed = parseSlashCommand('/m claude-3-haiku'); expect(parsed).not.toBeNull(); const cmd = registry.find(parsed!.command); expect(cmd).not.toBeNull(); // /m → model (alias map in registry) expect(cmd!.name === 'model' || cmd!.aliases.includes('m')).toBe(true); }); it('round-trip: /h → resolves to "help" (local command)', () => { const parsed = parseSlashCommand('/h'); expect(parsed).not.toBeNull(); const cmd = registry.find(parsed!.command); expect(cmd).not.toBeNull(); expect(cmd!.name === 'help' || cmd!.aliases.includes('h')).toBe(true); }); it('round-trip: /n → resolves to "new" via gateway manifest', () => { const parsed = parseSlashCommand('/n'); expect(parsed).not.toBeNull(); const cmd = registry.find(parsed!.command); expect(cmd).not.toBeNull(); expect(cmd!.name === 'new' || cmd!.aliases.includes('n')).toBe(true); }); it('round-trip: /a → resolves to "agent" via gateway manifest', () => { const parsed = parseSlashCommand('/a list'); expect(parsed).not.toBeNull(); const cmd = registry.find(parsed!.command); expect(cmd).not.toBeNull(); expect(cmd!.name === 'agent' || cmd!.aliases.includes('a')).toBe(true); }); it('round-trip: /pref → resolves to "preferences" via alias', () => { const parsed = parseSlashCommand('/pref show'); expect(parsed).not.toBeNull(); const cmd = registry.find(parsed!.command); expect(cmd).not.toBeNull(); expect(cmd!.name === 'preferences' || cmd!.aliases.includes('pref')).toBe(true); }); it('round-trip: /t → resolves to "thinking" via alias', () => { const parsed = parseSlashCommand('/t high'); expect(parsed).not.toBeNull(); const cmd = registry.find(parsed!.command); expect(cmd).not.toBeNull(); expect(cmd!.name === 'thinking' || cmd!.aliases.includes('t')).toBe(true); }); // ── Local commands resolve to 'local' execution ── it('/help resolves to local execution', () => { const cmd = registry.find('help'); expect(cmd).not.toBeNull(); expect(cmd!.execution).toBe('local'); }); it('/stop resolves to local execution', () => { const cmd = registry.find('stop'); expect(cmd).not.toBeNull(); expect(cmd!.execution).toBe('local'); }); it('/cost resolves to local execution', () => { const cmd = registry.find('cost'); expect(cmd).not.toBeNull(); expect(cmd!.execution).toBe('local'); }); it('/status resolves to local execution (TUI local override)', () => { const cmd = registry.find('status'); expect(cmd).not.toBeNull(); // status is 'local' in the TUI registry (local takes precedence over gateway) expect(cmd!.execution).toBe('local'); }); // ── Unknown commands return null ── it('find() returns null for unknown command', () => { expect(registry.find('nonexistent')).toBeNull(); expect(registry.find('xyz')).toBeNull(); expect(registry.find('')).toBeNull(); }); it('find() returns null when no gateway manifest and command not local', () => { const emptyRegistry = new CommandRegistry(); expect(emptyRegistry.find('model')).toBeNull(); expect(emptyRegistry.find('gc')).toBeNull(); }); // ── getAll returns combined local + gateway ── it('getAll() includes both local and gateway commands', () => { const all = registry.getAll(); const names = all.map((c) => c.name); // Local commands expect(names).toContain('help'); expect(names).toContain('stop'); expect(names).toContain('cost'); expect(names).toContain('status'); // Gateway commands expect(names).toContain('model'); expect(names).toContain('gc'); }); it('getLocalCommands() returns only local commands', () => { const local = registry.getLocalCommands(); expect(local.every((c) => c.execution === 'local')).toBe(true); expect(local.some((c) => c.name === 'help')).toBe(true); expect(local.some((c) => c.name === 'stop')).toBe(true); }); }); // ─── filterCommands (autocomplete) ──────────────────────────────────────────── describe('filterCommands (from CommandAutocomplete)', () => { // Import inline since filterCommands is not exported — replicate the logic here function filterCommands(commands: CommandDef[], query: string): CommandDef[] { if (!query) return commands; const q = query.toLowerCase(); return commands.filter( (c) => c.name.includes(q) || c.aliases.some((a) => a.includes(q)) || c.description.toLowerCase().includes(q), ); } const commands: CommandDef[] = [ { name: 'model', description: 'Switch the active model', aliases: ['m'], scope: 'core', execution: 'socket', available: true, }, { name: 'mission', description: 'View or set active mission', aliases: [], scope: 'agent', execution: 'socket', available: true, }, { name: 'help', description: 'Show available commands', aliases: ['h'], scope: 'core', execution: 'local', available: true, }, { name: 'gc', description: 'Trigger garbage collection sweep', aliases: [], scope: 'core', execution: 'socket', available: true, }, ]; it('returns all commands when query is empty', () => { expect(filterCommands(commands, '')).toHaveLength(commands.length); }); it('filters by name prefix "mi" → mission only (not model, as "mi" not in model name or aliases)', () => { const result = filterCommands(commands, 'mi'); const names = result.map((c) => c.name); expect(names).toContain('mission'); expect(names).not.toContain('gc'); }); it('filters by name prefix "mo" → model only', () => { const result = filterCommands(commands, 'mo'); const names = result.map((c) => c.name); expect(names).toContain('model'); expect(names).not.toContain('mission'); expect(names).not.toContain('gc'); }); it('filters by exact name "gc" → gc only', () => { const result = filterCommands(commands, 'gc'); expect(result).toHaveLength(1); expect(result[0]!.name).toBe('gc'); }); it('filters by alias "h" → help', () => { const result = filterCommands(commands, 'h'); const names = result.map((c) => c.name); expect(names).toContain('help'); }); it('filters by description keyword "switch" → model', () => { const result = filterCommands(commands, 'switch'); const names = result.map((c) => c.name); expect(names).toContain('model'); }); it('returns empty array when no commands match', () => { const result = filterCommands(commands, 'zzznotfound'); expect(result).toHaveLength(0); }); });