evolution-memory.test.js 5.34 KB
/**
 * Unit Tests — evolution-memory.js
 */
import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest';
import fs from 'fs';
import path from 'path';

const { EvolutionMemory, MEMORY_FILE, MAX_RECORDS } = require('../../src/core/evolution-memory');

describe('EvolutionMemory', () => {
  const TMP = '/tmp/test-evolution-memory';
  const SKILL_DIR = path.join(TMP, 'test-skill');

  beforeAll(() => fs.mkdirSync(SKILL_DIR, { recursive: true }));
  afterAll(() => { try { fs.rmSync(TMP, { recursive: true }); } catch {} });

  beforeEach(() => {
    // 清理 memory 文件
    const fp = path.join(SKILL_DIR, MEMORY_FILE);
    try { fs.unlinkSync(fp); } catch {}
  });

  describe('getHistory', () => {
    it('should return empty history for new skill', () => {
      const memory = new EvolutionMemory(SKILL_DIR);
      const history = memory.getHistory();
      expect(history.iterations).toEqual([]);
      expect(history.bestScore).toBeNull();
      expect(history.totalAttempts).toBe(0);
    });
  });

  describe('record', () => {
    it('should record an iteration', () => {
      const memory = new EvolutionMemory(SKILL_DIR);
      memory.record({
        iteration: 0,
        strategy: { approach: 'full', steps: ['hunt', 'digest', 'skill_create'] },
        score: 72,
        weakDimensions: ['accuracy', 'robustness'],
        lessonsLearned: ['需要更详细的 hunt'],
      });

      const history = memory.getHistory();
      expect(history.iterations).toHaveLength(1);
      expect(history.iterations[0].score).toBe(72);
      expect(history.totalAttempts).toBe(1);
      expect(history.bestScore).toBe(72);
    });

    it('should track bestScore across multiple records', () => {
      const memory = new EvolutionMemory(SKILL_DIR);
      memory.record({ iteration: 0, strategy: { approach: 'full' }, score: 60 });
      memory.record({ iteration: 1, strategy: { approach: 'refine' }, score: 85 });
      memory.record({ iteration: 2, strategy: { approach: 'refine' }, score: 78 });

      const history = memory.getHistory();
      expect(history.bestScore).toBe(85);
      expect(history.totalAttempts).toBe(3);
    });

    it('should persist to disk and reload', () => {
      const memory1 = new EvolutionMemory(SKILL_DIR);
      memory1.record({ iteration: 0, strategy: { approach: 'full' }, score: 65 });

      // 新实例应从磁盘加载
      const memory2 = new EvolutionMemory(SKILL_DIR);
      const history = memory2.getHistory();
      expect(history.iterations).toHaveLength(1);
      expect(history.iterations[0].score).toBe(65);
    });
  });

  describe('getLessons', () => {
    it('should extract unique lessons from history', () => {
      const memory = new EvolutionMemory(SKILL_DIR);
      memory.record({ iteration: 0, score: 50, lessonsLearned: ['教训A', '教训B'] });
      memory.record({ iteration: 1, score: 60, lessonsLearned: ['教训B', '教训C'] });

      const lessons = memory.getLessons();
      expect(lessons).toHaveLength(3);
      expect(lessons).toContain('教训A');
      expect(lessons).toContain('教训B');
      expect(lessons).toContain('教训C');
    });

    it('should return empty for no history', () => {
      const memory = new EvolutionMemory(SKILL_DIR);
      expect(memory.getLessons()).toEqual([]);
    });
  });

  describe('getBestStrategy', () => {
    it('should return strategy with highest score', () => {
      const memory = new EvolutionMemory(SKILL_DIR);
      memory.record({ iteration: 0, strategy: { approach: 'full' }, score: 50 });
      memory.record({ iteration: 1, strategy: { approach: 'refine' }, score: 88 });
      memory.record({ iteration: 2, strategy: { approach: 'restructure' }, score: 75 });

      const best = memory.getBestStrategy();
      expect(best.approach).toBe('refine');
    });

    it('should return null for empty history', () => {
      const memory = new EvolutionMemory(SKILL_DIR);
      expect(memory.getBestStrategy()).toBeNull();
    });
  });

  describe('getRecentWeakDimensions', () => {
    it('should return weak dimensions sorted by frequency', () => {
      const memory = new EvolutionMemory(SKILL_DIR);
      memory.record({ iteration: 0, score: 50, weakDimensions: ['accuracy', 'completeness'] });
      memory.record({ iteration: 1, score: 60, weakDimensions: ['accuracy', 'robustness'] });
      memory.record({ iteration: 2, score: 65, weakDimensions: ['accuracy'] });

      const weaks = memory.getRecentWeakDimensions(3);
      expect(weaks[0]).toBe('accuracy'); // 出现 3 次
    });
  });

  describe('isStagnant', () => {
    it('should detect stagnation', () => {
      const memory = new EvolutionMemory(SKILL_DIR);
      memory.record({ iteration: 0, score: 70 });
      memory.record({ iteration: 1, score: 71 });
      memory.record({ iteration: 2, score: 71 });

      expect(memory.isStagnant(3, 2)).toBe(true);
    });

    it('should not flag as stagnant when scores differ', () => {
      const memory = new EvolutionMemory(SKILL_DIR);
      memory.record({ iteration: 0, score: 60 });
      memory.record({ iteration: 1, score: 70 });
      memory.record({ iteration: 2, score: 75 });

      expect(memory.isStagnant(3, 2)).toBe(false);
    });

    it('should not flag with insufficient data', () => {
      const memory = new EvolutionMemory(SKILL_DIR);
      memory.record({ iteration: 0, score: 60 });

      expect(memory.isStagnant(3, 2)).toBe(false);
    });
  });
});