/** * Unit Tests — evolution-loop.js * * 使用 mock AgentManager 测试 RL Loop 的完整流程, * 不实际调用 CLI agents。 */ import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest'; import fs from 'fs'; import path from 'path'; const { EvolutionLoop } = require('../../services/evolution-loop'); const { EvolutionMemory, MEMORY_FILE } = require('../../src/core/evolution-memory'); // ══════════════════════════════════════ // Mock AgentManager // ══════════════════════════════════════ class MockAgentManager { constructor(responses = {}) { this.responses = responses; this.callLog = []; } async callAgent(agentName, prompt, options = {}) { this.callLog.push({ agent: agentName, prompt: prompt.substring(0, 200) }); const responseFn = this.responses[agentName]; const output = responseFn ? responseFn(prompt, this.callLog.length) : `[RESULT: DONE]\n完成${agentName}`; return { output }; } async closeAllSessions() {} } describe('EvolutionLoop', () => { const TMP = '/tmp/test-evolution-loop'; const SKILL_DIR = path.join(TMP, 'test-skill'); beforeAll(() => fs.mkdirSync(SKILL_DIR, { recursive: true })); afterAll(() => { try { fs.rmSync(TMP, { recursive: true }); } catch {} }); beforeEach(() => { // 清理 try { fs.rmSync(SKILL_DIR, { recursive: true }); } catch {} fs.mkdirSync(SKILL_DIR, { recursive: true }); }); // ── 辅助函数 ── function setupSkillFiles(score = null) { // 创建 SKILL.md fs.writeFileSync(path.join(SKILL_DIR, 'SKILL.md'), [ '---', 'name: test-skill', 'description: A test skill for unit testing', '---', '# Test Skill', '## 执行流程', '1. 步骤一', '2. 步骤二', '## 输出格式', '- 输出 JSON', ].join('\n')); // 创建 hunt/knowhow.md fs.mkdirSync(path.join(SKILL_DIR, 'hunt'), { recursive: true }); fs.writeFileSync(path.join(SKILL_DIR, 'hunt', 'knowhow.md'), '# Knowhow\n知识内容'); // 创建 digest/benchmark.md fs.mkdirSync(path.join(SKILL_DIR, 'digest'), { recursive: true }); fs.writeFileSync(path.join(SKILL_DIR, 'digest', 'benchmark.md'), [ '# Benchmark', '## Scoring Dimensions', '| # | Dimension | Weight |', '| 1 | Quality | 50% |', '| 2 | Accuracy | 50% |', ].join('\n')); } function createBenchmarkResponse(score) { return () => `以下是评估结果:\n\n\`\`\`json\n${JSON.stringify({ totalScore: score, passed: score >= 90, passThreshold: 90, dimensions: [ { name: 'Quality', weight: 0.5, score: score, label: score >= 90 ? 'Excellent' : 'Good', feedback: '质量' + (score >= 90 ? '优秀' : '待提升') }, { name: 'Accuracy', weight: 0.5, score: score, label: score >= 90 ? 'Excellent' : 'Good', feedback: '准确性' + (score >= 90 ? '优秀' : '待提升') }, ], weakDimensions: score < 90 ? ['Quality'] : [], strongDimensions: score >= 90 ? ['Quality', 'Accuracy'] : [], overallFeedback: `总分 ${score}`, }, null, 2)}\n\`\`\`\n\n[RESULT: ${score >= 70 ? 'PASS' : 'FAIL'}]\n评分: ${score}/100`; } // ══════════════════════════════════════ // Tests // ══════════════════════════════════════ describe('_perceive', () => { it('should detect empty skill directory', async () => { const manager = new MockAgentManager(); const loop = new EvolutionLoop({ skillName: 'test-skill', description: 'Test', skillDir: SKILL_DIR, manager, }); const state = await loop._perceive(); expect(state.hasSkillMd).toBe(false); expect(state.hasKnowhow).toBe(false); expect(state.hasBenchmark).toBe(false); }); it('should detect existing skill files', async () => { setupSkillFiles(); const manager = new MockAgentManager(); const loop = new EvolutionLoop({ skillName: 'test-skill', description: 'Test', skillDir: SKILL_DIR, manager, }); const state = await loop._perceive(); expect(state.hasSkillMd).toBe(true); expect(state.hasKnowhow).toBe(true); expect(state.hasBenchmark).toBe(true); }); }); describe('_plan', () => { it('should choose "full" for first iteration without SKILL.md', () => { const manager = new MockAgentManager(); const loop = new EvolutionLoop({ skillName: 'test-skill', description: 'Test', skillDir: SKILL_DIR, manager, }); const state = { hasSkillMd: false, hasKnowhow: false, hasBenchmark: false, previousScore: null }; const strategy = loop._plan(state, 0); expect(strategy.approach).toBe('full'); expect(strategy.steps).toEqual(['hunt', 'digest', 'skill_create']); }); it('should choose "refine" when previous score >= 70', () => { const manager = new MockAgentManager(); const loop = new EvolutionLoop({ skillName: 'test-skill', description: 'Test', skillDir: SKILL_DIR, manager, }); const state = { hasSkillMd: true, hasKnowhow: true, hasBenchmark: true, previousScore: 75 }; const strategy = loop._plan(state, 1); expect(strategy.approach).toBe('refine'); expect(strategy.steps).toEqual(['evolve']); }); it('should choose "restructure" when previous score < 70', () => { const manager = new MockAgentManager(); const loop = new EvolutionLoop({ skillName: 'test-skill', description: 'Test', skillDir: SKILL_DIR, manager, }); const state = { hasSkillMd: true, hasKnowhow: true, hasBenchmark: true, previousScore: 45 }; const strategy = loop._plan(state, 1); expect(strategy.approach).toBe('restructure'); expect(strategy.steps).toContain('hunt'); expect(strategy.steps).toContain('evolve'); }); it('should choose "build_benchmark" when SKILL.md exists but no benchmark', () => { const manager = new MockAgentManager(); const loop = new EvolutionLoop({ skillName: 'test-skill', description: 'Test', skillDir: SKILL_DIR, manager, }); const state = { hasSkillMd: true, hasKnowhow: true, hasBenchmark: false, previousScore: null }; const strategy = loop._plan(state, 0); expect(strategy.approach).toBe('build_benchmark'); expect(strategy.steps).toContain('digest'); }); }); describe('_parseScoreFromOutput', () => { it('should parse JSON code block', () => { const manager = new MockAgentManager(); const loop = new EvolutionLoop({ skillName: 'test-skill', description: 'Test', skillDir: SKILL_DIR, manager, }); const output = '评估结果:\n```json\n{"totalScore": 85, "passed": false, "weakDimensions": ["accuracy"]}\n```'; const score = loop._parseScoreFromOutput(output); expect(score.totalScore).toBe(85); expect(score.passed).toBe(false); expect(score.weakDimensions).toContain('accuracy'); }); it('should parse plain JSON', () => { const manager = new MockAgentManager(); const loop = new EvolutionLoop({ skillName: 'test-skill', description: 'Test', skillDir: SKILL_DIR, manager, }); const output = '{"totalScore": 92, "passed": true}'; const score = loop._parseScoreFromOutput(output); expect(score.totalScore).toBe(92); }); it('should fallback to regex extraction', () => { const manager = new MockAgentManager(); const loop = new EvolutionLoop({ skillName: 'test-skill', description: 'Test', skillDir: SKILL_DIR, manager, }); const output = '评分结果: 总分:78/100'; const score = loop._parseScoreFromOutput(output); expect(score.totalScore).toBe(78); }); }); describe('_reflect', () => { it('should detect goal achieved', () => { const manager = new MockAgentManager(); const loop = new EvolutionLoop({ skillName: 'test-skill', description: 'Test', skillDir: SKILL_DIR, manager, goalScore: 90, }); const score = { totalScore: 92, dimensions: [], weakDimensions: [], strongDimensions: ['Quality'] }; const reflection = loop._reflect(score, { approach: 'refine' }); expect(reflection.goalAchieved).toBe(true); }); it('should extract adjustments for weak dimensions', () => { const manager = new MockAgentManager(); const loop = new EvolutionLoop({ skillName: 'test-skill', description: 'Test', skillDir: SKILL_DIR, manager, goalScore: 90, }); const score = { totalScore: 72, dimensions: [ { name: 'Quality', score: 85, feedback: '质量不错' }, { name: 'Accuracy', score: 55, feedback: '准确性不足' }, ], weakDimensions: ['Accuracy'], }; const reflection = loop._reflect(score, { approach: 'refine' }); expect(reflection.goalAchieved).toBe(false); expect(reflection.adjustments).toHaveLength(1); expect(reflection.adjustments[0].dimension).toBe('Accuracy'); }); it('should generate lessons for failed refine at low score', () => { const manager = new MockAgentManager(); const loop = new EvolutionLoop({ skillName: 'test-skill', description: 'Test', skillDir: SKILL_DIR, manager, goalScore: 90, }); const score = { totalScore: 40, dimensions: [], weakDimensions: [] }; const reflection = loop._reflect(score, { approach: 'refine' }); expect(reflection.lessonsLearned.some(l => l.includes('restructure'))).toBe(true); }); }); describe('run (full loop)', () => { it('should achieve goal in one iteration with high score', async () => { setupSkillFiles(); const manager = new MockAgentManager({ 'innovator-evolver': () => '[RESULT: DONE]\n完成 evolve', 'benchmark': createBenchmarkResponse(92), }); const loop = new EvolutionLoop({ skillName: 'test-skill', description: 'Test skill', skillDir: SKILL_DIR, manager, maxIterations: 5, goalScore: 90, }); const result = await loop.run(); expect(result.status).toBe('goal_achieved'); expect(result.finalScore).toBe(92); expect(result.iterations).toBe(1); }); it('should iterate when score is below goal', async () => { setupSkillFiles(); let callCount = 0; const manager = new MockAgentManager({ 'innovator-evolver': () => '[RESULT: DONE]\n完成 evolve', 'benchmark': () => { callCount++; // 第一次 70,第二次 85,第三次 93 const scores = [70, 85, 93]; const score = scores[Math.min(callCount - 1, scores.length - 1)]; return createBenchmarkResponse(score)(); }, }); const loop = new EvolutionLoop({ skillName: 'test-skill', description: 'Test skill', skillDir: SKILL_DIR, manager, maxIterations: 5, goalScore: 90, }); const result = await loop.run(); expect(result.status).toBe('goal_achieved'); expect(result.finalScore).toBe(93); expect(result.iterations).toBeGreaterThan(1); // 验证 Memory 被写入 const memory = new EvolutionMemory(SKILL_DIR); expect(memory.getHistory().totalAttempts).toBe(result.iterations); }); it('should return max_iterations when goal not reached', async () => { setupSkillFiles(); const manager = new MockAgentManager({ 'innovator-evolver': () => '[RESULT: DONE]\n完成 evolve', 'benchmark': createBenchmarkResponse(65), // 始终 65 分 }); const loop = new EvolutionLoop({ skillName: 'test-skill', description: 'Test skill', skillDir: SKILL_DIR, manager, maxIterations: 2, goalScore: 90, }); const result = await loop.run(); // 可能是 stagnant (连续分数不变) 或 max_iterations expect(['max_iterations', 'stagnant']).toContain(result.status); expect(result.bestScore).toBe(65); }); it('should detect stagnation and stop early', async () => { setupSkillFiles(); let callCount = 0; const manager = new MockAgentManager({ 'innovator-evolver': () => '[RESULT: DONE]\n完成', 'benchmark': () => { callCount++; // 分数几乎不变: 70, 71, 71 const scores = [70, 71, 71, 71, 71]; return createBenchmarkResponse(scores[Math.min(callCount - 1, scores.length - 1)])(); }, }); const loop = new EvolutionLoop({ skillName: 'test-skill', description: 'Test skill', skillDir: SKILL_DIR, manager, maxIterations: 5, goalScore: 90, }); const result = await loop.run(); expect(result.status).toBe('stagnant'); expect(result.iterations).toBeLessThan(5); // 应在 5 轮前停止 }); it('should use full strategy for new skill without files', async () => { // 不调用 setupSkillFiles — SKILL_DIR 是空的 const manager = new MockAgentManager({ 'l3-hunt': () => { // Hunt 创建 knowhow fs.mkdirSync(path.join(SKILL_DIR, 'hunt'), { recursive: true }); fs.writeFileSync(path.join(SKILL_DIR, 'hunt', 'knowhow.md'), '# Knowhow'); return '[RESULT: DONE]'; }, 'l2-digest': () => { // Digest 创建 benchmark fs.mkdirSync(path.join(SKILL_DIR, 'digest'), { recursive: true }); fs.writeFileSync(path.join(SKILL_DIR, 'digest', 'benchmark.md'), '# Benchmark'); return '[RESULT: DONE]'; }, 'skill-creator': () => { // SkillCreate 创建 SKILL.md fs.writeFileSync(path.join(SKILL_DIR, 'SKILL.md'), '---\nname: test\n---\n# Test Skill\n## 执行流程\n## 输出格式'); return '[RESULT: DONE]'; }, 'benchmark': createBenchmarkResponse(91), }); const loop = new EvolutionLoop({ skillName: 'test-skill', description: 'Test skill', skillDir: SKILL_DIR, manager, maxIterations: 3, goalScore: 90, }); const result = await loop.run(); expect(result.status).toBe('goal_achieved'); expect(result.finalScore).toBe(91); // 验证 agents 被正确调用 const agentsCalled = manager.callLog.map(c => c.agent); expect(agentsCalled).toContain('l3-hunt'); expect(agentsCalled).toContain('l2-digest'); expect(agentsCalled).toContain('skill-creator'); }); }); describe('_fallbackScore', () => { it('should give higher score for well-structured SKILL.md', () => { const manager = new MockAgentManager(); const loop = new EvolutionLoop({ skillName: 'test-skill', description: 'Test', skillDir: SKILL_DIR, manager, }); // 空目录 let score = loop._fallbackScore(); expect(score.totalScore).toBe(30); // 无 SKILL.md // 有 SKILL.md setupSkillFiles(); score = loop._fallbackScore(); expect(score.totalScore).toBeGreaterThanOrEqual(50); // 有结构的 SKILL.md }); }); });