#!/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 { OkrService } = require('../services/okr-service'); const { PipelineService } = require('../services/pipeline-service'); const { CompanyHeartbeatService } = require('../services/company-heartbeat-service'); const DATA_DIR = path.resolve(__dirname, '../../data'); // 公司数据在 data/companies/{id}/ 下(company-service.js:18),不是 data/{id}/。 // 本脚本长期按后者找,导致所有断言全失败——这是 data/companies/ 那次目录调整时漏改的。 const COMPANIES_DIR = path.join(DATA_DIR, 'companies'); // 模板库 = 本体库。28 批次 1 把 `system/templates/` 整体并进 `system/agents/`, // 这里必须跟着 company-service 的 TEMPLATES_DIR(= agent-locator.AGENTS_ROOT)走: // 指着已删除的旧目录不只是断言失败——Phase 7b 的 depositTemplate 会往 // `system/agents/hot-topic-hunter` 写,而收尾的 rmSync 按旧路径删,删不掉, // 于是每跑一次就在本体库里留一份测试残渣。 const TEMPLATES_DIR = require('../services/agent-locator').AGENTS_ROOT; const TEST_COMPANY_ID = 'test-media-co'; const TEST_COMPANY_DIR = path.join(COMPANIES_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, 'okr')), 'okr/ 目录已创建'); 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. 招聘 CXO (从模板) ─── // ⚠️ 这里刻意用 coo 而不是 ceo:system/templates/ceo/ 只有 plan.md + todo.md, // 没有 agent.md / soul.md / config.json,拿它测"模板骨架是否被完整复制"测不出东西 // (而且缺 config.json 就没有 type='cxo',company.ceo 也标不上)。 // coo 是同为 type='cxo' 且文件完整的模板。ceo 模板本身的残缺是另一个待补的问题。 const CXO_ROLE = 'coo'; console.log(`\n👔 Phase 4: 招聘 ${CXO_ROLE} (模板命中)`); const hireCeo = svc.hire(TEST_COMPANY_ID, CXO_ROLE); assert(hireCeo.status === 'hired_from_template', '从模板招聘成功'); const ceoDir = path.join(TEST_COMPANY_DIR, 'agents', CXO_ROLE); assert(fs.existsSync(ceoDir), 'CXO Agent 目录已创建'); 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 已初始化'); // ⚠️ P1 虚拟引用寻址(commit 226489d)之后,agent.md 不再物理拷贝到私有目录, // 改为 config.json 里的 systemAgentRef 指向共享模板,公司上下文单独落 company-context.md。 // 本段断言在 P1 之前是"agent.md 已复制 + 注入了公司 ID/名称",已按新模型改写。 assert(!fs.existsSync(path.join(ceoDir, 'agent.md')), 'agent.md 不再物理拷贝(P1 引用模式)'); const ceoCfgRef = JSON.parse(fs.readFileSync(path.join(ceoDir, 'config.json'), 'utf-8')); assert(ceoCfgRef.systemAgentRef === CXO_ROLE, `config.json 记录了 systemAgentRef=${CXO_ROLE}`); const ceoCtxPath = path.join(ceoDir, 'company-context.md'); assert(fs.existsSync(ceoCtxPath), 'company-context.md 已生成'); const ceoCtx = fs.readFileSync(ceoCtxPath, 'utf-8'); assert(ceoCtx.includes(TEST_COMPANY_ID), 'company-context.md 注入了公司 ID'); assert(ceoCtx.includes('测试自媒体公司'), 'company-context.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 === CXO_ROLE, 'company.json 中 CEO 已标记(现有实现:任何 type=cxo 的角色都会覆盖此字段)'); assert(companyAfterCeo.agents.includes(CXO_ROLE), `company.json agents 列表包含 ${CXO_ROLE}`); // ─── 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: '全网热点监控、选题建议', }); // T5b(27-p2-org-graph.md §4.1 第 7 步)起:模板不存在时不再走 needs_training 全量培养, // 改为先从 system/templates/base/ 元 agent 继承骨架,再交 agent-creator 填充。 assert(hireNew.status === 'hired_from_base', '返回 hired_from_base 状态(从 base 元 agent 继承)'); 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 已生成'); // ⚠️ base 元 agent(system/templates/base/)目前只有 agent.md + config.json, // 所以继承出来的骨架不含 soul.md / identity.md / tools.md / todo.md —— 这些由 // agent-creator 后续填充。这里只断言 base 继承本身保证的东西,不断言 agent-creator 的产物。 const hthCfg = JSON.parse(fs.readFileSync(path.join(hthDir, 'config.json'), 'utf-8')); assert(hthCfg.inheritedFrom === 'base', 'config.json 标记 inheritedFrom=base'); assert(fs.existsSync(path.join(hthDir, 'config.json')), 'config.json 已生成'); 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(TEMPLATES_DIR, '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(COMPANIES_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, CXO_ROLE); 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 === CXO_ROLE), '列表包含 CXO'); 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, CXO_ROLE); assert(companyAgentDir !== null, '公司内 CEO 可定位'); assert(companyAgentDir.includes(TEST_COMPANY_ID), '路径包含公司 ID'); const globalMasterDir = CompanyService.resolveAgentDir(null, 'master'); assert(globalMasterDir !== null, '全局 Master 可定位'); // 全局 agent 在 system/agents/ 下,不是 data/agents/(同属那次目录调整时漏改的断言) assert(globalMasterDir.includes('system/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. Phase 3: Company Goals (公司级 OKR) ─── console.log('\n🎯 Phase 13: Company Goals (公司级 OKR)'); const companyGoals = new OkrService(TEST_COMPANY_ID); // 13a: 创建 Goal const goal = companyGoals.create({ title: '30天粉丝破万', description: '通过日更优质内容实现公众号粉丝增长', owner: 'ceo', }); assert(goal.id === 'O-001', `Objective ID = O-001 (实际: ${goal.id})`); assert(goal.title === '30天粉丝破万', 'Goal 标题正确'); // 13b: 检查数据存储在公司目录下 const goalFilePath = path.join(TEST_COMPANY_DIR, 'okr', 'O-001', 'objective.json'); assert(fs.existsSync(goalFilePath), 'Goal 存储在公司目录下'); // 13c: 添加节点 (Objective + Key Results) const obj = companyGoals.addNode('O-001', null, { title: '建立内容产出流水线', owner: 'ceo', priority: 'P0', }); assert(obj.nodeId === 'O-001.1', `Objective ID = O-001.1 (实际: ${obj.nodeId})`); const kr1 = companyGoals.addNode('O-001', 'O-001.1', { title: '日更1篇高质量文章', owner: 'content-writer', priority: 'P0', }); assert(kr1.nodeId === 'O-001.1.1', `KR ID = O-001.1.1 (实际: ${kr1.nodeId})`); const kr2 = companyGoals.addNode('O-001', 'O-001.1', { title: '建立选题数据库 (≥100 条)', owner: 'hot-topic-hunter', priority: 'P1', }); assert(kr2.nodeId === 'O-001.1.2', `KR2 ID = O-001.1.2 (实际: ${kr2.nodeId})`); // 13d: 更新节点状态 companyGoals.updateNode('O-001', 'O-001.1.1', { status: 'in_progress' }); const updatedNode = companyGoals.getNode('O-001', 'O-001.1.1'); assert(updatedNode.status === 'in_progress', '节点状态更新为 in_progress'); // 13e: 进度计算 const progress = companyGoals.getProgress('O-001'); assert(progress.total === 2, `叶子节点总数 = 2 (实际: ${progress.total})`); assert(progress.inProgress === 1, `进行中 = 1 (实际: ${progress.inProgress})`); // 13f: Goal list const goalList = companyGoals.list(); assert(goalList.length === 1, `Goal 列表长度 = 1 (实际: ${goalList.length})`); assert(goalList[0].id === 'O-001', '列表中 Objective ID 正确'); // 13g: 验证 OKR 是公司隔离的 (全局 OkrService 看不到公司的目标) const globalGoals = new OkrService(); const globalList = globalGoals.list(); const hasCompanyGoal = globalList.some(g => g.id === 'O-001' && g.title === '30天粉丝破万'); assert(!hasCompanyGoal, '全局 OkrService 看不到公司级 OKR (隔离)'); // ─── 14. Phase 4: Pipeline SOP 流水线 ─── console.log('\n🛠️ Phase 14: Pipeline SOP 流水线'); const pSvc = new PipelineService(TEST_COMPANY_ID); // 14a: 创建流水线 const pipeline = pSvc.createPipeline({ id: 'daily-content', name: '每日内容产出', description: '选题 → 写稿 → 审核 → 发布', steps: [ { name: '热点搜索与选题', agent: 'hot-topic-hunter', prompt: '搜索今日热点并提供3个选题建议' }, { name: '内容写作', agent: 'content-writer', prompt: '根据选题写一篇3000字文章' }, { name: '质量审核', agent: 'qa-judge', prompt: '审核文章质量,给出修改意见' }, { name: '发布推送', agent: 'publisher', prompt: '排版并发布到公众号' }, ], }); assert(pipeline.id === 'daily-content', '流水线 ID 正确'); assert(pipeline.steps.length === 4, `步骤数 = 4 (实际: ${pipeline.steps.length})`); assert(pipeline.steps[0].agent === 'hot-topic-hunter', '第1步 agent 正确'); assert(pipeline.steps[1].dependsOn === 0, '第2步依赖第1步'); // 14b: 列出流水线 const pipelines = pSvc.listPipelines(); assert(pipelines.length === 1, `流水线数量 = 1 (实际: ${pipelines.length})`); // 14c: 获取流水线定义 const fetPipeline = pSvc.getPipeline('daily-content'); assert(fetPipeline !== null, '获取流水线定义成功'); assert(fetPipeline.name === '每日内容产出', '流水线名称正确'); // 14d: 启动运行 const run = pSvc.startRun('daily-content', { trigger: 'heartbeat', input: { date: '2026-03-25' }, }); assert(run.status === 'running', 'Run 状态为 running'); assert(run.steps.length === 4, `Run 步骤数 = 4`); assert(run.steps[0].status === 'ready', '第1步状态为 ready'); assert(run.steps[1].status === 'waiting', '第2步状态为 waiting'); assert(run.currentStep === 0, '当前步骤 = 0'); // 14e: 获取当前任务 const curTask = pSvc.getCurrentTask(run.runId); assert(curTask !== null, '当前任务不为空'); assert(curTask.agent === 'hot-topic-hunter', '当前任务分配给 hot-topic-hunter'); assert(curTask.stepIndex === 0, '当前任务是第0步'); // 14f: 标记步骤开始 pSvc.markStepStarted(run.runId, 0); const runAfterStart = pSvc.getRun(run.runId); assert(runAfterStart.steps[0].status === 'in_progress', '第1步状态变为 in_progress'); // 14g: 推进步骤 0 → 1 const run1 = pSvc.advanceStep(run.runId, 0, { output: { topics: ['AI提效工具', '副业经济', '新能源资讯'] }, }); assert(run1.steps[0].status === 'done', '第1步完成'); assert(run1.steps[1].status === 'ready', '第2步变为 ready'); assert(run1.currentStep === 1, '当前步骤推进到 1'); assert(run1.steps[1].input !== null, '第2步的 input 为第1步的 output'); // 14h: 推进到最后一步 pSvc.advanceStep(run.runId, 1, { output: { draft: '文章初稿...' } }); pSvc.advanceStep(run.runId, 2, { output: { approved: true, score: 92 } }); const runFinal = pSvc.advanceStep(run.runId, 3, { output: { publishUrl: 'https://mp.weixin.qq.com/s/xxx' } }); assert(runFinal.status === 'completed', 'Run 全部完成'); assert(runFinal.output !== null, 'Run 携带最终 output'); // 14i: 列出运行记录 const runs = pSvc.listRuns(); assert(runs.length === 1, `运行记录数 = 1 (实际: ${runs.length})`); assert(runs[0].status === 'completed', '运行状态为 completed'); // 14j: 更新流水线 const updPipeline = pSvc.updatePipeline('daily-content', { name: '每日内容产出 v2' }); assert(updPipeline.name === '每日内容产出 v2', '流水线名称更新成功'); // ─── 15. Phase 5: 心跳服务 + Pipeline 自动触发 ─── console.log('\n💓 Phase 15: 心跳服务 + Pipeline 自动触发'); // 15a: 创建带 cron 的流水线 const pSvc2 = new PipelineService(TEST_COMPANY_ID); const cronPipeline = pSvc2.createPipeline({ id: 'weekly-report', name: '周报汇总', description: '每周自动汇总运营数据', cron: { intervalHours: 168 }, // 每168小时 = 每周 steps: [ { name: '数据汇总', agent: 'ceo', prompt: '汇总本周运营数据' }, { name: '报告生成', agent: 'content-writer', prompt: '生成周报' }, ], }); assert(cronPipeline.cron !== null, '流水线包含 cron 配置'); assert(cronPipeline.cron.intervalHours === 168, 'cron 间隔 = 168小时'); // 15b: 心跳服务实例化 const hbSvc = new CompanyHeartbeatService(); assert(hbSvc.timers.size === 0, '初始无计时器'); // 15c: _shouldRunPipeline 检测 (cron 无运行记录 → 应该运行) const shouldRun1 = hbSvc._shouldRunPipeline(pSvc2, cronPipeline); assert(shouldRun1 === true, '无历史运行 → 应该触发'); // 15d: 手动启动一次 Run 后再检测 pSvc2.startRun('weekly-report', { trigger: 'test' }); const shouldRun2 = hbSvc._shouldRunPipeline(pSvc2, cronPipeline); assert(shouldRun2 === false, '刚运行过 → 不应触发 (间隔未到)'); // 15e: 注册/取消公司心跳 svc.update(TEST_COMPANY_ID, { status: 'running', heartbeat: { enabled: true, intervalMinutes: 5 } }); hbSvc.registerCompany(TEST_COMPANY_ID); assert(hbSvc.timers.has(TEST_COMPANY_ID), '公司心跳已注册'); hbSvc.unregisterCompany(TEST_COMPANY_ID); assert(!hbSvc.timers.has(TEST_COMPANY_ID), '公司心跳已取消'); // 15f: 驱动运行中的 Run (检查 getCurrentTask) const testRun = pSvc2.startRun('daily-content', { trigger: 'heartbeat-test' }); const readyTask = pSvc2.getCurrentTask(testRun.runId); assert(readyTask !== null, '新 Run 有 ready 任务'); assert(readyTask.agent === 'hot-topic-hunter', 'Ready 任务分配给正确 Agent'); // 15g: 超时检测 (模拟超时场景) pSvc2.markStepStarted(testRun.runId, 0); // 手动篡改 startedAt 为很久以前 const runData = pSvc2.getRun(testRun.runId); runData.steps[0].startedAt = new Date(Date.now() - 60 * 60 * 1000).toISOString(); // 1小时前 fs.writeFileSync(path.join(TEST_COMPANY_DIR, 'pipelines', 'runs', `${testRun.runId}.json`), JSON.stringify(runData, null, 2), 'utf-8'); // 清理 hbSvc.stop(); // ─── 16. 关停公司 ─── console.log('\n🛑 Phase 16: 关停公司'); const shutdown = svc.shutdown(TEST_COMPANY_ID); assert(shutdown.status === 'shutdown', '状态变为 shutdown'); assert(shutdown.heartbeat.enabled === false, '心跳已关闭'); // ─── 清理 ─── cleanup(); // 清理测试生成的模板 const testTplDir = path.join(TEMPLATES_DIR, 'hot-topic-hunter'); if (fs.existsSync(testTplDir)) fs.rmSync(testTplDir, { recursive: true, force: true }); // 恢复 registry (移除测试模板) const regPath = path.join(TEMPLATES_DIR, '_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 + 2 + 3 + 4 + 闭环 生产就绪!\n'); } else { console.log(` ⚠️ ${failed} 个测试未通过\n`); process.exit(1); } } main().catch(err => { console.error('测试异常:', err); cleanup(); process.exit(1); });