This commit is contained in:
307
packages/agent/tests/workflow-commands.test.ts
Normal file
307
packages/agent/tests/workflow-commands.test.ts
Normal file
@@ -0,0 +1,307 @@
|
||||
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');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user