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:
253
apps/gateway/src/commands/commands.integration.spec.ts
Normal file
253
apps/gateway/src/commands/commands.integration.spec.ts
Normal file
@@ -0,0 +1,253 @@
|
||||
/**
|
||||
* 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([]),
|
||||
};
|
||||
|
||||
// ─── 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,
|
||||
null, // reloadService (optional)
|
||||
null, // chatGateway (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
|
||||
it('/gc calls SessionGCService.sweepOrphans with userId', async () => {
|
||||
const payload: SlashCommandPayload = { command: 'gc', conversationId };
|
||||
const result = await executor.execute(payload, userId);
|
||||
expect(mockSessionGC.sweepOrphans).toHaveBeenCalledWith(userId);
|
||||
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);
|
||||
});
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user