/** * Agents Route 测试 * * 测试 Agent 预设管理 REST API 端点 */ import { describe, it, expect, beforeEach, vi } from 'vitest'; import { Hono } from 'hono'; // Create mock agent module const mockAgentRegistry = vi.hoisted(() => ({ init: vi.fn().mockResolvedValue(undefined), get: vi.fn(), list: vi.fn(), })); const mockPresetAgents = vi.hoisted(() => ({ general: { description: 'General purpose agent', mode: 'subagent' as const, model: { model: 'claude-3-5-sonnet' }, }, explore: { description: 'Code exploration agent', mode: 'subagent' as const, }, })); const mockAgentModule = vi.hoisted(() => ({ agentRegistry: mockAgentRegistry, loadAgentConfig: vi.fn(), saveAgentConfig: vi.fn(), presetAgents: mockPresetAgents, isPresetAgent: vi.fn((name: string) => name in mockPresetAgents), })); vi.mock('@ai-assistant/core', () => mockAgentModule); vi.mock('../../../src/routes/config.js', () => ({ getConfig: vi.fn(() => ({ workdir: '/test/workdir' })), })); import { agentsRouter } from '../../../src/routes/agents.js'; // Create test app const app = new Hono(); app.route('/agents', agentsRouter); describe('Agents Route', () => { beforeEach(() => { vi.clearAllMocks(); mockAgentModule.loadAgentConfig.mockResolvedValue(null); mockAgentModule.saveAgentConfig.mockResolvedValue(undefined); }); describe('GET /agents - 获取所有 Agent 列表', () => { it('返回 Agent 列表', async () => { mockAgentRegistry.list.mockReturnValue([ { name: 'general', description: 'General purpose agent', mode: 'subagent' }, { name: 'explore', description: 'Code exploration agent', mode: 'subagent' }, ]); const res = await app.request('/agents'); const json = await res.json(); expect(res.status).toBe(200); expect(json.success).toBe(true); expect(json.data).toHaveLength(2); expect(json.data[0].name).toBe('general'); }); it('返回空列表', async () => { mockAgentRegistry.list.mockReturnValue([]); const res = await app.request('/agents'); const json = await res.json(); expect(res.status).toBe(200); expect(json.data).toEqual([]); }); it('包含 isPreset 和 isCustomized 标记', async () => { mockAgentRegistry.list.mockReturnValue([ { name: 'general', description: 'General purpose agent', mode: 'subagent' }, ]); mockAgentModule.loadAgentConfig.mockResolvedValue({ agents: { general: { model: { model: 'custom-model' } } }, }); const res = await app.request('/agents'); const json = await res.json(); expect(json.data[0].isPreset).toBe(true); expect(json.data[0].isCustomized).toBe(true); }); }); describe('GET /agents/presets - 获取预设 Agent 列表', () => { it('返回预设列表', async () => { const res = await app.request('/agents/presets'); const json = await res.json(); expect(res.status).toBe(200); expect(json.success).toBe(true); expect(json.data).toHaveLength(2); expect(json.data.some((a: { name: string }) => a.name === 'general')).toBe(true); expect(json.data.some((a: { name: string }) => a.name === 'explore')).toBe(true); }); it('预设都标记为 isPreset: true', async () => { const res = await app.request('/agents/presets'); const json = await res.json(); expect(json.data.every((a: { isPreset: boolean }) => a.isPreset === true)).toBe(true); }); }); describe('GET /agents/defaults - 获取全局默认配置', () => { it('返回默认配置', async () => { mockAgentModule.loadAgentConfig.mockResolvedValue({ defaults: { maxSteps: 10, model: { model: 'claude-3-5-sonnet' }, }, }); const res = await app.request('/agents/defaults'); const json = await res.json(); expect(res.status).toBe(200); expect(json.success).toBe(true); expect(json.data.maxSteps).toBe(10); }); it('无配置时返回空对象', async () => { mockAgentModule.loadAgentConfig.mockResolvedValue(null); const res = await app.request('/agents/defaults'); const json = await res.json(); expect(res.status).toBe(200); expect(json.data).toEqual({}); }); }); describe('PUT /agents/defaults - 更新全局默认配置', () => { it('更新成功', async () => { const res = await app.request('/agents/defaults', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ maxSteps: 20 }), }); const json = await res.json(); expect(res.status).toBe(200); expect(json.success).toBe(true); expect(mockAgentModule.saveAgentConfig).toHaveBeenCalled(); }); }); describe('GET /agents/:name - 获取单个 Agent 详情', () => { it('返回 Agent 详情', async () => { mockAgentRegistry.get.mockReturnValue({ name: 'general', description: 'General purpose agent', mode: 'subagent', }); const res = await app.request('/agents/general'); const json = await res.json(); expect(res.status).toBe(200); expect(json.success).toBe(true); expect(json.data.name).toBe('general'); expect(json.data.isPreset).toBe(true); }); it('Agent 不存在返回 404', async () => { mockAgentRegistry.get.mockReturnValue(undefined); const res = await app.request('/agents/non-existent'); const json = await res.json(); expect(res.status).toBe(404); expect(json.success).toBe(false); expect(json.error).toContain('not found'); }); }); describe('POST /agents - 创建新 Agent', () => { it('创建成功', async () => { mockAgentRegistry.get.mockReturnValue({ name: 'my-agent', description: 'My custom agent', mode: 'subagent', }); const res = await app.request('/agents', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: 'my-agent', description: 'My custom agent', mode: 'subagent', }), }); const json = await res.json(); expect(res.status).toBe(200); expect(json.success).toBe(true); expect(mockAgentModule.saveAgentConfig).toHaveBeenCalled(); }); it('缺少名称返回 400', async () => { const res = await app.request('/agents', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ description: 'No name agent' }), }); const json = await res.json(); expect(res.status).toBe(400); expect(json.success).toBe(false); expect(json.error).toContain('name is required'); }); it('名称与预设冲突返回 400', async () => { const res = await app.request('/agents', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: 'general', description: 'Trying to create preset', mode: 'subagent', }), }); const json = await res.json(); expect(res.status).toBe(400); expect(json.error).toContain('preset name'); }); it('Agent 已存在返回 409', async () => { mockAgentModule.loadAgentConfig.mockResolvedValue({ agents: { 'my-agent': { description: 'Existing' } }, }); const res = await app.request('/agents', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: 'my-agent', description: 'Duplicate', mode: 'subagent', }), }); const json = await res.json(); expect(res.status).toBe(409); expect(json.error).toContain('already exists'); }); }); describe('PUT /agents/:name - 更新 Agent', () => { it('更新成功', async () => { mockAgentRegistry.get.mockReturnValue({ name: 'general', description: 'Updated description', mode: 'subagent', }); const res = await app.request('/agents/general', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ description: 'Updated description', mode: 'subagent', }), }); const json = await res.json(); expect(res.status).toBe(200); expect(json.success).toBe(true); expect(json.data.isCustomized).toBe(true); }); }); describe('DELETE /agents/:name - 删除 Agent', () => { it('删除自定义 Agent 成功', async () => { mockAgentModule.loadAgentConfig.mockResolvedValue({ agents: { 'my-agent': { description: 'Custom agent' } }, }); const res = await app.request('/agents/my-agent', { method: 'DELETE', }); const json = await res.json(); expect(res.status).toBe(200); expect(json.success).toBe(true); expect(mockAgentModule.saveAgentConfig).toHaveBeenCalled(); }); it('删除预设 Agent 返回 400', async () => { mockAgentModule.loadAgentConfig.mockResolvedValue({ agents: {} }); const res = await app.request('/agents/general', { method: 'DELETE', }); const json = await res.json(); expect(res.status).toBe(400); expect(json.error).toContain('Cannot delete preset'); }); it('Agent 不存在返回 404', async () => { mockAgentModule.loadAgentConfig.mockResolvedValue({ agents: {} }); mockAgentModule.isPresetAgent.mockReturnValue(false); const res = await app.request('/agents/non-existent', { method: 'DELETE', }); const json = await res.json(); expect(res.status).toBe(404); expect(json.error).toContain('not found'); }); }); });