@ghyper9023/pi-dev-workflow 0.4.0 → 0.4.2

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.
Files changed (43) hide show
  1. package/.pi-dev-output/pi-grill/answers/answer-mpds3by7-20260520-1606.md +14 -0
  2. package/.pi-dev-output/pi-grill/answers/answer-mpfe77f1-20260521-1913.md +58 -0
  3. package/.pi-dev-output/pi-grill/answers/answer-mpfh37wu-20260521-2034.md +13 -0
  4. package/.pi-dev-output/pi-grill/answers/answer-mpfi5q4c-20260521-2104.md +13 -0
  5. package/.pi-dev-output/pi-grill/answers/answer-mpfizccb-20260521-2127.md +13 -0
  6. package/.pi-dev-output/pi-grill/answers/answer-mpfjk78k-20260521-2143.md +13 -0
  7. package/.pi-dev-output/pi-grill/questions/questions-mpfdz1tz-20260521-1907.json +94 -0
  8. package/.pi-dev-output/pi-plans/20260520-153000-fix-workflow-engine-bugs.md +150 -0
  9. package/.pi-dev-output/pi-plans/20260521-113000-fix-loopcount-timeout.md +215 -0
  10. package/.pi-dev-output/pi-plans/20260521-1730-grill-input-wrap-back-fix.md +240 -0
  11. package/.pi-dev-output/pi-plans/20260521-230000-fix-timeout-display-loopcount-gitdiff.md +253 -0
  12. package/.pi-dev-output/pi-plans/20260521-230500-esc-double-press-confirm-workflow.md +137 -0
  13. package/.pi-dev-output/pi-plans/20260521-235000-fix-gitdiff-loopcount.md +258 -0
  14. package/.pi-dev-output/pi-review/html/20260521-2305-review-workflow-index.html +196 -0
  15. package/.pi-dev-output/pi-review/md/review-20260520-100000.md +91 -0
  16. package/.pi-dev-output/pi-review/md/review-20260521-140000.md +191 -0
  17. package/.pi-dev-output/pi-review/md/review-20260521-190000.md +189 -0
  18. package/.pi-dev-output/pi-review/md/review-20260521-204500.md +241 -0
  19. package/.pi-dev-output/pi-review/md/review-20260521-214500.md +270 -0
  20. package/.pi-dev-output/pi-review/md/review-20260521-215158.md +214 -0
  21. package/.pi-dev-output/pi-review/md/review-20260521-234500.md +201 -0
  22. package/.pi-dev-output/pi-review/md/review-20260521-235500.md +422 -0
  23. package/.pi-dev-output/pi-review/md/review-20260522-000000.md +212 -0
  24. package/.pi-dev-output/pi-review/md/review-20260522-003000.md +377 -0
  25. package/.pi-dev-output/pi-review/md/review-20260522-003500.md +296 -0
  26. package/.pi-dev-output/pi-workflow/checkpoint-20260520-153000-fix-workflow-engine-bugs.json +108 -0
  27. package/.pi-dev-output/pi-workflow/checkpoint-20260521-113000-fix-loopcount-timeout.json +402 -0
  28. package/.pi-dev-output/pi-workflow/checkpoint-20260521-1730-grill-input-wrap-back-fix.json +447 -0
  29. package/.pi-dev-output/pi-workflow/checkpoint-20260521-230000-fix-timeout-display-loopcount-gitdiff.json +708 -0
  30. package/.pi-dev-output/pi-workflow/checkpoint-20260521-230500-esc-double-press-confirm-workflow.json +365 -0
  31. package/.pi-dev-output/pi-workflow/checkpoint-20260521-235000-fix-gitdiff-loopcount.json +395 -0
  32. package/.pi-dev-output/pi-workflow/checkpoint-archive-mpfhyxc5.json +30 -0
  33. package/.pi-dev-output/pi-workflow/checkpoint-archive-mpfi2unc.json +49 -0
  34. package/.pi-dev-output/pi-workflow/checkpoint-archive-mpfi382e.json +59 -0
  35. package/.pi-dev-output/pi-workflow/checkpoint-archive-mpfi5r22.json +76 -0
  36. package/extensions/dev-prompts.ts +16 -8
  37. package/extensions/grill-me-agent.ts +23 -7
  38. package/extensions/ui-helpers.ts +59 -8
  39. package/extensions/workflow-engine.ts +116 -35
  40. package/package.json +1 -1
  41. package/tests/test-loopcount-timeout-fix.mjs +336 -0
  42. package/tests/test-workflow-engine-bugs.mjs +349 -0
  43. package/themes/oh-my-pi-titanium.json +90 -0
@@ -0,0 +1,336 @@
1
+ /**
2
+ * test-loopcount-timeout-fix.mjs
3
+ *
4
+ * 测试 loopCount UI 同步、reviewer 独立超时、默认超时值修改
5
+ *
6
+ * 测试范围:
7
+ * 1. loopCount 在每次循环后通过 updateWidgetStep 同步到 widget
8
+ * 2. reviewer 使用 reviewTimeoutMs(独立超时)
9
+ * 3. 默认超时值 worker=30min (1_800_000), trimmer=20min (1_200_000), reviewer=15min (900_000)
10
+ * 4. loop-group 行不显示 timeoutMs(通过静态分析 updateWidgetStep 参数)
11
+ * 5. sub-step 在 runAgentWithProgress 中写入 detail(超时信息)
12
+ * 6. 回归:非 loop-group 步骤仍正常显示超时
13
+ */
14
+
15
+ import * as fs from "node:fs";
16
+ import * as path from "node:path";
17
+ import { fileURLToPath } from "node:url";
18
+
19
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
20
+ const EXT_DIR = path.resolve(__dirname, "..", "extensions");
21
+
22
+ // ── Test helpers ──
23
+
24
+ let passed = 0;
25
+ let failed = 0;
26
+ const results = [];
27
+
28
+ function assert(label, condition, detail = "") {
29
+ if (condition) {
30
+ passed++;
31
+ results.push({ label, ok: true });
32
+ } else {
33
+ failed++;
34
+ results.push({ label, ok: false, detail });
35
+ console.error(` ❌ ${label}${detail ? " — " + detail : ""}`);
36
+ }
37
+ }
38
+
39
+ function assertIncludes(label, text, pattern, detail = "") {
40
+ assert(label, text.includes(pattern), detail);
41
+ }
42
+
43
+ function assertNotIncludes(label, text, pattern, detail = "") {
44
+ assert(label, !text.includes(pattern), detail);
45
+ }
46
+
47
+ // ── Read source files ──
48
+
49
+ const weContent = fs.readFileSync(path.join(EXT_DIR, "workflow-engine.ts"), "utf8");
50
+ const dpContent = fs.readFileSync(path.join(EXT_DIR, "dev-prompts.ts"), "utf8");
51
+ const uhContent = fs.readFileSync(path.join(EXT_DIR, "ui-helpers.ts"), "utf8");
52
+
53
+ // ═══════════════════════════════════════════════════════════════
54
+ // Test 1: loopCount UI sync
55
+ // 验证 executeLoopGroup 中 loopCount++ 后调用 updateWidgetStep
56
+ // ═══════════════════════════════════════════════════════════════
57
+
58
+ console.log("\n📋 Test 1: loopCount UI 同步");
59
+
60
+ assertIncludes(
61
+ "1.1 executeLoopGroup 中有 loopCount++ 后立即更新 UI",
62
+ weContent,
63
+ "// 立即更新 UI 显示当前循环次数",
64
+ );
65
+
66
+ assertIncludes(
67
+ "1.2 updateWidgetStep 调用中传入 loopCount",
68
+ weContent,
69
+ "loopCount,",
70
+ );
71
+
72
+ assertIncludes(
73
+ "1.3 updateWidgetStep 调用中包含 loopCount 参数名",
74
+ weContent,
75
+ "loopCount,", // 在 extra 对象中
76
+ );
77
+
78
+ assertIncludes(
79
+ "1.4 updateWidgetStep 调用中包含 maxLoops",
80
+ weContent,
81
+ "maxLoops: step.maxLoops",
82
+ );
83
+
84
+ // ═══════════════════════════════════════════════════════════════
85
+ // Test 2: Reviewer 独立超时
86
+ // 验证 reviewer 使用 reviewTimeoutMs 而非 step.timeoutMs
87
+ // ═══════════════════════════════════════════════════════════════
88
+
89
+ console.log("\n📋 Test 2: Reviewer 独立超时");
90
+
91
+ assertIncludes(
92
+ "2.1 executeLoopGroup 中定义了 reviewTimeoutMs 变量",
93
+ weContent,
94
+ "const reviewTimeoutMs = step.reviewTimeoutMs ?? step.timeoutMs;",
95
+ );
96
+
97
+ assertIncludes(
98
+ "2.2 reviewer 的 runAgentWithProgress 使用 reviewTimeoutMs",
99
+ weContent,
100
+ "step.reviewAgentName!, reviewTimeoutMs);",
101
+ );
102
+
103
+ assert(
104
+ "2.3 reviewer 调用不再使用 step.timeoutMs",
105
+ !weContent.includes("step.reviewAgentName!, step.timeoutMs)"),
106
+ "reviewer 的 runAgentWithProgress 不应该使用 step.timeoutMs",
107
+ );
108
+
109
+ assertIncludes(
110
+ "2.4 Worker 仍使用 step.timeoutMs(未受 reviewer 变更影响)",
111
+ weContent,
112
+ "step.loopAgentName!, step.timeoutMs)",
113
+ "注意:worker 的 runAgentWithProgress 仍使用 step.timeoutMs",
114
+ );
115
+
116
+ // ═══════════════════════════════════════════════════════════════
117
+ // Test 3: 默认超时值验证(读取 dev-prompts.ts)
118
+ // ═══════════════════════════════════════════════════════════════
119
+
120
+ console.log("\n📋 Test 3: 默认超时值验证");
121
+
122
+ // Helper: extract all loop-group config blocks
123
+ function extractLoopGroups(source) {
124
+ const lines = source.split("\n");
125
+ const groups = [];
126
+ let currentBlock = null;
127
+ let braceDepth = 0;
128
+
129
+ for (const line of lines) {
130
+ if (line.includes("type: \"loop-group\"")) {
131
+ currentBlock = { start: lines.indexOf(line), lines: [line], loopAgent: "", reviewAgent: "", timeoutMs: 0, reviewTimeoutMs: 0 };
132
+ braceDepth = 0;
133
+ continue;
134
+ }
135
+ if (currentBlock) {
136
+ currentBlock.lines.push(line);
137
+ if (line.includes("loopAgentName:")) {
138
+ const m = line.match(/loopAgentName:\s*"(\w+)"/);
139
+ if (m) currentBlock.loopAgent = m[1];
140
+ }
141
+ if (line.includes("reviewAgentName:")) {
142
+ const m = line.match(/reviewAgentName:\s*"(\w+)"/);
143
+ if (m) currentBlock.reviewAgent = m[1];
144
+ }
145
+ if (line.includes("timeoutMs:")) {
146
+ const m = line.match(/timeoutMs:\s*(\d[\d_]*)/);
147
+ if (m) currentBlock.timeoutMs = parseInt(m[1].replace(/_/g, ""), 10);
148
+ }
149
+ if (line.includes("reviewTimeoutMs:")) {
150
+ const m = line.match(/reviewTimeoutMs:\s*(\d[\d_]*)/);
151
+ if (m) currentBlock.reviewTimeoutMs = parseInt(m[1].replace(/_/g, ""), 10);
152
+ }
153
+ if (line.includes("},") || line.includes("} ;") || line.includes("};")) {
154
+ braceDepth++;
155
+ if (braceDepth >= 1) {
156
+ groups.push(currentBlock);
157
+ currentBlock = null;
158
+ }
159
+ }
160
+ }
161
+ }
162
+ return groups;
163
+ }
164
+
165
+ const loopGroups = extractLoopGroups(dpContent);
166
+
167
+ assert("3.0 找到至少 6 个 loop-group 配置", loopGroups.length >= 6, `找到 ${loopGroups.length} 个`);
168
+
169
+ // worker 相关:timeoutMs 应为 1_800_000 (30min)
170
+ const workerGroups = loopGroups.filter(g => g.loopAgent === "worker");
171
+ for (const g of workerGroups) {
172
+ assert(
173
+ `3.1 Worker loop-group 超时=30min: ${g.loopAgent} → ${g.timeoutMs}`,
174
+ g.timeoutMs === 1_800_000,
175
+ `期望 1_800_000,实际 ${g.timeoutMs}`,
176
+ );
177
+ assert(
178
+ `3.2 Worker loop-group reviewTimeoutMs=15min: ${g.loopAgent} → ${g.reviewTimeoutMs}`,
179
+ g.reviewTimeoutMs === 900_000,
180
+ `期望 900_000,实际 ${g.reviewTimeoutMs}`,
181
+ );
182
+ }
183
+
184
+ // trimmer 相关:timeoutMs 应为 1_200_000 (20min)
185
+ const trimmerGroups = loopGroups.filter(g => g.loopAgent === "trimmer");
186
+ for (const g of trimmerGroups) {
187
+ assert(
188
+ `3.3 Trimmer loop-group 超时=20min: ${g.loopAgent} → ${g.timeoutMs}`,
189
+ g.timeoutMs === 1_200_000,
190
+ `期望 1_200_000,实际 ${g.timeoutMs}`,
191
+ );
192
+ assert(
193
+ `3.4 Trimmer loop-group reviewTimeoutMs=15min: ${g.loopAgent} → ${g.reviewTimeoutMs}`,
194
+ g.reviewTimeoutMs === 900_000,
195
+ `期望 900_000,实际 ${g.reviewTimeoutMs}`,
196
+ );
197
+ }
198
+
199
+ // 所有 loop-group 都应有 reviewTimeoutMs
200
+ for (const g of loopGroups) {
201
+ assert(
202
+ `3.5 ${g.loopAgent} loop-group 有 reviewTimeoutMs`,
203
+ g.reviewTimeoutMs > 0,
204
+ `缺少 reviewTimeoutMs`,
205
+ );
206
+ }
207
+
208
+ // ═══════════════════════════════════════════════════════════════
209
+ // Test 4: loop-group 行不显示 timeout
210
+ // 验证 updateWidgetStep 对 loop-group 步骤不传 timeoutMs
211
+ // ═══════════════════════════════════════════════════════════════
212
+
213
+ console.log("\n📋 Test 4: loop-group 行不显示 timeout");
214
+
215
+ assertIncludes(
216
+ "4.1 loop-group running 状态不传 timeoutMs",
217
+ weContent,
218
+ 'step.type === "loop-group" ? undefined : step.timeoutMs',
219
+ );
220
+
221
+ // ═══════════════════════════════════════════════════════════════
222
+ // Test 5: sub-step 显示 timeout
223
+ // 验证 runAgentWithProgress 在 sub-step 的 detail 中写入超时
224
+ // ═══════════════════════════════════════════════════════════════
225
+
226
+ console.log("\n📋 Test 5: sub-step 显示 timeout");
227
+
228
+ assertIncludes(
229
+ "5.1 runAgentWithProgress 在新建 sub-step 时写入 detail",
230
+ weContent,
231
+ "detail: `超时时间${formatTimeout(timeoutMs)}`",
232
+ );
233
+
234
+ assertIncludes(
235
+ "5.2 runAgentWithProgress 在复用 sub-step 时更新 detail",
236
+ weContent,
237
+ "existing.detail = `超时时间${formatTimeout(timeoutMs)}`;",
238
+ );
239
+
240
+ // ═══════════════════════════════════════════════════════════════
241
+ // Test 6: formatTimeout 已导出
242
+ // ═══════════════════════════════════════════════════════════════
243
+
244
+ console.log("\n📋 Test 6: formatTimeout 导出");
245
+
246
+ assertIncludes(
247
+ "6.1 formatTimeout 从 ui-helpers.ts 导出",
248
+ uhContent,
249
+ "export function formatTimeout",
250
+ );
251
+
252
+ assertIncludes(
253
+ "6.2 formatTimeout 被 workflow-engine.ts 导入",
254
+ weContent,
255
+ "formatTimeout,",
256
+ );
257
+
258
+ // ═══════════════════════════════════════════════════════════════
259
+ // Test 7: WorkflowStepDef 接口有 reviewTimeoutMs 字段
260
+ // ═══════════════════════════════════════════════════════════════
261
+
262
+ console.log("\n📋 Test 7: WorkflowStepDef 接口");
263
+
264
+ assertIncludes(
265
+ "7.1 WorkflowStepDef 有 reviewTimeoutMs 字段定义",
266
+ weContent,
267
+ "reviewTimeoutMs?: number;",
268
+ );
269
+
270
+ // ═══════════════════════════════════════════════════════════════
271
+ // Test 8: 回归测试 — 非 loop-group 功能不受影响
272
+ // ═══════════════════════════════════════════════════════════════
273
+
274
+ console.log("\n📋 Test 8: 回归测试");
275
+
276
+ assertIncludes(
277
+ "8.1 非 loop-group 步骤运行状态仍传 timeoutMs",
278
+ weContent,
279
+ "timeoutMs: step.type === \"loop-group\" ? undefined : step.timeoutMs,", // 对于非 loop-group 仍传
280
+ );
281
+
282
+ assertIncludes(
283
+ "8.2 非 loop-group 步骤 done 状态仍传 timeoutMs",
284
+ weContent,
285
+ "timeoutMs: step.type === \"loop-group\" ? undefined : step.timeoutMs,", // done 状态同样逻辑
286
+ );
287
+
288
+ // 验证 executeSingleStep 未受影响 — 它仍然传递超时给 runAgentWithProgress
289
+ if (weContent.includes("executeSingleStep")) {
290
+ assert(
291
+ "8.3 executeSingleStep 中仍传递 timeoutMs",
292
+ true,
293
+ );
294
+ }
295
+
296
+ // ═══════════════════════════════════════════════════════════════
297
+ // Test 9: sub-step 重置 — 每次循环重置 sub-step 状态
298
+ // ═══════════════════════════════════════════════════════════════
299
+
300
+ console.log("\n📋 Test 9: sub-step 状态重置");
301
+
302
+ assertIncludes(
303
+ "9.1 每次循环开始时重置 sub-step 状态",
304
+ weContent,
305
+ "// 每次循环开始时重置 sub-step 状态",
306
+ );
307
+
308
+ assert(
309
+ "9.2 loopAgent sub-step 在循环开始被重置为 pending",
310
+ weContent.includes('setWidgetSubStepStatus(stepIndex, step.loopAgentName!, "pending")'),
311
+ );
312
+
313
+ assert(
314
+ "9.3 reviewAgent sub-step 在循环开始被重置为 pending",
315
+ weContent.includes('setWidgetSubStepStatus(stepIndex, step.reviewAgentName!, "pending")'),
316
+ );
317
+
318
+ // ═══════════════════════════════════════════════════════════════
319
+ // Summary
320
+ // ═══════════════════════════════════════════════════════════════
321
+
322
+ console.log("\n" + "═".repeat(60));
323
+ console.log(`结果: ${passed} 通过, ${failed} 失败, 共 ${passed + failed} 个测试`);
324
+ console.log("═".repeat(60));
325
+
326
+ if (failed > 0) {
327
+ console.error("\n⚠️ 部分测试未通过:");
328
+ for (const r of results) {
329
+ if (!r.ok) {
330
+ console.error(` ❌ ${r.label}${r.detail ? " — " + r.detail : ""}`);
331
+ }
332
+ }
333
+ process.exit(1);
334
+ } else {
335
+ console.log("\n✅ 全部通过");
336
+ }
@@ -0,0 +1,349 @@
1
+ /**
2
+ * test-workflow-engine-bugs.mjs — 复现并验证 Bug A 和 Bug B 的修复
3
+ *
4
+ * Bug A — executeLoopGroup 缺少 exitCode 检查
5
+ * Bug B — setTimeout cleanupWidget 竞态条件
6
+ *
7
+ * Run: node tests/test-workflow-engine-bugs.mjs
8
+ */
9
+
10
+ import * as fs from "node:fs";
11
+ import * as path from "node:path";
12
+ import { fileURLToPath } from "node:url";
13
+
14
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
15
+ const EXT_PATH = path.resolve(__dirname, "../extensions/workflow-engine.ts");
16
+
17
+ // ── Read source file for static analysis ─────────────────────
18
+
19
+ let source;
20
+ try {
21
+ source = fs.readFileSync(EXT_PATH, "utf-8");
22
+ } catch (e) {
23
+ console.error(`Failed to read source file: ${e.message}`);
24
+ process.exit(1);
25
+ }
26
+
27
+ console.log(`📄 源文件: ${EXT_PATH}`);
28
+ console.log(`📏 文件大小: ${source.length} 字节\n`);
29
+
30
+ // ── Helpers ──────────────────────────────────────────────────
31
+
32
+ let pass = 0;
33
+ let fail = 0;
34
+
35
+ function assert(condition, msg) {
36
+ if (condition) {
37
+ pass++;
38
+ console.log(` ✅ ${msg}`);
39
+ } else {
40
+ fail++;
41
+ console.error(` ❌ ${msg}`);
42
+ }
43
+ }
44
+
45
+ function assertEq(actual, expected, msg) {
46
+ const ok = actual === expected;
47
+ if (ok) {
48
+ pass++;
49
+ console.log(` ✅ ${msg}`);
50
+ } else {
51
+ fail++;
52
+ console.error(` ❌ ${msg} — 期望 ${JSON.stringify(expected)}, 得到 ${JSON.stringify(actual)}`);
53
+ }
54
+ }
55
+
56
+ function assertTrue(actual, msg) { assertEq(actual, true, msg); }
57
+ function assertFalse(actual, msg) { assertEq(actual, false, msg); }
58
+ function assertNotNull(actual, msg) {
59
+ if (actual !== null && actual !== undefined) {
60
+ pass++;
61
+ console.log(` ✅ ${msg}`);
62
+ } else {
63
+ fail++;
64
+ console.error(` ❌ ${msg} — 期望非 null, 得到 ${JSON.stringify(actual)}`);
65
+ }
66
+ }
67
+
68
+ function assertThrows(fn, msg) {
69
+ try {
70
+ fn();
71
+ fail++;
72
+ console.error(` ❌ ${msg} — 期望抛出异常但未抛出`);
73
+ } catch {
74
+ pass++;
75
+ console.log(` ✅ ${msg}`);
76
+ }
77
+ }
78
+
79
+ // ═══════════════════════════════════════════════════════════════
80
+ // isTimeoutResult — 从源代码导入逻辑(模拟)
81
+ // ═══════════════════════════════════════════════════════════════
82
+
83
+ function simulateIsTimeoutResult(result) {
84
+ return result.exitCode === -1 && result.stderr.includes("timed out");
85
+ }
86
+
87
+ console.log("═══ Bug A 测试 — executeLoopGroup exitCode 检查 ═══\n");
88
+
89
+ // ── Test 1: 模拟 SubagentResult 对象,验证非零退出码被正确识别 ──
90
+ console.log("📋 测试 1: 非零退出码识别\n");
91
+
92
+ const resultError = { exitCode: 1, stderr: "Agent crashed: OOM", output: "" };
93
+ assertFalse(simulateIsTimeoutResult(resultError), "exitCode=1 不应被 isTimeoutResult 误判为超时");
94
+ assertEq(resultError.exitCode, 1, "exitCode 应为 1");
95
+ assert(resultError.exitCode !== 0, "exitCode 非零");
96
+
97
+ const resultTimeout = { exitCode: -1, stderr: "timed out after 30s", output: "" };
98
+ assertTrue(simulateIsTimeoutResult(resultTimeout), "exitCode=-1 + 'timed out' 应被识别为超时");
99
+
100
+ const resultSuccess = { exitCode: 0, stderr: "", output: "ok" };
101
+ assertFalse(simulateIsTimeoutResult(resultSuccess), "exitCode=0 不应被识别为超时");
102
+ assertEq(resultSuccess.exitCode, 0, "exitCode 应为 0");
103
+
104
+ // ── Test 2: 验证源代码中存在 exitCode 检查(Bug A 修复验证) ──
105
+ console.log("\n📋 测试 2: 源代码静态分析 — Bug A 修复存在性\n");
106
+
107
+ // 检查 executeLoopGroup 函数中是否有 exitCode !== 0 的检查
108
+ const executeLoopGroupStart = source.indexOf("async function executeLoopGroup");
109
+ assert(executeLoopGroupStart !== -1, "找到 executeLoopGroup 函数");
110
+
111
+ // 在 executeLoopGroup 函数体中搜索 exitCode 检查
112
+ const executeLoopGroupBody = source.slice(executeLoopGroupStart);
113
+ const hasExitCodeCheckInLoopGroup = /exitCode\s*!==\s*0/.test(executeLoopGroupBody);
114
+ assertTrue(hasExitCodeCheckInLoopGroup, "executeLoopGroup 中存在 exitCode !== 0 检查");
115
+
116
+ // 检查是否在 isTimeoutResult 之前有 exitCode 检查
117
+ const idxAgentResult = executeLoopGroupBody.indexOf("let agentResult = await runAgentWithProgress(loopAgent");
118
+ assert(idxAgentResult !== -1, "找到 agentResult 赋值");
119
+
120
+ // 检查 agentResult 赋值之后、isTimeoutResult 检查之前是否有 exitCode 检查
121
+ const afterAgentResult = executeLoopGroupBody.slice(idxAgentResult);
122
+ const idxIsTimeout = afterAgentResult.indexOf("if (isTimeoutResult(agentResult))");
123
+ assert(idxIsTimeout !== -1, "找到 isTimeoutResult 检查");
124
+
125
+ const beforeTimeout = afterAgentResult.slice(0, idxIsTimeout);
126
+ const hasExitCodeBeforeTimeout = /exitCode\s*!==\s*0/.test(beforeTimeout);
127
+ assertTrue(hasExitCodeBeforeTimeout, "exitCode 检查位于 isTimeoutResult 检查之前");
128
+
129
+ // ── Test 3: 验证 full-auto 模式下 throw Error ──
130
+ console.log("\n📋 测试 3: full-auto 模式下 exitCode 检查会 throw Error\n");
131
+
132
+ // 检查是否存在 full-auto 分支中的 throw new Error 模式
133
+ const hasFullAutoErrorInLoopGroup = /mode\s*===\s*"full-auto"[\s\S]{0,200}throw new Error/.test(executeLoopGroupBody);
134
+ assertTrue(hasFullAutoErrorInLoopGroup, "full-auto 模式有 throw new Error");
135
+
136
+ // ── Test 4: 验证非 full-auto 模式下弹出 UI 选择 ──
137
+ console.log("\n📋 测试 4: 非 full-auto 模式下弹出 UI 选择\n");
138
+
139
+ // 检查 exitCode 分支有重新执行/跳过/取消选择的相关文本
140
+ const hasRetryOption = executeLoopGroupBody.includes("重新执行");
141
+ assertTrue(hasRetryOption, "exitCode 分支有 '重新执行' 选项");
142
+
143
+ const hasSkipOption = executeLoopGroupBody.includes("跳过此步骤");
144
+ assertTrue(hasSkipOption, "exitCode 分支有 '跳过此步骤' 选项");
145
+
146
+ const hasCancelOption = executeLoopGroupBody.includes("取消工作流");
147
+ assertTrue(hasCancelOption, "exitCode 分支有 '取消工作流' 选项");
148
+
149
+ // 验证选择处理逻辑
150
+ const hasCancelBranch = /choice\.startsWith\("3"\)[\s\S]{0,50}cancelWorkflow/.test(executeLoopGroupBody);
151
+ assertTrue(hasCancelBranch, "取消选项调用 cancelWorkflow");
152
+
153
+ const hasSkipBranch = /choice\.startsWith\("2"\)[\s\S]{0,50}skipped/.test(executeLoopGroupBody);
154
+ assertTrue(hasSkipBranch, "跳过选项设置 status 为 skipped");
155
+
156
+ const hasRetryBranch = /\[RETRY\]/.test(executeLoopGroupBody);
157
+ assertTrue(hasRetryBranch, "重新执行使用 [RETRY] 标记");
158
+
159
+ // ── Test 5: 验证 executeSingleStep 的 exitCode 检查未被破坏 ──
160
+ console.log("\n📋 测试 5: executeSingleStep 的 exitCode 检查仍然存在\n");
161
+
162
+ const executeSingleStepStart = source.indexOf("async function executeSingleStep");
163
+ assert(executeSingleStepStart !== -1, "找到 executeSingleStep 函数");
164
+ const singleStepBody = source.slice(executeSingleStepStart);
165
+ const hasExitCodeInSingleStep = /exitCode\s*!==\s*0\s*&&\s*result\.stderr/.test(singleStepBody);
166
+ assertTrue(hasExitCodeInSingleStep, "executeSingleStep 中仍有 exitCode 检查");
167
+
168
+ // ── Test 6: 模拟 Bug A 的 exitCode 检查行为逻辑 ──
169
+ console.log("\n📋 测试 6: exitCode 检查行为逻辑验证\n");
170
+
171
+ function simulateBugAFix(result, mode) {
172
+ // 模拟 Bug A 修复逻辑
173
+ if (result.exitCode !== 0 && !simulateIsTimeoutResult(result)) {
174
+ if (mode === "full-auto") {
175
+ throw new Error(`Agent testAgent 异常退出 (exit ${result.exitCode}): ${result.stderr.slice(0, 200)}`);
176
+ } else {
177
+ // 模拟选择了"重新执行"
178
+ return "retry";
179
+ }
180
+ }
181
+ if (simulateIsTimeoutResult(result)) {
182
+ return "timeout";
183
+ }
184
+ return "ok";
185
+ }
186
+
187
+ // 非零退出码 + full-auto 模式 → 抛出 Error
188
+ assertThrows(() => {
189
+ simulateBugAFix({ exitCode: 1, stderr: "crash", output: "" }, "full-auto");
190
+ }, "full-auto + exitCode=1 → throw Error");
191
+
192
+ // 非零退出码 + 非 full-auto 模式 → 返回 retry
193
+ assertEq(simulateBugAFix({ exitCode: 1, stderr: "crash", output: "" }, "attended"), "retry", "attended + exitCode=1 → retry");
194
+ assertEq(simulateBugAFix({ exitCode: 1, stderr: "crash", output: "" }, "full-attended"), "retry", "full-attended + exitCode=1 → retry");
195
+
196
+ // 超时 → timeout
197
+ assertEq(simulateBugAFix({ exitCode: -1, stderr: "timed out", output: "" }, "full-auto"), "timeout", "full-auto + exitCode=-1 → timeout");
198
+ assertEq(simulateBugAFix({ exitCode: -1, stderr: "timed out", output: "" }, "attended"), "timeout", "attended + exitCode=-1 → timeout");
199
+
200
+ // 正常退出 → ok
201
+ assertEq(simulateBugAFix({ exitCode: 0, stderr: "", output: "ok" }, "full-auto"), "ok", "full-auto + exitCode=0 → ok");
202
+ assertEq(simulateBugAFix({ exitCode: 0, stderr: "", output: "ok" }, "attended"), "ok", "attended + exitCode=0 → ok");
203
+
204
+
205
+ console.log("\n═══ Bug B 测试 — setTimeout cleanupWidget 竞态条件 ═══\n");
206
+
207
+ // ── Test 7: _cleanupTimer 变量声明存在 ──
208
+ console.log("📋 测试 7: _cleanupTimer 变量声明\n");
209
+
210
+ const hasCleanupTimerVar = source.includes("_cleanupTimer: ReturnType<typeof setTimeout> | null = null");
211
+ assertTrue(hasCleanupTimerVar, "存在 _cleanupTimer 变量声明");
212
+
213
+ // ── Test 8: initWidget 中清除旧定时器 ──
214
+ console.log("\n📋 测试 8: initWidget 清除旧定时器\n");
215
+
216
+ const initWidgetStart = source.indexOf("function initWidget");
217
+ assert(initWidgetStart !== -1, "找到 initWidget 函数");
218
+ const initWidgetBody = source.slice(initWidgetStart, initWidgetStart + 500);
219
+
220
+ const hasTimerClearInInit = /if\s*\(_cleanupTimer\)[\s\S]{0,50}clearTimeout/.test(initWidgetBody);
221
+ assertTrue(hasTimerClearInInit, "initWidget 中有 clearTimeout(_cleanupTimer)");
222
+
223
+ const hasTimerNullInInit = /_cleanupTimer\s*=\s*null/.test(initWidgetBody);
224
+ assertTrue(hasTimerNullInInit, "initWidget 中有 _cleanupTimer = null");
225
+
226
+ // ── Test 9: cleanupWidget 中清除定时器 ──
227
+ console.log("\n📋 测试 9: cleanupWidget 清除定时器\n");
228
+
229
+ const cleanupWidgetStart = source.indexOf("function cleanupWidget");
230
+ assert(cleanupWidgetStart !== -1, "找到 cleanupWidget 函数");
231
+ const cleanupWidgetBody = source.slice(cleanupWidgetStart, cleanupWidgetStart + 500);
232
+
233
+ const hasTimerClearInCleanup = /if\s*\(_cleanupTimer\)[\s\S]{0,50}clearTimeout/.test(cleanupWidgetBody);
234
+ assertTrue(hasTimerClearInCleanup, "cleanupWidget 中有 clearTimeout(_cleanupTimer)");
235
+
236
+ // ── Test 10: executeWorkflowBackground 中使用 _cleanupTimer ──
237
+ console.log("\n📋 测试 10: executeWorkflowBackground 使用 _cleanupTimer\n");
238
+
239
+ const execBgStart = source.indexOf("async function executeWorkflowBackground");
240
+ assert(execBgStart !== -1, "找到 executeWorkflowBackground 函数");
241
+ const execBgBody = source.slice(execBgStart);
242
+
243
+ // 找到"Cleanup widget after delay"注释
244
+ const cleanupCommentIdx = execBgBody.indexOf("Cleanup widget after delay");
245
+ assert(cleanupCommentIdx !== -1, "找到 'Cleanup widget after delay' 注释");
246
+ const cleanupSection = execBgBody.slice(cleanupCommentIdx, cleanupCommentIdx + 200);
247
+
248
+ const hasClearBeforeTimeout = /clearTimeout/.test(cleanupSection);
249
+ assertTrue(hasClearBeforeTimeout, "定时器设置前清除旧定时器");
250
+
251
+ const hasTimerAssignment = /_cleanupTimer\s*=\s*setTimeout/.test(cleanupSection);
252
+ assertTrue(hasTimerAssignment, "使用 _cleanupTimer = setTimeout(...)");
253
+
254
+ const hasTimerNullInCallback = /_cleanupTimer\s*=\s*null/.test(cleanupSection);
255
+ assertTrue(hasTimerNullInCallback, "定时器回调中重置 _cleanupTimer = null");
256
+
257
+ // ── Test 11: cancelWorkflow 回调中使用 _cleanupTimer ──
258
+ console.log("\n📋 测试 11: cancelWorkflow 回调使用 _cleanupTimer\n");
259
+
260
+ const cancelCallbackSection = source.slice(execBgStart);
261
+ const archiveIdx = cancelCallbackSection.lastIndexOf("Archive checkpoint on cancel");
262
+ assert(archiveIdx !== -1, "找到 'Archive checkpoint on cancel' 注释");
263
+ const cancelTimeoutSection = cancelCallbackSection.slice(archiveIdx, archiveIdx + 250);
264
+
265
+ const hasClearInCancel = /clearTimeout/.test(cancelTimeoutSection);
266
+ assertTrue(hasClearInCancel, "cancel 分支清除旧定时器");
267
+
268
+ const hasTimerInCancel = /_cleanupTimer\s*=\s*setTimeout/.test(cancelTimeoutSection);
269
+ assertTrue(hasTimerInCancel, "cancel 分支使用 _cleanupTimer = setTimeout(...)");
270
+
271
+ // ── Test 12: 模拟定时器竞态场景 ──
272
+ console.log("\n📋 测试 12: 定时器竞态场景模拟\n");
273
+
274
+ // 模拟 Bug B 修复逻辑
275
+ let cleanupTimer = null;
276
+ let workflowRunning = false;
277
+ let cleanupCount = 0;
278
+
279
+ function simulateCleanupWidget() {
280
+ if (cleanupTimer) {
281
+ clearTimeout(cleanupTimer);
282
+ cleanupTimer = null;
283
+ }
284
+ workflowRunning = false;
285
+ cleanupCount++;
286
+ }
287
+
288
+ function simulateInitWidget() {
289
+ if (cleanupTimer) {
290
+ clearTimeout(cleanupTimer);
291
+ cleanupTimer = null;
292
+ }
293
+ workflowRunning = true;
294
+ }
295
+
296
+ function simulateStartWorkflow() {
297
+ // 清除旧定时器
298
+ if (cleanupTimer) {
299
+ clearTimeout(cleanupTimer);
300
+ cleanupTimer = null;
301
+ }
302
+ // 设置新的清理定时器
303
+ cleanupTimer = setTimeout(() => {
304
+ cleanupTimer = null;
305
+ simulateCleanupWidget();
306
+ }, 5000);
307
+ }
308
+
309
+ // 场景:工作流1完成 → 设置定时器 → 工作流2开始 → 旧定时器不应触发
310
+ simulateStartWorkflow(); // 工作流1完成
311
+ assertNotNull(cleanupTimer, "工作流1完成后设置了定时器");
312
+ assertEq(workflowRunning, false, "工作流1已标记为未运行");
313
+
314
+ simulateInitWidget(); // 工作流2开始
315
+ assertEq(workflowRunning, true, "工作流2已开始");
316
+ assertEq(cleanupTimer, null, "工作流2启动时清除了旧的 cleanupTimer");
317
+
318
+ // 手动触发旧定时器(不应影响新工作流)
319
+ if (cleanupTimer) {
320
+ const oldTimer = cleanupTimer;
321
+ clearTimeout(cleanupTimer);
322
+ cleanupTimer = null;
323
+ console.log(" ℹ️ 旧定时器已清除,模拟触发不会影响新工作流");
324
+ }
325
+ // 验证新工作流状态未受影响
326
+ assertEq(workflowRunning, true, "工作流2仍在运行");
327
+ assertEq(cleanupTimer, null, "定时器已被清除");
328
+
329
+ // 场景:同时调用 cleanupWidget 应清除定时器
330
+ cleanupTimer = setTimeout(() => {}, 5000);
331
+ assertNotNull(cleanupTimer, "重新设置了一个定时器");
332
+ simulateCleanupWidget();
333
+ assertEq(cleanupTimer, null, "cleanupWidget 清除了定时器");
334
+
335
+ // 场景:空定时器时调用 initWidget(无竞态条件)
336
+ cleanupTimer = null;
337
+ simulateInitWidget();
338
+ assertEq(workflowRunning, true, "空定时器时启动工作流正常");
339
+
340
+
341
+ console.log("\n═══════════════════════════════════════════════════════\n");
342
+ console.log(`📊 结果: ${pass} 通过, ${fail} 失败\n`);
343
+
344
+ if (fail > 0) {
345
+ console.error("❌ 部分测试失败");
346
+ process.exit(1);
347
+ } else {
348
+ console.log("✅ 全部通过");
349
+ }