- Updated all package.json name fields and dependency references - Updated all TypeScript/JavaScript imports - Updated .woodpecker/publish.yml filters and registry paths - Updated tools/install.sh scope default - Updated .npmrc registry paths (worktree + host) - Enhanced update-checker.ts with checkForAllUpdates() multi-package support - Updated CLI update command to show table of all packages - Added KNOWN_PACKAGES, formatAllPackagesTable, getInstallAllCommand - Marked checkForUpdate() with @deprecated JSDoc Closes #391
264 lines
9.7 KiB
TypeScript
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 '@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<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);
|
|
});
|
|
}
|
|
});
|