import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'; import * as fs from 'fs/promises'; import * as childProcess from 'child_process'; import { CommandExecutor, createCommandExecutor, getCommandRegistry, resetCommandRegistry, } from '../../../src/commands/index.js'; import type { Command } from '../../../src/commands/types.js'; // Mock fs vi.mock('fs/promises', () => ({ readFile: vi.fn(), })); // Mock child_process vi.mock('child_process', () => ({ exec: vi.fn(), })); // Mock util.promisify to return our mocked exec vi.mock('util', () => ({ promisify: (fn: unknown) => { if (fn === childProcess.exec) { return vi.fn().mockImplementation((cmd: string, _options: unknown) => { // 默认返回命令输出 return Promise.resolve({ stdout: `output of: ${cmd}`, stderr: '' }); }); } return fn; }, })); // Mock loader and builtin commands vi.mock('../../../src/commands/loader.js', () => ({ commandLoader: { loadFromDirectory: vi.fn().mockResolvedValue([]), getUserCommandsDir: vi.fn().mockReturnValue('/mock/user/commands'), getProjectCommandsDir: vi.fn().mockReturnValue('/mock/project/commands'), }, })); vi.mock('../../../src/commands/builtin/index.js', () => ({ builtinCommands: [], })); describe('CommandExecutor - Command 执行器', () => { let executor: CommandExecutor; beforeEach(() => { resetCommandRegistry(); executor = new CommandExecutor('/test/workdir'); }); afterEach(() => { vi.clearAllMocks(); }); describe('parseInput - 解析输入', () => { it('解析简单命令', () => { const result = executor.parseInput('/test'); expect(result).not.toBeNull(); expect(result?.command).toBe('test'); expect(result?.arguments).toBe(''); expect(result?.args).toEqual([]); }); it('解析带参数的命令', () => { const result = executor.parseInput('/review main..feature'); expect(result).not.toBeNull(); expect(result?.command).toBe('review'); expect(result?.arguments).toBe('main..feature'); expect(result?.args).toEqual(['main..feature']); }); it('解析多个参数', () => { const result = executor.parseInput('/deploy staging production'); expect(result).not.toBeNull(); expect(result?.command).toBe('deploy'); expect(result?.arguments).toBe('staging production'); expect(result?.args).toEqual(['staging', 'production']); }); it('解析带引号的参数', () => { const result = executor.parseInput('/commit "fix: bug fix"'); expect(result).not.toBeNull(); expect(result?.command).toBe('commit'); expect(result?.args).toEqual(['fix: bug fix']); }); it('解析带单引号的参数', () => { const result = executor.parseInput("/test 'hello world' foo"); expect(result).not.toBeNull(); expect(result?.args).toEqual(['hello world', 'foo']); }); it('不是命令时返回 null', () => { const result = executor.parseInput('not a command'); expect(result).toBeNull(); }); it('空输入返回 null', () => { const result = executor.parseInput(''); expect(result).toBeNull(); }); it('包含工作目录', () => { const result = executor.parseInput('/test'); expect(result?.workdir).toBe('/test/workdir'); }); }); describe('execute - 执行命令', () => { beforeEach(async () => { const registry = getCommandRegistry(); await registry.initialize('/test'); // 手动注册测试命令 const testCommand: Command = { name: 'greet', description: '打招呼', template: 'Hello, $ARGUMENTS!', source: 'builtin', }; registry.register(testCommand); }); it('执行存在的命令', async () => { const input = executor.parseInput('/greet World'); expect(input).not.toBeNull(); const result = await executor.execute(input!); expect(result.success).toBe(true); expect(result.prompt).toBe('Hello, World!'); }); it('命令不存在时返回错误', async () => { const input = executor.parseInput('/nonexistent'); expect(input).not.toBeNull(); const result = await executor.execute(input!); expect(result.success).toBe(false); expect(result.error).toContain('Command 不存在'); }); it('返回 agent 配置', async () => { const registry = getCommandRegistry(); const agentCommand: Command = { name: 'code', description: '编码', template: 'Write code', agent: 'coder', source: 'builtin', }; registry.register(agentCommand); const input = executor.parseInput('/code'); const result = await executor.execute(input!); expect(result.success).toBe(true); expect(result.agent).toBe('coder'); }); it('返回 model 配置', async () => { const registry = getCommandRegistry(); const modelCommand: Command = { name: 'complex', description: '复杂任务', template: 'Complex task', model: 'opus', source: 'builtin', }; registry.register(modelCommand); const input = executor.parseInput('/complex'); const result = await executor.execute(input!); expect(result.success).toBe(true); expect(result.model).toBe('opus'); }); }); describe('renderTemplate - 模板渲染', () => { it('替换 $ARGUMENTS', async () => { const input = { command: 'test', arguments: 'hello world', args: ['hello', 'world'], workdir: '/test', }; const result = await executor.renderTemplate('Say: $ARGUMENTS', input); expect(result).toBe('Say: hello world'); }); it('替换位置参数 $1(单个参数时获取全部)', async () => { const input = { command: 'test', arguments: 'foo bar', args: ['foo', 'bar'], workdir: '/test', }; // 当 $1 是最大参数索引时,获取所有剩余参数 const result = await executor.renderTemplate('First: $1', input); expect(result).toBe('First: foo bar'); }); it('替换多个位置参数', async () => { const input = { command: 'test', arguments: 'a b c', args: ['a', 'b', 'c'], workdir: '/test', }; const result = await executor.renderTemplate('$1 and $2', input); expect(result).toBe('a and b c'); }); it('最后一个位置参数获取剩余所有', async () => { const input = { command: 'test', arguments: 'a b c d', args: ['a', 'b', 'c', 'd'], workdir: '/test', }; const result = await executor.renderTemplate('First: $1, Rest: $2', input); expect(result).toBe('First: a, Rest: b c d'); }); it('缺少参数时替换为空字符串', async () => { const input = { command: 'test', arguments: '', args: [], workdir: '/test', }; const result = await executor.renderTemplate('Value: $1', input); expect(result).toBe('Value: '); }); it('处理文件引用 @filepath', async () => { const mockContent = 'file content here'; vi.mocked(fs.readFile).mockResolvedValue(mockContent); const input = { command: 'test', arguments: '', args: [], workdir: '/test', }; const result = await executor.renderTemplate('Review: @src/file.ts', input); expect(result).toContain('file content here'); expect(result).toContain('```ts'); }); it('文件不存在时显示提示', async () => { vi.mocked(fs.readFile).mockRejectedValue(new Error('ENOENT')); const input = { command: 'test', arguments: '', args: [], workdir: '/test', }; const result = await executor.renderTemplate('Review: @nonexistent.ts', input); expect(result).toContain('[文件不存在: nonexistent.ts]'); }); }); describe('createCommandExecutor - 工厂函数', () => { it('创建执行器实例', () => { const exec = createCommandExecutor('/custom/path'); expect(exec).toBeInstanceOf(CommandExecutor); }); it('默认使用 process.cwd', () => { const exec = createCommandExecutor(); const input = exec.parseInput('/test'); expect(input?.workdir).toBe(process.cwd()); }); }); });