prompt-builder.test.js 5.35 KB
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
const path = require('path');
const fs = require('fs');
const { PromptBuilder } = require('../../core/prompt-builder');

describe('PromptBuilder', () => {
  const baseDir = path.join(__dirname, '..', 'fixtures');
  const configPath = path.join(baseDir, 'config.json');
  let config;
  let builder;

  beforeEach(() => {
    config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
    builder = new PromptBuilder(config, baseDir);
  });

  describe('getProtocol', () => {
    it('should load executor protocol', () => {
      const protocol = builder.getProtocol('executor');
      expect(protocol).toContain('[RESULT: DONE]');
      expect(protocol).toContain('[RESULT: ERROR]');
    });

    it('should load validator protocol', () => {
      const protocol = builder.getProtocol('validator');
      expect(protocol).toContain('[RESULT: PASS]');
      expect(protocol).toContain('[RESULT: FAIL]');
    });

    it('should load master protocol', () => {
      const protocol = builder.getProtocol('master');
      expect(protocol).toContain('[CALL:');
      expect(protocol).toContain('{{AVAILABLE_AGENTS}}');
    });

    it('should return empty string for unknown type', () => {
      const protocol = builder.getProtocol('unknown');
      expect(protocol).toBe('');
    });
  });

  describe('getAgentPrompt', () => {
    it('should load coder prompt', () => {
      const prompt = builder.getAgentPrompt('coder');
      expect(prompt).toContain('Coder');
      expect(prompt).toContain('代码');
    });

    it('should load verifier prompt', () => {
      const prompt = builder.getAgentPrompt('verifier');
      expect(prompt).toContain('Verifier');
    });

    it('should throw for unknown agent', () => {
      expect(() => builder.getAgentPrompt('unknown')).toThrow('Agent not found');
    });
  });

  describe('build', () => {
    it('should combine protocol and agent prompt for executor', () => {
      const result = builder.build('coder');

      // 应该包含 executor protocol
      expect(result).toContain('[RESULT: DONE]');
      expect(result).toContain('[RESULT: ERROR]');

      // 应该包含 coder prompt
      expect(result).toContain('Coder');
    });

    it('should combine protocol and agent prompt for validator', () => {
      const result = builder.build('verifier');

      // 应该包含 validator protocol
      expect(result).toContain('[RESULT: PASS]');
      expect(result).toContain('[RESULT: FAIL]');

      // 应该包含 verifier prompt
      expect(result).toContain('Verifier');
    });

    it('should replace {{AVAILABLE_AGENTS}} in master protocol', () => {
      const result = builder.build('master');

      // 不应该包含占位符
      expect(result).not.toContain('{{AVAILABLE_AGENTS}}');

      // 应该包含实际的 agent 列表
      expect(result).toContain('coder');
      expect(result).toContain('verifier');
      expect(result).toContain('frontend-tester');
    });

    it('should append user task when provided', () => {
      const result = builder.build('coder', '实现一个计算器');

      expect(result).toContain('当前任务');
      expect(result).toContain('实现一个计算器');
    });

    it('should not include task section when no task provided', () => {
      const result = builder.build('coder');

      expect(result).not.toContain('当前任务');
    });
  });

  describe('build - companyContextPath (P1 virtual reference)', () => {
    const os = require('os');
    let tmpFile;

    afterEach(() => {
      if (tmpFile && fs.existsSync(tmpFile)) {
        fs.unlinkSync(tmpFile);
      }
      tmpFile = undefined;
    });

    it('appends company-context.md content when companyContextPath exists', () => {
      tmpFile = path.join(os.tmpdir(), `company-context-${Date.now()}.md`);
      fs.writeFileSync(tmpFile, '## 所属公司\n- **公司ID**: test-co', 'utf-8');

      config.agents.coder.companyContextPath = tmpFile;
      const result = builder.build('coder');

      expect(result).toContain('## 所属公司');
      expect(result).toContain('test-co');
    });

    // <timestamp> in getSystemStateXML() is wall-clock and can legitimately
    // differ by a millisecond between two build() calls, so strip it before
    // comparing outputs for structural equality.
    const stripTimestamp = (s) => s.replace(/<timestamp>.*?<\/timestamp>/, '<timestamp/>');

    it('does not change output when companyContextPath is null (legacy mode)', () => {
      const baseline = stripTimestamp(builder.build('coder'));

      config.agents.coder.companyContextPath = null;
      const withNull = stripTimestamp(builder.build('coder'));

      expect(withNull).toBe(baseline);
    });

    it('does not change output when companyContextPath points to a nonexistent file', () => {
      const baseline = stripTimestamp(builder.build('coder'));

      config.agents.coder.companyContextPath = path.join(os.tmpdir(), 'does-not-exist-xyz.md');
      const withMissing = stripTimestamp(builder.build('coder'));

      expect(withMissing).toBe(baseline);
    });
  });

  describe('caching', () => {
    it('should cache loaded files', () => {
      // 第一次加载
      builder.build('coder');

      // 第二次加载应该使用缓存
      const cacheSize = builder.promptCache.size;
      builder.build('coder');

      expect(builder.promptCache.size).toBe(cacheSize);
    });
  });
});