Files
waggle-os/packages/agent/tests/workflow-commands.test.ts
Oleg Maslov 0c3e2ead3b
Some checks failed
Installer Smoke / installer-smoke (push) Has been cancelled
moving
2026-09-02 10:10:29 +02:00

308 lines
11 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from 'vitest';
import { CommandRegistry, type CommandContext } from '../src/commands/command-registry.js';
import { registerWorkflowCommands } from '../src/commands/workflow-commands.js';
function mockContext(overrides?: Partial<CommandContext>): CommandContext {
return {
workspaceId: 'test-ws',
sessionId: 'test-session',
runWorkflow: vi.fn().mockResolvedValue('Workflow result'),
searchMemory: vi.fn().mockResolvedValue('Memory result'),
getWorkspaceState: vi.fn().mockResolvedValue('Workspace state data'),
listSkills: vi.fn().mockReturnValue(['skill-a', 'skill-b']),
spawnAgent: vi.fn().mockResolvedValue('Agent result'),
...overrides,
};
}
describe('CommandRegistry', () => {
it('register and retrieve by name', () => {
const registry = new CommandRegistry();
registry.register({
name: 'test',
aliases: [],
description: 'A test command',
usage: '/test',
handler: async () => 'ok',
});
const cmd = registry.get('test');
expect(cmd).toBeDefined();
expect(cmd!.name).toBe('test');
});
it('retrieve by alias', () => {
const registry = new CommandRegistry();
registry.register({
name: 'test',
aliases: ['t', 'tst'],
description: 'A test command',
usage: '/test',
handler: async () => 'ok',
});
expect(registry.get('t')?.name).toBe('test');
expect(registry.get('tst')?.name).toBe('test');
});
it('list all commands', () => {
const registry = new CommandRegistry();
registry.register({ name: 'a', aliases: [], description: '', usage: '', handler: async () => '' });
registry.register({ name: 'b', aliases: [], description: '', usage: '', handler: async () => '' });
expect(registry.list()).toHaveLength(2);
});
it('execute parses command and args', async () => {
const registry = new CommandRegistry();
const handler = vi.fn().mockResolvedValue('executed');
registry.register({ name: 'greet', aliases: [], description: '', usage: '', handler });
const ctx = mockContext();
const result = await registry.execute('/greet hello world', ctx);
expect(result).toBe('executed');
expect(handler).toHaveBeenCalledWith('hello world', ctx);
});
it('isCommand returns true for /commands, false for regular text', () => {
const registry = new CommandRegistry();
expect(registry.isCommand('/research quantum')).toBe(true);
expect(registry.isCommand('/now')).toBe(true);
expect(registry.isCommand('hello world')).toBe(false);
expect(registry.isCommand('not a /command')).toBe(false);
expect(registry.isCommand('/ space')).toBe(false);
});
it('search returns partial matches', () => {
const registry = new CommandRegistry();
registerWorkflowCommands(registry);
const results = registry.search('re');
const names = results.map(c => c.name);
expect(names).toContain('research');
expect(names).toContain('review');
});
it('unknown command returns error message', async () => {
const registry = new CommandRegistry();
registerWorkflowCommands(registry);
const ctx = mockContext();
const result = await registry.execute('/nonexistent', ctx);
expect(result).toContain('Unknown command');
expect(result).toContain('/nonexistent');
});
});
describe('Workflow Commands', () => {
let registry: CommandRegistry;
beforeEach(() => {
registry = new CommandRegistry();
registerWorkflowCommands(registry);
});
it('/catchup calls getWorkspaceState and formats result', async () => {
const ctx = mockContext();
const result = await registry.execute('/catchup', ctx);
expect(ctx.getWorkspaceState).toHaveBeenCalled();
expect(result).toContain('Catch-Up Briefing');
expect(result).toContain('Workspace state data');
});
it('/now calls getWorkspaceState', async () => {
const ctx = mockContext();
const result = await registry.execute('/now', ctx);
expect(ctx.getWorkspaceState).toHaveBeenCalled();
expect(result).toContain('Right Now');
expect(result).toContain('Workspace state data');
});
it('/research calls runWorkflow with research-team', async () => {
const ctx = mockContext();
const result = await registry.execute('/research quantum computing', ctx);
expect(ctx.runWorkflow).toHaveBeenCalledWith('research-team', 'quantum computing');
expect(result).toBe('Workflow result');
});
it('/research without topic returns error', async () => {
const ctx = mockContext();
const result = await registry.execute('/research', ctx);
expect(result).toContain('Usage');
expect(result).toContain('/research <topic>');
expect(ctx.runWorkflow).not.toHaveBeenCalled();
});
it('/draft calls runWorkflow with review-pair', async () => {
const ctx = mockContext();
const result = await registry.execute('/draft blog post about AI', ctx);
expect(ctx.runWorkflow).toHaveBeenCalledWith('review-pair', 'blog post about AI');
expect(result).toBe('Workflow result');
});
it('/spawn calls spawnAgent with role and task', async () => {
const ctx = mockContext();
const result = await registry.execute('/spawn researcher Find papers on AI', ctx);
expect(ctx.spawnAgent).toHaveBeenCalledWith('researcher', 'Find papers on AI');
expect(result).toBe('Agent result');
});
it('/spawn without role returns error', async () => {
const ctx = mockContext();
const result = await registry.execute('/spawn', ctx);
expect(result).toContain('Usage');
expect(result).toContain('/spawn <role>');
expect(ctx.spawnAgent).not.toHaveBeenCalled();
});
it('/skills calls listSkills', async () => {
const ctx = mockContext();
const result = await registry.execute('/skills', ctx);
expect(ctx.listSkills).toHaveBeenCalled();
expect(result).toContain('skill-a');
expect(result).toContain('skill-b');
expect(result).toContain('2 skill(s)');
});
it('/cli allow and deny persist through the command context', async () => {
let allowlist: string[] = [];
const ctx = mockContext({
getCliAllowlist: () => allowlist,
updateCliAllowlist: (action, name) => {
const before = [...allowlist];
const key = name.toLowerCase();
allowlist = action === 'allow'
? before.some(item => item.toLowerCase() === key) ? before : [...before, name]
: before.filter(item => item.toLowerCase() !== key);
return { changed: before.length !== allowlist.length, allowlist };
},
});
expect(await registry.execute('/cli allow git', ctx)).toContain('Allowed "git"');
expect(await registry.execute('/cli', ctx)).toContain('Allowed CLI tools: git');
expect(await registry.execute('/cli deny git', ctx)).toContain('Denied "git"');
expect(await registry.execute('/cli', ctx)).toContain('No CLI tools explicitly allowed');
});
it('/memory calls searchMemory', async () => {
const ctx = mockContext();
const result = await registry.execute('/memory architecture decisions', ctx);
expect(ctx.searchMemory).toHaveBeenCalledWith('architecture decisions');
expect(result).toContain('Memory Search');
expect(result).toContain('Memory result');
});
it('/memory without query shows help', async () => {
const ctx = mockContext();
const result = await registry.execute('/memory', ctx);
expect(result).toContain('Usage');
expect(result).toContain('/memory <query>');
expect(ctx.searchMemory).not.toHaveBeenCalled();
});
it('/plan calls runWorkflow with plan-execute', async () => {
const ctx = mockContext();
const result = await registry.execute('/plan Build a dashboard', ctx);
expect(ctx.runWorkflow).toHaveBeenCalledWith('plan-execute', 'Build a dashboard');
expect(result).toBe('Workflow result');
});
it('/plan without goal returns error', async () => {
const ctx = mockContext();
const result = await registry.execute('/plan', ctx);
expect(result).toContain('Usage');
expect(result).toContain('/plan <goal>');
expect(ctx.runWorkflow).not.toHaveBeenCalled();
});
it('/focus returns context hint', async () => {
const ctx = mockContext();
const result = await registry.execute('/focus database optimization', ctx);
expect(result).toContain('Focus');
expect(result).toContain('database optimization');
expect(result).toContain('Tip');
expect(result).not.toContain('System instruction');
});
it('/decide without question returns error', async () => {
const ctx = mockContext();
const result = await registry.execute('/decide', ctx);
expect(result).toContain('Usage');
expect(result).toContain('/decide <question>');
});
it('/decide with question delegates to runWorkflow when available', async () => {
const ctx = mockContext();
const result = await registry.execute('/decide PostgreSQL or MongoDB?', ctx);
// B7: With runWorkflow available, /decide delegates to workflow
expect(ctx.runWorkflow).toHaveBeenCalledWith('decision-analysis', 'PostgreSQL or MongoDB?');
expect(result).toBe('Workflow result');
});
it('/review calls runWorkflow', async () => {
const ctx = mockContext();
const result = await registry.execute('/review', ctx);
expect(ctx.runWorkflow).toHaveBeenCalledWith('review-pair', expect.stringContaining('Review'));
expect(result).toBe('Workflow result');
});
it('/status calls getWorkspaceState', async () => {
const ctx = mockContext();
const result = await registry.execute('/status', ctx);
expect(ctx.getWorkspaceState).toHaveBeenCalled();
expect(result).toContain('Status Report');
expect(result).toContain('Workspace state data');
});
it('/help lists all commands', async () => {
const ctx = mockContext();
const result = await registry.execute('/help', ctx);
expect(result).toContain('Available Commands');
expect(result).toContain('/catchup');
expect(result).toContain('/research');
expect(result).toContain('/help');
});
it('all 22 commands are registered', () => {
const commands = registry.list();
expect(commands).toHaveLength(22);
const names = commands.map(c => c.name);
expect(names).toContain('catchup');
expect(names).toContain('now');
expect(names).toContain('research');
expect(names).toContain('draft');
expect(names).toContain('decide');
expect(names).toContain('review');
expect(names).toContain('spawn');
expect(names).toContain('skills');
expect(names).toContain('status');
expect(names).toContain('memory');
expect(names).toContain('plan');
expect(names).toContain('focus');
expect(names).toContain('help');
expect(names).toContain('plugins');
expect(names).toContain('export');
expect(names).toContain('import');
expect(names).toContain('settings');
expect(names).toContain('cli');
expect(names).toContain('pr');
});
});