agent-manager-p1.test.js 5.38 KB
/**
 * Unit Tests — AgentManager.getAgentConfig() P1 虚拟引用寻址
 *
 * 覆盖 docs/todo/system-design/27-p1-virtual-reference.md §4.2 / §6-1,2,4:
 *   - config.json 带 systemAgentRef 且 system_agents 表命中 template 行 → prompt 指向共享
 *     agent.md,companyContextPath 指向私有 company-context.md
 *   - config.json 没有 systemAgentRef(legacy agent) → prompt 仍指向私有 agent.md,行为不变
 *
 * Uses GRAPH_DB_PATH env override (same pattern as graph-service.test.js) so the real
 * data/graph.sqlite is never touched. Also creates a temporary company directory under
 * data/companies/ that is removed in afterAll.
 */
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import fs from 'fs';
import path from 'path';

describe('AgentManager.getAgentConfig() — P1 virtual reference addressing', () => {
  const TEST_DB_PATH = '/tmp/test-agent-manager-p1-graph.sqlite';
  const REPO_ROOT = path.resolve(__dirname, '../../..');
  const TMP_COMPANY_ID = `tmp-p1-test-co-${Date.now()}`;
  const AGENTS_ROOT = path.join(REPO_ROOT, 'data/companies', TMP_COMPANY_ID, 'agents');

  let graphDb;
  let AgentManager;

  beforeAll(() => {
    if (fs.existsSync(TEST_DB_PATH)) fs.unlinkSync(TEST_DB_PATH);
    process.env.GRAPH_DB_PATH = TEST_DB_PATH;
    delete require.cache[require.resolve('../../services/graph-db')];
    delete require.cache[require.resolve('../../services/graph-service')];
    delete require.cache[require.resolve('../../src/core/agent-manager')];
    graphDb = require('../../services/graph-db');
    AgentManager = require('../../src/core/agent-manager');

    // Seed a template row for "qa-judge-p1-test" pointing at a fixture-free legacy_path
    // (the shared agent.md just needs to exist so lookupSharedAgentMd's result is a real file).
    const db = graphDb.getDb();
    const now = new Date().toISOString();
    db.prepare(`
      INSERT INTO system_agents (id, name, scope, owner_company_id, source_kind, legacy_path, status, benchmark_score, created_at, updated_at)
      VALUES (?, ?, 'public', NULL, 'template', ?, 'active', NULL, ?, ?)
    `).run('qa-judge', 'qa-judge', 'system/templates/qa-judge', now, now);

    fs.mkdirSync(AGENTS_ROOT, { recursive: true });
  });

  afterAll(() => {
    try { fs.rmSync(path.join(REPO_ROOT, 'data/companies', TMP_COMPANY_ID), { recursive: true, force: true }); } catch {}
    try { fs.unlinkSync(TEST_DB_PATH); } catch {}
    try { fs.unlinkSync(`${TEST_DB_PATH}-shm`); } catch {}
    try { fs.unlinkSync(`${TEST_DB_PATH}-wal`); } catch {}
    delete process.env.GRAPH_DB_PATH;
  });

  function makeAgentDir(role, configJson) {
    const dir = path.join(AGENTS_ROOT, role);
    fs.mkdirSync(dir, { recursive: true });
    if (configJson !== undefined) {
      fs.writeFileSync(path.join(dir, 'config.json'), JSON.stringify(configJson), 'utf-8');
    }
    // Legacy agent.md always present in the private dir (P1 doesn't delete it if it exists
    // for the reference-mode case — company-service.hire() simply never writes it there).
    fs.writeFileSync(path.join(dir, 'agent.md'), '# legacy private agent.md\n', 'utf-8');
    return dir;
  }

  it('reference mode: systemAgentRef present + template row found -> prompt points at shared agent.md, companyContextPath set', () => {
    const role = 'qa-judge-referenced';
    const agentDir = makeAgentDir(role, { systemAgentRef: 'qa-judge' });

    const manager = new AgentManager({}, REPO_ROOT);
    const { agent } = manager.getAgentConfig(`${TMP_COMPANY_ID}:${role}`);

    const expectedSharedPath = path.resolve(REPO_ROOT, 'system/templates/qa-judge', 'agent.md');
    expect(agent.prompt).toBe(expectedSharedPath);
    expect(agent.companyContextPath).toBe(path.join(agentDir, 'company-context.md'));
    // Hard constraint: workDir must remain the private directory (CLI cwd for soul.md/identity.md/.claude/skills/).
    expect(agent.workDir).toBe(agentDir);
  });

  it('legacy mode: no systemAgentRef -> prompt points at private agent.md, companyContextPath falsy', () => {
    const role = 'legacy-role';
    const agentDir = makeAgentDir(role, { someOtherField: true });

    const manager = new AgentManager({}, REPO_ROOT);
    const { agent } = manager.getAgentConfig(`${TMP_COMPANY_ID}:${role}`);

    expect(agent.prompt).toBe(path.join(agentDir, 'agent.md'));
    expect(agent.companyContextPath).toBeFalsy();
    expect(agent.workDir).toBe(agentDir);
  });

  it('legacy mode: no config.json at all -> prompt points at private agent.md, companyContextPath falsy', () => {
    const role = 'no-config-role';
    const agentDir = makeAgentDir(role, undefined);

    const manager = new AgentManager({}, REPO_ROOT);
    const { agent } = manager.getAgentConfig(`${TMP_COMPANY_ID}:${role}`);

    expect(agent.prompt).toBe(path.join(agentDir, 'agent.md'));
    expect(agent.companyContextPath).toBeFalsy();
    expect(agent.workDir).toBe(agentDir);
  });

  it('defensive fallback: systemAgentRef present but no matching template row -> falls back to private agent.md', () => {
    const role = 'dangling-ref-role';
    const agentDir = makeAgentDir(role, { systemAgentRef: 'does-not-exist-in-system-agents' });

    const manager = new AgentManager({}, REPO_ROOT);
    const { agent } = manager.getAgentConfig(`${TMP_COMPANY_ID}:${role}`);

    expect(agent.prompt).toBe(path.join(agentDir, 'agent.md'));
    expect(agent.companyContextPath).toBeNull();
    expect(agent.workDir).toBe(agentDir);
  });
});