@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ghyper9023/pi-dev-workflow",
3
- "version": "0.3.3",
3
+ "version": "0.4.1",
4
4
  "keywords": [
5
5
  "pi-package"
6
6
  ],
@@ -49,8 +49,8 @@ description: review 代码并输出交互式 HTML 报告。适用于代码审查
49
49
  1. 根据用户指令运行 `git diff HEAD` 或 `git log -p -n <N>` 获取改动,或遍历代码库。
50
50
  2. 合并改动,按文件分组。
51
51
  3. 对每个文件进行审查,并生成符合上述要求的 HTML。
52
- 4. 自动确保 `pi-dev-output/pi-review/` 文件夹存在于项目根目录,若不存在则创建;并确保 `pi-dev-output/` 下的 `.gitignore` 包含 `*`(忽略所有内容但保留目录)。
52
+ 4. 自动确保 `.pi-dev-output/pi-review/html/` 文件夹存在于项目根目录,若不存在则创建。
53
53
  5. HTML 文件命名格式:`年月日时分-任务简述(极简10词以内)-index.html`,若同名文件已存在则编号递增(如 `index1.html`)。
54
- 6. 直接输出 HTML 文件到 `pi-dev-output/pi-review/` 目录,**不要添加任何解释性文字**,仅简短说明工作完成和输出文件路径即可。
54
+ 6. 直接输出 HTML 文件到 `.pi-dev-output/pi-review/html/` 目录,**不要添加任何解释性文字**,仅简短说明工作完成和输出文件路径即可。
55
55
 
56
56
  请严格遵循以上规则。
@@ -15,7 +15,7 @@ A deep module (as opposed to a shallow module) is one which encapsulates a lot o
15
15
 
16
16
  Check with the user that these modules match their expectations. Check with the user which modules they want tests written for.
17
17
 
18
- 3. Write the PRD using the template below, then save it to `pi-dev-output/pi-prd/` directory.
18
+ 3. Write the PRD using the template below, then save it to `.pi-dev-output/pi-prd/` directory.
19
19
 
20
20
  <prd-template>
21
21
 
@@ -0,0 +1,243 @@
1
+ /**
2
+ * test-grill-json-fix.mjs — Verify the JSON stability fixes for grill-me-agent
3
+ *
4
+ * Tests:
5
+ * 1. writeToolPromptSuffix 包含 JSON Schema
6
+ * 2. writeToolPromptSuffix 包含转义规则
7
+ * 3. writeToolPromptSuffix 包含自我校验指令
8
+ * 4. readQuestionsFromFile 正确处理合法 JSON
9
+ * 5. readQuestionsFromFile 正确处理无效 JSON(返回空数组 + 不报错)
10
+ * 6. readQuestionsFromFile 正确处理含特殊字符的 JSON
11
+ * 7. 输出文件不再被删除
12
+ * 8. 重试 prompt 包含错误上下文
13
+ * 9. ensureOutputDir 的 .gitignore 让文件不被 git 跟踪(保留在本地)
14
+ *
15
+ * Run: node tests/test-grill-json-fix.mjs
16
+ */
17
+
18
+ import * as fs from "node:fs";
19
+ import * as path from "node:path";
20
+ import { fileURLToPath } from "node:url";
21
+
22
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
23
+ const EXT_PATH = path.resolve(__dirname, "../extensions/grill-me-agent.ts");
24
+
25
+ // ── Helpers ──────────────────────────────────────────────────
26
+
27
+ let pass = 0;
28
+ let fail = 0;
29
+
30
+ function assert(condition, msg) {
31
+ if (condition) {
32
+ pass++;
33
+ console.log(` ✅ ${msg}`);
34
+ } else {
35
+ fail++;
36
+ console.error(` ❌ ${msg}`);
37
+ }
38
+ }
39
+
40
+ // ── Read the source file ─────────────────────────────────────
41
+
42
+ let source;
43
+ try {
44
+ source = fs.readFileSync(EXT_PATH, "utf-8");
45
+ } catch (e) {
46
+ console.error(`Failed to read source file: ${e.message}`);
47
+ process.exit(1);
48
+ }
49
+
50
+ console.log(`📄 源文件: ${EXT_PATH}`);
51
+ console.log(`📏 文件大小: ${source.length} 字节\n`);
52
+
53
+ // ── Test 1: Contains JSON Schema ─────────────────────────────
54
+
55
+ assert(
56
+ source.includes('"$schema": "http://json-schema.org/draft-07/schema#"'),
57
+ "writeToolPromptSuffix 应包含 Draft-07 JSON Schema",
58
+ );
59
+
60
+ assert(
61
+ source.includes('"required": ["id", "question", "options"]'),
62
+ "JSON Schema 要求 id, question, options 为必填字段",
63
+ );
64
+
65
+ assert(
66
+ source.includes('"minItems": 1'),
67
+ "JSON Schema 要求 options 至少 1 项",
68
+ );
69
+
70
+ // ── Test 2: Contains escaping rules ──────────────────────────
71
+
72
+ assert(
73
+ source.includes("String escaping rules") ||
74
+ source.includes("JSON-escaped"),
75
+ "writeToolPromptSuffix 应包含字符串转义规则",
76
+ );
77
+
78
+ assert(
79
+ source.includes("Double quotes inside text"),
80
+ "转义规则应提及双引号",
81
+ );
82
+
83
+ assert(
84
+ source.includes("Newlines"),
85
+ "转义规则应提及换行符",
86
+ );
87
+
88
+ assert(
89
+ source.includes("Backslashes"),
90
+ "转义规则应提及反斜杠",
91
+ );
92
+
93
+ // ── Test 3: Contains self-review instruction ─────────────────
94
+
95
+ assert(
96
+ source.includes("Self-review") ||
97
+ source.includes("mentally validate") ||
98
+ source.includes("mentally validate your JSON"),
99
+ "writeToolPromptSuffix 应包含自我校验指令",
100
+ );
101
+
102
+ // ── Test 4: Example uses unicode escapes ─────────────────────
103
+ // Note: The source file has \\uXXXX (double backslash in raw text)
104
+ // because TypeScript single-quoted string \\ → single backslash in value.
105
+ // search for "\\\\u" (two backslashes in raw file = "\\u" in JS regex)
106
+ assert(
107
+ source.includes("\\\\u9879\\\\u76ee") ||
108
+ (source.includes("\\\\u9879") && source.includes("u9879")),
109
+ "示例应包含 JSON unicode 转义序列(合法 JSON 示例)",
110
+ );
111
+
112
+ // ── Test 5: readQuestionsFromFile has error logging ──────────
113
+
114
+ assert(
115
+ source.includes("[grill-me-agent] JSON parse error in"),
116
+ "readQuestionsFromFile 应在 JSON 解析失败时输出错误日志",
117
+ );
118
+
119
+ // ── Test 6: Output files are no longer deleted ───────────────
120
+
121
+ assert(
122
+ !source.includes("fs.unlinkSync(outputFilePath)") &&
123
+ !source.includes("fs.unlinkSync(retryPath)"),
124
+ "输出文件不应再被 fs.unlinkSync 删除",
125
+ );
126
+
127
+ // ── Test 7: Retry prompt includes error context ──────────────
128
+
129
+ assert(
130
+ source.includes("Previous attempt had JSON errors") ||
131
+ source.includes("parseErrorMsg"),
132
+ "重试 prompt 应包含前次错误的上下文",
133
+ );
134
+
135
+ // ── Test 8: Parse valid JSON ─────────────────────────────────
136
+
137
+ function simulateReadQuestions(raw) {
138
+ try {
139
+ const parsed = JSON.parse(raw);
140
+ const items = Array.isArray(parsed) ? parsed : parsed.questions;
141
+ if (!Array.isArray(items)) return 0;
142
+ return items.filter(
143
+ (q) => q && typeof q.question === "string" && Array.isArray(q.options) && q.options.length > 0,
144
+ ).length;
145
+ } catch {
146
+ return 0;
147
+ }
148
+ }
149
+
150
+ const validJson = JSON.stringify({
151
+ questions: [
152
+ { id: 1, question: "测试问题", options: ["选项A", "选项B"] },
153
+ { id: 2, question: "第二个问题", options: ["选项X", "选项Y", "选项Z"] },
154
+ ],
155
+ });
156
+
157
+ assert(
158
+ simulateReadQuestions(validJson) === 2,
159
+ "readQuestionsFromFile 应解析合法 JSON 并返回正确数量的问题",
160
+ );
161
+
162
+ // ── Test 9: Invalid JSON (unescaped double quotes) ───────────
163
+
164
+ const invalidJson = `{
165
+ "questions": [
166
+ {
167
+ "id": 1,
168
+ "question": "他说"这个不行",如何处理?",
169
+ "options": ["忽略", "修复"]
170
+ }
171
+ ]
172
+ }`;
173
+
174
+ assert(
175
+ simulateReadQuestions(invalidJson) === 0,
176
+ "readQuestionsFromFile 遇到非法 JSON 应返回空数组,而非崩溃",
177
+ );
178
+
179
+ // ── Test 10: JSON with properly escaped special chars ────────
180
+
181
+ const escapedJson = JSON.stringify({
182
+ questions: [
183
+ {
184
+ id: 1,
185
+ question: '他说"这个不行",如何处理?',
186
+ options: ["忽略", "修复"],
187
+ },
188
+ ],
189
+ });
190
+
191
+ assert(
192
+ simulateReadQuestions(escapedJson) === 1,
193
+ "readQuestionsFromFile 应解析含转义双引号的合法 JSON",
194
+ );
195
+
196
+ // ── Test 11: Bare array format backward compat ───────────────
197
+
198
+ const bareArray = JSON.stringify([
199
+ { id: 1, question: "测试问题", options: ["A", "B"] },
200
+ ]);
201
+
202
+ assert(
203
+ simulateReadQuestions(bareArray) === 1,
204
+ "readQuestionsFromFile 应后向兼容裸数组格式 [...]",
205
+ );
206
+
207
+ // ── Test 12: Missing vs empty fields ─────────────────────────
208
+ // Empty string "" passes filter (typeof "" === "string" is true).
209
+ // Only truly missing (undefined) fields are filtered.
210
+
211
+ const withIncomplete = JSON.stringify({
212
+ questions: [
213
+ { id: 1, question: "完整问题", options: ["A"] },
214
+ { id: 2, question: "", options: ["A"] }, // empty string, passes filter
215
+ { id: 3, options: ["A"] }, // missing question, filtered
216
+ { id: 4, question: "无选项" }, // missing options, filtered
217
+ ],
218
+ });
219
+
220
+ assert(
221
+ simulateReadQuestions(withIncomplete) === 2,
222
+ "readQuestionsFromFile 应过滤缺失字段的问题,空字符串字段应保留",
223
+ );
224
+
225
+ // ── Test 13: gitignore for output dir exists ─────────────────
226
+
227
+ assert(
228
+ source.includes("pi-grill"),
229
+ "代码中应引用 pi-grill 输出目录",
230
+ );
231
+
232
+ // ── Summary ──────────────────────────────────────────────────
233
+
234
+ console.log(`\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
235
+ console.log(`结果: ${pass} 通过, ${fail} 失败, 共 ${pass + fail} 个测试`);
236
+ console.log(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
237
+
238
+ if (fail > 0) {
239
+ console.error("\n⚠️ 部分测试未通过");
240
+ process.exit(1);
241
+ } else {
242
+ console.log("\n✅ 所有测试通过");
243
+ }
@@ -0,0 +1,177 @@
1
+ /**
2
+ * test-output-directory-structure.mjs — 验证输出目录结构调整正确
3
+ *
4
+ * Bug: 原先 .pi-dev-output/pi-grill/ 直接存放 answer-*.md 和 questions-*.json,
5
+ * .pi-dev-output/pi-review/ 直接存放 *.html 和 *.md,没有分类子目录。
6
+ *
7
+ * 预期新结构:
8
+ * .pi-dev-output/pi-grill/questions/ → questions-<id>-<YYYYMMDD-HHmm>.json
9
+ * .pi-dev-output/pi-grill/answers/ → answer-<id>-<YYYYMMDD-HHmm>.md
10
+ * .pi-dev-output/pi-review/html/ → *.html
11
+ * .pi-dev-output/pi-review/md/ → *.md
12
+ *
13
+ * Run: node tests/test-output-directory-structure.mjs
14
+ */
15
+
16
+ import * as fs from "node:fs";
17
+ import * as path from "node:path";
18
+ import { fileURLToPath } from "node:url";
19
+
20
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
21
+ const GRILL_ME_PATH = path.resolve(__dirname, "../extensions/grill-me-agent.ts");
22
+ const SUB_AGENTS_PATH = path.resolve(__dirname, "../extensions/sub-agents.ts");
23
+ const WORKFLOW_PATH = path.resolve(__dirname, "../extensions/workflow-engine.ts");
24
+
25
+ // ── Helpers ──────────────────────────────────────────────────
26
+
27
+ let pass = 0;
28
+ let fail = 0;
29
+
30
+ function assert(condition, msg) {
31
+ if (condition) {
32
+ pass++;
33
+ console.log(` ✅ ${msg}`);
34
+ } else {
35
+ fail++;
36
+ console.error(` ❌ ${msg}`);
37
+ }
38
+ }
39
+
40
+ function assertIncludes(source, substr, msg) {
41
+ assert(source.includes(substr), msg);
42
+ }
43
+
44
+ function assertNotIncludes(source, substr, msg) {
45
+ assert(!source.includes(substr), msg);
46
+ }
47
+
48
+ // ═══════════════════════════════════════════════════════════════
49
+ // 1. grill-me-agent.ts — 常量与路径
50
+ // ═══════════════════════════════════════════════════════════════
51
+
52
+ console.log("📋 grill-me-agent.ts — 目录常量与文件路径\n");
53
+
54
+ const grillMe = fs.readFileSync(GRILL_ME_PATH, "utf-8");
55
+
56
+ // 1a. 新常量定义
57
+ assertIncludes(grillMe, 'GRILL_ANSWERS_DIRNAME = "answers"', "定义 GRILL_ANSWERS_DIRNAME = answers");
58
+ assertIncludes(grillMe, 'GRILL_QUESTIONS_DIRNAME = "questions"', "定义 GRILL_QUESTIONS_DIRNAME = questions");
59
+
60
+ // 1b. grillOutputPath 写入 questions 子目录 + 新文件名格式
61
+ assertIncludes(grillMe, 'path.join(GRILL_DIRNAME, GRILL_QUESTIONS_DIRNAME)', "grillOutputPath 使用 questions 子目录");
62
+ assertIncludes(grillMe, "questions-${ts}-${formatTimestamp()}.json", "grillOutputPath 文件名含 formatTimestamp");
63
+
64
+ // 1c. saveAnswerFile 写入 answers 子目录 + 新文件名格式
65
+ assertIncludes(grillMe, 'path.join(GRILL_DIRNAME, GRILL_ANSWERS_DIRNAME)', "saveAnswerFile 使用 answers 子目录");
66
+ assertIncludes(grillMe, "answer-${ts}-${formatTimestamp()}.md", "saveAnswerFile 文件名含 formatTimestamp");
67
+ assertIncludes(grillMe, "DEV_OUTPUT_DIR, GRILL_DIRNAME, GRILL_ANSWERS_DIRNAME, filename", "saveAnswerFile 返回路径含 answers 子目录");
68
+
69
+ // 1d. recoverFromBackup 从 answers 子目录读取
70
+ assertIncludes(grillMe, "GRILL_DIRNAME, GRILL_ANSWERS_DIRNAME", "recoverFromBackup 从 answers 子目录读取");
71
+
72
+ // 1e. formatTimestamp 辅助函数
73
+ assertIncludes(grillMe, "function formatTimestamp", "定义 formatTimestamp 辅助函数");
74
+
75
+ // 1f. 旧文件名格式不应再出现
76
+ // Note: still allow "questions-" prefix in path.join for grillOutputPath
77
+ const oldSavePattern = "answer-${ts}.md";
78
+ assertNotIncludes(grillMe, oldSavePattern, "saveAnswerFile 不使用旧格式 answer-${ts}.md");
79
+
80
+ const oldQuestionsPattern = "questions-${ts}.json";
81
+ assertNotIncludes(grillMe, oldQuestionsPattern, "grillOutputPath 不使用旧格式 questions-${ts}.json");
82
+
83
+ // ═══════════════════════════════════════════════════════════════
84
+ // 2. sub-agents.ts — findNewestReviewHtml 新增优先路径
85
+ // ═══════════════════════════════════════════════════════════════
86
+
87
+ console.log("\n📋 sub-agents.ts — findNewestReviewHtml 搜索路径\n");
88
+
89
+ const subAgents = fs.readFileSync(SUB_AGENTS_PATH, "utf-8");
90
+ assertIncludes(subAgents, '".pi-dev-output", "pi-review", "html"', "findNewestReviewHtml 优先搜索 pi-review/html/");
91
+
92
+ // ═══════════════════════════════════════════════════════════════
93
+ // 3. workflow-engine.ts — reviewer 报告写入路径
94
+ // ═══════════════════════════════════════════════════════════════
95
+
96
+ console.log("\n📋 workflow-engine.ts — buildReviewTask 输出路径\n");
97
+
98
+ const workflowEngine = fs.readFileSync(WORKFLOW_PATH, "utf-8");
99
+ assertIncludes(workflowEngine, ".pi-dev-output/pi-review/md/", "buildReviewTask 告诉 reviewer 写入 pi-review/md/");
100
+
101
+ // ═══════════════════════════════════════════════════════════════
102
+ // 4. agent 定义文件一致性
103
+ // ═══════════════════════════════════════════════════════════════
104
+
105
+ console.log("\n📋 Agent 定义文件路径一致性\n");
106
+
107
+ const reviewAgent = fs.readFileSync(
108
+ path.resolve(__dirname, "../agents/review-agent.md"), "utf-8");
109
+ assertIncludes(reviewAgent, ".pi-dev-output/pi-review/html/",
110
+ "review-agent 写入 .pi-dev-output/pi-review/html/");
111
+ assertNotIncludes(reviewAgent, ".pi-dev-output/pi-review/ 目录",
112
+ "review-agent 不再引用旧的 .pi-dev-output/pi-review/ (无子目录)");
113
+
114
+ const workflowReviewer = fs.readFileSync(
115
+ path.resolve(__dirname, "../agents/workflow/reviewer-agent.md"), "utf-8");
116
+ assertIncludes(workflowReviewer, ".pi-dev-output/pi-review/md/",
117
+ "workflow/reviewer-agent 写入 .pi-dev-output/pi-review/md/");
118
+
119
+ // ═══════════════════════════════════════════════════════════════
120
+ // 5. review-html SKILL 路径一致性
121
+ // ═══════════════════════════════════════════════════════════════
122
+
123
+ console.log("\n📋 skills/review-html 路径一致性\n");
124
+
125
+ const reviewSkill = fs.readFileSync(
126
+ path.resolve(__dirname, "../skills/review-html/SKILL.md"), "utf-8");
127
+ assertIncludes(reviewSkill, ".pi-dev-output/pi-review/html/",
128
+ "review-html skill 写入 .pi-dev-output/pi-review/html/");
129
+ assertNotIncludes(reviewSkill, ".pi-dev-output/pi-review/ 目录",
130
+ "review-html skill 不再引用旧的 .pi-dev-output/pi-review/ (无子目录)");
131
+
132
+ // ═══════════════════════════════════════════════════════════════
133
+ // 6. 文件名格式验证:saveAnswerFile 和 grillOutputPath 生成的名称含时间戳
134
+ // ═══════════════════════════════════════════════════════════════
135
+
136
+ console.log("\n📋 文件名格式验证\n");
137
+
138
+ // Simulate the formatTimestamp function
139
+ function formatTimestamp() {
140
+ const now = new Date();
141
+ const Y = now.getFullYear().toString();
142
+ const M = (now.getMonth() + 1).toString().padStart(2, "0");
143
+ const D = now.getDate().toString().padStart(2, "0");
144
+ const h = now.getHours().toString().padStart(2, "0");
145
+ const m = now.getMinutes().toString().padStart(2, "0");
146
+ return `${Y}${M}${D}-${h}${m}`;
147
+ }
148
+
149
+ // Verify the format matches YYYYMMDD-HHmm
150
+ const ts = formatTimestamp();
151
+ const formatRegex = /^\d{8}-\d{4}$/;
152
+ assert(formatRegex.test(ts), `formatTimestamp 格式正确 (${ts} 匹配 YYYYMMDD-HHmm)`);
153
+
154
+ // Simulate filename generation
155
+ const id = Date.now().toString(36);
156
+ const answerFilename = `answer-${id}-${ts}.md`;
157
+ const questionsFilename = `questions-${id}-${ts}.json`;
158
+
159
+ assert(answerFilename.startsWith("answer-"), "answer 文件名以 answer- 开头");
160
+ assert(answerFilename.endsWith(".md"), "answer 文件名以 .md 结尾");
161
+ assert(questionsFilename.startsWith("questions-"), "questions 文件名以 questions- 开头");
162
+ assert(questionsFilename.endsWith(".json"), "questions 文件名以 .json 结尾");
163
+
164
+ // ═══════════════════════════════════════════════════════════════
165
+ // Summary
166
+ // ═══════════════════════════════════════════════════════════════
167
+
168
+ console.log(`\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
169
+ console.log(`结果: ${pass} 通过, ${fail} 失败, 共 ${pass + fail} 个测试`);
170
+ console.log(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
171
+
172
+ if (fail > 0) {
173
+ console.error("\n⚠️ 部分测试未通过");
174
+ process.exit(1);
175
+ } else {
176
+ console.log("\n✅ 所有测试通过 — 输出目录结构调整正确");
177
+ }
@@ -0,0 +1,187 @@
1
+ /**
2
+ * test-save-answer-file-workflow.mjs — 验证 saveAnswerFile 在 Workflow 路径中未被跳过
3
+ *
4
+ * 重构后所有 workflow 入口统一经过 promptWorkflowDecision() 函数,
5
+ * 该函数在调用 runWorkflow 前一定会调用 saveAnswerFile。
6
+ *
7
+ * 本测试验证:
8
+ * 1. promptWorkflowDecision 函数体:两个路径(默认/自定义)都在 runWorkflow 前调用了 saveAnswerFile
9
+ * 2. 所有 caller(runWizardWithGrill / runWizard / dev-feat handler)都调用 promptWorkflowDecision
10
+ * 3. 所有非 workflow 路径的 saveAnswerFile 未被移除
11
+ *
12
+ * Run: node tests/test-save-answer-file-workflow.mjs
13
+ */
14
+
15
+ import * as fs from "node:fs";
16
+ import * as path from "node:path";
17
+ import { fileURLToPath } from "node:url";
18
+
19
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
20
+ const SOURCE_PATH = path.resolve(__dirname, "../extensions/dev-prompts.ts");
21
+
22
+ // ── Helpers ──────────────────────────────────────────────────
23
+
24
+ let pass = 0;
25
+ let fail = 0;
26
+
27
+ function assert(condition, msg) {
28
+ if (condition) {
29
+ pass++;
30
+ console.log(` ✅ ${msg}`);
31
+ } else {
32
+ fail++;
33
+ console.error(` ❌ ${msg}`);
34
+ }
35
+ }
36
+
37
+ function assertEq(actual, expected, msg) {
38
+ if (actual === expected) {
39
+ pass++;
40
+ console.log(` ✅ ${msg}`);
41
+ } else {
42
+ fail++;
43
+ console.error(` ❌ ${msg} — 期望 ${JSON.stringify(expected)}, 得到 ${JSON.stringify(actual)}`);
44
+ }
45
+ }
46
+
47
+ // ═══════════════════════════════════════════════════════════════
48
+ // Read source
49
+ // ═══════════════════════════════════════════════════════════════
50
+
51
+ let source;
52
+ try {
53
+ source = fs.readFileSync(SOURCE_PATH, "utf-8");
54
+ } catch (e) {
55
+ console.error(`Failed to read source file: ${e.message}`);
56
+ process.exit(1);
57
+ }
58
+
59
+ console.log(`📄 源文件: ${SOURCE_PATH}`);
60
+ console.log(`📏 文件大小: ${source.length} 字节\n`);
61
+
62
+ // ═══════════════════════════════════════════════════════════════
63
+ // Test 1: promptWorkflowDecision 在 runWorkflow 前调用 saveAnswerFile
64
+ // ═══════════════════════════════════════════════════════════════
65
+
66
+ console.log("📋 Test 1: promptWorkflowDecision 的两个路径都在 runWorkflow 前调用 saveAnswerFile\n");
67
+
68
+ // Find the promptWorkflowDecision function body
69
+ const pwdMatch = source.match(/async function promptWorkflowDecision\([\s\S]*?^\}/m);
70
+ assert(pwdMatch !== null, "应找到 promptWorkflowDecision 函数");
71
+
72
+ const pwdFunc = pwdMatch[0];
73
+ assert(
74
+ pwdFunc.includes("saveAnswerFile(ctx.cwd, finalPrompt)"),
75
+ "promptWorkflowDecision 应包含 saveAnswerFile(finalPrompt) 调用",
76
+ );
77
+
78
+ // Verify the default path (choice "1"): saveAnswerFile before runWorkflow
79
+ const defaultPath = pwdFunc.match(/if \(choice\.startsWith\("1"\)\) \{[\s\S]*?return true;\s*\}/);
80
+ assert(defaultPath !== null, "默认模式路径应存在");
81
+ if (defaultPath) {
82
+ const saveIdx = defaultPath[0].indexOf("saveAnswerFile");
83
+ const runIdx = defaultPath[0].indexOf("runWorkflow");
84
+ assert(saveIdx >= 0 && saveIdx < runIdx, "默认路径应在 runWorkflow 前调用 saveAnswerFile");
85
+ }
86
+
87
+ // Verify the custom path: saveAnswerFile before runWorkflow
88
+ const customPath = pwdFunc.match(/if \(customSteps\.length === 0\) \{[\s\S]*?return true;\s*\}/);
89
+ assert(customPath !== null, "自定义模式路径应存在");
90
+ if (customPath) {
91
+ const saveIdx = customPath[0].indexOf("saveAnswerFile");
92
+ const runIdx = customPath[0].indexOf("runWorkflow");
93
+ assert(saveIdx >= 0 && runIdx >= 0, "自定义路径应包含 saveAnswerFile 和 runWorkflow");
94
+ assert(saveIdx < runIdx, "自定义路径应在 runWorkflow 前调用 saveAnswerFile");
95
+ }
96
+
97
+ // ═══════════════════════════════════════════════════════════════
98
+ // Test 2: 所有 caller 正确调用 promptWorkflowDecision
99
+ // ═══════════════════════════════════════════════════════════════
100
+
101
+ console.log("\n📋 Test 2: 所有 caller 正确调用 promptWorkflowDecision\n");
102
+
103
+ // Count occurrences of promptWorkflowDecision calls in handlers (not definition)
104
+ // We need at least: runWizardWithGrill + runWizard + dev-feat = 3 call sites
105
+ const workflowDecisionCalls = source.match(/await promptWorkflowDecision\(/g);
106
+ assert(workflowDecisionCalls !== null && workflowDecisionCalls.length >= 3,
107
+ `应找到至少 3 个 promptWorkflowDecision 调用,实际 ${workflowDecisionCalls?.length ?? 0}`);
108
+ console.log(` 共 ${workflowDecisionCalls.length} 个 promptWorkflowDecision 调用`);
109
+
110
+ // ═══════════════════════════════════════════════════════════════
111
+ // Test 3: runWizard 的非 workflow 路径调用 saveAnswerFile
112
+ // ═══════════════════════════════════════════════════════════════
113
+
114
+ console.log("\n📋 Test 3: runWizard 非 workflow 路径调用 saveAnswerFile\n");
115
+
116
+ const runWizardMatch = source.match(/async function runWizard\([\s\S]*?\n\}/);
117
+ assert(runWizardMatch !== null, "应找到 runWizard 函数");
118
+
119
+ const runWizardFunc = runWizardMatch[0];
120
+ assert(
121
+ runWizardFunc.includes("saveAnswerFile(ctx.cwd, prompt);"),
122
+ "runWizard 非 workflow 路径应调用 saveAnswerFile(ctx.cwd, prompt)",
123
+ );
124
+
125
+ // ═══════════════════════════════════════════════════════════════
126
+ // Test 4: runWizardWithGrill 的非 workflow 路径保留 saveAnswerFile
127
+ // ═══════════════════════════════════════════════════════════════
128
+
129
+ console.log("\n📋 Test 4: runWizardWithGrill 非 workflow 路径保留 saveAnswerFile\n");
130
+
131
+ const runWizardWithGrillMatch = source.match(/async function runWizardWithGrill\([\s\S]*?\n\}/);
132
+ assert(runWizardWithGrillMatch !== null, "应找到 runWizardWithGrill 函数");
133
+
134
+ const grillFunc = runWizardWithGrillMatch[0];
135
+ assert(
136
+ grillFunc.includes("const answerPath = saveAnswerFile(ctx.cwd, finalPrompt);"),
137
+ "runWizardWithGrill 的非 workflow 路径应包含 answerPath = saveAnswerFile(...)",
138
+ );
139
+
140
+ // ═══════════════════════════════════════════════════════════════
141
+ // Test 5: dev-feat handler 调用 promptWorkflowDecision
142
+ // ═══════════════════════════════════════════════════════════════
143
+
144
+ console.log("\n📋 Test 5: dev-feat handler 调用 promptWorkflowDecision\n");
145
+
146
+ // Find the dev-feat registerCommand block
147
+ const featMatch = source.match(/pi\.registerCommand\("dev-feat"[\s\S]*?\n\t\}\);/);
148
+ assert(featMatch !== null, "应找到 /dev-feat handler");
149
+
150
+ const featHandler = featMatch[0];
151
+ assert(
152
+ featHandler.includes("promptWorkflowDecision(ctx, pi, finalPrompt, FEAT_WORKFLOW_STEPS)"),
153
+ "dev-feat handler 应调用 promptWorkflowDecision 并传递 FEAT_WORKFLOW_STEPS",
154
+ );
155
+
156
+ // ═══════════════════════════════════════════════════════════════
157
+ // Test 6: 所有 saveAnswerFile 调用之前都有 finalPrompt/prompt 已赋值(空安全)
158
+ // ═══════════════════════════════════════════════════════════════
159
+
160
+ console.log("\n📋 Test 6: saveAnswerFile 调用时的参数非空\n");
161
+
162
+ const saveCalls = source.match(/saveAnswerFile\(ctx\.cwd, \w+\)/g);
163
+ assert(saveCalls !== null && saveCalls.length >= 5,
164
+ `应找到至少 5 个 saveAnswerFile 调用,实际 ${saveCalls?.length ?? 0}`);
165
+
166
+ console.log(` 共 ${saveCalls.length} 个 saveAnswerFile 调用`);
167
+ for (const call of saveCalls) {
168
+ assert(
169
+ call.includes("finalPrompt") || call.includes("prompt") || call.includes("recovered"),
170
+ `saveAnswerFile 参数应为 finalPrompt/prompt/recovered,实际: ${call}`,
171
+ );
172
+ }
173
+
174
+ // ═══════════════════════════════════════════════════════════════
175
+ // Summary
176
+ // ═══════════════════════════════════════════════════════════════
177
+
178
+ console.log(`\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
179
+ console.log(`结果: ${pass} 通过, ${fail} 失败, 共 ${pass + fail} 个测试`);
180
+ console.log(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
181
+
182
+ if (fail > 0) {
183
+ console.error("\n⚠️ 部分测试未通过");
184
+ process.exit(1);
185
+ } else {
186
+ console.log("\n✅ 所有测试通过 — saveAnswerFile 在所有 workflow 路径和非 workflow 路径中均被正确调用");
187
+ }