@ghyper9023/pi-dev-workflow 0.3.3 → 0.4.1
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.
- package/.pi-dev-output/pi-grill/answers/answer-mpds3by7-20260520-1606.md +14 -0
- package/.pi-dev-output/pi-plans/20260520-153000-fix-workflow-engine-bugs.md +150 -0
- package/.pi-dev-output/pi-workflow/checkpoint-20260520-153000-fix-workflow-engine-bugs.json +108 -0
- package/README.md +171 -29
- package/agents/review-agent.md +5 -5
- package/agents/workflow/docWriter-agent.md +29 -0
- package/agents/workflow/planner-agent.md +80 -0
- package/agents/workflow/reviewer-agent.md +44 -0
- package/agents/workflow/trimmer-agent.md +34 -0
- package/agents/workflow/worker-agent.md +29 -0
- package/extensions/dev-prompts.ts +375 -75
- package/extensions/git-commands.ts +3 -13
- package/extensions/grill-me-agent.ts +138 -66
- package/extensions/sub-agents.ts +32 -11
- package/extensions/ui-helpers.ts +1029 -0
- package/extensions/workflow-engine.ts +1748 -0
- package/package.json +1 -1
- package/skills/review-html/SKILL.md +2 -2
- package/skills/to-prd/SKILL.md +1 -1
- package/tests/test-grill-json-fix.mjs +243 -0
- package/tests/test-output-directory-structure.mjs +177 -0
- package/tests/test-save-answer-file-workflow.mjs +187 -0
- package/tests/test-workflow-config.mjs +244 -0
- package/tests/test-workflow-engine-bugs.mjs +349 -0
- package/tests/test-workflow-engine.mjs +518 -0
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* test-workflow-config.mjs — 验证所有 dev-* 命令正确激活 Workflow 选项
|
|
3
|
+
*
|
|
4
|
+
* Bug: /dev-fix 的 handler 调用 runWizardWithGrill 时未传入 workflowConfig,
|
|
5
|
+
* 导致 FIX_WORKFLOW_STEPS(已定义却未使用)永远不会被触发。工作流确认对话框不出现,
|
|
6
|
+
* 用户无法进入自动化工作流。
|
|
7
|
+
*
|
|
8
|
+
* 本测试通过静态分析源码,验证:
|
|
9
|
+
* 1. 每个定义了 *_WORKFLOW_STEPS 常量的 dev-* 命令,其 handler 必须将
|
|
10
|
+
* 该常量作为 workflowConfig 传给 runWizardWithGrill / runWizard,
|
|
11
|
+
* 或在内联 handler 中直接调用 runWorkflow。
|
|
12
|
+
* 2. 没有遗漏或「定义但未使用」的 WORKFLOW_STEPS。
|
|
13
|
+
*
|
|
14
|
+
* Run: node tests/test-workflow-config.mjs
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import * as fs from "node:fs";
|
|
18
|
+
import * as path from "node:path";
|
|
19
|
+
import { fileURLToPath } from "node:url";
|
|
20
|
+
|
|
21
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
22
|
+
const SOURCE_PATH = path.resolve(__dirname, "../extensions/dev-prompts.ts");
|
|
23
|
+
|
|
24
|
+
// ── Helpers ──────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
let pass = 0;
|
|
27
|
+
let fail = 0;
|
|
28
|
+
|
|
29
|
+
function assert(condition, msg) {
|
|
30
|
+
if (condition) {
|
|
31
|
+
pass++;
|
|
32
|
+
console.log(` ✅ ${msg}`);
|
|
33
|
+
} else {
|
|
34
|
+
fail++;
|
|
35
|
+
console.error(` ❌ ${msg}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function assertEq(actual, expected, msg) {
|
|
40
|
+
if (actual === expected) {
|
|
41
|
+
pass++;
|
|
42
|
+
console.log(` ✅ ${msg}`);
|
|
43
|
+
} else {
|
|
44
|
+
fail++;
|
|
45
|
+
console.error(` ❌ ${msg} — 期望 ${JSON.stringify(expected)}, 得到 ${JSON.stringify(actual)}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ═══════════════════════════════════════════════════════════════
|
|
50
|
+
// Read source
|
|
51
|
+
// ═══════════════════════════════════════════════════════════════
|
|
52
|
+
|
|
53
|
+
let source;
|
|
54
|
+
try {
|
|
55
|
+
source = fs.readFileSync(SOURCE_PATH, "utf-8");
|
|
56
|
+
} catch (e) {
|
|
57
|
+
console.error(`Failed to read source file: ${e.message}`);
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
console.log(`📄 源文件: ${SOURCE_PATH}`);
|
|
62
|
+
console.log(`📏 文件大小: ${source.length} 字节\n`);
|
|
63
|
+
|
|
64
|
+
// ═══════════════════════════════════════════════════════════════
|
|
65
|
+
// Test 1: Every *_WORKFLOW_STEPS constant is used by its handler
|
|
66
|
+
// ═══════════════════════════════════════════════════════════════
|
|
67
|
+
|
|
68
|
+
console.log("📋 Test 1: 所有 *_WORKFLOW_STEPS 常量被其 handler 使用\n");
|
|
69
|
+
|
|
70
|
+
// Each entry: [constantName, expectedCommandName, expectedHandlerType]
|
|
71
|
+
// handlerType: "runWizardWithGrill" | "runWizard" | "inline"
|
|
72
|
+
const workflowSteps = [
|
|
73
|
+
{ const: "FEAT_WORKFLOW_STEPS", command: "dev-feat", type: "inline" }, // inline handler
|
|
74
|
+
{ const: "FIX_WORKFLOW_STEPS", command: "dev-fix", type: "runWizardWithGrill" }, // was BROKEN — missing arg
|
|
75
|
+
{ const: "DOC_WORKFLOW_STEPS", command: "dev-doc", type: "runWizardWithGrill" }, // has it
|
|
76
|
+
{ const: "REFACTOR_WORKFLOW_STEPS", command: "dev-refactor", type: "runWizardWithGrill" }, // has it
|
|
77
|
+
{ const: "TEST_WORKFLOW_STEPS", command: "dev-test", type: "runWizardWithGrill" }, // has it
|
|
78
|
+
{ const: "PERF_WORKFLOW_STEPS", command: "dev-perf", type: "runWizardWithGrill" }, // has it
|
|
79
|
+
{ const: "STYLE_WORKFLOW_STEPS", command: "dev-style", type: "runWizard" }, // has it
|
|
80
|
+
{ const: "SECURITY_WORKFLOW_STEPS", command: "dev-security", type: "runWizard" }, // has it
|
|
81
|
+
];
|
|
82
|
+
|
|
83
|
+
for (const ws of workflowSteps) {
|
|
84
|
+
// 1. Verify constant is defined in source
|
|
85
|
+
const constDefined = source.includes(`const ${ws.const}: WorkflowStepDef[] = [`);
|
|
86
|
+
assert(constDefined, `${ws.const} 应在源文件中定义`);
|
|
87
|
+
|
|
88
|
+
// 2. Usage check: the constant name must appear in the source more than just its definition.
|
|
89
|
+
// Count occurrences of the bare constant name (e.g. "FIX_WORKFLOW_STEPS").
|
|
90
|
+
// Definition has 1 occurrence (const XXX: ...), usage adds at least 1 more.
|
|
91
|
+
const bareName = ws.const;
|
|
92
|
+
const allOccurrences = source.match(new RegExp(bareName, 'g'));
|
|
93
|
+
const occurrenceCount = allOccurrences ? allOccurrences.length : 0;
|
|
94
|
+
// Minimum: 1 for definition + 1 for usage = 2
|
|
95
|
+
assert(occurrenceCount >= 2,
|
|
96
|
+
`${bareName} 应至少出现 2 次 (定义 + 使用), 实际 ${occurrenceCount} 次`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ═══════════════════════════════════════════════════════════════
|
|
100
|
+
// Test 2: runWizardWithGrill calls pass both grillOptions and workflowConfig
|
|
101
|
+
// ═════════════════──────────────────────────────────────────────
|
|
102
|
+
// All runWizardWithGrill calls that have a workflowConfig should
|
|
103
|
+
// have it as the last argument before the closing parenthesis.
|
|
104
|
+
|
|
105
|
+
console.log("\n📋 Test 2: runWizardWithGrill 调用完整性\n");
|
|
106
|
+
|
|
107
|
+
// Find all runWizardWithGrill(...) calls
|
|
108
|
+
const grillCalls = source.match(/await runWizardWithGrill\([\s\S]*?\);/g);
|
|
109
|
+
assert(grillCalls !== null && grillCalls.length >= 4,
|
|
110
|
+
`应找到至少 4 个 runWizardWithGrill 调用,实际 ${grillCalls?.length ?? 0}`);
|
|
111
|
+
|
|
112
|
+
console.log(` 共 ${grillCalls.length} 个 runWizardWithGrill 调用`);
|
|
113
|
+
|
|
114
|
+
for (let i = 0; i < grillCalls.length; i++) {
|
|
115
|
+
const call = grillCalls[i];
|
|
116
|
+
|
|
117
|
+
// Extract the command type from the call
|
|
118
|
+
const typeMatch = call.match(/await runWizardWithGrill\(\s*ctx,\s*pi,\s*"([^"]+)"/);
|
|
119
|
+
const type = typeMatch ? typeMatch[1] : `#${i}`;
|
|
120
|
+
|
|
121
|
+
// Count arguments: split by top-level commas (ignoring those inside braces/brackets)
|
|
122
|
+
let depth = 0;
|
|
123
|
+
let argCount = 0;
|
|
124
|
+
for (const ch of call) {
|
|
125
|
+
if (ch === '(' || ch === '{' || ch === '[') depth++;
|
|
126
|
+
else if (ch === ')' || ch === '}' || ch === ']') depth--;
|
|
127
|
+
else if (ch === ',' && depth === 1) argCount++;
|
|
128
|
+
}
|
|
129
|
+
// Number of arguments = commas + 1 (inside top-level parens)
|
|
130
|
+
// But the last comma before `)` doesn't count, so total args = number of top-level commas + 1
|
|
131
|
+
// Actually, let me recalculate: top-level commas separate args.
|
|
132
|
+
// For `runWizardWithGrill(a, b, c, ...)` — the paren depth at commas inside the function call is 1.
|
|
133
|
+
// The last `)` decrements depth and we shouldn't count commas after that.
|
|
134
|
+
// Let me just count the number of top-level commas before the last closing paren.
|
|
135
|
+
|
|
136
|
+
let topLevelCommas = 0;
|
|
137
|
+
depth = 0;
|
|
138
|
+
for (const ch of call) {
|
|
139
|
+
if (ch === '(') depth++;
|
|
140
|
+
else if (ch === ')') { depth--; if (depth === 0) break; }
|
|
141
|
+
else if (ch === ',' && depth === 1) topLevelCommas++;
|
|
142
|
+
}
|
|
143
|
+
const totalArgs = topLevelCommas + 1;
|
|
144
|
+
|
|
145
|
+
// runWizardWithGrill has 8 parameters (ctx, pi, type, label, questions, assembler, grillOptions?, workflowConfig?)
|
|
146
|
+
// If we see 7 args → missing workflowConfig; 8 args → has workflowConfig
|
|
147
|
+
const hasWorkflowConfig = totalArgs >= 8;
|
|
148
|
+
|
|
149
|
+
assert(hasWorkflowConfig,
|
|
150
|
+
`runWizardWithGrill("${type}") 应有 8 个参数 (当前 ${totalArgs}) — 缺少 workflowConfig 参数`);
|
|
151
|
+
|
|
152
|
+
if (hasWorkflowConfig) {
|
|
153
|
+
console.log(` ✅ "${type}": ${totalArgs} 参数, workflowConfig 已传递`);
|
|
154
|
+
} else {
|
|
155
|
+
console.error(` ❌ "${type}": ${totalArgs} 参数, 缺少 workflowConfig`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ═══════════════════════════════════════════════════════════════
|
|
160
|
+
// Test 3: Inline handlers (dev-feat) call promptWorkflowDecision
|
|
161
|
+
// ═══════════════════════════════════════════════════════════════
|
|
162
|
+
|
|
163
|
+
console.log("\n📋 Test 3: 内联 handler 调用 promptWorkflowDecision\n");
|
|
164
|
+
|
|
165
|
+
// dev-feat: inline handler that calls runWorkflow directly
|
|
166
|
+
const featHandler = source.match(/pi\.registerCommand\("dev-feat"[\s\S]*?FEAT_WORKFLOW_STEPS \}\);/);
|
|
167
|
+
assert(featHandler !== null, "应找到 /dev-feat handler");
|
|
168
|
+
|
|
169
|
+
const featCallsPromptDecision = featHandler && featHandler[0].includes("promptWorkflowDecision(ctx, pi, finalPrompt, FEAT_WORKFLOW_STEPS)");
|
|
170
|
+
assert(featCallsPromptDecision, "/dev-feat 内联 handler 应调用 promptWorkflowDecision 并传递 FEAT_WORKFLOW_STEPS");
|
|
171
|
+
|
|
172
|
+
// dev-workflow-continue: also uses FEAT_WORKFLOW_STEPS
|
|
173
|
+
const continueHandler = source.match(/pi\.registerCommand\("dev-workflow-continue"[\s\S]*?FEAT_WORKFLOW_STEPS \}\);/);
|
|
174
|
+
assert(continueHandler !== null, "应找到 /dev-workflow-continue handler");
|
|
175
|
+
if (continueHandler) {
|
|
176
|
+
assert(
|
|
177
|
+
continueHandler[0].includes("{ steps: FEAT_WORKFLOW_STEPS }"),
|
|
178
|
+
"/dev-workflow-continue handler 应引用 FEAT_WORKFLOW_STEPS",
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ═══════════════════════════════════════════════════════════════
|
|
183
|
+
// Test 4: runWizard calls that have workflow steps
|
|
184
|
+
// ═══════════════════════════════════════════════════════════════
|
|
185
|
+
|
|
186
|
+
console.log("\n📋 Test 4: runWizard 调用完整性\n");
|
|
187
|
+
|
|
188
|
+
// Find all runWizard(...) calls
|
|
189
|
+
const wizardCalls = source.match(/await runWizard\([\s\S]*?\);/g);
|
|
190
|
+
assert(wizardCalls !== null && wizardCalls.length >= 5,
|
|
191
|
+
`应找到至少 5 个 runWizard 调用,实际 ${wizardCalls?.length ?? 0}`);
|
|
192
|
+
|
|
193
|
+
console.log(` 共 ${wizardCalls.length} 个 runWizard 调用`);
|
|
194
|
+
|
|
195
|
+
// runWizard has 7 parameters (ctx, pi, type, label, questions, assembler, workflowConfig?)
|
|
196
|
+
// If the command has _WORKFLOW_STEPS defined nearby, it should pass it.
|
|
197
|
+
const nonWorkflowCommands = ["chore", "explain", "compare"]; // intentionally no workflow
|
|
198
|
+
|
|
199
|
+
for (let i = 0; i < wizardCalls.length; i++) {
|
|
200
|
+
const call = wizardCalls[i];
|
|
201
|
+
const typeMatch = call.match(/await runWizard\(\s*ctx,\s*pi,\s*"([^"]+)"/);
|
|
202
|
+
const type = typeMatch ? typeMatch[1] : `#${i}`;
|
|
203
|
+
|
|
204
|
+
// Count top-level arguments
|
|
205
|
+
let topLevelCommas = 0;
|
|
206
|
+
let depth = 0;
|
|
207
|
+
for (const ch of call) {
|
|
208
|
+
if (ch === '(') depth++;
|
|
209
|
+
else if (ch === ')') { depth--; if (depth === 0) break; }
|
|
210
|
+
else if (ch === ',' && depth === 1) topLevelCommas++;
|
|
211
|
+
}
|
|
212
|
+
const totalArgs = topLevelCommas + 1;
|
|
213
|
+
|
|
214
|
+
const expectsWorkflow = !nonWorkflowCommands.includes(type);
|
|
215
|
+
|
|
216
|
+
if (expectsWorkflow) {
|
|
217
|
+
const hasWorkflowConfig = totalArgs >= 7;
|
|
218
|
+
assert(hasWorkflowConfig,
|
|
219
|
+
`runWizard("${type}") 应有 7 个参数 (当前 ${totalArgs}) — 缺少 workflowConfig`);
|
|
220
|
+
if (hasWorkflowConfig) {
|
|
221
|
+
console.log(` ✅ "${type}": ${totalArgs} 参数, workflowConfig 已传递`);
|
|
222
|
+
}
|
|
223
|
+
} else {
|
|
224
|
+
// These commands intentionally don't pass workflowConfig
|
|
225
|
+
const noWorkflowConfig = totalArgs <= 6;
|
|
226
|
+
assert(noWorkflowConfig,
|
|
227
|
+
`runWizard("${type}") 应有 6 个参数 (当前 ${totalArgs}) — 此类命令不含 workflow`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// ═══════════════════════════════════════════════════════════════
|
|
232
|
+
// Summary
|
|
233
|
+
// ═══════════════════════════════════════════════════════════════
|
|
234
|
+
|
|
235
|
+
console.log(`\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
|
|
236
|
+
console.log(`结果: ${pass} 通过, ${fail} 失败, 共 ${pass + fail} 个测试`);
|
|
237
|
+
console.log(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
|
|
238
|
+
|
|
239
|
+
if (fail > 0) {
|
|
240
|
+
console.error("\n⚠️ 部分测试未通过");
|
|
241
|
+
process.exit(1);
|
|
242
|
+
} else {
|
|
243
|
+
console.log("\n✅ 所有测试通过 — 所有 dev-* 命令的 Workflow 配置均完整可用");
|
|
244
|
+
}
|
|
@@ -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
|
+
}
|