@ghyper9023/pi-dev-workflow 0.3.3 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,518 @@
1
+ /**
2
+ * test-workflow-engine.mjs — 测试工作流引擎核心逻辑
3
+ *
4
+ * Tests:
5
+ * 1. parseReviewerOutput — 解析 [REVIEW_SUMMARY] JSON
6
+ * 2. parseReviewerOutput — 兜底解析裸 JSON
7
+ * 3. parseReviewerOutput — 无效输入返回 null
8
+ * 4. Bug confirmation: parseReviewerOutput fails on JSON lines output
9
+ * 5. Fix verification: extractFinalOutput + parseReviewerOutput works
10
+ * 6. parseReviewerOutputFromFile — 读取审查文件
11
+ * 7. isTimeoutResult — 正确识别超时
12
+ * 8. isTimeoutResult — 非超时不误判
13
+ * 9. Checkpoint 序列化/反序列化
14
+ * 10. WorkflowStepDef 配置结构完整性
15
+ *
16
+ * Run: node tests/test-workflow-engine.mjs
17
+ */
18
+
19
+ import * as fs from "node:fs";
20
+ import * as path from "node:path";
21
+ import { fileURLToPath } from "node:url";
22
+
23
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
24
+ const EXT_PATH = path.resolve(__dirname, "../extensions/workflow-engine.ts");
25
+
26
+ // ── Helpers ──────────────────────────────────────────────────
27
+
28
+ let pass = 0;
29
+ let fail = 0;
30
+
31
+ function assert(condition, msg) {
32
+ if (condition) {
33
+ pass++;
34
+ console.log(` ✅ ${msg}`);
35
+ } else {
36
+ fail++;
37
+ console.error(` ❌ ${msg}`);
38
+ }
39
+ }
40
+
41
+ function assertEq(actual, expected, msg) {
42
+ const ok = actual === expected;
43
+ if (ok) {
44
+ pass++;
45
+ console.log(` ✅ ${msg}`);
46
+ } else {
47
+ fail++;
48
+ console.error(` ❌ ${msg} — 期望 ${JSON.stringify(expected)}, 得到 ${JSON.stringify(actual)}`);
49
+ }
50
+ }
51
+
52
+ // ── Read source ──────────────────────────────────────────────
53
+
54
+ let source;
55
+ try {
56
+ source = fs.readFileSync(EXT_PATH, "utf-8");
57
+ } catch (e) {
58
+ console.error(`Failed to read source file: ${e.message}`);
59
+ process.exit(1);
60
+ }
61
+
62
+ console.log(`📄 源文件: ${EXT_PATH}`);
63
+ console.log(`📏 文件大小: ${source.length} 字节\n`);
64
+
65
+ // ═══════════════════════════════════════════════════════════════
66
+ // 1. parseReviewerOutput — 解析 [REVIEW_SUMMARY] JSON
67
+ // ═══════════════════════════════════════════════════════════════
68
+
69
+ function simulateParseReviewerOutput(output) {
70
+ const match = output.match(/\[REVIEW_SUMMARY\]\s*(\{[\s\S]*?\})\s*\[\/REVIEW_SUMMARY\]/);
71
+ if (match) {
72
+ try {
73
+ const parsed = JSON.parse(match[1]);
74
+ if (parsed && typeof parsed.maxSeverity === "string") return parsed;
75
+ } catch {}
76
+ }
77
+ // fallback
78
+ const fallback = output.match(/\{"maxSeverity":\s*"(critical|medium|low)"[\s\S]*?\}/);
79
+ if (fallback) {
80
+ try {
81
+ const parsed = JSON.parse(fallback[0]);
82
+ if (parsed && typeof parsed.maxSeverity === "string") return parsed;
83
+ } catch {}
84
+ }
85
+ return null;
86
+ }
87
+
88
+ console.log("📋 parseReviewerOutput 测试\n");
89
+
90
+ // Test 1: 标准 [REVIEW_SUMMARY] 格式
91
+ const output1 = [
92
+ "其他审查内容...",
93
+ "[REVIEW_SUMMARY]",
94
+ '{"maxSeverity":"critical","critical":2,"medium":1,"low":3}',
95
+ "[/REVIEW_SUMMARY]",
96
+ ].join("\n");
97
+
98
+ const r1 = simulateParseReviewerOutput(output1);
99
+ assertEq(r1?.maxSeverity, "critical", "[REVIEW_SUMMARY] 应解析 critical");
100
+ assertEq(r1?.critical, 2, "critical 计数正确");
101
+ assertEq(r1?.medium, 1, "medium 计数正确");
102
+ assertEq(r1?.low, 3, "low 计数正确");
103
+
104
+ // Test 2: 不同严重等级
105
+ const output2 = [
106
+ "[REVIEW_SUMMARY]",
107
+ '{"maxSeverity":"medium","critical":0,"medium":1,"low":2}',
108
+ "[/REVIEW_SUMMARY]",
109
+ ].join("\n");
110
+ assertEq(simulateParseReviewerOutput(output2)?.maxSeverity, "medium", "medium 等级应正确解析");
111
+
112
+ const output3 = [
113
+ "[REVIEW_SUMMARY]",
114
+ '{"maxSeverity":"low","critical":0,"medium":0,"low":0}',
115
+ "[/REVIEW_SUMMARY]",
116
+ ].join("\n");
117
+ assertEq(simulateParseReviewerOutput(output3)?.maxSeverity, "low", "low 等级应正确解析");
118
+
119
+ // Test 3: 兜底裸 JSON (无 [REVIEW_SUMMARY] 标记)
120
+ const output4 = '其他文本 {"maxSeverity":"critical","critical":1} 更多文本';
121
+ assertEq(simulateParseReviewerOutput(output4)?.maxSeverity, "critical", "兜底裸 JSON 应解析 critical");
122
+
123
+ // Test 4: 无效输入
124
+ assertEq(simulateParseReviewerOutput(""), null, "空字符串应返回 null");
125
+ assertEq(simulateParseReviewerOutput("无 JSON 内容"), null, "无 JSON 应返回 null");
126
+ assertEq(simulateParseReviewerOutput('{"invalid": true}'), null, "缺少 maxSeverity 应返回 null");
127
+
128
+ // Test 5: 跨越多行的复杂输出
129
+ const output5 = `
130
+ 一些审查文本
131
+ 更多内容
132
+
133
+ [REVIEW_SUMMARY]
134
+ {"maxSeverity":"medium","critical":0,"medium":3,"low":5}
135
+ [/REVIEW_SUMMARY]
136
+
137
+ 末尾内容
138
+ `;
139
+ const r5 = simulateParseReviewerOutput(output5);
140
+ assertEq(r5?.maxSeverity, "medium", "跨行复杂输出应正确解析");
141
+ assertEq(r5?.medium, 3, "跨行复杂输出的 medium 计数");
142
+
143
+ // ═══════════════════════════════════════════════════════════════
144
+ // Bug fix: parseReviewerOutput on JSON-formatted subagent output
145
+ // ═══════════════════════════════════════════════════════════════
146
+
147
+ console.log("\n📋 parseReviewerOutput 在 JSON 格式输出下的行为测试\n");
148
+
149
+ // 模拟 --mode json 输出的 JSON lines 格式(subagent 原始输出)
150
+ function buildJsonLinesOutput(plainText) {
151
+ // 模拟 pi --mode json 的流式输出格式
152
+ const lines = [];
153
+ const chunkSize = 80;
154
+ for (let i = 0; i < plainText.length; i += chunkSize) {
155
+ const chunk = plainText.slice(i, i + chunkSize);
156
+ lines.push(JSON.stringify({
157
+ type: "message_update",
158
+ assistantMessageEvent: {
159
+ type: "text_delta",
160
+ delta: chunk,
161
+ },
162
+ }));
163
+ }
164
+ lines.push(JSON.stringify({
165
+ type: "message_update",
166
+ assistantMessageEvent: {
167
+ type: "text_end",
168
+ content: plainText,
169
+ },
170
+ }));
171
+ return lines.join("\n");
172
+ }
173
+
174
+ // 模拟 extractFinalOutput(复制自 sub-agents.ts 的核心逻辑)
175
+ function simulateExtractFinalOutput(jsonOutput) {
176
+ let result = "";
177
+ let textEndSeen = false;
178
+ for (const line of jsonOutput.split("\n")) {
179
+ if (!line.trim()) continue;
180
+ try {
181
+ const event = JSON.parse(line);
182
+ if (!textEndSeen && event.type === "message_update" &&
183
+ event.assistantMessageEvent?.type === "text_delta") {
184
+ result += event.assistantMessageEvent.delta || "";
185
+ }
186
+ if (event.type === "message_update" &&
187
+ event.assistantMessageEvent?.type === "text_end" &&
188
+ event.assistantMessageEvent.content) {
189
+ result = event.assistantMessageEvent.content;
190
+ textEndSeen = true;
191
+ }
192
+ } catch { /* skip non-JSON lines */ }
193
+ }
194
+ return result;
195
+ }
196
+
197
+ // Test 6: BUG CONFIRMATION — parseReviewerOutput 在 JSON lines 输出中返回 null
198
+ const criticalText = [
199
+ "# 代码审查报告",
200
+ "",
201
+ "## 严重问题",
202
+ "### C1: 关键 bug",
203
+ "",
204
+ "[REVIEW_SUMMARY]",
205
+ '{"maxSeverity":"critical","critical":2,"medium":1,"low":0}',
206
+ "[/REVIEW_SUMMARY]",
207
+ ].join("\n");
208
+
209
+ const jsonOutput = buildJsonLinesOutput(criticalText);
210
+ const resultOnRawJson = simulateParseReviewerOutput(jsonOutput);
211
+ assertEq(resultOnRawJson, null, "BUG: parseReviewerOutput 在原始 JSON lines 输出上应返回 null(确认 bug)");
212
+
213
+ // Test 7: FIX VERIFICATION — 先用 extractFinalOutput 提取纯文本,再解析 REVIEW_SUMMARY
214
+ const extracted = simulateExtractFinalOutput(jsonOutput);
215
+ const resultOnExtracted = simulateParseReviewerOutput(extracted);
216
+ assertEq(resultOnExtracted?.maxSeverity, "critical", "FIX: 提取纯文本后应正确解析 critical");
217
+ assertEq(resultOnExtracted?.critical, 2, "FIX: critical 计数应正确");
218
+
219
+ // Test 8: FIX VERIFICATION — 中等等级
220
+ const mediumText = [
221
+ "[REVIEW_SUMMARY]",
222
+ '{"maxSeverity":"medium","critical":0,"medium":3,"low":2}',
223
+ "[/REVIEW_SUMMARY]",
224
+ ].join("\n");
225
+ const jsonOutput2 = buildJsonLinesOutput(mediumText);
226
+ const extracted2 = simulateExtractFinalOutput(jsonOutput2);
227
+ const result2 = simulateParseReviewerOutput(extracted2);
228
+ assertEq(result2?.maxSeverity, "medium", "FIX: 提取后 medium 等级应正确解析");
229
+ assertEq(result2?.medium, 3, "FIX: medium 计数应正确");
230
+
231
+ // Test 9: extractSeverityFromText — 从 ### C1. / ### M1. / ### L1. 格式解析
232
+ console.log("\n📋 extractSeverityFromText 测试\n");
233
+
234
+ function simulateExtractSeverityFromText(text) {
235
+ const headerCritical = [...text.matchAll(/^###\s+C\d+\./gm)].length;
236
+ const headerMedium = [...text.matchAll(/^###\s+M\d+\./gm)].length;
237
+ const headerLow = [...text.matchAll(/^###\s+L\d+\./gm)].length;
238
+ if (headerCritical + headerMedium + headerLow > 0) {
239
+ return {
240
+ maxSeverity: headerCritical > 0 ? "critical" : headerMedium > 0 ? "medium" : "low",
241
+ critical: headerCritical,
242
+ medium: headerMedium,
243
+ low: headerLow,
244
+ };
245
+ }
246
+ const tableCritical = [...text.matchAll(/^\|\s*\w+\s*\|\s*critical/gim)].length;
247
+ const tableMedium = [...text.matchAll(/^\|\s*\w+\s*\|\s*medium/gim)].length;
248
+ const tableLow = [...text.matchAll(/^\|\s*\w+\s*\|\s*low/gim)].length;
249
+ if (tableCritical + tableMedium + tableLow > 0) {
250
+ return {
251
+ maxSeverity: tableCritical > 0 ? "critical" : tableMedium > 0 ? "medium" : "low",
252
+ critical: tableCritical,
253
+ medium: tableMedium,
254
+ low: tableLow,
255
+ };
256
+ }
257
+ const labelCritical = [...text.matchAll(/\*\*(?:Severity|严重程度|严重性)\*\*\s*:\s*critical/gi)].length;
258
+ const labelMedium = [...text.matchAll(/\*\*(?:Severity|严重程度|严重性)\*\*\s*:\s*medium/gi)].length;
259
+ const labelLow = [...text.matchAll(/\*\*(?:Severity|严重程度|严重性)\*\*\s*:\s*low/gi)].length;
260
+ if (labelCritical + labelMedium + labelLow > 0) {
261
+ return {
262
+ maxSeverity: labelCritical > 0 ? "critical" : labelMedium > 0 ? "medium" : "low",
263
+ critical: labelCritical,
264
+ medium: labelMedium,
265
+ low: labelLow,
266
+ };
267
+ }
268
+ return null;
269
+ }
270
+
271
+ // 模拟 review-20260519-230500.md 的格式
272
+ const reviewText1 = `# 代码审查\n\n### C1. [Bug] 第一个严重问题\n内容...\n### C2. [Bug] 第二个严重问题\n### M1. [优化] 中等问题\n### L1. [风格] 低优先级`;
273
+ const sev1 = simulateExtractSeverityFromText(reviewText1);
274
+ assertEq(sev1?.maxSeverity, "critical", "### C1. 格式应解析为 critical");
275
+ assertEq(sev1?.critical, 2, "### C1./C2. 应计数 2 critical");
276
+ assertEq(sev1?.medium, 1, "### M1. 应计数 1 medium");
277
+ assertEq(sev1?.low, 1, "### L1. 应计数 1 low");
278
+
279
+ // 模拟 review-20260519-231500.md 的混合格式(header 优先)
280
+ const reviewText2 = `# 复核审查\n\n### C1. [编译错误] 文件被截断\n### C2. [编译错误] buildCp 未定义\n### C3. 重复代码\n\n| 编号 | 等级 | 状态 | 说明 |\n| C2 | critical | ✅ | 确认 |\n| M1 | medium | ✅ | 确认 |`;
281
+ const sev2 = simulateExtractSeverityFromText(reviewText2);
282
+ assertEq(sev2?.maxSeverity, "critical", "混合格式 header 优先应解析为 critical");
283
+ assertEq(sev2?.critical, 3, "Header 优先: 3 critical (C1/C2/C3), 不重复计数表格行");
284
+
285
+ // 纯表格格式(无 header)
286
+ const reviewText3 = `| 编号 | 等级 | 说明 |\n| --- | --- | --- |\n| C1 | critical | Bug |\n| M1 | medium | 优化 |\n| M2 | medium | 重构 |`;
287
+ const sev3 = simulateExtractSeverityFromText(reviewText3);
288
+ assertEq(sev3?.maxSeverity, "critical", "纯表格格式应解析为 critical");
289
+ assertEq(sev3?.critical, 1, "纯表格: 1 critical");
290
+ assertEq(sev3?.medium, 2, "纯表格: 2 medium");
291
+
292
+ // **Severity**: critical 标签格式
293
+ const reviewText4 = `## 问题\n**Severity**: critical\n内容...\n**严重程度**: medium\n其他内容`;
294
+ const sev4 = simulateExtractSeverityFromText(reviewText4);
295
+ assertEq(sev4?.maxSeverity, "critical", "**Severity**: 标签格式应解析为 critical");
296
+ assertEq(sev4?.critical, 1, "标签格式 critical 计数");
297
+
298
+ // 无匹配
299
+ const reviewText5 = `# 正常文档\n没有任何严重标记`;
300
+ assertEq(simulateExtractSeverityFromText(reviewText5), null, "无匹配应返回 null");
301
+
302
+ // 空文本
303
+ assertEq(simulateExtractSeverityFromText(""), null, "空文本应返回 null");
304
+
305
+ // Test 10: readLatestReviewMd — 从实际审查文件读取
306
+ console.log("\n📋 readLatestReviewMd + extractSeverityFromText 集成测试\n");
307
+
308
+ function simulateReadLatestReviewMd(cwd) {
309
+ const reviewDir = path.join(cwd, ".pi-dev-output", "pi-review", "md");
310
+ try {
311
+ if (!fs.existsSync(reviewDir)) return null;
312
+ const files = fs.readdirSync(reviewDir)
313
+ .filter(f => f.endsWith(".md"))
314
+ .map(f => ({ name: f, mtime: fs.statSync(path.join(reviewDir, f)).mtimeMs }))
315
+ .sort((a, b) => b.mtime - a.mtime);
316
+ if (files.length === 0) return null;
317
+ return fs.readFileSync(path.join(reviewDir, files[0].name), "utf-8");
318
+ } catch { return null; }
319
+ }
320
+
321
+ const reviewContent = simulateReadLatestReviewMd(__dirname + "/..");
322
+ if (reviewContent) {
323
+ const result = simulateExtractSeverityFromText(reviewContent);
324
+ // 最新的审查文件(230500 或 231500)都有 critical 问题
325
+ assertEq(typeof result?.maxSeverity, "string", "从实际审查文件应解析出 severity");
326
+ // 至少有一个 critical(两份新文件都有)
327
+ assert(result?.critical > 0, "审查文件中应检测到 critical 问题");
328
+ } else {
329
+ assert(true, "无审查文件时跳过(非错误)");
330
+ }
331
+
332
+ // 不存在的目录
333
+ assertEq(simulateReadLatestReviewMd("/nonexistent/path"), null, "不存在的目录应返回 null");
334
+
335
+ // Test 11: 验证 extractFinalOutput 对已损坏/不完整输出的鲁棒性
336
+ const partialOutput = '{"type":"message_start"}\n{"invalid json\n';
337
+ const partialExtracted = simulateExtractFinalOutput(partialOutput);
338
+ assertEq(typeof partialExtracted, "string", "部分损坏的输出应返回字符串而非崩溃");
339
+
340
+ // ═══════════════════════════════════════════════════════════════
341
+ // 12. isTimeoutResult
342
+ // ═══════════════════════════════════════════════════════════════
343
+
344
+ console.log("\n📋 isTimeoutResult 测试\n");
345
+
346
+ function simulateIsTimeoutResult(result) {
347
+ return result.exitCode === -1 && (result.stderr || "").includes("timed out");
348
+ }
349
+
350
+ assertEq(
351
+ simulateIsTimeoutResult({ exitCode: -1, stderr: "timed out after 300s", output: "", durationMs: 0 }),
352
+ true,
353
+ "exitCode=-1 + timed out → true",
354
+ );
355
+ assertEq(
356
+ simulateIsTimeoutResult({ exitCode: 0, stderr: "", output: "ok", durationMs: 100 }),
357
+ false,
358
+ "exitCode=0 → false",
359
+ );
360
+ assertEq(
361
+ simulateIsTimeoutResult({ exitCode: 1, stderr: "some error", output: "", durationMs: 100 }),
362
+ false,
363
+ "exitCode=1 + 无 timed out → false",
364
+ );
365
+ assertEq(
366
+ simulateIsTimeoutResult({ exitCode: -1, stderr: "other error", output: "", durationMs: 100 }),
367
+ false,
368
+ "exitCode=-1 + 无 timed out → false",
369
+ );
370
+
371
+ // ═══════════════════════════════════════════════════════════════
372
+ // 3. Checkpoint 序列化/反序列化
373
+ // ═══════════════════════════════════════════════════════════════
374
+
375
+ console.log("\n📋 Checkpoint 序列化测试\n");
376
+
377
+ const sampleCheckpoint = {
378
+ version: 1,
379
+ createdAt: "2026-05-19T08:00:00.000Z",
380
+ updatedAt: "2026-05-19T08:30:00.000Z",
381
+ prompt: "测试 prompt",
382
+ mode: "attended",
383
+ steps: [
384
+ { status: "done", durationMs: 15000 },
385
+ { status: "done", durationMs: 45000, loopCount: 2 },
386
+ { status: "pending" },
387
+ { status: "pending" },
388
+ ],
389
+ currentStepIndex: 2,
390
+ loopCounts: { "worker-reviewer": 2 },
391
+ };
392
+
393
+ // 序列化 → 反序列化
394
+ const serialized = JSON.stringify(sampleCheckpoint);
395
+ const deserialized = JSON.parse(serialized);
396
+
397
+ assertEq(deserialized.version, 1, "checkpoint version 应保持");
398
+ assertEq(deserialized.mode, "attended", "mode 应保持");
399
+ assertEq(deserialized.currentStepIndex, 2, "currentStepIndex 应保持");
400
+ assertEq(deserialized.loopCounts["worker-reviewer"], 2, "loopCounts 应保持");
401
+ assertEq(deserialized.steps[1].loopCount, 2, "step loopCount 应保持");
402
+ assertEq(deserialized.steps[1].durationMs, 45000, "step durationMs 应保持");
403
+
404
+ // ═══════════════════════════════════════════════════════════════
405
+ // 4. WorkflowStepDef 配置结构完整性
406
+ // ═══════════════════════════════════════════════════════════════
407
+
408
+ console.log("\n📋 WorkflowStepDef 配置结构测试\n");
409
+
410
+ // 验证 dev-prompts.ts 中的 FEAT_WORKFLOW_STEPS
411
+ const devPromptsPath = path.resolve(__dirname, "../extensions/dev-prompts.ts");
412
+ const devPrompts = fs.readFileSync(devPromptsPath, "utf-8");
413
+
414
+ // 检查 FEAT_WORKFLOW_STEPS 定义是否存在
415
+ assert(
416
+ devPrompts.includes("FEAT_WORKFLOW_STEPS"),
417
+ "dev-prompts.ts 应定义 FEAT_WORKFLOW_STEPS",
418
+ );
419
+
420
+ // 检查是否包含 planner 定义
421
+ assert(
422
+ devPrompts.includes('agentName: "planner"'),
423
+ "FEAT_WORKFLOW_STEPS 应包含 planner agent",
424
+ );
425
+
426
+ // 检查 loop-group 类型的工作流步骤(全部配置合计)
427
+ const loopGroupCount = (devPrompts.match(/type: "loop-group"/g) || []).length;
428
+ assertEq(loopGroupCount, 8, "所有 WORKFLOW_STEPS 合计应包含 8 个 loop-group 步骤");
429
+
430
+ // 检查 confirm 类型
431
+ assert(
432
+ devPrompts.includes('type: "confirm"'),
433
+ "FEAT_WORKFLOW_STEPS 应包含 confirm 类型步骤 (docWriter)",
434
+ );
435
+
436
+ // 检查所有 agent name 引用
437
+ assert(devPrompts.includes('agentName: "planner"'), "包含 planner agent");
438
+ assert(devPrompts.includes('loopAgentName: "worker"'), "包含 worker loop agent");
439
+ assert(devPrompts.includes('loopAgentName: "trimmer"'), "包含 trimmer loop agent");
440
+ assert(devPrompts.includes('reviewAgentName: "reviewer"'), "包含 reviewer agent");
441
+ assert(devPrompts.includes('agentName: "docWriter"'), "包含 docWriter agent");
442
+
443
+ // ═══════════════════════════════════════════════════════════════
444
+ // 5. Agent 定义文件存在性验证
445
+ // ═══════════════════════════════════════════════════════════════
446
+
447
+ console.log("\n📋 Agent 定义文件存在性测试\n");
448
+
449
+ const agentDir = path.resolve(__dirname, "../agents/workflow");
450
+ const expectedAgents = ["planner-agent.md", "worker-agent.md", "reviewer-agent.md", "trimmer-agent.md", "docWriter-agent.md"];
451
+
452
+ for (const agentFile of expectedAgents) {
453
+ const fullPath = path.join(agentDir, agentFile);
454
+ assert(
455
+ fs.existsSync(fullPath),
456
+ `agents/workflow/${agentFile} 应存在`,
457
+ );
458
+ // 验证 frontmatter
459
+ const content = fs.readFileSync(fullPath, "utf-8");
460
+ assert(
461
+ content.startsWith("---"),
462
+ `${agentFile} 应以 YAML frontmatter 开头`,
463
+ );
464
+ assert(
465
+ /content.includes("name:")/,
466
+ `${agentFile} 应包含 name 字段`,
467
+ );
468
+ }
469
+
470
+ // ═══════════════════════════════════════════════════════════════
471
+ // 6. workflow-engine.ts 导出完整性
472
+ // ═══════════════════════════════════════════════════════════════
473
+
474
+ console.log("\n📋 workflow-engine.ts 导出完整性测试\n");
475
+
476
+ assert(
477
+ source.includes("export async function runWorkflow"),
478
+ "应导出 runWorkflow",
479
+ );
480
+ assert(
481
+ source.includes("export function parseReviewerOutput"),
482
+ "应导出 parseReviewerOutput",
483
+ );
484
+ assert(
485
+ source.includes("export function isTimeoutResult"),
486
+ "应导出 isTimeoutResult",
487
+ );
488
+ assert(
489
+ source.includes("export function loadCheckpointFromFile"),
490
+ "应导出 loadCheckpointFromFile",
491
+ );
492
+ assert(
493
+ source.includes("export function deleteCheckpointFile"),
494
+ "应导出 deleteCheckpointFile",
495
+ );
496
+ assert(
497
+ source.includes("export interface WorkflowStepDef"),
498
+ "应导出 WorkflowStepDef",
499
+ );
500
+ assert(
501
+ source.includes("export interface WorkflowConfig"),
502
+ "应导出 WorkflowConfig",
503
+ );
504
+
505
+ // ═══════════════════════════════════════════════════════════════
506
+ // Summary
507
+ // ═══════════════════════════════════════════════════════════════
508
+
509
+ console.log(`\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
510
+ console.log(`结果: ${pass} 通过, ${fail} 失败, 共 ${pass + fail} 个测试`);
511
+ console.log(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
512
+
513
+ if (fail > 0) {
514
+ console.error("\n⚠️ 部分测试未通过");
515
+ process.exit(1);
516
+ } else {
517
+ console.log("\n✅ 所有测试通过");
518
+ }