test-company-e2e.js 12.2 KB
#!/usr/bin/env node
/**
 * Company Layer — E2E 测试脚本
 *
 * 测试内容:
 *   1. Company CRUD (创建、查询、列表、更新、关停)
 *   2. 模板库查询
 *   3. Agent 招聘 (模板命中 / 模板未命中)
 *   4. 公司内 Agent 列表
 *   5. Agent 解雇
 *   6. resolveAgentDir 统一寻址
 *
 * 运行: node server/scripts/test-company-e2e.js
 */

const path = require('path');
const fs = require('fs');

// 直接引入 service 层 (不需要 HTTP)
const { CompanyService } = require('../services/company-service');

const DATA_DIR = path.resolve(__dirname, '../../data');
const TEST_COMPANY_ID = 'test-media-co';
const TEST_COMPANY_DIR = path.join(DATA_DIR, TEST_COMPANY_ID);

let passed = 0;
let failed = 0;

function assert(condition, label) {
  if (condition) {
    console.log(`  ✅ ${label}`);
    passed++;
  } else {
    console.error(`  ❌ ${label}`);
    failed++;
  }
}

function cleanup() {
  // 清理测试数据
  if (fs.existsSync(TEST_COMPANY_DIR)) {
    fs.rmSync(TEST_COMPANY_DIR, { recursive: true, force: true });
  }
}

async function main() {
  console.log('\n🏢 Company Layer E2E 测试\n');
  cleanup();

  const svc = new CompanyService();

  // ─── 1. 模板库 ───
  console.log('📦 Phase 1: 模板库');
  const templates = svc.listTemplates();
  assert(Array.isArray(templates), '模板列表返回数组');
  assert(templates.length >= 2, `模板数量 ≥ 2 (实际: ${templates.length})`);

  const ceoTpl = templates.find(t => t.id === 'ceo');
  assert(ceoTpl !== undefined, '存在 CEO 模板');
  assert(ceoTpl && ceoTpl.type === 'cxo', 'CEO 模板类型为 cxo');

  const qaTpl = templates.find(t => t.id === 'qa-judge');
  assert(qaTpl !== undefined, '存在 qa-judge 模板');

  // ─── 2. 创建公司 ───
  console.log('\n🏗️  Phase 2: 创建公司');
  const company = svc.create({
    id: TEST_COMPANY_ID,
    name: '测试自媒体公司',
    description: '一家用于 E2E 测试的自媒体公司',
  });

  assert(company.id === TEST_COMPANY_ID, `公司 ID = ${TEST_COMPANY_ID}`);
  assert(company.status === 'creating', '初始状态为 creating');
  assert(fs.existsSync(path.join(TEST_COMPANY_DIR, 'company.json')), 'company.json 已创建');
  assert(fs.existsSync(path.join(TEST_COMPANY_DIR, 'agents')), 'agents/ 目录已创建');
  assert(fs.existsSync(path.join(TEST_COMPANY_DIR, 'goals')), 'goals/ 目录已创建');
  assert(fs.existsSync(path.join(TEST_COMPANY_DIR, 'workspace')), 'workspace/ 目录已创建');
  assert(fs.existsSync(path.join(TEST_COMPANY_DIR, 'pipelines')), 'pipelines/ 目录已创建');

  // ─── 3. 查询公司 ───
  console.log('\n🔍 Phase 3: 查询公司');
  const fetched = svc.get(TEST_COMPANY_ID);
  assert(fetched !== null, 'get() 返回非空');
  assert(fetched.name === '测试自媒体公司', '公司名称正确');

  const list = svc.list();
  assert(list.some(c => c.id === TEST_COMPANY_ID), 'list() 中包含新公司');

  // ─── 4. 招聘 CEO (从模板) ───
  console.log('\n👔 Phase 4: 招聘 CEO (模板命中)');
  const hireCeo = svc.hire(TEST_COMPANY_ID, 'ceo');
  assert(hireCeo.status === 'hired_from_template', '从模板招聘成功');

  const ceoDir = path.join(TEST_COMPANY_DIR, 'agents', 'ceo');
  assert(fs.existsSync(ceoDir), 'CEO Agent 目录已创建');
  assert(fs.existsSync(path.join(ceoDir, 'agent.md')), 'agent.md 已复制');
  assert(fs.existsSync(path.join(ceoDir, 'soul.md')), 'soul.md 已复制');
  assert(fs.existsSync(path.join(ceoDir, 'config.json')), 'config.json 已复制');
  assert(fs.existsSync(path.join(ceoDir, 'memory', 'memory.md')), 'memory/memory.md 已初始化');

  // 检查上下文注入
  const ceoAgentMd = fs.readFileSync(path.join(ceoDir, 'agent.md'), 'utf-8');
  assert(ceoAgentMd.includes(TEST_COMPANY_ID), 'agent.md 注入了公司 ID');
  assert(ceoAgentMd.includes('测试自媒体公司'), 'agent.md 注入了公司名称');

  const ceoConfig = JSON.parse(fs.readFileSync(path.join(ceoDir, 'config.json'), 'utf-8'));
  assert(ceoConfig.companyId === TEST_COMPANY_ID, 'config.json 注入了 companyId');
  assert(ceoConfig.status === 'active', 'config.json 状态为 active');

  // 检查公司元数据更新
  const companyAfterCeo = svc.get(TEST_COMPANY_ID);
  assert(companyAfterCeo.ceo === 'ceo', 'company.json 中 CEO 已标记');
  assert(companyAfterCeo.agents.includes('ceo'), 'company.json agents 列表包含 ceo');

  // ─── 5. 招聘 qa-judge (从模板) ───
  console.log('\n🔍 Phase 5: 招聘 qa-judge (模板命中)');
  const hireQa = svc.hire(TEST_COMPANY_ID, 'qa-judge');
  assert(hireQa.status === 'hired_from_template', 'qa-judge 从模板招聘成功');

  // ─── 6. Phase 2: hireWithTraining — 模板不存在, 自动创建骨架 ───
  console.log('\n🎓 Phase 6: hireWithTraining (模板不存在 → 自动创建骨架)');
  const hireNew = await svc.hireWithTraining(TEST_COMPANY_ID, 'hot-topic-hunter', {
    type: 'skill',
    name: '热点搜索员',
    description: '全网热点监控、选题建议',
  });
  assert(hireNew.status === 'training', '返回 training 状态');

  const hthDir = path.join(TEST_COMPANY_DIR, 'agents', 'hot-topic-hunter');
  assert(fs.existsSync(hthDir), '热点搜索员骨架目录已创建');
  assert(fs.existsSync(path.join(hthDir, 'agent.md')), 'agent.md 已生成');
  assert(fs.existsSync(path.join(hthDir, 'soul.md')), 'soul.md 已生成');
  assert(fs.existsSync(path.join(hthDir, 'config.json')), 'config.json 已生成');
  assert(fs.existsSync(path.join(hthDir, 'todo.md')), 'todo.md 已生成');
  assert(fs.existsSync(path.join(hthDir, 'memory', 'memory.md')), 'memory/ 已初始化');

  const hthConfig = JSON.parse(fs.readFileSync(path.join(hthDir, 'config.json'), 'utf-8'));
  assert(hthConfig.status === 'training', 'config 状态为 training');
  assert(hthConfig.companyId === TEST_COMPANY_ID, 'config 含 companyId');

  // 检查公司 agents 列表已包含
  const companyWithHth = svc.get(TEST_COMPANY_ID);
  assert(companyWithHth.agents.includes('hot-topic-hunter'), 'company.agents 包含 hot-topic-hunter');

  // ─── 7. Phase 2: depositTemplate — Benchmark 门禁 ───
  console.log('\n🚧 Phase 7: depositTemplate — Benchmark 质量门禁');

  // 7a: 评分不够 → 拒绝
  const rejectResult = svc.depositTemplate(TEST_COMPANY_ID, 'hot-topic-hunter', 80);
  assert(rejectResult.status === 'rejected', '评分 80 被拒绝');
  assert(rejectResult.message.includes('80'), '拒绝消息包含分数');

  // 7b: 评分 ≥ 95 → 通过
  const depositResult = svc.depositTemplate(TEST_COMPANY_ID, 'hot-topic-hunter', 97);
  assert(depositResult.status === 'deposited', '评分 97 成功沉淀');
  assert(depositResult.templateId === 'hot-topic-hunter', '模板 ID 正确');

  // 检查模板已创建
  const tplDir = path.join(DATA_DIR, 'templates', 'hot-topic-hunter');
  assert(fs.existsSync(tplDir), '模板目录已创建');
  assert(fs.existsSync(path.join(tplDir, 'config.json')), '模板 config.json 存在');

  const tplConfig = JSON.parse(fs.readFileSync(path.join(tplDir, 'config.json'), 'utf-8'));
  assert(tplConfig.benchmarkScore === 97, '模板 config 含 benchmarkScore');
  assert(tplConfig.companyId === undefined, '模板 config 不含 companyId (已清除)');
  assert(tplConfig.status === 'ready', '模板 config 状态为 ready');

  // 检查 Agent 状态从 training → active
  const hthConfigAfter = JSON.parse(fs.readFileSync(path.join(hthDir, 'config.json'), 'utf-8'));
  assert(hthConfigAfter.status === 'active', 'Agent 状态从 training → active');

  // 检查模板注册表更新
  const updatedRegistry = svc.listTemplates();
  const hthTemplate = updatedRegistry.find(t => t.id === 'hot-topic-hunter');
  assert(hthTemplate !== undefined, '注册表包含新模板');
  assert(hthTemplate && hthTemplate.benchmarkScore === 97, '注册表分数正确');

  // ─── 8. Phase 2: 新模板可被第二家公司复用 ───
  console.log('\n🔄 Phase 8: 验证新模板可被第二家公司复用');
  const SECOND_COMPANY = 'test-manga-co';
  const secondDir = path.join(DATA_DIR, SECOND_COMPANY);
  try {
    svc.create({ id: SECOND_COMPANY, name: '测试漫剧公司', description: '漫剧测试' });
    const hireFromNewTpl = svc.hire(SECOND_COMPANY, 'hot-topic-hunter');
    assert(hireFromNewTpl.status === 'hired_from_template', '第二家公司从新模板招聘成功');

    const secondHthConfig = JSON.parse(fs.readFileSync(
      path.join(secondDir, 'agents', 'hot-topic-hunter', 'config.json'), 'utf-8'
    ));
    assert(secondHthConfig.companyId === SECOND_COMPANY, '第二家公司 companyId 正确注入');
  } finally {
    if (fs.existsSync(secondDir)) fs.rmSync(secondDir, { recursive: true, force: true });
  }

  // ─── 9. 重复招聘检测 ───
  console.log('\n🔁 Phase 9: 重复招聘检测');
  const hireDup = svc.hire(TEST_COMPANY_ID, 'ceo');
  assert(hireDup.status === 'already_exists', '重复招聘返回 already_exists');

  // ─── 10. 公司 Agent 列表 ───
  console.log('\n📋 Phase 10: 公司 Agent 列表');
  const agents = svc.listAgents(TEST_COMPANY_ID);
  assert(agents.length === 3, `Agent 数量 = 3 (实际: ${agents.length})`);
  assert(agents.some(a => a.id === 'ceo'), '列表包含 CEO');
  assert(agents.some(a => a.id === 'qa-judge'), '列表包含 qa-judge');
  assert(agents.some(a => a.id === 'hot-topic-hunter'), '列表包含 hot-topic-hunter');

  // ─── 11. resolveAgentDir ───
  console.log('\n📍 Phase 11: resolveAgentDir 统一寻址');
  const companyAgentDir = CompanyService.resolveAgentDir(TEST_COMPANY_ID, 'ceo');
  assert(companyAgentDir !== null, '公司内 CEO 可定位');
  assert(companyAgentDir.includes(TEST_COMPANY_ID), '路径包含公司 ID');

  const globalMasterDir = CompanyService.resolveAgentDir(null, 'master');
  assert(globalMasterDir !== null, '全局 Master 可定位');
  assert(globalMasterDir.includes('data/agents/master'), '全局路径正确');

  const notExist = CompanyService.resolveAgentDir(TEST_COMPANY_ID, 'nonexistent-agent');
  assert(notExist === null, '不存在的 Agent 返回 null');

  // ─── 12. 解雇 Agent ───
  console.log('\n🚪 Phase 12: 解雇 Agent');
  const fireResult = svc.fire(TEST_COMPANY_ID, 'qa-judge');
  assert(fireResult.status === 'suspended', '解雇返回 suspended');

  const companyAfterFire = svc.get(TEST_COMPANY_ID);
  assert(!companyAfterFire.agents.includes('qa-judge'), 'qa-judge 已从 agents 列表移除');

  const firedConfig = JSON.parse(fs.readFileSync(path.join(TEST_COMPANY_DIR, 'agents', 'qa-judge', 'config.json'), 'utf-8'));
  assert(firedConfig.status === 'suspended', 'qa-judge config 状态为 suspended');

  // ─── 13. 更新公司 ───
  console.log('\n✏️  Phase 13: 更新公司');
  const updated = svc.update(TEST_COMPANY_ID, {
    status: 'running',
    heartbeat: { enabled: true },
  });
  assert(updated.status === 'running', '状态更新为 running');
  assert(updated.heartbeat.enabled === true, '心跳已开启');

  // ─── 14. 关停公司 ───
  console.log('\n🛑 Phase 14: 关停公司');
  const shutdown = svc.shutdown(TEST_COMPANY_ID);
  assert(shutdown.status === 'shutdown', '状态变为 shutdown');
  assert(shutdown.heartbeat.enabled === false, '心跳已关闭');

  // ─── 清理 ───
  cleanup();
  // 清理测试生成的模板
  const testTplDir = path.join(DATA_DIR, 'templates', 'hot-topic-hunter');
  if (fs.existsSync(testTplDir)) fs.rmSync(testTplDir, { recursive: true, force: true });
  // 恢复 registry (移除测试模板)
  const regPath = path.join(DATA_DIR, 'templates', '_registry.json');
  if (fs.existsSync(regPath)) {
    const reg = JSON.parse(fs.readFileSync(regPath, 'utf-8'));
    const cleaned = reg.filter(t => t.id !== 'hot-topic-hunter');
    fs.writeFileSync(regPath, JSON.stringify(cleaned, null, 2), 'utf-8');
  }

  // ─── 结果 ───
  console.log(`\n${'═'.repeat(50)}`);
  console.log(`  📊 测试结果: ${passed} 通过, ${failed} 失败 (共 ${passed + failed})`);
  if (failed === 0) {
    console.log('  🎉 ALL PASSED — Phase 1 + Phase 2 生产就绪!\n');
  } else {
    console.log(`  ⚠️  ${failed} 个测试未通过\n`);
    process.exit(1);
  }
}

main().catch(err => {
  console.error('测试异常:', err);
  cleanup();
  process.exit(1);
});