test-company-e2e.js
12.2 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
#!/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);
});