Files
ai-terminal-assistant/packages/server/tests/unit/routes/commands.test.ts
T
kurihada 67e129fce2 fix(commands): 修复路由顺序导致 /content 端点被遮蔽的问题
- 将 /:name{.+}/execute 和 /:name{.+}/content 路由移到 /:name{.+} 之前
- Hono 的 {.+} 模式会贪婪匹配,导致 /test/content 被解析为 name='test/content'
- 更新测试用例以正确测试 /content 端点功能
2025-12-15 00:17:22 +08:00

430 lines
12 KiB
TypeScript

/**
* 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),
}));
// Track if module should be available
let moduleAvailable = true;
vi.mock('@ai-assistant/core', () => {
if (!moduleAvailable) {
throw new Error('Module not found');
}
return 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();
moduleAvailable = true;
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);
});
});
});