/** * Commands Route 测试 * * 测试斜杠命令管理 REST API 端点 */ import { describe, it, expect, beforeEach, vi } from 'vitest'; import { Hono } from 'hono'; // Create mock command module const mockCommandRegistry = vi.hoisted(() => ({ initialize: vi.fn().mockResolvedValue(undefined), reload: vi.fn().mockResolvedValue(undefined), get: vi.fn(), getAll: vi.fn(), list: vi.fn(), search: vi.fn(), getStats: vi.fn(), })); const mockCommandExecutor = vi.hoisted(() => ({ execute: vi.fn(), })); const mockCommandManager = vi.hoisted(() => ({ create: vi.fn(), update: vi.fn(), delete: vi.fn(), getContent: vi.fn(), })); const mockCommandModule = vi.hoisted(() => ({ getCommandRegistry: vi.fn(() => mockCommandRegistry), createCommandExecutor: vi.fn(() => mockCommandExecutor), createCommandManager: vi.fn(() => mockCommandManager), })); vi.mock('@ai-assistant/core', () => mockCommandModule); vi.mock('../../../src/routes/config.js', () => ({ getConfig: vi.fn(() => ({ workdir: '/test/workdir' })), })); import { commandsRouter } from '../../../src/routes/commands.js'; // Create test app const app = new Hono(); app.route('/commands', commandsRouter); describe('Commands Route', () => { beforeEach(() => { vi.clearAllMocks(); mockCommandRegistry.list.mockReturnValue([]); mockCommandRegistry.getStats.mockReturnValue({ total: 0, bySource: {} }); }); describe('GET /commands - 列出所有命令', () => { it('返回命令列表', async () => { mockCommandRegistry.list.mockReturnValue([ { name: 'test', description: 'Test command', source: 'builtin' }, { name: 'deploy', description: 'Deploy command', source: 'project' }, ]); mockCommandRegistry.getStats.mockReturnValue({ total: 2, bySource: { builtin: 1, project: 1 }, }); const res = await app.request('/commands'); const json = await res.json(); expect(res.status).toBe(200); expect(json.success).toBe(true); expect(json.data.commands).toHaveLength(2); expect(json.data.stats.total).toBe(2); }); it('返回空列表', async () => { const res = await app.request('/commands'); const json = await res.json(); expect(res.status).toBe(200); expect(json.data.commands).toEqual([]); }); }); describe('POST /commands/search - 搜索命令', () => { it('搜索返回结果', async () => { mockCommandRegistry.search.mockReturnValue([ { command: { name: 'test', description: 'Test', source: 'builtin', template: 'echo test' }, score: 0.9, }, ]); const res = await app.request('/commands/search', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ query: 'test', limit: 10 }), }); const json = await res.json(); expect(res.status).toBe(200); expect(json.success).toBe(true); expect(json.data).toHaveLength(1); expect(json.data[0].score).toBe(0.9); }); it('缺少查询参数返回 400', async () => { const res = await app.request('/commands/search', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({}), }); const json = await res.json(); expect(res.status).toBe(400); expect(json.success).toBe(false); }); }); describe('POST /commands/reload - 重新加载命令', () => { it('重新加载成功', async () => { mockCommandRegistry.getStats.mockReturnValue({ total: 5, bySource: { builtin: 3, project: 2 }, }); const res = await app.request('/commands/reload', { method: 'POST' }); const json = await res.json(); expect(res.status).toBe(200); expect(json.success).toBe(true); expect(json.data.message).toBe('Commands reloaded'); expect(mockCommandRegistry.reload).toHaveBeenCalled(); }); }); describe('GET /commands/:name - 获取命令详情', () => { it('返回命令详情', async () => { mockCommandRegistry.get.mockReturnValue({ name: 'test', description: 'Test command', template: 'echo test', source: 'builtin', }); const res = await app.request('/commands/test'); const json = await res.json(); expect(res.status).toBe(200); expect(json.success).toBe(true); expect(json.data.name).toBe('test'); expect(json.data.hasTemplate).toBe(true); }); it('命令不存在返回 404', async () => { mockCommandRegistry.get.mockReturnValue(undefined); const res = await app.request('/commands/non-existent'); const json = await res.json(); expect(res.status).toBe(404); expect(json.success).toBe(false); expect(json.error).toContain('not found'); }); it('支持包含斜杠的命令名', async () => { mockCommandRegistry.get.mockReturnValue({ name: 'deploy/staging', description: 'Deploy to staging', template: 'deploy staging', source: 'project', }); const res = await app.request('/commands/deploy/staging'); const json = await res.json(); expect(res.status).toBe(200); expect(json.data.name).toBe('deploy/staging'); }); }); describe('POST /commands/:name/execute - 执行命令', () => { it('执行成功', async () => { mockCommandRegistry.get.mockReturnValue({ name: 'test', template: 'echo {{args}}', source: 'builtin', }); mockCommandExecutor.execute.mockResolvedValue({ success: true, prompt: 'echo hello', }); const res = await app.request('/commands/test/execute', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ arguments: 'hello' }), }); const json = await res.json(); expect(res.status).toBe(200); expect(json.success).toBe(true); expect(json.data.prompt).toBe('echo hello'); }); it('执行失败返回 400', async () => { mockCommandExecutor.execute.mockResolvedValue({ success: false, error: 'Command not found', }); const res = await app.request('/commands/test/execute', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({}), }); const json = await res.json(); expect(res.status).toBe(400); expect(json.success).toBe(false); }); it('无请求体时默认空参数', async () => { mockCommandExecutor.execute.mockResolvedValue({ success: true, prompt: 'test', }); const res = await app.request('/commands/test/execute', { method: 'POST', }); const json = await res.json(); expect(res.status).toBe(200); expect(mockCommandExecutor.execute).toHaveBeenCalled(); }); }); describe('POST /commands - 创建命令', () => { it('创建成功', async () => { mockCommandManager.create.mockResolvedValue({ success: true, path: '/test/.claude/commands/my-command.md', }); const res = await app.request('/commands', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: 'my-command', description: 'My command', template: 'echo hello', scope: 'project', }), }); const json = await res.json(); expect(res.status).toBe(200); expect(json.success).toBe(true); expect(json.data.name).toBe('my-command'); }); it('缺少必需字段返回 400', async () => { const res = await app.request('/commands', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: 'test' }), }); const json = await res.json(); expect(res.status).toBe(400); expect(json.success).toBe(false); }); it('创建失败返回错误', async () => { mockCommandManager.create.mockResolvedValue({ success: false, error: 'Command already exists', }); const res = await app.request('/commands', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: 'existing', template: 'echo test', scope: 'project', }), }); const json = await res.json(); expect(res.status).toBe(400); expect(json.error).toContain('already exists'); }); }); describe('GET /commands/:name/content - 获取命令完整内容', () => { it('获取成功,返回包含 template 的完整内容', async () => { mockCommandManager.getContent.mockResolvedValue({ success: true, data: { name: 'test', description: 'Test command', template: 'echo test', source: 'project', sourcePath: '/test/.claude/commands/test.md', }, }); const res = await app.request('/commands/test/content'); const json = await res.json(); expect(res.status).toBe(200); expect(json.success).toBe(true); expect(json.data.template).toBe('echo test'); expect(json.data.name).toBe('test'); }); it('支持包含斜杠的命令名', async () => { mockCommandManager.getContent.mockResolvedValue({ success: true, data: { name: 'deploy/staging', template: 'deploy to staging', source: 'project', }, }); const res = await app.request('/commands/deploy/staging/content'); const json = await res.json(); expect(res.status).toBe(200); expect(json.success).toBe(true); expect(json.data.template).toBe('deploy to staging'); }); it('命令不存在返回 404', async () => { mockCommandManager.getContent.mockResolvedValue({ success: false, error: 'Command not found: non-existent', }); const res = await app.request('/commands/non-existent/content'); const json = await res.json(); expect(res.status).toBe(404); expect(json.success).toBe(false); expect(json.error).toContain('not found'); }); }); describe('PUT /commands/:name - 更新命令', () => { it('更新成功', async () => { mockCommandManager.update.mockResolvedValue({ success: true, path: '/test/.claude/commands/test.md', }); const res = await app.request('/commands/test', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ template: 'echo updated' }), }); const json = await res.json(); expect(res.status).toBe(200); expect(json.success).toBe(true); }); it('更新失败返回 400', async () => { mockCommandManager.update.mockResolvedValue({ success: false, error: 'Cannot update builtin command', }); const res = await app.request('/commands/builtin-cmd', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ template: 'new template' }), }); const json = await res.json(); expect(res.status).toBe(400); expect(json.success).toBe(false); }); }); describe('DELETE /commands/:name - 删除命令', () => { it('删除成功', async () => { mockCommandManager.delete.mockResolvedValue({ success: true, path: '/test/.claude/commands/test.md', }); const res = await app.request('/commands/test', { method: 'DELETE', }); const json = await res.json(); expect(res.status).toBe(200); expect(json.success).toBe(true); }); it('删除失败返回 400', async () => { mockCommandManager.delete.mockResolvedValue({ success: false, error: 'Cannot delete builtin command', }); const res = await app.request('/commands/builtin-cmd', { method: 'DELETE', }); const json = await res.json(); expect(res.status).toBe(400); expect(json.success).toBe(false); }); }); });