import { describe, it, expect } from 'vitest'; import { parseCommandSchema } from '@/utils/command/schema-parse'; import { createCommandRegistry, registerCommand, unregisterCommand, hasCommand, getCommand, runCommand, createCommandRunnerContext, type CommandRegistry, type CommandRunnerContextExport, } from '@/utils/command/command-registry'; import type { CommandRunner, PromptEvent } from '@/utils/command/command-runner'; type TestContext = { counter: number; log: string[]; }; describe('CommandRegistry', () => { it('should create an empty registry', () => { const registry = createCommandRegistry(); expect(registry.size).toBe(0); }); it('should register a command', () => { const registry = createCommandRegistry(); const runner: CommandRunner = { schema: parseCommandSchema('add '), run: async function (cmd) { return Number(cmd.params[0]) + Number(cmd.params[1]); }, }; registerCommand(registry, runner); expect(registry.size).toBe(1); expect(hasCommand(registry, 'add')).toBe(true); }); it('should unregister a command', () => { const registry = createCommandRegistry(); const runner: CommandRunner = { schema: parseCommandSchema('remove'), run: async () => {}, }; registerCommand(registry, runner); expect(hasCommand(registry, 'remove')).toBe(true); unregisterCommand(registry, 'remove'); expect(hasCommand(registry, 'remove')).toBe(false); }); it('should get a command runner', () => { const registry = createCommandRegistry(); const runner: CommandRunner = { schema: parseCommandSchema('get'), run: async () => {}, }; registerCommand(registry, runner); const retrieved = getCommand(registry, 'get'); expect(retrieved).toBe(runner); }); it('should return undefined for unknown command', () => { const registry = createCommandRegistry(); const retrieved = getCommand(registry, 'unknown'); expect(retrieved).toBeUndefined(); }); }); describe('runCommand', () => { it('should run a command successfully', async () => { const registry = createCommandRegistry(); const runner: CommandRunner = { schema: parseCommandSchema('add '), run: async function (cmd) { return Number(cmd.params[0]) + Number(cmd.params[1]); }, }; registerCommand(registry, runner); const result = await runCommand(registry, { counter: 0, log: [] }, 'add 1 2'); expect(result.success).toBe(true); if (result.success) { expect(result.result).toBe(3); } }); it('should fail for unknown command', async () => { const registry = createCommandRegistry(); const result = await runCommand(registry, { counter: 0, log: [] }, 'unknown'); expect(result.success).toBe(false); if (!result.success) { expect(result.error).toContain('Unknown command'); } }); it('should fail for invalid command params', async () => { const registry = createCommandRegistry(); const runner: CommandRunner = { schema: parseCommandSchema('add '), run: async () => {}, }; registerCommand(registry, runner); const result = await runCommand(registry, { counter: 0, log: [] }, 'add 1'); expect(result.success).toBe(false); if (!result.success) { expect(result.error).toContain('参数不足'); } }); it('should access context via this.context', async () => { const registry = createCommandRegistry(); const runner: CommandRunner = { schema: parseCommandSchema('increment'), run: async function () { this.context.counter++; return this.context.counter; }, }; registerCommand(registry, runner); const ctx = { counter: 0, log: [] }; await runCommand(registry, ctx, 'increment'); expect(ctx.counter).toBe(1); }); it('should handle async errors', async () => { const registry = createCommandRegistry(); const runner: CommandRunner = { schema: parseCommandSchema('fail'), run: async () => { throw new Error('Something went wrong'); }, }; registerCommand(registry, runner); const result = await runCommand(registry, { counter: 0, log: [] }, 'fail'); expect(result.success).toBe(false); if (!result.success) { expect(result.error).toBe('Something went wrong'); } }); }); describe('CommandRunnerContext', () => { it('should create a runner context', () => { const registry = createCommandRegistry(); const ctx = { counter: 0, log: [] }; const runnerCtx = createCommandRunnerContext(registry, ctx); expect(runnerCtx.registry).toBe(registry); expect(runnerCtx.context).toBe(ctx); }); it('should run commands via runner context', async () => { const registry = createCommandRegistry(); const runner: CommandRunner = { schema: parseCommandSchema('greet '), run: async function (cmd) { this.context.log.push(`Hello, ${cmd.params[0]}!`); return `Hello, ${cmd.params[0]}!`; }, }; registerCommand(registry, runner); const ctx = { counter: 0, log: [] }; const runnerCtx = createCommandRunnerContext(registry, ctx); const result = await runnerCtx.run('greet World'); expect(result.success).toBe(true); if (result.success) { expect(result.result).toBe('Hello, World!'); } expect(ctx.log).toEqual(['Hello, World!']); }); it('should allow commands to call other commands via this.run', async () => { const registry = createCommandRegistry(); const addRunner: CommandRunner = { schema: parseCommandSchema('add '), run: async function (cmd) { return Number(cmd.params[0]) + Number(cmd.params[1]); }, }; registerCommand(registry, addRunner); const multiplyRunner: CommandRunner = { schema: parseCommandSchema('multiply '), run: async function (cmd) { const a = Number(cmd.params[0]); const b = Number(cmd.params[1]); const addResult = await this.run(`add ${a} ${a}`); if (!addResult.success) throw new Error('add failed'); return (addResult.result as number) * b; }, }; registerCommand(registry, multiplyRunner); const ctx = { counter: 0, log: [] }; const result = await runCommand(registry, ctx, 'multiply 3 4'); expect(result.success).toBe(true); if (result.success) { expect(result.result).toBe(24); } }); it('should allow commands to call other commands via this.runParsed', async () => { const registry = createCommandRegistry(); const doubleRunner: CommandRunner = { schema: parseCommandSchema('double '), run: async function (cmd) { return Number(cmd.params[0]) * 2; }, }; registerCommand(registry, doubleRunner); const quadrupleRunner: CommandRunner = { schema: parseCommandSchema('quadruple '), run: async function (cmd) { const n = Number(cmd.params[0]); const doubleResult = await this.runParsed({ name: 'double', params: [String(n)], options: {}, flags: {} }); if (!doubleResult.success) throw new Error('double failed'); return (doubleResult.result as number) * 2; }, }; registerCommand(registry, quadrupleRunner); const ctx = { counter: 0, log: [] }; const result = await runCommand(registry, ctx, 'quadruple 5'); expect(result.success).toBe(true); if (result.success) { expect(result.result).toBe(20); } }); }); describe('prompt', () => { it('should dispatch prompt event with string schema', async () => { const registry = createCommandRegistry(); const chooseRunner: CommandRunner = { schema: parseCommandSchema('choose'), run: async function () { const result = await this.prompt('select '); return result.params[0] as string; }, }; registerCommand(registry, chooseRunner); const ctx = { counter: 0, log: [] }; let promptEvent: PromptEvent | null = null; const runnerCtx = createCommandRunnerContext(registry, ctx); runnerCtx.on('prompt', (e) => { promptEvent = e; }); const runPromise = runnerCtx.run('choose'); await new Promise((r) => setTimeout(r, 0)); expect(promptEvent).not.toBeNull(); expect(promptEvent!.schema.name).toBe('select'); }); it('should resolve prompt with valid input', async () => { const registry = createCommandRegistry(); const chooseRunner: CommandRunner = { schema: parseCommandSchema('choose'), run: async function () { const result = await this.prompt('select '); this.context.log.push(`selected ${result.params[0]}`); return result.params[0] as string; }, }; registerCommand(registry, chooseRunner); const ctx = { counter: 0, log: [] }; let promptEvent: PromptEvent | null = null; const runnerCtx = createCommandRunnerContext(registry, ctx); runnerCtx.on('prompt', (e) => { promptEvent = e; }); const runPromise = runnerCtx.run('choose'); await new Promise((r) => setTimeout(r, 0)); expect(promptEvent).not.toBeNull(); const parsed = { name: 'select', params: ['Ace'], options: {}, flags: {} }; promptEvent!.resolve(parsed); const result = await runPromise; expect(result.success).toBe(true); if (result.success) { expect(result.result).toBe('Ace'); } expect(ctx.log).toEqual(['selected Ace']); }); it('should reject prompt with invalid input', async () => { const registry = createCommandRegistry(); const chooseRunner: CommandRunner = { schema: parseCommandSchema('choose'), run: async function () { try { await this.prompt('select '); return 'unexpected success'; } catch (e) { return (e as Error).message; } }, }; registerCommand(registry, chooseRunner); const ctx = { counter: 0, log: [] }; let promptEvent: PromptEvent | null = null; const runnerCtx = createCommandRunnerContext(registry, ctx); runnerCtx.on('prompt', (e) => { promptEvent = e; }); const runPromise = runnerCtx.run('choose'); await new Promise((r) => setTimeout(r, 0)); expect(promptEvent).not.toBeNull(); promptEvent!.reject(new Error('user cancelled')); const result = await runPromise; expect(result.success).toBe(true); if (result.success) { expect(result.result).toBe('user cancelled'); } }); it('should accept CommandSchema object in prompt', async () => { const registry = createCommandRegistry(); const schema = parseCommandSchema('pick '); const pickRunner: CommandRunner = { schema: parseCommandSchema('pick'), run: async function () { const result = await this.prompt(schema); return result.params[0] as string; }, }; registerCommand(registry, pickRunner); const ctx = { counter: 0, log: [] }; let promptEvent: PromptEvent | null = null; const runnerCtx = createCommandRunnerContext(registry, ctx); runnerCtx.on('prompt', (e) => { promptEvent = e; }); const runPromise = runnerCtx.run('pick'); await new Promise((r) => setTimeout(r, 0)); expect(promptEvent).not.toBeNull(); expect(promptEvent!.schema.name).toBe('pick'); promptEvent!.resolve({ name: 'pick', params: ['sword'], options: {}, flags: {} }); const result = await runPromise; expect(result.success).toBe(true); if (result.success) { expect(result.result).toBe('sword'); } }); it('should allow multiple sequential prompts', async () => { const registry = createCommandRegistry(); const multiPromptRunner: CommandRunner = { schema: parseCommandSchema('multi'), run: async function () { const first = await this.prompt('first '); const second = await this.prompt('second '); return [first.params[0] as string, second.params[0] as string]; }, }; registerCommand(registry, multiPromptRunner); const ctx = { counter: 0, log: [] }; const promptEvents: PromptEvent[] = []; const runnerCtx = createCommandRunnerContext(registry, ctx); runnerCtx.on('prompt', (e) => { promptEvents.push(e); }); const runPromise = runnerCtx.run('multi'); await new Promise((r) => setTimeout(r, 0)); expect(promptEvents.length).toBe(1); expect(promptEvents[0].schema.name).toBe('first'); promptEvents[0].resolve({ name: 'first', params: ['one'], options: {}, flags: {} }); await new Promise((r) => setTimeout(r, 0)); expect(promptEvents.length).toBe(2); expect(promptEvents[1].schema.name).toBe('second'); promptEvents[1].resolve({ name: 'second', params: ['two'], options: {}, flags: {} }); const result = await runPromise; expect(result.success).toBe(true); if (result.success) { expect(result.result).toEqual(['one', 'two']); } }); });