@ghyper9023/pi-dev-workflow 0.6.0 → 0.6.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.
|
@@ -51,7 +51,7 @@ extra-args:
|
|
|
51
51
|
|
|
52
52
|
请严格按照以下 Markdown 格式生成计划文件:
|
|
53
53
|
|
|
54
|
-
|
|
54
|
+
````markdown
|
|
55
55
|
# {功能名称} — 实施计划
|
|
56
56
|
|
|
57
57
|
## 概述
|
|
@@ -128,4 +128,4 @@ extra-args:
|
|
|
128
128
|
## ⚠️ 关键注意事项与风险防御
|
|
129
129
|
* **潜在风险点**:注意 `discountPrice` 为空时的默认回退机制,避免在生产环境引发 `NaN` 错误。
|
|
130
130
|
* **手动确认**:需确保上游网关已放行新字段,否则本地集成测试通过后线上也可能获取不到数据。
|
|
131
|
-
|
|
131
|
+
````
|
|
@@ -54,7 +54,7 @@ extra-args:
|
|
|
54
54
|
|
|
55
55
|
写入 `.pi-dev-output/pi-review/md/` 的文件必须采用以下格式:
|
|
56
56
|
|
|
57
|
-
|
|
57
|
+
````markdown
|
|
58
58
|
# 🔍 代码审查报告 — {功能/任务名称}
|
|
59
59
|
|
|
60
60
|
## 📊 审计摘要
|
|
@@ -64,7 +64,7 @@ extra-args:
|
|
|
64
64
|
|
|
65
65
|
| 🔴 严重 (Critical) | 🟡 中等 (Medium) | 🟢 低优先级 (Low) |
|
|
66
66
|
| :---: | :---: | :---: |
|
|
67
|
-
|
|
|
67
|
+
| 1 | 2 | 3 |
|
|
68
68
|
|
|
69
69
|
---
|
|
70
70
|
|
|
@@ -85,7 +85,16 @@ extra-args:
|
|
|
85
85
|
```
|
|
86
86
|
### [🟡 中等] 示例:`src/components/List.tsx` 重复渲染隐患
|
|
87
87
|
...
|
|
88
|
+
|
|
89
|
+
### [🟢 低优先级] 示例:`src/application/mod.rs` 未格式化
|
|
90
|
+
...
|
|
91
|
+
|
|
92
|
+
```json
|
|
93
|
+
[REVIEW_SUMMARY]
|
|
94
|
+
{"maxSeverity":"critical","critical":1,"medium":2,"low":3}
|
|
95
|
+
[/REVIEW_SUMMARY]
|
|
88
96
|
```
|
|
97
|
+
````
|
|
89
98
|
|
|
90
99
|
---
|
|
91
100
|
|
|
@@ -389,20 +389,16 @@ function getGitDiffChanges(cwd: string): GitFileChange[] {
|
|
|
389
389
|
}
|
|
390
390
|
|
|
391
391
|
/**
|
|
392
|
-
*
|
|
393
|
-
*
|
|
392
|
+
* 检查路径是否为工作流生成的产物路径(.pi-dev-output/),
|
|
393
|
+
* 若是则不应出现在 A: M: D: 文件变更列表中。
|
|
394
|
+
* 该目录已加入 .gitignore,git diff 本身已忽略,
|
|
395
|
+
* 此函数作为防御性过滤。
|
|
394
396
|
*/
|
|
395
|
-
function
|
|
396
|
-
|
|
397
|
-
case "new": case "write": case "create": case "add": case "created": case "added":
|
|
398
|
-
return "A";
|
|
399
|
-
case "delete": case "remove": case "deleted": case "removed":
|
|
400
|
-
return "D";
|
|
401
|
-
default:
|
|
402
|
-
return "M";
|
|
403
|
-
}
|
|
397
|
+
function isWorkflowArtifactPath(filePath: string): boolean {
|
|
398
|
+
return filePath.startsWith(".pi-dev-output/");
|
|
404
399
|
}
|
|
405
400
|
|
|
401
|
+
|
|
406
402
|
/**
|
|
407
403
|
* Check whether a file's current content hash differs from its baseline.
|
|
408
404
|
* Returns true if the hash changed or the file is no longer accessible.
|
|
@@ -453,6 +449,7 @@ function captureBaseline(cwd: string): void {
|
|
|
453
449
|
* loop-group, then reviewed by reviewer) without being collapsed into one.
|
|
454
450
|
* Without :stepIndex, the Set dedup would wrongly skip a file modified in
|
|
455
451
|
* a later step just because it was already tracked in an earlier step.
|
|
452
|
+
*
|
|
456
453
|
*/
|
|
457
454
|
function updateToolsFromGit(cwd: string, stepIndex: number, agentName: string): void {
|
|
458
455
|
const currentChanges = getGitDiffChanges(cwd);
|
|
@@ -480,7 +477,7 @@ function updateToolsFromGit(cwd: string, stepIndex: number, agentName: string):
|
|
|
480
477
|
// and should NOT be reported as user repo changes. Real git clients (VSCode, Zed) also
|
|
481
478
|
// ignore this directory (it's in .gitignore). This filter is defense-in-depth.
|
|
482
479
|
// Note: git always uses forward slashes in paths, even on Windows.
|
|
483
|
-
if (change.path
|
|
480
|
+
if (isWorkflowArtifactPath(change.path)) continue;
|
|
484
481
|
|
|
485
482
|
seen.add(`${change.path}:${stepIndex}`);
|
|
486
483
|
const type: FileChangeEntry["type"] =
|
|
@@ -1094,35 +1091,81 @@ async function runAgentWithProgress(
|
|
|
1094
1091
|
}
|
|
1095
1092
|
}
|
|
1096
1093
|
|
|
1097
|
-
//
|
|
1098
|
-
//
|
|
1099
|
-
//
|
|
1100
|
-
//
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1094
|
+
// ── Start git diff polling for real file changes ──
|
|
1095
|
+
// This replaces the unreliable regex-based tool call sniffing that was removed.
|
|
1096
|
+
// git diff --name-status is the same approach used by VSCode, Zed, and every
|
|
1097
|
+
// professional git client — it's deterministic, noise-free, and 100% accurate.
|
|
1098
|
+
let _gitPollTimer: ReturnType<typeof setInterval> | null = null;
|
|
1099
|
+
const _pollSeen = new Set<string>();
|
|
1100
|
+
if (_workflowCwd) {
|
|
1101
|
+
_gitPollTimer = setInterval(() => {
|
|
1102
|
+
try {
|
|
1103
|
+
const currentChanges = getGitDiffChanges(_workflowCwd);
|
|
1104
|
+
for (const change of currentChanges) {
|
|
1105
|
+
const key = `${change.path}:${stepIndex}`;
|
|
1106
|
+
if (_pollSeen.has(key)) continue;
|
|
1107
|
+
_pollSeen.add(key);
|
|
1108
|
+
|
|
1109
|
+
if (isWorkflowArtifactPath(change.path)) continue;
|
|
1110
|
+
if (_workflowBaseline.length > 0) {
|
|
1111
|
+
const baseEntry = _workflowBaseline.find(b => b.path === change.path);
|
|
1112
|
+
if (baseEntry) {
|
|
1113
|
+
if (!hasContentChanged(_workflowCwd, change.path, baseEntry.hash)) continue;
|
|
1114
|
+
// ⭐ 同步删除已变更的基线条目,与 updateToolsFromGit 行为一致
|
|
1115
|
+
_workflowBaseline = _workflowBaseline.filter(b => b.path !== change.path);
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
// ⭐ Also record in _workflowFileChanges so the completion report file tree
|
|
1120
|
+
// and revertStepChanges have accurate data. The final updateToolsFromGit call
|
|
1121
|
+
// will skip this via seen, so we must record it here.
|
|
1122
|
+
const type: FileChangeEntry["type"] =
|
|
1123
|
+
change.status === "A" ? "new" :
|
|
1124
|
+
change.status === "D" ? "delete" :
|
|
1125
|
+
"edit";
|
|
1126
|
+
_workflowFileChanges.push({
|
|
1127
|
+
agent: agentName,
|
|
1128
|
+
stepIndex,
|
|
1129
|
+
type,
|
|
1130
|
+
filePath: change.path,
|
|
1131
|
+
timestamp: new Date().toISOString(),
|
|
1132
|
+
});
|
|
1133
|
+
addWidgetSubStepTool(stepIndex, agentName, `${change.status} ${change.path}`);
|
|
1134
|
+
_widgetExtraToolCount++;
|
|
1135
|
+
}
|
|
1136
|
+
} catch { /* 静默忽略 git 错误 */ }
|
|
1137
|
+
}, 5000);
|
|
1138
|
+
_gitPollTimer.unref();
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
// Run the sub-agent with progress reporting
|
|
1142
|
+
// The toolMatch regex sniffing has been removed — it was the primary source of
|
|
1143
|
+
// false positives in the A: M: D: panel (matching AI natural language as tool calls).
|
|
1144
|
+
// File change detection is now handled entirely by git diff polling (above) and
|
|
1145
|
+
// the final updateToolsFromGit call (below), both of which use git's own API.
|
|
1146
|
+
let result;
|
|
1147
|
+
try {
|
|
1148
|
+
result = await spawnSubagent(agent, task, _workflowCwd, signal, timeoutMs, (progress) => {
|
|
1149
|
+
// Detect output file paths — only match .pi-dev-output paths belonging to current workflow
|
|
1150
|
+
if (_workflowId) {
|
|
1151
|
+
const outputMatch = progress.match(/\.pi-dev-output\/[a-zA-Z0-9_\/\.-]+/i);
|
|
1152
|
+
if (outputMatch) {
|
|
1153
|
+
const pathCandidate = outputMatch[0]!.trim();
|
|
1154
|
+
// ⭐ 严格白名单:拒绝中文、引号、括号等非路径字符
|
|
1155
|
+
if (pathCandidate.length > 15 && pathCandidate.length < 300 &&
|
|
1156
|
+
pathCandidate.includes(_workflowId) &&
|
|
1157
|
+
/^[\w.\/-]+$/.test(pathCandidate)) {
|
|
1158
|
+
addWidgetSubStepOutput(stepIndex, agentName, pathCandidate);
|
|
1159
|
+
}
|
|
1122
1160
|
}
|
|
1123
1161
|
}
|
|
1162
|
+
}, _workflowId ? { workflowId: _workflowId } : undefined);
|
|
1163
|
+
} finally {
|
|
1164
|
+
if (_gitPollTimer) {
|
|
1165
|
+
clearInterval(_gitPollTimer);
|
|
1166
|
+
_gitPollTimer = null;
|
|
1124
1167
|
}
|
|
1125
|
-
}
|
|
1168
|
+
}
|
|
1126
1169
|
|
|
1127
1170
|
const agentDuration = Date.now() - agentStartTime;
|
|
1128
1171
|
|
|
@@ -1142,23 +1185,30 @@ async function runAgentWithProgress(
|
|
|
1142
1185
|
// from code snippets). Instead, we rely entirely on git diff --name-status, which is
|
|
1143
1186
|
// the same approach used by VSCode, Zed, and every other professional git client.
|
|
1144
1187
|
// It's deterministic, noise-free, and always produces correct relative paths.
|
|
1145
|
-
const allOutput = (result.output || "") + "\n" + (result.stderr || "");
|
|
1146
1188
|
const finalOutput = extractFinalOutput(result.output) || result.output;
|
|
1147
|
-
const searchText = allOutput + "\n" + finalOutput;
|
|
1148
1189
|
|
|
1149
1190
|
// Find output file paths (plans, reviews) for display in the "output:" section
|
|
1150
1191
|
// These are workflow artifacts (.pi-dev-output/) — only show files belonging to
|
|
1151
1192
|
// the CURRENT workflow to avoid cross-contamination from previous runs.
|
|
1152
1193
|
// Note: file change detection itself relies exclusively on git diff below, not text parsing.
|
|
1194
|
+
// ⭐ IMPORTANT: Only search in CLEAN extracted text (finalOutput), NOT raw JSON output.
|
|
1195
|
+
// The old approach concatenated allOutput (raw JSON + stderr) into searchText and
|
|
1196
|
+
// matched against it, producing false positives from JSON stringified content.
|
|
1197
|
+
// By restricting to extractFinalOutput's clean text, we avoid matching against
|
|
1198
|
+
// JSON-escaped paths, code snippets, or natural language in raw agent output.
|
|
1199
|
+
const cleanText = finalOutput || result.output || "";
|
|
1153
1200
|
const seenOutputs = new Set<string>();
|
|
1154
1201
|
if (_workflowId) {
|
|
1155
1202
|
// Only match .pi-dev-output paths containing the current workflow UUID
|
|
1156
1203
|
const escapedId = _workflowId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
1157
|
-
const outputPattern = new RegExp(`\\.pi-dev-output\\/[
|
|
1204
|
+
const outputPattern = new RegExp(`\\.pi-dev-output\\/[a-zA-Z0-9_\\/.-]*${escapedId}[a-zA-Z0-9_\\/.-]*`, 'g');
|
|
1158
1205
|
let m;
|
|
1159
|
-
while ((m = outputPattern.exec(
|
|
1206
|
+
while ((m = outputPattern.exec(cleanText)) !== null) {
|
|
1160
1207
|
const path_ = m[0]!.trim();
|
|
1161
1208
|
if (path_.length > 15 && path_.length < 300 && !seenOutputs.has(path_)) {
|
|
1209
|
+
// ⭐ 严格白名单:仅允许 [a-zA-Z0-9_./-] 字符
|
|
1210
|
+
// 拒绝包含中文、引号、括号、空格等非路径字符的乱码匹配
|
|
1211
|
+
if (!/^[\w.\/-]+$/.test(path_)) continue;
|
|
1162
1212
|
seenOutputs.add(path_);
|
|
1163
1213
|
addWidgetSubStepOutput(stepIndex, agentName, path_);
|
|
1164
1214
|
}
|
|
@@ -1167,9 +1217,11 @@ async function runAgentWithProgress(
|
|
|
1167
1217
|
// No workflow ID (unlikely): match .pi-dev-output paths conservatively
|
|
1168
1218
|
const outputPattern = /\.pi-dev-output\/[a-zA-Z0-9_\/-]+\.[a-zA-Z0-9]+/g;
|
|
1169
1219
|
let m;
|
|
1170
|
-
while ((m = outputPattern.exec(
|
|
1220
|
+
while ((m = outputPattern.exec(cleanText)) !== null) {
|
|
1171
1221
|
const path_ = m[0]!.trim();
|
|
1172
1222
|
if (path_.length > 15 && path_.length < 300 && !seenOutputs.has(path_)) {
|
|
1223
|
+
// ⭐ 严格白名单
|
|
1224
|
+
if (!/^[\w.\/-]+$/.test(path_)) continue;
|
|
1173
1225
|
seenOutputs.add(path_);
|
|
1174
1226
|
addWidgetSubStepOutput(stepIndex, agentName, path_);
|
|
1175
1227
|
}
|
package/package.json
CHANGED
|
@@ -502,12 +502,13 @@ assertTrue(hasWorkSummaryKey, "executeSingleStep 中保留 '工作总结' 条目
|
|
|
502
502
|
const hasWorkSummaryUpdate = fullSingleStep.includes('updateChainContext');
|
|
503
503
|
assertTrue(hasWorkSummaryUpdate, "executeSingleStep 中仍有 updateChainContext 调用(工作总结)");
|
|
504
504
|
|
|
505
|
-
// Verify the removed summary keys are no longer present
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
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");
|
|
511
512
|
|
|
512
513
|
|
|
513
514
|
console.log("\n═══ Bug H 测试 — Agent 前置元数据解析 ═══\n");
|
|
@@ -654,20 +655,25 @@ console.log("\n");
|
|
|
654
655
|
|
|
655
656
|
console.log("═══ Bug J 测试 — _workflowFileChanges 仅来自 git diff ═══\n");
|
|
656
657
|
|
|
657
|
-
// ── Test J1: _workflowFileChanges.push
|
|
658
|
-
console.log("📋 测试 J1: _workflowFileChanges.push
|
|
658
|
+
// ── Test J1: _workflowFileChanges.push 出现在 updateToolsFromGit 和 git diff 轮询 ──
|
|
659
|
+
console.log("📋 测试 J1: _workflowFileChanges.push 出现在 updateToolsFromGit 和 git diff 轮询\n");
|
|
659
660
|
|
|
660
661
|
// Count all _workflowFileChanges.push occurrences
|
|
662
|
+
// There should be 2: one in git diff polling (runAgentWithProgress) and one in updateToolsFromGit
|
|
661
663
|
const pushMatches = [...source.matchAll(/_workflowFileChanges\.push\(/g)];
|
|
662
|
-
assertEq(pushMatches.length,
|
|
664
|
+
assertEq(pushMatches.length, 2, "_workflowFileChanges.push 出现 2 次(轮询 + updateToolsFromGit)");
|
|
663
665
|
|
|
664
|
-
// Verify the
|
|
665
|
-
// the function body of updateToolsFromGit contains the push.
|
|
666
|
+
// Verify the push is inside updateToolsFromGit
|
|
666
667
|
const updateToolsStart = source.indexOf("function updateToolsFromGit");
|
|
667
668
|
const nextFuncStart = source.indexOf("function saveCheckpoint", updateToolsStart);
|
|
668
669
|
const updateToolsBody = source.slice(updateToolsStart, nextFuncStart);
|
|
669
|
-
|
|
670
|
-
|
|
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");
|
|
671
677
|
|
|
672
678
|
// ── Test J2: addWidgetSubStepTool 不再污染 _workflowFileChanges ──
|
|
673
679
|
console.log("\n📋 测试 J2: addWidgetSubStepTool 不再污染 _workflowFileChanges\n");
|
|
@@ -677,7 +683,7 @@ const addWidgetFunc = source.slice(
|
|
|
677
683
|
source.indexOf("function addWidgetSubStepOutput")
|
|
678
684
|
);
|
|
679
685
|
const hasNoFileChangesRef = !addWidgetFunc.includes("_workflowFileChanges");
|
|
680
|
-
assertTrue(hasNoFileChangesRef, "addWidgetSubStepTool
|
|
686
|
+
assertTrue(hasNoFileChangesRef, "addWidgetSubStepTool 函数体中不再引用 _workflowFileChanges");
|
|
681
687
|
|
|
682
688
|
// ── Test J3: executeSingleStep 中不再有基于 _workflowFileChanges 的统计 ──
|
|
683
689
|
console.log("\n📋 测试 J3: executeSingleStep 中不再有 '执行摘要' 类 chain context\n");
|
|
@@ -694,8 +700,11 @@ const loopGroupBody = source.slice(loopGroupStart);
|
|
|
694
700
|
const hasNoCodeImplSummary = !loopGroupBody.includes('"代码实施摘要"') && !loopGroupBody.includes('"代码精简摘要"');
|
|
695
701
|
assertTrue(hasNoCodeImplSummary, "executeLoopGroup 中不再有 '代码实施摘要' 或 '代码精简摘要' chain context");
|
|
696
702
|
|
|
697
|
-
|
|
698
|
-
|
|
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");
|
|
699
708
|
|
|
700
709
|
// ── Test J5: 工作总结条目在所有 agent 函数中仍保留 ──
|
|
701
710
|
console.log("\n📋 测试 J5: '工作总结' 条目在所有 agent 函数中仍保留\n");
|
|
@@ -733,11 +742,14 @@ console.log("\n📋 测试 K3: outputPathPatterns 已简化\n");
|
|
|
733
742
|
const hasOutputPathPatterns = source.includes("const outputPathPatterns = [");
|
|
734
743
|
assertFalse(hasOutputPathPatterns, "outputPathPatterns 数组已移除(替换为 inline workflowId 过滤)");
|
|
735
744
|
|
|
736
|
-
// ── Test K4: updateToolsFromGit 有
|
|
737
|
-
console.log("\n📋 测试 K4: updateToolsFromGit
|
|
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/ 路径");
|
|
738
750
|
|
|
739
|
-
const
|
|
740
|
-
assertTrue(
|
|
751
|
+
const hasIsWorkflowArtifactFunc = source.includes('function isWorkflowArtifactPath');
|
|
752
|
+
assertTrue(hasIsWorkflowArtifactFunc, "isWorkflowArtifactPath 辅助函数存在");
|
|
741
753
|
|
|
742
754
|
// ── Test K5: output 路径匹配使用 escapedId + workflowId ──
|
|
743
755
|
console.log("\n📋 测试 K5: output 路径使用 workflowId 过滤\n");
|
|
@@ -751,15 +763,19 @@ console.log("\n📋 测试 K6: progress handler 使用 workflowId\n");
|
|
|
751
763
|
const hasIncludesWorkflowId = source.includes("pathCandidate.includes(_workflowId)");
|
|
752
764
|
assertTrue(hasIncludesWorkflowId, "progress handler 使用 pathCandidate.includes(_workflowId)");
|
|
753
765
|
|
|
754
|
-
// ── Test K7: addWidgetSubStepTool 只来自 git diff
|
|
755
|
-
console.log("\n📋 测试 K7: addWidgetSubStepTool
|
|
766
|
+
// ── Test K7: addWidgetSubStepTool 只来自 git diff(regex 嗅探已移除)──
|
|
767
|
+
console.log("\n📋 测试 K7: addWidgetSubStepTool 仅来自 git diff\n");
|
|
756
768
|
|
|
757
|
-
// Verify the
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
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
|
|
761
774
|
assertTrue(source.includes("addWidgetSubStepTool(stepIndex, agentName, `${change.status} ${change.path}`)"), "updateToolsFromGit 调用 addWidgetSubStepTool");
|
|
762
|
-
|
|
775
|
+
|
|
776
|
+
// Verify git diff polling timer exists (replacement for regex sniffing)
|
|
777
|
+
const hasGitPollTimer = source.includes('_gitPollTimer = setInterval');
|
|
778
|
+
assertTrue(hasGitPollTimer, "git diff 轮询定时器已添加");
|
|
763
779
|
|
|
764
780
|
// ── Test K8: addWidgetSubStepOutput 仍存在 (output 路径展示有用) ──
|
|
765
781
|
console.log("\n📋 测试 K8: addWidgetSubStepOutput 仍保留\n");
|
|
@@ -769,6 +785,118 @@ assertTrue(hasAddWidgetOutput, "addWidgetSubStepOutput 函数声明仍保留");
|
|
|
769
785
|
|
|
770
786
|
|
|
771
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
|
+
|
|
772
900
|
console.log("\n═══════════════════════════════════════════════════════\n");
|
|
773
901
|
console.log(`📊 结果: ${pass} 通过, ${fail} 失败\n`);
|
|
774
902
|
|