patchwarden 0.6.4 → 1.1.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.
Files changed (212) hide show
  1. package/README.en.md +18 -18
  2. package/README.md +20 -20
  3. package/dist/agents/agentRouter.d.ts +40 -0
  4. package/dist/agents/agentRouter.js +95 -0
  5. package/dist/config.d.ts +1 -1
  6. package/dist/config.js +6 -2
  7. package/dist/controlCenter.js +15 -2
  8. package/dist/doctor.js +32 -1
  9. package/dist/goal/acceptanceEngine.d.ts +62 -0
  10. package/dist/goal/acceptanceEngine.js +103 -0
  11. package/dist/goal/acceptanceTemplate.d.ts +10 -0
  12. package/dist/goal/acceptanceTemplate.js +104 -0
  13. package/dist/goal/goalGraph.d.ts +58 -0
  14. package/dist/goal/goalGraph.js +189 -0
  15. package/dist/goal/goalProgress.d.ts +81 -0
  16. package/dist/goal/goalProgress.js +216 -0
  17. package/dist/goal/goalStatus.d.ts +60 -0
  18. package/dist/goal/goalStatus.js +137 -0
  19. package/dist/goal/goalStore.d.ts +79 -0
  20. package/dist/goal/goalStore.js +211 -0
  21. package/dist/goal/handoffExport.d.ts +34 -0
  22. package/dist/goal/handoffExport.js +183 -0
  23. package/dist/goal/subgoalSync.d.ts +40 -0
  24. package/dist/goal/subgoalSync.js +72 -0
  25. package/dist/goal/worktreeManager.d.ts +96 -0
  26. package/dist/goal/worktreeManager.js +292 -0
  27. package/dist/logging.d.ts +44 -0
  28. package/dist/logging.js +68 -0
  29. package/dist/release/releaseGate.d.ts +99 -0
  30. package/dist/release/releaseGate.js +475 -0
  31. package/dist/runner/postTaskCleanup.d.ts +13 -0
  32. package/dist/runner/postTaskCleanup.js +147 -0
  33. package/dist/runner/runTask.js +50 -10
  34. package/dist/security/discoveryTokenStore.d.ts +62 -0
  35. package/dist/security/discoveryTokenStore.js +100 -0
  36. package/dist/security/toolInvocationGuard.d.ts +35 -0
  37. package/dist/security/toolInvocationGuard.js +153 -0
  38. package/dist/smoke-test.js +18 -10
  39. package/dist/taskRuntime.d.ts +17 -0
  40. package/dist/test/unit/acceptance-engine.test.d.ts +1 -0
  41. package/dist/test/unit/acceptance-engine.test.js +228 -0
  42. package/dist/test/unit/agent-router.test.d.ts +1 -0
  43. package/dist/test/unit/agent-router.test.js +287 -0
  44. package/dist/test/unit/audit-checks.test.d.ts +1 -0
  45. package/dist/test/unit/audit-checks.test.js +350 -0
  46. package/dist/test/unit/diagnose-task.test.d.ts +1 -0
  47. package/dist/test/unit/diagnose-task.test.js +457 -0
  48. package/dist/test/unit/discovery-token-store.test.d.ts +1 -0
  49. package/dist/test/unit/discovery-token-store.test.js +139 -0
  50. package/dist/test/unit/goal-graph.test.d.ts +1 -0
  51. package/dist/test/unit/goal-graph.test.js +298 -0
  52. package/dist/test/unit/goal-progress.test.d.ts +1 -0
  53. package/dist/test/unit/goal-progress.test.js +381 -0
  54. package/dist/test/unit/goal-status.test.d.ts +1 -0
  55. package/dist/test/unit/goal-status.test.js +215 -0
  56. package/dist/test/unit/goal-store.test.d.ts +1 -0
  57. package/dist/test/unit/goal-store.test.js +253 -0
  58. package/dist/test/unit/goal-subgoal-task.test.d.ts +1 -0
  59. package/dist/test/unit/goal-subgoal-task.test.js +55 -0
  60. package/dist/test/unit/goal-tools-registry.test.d.ts +1 -0
  61. package/dist/test/unit/goal-tools-registry.test.js +190 -0
  62. package/dist/test/unit/handoff-export.test.d.ts +1 -0
  63. package/dist/test/unit/handoff-export.test.js +263 -0
  64. package/dist/test/unit/invoke-discovered-tool.test.d.ts +1 -0
  65. package/dist/test/unit/invoke-discovered-tool.test.js +167 -0
  66. package/dist/test/unit/logging.test.js +127 -5
  67. package/dist/test/unit/post-task-cleanup.test.d.ts +1 -0
  68. package/dist/test/unit/post-task-cleanup.test.js +48 -0
  69. package/dist/test/unit/reconcile-tasks.test.d.ts +1 -0
  70. package/dist/test/unit/reconcile-tasks.test.js +456 -0
  71. package/dist/test/unit/release-gate.test.d.ts +1 -0
  72. package/dist/test/unit/release-gate.test.js +242 -0
  73. package/dist/test/unit/safe-views.test.d.ts +1 -0
  74. package/dist/test/unit/safe-views.test.js +171 -0
  75. package/dist/test/unit/schema-drift-check.test.d.ts +1 -0
  76. package/dist/test/unit/schema-drift-check.test.js +175 -0
  77. package/dist/test/unit/subgoal-sync.test.d.ts +1 -0
  78. package/dist/test/unit/subgoal-sync.test.js +183 -0
  79. package/dist/test/unit/tool-invocation-guard.test.d.ts +1 -0
  80. package/dist/test/unit/tool-invocation-guard.test.js +432 -0
  81. package/dist/test/unit/tool-usage-stats.test.d.ts +1 -0
  82. package/dist/test/unit/tool-usage-stats.test.js +300 -0
  83. package/dist/test/unit/toolSearch.test.d.ts +1 -0
  84. package/dist/test/unit/toolSearch.test.js +571 -0
  85. package/dist/test/unit/worktree-manager.test.d.ts +1 -0
  86. package/dist/test/unit/worktree-manager.test.js +176 -0
  87. package/dist/tools/auditTask.d.ts +103 -1
  88. package/dist/tools/auditTask.js +461 -8
  89. package/dist/tools/cancelTask.js +1 -1
  90. package/dist/tools/checkReleaseGate.d.ts +21 -0
  91. package/dist/tools/checkReleaseGate.js +22 -0
  92. package/dist/tools/createTask.d.ts +18 -2
  93. package/dist/tools/createTask.js +29 -1
  94. package/dist/tools/diagnoseTask.d.ts +45 -0
  95. package/dist/tools/diagnoseTask.js +347 -0
  96. package/dist/tools/discardWorktree.d.ts +24 -0
  97. package/dist/tools/discardWorktree.js +27 -0
  98. package/dist/tools/discoverTools.d.ts +11 -0
  99. package/dist/tools/discoverTools.js +39 -0
  100. package/dist/tools/explainTool.d.ts +11 -0
  101. package/dist/tools/explainTool.js +21 -0
  102. package/dist/tools/getTaskSummary.js +2 -1
  103. package/dist/tools/goalSubgoalTask.d.ts +51 -0
  104. package/dist/tools/goalSubgoalTask.js +110 -0
  105. package/dist/tools/healthCheck.d.ts +1 -0
  106. package/dist/tools/healthCheck.js +5 -1
  107. package/dist/tools/invokeDiscoveredTool.d.ts +37 -0
  108. package/dist/tools/invokeDiscoveredTool.js +112 -0
  109. package/dist/tools/listTasks.d.ts +3 -1
  110. package/dist/tools/listTasks.js +14 -1
  111. package/dist/tools/mergeWorktree.d.ts +24 -0
  112. package/dist/tools/mergeWorktree.js +27 -0
  113. package/dist/tools/reconcileTasks.d.ts +41 -0
  114. package/dist/tools/reconcileTasks.js +352 -0
  115. package/dist/tools/registry.js +550 -0
  116. package/dist/tools/safeStatus.d.ts +31 -1
  117. package/dist/tools/safeStatus.js +43 -1
  118. package/dist/tools/safeViews.d.ts +256 -0
  119. package/dist/tools/safeViews.js +250 -0
  120. package/dist/tools/schemaDriftCheck.d.ts +46 -0
  121. package/dist/tools/schemaDriftCheck.js +80 -0
  122. package/dist/tools/toolCatalog.d.ts +4 -3
  123. package/dist/tools/toolCatalog.js +33 -11
  124. package/dist/tools/toolRegistry.d.ts +61 -0
  125. package/dist/tools/toolRegistry.js +724 -0
  126. package/dist/tools/toolSearch.d.ts +110 -0
  127. package/dist/tools/toolSearch.js +331 -0
  128. package/dist/tools/toolUsageStats.d.ts +41 -0
  129. package/dist/tools/toolUsageStats.js +116 -0
  130. package/dist/tools/waitForTask.js +1 -0
  131. package/dist/version.d.ts +2 -2
  132. package/dist/version.js +2 -2
  133. package/dist/watcherStatus.d.ts +29 -0
  134. package/dist/watcherStatus.js +92 -1
  135. package/package.json +3 -2
  136. package/scripts/checks/control-smoke.js +2 -2
  137. package/scripts/checks/doctor-smoke.js +1 -0
  138. package/scripts/checks/lifecycle-smoke.js +13 -9
  139. package/scripts/checks/mcp-manifest-check.js +53 -6
  140. package/scripts/checks/mcp-smoke.js +36 -9
  141. package/scripts/checks/watcher-supervisor-smoke.js +2 -2
  142. package/src/agents/agentRouter.ts +149 -0
  143. package/src/config.ts +9 -3
  144. package/src/controlCenter.ts +17 -3
  145. package/src/doctor.ts +36 -1
  146. package/src/goal/acceptanceEngine.ts +160 -0
  147. package/src/goal/acceptanceTemplate.ts +121 -0
  148. package/src/goal/goalGraph.ts +225 -0
  149. package/src/goal/goalProgress.ts +301 -0
  150. package/src/goal/goalStatus.ts +234 -0
  151. package/src/goal/goalStore.ts +306 -0
  152. package/src/goal/handoffExport.ts +211 -0
  153. package/src/goal/subgoalSync.ts +82 -0
  154. package/src/goal/worktreeManager.ts +404 -0
  155. package/src/logging.ts +91 -0
  156. package/src/release/releaseGate.ts +567 -0
  157. package/src/runner/postTaskCleanup.ts +154 -0
  158. package/src/runner/runTask.ts +49 -10
  159. package/src/security/discoveryTokenStore.ts +157 -0
  160. package/src/security/toolInvocationGuard.ts +251 -0
  161. package/src/smoke-test.ts +15 -7
  162. package/src/taskRuntime.ts +17 -0
  163. package/src/test/unit/acceptance-engine.test.ts +261 -0
  164. package/src/test/unit/agent-router.test.ts +342 -0
  165. package/src/test/unit/audit-checks.test.ts +567 -0
  166. package/src/test/unit/diagnose-task.test.ts +544 -0
  167. package/src/test/unit/discovery-token-store.test.ts +181 -0
  168. package/src/test/unit/goal-graph.test.ts +347 -0
  169. package/src/test/unit/goal-progress.test.ts +538 -0
  170. package/src/test/unit/goal-status.test.ts +270 -0
  171. package/src/test/unit/goal-store.test.ts +318 -0
  172. package/src/test/unit/goal-subgoal-task.test.ts +72 -0
  173. package/src/test/unit/goal-tools-registry.test.ts +243 -0
  174. package/src/test/unit/handoff-export.test.ts +295 -0
  175. package/src/test/unit/invoke-discovered-tool.test.ts +216 -0
  176. package/src/test/unit/logging.test.ts +177 -5
  177. package/src/test/unit/post-task-cleanup.test.ts +53 -0
  178. package/src/test/unit/reconcile-tasks.test.ts +551 -0
  179. package/src/test/unit/release-gate.test.ts +314 -0
  180. package/src/test/unit/safe-views.test.ts +184 -0
  181. package/src/test/unit/schema-drift-check.test.ts +258 -0
  182. package/src/test/unit/subgoal-sync.test.ts +236 -0
  183. package/src/test/unit/tool-invocation-guard.test.ts +542 -0
  184. package/src/test/unit/tool-usage-stats.test.ts +384 -0
  185. package/src/test/unit/toolSearch.test.ts +756 -0
  186. package/src/test/unit/worktree-manager.test.ts +247 -0
  187. package/src/tools/auditTask.ts +831 -402
  188. package/src/tools/cancelTask.ts +1 -1
  189. package/src/tools/checkReleaseGate.ts +45 -0
  190. package/src/tools/createTask.ts +64 -6
  191. package/src/tools/diagnoseTask.ts +460 -0
  192. package/src/tools/discardWorktree.ts +42 -0
  193. package/src/tools/discoverTools.ts +51 -0
  194. package/src/tools/explainTool.ts +26 -0
  195. package/src/tools/getTaskSummary.ts +2 -1
  196. package/src/tools/goalSubgoalTask.ts +170 -0
  197. package/src/tools/healthCheck.ts +4 -1
  198. package/src/tools/invokeDiscoveredTool.ts +163 -0
  199. package/src/tools/listTasks.ts +16 -2
  200. package/src/tools/mergeWorktree.ts +42 -0
  201. package/src/tools/reconcileTasks.ts +464 -0
  202. package/src/tools/registry.ts +618 -1
  203. package/src/tools/safeStatus.ts +73 -2
  204. package/src/tools/safeViews.ts +271 -0
  205. package/src/tools/schemaDriftCheck.ts +120 -0
  206. package/src/tools/toolCatalog.ts +34 -11
  207. package/src/tools/toolRegistry.ts +786 -0
  208. package/src/tools/toolSearch.ts +464 -0
  209. package/src/tools/toolUsageStats.ts +151 -0
  210. package/src/tools/waitForTask.ts +1 -0
  211. package/src/version.ts +2 -2
  212. package/src/watcherStatus.ts +97 -1
@@ -0,0 +1,228 @@
1
+ import { describe, it } from "node:test";
2
+ import { strict as assert } from "node:assert";
3
+ import { evaluateAcceptance, verdictToStatus, } from "../../goal/acceptanceEngine.js";
4
+ import { renderAcceptanceMarkdown } from "../../goal/acceptanceTemplate.js";
5
+ // ── Helpers ───────────────────────────────────────────────────────
6
+ function makeEvidence(overrides = {}) {
7
+ return {
8
+ task_id: "task_test_001",
9
+ task_status: "done_by_agent",
10
+ result_md_exists: true,
11
+ result_json_exists: true,
12
+ verify_json_exists: true,
13
+ test_log_exists: true,
14
+ git_diff_exists: true,
15
+ verify_status: "passed",
16
+ new_out_of_scope_changes: [],
17
+ goal: null,
18
+ scope: null,
19
+ forbidden: null,
20
+ verification: null,
21
+ done_evidence: null,
22
+ artifact_status: "collected",
23
+ release_claims_unverified: false,
24
+ checks: [
25
+ { name: "task_status", result: "pass", detail: "Task status is done_by_agent." },
26
+ { name: "result_md_exists", result: "pass", detail: "result.md found." },
27
+ { name: "verify_status", result: "pass", detail: "Verification passed." },
28
+ ],
29
+ ...overrides,
30
+ };
31
+ }
32
+ // ── Tests ─────────────────────────────────────────────────────────
33
+ describe("acceptanceEngine", () => {
34
+ describe("evaluateAcceptance — 基础场景", () => {
35
+ it("所有检查通过 → ACCEPTED", () => {
36
+ const evidence = makeEvidence();
37
+ const result = evaluateAcceptance(evidence);
38
+ assert.equal(result.verdict, "ACCEPTED");
39
+ assert.equal(result.acceptance_status, "accepted");
40
+ assert.equal(result.fail_checks.length, 0);
41
+ assert.equal(result.warn_checks.length, 0);
42
+ assert.ok(result.reason.includes("All acceptance checks passed"));
43
+ });
44
+ it("存在 fail 级检查项 → REJECTED", () => {
45
+ const evidence = makeEvidence({
46
+ checks: [
47
+ { name: "task_status", result: "pass", detail: "ok" },
48
+ { name: "result_md_exists", result: "fail", detail: "result.md is missing." },
49
+ { name: "verify_status", result: "pass", detail: "ok" },
50
+ ],
51
+ });
52
+ const result = evaluateAcceptance(evidence);
53
+ assert.equal(result.verdict, "REJECTED");
54
+ assert.equal(result.acceptance_status, "rejected");
55
+ assert.equal(result.fail_checks.length, 1);
56
+ assert.equal(result.fail_checks[0].name, "result_md_exists");
57
+ assert.ok(result.reason.includes("fail-level"));
58
+ });
59
+ it("存在 warn 级检查项(无 fail)→ NEEDS_FIX", () => {
60
+ const evidence = makeEvidence({
61
+ checks: [
62
+ { name: "task_status", result: "pass", detail: "ok" },
63
+ { name: "git_diff_exists", result: "warn", detail: "git.diff is missing." },
64
+ { name: "test_log_exists", result: "warn", detail: "test.log is missing." },
65
+ ],
66
+ });
67
+ const result = evaluateAcceptance(evidence);
68
+ assert.equal(result.verdict, "NEEDS_FIX");
69
+ assert.equal(result.acceptance_status, "needs_fix");
70
+ assert.equal(result.warn_checks.length, 2);
71
+ assert.ok(result.reason.includes("warn-level"));
72
+ });
73
+ it("存在 release claims 但无法验证 → BLOCKED_BY_APPROVAL(即使有 warn)", () => {
74
+ const evidence = makeEvidence({
75
+ release_claims_unverified: true,
76
+ checks: [
77
+ { name: "task_status", result: "pass", detail: "ok" },
78
+ { name: "release_claims_unverified", result: "warn", detail: "Found release claims." },
79
+ ],
80
+ });
81
+ const result = evaluateAcceptance(evidence);
82
+ assert.equal(result.verdict, "BLOCKED_BY_APPROVAL");
83
+ assert.equal(result.acceptance_status, "blocked");
84
+ assert.ok(result.reason.includes("Release claims"));
85
+ assert.ok(result.required_evidence.some((e) => e.includes("Verify remote")));
86
+ });
87
+ it("fail 级检查项优先于 release claims", () => {
88
+ const evidence = makeEvidence({
89
+ release_claims_unverified: true,
90
+ checks: [
91
+ { name: "task_status", result: "fail", detail: "Task failed." },
92
+ { name: "release_claims_unverified", result: "warn", detail: "Found release claims." },
93
+ ],
94
+ });
95
+ const result = evaluateAcceptance(evidence);
96
+ assert.equal(result.verdict, "REJECTED");
97
+ assert.equal(result.acceptance_status, "rejected");
98
+ });
99
+ it("多个 fail 级检查项全部收集", () => {
100
+ const evidence = makeEvidence({
101
+ checks: [
102
+ { name: "result_md_exists", result: "fail", detail: "missing" },
103
+ { name: "verify_status", result: "fail", detail: "failed" },
104
+ { name: "scope_changes", result: "fail", detail: "out of scope" },
105
+ ],
106
+ });
107
+ const result = evaluateAcceptance(evidence);
108
+ assert.equal(result.verdict, "REJECTED");
109
+ assert.equal(result.fail_checks.length, 3);
110
+ });
111
+ });
112
+ describe("verdictToStatus", () => {
113
+ it("ACCEPTED → accepted", () => {
114
+ assert.equal(verdictToStatus("ACCEPTED"), "accepted");
115
+ });
116
+ it("REJECTED → rejected", () => {
117
+ assert.equal(verdictToStatus("REJECTED"), "rejected");
118
+ });
119
+ it("NEEDS_FIX → needs_fix", () => {
120
+ assert.equal(verdictToStatus("NEEDS_FIX"), "needs_fix");
121
+ });
122
+ it("BLOCKED_BY_APPROVAL → blocked", () => {
123
+ assert.equal(verdictToStatus("BLOCKED_BY_APPROVAL"), "blocked");
124
+ });
125
+ });
126
+ describe("evaluateAcceptance — next_suggested_task", () => {
127
+ it("ACCEPTED 的 next_suggested_task 包含 'accepted'", () => {
128
+ const result = evaluateAcceptance(makeEvidence());
129
+ assert.ok(result.next_suggested_task.includes("accepted"));
130
+ });
131
+ it("REJECTED 的 next_suggested_task 包含 fail 检查名", () => {
132
+ const result = evaluateAcceptance(makeEvidence({
133
+ checks: [{ name: "result_md_exists", result: "fail", detail: "missing" }],
134
+ }));
135
+ assert.ok(result.next_suggested_task.includes("result_md_exists"));
136
+ });
137
+ it("NEEDS_FIX 的 next_suggested_task 包含 warn 检查名", () => {
138
+ const result = evaluateAcceptance(makeEvidence({
139
+ checks: [{ name: "git_diff_exists", result: "warn", detail: "missing" }],
140
+ }));
141
+ assert.ok(result.next_suggested_task.includes("git_diff_exists"));
142
+ });
143
+ it("BLOCKED_BY_APPROVAL 的 next_suggested_task 包含 'verify'", () => {
144
+ const result = evaluateAcceptance(makeEvidence({ release_claims_unverified: true }));
145
+ assert.ok(result.next_suggested_task.toLowerCase().includes("verify"));
146
+ });
147
+ });
148
+ describe("evaluateAcceptance — reasons 可追溯", () => {
149
+ it("REJECTED 的 reasons 包含 [FAIL] 前缀", () => {
150
+ const result = evaluateAcceptance(makeEvidence({
151
+ checks: [{ name: "result_md_exists", result: "fail", detail: "missing" }],
152
+ }));
153
+ assert.ok(result.reasons.some((r) => r.includes("[FAIL]")));
154
+ });
155
+ it("NEEDS_FIX 的 reasons 包含 [WARN] 前缀", () => {
156
+ const result = evaluateAcceptance(makeEvidence({
157
+ checks: [{ name: "git_diff_exists", result: "warn", detail: "missing" }],
158
+ }));
159
+ assert.ok(result.reasons.some((r) => r.includes("[WARN]")));
160
+ });
161
+ it("BLOCKED_BY_APPROVAL 的 reasons 包含 [BLOCKED] 前缀", () => {
162
+ const result = evaluateAcceptance(makeEvidence({ release_claims_unverified: true }));
163
+ assert.ok(result.reasons.some((r) => r.includes("[BLOCKED]")));
164
+ });
165
+ it("ACCEPTED 的 reasons 非空", () => {
166
+ const result = evaluateAcceptance(makeEvidence());
167
+ assert.ok(result.reasons.length > 0);
168
+ });
169
+ });
170
+ describe("renderAcceptanceMarkdown", () => {
171
+ it("生成包含 verdict 的 ACCEPTANCE.md 内容", () => {
172
+ const evidence = makeEvidence();
173
+ const result = evaluateAcceptance(evidence);
174
+ const md = renderAcceptanceMarkdown("task_test_001", result, evidence);
175
+ assert.ok(md.includes("# Acceptance Report — task_test_001"));
176
+ assert.ok(md.includes("**ACCEPTED**"));
177
+ assert.ok(md.includes("## Evidence Summary"));
178
+ assert.ok(md.includes("## Verdict"));
179
+ });
180
+ it("REJECTED 的 Markdown 包含 fail 检查", () => {
181
+ const evidence = makeEvidence({
182
+ checks: [{ name: "result_md_exists", result: "fail", detail: "missing" }],
183
+ });
184
+ const result = evaluateAcceptance(evidence);
185
+ const md = renderAcceptanceMarkdown("task_test_002", result, evidence);
186
+ assert.ok(md.includes("**REJECTED**"));
187
+ assert.ok(md.includes("## Fail-Level Checks"));
188
+ assert.ok(md.includes("result_md_exists"));
189
+ });
190
+ it("NEEDS_FIX 的 Markdown 包含 warn 检查", () => {
191
+ const evidence = makeEvidence({
192
+ checks: [{ name: "git_diff_exists", result: "warn", detail: "missing" }],
193
+ });
194
+ const result = evaluateAcceptance(evidence);
195
+ const md = renderAcceptanceMarkdown("task_test_003", result, evidence);
196
+ assert.ok(md.includes("**NEEDS_FIX**"));
197
+ assert.ok(md.includes("## Warn-Level Checks"));
198
+ });
199
+ it("BLOCKED 的 Markdown 包含 required_evidence", () => {
200
+ const evidence = makeEvidence({ release_claims_unverified: true });
201
+ const result = evaluateAcceptance(evidence);
202
+ const md = renderAcceptanceMarkdown("task_test_004", result, evidence);
203
+ assert.ok(md.includes("**BLOCKED_BY_APPROVAL**"));
204
+ assert.ok(md.includes("## Required Evidence"));
205
+ });
206
+ it("有 goal/scope 时显示 Task Acceptance Criteria", () => {
207
+ const evidence = makeEvidence({
208
+ goal: "Implement feature X",
209
+ scope: ["src/feature.ts"],
210
+ forbidden: [".env"],
211
+ verification: ["npm test"],
212
+ done_evidence: ["result.md", "test.log"],
213
+ });
214
+ const result = evaluateAcceptance(evidence);
215
+ const md = renderAcceptanceMarkdown("task_test_005", result, evidence);
216
+ assert.ok(md.includes("## Task Acceptance Criteria"));
217
+ assert.ok(md.includes("Implement feature X"));
218
+ assert.ok(md.includes("src/feature.ts"));
219
+ assert.ok(md.includes(".env"));
220
+ });
221
+ it("无 goal/scope 时不显示 Task Acceptance Criteria section", () => {
222
+ const evidence = makeEvidence();
223
+ const result = evaluateAcceptance(evidence);
224
+ const md = renderAcceptanceMarkdown("task_test_006", result, evidence);
225
+ assert.ok(!md.includes("## Task Acceptance Criteria"));
226
+ });
227
+ });
228
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,287 @@
1
+ import { describe, it } from "node:test";
2
+ import { strict as assert } from "node:assert";
3
+ import { routeAgent, AGENT_ROUTING_RULES, } from "../../agents/agentRouter.js";
4
+ import { PatchWardenError } from "../../errors.js";
5
+ // ── Helpers ───────────────────────────────────────────────────────
6
+ const ALL_AGENTS = ["codex", "opencode", "claude", "patchwarden-direct", "patchwarden-audit"];
7
+ function scopeFiles(count) {
8
+ return Array.from({ length: count }, (_, i) => `file_${i}.ts`);
9
+ }
10
+ // ── Tests ─────────────────────────────────────────────────────────
11
+ describe("routeAgent", () => {
12
+ // ── 1. 大规模代码修改 → opencode ────────────────────────────────
13
+ it("scope 15 个文件 → opencode, fallback=false, reason 含 'large scope'", () => {
14
+ const result = routeAgent({
15
+ goal: "大规模修改",
16
+ scope: scopeFiles(15),
17
+ configuredAgents: ALL_AGENTS,
18
+ });
19
+ assert.equal(result.recommended_agent, "opencode");
20
+ assert.equal(result.fallback, false);
21
+ assert.ok(result.reason.includes("large scope"), `reason should contain 'large scope', got: ${result.reason}`);
22
+ assert.ok(result.reason.includes("15"), `reason should contain file count '15', got: ${result.reason}`);
23
+ assert.ok(ALL_AGENTS.includes(result.recommended_agent));
24
+ });
25
+ // ── 2. 单文件修改 → patchwarden-direct ──────────────────────────
26
+ it("scope 1 个文件 → patchwarden-direct, fallback=false", () => {
27
+ const result = routeAgent({
28
+ goal: "修复一个 bug",
29
+ scope: ["src/index.ts"],
30
+ configuredAgents: ALL_AGENTS,
31
+ });
32
+ assert.equal(result.recommended_agent, "patchwarden-direct");
33
+ assert.equal(result.fallback, false);
34
+ assert.ok(ALL_AGENTS.includes(result.recommended_agent));
35
+ });
36
+ // ── 3. 文档整理 → claude ────────────────────────────────────────
37
+ it("goal 含 '更新 README 文档' → claude(若 configured)", () => {
38
+ const result = routeAgent({
39
+ goal: "更新 README 文档",
40
+ configuredAgents: ALL_AGENTS,
41
+ });
42
+ assert.equal(result.recommended_agent, "claude");
43
+ assert.equal(result.fallback, false);
44
+ assert.ok(result.reason.includes("documentation"), `reason should contain 'documentation', got: ${result.reason}`);
45
+ assert.ok(ALL_AGENTS.includes(result.recommended_agent));
46
+ });
47
+ it("goal 含文档关键词但 claude 未配置 → fallback 到 opencode", () => {
48
+ const configured = ["codex", "opencode"];
49
+ const result = routeAgent({
50
+ goal: "更新 README 文档",
51
+ configuredAgents: configured,
52
+ });
53
+ assert.equal(result.recommended_agent, "opencode");
54
+ assert.equal(result.fallback, true);
55
+ assert.ok(configured.includes(result.recommended_agent));
56
+ });
57
+ it("goal 含文档关键词但 claude 和 opencode 都未配置 → fallback 到第一个", () => {
58
+ const configured = ["codex"];
59
+ const result = routeAgent({
60
+ goal: "更新 README 文档",
61
+ configuredAgents: configured,
62
+ });
63
+ assert.equal(result.recommended_agent, "codex");
64
+ assert.equal(result.fallback, true);
65
+ assert.ok(configured.includes(result.recommended_agent));
66
+ });
67
+ // ── 4. 复杂推理和重构 → codex ───────────────────────────────────
68
+ it("goal 含 '跨模块重构' → codex", () => {
69
+ const result = routeAgent({
70
+ goal: "跨模块重构认证模块",
71
+ configuredAgents: ALL_AGENTS,
72
+ });
73
+ assert.equal(result.recommended_agent, "codex");
74
+ assert.equal(result.fallback, false);
75
+ assert.ok(result.reason.includes("refactor"), `reason should contain 'refactor', got: ${result.reason}`);
76
+ assert.ok(ALL_AGENTS.includes(result.recommended_agent));
77
+ });
78
+ it("goal 含 'redesign' → codex", () => {
79
+ const result = routeAgent({
80
+ goal: "Redesign the data layer",
81
+ configuredAgents: ALL_AGENTS,
82
+ });
83
+ assert.equal(result.recommended_agent, "codex");
84
+ assert.equal(result.fallback, false);
85
+ assert.ok(ALL_AGENTS.includes(result.recommended_agent));
86
+ });
87
+ // ── 5. 验收审计 → patchwarden-audit ─────────────────────────────
88
+ it("goal 含 '审计任务' → patchwarden-audit", () => {
89
+ const result = routeAgent({
90
+ goal: "执行验收审计任务",
91
+ configuredAgents: ALL_AGENTS,
92
+ });
93
+ assert.equal(result.recommended_agent, "patchwarden-audit");
94
+ assert.equal(result.fallback, false);
95
+ assert.ok(result.reason.includes("audit"), `reason should contain 'audit', got: ${result.reason}`);
96
+ assert.ok(ALL_AGENTS.includes(result.recommended_agent));
97
+ });
98
+ it("goal 含 'verify' → patchwarden-audit", () => {
99
+ const result = routeAgent({
100
+ goal: "verify the release artifacts",
101
+ configuredAgents: ALL_AGENTS,
102
+ });
103
+ assert.equal(result.recommended_agent, "patchwarden-audit");
104
+ assert.equal(result.fallback, false);
105
+ assert.ok(ALL_AGENTS.includes(result.recommended_agent));
106
+ });
107
+ // ── 6. 无匹配 → fallback 到 configuredAgents[0] ────────────────
108
+ it("无匹配(goal='普通任务', scope=['a.ts','b.ts'])→ fallback=true, recommended_agent=configuredAgents[0]", () => {
109
+ const configured = ["codex", "opencode"];
110
+ const result = routeAgent({
111
+ goal: "普通任务",
112
+ scope: ["a.ts", "b.ts"],
113
+ configuredAgents: configured,
114
+ });
115
+ assert.equal(result.recommended_agent, configured[0]);
116
+ assert.equal(result.fallback, true);
117
+ assert.ok(configured.includes(result.recommended_agent));
118
+ });
119
+ // ── 7. 白名单约束:推荐结果必须在 configuredAgents 中 ──────────
120
+ it("所有场景断言 result.recommended_agent 在 configuredAgents 中", () => {
121
+ const configured = ["codex", "opencode"];
122
+ const inputs = [
123
+ { goal: "大规模修改", scope: scopeFiles(15), configuredAgents: configured },
124
+ { goal: "单文件", scope: ["a.ts"], configuredAgents: configured },
125
+ { goal: "审计任务", configuredAgents: configured },
126
+ { goal: "重构模块", configuredAgents: configured },
127
+ { goal: "更新文档", configuredAgents: configured },
128
+ { goal: "普通任务", scope: ["a.ts", "b.ts"], configuredAgents: configured },
129
+ ];
130
+ for (const input of inputs) {
131
+ const result = routeAgent(input);
132
+ assert.ok(configured.includes(result.recommended_agent), `recommended_agent "${result.recommended_agent}" not in configuredAgents [${configured.join(", ")}] for goal "${input.goal}"`);
133
+ }
134
+ });
135
+ it("白名单约束:opencode 未配置时大规模 scope 回退到第一个 agent", () => {
136
+ const configured = ["codex"];
137
+ const result = routeAgent({
138
+ goal: "大规模修改",
139
+ scope: scopeFiles(15),
140
+ configuredAgents: configured,
141
+ });
142
+ assert.equal(result.recommended_agent, "codex");
143
+ assert.equal(result.fallback, true);
144
+ assert.ok(result.reason.includes("fallback"), `reason should contain 'fallback', got: ${result.reason}`);
145
+ assert.ok(configured.includes(result.recommended_agent));
146
+ });
147
+ it("白名单约束:patchwarden-direct 未配置时单文件 scope 回退", () => {
148
+ const configured = ["codex", "opencode"];
149
+ const result = routeAgent({
150
+ goal: "修复 bug",
151
+ scope: ["a.ts"],
152
+ configuredAgents: configured,
153
+ });
154
+ assert.equal(result.recommended_agent, "codex");
155
+ assert.equal(result.fallback, true);
156
+ assert.ok(configured.includes(result.recommended_agent));
157
+ });
158
+ it("白名单约束:patchwarden-audit 未配置时审计任务回退", () => {
159
+ const configured = ["codex", "opencode"];
160
+ const result = routeAgent({
161
+ goal: "审计任务",
162
+ configuredAgents: configured,
163
+ });
164
+ assert.equal(result.recommended_agent, "codex");
165
+ assert.equal(result.fallback, true);
166
+ assert.ok(configured.includes(result.recommended_agent));
167
+ });
168
+ it("白名单约束:codex 未配置时重构任务回退", () => {
169
+ const configured = ["opencode"];
170
+ const result = routeAgent({
171
+ goal: "跨模块重构",
172
+ configuredAgents: configured,
173
+ });
174
+ assert.equal(result.recommended_agent, "opencode");
175
+ assert.equal(result.fallback, true);
176
+ assert.ok(configured.includes(result.recommended_agent));
177
+ });
178
+ // ── 8. configuredAgents 为空 → 抛错 ────────────────────────────
179
+ it("configuredAgents 为空 → 抛 PatchWardenError(no_agent_configured)", () => {
180
+ assert.throws(() => routeAgent({ goal: "任意任务", configuredAgents: [] }), (err) => {
181
+ assert.ok(err instanceof PatchWardenError, "err should be PatchWardenError");
182
+ assert.equal(err.reason, "no_agent_configured");
183
+ return true;
184
+ });
185
+ });
186
+ // ── 9. 路由规则优先级验证 ───────────────────────────────────────
187
+ it("优先级:scope > 10 优先于文档关键词", () => {
188
+ // scope 15 个文件 + goal 含文档关键词 → opencode(规则1 优先于规则5)
189
+ const result = routeAgent({
190
+ goal: "更新文档",
191
+ scope: scopeFiles(15),
192
+ configuredAgents: ALL_AGENTS,
193
+ });
194
+ assert.equal(result.recommended_agent, "opencode");
195
+ assert.equal(result.fallback, false);
196
+ });
197
+ it("优先级:scope === 1 优先于审计关键词", () => {
198
+ // scope 1 个文件 + goal 含审计关键词 → patchwarden-direct(规则2 优先于规则3)
199
+ const result = routeAgent({
200
+ goal: "审计任务",
201
+ scope: ["a.ts"],
202
+ configuredAgents: ALL_AGENTS,
203
+ });
204
+ assert.equal(result.recommended_agent, "patchwarden-direct");
205
+ assert.equal(result.fallback, false);
206
+ });
207
+ it("优先级:审计关键词优先于重构关键词", () => {
208
+ // goal 同时含审计和重构关键词 → patchwarden-audit(规则3 优先于规则4)
209
+ const result = routeAgent({
210
+ goal: "审计重构模块",
211
+ configuredAgents: ALL_AGENTS,
212
+ });
213
+ assert.equal(result.recommended_agent, "patchwarden-audit");
214
+ assert.equal(result.fallback, false);
215
+ });
216
+ it("优先级:重构关键词优先于文档关键词", () => {
217
+ // goal 同时含重构和文档关键词 → codex(规则4 优先于规则5)
218
+ const result = routeAgent({
219
+ goal: "重构文档模块",
220
+ configuredAgents: ALL_AGENTS,
221
+ });
222
+ assert.equal(result.recommended_agent, "codex");
223
+ assert.equal(result.fallback, false);
224
+ });
225
+ // ── 10. inline_plan 文本特征匹配 ────────────────────────────────
226
+ it("inline_plan 含重构关键词 → codex", () => {
227
+ const result = routeAgent({
228
+ goal: "改进代码",
229
+ inline_plan: "需要 refactor 数据访问层",
230
+ configuredAgents: ALL_AGENTS,
231
+ });
232
+ assert.equal(result.recommended_agent, "codex");
233
+ assert.equal(result.fallback, false);
234
+ });
235
+ it("inline_plan 含审计关键词 → patchwarden-audit", () => {
236
+ const result = routeAgent({
237
+ goal: "检查工作",
238
+ inline_plan: "执行 audit 验收流程",
239
+ configuredAgents: ALL_AGENTS,
240
+ });
241
+ assert.equal(result.recommended_agent, "patchwarden-audit");
242
+ assert.equal(result.fallback, false);
243
+ });
244
+ // ── 11. AGENT_ROUTING_RULES 常量结构断言 ────────────────────────
245
+ it("AGENT_ROUTING_RULES 导出完整路由规则结构", () => {
246
+ assert.equal(AGENT_ROUTING_RULES.largeScope.agent, "opencode");
247
+ assert.equal(AGENT_ROUTING_RULES.largeScope.threshold, 10);
248
+ assert.equal(AGENT_ROUTING_RULES.singleFile.agent, "patchwarden-direct");
249
+ assert.equal(AGENT_ROUTING_RULES.audit.agent, "patchwarden-audit");
250
+ assert.ok(AGENT_ROUTING_RULES.audit.keywords.includes("审计"));
251
+ assert.ok(AGENT_ROUTING_RULES.audit.keywords.includes("audit"));
252
+ assert.ok(AGENT_ROUTING_RULES.audit.keywords.includes("验收"));
253
+ assert.ok(AGENT_ROUTING_RULES.audit.keywords.includes("verify"));
254
+ assert.equal(AGENT_ROUTING_RULES.refactor.agent, "codex");
255
+ assert.ok(AGENT_ROUTING_RULES.refactor.keywords.includes("重构"));
256
+ assert.ok(AGENT_ROUTING_RULES.refactor.keywords.includes("refactor"));
257
+ assert.ok(AGENT_ROUTING_RULES.refactor.keywords.includes("跨模块"));
258
+ assert.ok(AGENT_ROUTING_RULES.refactor.keywords.includes("redesign"));
259
+ assert.equal(AGENT_ROUTING_RULES.documentation.agent, "claude");
260
+ assert.equal(AGENT_ROUTING_RULES.documentation.fallbackAgent, "opencode");
261
+ assert.ok(AGENT_ROUTING_RULES.documentation.keywords.includes("文档"));
262
+ assert.ok(AGENT_ROUTING_RULES.documentation.keywords.includes("readme"));
263
+ assert.ok(AGENT_ROUTING_RULES.documentation.keywords.includes("changelog"));
264
+ assert.ok(AGENT_ROUTING_RULES.documentation.keywords.includes("doc"));
265
+ });
266
+ // ── 12. scope 边界值测试 ────────────────────────────────────────
267
+ it("scope 正好 10 个文件(threshold 边界)→ 不触发 large scope 规则", () => {
268
+ // threshold 是 10,> 10 才触发,所以 10 个文件不触发规则1
269
+ const result = routeAgent({
270
+ goal: "普通任务",
271
+ scope: scopeFiles(10),
272
+ configuredAgents: ALL_AGENTS,
273
+ });
274
+ // 10 个文件不触发规则1,也不触发规则2(不是1个),无关键词 → fallback
275
+ assert.equal(result.fallback, true);
276
+ assert.equal(result.recommended_agent, ALL_AGENTS[0]);
277
+ });
278
+ it("scope 正好 11 个文件 → 触发 large scope 规则", () => {
279
+ const result = routeAgent({
280
+ goal: "普通任务",
281
+ scope: scopeFiles(11),
282
+ configuredAgents: ALL_AGENTS,
283
+ });
284
+ assert.equal(result.recommended_agent, "opencode");
285
+ assert.equal(result.fallback, false);
286
+ });
287
+ });
@@ -0,0 +1 @@
1
+ export {};