Files
stack/apps/gateway/src/commands/commands.integration.spec.ts
jason.woltje 0d12471868
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/publish Pipeline was successful
feat: add web search, file edit, MCP management, file refs, and /stop to CLI/TUI (#348)
2026-04-02 18:08:30 +00:00

264 lines
9.7 KiB
TypeScript

/**
* 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 '@mosaic/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<string, string> = {
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);
});
}
});