/** * Integration tests for the gateway command system (P8-019) * * Covers: * - CommandRegistryService.getManifest() returns 12+ core commands * - All core commands have correct execution types * - Alias resolution works for all defined aliases * - CommandExecutorService routes known/unknown commands correctly * - /gc handler calls SessionGCService.sweepOrphans * - /system handler calls SystemOverrideService.set * - Unknown command returns descriptive error */ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { CommandRegistryService } from './command-registry.service.js'; import { CommandExecutorService } from './command-executor.service.js'; import type { SlashCommandPayload } from '@mosaicstack/types'; // ─── Mocks ─────────────────────────────────────────────────────────────────── const mockAgentService = { getSession: vi.fn(() => undefined), }; const mockSystemOverride = { set: vi.fn().mockResolvedValue(undefined), get: vi.fn().mockResolvedValue(null), clear: vi.fn().mockResolvedValue(undefined), renew: vi.fn().mockResolvedValue(undefined), }; const mockSessionGC = { sweepOrphans: vi.fn().mockResolvedValue({ orphanedSessions: 3, totalCleaned: [], duration: 12 }), }; const mockRedis = { set: vi.fn().mockResolvedValue('OK'), get: vi.fn().mockResolvedValue(null), del: vi.fn().mockResolvedValue(0), keys: vi.fn().mockResolvedValue([]), }; const mockBrain = { agents: { findByName: vi.fn().mockResolvedValue(undefined), findById: vi.fn().mockResolvedValue(undefined), create: vi.fn(), }, }; // ─── Helpers ───────────────────────────────────────────────────────────────── function buildRegistry(): CommandRegistryService { const svc = new CommandRegistryService(); svc.onModuleInit(); // seed core commands return svc; } function buildExecutor(registry: CommandRegistryService): CommandExecutorService { return new CommandExecutorService( registry as never, mockAgentService as never, mockSystemOverride as never, mockSessionGC as never, mockRedis as never, mockBrain as never, null, // reloadService (optional) null, // chatGateway (optional) null, // mcpClient (optional) ); } // ─── Registry Tests ─────────────────────────────────────────────────────────── describe('CommandRegistryService — integration', () => { let registry: CommandRegistryService; beforeEach(() => { registry = buildRegistry(); }); it('getManifest() returns 12 or more core commands after onModuleInit', () => { const manifest = registry.getManifest(); expect(manifest.commands.length).toBeGreaterThanOrEqual(12); }); it('manifest version is 1', () => { expect(registry.getManifest().version).toBe(1); }); it('manifest.skills is an array', () => { expect(Array.isArray(registry.getManifest().skills)).toBe(true); }); it('all commands have required fields: name, description, execution, scope, available', () => { for (const cmd of registry.getManifest().commands) { expect(typeof cmd.name).toBe('string'); expect(typeof cmd.description).toBe('string'); expect(['local', 'socket', 'rest', 'hybrid']).toContain(cmd.execution); expect(['core', 'agent', 'admin']).toContain(cmd.scope); expect(typeof cmd.available).toBe('boolean'); } }); // Execution type verification for core commands const expectedExecutionTypes: Record = { model: 'socket', thinking: 'socket', new: 'socket', clear: 'socket', compact: 'socket', retry: 'socket', rename: 'rest', history: 'rest', export: 'rest', preferences: 'rest', system: 'socket', help: 'local', gc: 'socket', agent: 'socket', provider: 'hybrid', mission: 'socket', prdy: 'socket', tools: 'socket', reload: 'socket', }; for (const [name, expectedExecution] of Object.entries(expectedExecutionTypes)) { it(`command "${name}" has execution type "${expectedExecution}"`, () => { const cmd = registry.getManifest().commands.find((c) => c.name === name); expect(cmd, `command "${name}" not found`).toBeDefined(); expect(cmd!.execution).toBe(expectedExecution); }); } // Alias resolution checks const expectedAliases: Array<[string, string]> = [ ['m', 'model'], ['t', 'thinking'], ['n', 'new'], ['a', 'agent'], ['s', 'status'], ['h', 'help'], ['pref', 'preferences'], ]; for (const [alias, commandName] of expectedAliases) { it(`alias "/${alias}" resolves to command "${commandName}" via aliases array`, () => { const cmd = registry .getManifest() .commands.find((c) => c.name === commandName || c.aliases?.includes(alias)); expect(cmd, `command with alias "${alias}" not found`).toBeDefined(); }); } }); // ─── Executor Tests ─────────────────────────────────────────────────────────── describe('CommandExecutorService — integration', () => { let registry: CommandRegistryService; let executor: CommandExecutorService; const userId = 'user-integ-001'; const conversationId = 'conv-integ-001'; beforeEach(() => { vi.clearAllMocks(); registry = buildRegistry(); executor = buildExecutor(registry); }); // Unknown command returns error it('unknown command returns success:false with descriptive message', async () => { const payload: SlashCommandPayload = { command: 'nonexistent', conversationId }; const result = await executor.execute(payload, userId); expect(result.success).toBe(false); expect(result.message).toContain('nonexistent'); expect(result.command).toBe('nonexistent'); }); // /gc handler calls SessionGCService.sweepOrphans (admin-only, no userId arg) it('/gc calls SessionGCService.sweepOrphans without arguments', async () => { const payload: SlashCommandPayload = { command: 'gc', conversationId }; const result = await executor.execute(payload, userId); expect(mockSessionGC.sweepOrphans).toHaveBeenCalledWith(); expect(result.success).toBe(true); expect(result.message).toContain('GC sweep complete'); expect(result.message).toContain('3 orphaned sessions'); }); // /system with args calls SystemOverrideService.set it('/system with text calls SystemOverrideService.set', async () => { const override = 'You are a helpful assistant.'; const payload: SlashCommandPayload = { command: 'system', args: override, conversationId }; const result = await executor.execute(payload, userId); expect(mockSystemOverride.set).toHaveBeenCalledWith(conversationId, override); expect(result.success).toBe(true); expect(result.message).toContain('override set'); }); // /system with no args clears the override it('/system with no args calls SystemOverrideService.clear', async () => { const payload: SlashCommandPayload = { command: 'system', conversationId }; const result = await executor.execute(payload, userId); expect(mockSystemOverride.clear).toHaveBeenCalledWith(conversationId); expect(result.success).toBe(true); expect(result.message).toContain('cleared'); }); // /model with model name returns success it('/model with a model name returns success', async () => { const payload: SlashCommandPayload = { command: 'model', args: 'claude-3-opus', conversationId, }; const result = await executor.execute(payload, userId); expect(result.success).toBe(true); expect(result.command).toBe('model'); expect(result.message).toContain('claude-3-opus'); }); // /thinking with valid level returns success it('/thinking with valid level returns success', async () => { const payload: SlashCommandPayload = { command: 'thinking', args: 'high', conversationId }; const result = await executor.execute(payload, userId); expect(result.success).toBe(true); expect(result.message).toContain('high'); }); // /thinking with invalid level returns usage message it('/thinking with invalid level returns usage message', async () => { const payload: SlashCommandPayload = { command: 'thinking', args: 'invalid', conversationId }; const result = await executor.execute(payload, userId); expect(result.success).toBe(true); expect(result.message).toContain('Usage:'); }); // /new command returns success it('/new returns success', async () => { const payload: SlashCommandPayload = { command: 'new', conversationId }; const result = await executor.execute(payload, userId); expect(result.success).toBe(true); expect(result.command).toBe('new'); }); // /reload without reloadService returns failure it('/reload without ReloadService returns failure', async () => { const payload: SlashCommandPayload = { command: 'reload', conversationId }; const result = await executor.execute(payload, userId); expect(result.success).toBe(false); expect(result.message).toContain('ReloadService'); }); // Commands not yet fully implemented return a fallback response const stubCommands = ['clear', 'compact', 'retry']; for (const cmd of stubCommands) { it(`/${cmd} returns success (stub)`, async () => { const payload: SlashCommandPayload = { command: cmd, conversationId }; const result = await executor.execute(payload, userId); expect(result.success).toBe(true); expect(result.command).toBe(cmd); }); } });