feat: verify Phase 8 platform architecture + integration tests (P8-019) (#185)
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
Co-authored-by: Jason Woltje <jason@diversecanvas.com> Co-committed-by: Jason Woltje <jason@diversecanvas.com>
This commit was merged in pull request #185.
This commit is contained in:
348
packages/cli/src/tui/commands/commands.integration.spec.ts
Normal file
348
packages/cli/src/tui/commands/commands.integration.spec.ts
Normal file
@@ -0,0 +1,348 @@
|
||||
/**
|
||||
* 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 '@mosaic/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 <agent-id>' }],
|
||||
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 <id>' }],
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user