1b7d55848d
- 删除 Server 中 60+ 个与 Core 重复的类型定义 - 将动态导入 (await import) 改为静态类型导入 (import type) - 保留必要的运行时静态导入 - 修复测试文件中的 mock 初始化问题 - 净删除约 960 行重复代码 重构文件: - routes/checkpoints.ts: 删除 155 行重复类型 - routes/agents.ts: 删除 93 行重复类型 - routes/commands.ts: 删除 83 行重复类型 - routes/mcp.ts: 修复类型窄化 - routes/hooks.ts: 已使用静态导入 - routes/providers.ts: 删除 63 行重复类型 - session/manager.ts: 删除 41 行重复类型 - routes/sessions.ts: 添加类型导入 - permission/handler.ts: 添加类型导入
421 lines
12 KiB
TypeScript
421 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),
|
|
}));
|
|
|
|
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);
|
|
});
|
|
});
|
|
});
|