test-company-e2e.js
18.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
#!/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 { GoalsService } = require('../services/goals');
const { PipelineService } = require('../services/pipeline-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. Phase 3: Company Goals (公司级 OKR) ───
console.log('\n🎯 Phase 13: Company Goals (公司级 OKR)');
const companyGoals = new GoalsService(TEST_COMPANY_ID);
// 13a: 创建 Goal
const goal = companyGoals.create({
title: '30天粉丝破万',
description: '通过日更优质内容实现公众号粉丝增长',
owner: 'ceo',
});
assert(goal.id === 'G-001', `Goal ID = G-001 (实际: ${goal.id})`);
assert(goal.title === '30天粉丝破万', 'Goal 标题正确');
// 13b: 检查数据存储在公司目录下
const goalFilePath = path.join(TEST_COMPANY_DIR, 'goals', 'G-001', 'goal.json');
assert(fs.existsSync(goalFilePath), 'Goal 存储在公司目录下');
// 13c: 添加节点 (Objective + Key Results)
const obj = companyGoals.addNode('G-001', null, {
title: '建立内容产出流水线',
owner: 'ceo',
priority: 'P0',
});
assert(obj.nodeId === 'G-001.1', `Objective ID = G-001.1 (实际: ${obj.nodeId})`);
const kr1 = companyGoals.addNode('G-001', 'G-001.1', {
title: '日更1篇高质量文章',
owner: 'content-writer',
priority: 'P0',
});
assert(kr1.nodeId === 'G-001.1.1', `KR ID = G-001.1.1 (实际: ${kr1.nodeId})`);
const kr2 = companyGoals.addNode('G-001', 'G-001.1', {
title: '建立选题数据库 (≥100 条)',
owner: 'hot-topic-hunter',
priority: 'P1',
});
assert(kr2.nodeId === 'G-001.1.2', `KR2 ID = G-001.1.2 (实际: ${kr2.nodeId})`);
// 13d: 更新节点状态
companyGoals.updateNode('G-001', 'G-001.1.1', { status: 'in_progress' });
const updatedNode = companyGoals.getNode('G-001', 'G-001.1.1');
assert(updatedNode.status === 'in_progress', '节点状态更新为 in_progress');
// 13e: 进度计算
const progress = companyGoals.getProgress('G-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 === 'G-001', '列表中 Goal ID 正确');
// 13g: 验证 OKR 是公司隔离的 (全局 GoalsService 看不到公司的目标)
const globalGoals = new GoalsService();
const globalList = globalGoals.list();
const hasCompanyGoal = globalList.some(g => g.id === 'G-001' && g.title === '30天粉丝破万');
assert(!hasCompanyGoal, '全局 GoalsService 看不到公司级 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. 更新公司 ───
console.log('\n✏️ Phase 15: 更新公司');
const updated = svc.update(TEST_COMPANY_ID, {
status: 'running',
heartbeat: { enabled: true },
});
assert(updated.status === 'running', '状态更新为 running');
assert(updated.heartbeat.enabled === true, '心跳已开启');
// ─── 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(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 + 2 + 3 + 4 生产就绪!\n');
} else {
console.log(` ⚠️ ${failed} 个测试未通过\n`);
process.exit(1);
}
}
main().catch(err => {
console.error('测试异常:', err);
cleanup();
process.exit(1);
});