/** * Tests for Phase 4: Agent Swarm + Skill Discovery */ import { describe, it, expect, beforeEach, vi } from 'vitest'; import { AgentSwarm, resetAgentSwarm, getAgentSwarm, AgentExecutor, } from '../../desktop/src/lib/agent-swarm'; import { SkillDiscoveryEngine, resetSkillDiscovery, getSkillDiscovery, } from '../../desktop/src/lib/skill-discovery'; import { MemoryManager, resetMemoryManager } from '../../desktop/src/lib/agent-memory'; // === localStorage mock === const store: Record = {}; const localStorageMock = { getItem: (key: string) => store[key] ?? null, setItem: (key: string, value: string) => { store[key] = value; }, removeItem: (key: string) => { delete store[key]; }, clear: () => { for (const k of Object.keys(store)) delete store[k]; }, get length() { return Object.keys(store).length; }, key: (i: number) => Object.keys(store)[i] ?? null, }; Object.defineProperty(globalThis, 'localStorage', { value: localStorageMock, writable: true }); // === Agent Swarm Tests === describe('AgentSwarm', () => { let swarm: AgentSwarm; let mockExecutor: ReturnType>; beforeEach(() => { localStorageMock.clear(); resetAgentSwarm(); resetMemoryManager(); mockExecutor = vi.fn( async (agentId: string, prompt: string, _context?: string) => `[${agentId}] 完成: ${prompt.slice(0, 30)}` ); swarm = new AgentSwarm({ coordinator: 'main-agent', specialists: [ { agentId: 'dev-agent', role: '开发工程师', capabilities: ['coding', 'review'] }, { agentId: 'pm-agent', role: '产品经理', capabilities: ['planning', 'requirements'] }, { agentId: 'qa-agent', role: '测试工程师', capabilities: ['testing', 'qa'] }, ], }); swarm.setExecutor(mockExecutor as unknown as AgentExecutor); }); describe('createTask', () => { it('creates task with auto decomposition', () => { const task = swarm.createTask('实现用户登录功能'); expect(task.id).toMatch(/^swarm_/); expect(task.description).toBe('实现用户登录功能'); expect(task.status).toBe('planning'); expect(task.subtasks.length).toBe(3); // one per specialist }); it('creates task with manual subtasks', () => { const task = swarm.createTask('发布新版本', { subtasks: [ { assignedTo: 'dev-agent', description: '打包构建' }, { assignedTo: 'qa-agent', description: '回归测试' }, ], }); expect(task.subtasks.length).toBe(2); expect(task.subtasks[0].assignedTo).toBe('dev-agent'); expect(task.subtasks[1].assignedTo).toBe('qa-agent'); }); it('creates task with custom communication style', () => { const task = swarm.createTask('讨论技术选型', { communicationStyle: 'debate', }); expect(task.communicationStyle).toBe('debate'); }); it('falls back to coordinator when no specialists', () => { const emptySwarm = new AgentSwarm({ coordinator: 'solo' }); emptySwarm.setExecutor(mockExecutor as unknown as AgentExecutor); const task = emptySwarm.createTask('单人任务'); expect(task.subtasks.length).toBe(1); expect(task.subtasks[0].assignedTo).toBe('solo'); }); }); describe('execute - sequential', () => { it('executes subtasks in order with context chaining', async () => { const task = swarm.createTask('设计并实现API', { communicationStyle: 'sequential', subtasks: [ { assignedTo: 'pm-agent', description: '需求分析' }, { assignedTo: 'dev-agent', description: '编码实现' }, ], }); const result = await swarm.execute(task); expect(result.task.status).toBe('done'); expect(mockExecutor).toHaveBeenCalledTimes(2); // 2 subtasks // First call has empty context expect(mockExecutor.mock.calls[0][2]).toBe(''); // Second call has previous result as context expect(mockExecutor.mock.calls[1][2]).toContain('前一个Agent的输出'); }); it('handles subtask failure gracefully', async () => { mockExecutor.mockImplementationOnce(async () => { throw new Error('Agent offline'); }); const task = swarm.createTask('有风险的任务', { subtasks: [ { assignedTo: 'dev-agent', description: '可能失败的任务' }, { assignedTo: 'qa-agent', description: '后续任务' }, ], }); const result = await swarm.execute(task); expect(result.task.subtasks[0].status).toBe('failed'); expect(result.task.subtasks[0].error).toBe('Agent offline'); expect(result.task.status).toBe('done'); // task still completes }); }); describe('execute - parallel', () => { it('executes all subtasks simultaneously', async () => { const task = swarm.createTask('全方位分析', { communicationStyle: 'parallel', }); const result = await swarm.execute(task); expect(result.task.status).toBe('done'); expect(result.participantCount).toBe(3); // All subtasks should be done const doneCount = result.task.subtasks.filter(s => s.status === 'done').length; expect(doneCount).toBe(3); }); it('continues even if some parallel subtasks fail', async () => { mockExecutor .mockImplementationOnce(async () => 'success-1') .mockImplementationOnce(async () => { throw new Error('fail'); }) .mockImplementationOnce(async () => 'success-2'); const task = swarm.createTask('混合结果', { communicationStyle: 'parallel' }); const result = await swarm.execute(task); const done = result.task.subtasks.filter(s => s.status === 'done'); const failed = result.task.subtasks.filter(s => s.status === 'failed'); expect(done.length).toBe(2); expect(failed.length).toBe(1); }); }); describe('execute - debate', () => { it('runs multiple rounds of debate', async () => { const task = swarm.createTask('选择数据库: PostgreSQL vs MongoDB', { communicationStyle: 'debate', }); const result = await swarm.execute(task); expect(result.task.status).toBe('done'); // Should have subtasks from multiple rounds expect(result.task.subtasks.length).toBeGreaterThanOrEqual(3); }); it('stops early on consensus', async () => { // Make all agents return identical responses to trigger consensus mockExecutor.mockImplementation(async () => '我建议使用 PostgreSQL 因为它支持 JSONB 和强一致性'); const task = swarm.createTask('数据库选型', { communicationStyle: 'debate' }); const result = await swarm.execute(task); expect(result.task.status).toBe('done'); // Should stop before max rounds due to consensus }); }); describe('history', () => { it('stores executed tasks in history', async () => { const task1 = swarm.createTask('任务1', { subtasks: [{ assignedTo: 'dev-agent', description: '小任务' }], }); await swarm.execute(task1); const history = swarm.getHistory(); expect(history.length).toBe(1); expect(history[0].description).toBe('任务1'); }); it('retrieves task by ID', async () => { const task = swarm.createTask('查找任务', { subtasks: [{ assignedTo: 'dev-agent', description: '测试' }], }); await swarm.execute(task); const found = swarm.getTask(task.id); expect(found).toBeDefined(); expect(found!.description).toBe('查找任务'); }); it('persists history to localStorage', async () => { const task = swarm.createTask('持久化测试', { subtasks: [{ assignedTo: 'dev-agent', description: '测试' }], }); await swarm.execute(task); // Create new instance — should load from localStorage const swarm2 = new AgentSwarm(); const history = swarm2.getHistory(); expect(history.length).toBe(1); }); }); describe('specialist management', () => { it('lists specialists', () => { expect(swarm.getSpecialists().length).toBe(3); }); it('adds a specialist', () => { swarm.addSpecialist({ agentId: 'design-agent', role: '设计师', capabilities: ['UI', 'UX'] }); expect(swarm.getSpecialists().length).toBe(4); }); it('updates existing specialist', () => { swarm.addSpecialist({ agentId: 'dev-agent', role: '高级开发', capabilities: ['coding', 'architecture'] }); const specs = swarm.getSpecialists(); expect(specs.length).toBe(3); expect(specs.find(s => s.agentId === 'dev-agent')!.role).toBe('高级开发'); }); it('removes a specialist', () => { swarm.removeSpecialist('qa-agent'); expect(swarm.getSpecialists().length).toBe(2); }); }); describe('config', () => { it('returns current config', () => { const config = swarm.getConfig(); expect(config.coordinator).toBe('main-agent'); expect(config.specialists.length).toBe(3); }); it('updates config', () => { swarm.updateConfig({ maxRoundsDebate: 5 }); expect(swarm.getConfig().maxRoundsDebate).toBe(5); }); }); describe('singleton', () => { it('returns same instance', () => { const a = getAgentSwarm(); const b = getAgentSwarm(); expect(a).toBe(b); }); it('resets singleton', () => { const a = getAgentSwarm(); resetAgentSwarm(); const b = getAgentSwarm(); expect(a).not.toBe(b); }); }); describe('error handling', () => { it('throws if no executor set', async () => { const noExecSwarm = new AgentSwarm(); const task = noExecSwarm.createTask('无执行器'); await expect(noExecSwarm.execute(task)).rejects.toThrow('No executor'); }); }); }); // === Skill Discovery Tests === describe('SkillDiscoveryEngine', () => { let engine: SkillDiscoveryEngine; beforeEach(() => { localStorageMock.clear(); resetSkillDiscovery(); resetMemoryManager(); engine = new SkillDiscoveryEngine(); }); describe('searchSkills', () => { it('returns all skills for empty query', () => { const result = engine.searchSkills(''); expect(result.results.length).toBeGreaterThan(0); expect(result.totalAvailable).toBe(result.results.length); }); it('finds skills by name', () => { const result = engine.searchSkills('Code Review'); expect(result.results.length).toBeGreaterThan(0); expect(result.results[0].id).toBe('code-review'); }); it('finds skills by Chinese trigger', () => { const result = engine.searchSkills('审查代码'); expect(result.results.length).toBeGreaterThan(0); expect(result.results[0].id).toBe('code-review'); }); it('finds skills by capability', () => { const result = engine.searchSkills('安全审计'); expect(result.results.length).toBeGreaterThan(0); const ids = result.results.map(s => s.id); expect(ids).toContain('security-engineer'); }); it('finds skills by category keyword', () => { const result = engine.searchSkills('development'); expect(result.results.length).toBeGreaterThan(0); }); it('returns empty for non-matching query', () => { const result = engine.searchSkills('量子计算'); expect(result.results.length).toBe(0); }); it('ranks exact trigger match higher', () => { const result = engine.searchSkills('git'); expect(result.results.length).toBeGreaterThan(0); expect(result.results[0].id).toBe('git'); }); }); describe('suggestSkills', () => { it('suggests skills based on conversation content', async () => { const conversations = [ { role: 'user', content: '帮我审查一下这段代码的安全性' }, { role: 'assistant', content: '好的,我来检查...' }, { role: 'user', content: '还需要做一下API测试' }, ]; const suggestions = await engine.suggestSkills(conversations, 'agent-1'); expect(suggestions.length).toBeGreaterThan(0); // Should suggest security or code review related skills const ids = suggestions.map(s => s.skill.id); expect(ids.some(id => ['code-review', 'security-engineer', 'api-tester'].includes(id))).toBe(true); }); it('returns empty for unrelated conversations', async () => { const conversations = [ { role: 'user', content: '今天天气真好' }, { role: 'assistant', content: '是的' }, ]; const suggestions = await engine.suggestSkills(conversations, 'agent-1'); // May or may not have suggestions, but shouldn't crash expect(Array.isArray(suggestions)).toBe(true); }); it('limits results to specified count', async () => { const conversations = [ { role: 'user', content: '帮我做代码审查、数据分析、API测试、安全检查、前端开发、写文章' }, ]; const suggestions = await engine.suggestSkills(conversations, 'agent-1', 3); expect(suggestions.length).toBeLessThanOrEqual(3); }); it('includes confidence score and reason', async () => { const conversations = [ { role: 'user', content: '帮我审查代码' }, ]; const suggestions = await engine.suggestSkills(conversations, 'agent-1'); if (suggestions.length > 0) { expect(suggestions[0].confidence).toBeGreaterThan(0); expect(suggestions[0].confidence).toBeLessThanOrEqual(1); expect(suggestions[0].reason.length).toBeGreaterThan(0); expect(suggestions[0].matchedPatterns.length).toBeGreaterThan(0); } }); }); describe('skill management', () => { it('gets all skills', () => { const skills = engine.getAllSkills(); expect(skills.length).toBeGreaterThan(0); }); it('filters by category', () => { const devSkills = engine.getSkillsByCategory('development'); expect(devSkills.length).toBeGreaterThan(0); expect(devSkills.every(s => s.category === 'development')).toBe(true); }); it('lists categories', () => { const categories = engine.getCategories(); expect(categories.length).toBeGreaterThan(0); expect(categories).toContain('development'); }); it('registers a new skill', () => { const countBefore = engine.getAllSkills().length; engine.registerSkill({ id: 'custom-skill', name: 'Custom Skill', description: 'A custom skill', triggers: ['custom'], capabilities: ['custom-work'], toolDeps: [], installed: false, category: 'custom', }); expect(engine.getAllSkills().length).toBe(countBefore + 1); }); it('updates existing skill on re-register', () => { engine.registerSkill({ id: 'code-review', name: 'Code Review Pro', description: 'Enhanced code review', triggers: ['审查代码'], capabilities: ['深度分析'], toolDeps: ['read'], installed: true, category: 'development', }); const skill = engine.getAllSkills().find(s => s.id === 'code-review'); expect(skill!.name).toBe('Code Review Pro'); }); it('toggles install status', () => { engine.setSkillInstalled('code-review', false); const skill = engine.getAllSkills().find(s => s.id === 'code-review'); expect(skill!.installed).toBe(false); engine.setSkillInstalled('code-review', true); const skill2 = engine.getAllSkills().find(s => s.id === 'code-review'); expect(skill2!.installed).toBe(true); }); }); describe('persistence', () => { it('persists skills to localStorage', () => { engine.registerSkill({ id: 'persist-test', name: 'Persist Test', description: 'test', triggers: [], capabilities: [], toolDeps: [], installed: false, }); const engine2 = new SkillDiscoveryEngine(); const skill = engine2.getAllSkills().find(s => s.id === 'persist-test'); expect(skill).toBeDefined(); }); it('caches suggestions', async () => { const conversations = [ { role: 'user', content: '帮我审查代码' }, ]; await engine.suggestSkills(conversations, 'agent-1'); const cached = engine.getLastSuggestions(); expect(Array.isArray(cached)).toBe(true); }); }); describe('singleton', () => { it('returns same instance', () => { const a = getSkillDiscovery(); const b = getSkillDiscovery(); expect(a).toBe(b); }); it('resets singleton', () => { const a = getSkillDiscovery(); resetSkillDiscovery(); const b = getSkillDiscovery(); expect(a).not.toBe(b); }); }); });