@ghyper9023/pi-dev-workflow 0.6.3 → 0.7.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.
- package/.github/workflows/release.yml +56 -0
- package/.pre-commit-config.yaml +75 -0
- package/.version/RELEASE-v0.7.0.md +85 -0
- package/README.md +80 -228
- package/extensions/dev-prompts.ts +351 -450
- package/extensions/git-commands.ts +62 -171
- package/extensions/grill-me-agent.ts +98 -137
- package/extensions/session-utils.ts +169 -0
- package/extensions/ui-helpers.ts +2 -780
- package/package.json +2 -2
- package/prompts/APPEND_SYSTEM.md +63 -59
- package/skills/review-html/SKILL.md +1 -1
- package/tests/test-no-subagents.mjs +146 -0
- package/.doc/AGENT-FRONTMATTER-REFERENCE.md +0 -198
- package/agents/git-agent.md +0 -44
- package/agents/grill/dev-doc-grill-agent.md +0 -42
- package/agents/grill/dev-fix-grill-agent.md +0 -44
- package/agents/grill/dev-grill-agent.md +0 -40
- package/agents/grill/dev-perf-grill-agent.md +0 -45
- package/agents/grill/dev-prd-agent.md +0 -60
- package/agents/grill/dev-refactor-grill-agent.md +0 -46
- package/agents/grill/dev-test-grill-agent.md +0 -45
- package/agents/review-agent.md +0 -53
- package/agents/workflow/docWriter-agent.md +0 -53
- package/agents/workflow/planner-agent.md +0 -131
- package/agents/workflow/reviewer-agent.md +0 -128
- package/agents/workflow/trimmer-agent.md +0 -78
- package/agents/workflow/worker-agent.md +0 -70
- package/extensions/sub-agents.ts +0 -954
- package/extensions/workflow-engine.ts +0 -2005
- package/tests/test-grill-json-fix.mjs +0 -243
- package/tests/test-loopcount-timeout-fix.mjs +0 -336
- package/tests/test-output-directory-structure.mjs +0 -177
- package/tests/test-save-answer-file-workflow.mjs +0 -187
- package/tests/test-workflow-config.mjs +0 -244
- package/tests/test-workflow-engine-bugs.mjs +0 -908
- package/tests/test-workflow-engine.mjs +0 -518
|
@@ -1,908 +0,0 @@
|
|
|
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═══ Bug C 测试 — buildTaskForStep worker 注入 prompt(review 反馈循环) ═══\n");
|
|
342
|
-
|
|
343
|
-
// ── Test C1: worker 有 planContent 时 prompt 不被忽略 ──
|
|
344
|
-
console.log("📋 测试 C1: worker 有 planContent 时 prompt 不被忽略\n");
|
|
345
|
-
|
|
346
|
-
// 模拟 buildTaskForStep 行为
|
|
347
|
-
function simulateBuildTaskForStep(agentName, prompt, planFileRelPath, cwd, planContentExists) {
|
|
348
|
-
if (agentName === "worker") {
|
|
349
|
-
const planContent = planContentExists ? "# 实施计划\n1. 修改 login.ts\n2. 添加 auth 中间件" : undefined;
|
|
350
|
-
if (planContent) {
|
|
351
|
-
const result = [
|
|
352
|
-
"请根据以下实施计划逐步实现代码改动。",
|
|
353
|
-
"",
|
|
354
|
-
"## 实施计划",
|
|
355
|
-
planContent,
|
|
356
|
-
"",
|
|
357
|
-
"## 原始需求与修改反馈",
|
|
358
|
-
prompt,
|
|
359
|
-
"",
|
|
360
|
-
"请严格按照计划中的步骤实施,不要做计划外的修改。",
|
|
361
|
-
].join("\n");
|
|
362
|
-
// 验证 prompt 包含在结果中
|
|
363
|
-
return result.includes("\n" + prompt) || result.includes(prompt + "\n");
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
return false;
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
// 场景 A: prompt 包含原始需求 + 审查反馈
|
|
370
|
-
const promptWithFeedback = [
|
|
371
|
-
"[fix] 修复 login.ts 中的 401 错误",
|
|
372
|
-
"",
|
|
373
|
-
"## 上次审查发现的问题",
|
|
374
|
-
'审查摘要: {"maxSeverity":"critical","critical":2,"medium":1,"low":0}',
|
|
375
|
-
"请修复 2 个严重问题后重新运行。",
|
|
376
|
-
].join("\n");
|
|
377
|
-
|
|
378
|
-
const hasPromptWithPlan = simulateBuildTaskForStep("worker", promptWithFeedback, "plan.md", "/cwd", true);
|
|
379
|
-
assertTrue(hasPromptWithPlan, "worker 有 planContent 时 prompt(含审查反馈)被注入到任务中");
|
|
380
|
-
|
|
381
|
-
// 场景 B: prompt 是原始需求(第一轮循环)
|
|
382
|
-
const originalPrompt = "[fix] 修复 login.ts 中的 401 错误";
|
|
383
|
-
const hasOriginalPrompt = simulateBuildTaskForStep("worker", originalPrompt, "plan.md", "/cwd", true);
|
|
384
|
-
assertTrue(hasOriginalPrompt, "worker 有 planContent 时原始 prompt 也被注入");
|
|
385
|
-
|
|
386
|
-
// ── Test C2: 源代码静态分析 ──
|
|
387
|
-
console.log("\n📋 测试 C2: 源代码中 buildTaskForStep worker 分支包含 prompt\n");
|
|
388
|
-
|
|
389
|
-
const workerBranchRegex = /if \(agentName === \"worker\"\)[\s\S]{0,1000}\]\.join\(\"\\n\"\);/;
|
|
390
|
-
const workerMatch = source.match(workerBranchRegex);
|
|
391
|
-
assertNotNull(workerMatch, "找到 worker 分支");
|
|
392
|
-
|
|
393
|
-
const hasOriginalRequirementFeedback = workerMatch?.[0]?.includes("## 原始需求与修改反馈") ?? false;
|
|
394
|
-
assertTrue(hasOriginalRequirementFeedback, "worker prompt 模板包含 '## 原始需求与修改反馈' 节");
|
|
395
|
-
|
|
396
|
-
// ── Test C3: 源代码中 no-plan 分支保持不变 ──
|
|
397
|
-
console.log("\n📋 测试 C3: worker 无 plan 分支保持不变\n");
|
|
398
|
-
|
|
399
|
-
const noPlanBranchMatch = source.match(/请根据以下功能需求实施代码改动[\s\S]{0,200}请先分析代码库,制定简要计划,再逐步实施/);
|
|
400
|
-
assertNotNull(noPlanBranchMatch, "无 plan 分支仍然存在");
|
|
401
|
-
const noPlanHasFeedback = noPlanBranchMatch?.[0]?.includes("## 原始需求与修改反馈") ?? false;
|
|
402
|
-
assertFalse(noPlanHasFeedback, "无 plan 分支不含 '## 原始需求与修改反馈'");
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
console.log("\n═══ Bug D 测试 — hasContentChanged 使用 execSync 而非 require ═══\n");
|
|
406
|
-
|
|
407
|
-
// ── Test D1: 源代码不包含 require('child_process') ──
|
|
408
|
-
console.log("📋 测试 D1: hasContentChanged 不使用 require('child_process')\n");
|
|
409
|
-
|
|
410
|
-
const hasRequireChildProc = source.includes("require('child_process')") || source.includes('require("child_process")');
|
|
411
|
-
assertFalse(hasRequireChildProc, "源代码中不包含 require('child_process')");
|
|
412
|
-
|
|
413
|
-
// ── Test D2: hasContentChanged 使用 execSync ──
|
|
414
|
-
console.log("\n📋 测试 D2: hasContentChanged 使用 execSync\n");
|
|
415
|
-
|
|
416
|
-
const funcStart = source.indexOf("function hasContentChanged");
|
|
417
|
-
assert(funcStart !== -1, "找到 hasContentChanged 函数");
|
|
418
|
-
const funcBody = source.slice(funcStart, funcStart + 400);
|
|
419
|
-
const hasExecSync = funcBody.includes("execSync(");
|
|
420
|
-
assertTrue(hasExecSync, "hasContentChanged 使用 execSync");
|
|
421
|
-
const noOldSpawnSync = !funcBody.includes("spawnSync");
|
|
422
|
-
assertTrue(noOldSpawnSync, "hasContentChanged 不使用 spawnSync");
|
|
423
|
-
|
|
424
|
-
// ── Test D3: 模拟 hasContentChanged 行为逻辑 ──
|
|
425
|
-
console.log("\n📋 测试 D3: hasContentChanged 逻辑验证\n");
|
|
426
|
-
|
|
427
|
-
function simulateHasContentChanged(currentHash, baselineHash) {
|
|
428
|
-
try {
|
|
429
|
-
// 模拟 execSync 返回 hash
|
|
430
|
-
const data = currentHash;
|
|
431
|
-
return data.trim() !== baselineHash;
|
|
432
|
-
} catch {
|
|
433
|
-
return true;
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
assertTrue(simulateHasContentChanged("abc123", "def456"), "不同 hash → changed");
|
|
438
|
-
assertFalse(simulateHasContentChanged("abc123", "abc123"), "相同 hash → unchanged");
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
console.log("\n═══ Bug E 测试 — trimmer prompt 包含 plan 上下文 ═══\n");
|
|
442
|
-
|
|
443
|
-
// ── Test E1: 源代码静态分析 ──
|
|
444
|
-
console.log("📋 测试 E1: buildTaskForStep trimmer 分支包含 plan 上下文\n");
|
|
445
|
-
|
|
446
|
-
const trimmerStart = source.indexOf('if (agentName === "trimmer")');
|
|
447
|
-
assert(trimmerStart !== -1, "找到 trimmer 分支");
|
|
448
|
-
const trimmerSection = source.slice(trimmerStart, trimmerStart + 600);
|
|
449
|
-
|
|
450
|
-
const trimmerHasWarning = trimmerSection.includes("注意:以下实施计划");
|
|
451
|
-
assertTrue(trimmerHasWarning, "trimmer prompt 包含 plan 保护提示");
|
|
452
|
-
|
|
453
|
-
const trimmerHasPlanContent = trimmerSection.includes("## 实施计划(改动范围)");
|
|
454
|
-
assertTrue(trimmerHasPlanContent, "trimmer prompt 包含 '## 实施计划(改动范围)’ 节");
|
|
455
|
-
|
|
456
|
-
const trimmerHasSpread = trimmerSection.includes("...(planContent ?");
|
|
457
|
-
assertTrue(trimmerHasSpread, "trimmer prompt 条件性包含 planContent");
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
console.log("\n═══ Bug F 测试 — 注释与代码一致性(5s vs 3s) ═══\n");
|
|
461
|
-
|
|
462
|
-
// ── Test F1: Esc 双击超时注释匹配代码 ──
|
|
463
|
-
console.log("📋 测试 F1: Esc 双击注释与代码一致\n");
|
|
464
|
-
|
|
465
|
-
const escCommentMatch = source.match(/Second Esc press within (\d+)s/);
|
|
466
|
-
if (escCommentMatch) {
|
|
467
|
-
const commentVal = parseInt(escCommentMatch[1], 10);
|
|
468
|
-
assertEq(commentVal, 3, `注释说 ${commentVal}s,代码中阈值应为 ${commentVal}s (3000ms)`);
|
|
469
|
-
|
|
470
|
-
// 验证注释值匹配 actual timeout in ms
|
|
471
|
-
const thresholdMs = 3000;
|
|
472
|
-
const thresholdSec = thresholdMs / 1000;
|
|
473
|
-
assertEq(commentVal, thresholdSec, `注释值 ${commentVal}s 匹配代码阈值 ${thresholdSec}s`);
|
|
474
|
-
} else {
|
|
475
|
-
// Try the previous comment text
|
|
476
|
-
const prevCommentMatch = source.match(/Second Esc press within [\w\s]+ → confirm cancel/);
|
|
477
|
-
if (prevCommentMatch) {
|
|
478
|
-
const commentText = prevCommentMatch[0];
|
|
479
|
-
// Should contain "3s" now
|
|
480
|
-
assertTrue(commentText.includes("3s"), `注释现在说 "3s",得到: "${commentText}"`);
|
|
481
|
-
} else {
|
|
482
|
-
fail++;
|
|
483
|
-
console.error(" ❌ 找不到 Esc 注释");
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
console.log("\n═══ Bug G 测试 — 链上下文传递 (executeSingleStep) ═══\n");
|
|
489
|
-
|
|
490
|
-
// ── Test G1: executeSingleStep 中包含链上下文捕获逻辑 ──
|
|
491
|
-
console.log("📋 测试 G1: executeSingleStep 中有 chain context 捕获\n");
|
|
492
|
-
|
|
493
|
-
const singleStepFuncStart = source.indexOf("async function executeSingleStep");
|
|
494
|
-
assert(singleStepFuncStart !== -1, "找到 executeSingleStep 函数");
|
|
495
|
-
const singleStepEndSearch = source.indexOf("async function executeLoopGroup", singleStepFuncStart);
|
|
496
|
-
const fullSingleStep = source.slice(singleStepFuncStart, singleStepEndSearch);
|
|
497
|
-
|
|
498
|
-
// Verify 工作总结 entries still exist (preserved - from extractFinalOutput)
|
|
499
|
-
const hasWorkSummaryKey = fullSingleStep.includes('工作总结');
|
|
500
|
-
assertTrue(hasWorkSummaryKey, "executeSingleStep 中保留 '工作总结' 条目");
|
|
501
|
-
|
|
502
|
-
const hasWorkSummaryUpdate = fullSingleStep.includes('updateChainContext');
|
|
503
|
-
assertTrue(hasWorkSummaryUpdate, "executeSingleStep 中仍有 updateChainContext 调用(工作总结)");
|
|
504
|
-
|
|
505
|
-
// Verify the removed summary keys are no longer present in updateChainContext calls
|
|
506
|
-
// (comments may still reference them for historical context — that's fine)
|
|
507
|
-
const chainCtxCalls = [...fullSingleStep.matchAll(/updateChainContext\(`([^`]+)`/g)].map(m => m[1]);
|
|
508
|
-
const plannerInCode = chainCtxCalls.some(k => k.includes('计划制定摘要'));
|
|
509
|
-
assertFalse(plannerInCode, "updateChainContext 中已不使用 '计划制定摘要' key");
|
|
510
|
-
const docWriterInCode = chainCtxCalls.some(k => k.includes('文档更新摘要'));
|
|
511
|
-
assertFalse(docWriterInCode, "updateChainContext 中已不使用 '文档更新摘要' key");
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
console.log("\n═══ Bug H 测试 — Agent 前置元数据解析 ═══\n");
|
|
515
|
-
|
|
516
|
-
// ── Test H1: 所有 workflow agent 的 session 已启用(可追溯) ──
|
|
517
|
-
console.log("📋 测试 H1: 所有 workflow agent 的 session 已启用(session: true)\n");
|
|
518
|
-
|
|
519
|
-
const agentFiles = [
|
|
520
|
-
"agents/workflow/planner-agent.md",
|
|
521
|
-
"agents/workflow/worker-agent.md",
|
|
522
|
-
"agents/workflow/reviewer-agent.md",
|
|
523
|
-
"agents/workflow/trimmer-agent.md",
|
|
524
|
-
"agents/workflow/docWriter-agent.md",
|
|
525
|
-
"agents/review-agent.md",
|
|
526
|
-
];
|
|
527
|
-
for (const af of agentFiles) {
|
|
528
|
-
const agentPath = path.resolve(__dirname, "..", af);
|
|
529
|
-
if (fs.existsSync(agentPath)) {
|
|
530
|
-
const content = fs.readFileSync(agentPath, "utf-8");
|
|
531
|
-
const hasSessionTrue = content.includes("session: true");
|
|
532
|
-
assertTrue(hasSessionTrue, `${af} 包含 session: true`);
|
|
533
|
-
const hasSessionFalse = content.includes("session: false");
|
|
534
|
-
assertFalse(hasSessionFalse, `${af} 不包含 session: false`);
|
|
535
|
-
} else {
|
|
536
|
-
console.log(` ℹ️ 跳过不存在的文件: ${af}`);
|
|
537
|
-
}
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
// ── Test H2: 所有 workflow agent 有 MCP/SKILL 可用声明(工具白名单已移除 → MCP 实际可用) ──
|
|
541
|
-
console.log("\n📋 测试 H2: workflow agent 包含 MCP/SKILL 可用声明(白名单已移除,MCP 实际可用)\n");
|
|
542
|
-
|
|
543
|
-
const workflowAgentFiles = [
|
|
544
|
-
"agents/workflow/planner-agent.md",
|
|
545
|
-
"agents/workflow/worker-agent.md",
|
|
546
|
-
"agents/workflow/reviewer-agent.md",
|
|
547
|
-
"agents/workflow/trimmer-agent.md",
|
|
548
|
-
"agents/workflow/docWriter-agent.md",
|
|
549
|
-
];
|
|
550
|
-
for (const af of workflowAgentFiles) {
|
|
551
|
-
const agentPath = path.resolve(__dirname, "..", af);
|
|
552
|
-
if (fs.existsSync(agentPath)) {
|
|
553
|
-
const content = fs.readFileSync(agentPath, "utf-8");
|
|
554
|
-
const hasExtraToolsSection = content.includes("## 额外可用工具");
|
|
555
|
-
assertTrue(hasExtraToolsSection, `${af} 包含 '## 额外可用工具' 节`);
|
|
556
|
-
const hasMcpClaim = content.includes("MCP");
|
|
557
|
-
assertTrue(hasMcpClaim, `${af} 包含 MCP 声明`);
|
|
558
|
-
const hasSkillClaim = content.includes("SKILL");
|
|
559
|
-
assertTrue(hasSkillClaim, `${af} 包含 SKILL 声明`);
|
|
560
|
-
} else {
|
|
561
|
-
console.log(` ℹ️ 跳过不存在的文件: ${af}`);
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
// ── Test H3: workflow agent 没有 tools 白名单(以允许 MCP 工具) ──
|
|
566
|
-
console.log("\n📋 测试 H3: workflow agent 没有 tools 白名单限制\n");
|
|
567
|
-
|
|
568
|
-
for (const af of workflowAgentFiles) {
|
|
569
|
-
const agentPath = path.resolve(__dirname, "..", af);
|
|
570
|
-
if (fs.existsSync(agentPath)) {
|
|
571
|
-
const content = fs.readFileSync(agentPath, "utf-8");
|
|
572
|
-
// Should NOT have a tools: line in frontmatter
|
|
573
|
-
const hasToolsLine = /^tools:/.test(content.split("---")?.[1] ?? "");
|
|
574
|
-
assertFalse(hasToolsLine, `${af} 无 tools: 行(白名单已移除)`);
|
|
575
|
-
} else {
|
|
576
|
-
console.log(` ℹ️ 跳过不存在的文件: ${af}`);
|
|
577
|
-
}
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
// ── Test H4: sub-agents.ts 中 session 使用完整路径 + .jsonl ──
|
|
581
|
-
console.log("\n📋 测试 H4: sub-agents.ts session 路径构造\n");
|
|
582
|
-
|
|
583
|
-
const subAgentSource = fs.readFileSync(path.resolve(__dirname, "../extensions/sub-agents.ts"), "utf-8");
|
|
584
|
-
const hasMkdirSync = subAgentSource.includes("fs.mkdirSync(sessionDir");
|
|
585
|
-
assertTrue(hasMkdirSync, "spawnSubagent 创建 session 目录");
|
|
586
|
-
const hasJsonlPath = subAgentSource.includes(".jsonl");
|
|
587
|
-
assertTrue(hasJsonlPath, "session 文件路径包含 .jsonl 扩展名");
|
|
588
|
-
const hasSessionPath = subAgentSource.includes("path.join(sessionDir");
|
|
589
|
-
assertTrue(hasSessionPath, "使用 path.join 构建完整 session 路径");
|
|
590
|
-
const noSessionDirArg = subAgentSource.includes("--session-dir");
|
|
591
|
-
assertFalse(noSessionDirArg, "不再使用 --session-dir(改用完整路径 --session)");
|
|
592
|
-
|
|
593
|
-
// ── Test H5: Agent frontmatter 字段解析正确 ──
|
|
594
|
-
console.log("\n📋 测试 H5: Agent frontmatter 字段解析\n");
|
|
595
|
-
|
|
596
|
-
// subAgentSource 已在 H4 中声明,此处直接复用
|
|
597
|
-
|
|
598
|
-
const hasThinkingField = subAgentSource.includes('fields.thinking');
|
|
599
|
-
assertTrue(hasThinkingField, "loadAgent 解析 thinking 字段");
|
|
600
|
-
|
|
601
|
-
const hasSessionField = subAgentSource.includes('fields.session');
|
|
602
|
-
assertTrue(hasSessionField, "loadAgent 解析 session 字段");
|
|
603
|
-
|
|
604
|
-
const hasSessionDirField = subAgentSource.includes('fields["session-dir"]');
|
|
605
|
-
assertTrue(hasSessionDirField, "loadAgent 解析 session-dir 字段");
|
|
606
|
-
|
|
607
|
-
const hasNoContextField = subAgentSource.includes('fields["no-context"]');
|
|
608
|
-
assertTrue(hasNoContextField, "loadAgent 解析 no-context 字段");
|
|
609
|
-
|
|
610
|
-
const hasNoExtensionsField = subAgentSource.includes('fields["no-extensions"]');
|
|
611
|
-
assertTrue(hasNoExtensionsField, "loadAgent 解析 no-extensions 字段");
|
|
612
|
-
|
|
613
|
-
const hasExtraArgsField = subAgentSource.includes('fields["extra-args"]');
|
|
614
|
-
assertTrue(hasExtraArgsField, "loadAgent 解析 extra-args 字段");
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
console.log("\n═══ Bug I 测试 — 工作流 UUID 溯源机制 ═══\n");
|
|
618
|
-
|
|
619
|
-
// ── Test I1: workflowId 注入到 buildTaskForStep ──
|
|
620
|
-
console.log("📋 测试 I1: buildTaskForStep 接收 workflowId 参数\n");
|
|
621
|
-
|
|
622
|
-
const bldFuncStart = source.indexOf("function buildTaskForStep");
|
|
623
|
-
assert(bldFuncStart !== -1, "找到 buildTaskForStep 函数");
|
|
624
|
-
const bldFuncParams = source.slice(bldFuncStart, bldFuncStart + 300);
|
|
625
|
-
|
|
626
|
-
const hasWorkflowIdParam = bldFuncParams.includes("workflowId");
|
|
627
|
-
assertTrue(hasWorkflowIdParam, "buildTaskForStep 接收 workflowId 参数");
|
|
628
|
-
|
|
629
|
-
const hasChainContextParam = bldFuncParams.includes("chainContext");
|
|
630
|
-
assertTrue(hasChainContextParam, "buildTaskForStep 接收 chainContext 参数");
|
|
631
|
-
|
|
632
|
-
// ── Test I2: CheckpointData 包含 workflowId ──
|
|
633
|
-
console.log("\n📋 测试 I2: CheckpointData 包含 workflowId\n");
|
|
634
|
-
|
|
635
|
-
const checkpointDataMatch = source.match(/interface CheckpointData [\s\S]{0,500}workflowId/);
|
|
636
|
-
assertNotNull(checkpointDataMatch, "CheckpointData 接口包含 workflowId");
|
|
637
|
-
|
|
638
|
-
// ── Test I3: buildWorkflowInfoBlock 函数存在 ──
|
|
639
|
-
console.log("\n📋 测试 I3: buildWorkflowInfoBlock 函数存在\n");
|
|
640
|
-
|
|
641
|
-
const hasBuildWorkflowInfoBlock = source.includes("function buildWorkflowInfoBlock");
|
|
642
|
-
assertTrue(hasBuildWorkflowInfoBlock, "buildWorkflowInfoBlock 函数存在");
|
|
643
|
-
|
|
644
|
-
// ── Test I4: buildReviewTask 也接收 workflowId ──
|
|
645
|
-
console.log("\n📋 测试 I4: buildReviewTask 接收 workflowId\n");
|
|
646
|
-
|
|
647
|
-
const reviewTaskStart = source.indexOf("function buildReviewTask");
|
|
648
|
-
assert(reviewTaskStart !== -1, "找到 buildReviewTask 函数");
|
|
649
|
-
const reviewTaskParams = source.slice(reviewTaskStart, reviewTaskStart + 200);
|
|
650
|
-
const reviewHasWorkflowId = reviewTaskParams.includes("workflowId");
|
|
651
|
-
assertTrue(reviewHasWorkflowId, "buildReviewTask 接收 workflowId 参数");
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
console.log("\n");
|
|
655
|
-
|
|
656
|
-
console.log("═══ Bug J 测试 — _workflowFileChanges 仅来自 git diff ═══\n");
|
|
657
|
-
|
|
658
|
-
// ── Test J1: _workflowFileChanges.push 出现在 updateToolsFromGit 和 git diff 轮询 ──
|
|
659
|
-
console.log("📋 测试 J1: _workflowFileChanges.push 出现在 updateToolsFromGit 和 git diff 轮询\n");
|
|
660
|
-
|
|
661
|
-
// Count all _workflowFileChanges.push occurrences
|
|
662
|
-
// There should be 2: one in git diff polling (runAgentWithProgress) and one in updateToolsFromGit
|
|
663
|
-
const pushMatches = [...source.matchAll(/_workflowFileChanges\.push\(/g)];
|
|
664
|
-
assertEq(pushMatches.length, 2, "_workflowFileChanges.push 出现 2 次(轮询 + updateToolsFromGit)");
|
|
665
|
-
|
|
666
|
-
// Verify the push is inside updateToolsFromGit
|
|
667
|
-
const updateToolsStart = source.indexOf("function updateToolsFromGit");
|
|
668
|
-
const nextFuncStart = source.indexOf("function saveCheckpoint", updateToolsStart);
|
|
669
|
-
const updateToolsBody = source.slice(updateToolsStart, nextFuncStart);
|
|
670
|
-
assertTrue(updateToolsBody.includes("_workflowFileChanges.push"), "updateToolsFromGit 函数体内包含 _workflowFileChanges.push");
|
|
671
|
-
|
|
672
|
-
// Verify the push is also inside the git diff polling (setInterval) in runAgentWithProgress
|
|
673
|
-
const runAgentStart = source.indexOf("async function runAgentWithProgress");
|
|
674
|
-
const runAgentEnd = source.indexOf("async function executeSingleStep", runAgentStart);
|
|
675
|
-
const runAgentBody = source.slice(runAgentStart, runAgentEnd);
|
|
676
|
-
assertTrue(runAgentBody.includes("_workflowFileChanges.push"), "git diff 轮询(setInterval)中包含 _workflowFileChanges.push");
|
|
677
|
-
|
|
678
|
-
// ── Test J2: addWidgetSubStepTool 不再污染 _workflowFileChanges ──
|
|
679
|
-
console.log("\n📋 测试 J2: addWidgetSubStepTool 不再污染 _workflowFileChanges\n");
|
|
680
|
-
|
|
681
|
-
const addWidgetFunc = source.slice(
|
|
682
|
-
source.indexOf("function addWidgetSubStepTool"),
|
|
683
|
-
source.indexOf("function addWidgetSubStepOutput")
|
|
684
|
-
);
|
|
685
|
-
const hasNoFileChangesRef = !addWidgetFunc.includes("_workflowFileChanges");
|
|
686
|
-
assertTrue(hasNoFileChangesRef, "addWidgetSubStepTool 函数体中不再引用 _workflowFileChanges");
|
|
687
|
-
|
|
688
|
-
// ── Test J3: executeSingleStep 中不再有基于 _workflowFileChanges 的统计 ──
|
|
689
|
-
console.log("\n📋 测试 J3: executeSingleStep 中不再有 '执行摘要' 类 chain context\n");
|
|
690
|
-
|
|
691
|
-
const hasNoExecutorSummary = !fullSingleStep.includes('执行摘要');
|
|
692
|
-
assertTrue(hasNoExecutorSummary, "executeSingleStep 中不再有 '执行摘要' chain context");
|
|
693
|
-
|
|
694
|
-
// ── Test J4: executeLoopGroup 中不再有 "代码实施/精简摘要" chain context ──
|
|
695
|
-
console.log("\n📋 测试 J4: executeLoopGroup 中不再有 '代码实施/精简摘要' chain context\n");
|
|
696
|
-
|
|
697
|
-
const loopGroupStart = source.indexOf("async function executeLoopGroup");
|
|
698
|
-
assert(loopGroupStart !== -1, "找到 executeLoopGroup 函数");
|
|
699
|
-
const loopGroupBody = source.slice(loopGroupStart);
|
|
700
|
-
const hasNoCodeImplSummary = !loopGroupBody.includes('"代码实施摘要"') && !loopGroupBody.includes('"代码精简摘要"');
|
|
701
|
-
assertTrue(hasNoCodeImplSummary, "executeLoopGroup 中不再有 '代码实施摘要' 或 '代码精简摘要' chain context");
|
|
702
|
-
|
|
703
|
-
// Verify the removed review feedback keys are no longer present in updateChainContext calls
|
|
704
|
-
// (comments may still reference them for historical context — that's fine)
|
|
705
|
-
const chainCtxCallsLoop = [...loopGroupBody.matchAll(/updateChainContext\(`([^`]+)`/g)].map(m => m[1]);
|
|
706
|
-
const reviewInCode = chainCtxCallsLoop.some(k => k.includes('代码审查反馈') || k.includes('精简审查反馈'));
|
|
707
|
-
assertFalse(reviewInCode, "updateChainContext 中已不使用 '代码审查反馈' 或 '精简审查反馈' key");
|
|
708
|
-
|
|
709
|
-
// ── Test J5: 工作总结条目在所有 agent 函数中仍保留 ──
|
|
710
|
-
console.log("\n📋 测试 J5: '工作总结' 条目在所有 agent 函数中仍保留\n");
|
|
711
|
-
|
|
712
|
-
const hasWorkSummaryInSingleStep = fullSingleStep.includes('工作总结');
|
|
713
|
-
assertTrue(hasWorkSummaryInSingleStep, "executeSingleStep 保留 '工作总结' 条目");
|
|
714
|
-
|
|
715
|
-
const hasWorkSummaryInLoop = loopGroupBody.includes('工作总结');
|
|
716
|
-
assertTrue(hasWorkSummaryInLoop, "executeLoopGroup 保留 '工作总结' 条目");
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
console.log("\n=== 增强功能测试汇总 ===\n");
|
|
720
|
-
console.log("📋 附加测试覆盖:");
|
|
721
|
-
console.log(" - G: 链上下文传递 (executeSingleStep) — 已更新验证工作总结");
|
|
722
|
-
console.log(" - H: Agent 前置元数据解析 + MCP/SKILL 移除");
|
|
723
|
-
console.log(" - I: 工作流 UUID 溯源机制");
|
|
724
|
-
console.log(" - J: _workflowFileChanges 仅来自 git diff");
|
|
725
|
-
console.log("\n═══ Bug K 测试 — git diff 精准识别 + .pi-dev-output 过滤 ═══\n");
|
|
726
|
-
|
|
727
|
-
// ── Test K1: filePatterns 已移除 — 不再用 regex 猜文件路径 ──
|
|
728
|
-
console.log("📋 测试 K1: filePatterns 已移除\n");
|
|
729
|
-
|
|
730
|
-
const hasFilePatterns = source.includes("const filePatterns = [");
|
|
731
|
-
assertFalse(hasFilePatterns, "filePatterns 数组已移除(不再用 regex 嗅探文件路径)");
|
|
732
|
-
|
|
733
|
-
// ── Test K2: seenTools 文本解析 fallback 已移除 ──
|
|
734
|
-
console.log("\n📋 测试 K2: seenTools JSON fallback 已移除\n");
|
|
735
|
-
|
|
736
|
-
const hasJsonFallback = source.includes("if (seenTools.size === 0)");
|
|
737
|
-
assertFalse(hasJsonFallback, "seenTools JSON tool_use fallback 已移除");
|
|
738
|
-
|
|
739
|
-
// ── Test K3: outputPathPatterns 统一 inline 模式已移除 ──
|
|
740
|
-
console.log("\n📋 测试 K3: outputPathPatterns 已简化\n");
|
|
741
|
-
|
|
742
|
-
const hasOutputPathPatterns = source.includes("const outputPathPatterns = [");
|
|
743
|
-
assertFalse(hasOutputPathPatterns, "outputPathPatterns 数组已移除(替换为 inline workflowId 过滤)");
|
|
744
|
-
|
|
745
|
-
// ── Test K4: updateToolsFromGit 有 isWorkflowArtifactPath 过滤 ──
|
|
746
|
-
console.log("\n📋 测试 K4: updateToolsFromGit 使用 isWorkflowArtifactPath 过滤\n");
|
|
747
|
-
|
|
748
|
-
const hasIsWorkflowArtifactPath = source.includes('isWorkflowArtifactPath(change.path)');
|
|
749
|
-
assertTrue(hasIsWorkflowArtifactPath, "updateToolsFromGit 使用 isWorkflowArtifactPath 跳过 .pi-dev-output/ 路径");
|
|
750
|
-
|
|
751
|
-
const hasIsWorkflowArtifactFunc = source.includes('function isWorkflowArtifactPath');
|
|
752
|
-
assertTrue(hasIsWorkflowArtifactFunc, "isWorkflowArtifactPath 辅助函数存在");
|
|
753
|
-
|
|
754
|
-
// ── Test K5: output 路径匹配使用 escapedId + workflowId ──
|
|
755
|
-
console.log("\n📋 测试 K5: output 路径使用 workflowId 过滤\n");
|
|
756
|
-
|
|
757
|
-
const hasEscapedId = source.includes("const escapedId = _workflowId.replace(");
|
|
758
|
-
assertTrue(hasEscapedId, "output 路径匹配使用 escapedId");
|
|
759
|
-
|
|
760
|
-
// ── Test K6: progress handler output match 使用 _workflowId ──
|
|
761
|
-
console.log("\n📋 测试 K6: progress handler 使用 workflowId\n");
|
|
762
|
-
|
|
763
|
-
const hasIncludesWorkflowId = source.includes("pathCandidate.includes(_workflowId)");
|
|
764
|
-
assertTrue(hasIncludesWorkflowId, "progress handler 使用 pathCandidate.includes(_workflowId)");
|
|
765
|
-
|
|
766
|
-
// ── Test K7: addWidgetSubStepTool 只来自 git diff(regex 嗅探已移除)──
|
|
767
|
-
console.log("\n📋 测试 K7: addWidgetSubStepTool 仅来自 git diff\n");
|
|
768
|
-
|
|
769
|
-
// Verify the toolMatch regex sniffing has been removed (source 1)
|
|
770
|
-
const hasToolMatchRegex = source.includes('const toolMatch = progress.match(/(edit|read|write|new|bash|grep|find|ls|delete|remove)');
|
|
771
|
-
assertFalse(hasToolMatchRegex, "progress handler 中已移除 toolMatch 正则 (source 1)");
|
|
772
|
-
|
|
773
|
-
// Verify the updateToolsFromGit call site still exists
|
|
774
|
-
assertTrue(source.includes("addWidgetSubStepTool(stepIndex, agentName, `${change.status} ${change.path}`)"), "updateToolsFromGit 调用 addWidgetSubStepTool");
|
|
775
|
-
|
|
776
|
-
// Verify git diff polling timer exists (replacement for regex sniffing)
|
|
777
|
-
const hasGitPollTimer = source.includes('_gitPollTimer = setInterval');
|
|
778
|
-
assertTrue(hasGitPollTimer, "git diff 轮询定时器已添加");
|
|
779
|
-
|
|
780
|
-
// ── Test K8: addWidgetSubStepOutput 仍存在 (output 路径展示有用) ──
|
|
781
|
-
console.log("\n📋 测试 K8: addWidgetSubStepOutput 仍保留\n");
|
|
782
|
-
|
|
783
|
-
const hasAddWidgetOutput = source.includes("function addWidgetSubStepOutput");
|
|
784
|
-
assertTrue(hasAddWidgetOutput, "addWidgetSubStepOutput 函数声明仍保留");
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
console.log("\n═══ Bug L 测试 — output 路径白名单验证 ═══\n");
|
|
789
|
-
|
|
790
|
-
// ── Test L1: output 路径白名单验证 ──
|
|
791
|
-
console.log("\n📋 测试 L1: output 路径白名单逻辑\n");
|
|
792
|
-
|
|
793
|
-
function isValidOutputPath(path) {
|
|
794
|
-
return /^[\w.\/-]+$/.test(path) &&
|
|
795
|
-
path.length > 15 && path.length < 300 &&
|
|
796
|
-
path.includes("019e57e7");
|
|
797
|
-
}
|
|
798
|
-
|
|
799
|
-
// 应接受的合法路径
|
|
800
|
-
assertTrue(isValidOutputPath(".pi-dev-output/pi-plans/20260524-test-019e57e7.md"), "合法路径应被接受");
|
|
801
|
-
assertTrue(isValidOutputPath(".pi-dev-output/pi-review/md/review-20260524-019e57e7.md"), "review 路径应被接受");
|
|
802
|
-
|
|
803
|
-
// 应拒绝的乱码路径
|
|
804
|
-
assertFalse(isValidOutputPath(".pi-dev-output/pi-plans/中grepUUID\"019e57e7\"找到)"), "含中文路径应被拒绝");
|
|
805
|
-
assertFalse(isValidOutputPath(".pi-dev-output/pi-plans/(测试)019e57e7.md"), "含括号路径应被拒绝");
|
|
806
|
-
|
|
807
|
-
// ── Test L2: post-completion output 检测仅搜索 clean text ──
|
|
808
|
-
console.log("\n📋 测试 L2: output 检测仅搜索 clean text(source 2 修复验证)\n");
|
|
809
|
-
|
|
810
|
-
// 验证源代码中 outputPattern.exec 搜索的是 cleanText 而非 searchText
|
|
811
|
-
const hasSearchTextOutput = source.includes('outputPattern.exec(searchText)');
|
|
812
|
-
assertFalse(hasSearchTextOutput, "output 检测不再搜索 searchText (raw JSON)");
|
|
813
|
-
|
|
814
|
-
// 验证替换为 cleanText
|
|
815
|
-
const hasCleanTextOutput = source.includes('outputPattern.exec(cleanText)');
|
|
816
|
-
assertTrue(hasCleanTextOutput, "output 检测搜索 cleanText (extracted text)");
|
|
817
|
-
|
|
818
|
-
// ── Test L3: post-completion output 有白名单验证 ──
|
|
819
|
-
console.log("\n📋 测试 L3: post-completion output 有白名单验证\n");
|
|
820
|
-
|
|
821
|
-
const hasWhitelistCheck = source.includes('/^[\\w.\\/-]+\$/.test(path_)');
|
|
822
|
-
assertTrue(hasWhitelistCheck, "post-completion output 检测包含 /^[\\w.\\/-]+\$/ 白名单验证");
|
|
823
|
-
|
|
824
|
-
// ── Test L4: progress handler output 检测使用 whitelist regex ──
|
|
825
|
-
console.log("\n📋 测试 L4: progress handler output 检测使用 whitelist regex\n");
|
|
826
|
-
|
|
827
|
-
const hasProgressWhitelist = source.includes('.pi-dev-output\\/[a-zA-Z0-9_\\/\\.-]+');
|
|
828
|
-
assertTrue(hasProgressWhitelist, "progress handler 使用 [a-zA-Z0-9_\\/\\.-]+ 白名单正则");
|
|
829
|
-
|
|
830
|
-
// ── Test L5: isWorkflowArtifactPath 在 git diff 轮询中使用 ──
|
|
831
|
-
console.log("\n📋 测试 L5: isWorkflowArtifactPath 在 git diff 轮询中使用\n");
|
|
832
|
-
|
|
833
|
-
const hasPollIsWorkflowArtifact = source.includes('isWorkflowArtifactPath(change.path)');
|
|
834
|
-
assertTrue(hasPollIsWorkflowArtifact, "git diff 轮询使用 isWorkflowArtifactPath 过滤");
|
|
835
|
-
|
|
836
|
-
// ── Test L6: git diff 轮询也向 _workflowFileChanges 推送 ──
|
|
837
|
-
console.log("\n📋 测试 L6: git diff 轮询向 _workflowFileChanges 推送\n");
|
|
838
|
-
|
|
839
|
-
// Verify the polling code pushes to _workflowFileChanges before addWidgetSubStepTool
|
|
840
|
-
const pollTimerBodyStart = source.indexOf('_gitPollTimer = setInterval');
|
|
841
|
-
const pollTimerBodyEnd = source.indexOf('_gitPollTimer.unref()');
|
|
842
|
-
const pollTimerBody = source.slice(pollTimerBodyStart, pollTimerBodyEnd);
|
|
843
|
-
const hasPushInPoll = pollTimerBody.includes('_workflowFileChanges.push');
|
|
844
|
-
assertTrue(hasPushInPoll, "git diff 轮询(setInterval)中包含 _workflowFileChanges.push");
|
|
845
|
-
|
|
846
|
-
// Verify the type mapping (A→new, D→delete, else→edit) exists in polling code
|
|
847
|
-
const hasTypeMappingInPoll = pollTimerBody.includes('change.status === "A" ? "new"');
|
|
848
|
-
assertTrue(hasTypeMappingInPoll, "git diff 轮询中文件变更的类型映射正确");
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
// ── Test L7: _pollSeen 使用 path:stepIndex 而非 status:path ──
|
|
853
|
-
console.log("\n📋 测试 L7: _pollSeen 使用 path:stepIndex 而非 status:path\n");
|
|
854
|
-
|
|
855
|
-
// 验证轮询代码不再使用 status:path 作为键
|
|
856
|
-
const hasStatusPathKeyInPoll = source.includes('const key = `\${change.status}:\${change.path}`');
|
|
857
|
-
assertFalse(hasStatusPathKeyInPoll, "_pollSeen 不使用 status:path 作为键");
|
|
858
|
-
|
|
859
|
-
// 验证轮询代码使用 path:stepIndex 作为键
|
|
860
|
-
const hasPathStepKeyInPoll = source.includes('const key = `\${change.path}:\${stepIndex}`');
|
|
861
|
-
assertTrue(hasPathStepKeyInPoll, "_pollSeen 使用 path:stepIndex 作为键");
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
// ── Test L8: toGitStatus 函数已删除 ──
|
|
865
|
-
console.log("\n📋 测试 L8: toGitStatus 函数已删除(无调用者死代码)\n");
|
|
866
|
-
|
|
867
|
-
const hasToGitStatusFunc = source.includes("function toGitStatus(");
|
|
868
|
-
assertFalse(hasToGitStatusFunc, "toGitStatus 函数已删除");
|
|
869
|
-
|
|
870
|
-
const hasToGitStatusRef = source.includes("toGitStatus");
|
|
871
|
-
assertFalse(hasToGitStatusRef, "toGitStatus 无任何引用残留");
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
// ── Test L9: updateToolsFromGit 不再有 pollSeen 参数 ──
|
|
875
|
-
console.log("\n📋 测试 L9: updateToolsFromGit 不再有 pollSeen 死代码参数\n");
|
|
876
|
-
|
|
877
|
-
const hasPollSeenParam = source.includes("pollSeen?: Set<string>");
|
|
878
|
-
assertFalse(hasPollSeenParam, "updateToolsFromGit 无 pollSeen 参数");
|
|
879
|
-
|
|
880
|
-
const hasPollSeenCheck = source.includes("pollSeen?.has(");
|
|
881
|
-
assertFalse(hasPollSeenCheck, "updateToolsFromGit 无 pollSeen?.has() 检查");
|
|
882
|
-
|
|
883
|
-
const hasPollSeenPass = source.includes("updateToolsFromGit(_workflowCwd, stepIndex, agentName, _pollSeen)");
|
|
884
|
-
assertFalse(hasPollSeenPass, "updateToolsFromGit 调用处无 _pollSeen 参数传递");
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
// ── Test L10: post-completion output 正则使用白名单字符类 ──
|
|
888
|
-
console.log("\n📋 测试 L10: post-completion output 正则使用白名单字符类\n");
|
|
889
|
-
|
|
890
|
-
// 验证 post-completion outputPattern 使用白名单 [a-zA-Z0-9] 而非排除法 [^...]
|
|
891
|
-
const hasPostCompletionWhitelist = source.includes("outputPattern = new RegExp(\`\\\\.pi-dev-output\\\\/[a-zA-Z0-9_\\\\/.-]");
|
|
892
|
-
assertTrue(hasPostCompletionWhitelist, "post-completion output 正则使用 [a-zA-Z0-9_\\\\/.-] 白名单字符类");
|
|
893
|
-
|
|
894
|
-
// 验证不再使用排除字符类 (用反确认方式)
|
|
895
|
-
const hasNegatedClassOutput = source.includes("\\[^\\\\s,;)\\\\]}");
|
|
896
|
-
const hasNegatedClassOutput2 = source.includes("[^\\\\s,;)\\\\]}");
|
|
897
|
-
assertFalse(hasNegatedClassOutput || hasNegatedClassOutput2, "post-completion output 正则不使用排除法 [^\\\\s,;)\\\\]}");
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
console.log("\n═══════════════════════════════════════════════════════\n");
|
|
901
|
-
console.log(`📊 结果: ${pass} 通过, ${fail} 失败\n`);
|
|
902
|
-
|
|
903
|
-
if (fail > 0) {
|
|
904
|
-
console.error("❌ 部分测试失败");
|
|
905
|
-
process.exit(1);
|
|
906
|
-
} else {
|
|
907
|
-
console.log("✅ 全部通过");
|
|
908
|
-
}
|