patchwarden 0.6.1 → 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 (261) hide show
  1. package/PatchWarden-Control-Tray.cmd +11 -0
  2. package/PatchWarden-Control.cmd +6 -0
  3. package/PatchWarden-Desktop.cmd +5 -0
  4. package/PatchWarden.cmd +1 -1
  5. package/README.en.md +112 -24
  6. package/README.md +36 -25
  7. package/Restart-PatchWarden-Control.cmd +6 -0
  8. package/Stop-PatchWarden.cmd +11 -0
  9. package/dist/agents/agentRouter.d.ts +40 -0
  10. package/dist/agents/agentRouter.js +95 -0
  11. package/dist/config.d.ts +1 -1
  12. package/dist/config.js +6 -2
  13. package/dist/controlCenter.d.ts +14 -0
  14. package/dist/controlCenter.js +2015 -0
  15. package/dist/doctor.js +35 -4
  16. package/dist/goal/acceptanceEngine.d.ts +62 -0
  17. package/dist/goal/acceptanceEngine.js +103 -0
  18. package/dist/goal/acceptanceTemplate.d.ts +10 -0
  19. package/dist/goal/acceptanceTemplate.js +104 -0
  20. package/dist/goal/goalGraph.d.ts +58 -0
  21. package/dist/goal/goalGraph.js +189 -0
  22. package/dist/goal/goalProgress.d.ts +81 -0
  23. package/dist/goal/goalProgress.js +216 -0
  24. package/dist/goal/goalStatus.d.ts +60 -0
  25. package/dist/goal/goalStatus.js +137 -0
  26. package/dist/goal/goalStore.d.ts +79 -0
  27. package/dist/goal/goalStore.js +211 -0
  28. package/dist/goal/handoffExport.d.ts +34 -0
  29. package/dist/goal/handoffExport.js +183 -0
  30. package/dist/goal/subgoalSync.d.ts +40 -0
  31. package/dist/goal/subgoalSync.js +72 -0
  32. package/dist/goal/worktreeManager.d.ts +96 -0
  33. package/dist/goal/worktreeManager.js +292 -0
  34. package/dist/logging.d.ts +44 -0
  35. package/dist/logging.js +68 -0
  36. package/dist/release/releaseGate.d.ts +99 -0
  37. package/dist/release/releaseGate.js +475 -0
  38. package/dist/runner/postTaskCleanup.d.ts +13 -0
  39. package/dist/runner/postTaskCleanup.js +147 -0
  40. package/dist/runner/runTask.js +50 -10
  41. package/dist/security/discoveryTokenStore.d.ts +62 -0
  42. package/dist/security/discoveryTokenStore.js +100 -0
  43. package/dist/security/toolInvocationGuard.d.ts +35 -0
  44. package/dist/security/toolInvocationGuard.js +153 -0
  45. package/dist/smoke-test.js +18 -10
  46. package/dist/taskRuntime.d.ts +17 -0
  47. package/dist/test/unit/acceptance-engine.test.d.ts +1 -0
  48. package/dist/test/unit/acceptance-engine.test.js +228 -0
  49. package/dist/test/unit/agent-router.test.d.ts +1 -0
  50. package/dist/test/unit/agent-router.test.js +287 -0
  51. package/dist/test/unit/audit-checks.test.d.ts +1 -0
  52. package/dist/test/unit/audit-checks.test.js +350 -0
  53. package/dist/test/unit/diagnose-task.test.d.ts +1 -0
  54. package/dist/test/unit/diagnose-task.test.js +457 -0
  55. package/dist/test/unit/discovery-token-store.test.d.ts +1 -0
  56. package/dist/test/unit/discovery-token-store.test.js +139 -0
  57. package/dist/test/unit/goal-graph.test.d.ts +1 -0
  58. package/dist/test/unit/goal-graph.test.js +298 -0
  59. package/dist/test/unit/goal-progress.test.d.ts +1 -0
  60. package/dist/test/unit/goal-progress.test.js +381 -0
  61. package/dist/test/unit/goal-status.test.d.ts +1 -0
  62. package/dist/test/unit/goal-status.test.js +215 -0
  63. package/dist/test/unit/goal-store.test.d.ts +1 -0
  64. package/dist/test/unit/goal-store.test.js +253 -0
  65. package/dist/test/unit/goal-subgoal-task.test.d.ts +1 -0
  66. package/dist/test/unit/goal-subgoal-task.test.js +55 -0
  67. package/dist/test/unit/goal-tools-registry.test.d.ts +1 -0
  68. package/dist/test/unit/goal-tools-registry.test.js +190 -0
  69. package/dist/test/unit/handoff-export.test.d.ts +1 -0
  70. package/dist/test/unit/handoff-export.test.js +263 -0
  71. package/dist/test/unit/invoke-discovered-tool.test.d.ts +1 -0
  72. package/dist/test/unit/invoke-discovered-tool.test.js +167 -0
  73. package/dist/test/unit/logging.test.js +127 -5
  74. package/dist/test/unit/post-task-cleanup.test.d.ts +1 -0
  75. package/dist/test/unit/post-task-cleanup.test.js +48 -0
  76. package/dist/test/unit/reconcile-tasks.test.d.ts +1 -0
  77. package/dist/test/unit/reconcile-tasks.test.js +456 -0
  78. package/dist/test/unit/release-gate.test.d.ts +1 -0
  79. package/dist/test/unit/release-gate.test.js +242 -0
  80. package/dist/test/unit/safe-views.test.d.ts +1 -0
  81. package/dist/test/unit/safe-views.test.js +171 -0
  82. package/dist/test/unit/schema-drift-check.test.d.ts +1 -0
  83. package/dist/test/unit/schema-drift-check.test.js +175 -0
  84. package/dist/test/unit/subgoal-sync.test.d.ts +1 -0
  85. package/dist/test/unit/subgoal-sync.test.js +183 -0
  86. package/dist/test/unit/tool-invocation-guard.test.d.ts +1 -0
  87. package/dist/test/unit/tool-invocation-guard.test.js +432 -0
  88. package/dist/test/unit/tool-usage-stats.test.d.ts +1 -0
  89. package/dist/test/unit/tool-usage-stats.test.js +300 -0
  90. package/dist/test/unit/toolSearch.test.d.ts +1 -0
  91. package/dist/test/unit/toolSearch.test.js +571 -0
  92. package/dist/test/unit/worktree-manager.test.d.ts +1 -0
  93. package/dist/test/unit/worktree-manager.test.js +176 -0
  94. package/dist/tools/auditTask.d.ts +103 -1
  95. package/dist/tools/auditTask.js +461 -8
  96. package/dist/tools/cancelTask.js +1 -1
  97. package/dist/tools/checkReleaseGate.d.ts +21 -0
  98. package/dist/tools/checkReleaseGate.js +22 -0
  99. package/dist/tools/createTask.d.ts +18 -2
  100. package/dist/tools/createTask.js +29 -1
  101. package/dist/tools/diagnoseTask.d.ts +45 -0
  102. package/dist/tools/diagnoseTask.js +347 -0
  103. package/dist/tools/discardWorktree.d.ts +24 -0
  104. package/dist/tools/discardWorktree.js +27 -0
  105. package/dist/tools/discoverTools.d.ts +11 -0
  106. package/dist/tools/discoverTools.js +39 -0
  107. package/dist/tools/explainTool.d.ts +11 -0
  108. package/dist/tools/explainTool.js +21 -0
  109. package/dist/tools/getTaskSummary.js +2 -1
  110. package/dist/tools/goalSubgoalTask.d.ts +51 -0
  111. package/dist/tools/goalSubgoalTask.js +110 -0
  112. package/dist/tools/healthCheck.d.ts +1 -0
  113. package/dist/tools/healthCheck.js +5 -1
  114. package/dist/tools/invokeDiscoveredTool.d.ts +37 -0
  115. package/dist/tools/invokeDiscoveredTool.js +112 -0
  116. package/dist/tools/listTasks.d.ts +3 -1
  117. package/dist/tools/listTasks.js +14 -1
  118. package/dist/tools/mergeWorktree.d.ts +24 -0
  119. package/dist/tools/mergeWorktree.js +27 -0
  120. package/dist/tools/reconcileTasks.d.ts +41 -0
  121. package/dist/tools/reconcileTasks.js +352 -0
  122. package/dist/tools/registry.js +550 -0
  123. package/dist/tools/safeStatus.d.ts +31 -1
  124. package/dist/tools/safeStatus.js +43 -1
  125. package/dist/tools/safeViews.d.ts +256 -0
  126. package/dist/tools/safeViews.js +250 -0
  127. package/dist/tools/schemaDriftCheck.d.ts +46 -0
  128. package/dist/tools/schemaDriftCheck.js +80 -0
  129. package/dist/tools/toolCatalog.d.ts +4 -3
  130. package/dist/tools/toolCatalog.js +33 -11
  131. package/dist/tools/toolRegistry.d.ts +61 -0
  132. package/dist/tools/toolRegistry.js +724 -0
  133. package/dist/tools/toolSearch.d.ts +110 -0
  134. package/dist/tools/toolSearch.js +331 -0
  135. package/dist/tools/toolUsageStats.d.ts +41 -0
  136. package/dist/tools/toolUsageStats.js +116 -0
  137. package/dist/tools/waitForTask.js +1 -0
  138. package/dist/version.d.ts +2 -2
  139. package/dist/version.js +2 -2
  140. package/dist/watcherStatus.d.ts +29 -0
  141. package/dist/watcherStatus.js +92 -1
  142. package/docs/control-center/README.md +33 -0
  143. package/docs/control-center/control-center-daily-driver.md +211 -0
  144. package/docs/control-center/control-center-mvp.md +205 -0
  145. package/docs/control-center/control-center-phase2.md +159 -0
  146. package/docs/demo.md +3 -0
  147. package/docs/release-v0.6.4.md +45 -0
  148. package/examples/openai-tunnel/README.md +5 -5
  149. package/examples/openai-tunnel/tunnel-client.example.yaml +3 -3
  150. package/package.json +25 -16
  151. package/scripts/README.md +47 -0
  152. package/scripts/{brand-check.js → checks/brand-check.js} +2 -2
  153. package/scripts/checks/control-center-smoke.js +1098 -0
  154. package/scripts/{control-smoke.js → checks/control-smoke.js} +19 -5
  155. package/scripts/{doctor-smoke.js → checks/doctor-smoke.js} +2 -1
  156. package/scripts/{http-mcp-smoke.js → checks/http-mcp-smoke.js} +2 -2
  157. package/scripts/{lifecycle-smoke.js → checks/lifecycle-smoke.js} +25 -21
  158. package/scripts/{mcp-manifest-check.js → checks/mcp-manifest-check.js} +54 -7
  159. package/scripts/{mcp-smoke.js → checks/mcp-smoke.js} +40 -11
  160. package/scripts/{package-manifest-check.js → checks/package-manifest-check.js} +2 -1
  161. package/scripts/{tunnel-supervisor-smoke.js → checks/tunnel-supervisor-smoke.js} +2 -2
  162. package/scripts/{unit-tests.js → checks/unit-tests.js} +1 -1
  163. package/scripts/{watcher-supervisor-smoke.js → checks/watcher-supervisor-smoke.js} +13 -7
  164. package/scripts/control/control-center-tray.ps1 +281 -0
  165. package/scripts/{get-patchwarden-health.ps1 → control/get-patchwarden-health.ps1} +3 -3
  166. package/scripts/{manage-patchwarden.ps1 → control/manage-patchwarden.ps1} +30 -4
  167. package/scripts/control/restart-control-center.ps1 +173 -0
  168. package/scripts/{restart-patchwarden.ps1 → control/restart-patchwarden.ps1} +1 -1
  169. package/scripts/control/start-control-center.ps1 +263 -0
  170. package/scripts/{start-patchwarden-tunnel.ps1 → control/start-patchwarden-tunnel.ps1} +48 -6
  171. package/scripts/control/stop-patchwarden.ps1 +114 -0
  172. package/scripts/launchers/Check-PatchWarden-Health.cmd +1 -1
  173. package/scripts/launchers/Reset-PatchWarden-Tunnel-Key.cmd +1 -1
  174. package/scripts/launchers/Restart-PatchWarden.cmd +1 -1
  175. package/scripts/launchers/Start-PatchWarden-Direct-Tunnel.cmd +1 -1
  176. package/scripts/launchers/Start-PatchWarden-Tunnel.cmd +1 -1
  177. package/scripts/{patchwarden-mcp-direct.cmd → mcp/patchwarden-mcp-direct.cmd} +1 -1
  178. package/scripts/{patchwarden-mcp-stdio.cmd → mcp/patchwarden-mcp-stdio.cmd} +1 -1
  179. package/scripts/{pack-clean.js → release/pack-clean.js} +9 -1
  180. package/src/agents/agentRouter.ts +149 -0
  181. package/src/config.ts +9 -3
  182. package/src/controlCenter.ts +2166 -0
  183. package/src/doctor.ts +40 -5
  184. package/src/goal/acceptanceEngine.ts +160 -0
  185. package/src/goal/acceptanceTemplate.ts +121 -0
  186. package/src/goal/goalGraph.ts +225 -0
  187. package/src/goal/goalProgress.ts +301 -0
  188. package/src/goal/goalStatus.ts +234 -0
  189. package/src/goal/goalStore.ts +306 -0
  190. package/src/goal/handoffExport.ts +211 -0
  191. package/src/goal/subgoalSync.ts +82 -0
  192. package/src/goal/worktreeManager.ts +404 -0
  193. package/src/logging.ts +91 -0
  194. package/src/release/releaseGate.ts +567 -0
  195. package/src/runner/postTaskCleanup.ts +154 -0
  196. package/src/runner/runTask.ts +49 -10
  197. package/src/security/discoveryTokenStore.ts +157 -0
  198. package/src/security/toolInvocationGuard.ts +251 -0
  199. package/src/smoke-test.ts +15 -7
  200. package/src/taskRuntime.ts +17 -0
  201. package/src/test/unit/acceptance-engine.test.ts +261 -0
  202. package/src/test/unit/agent-router.test.ts +342 -0
  203. package/src/test/unit/audit-checks.test.ts +567 -0
  204. package/src/test/unit/diagnose-task.test.ts +544 -0
  205. package/src/test/unit/discovery-token-store.test.ts +181 -0
  206. package/src/test/unit/goal-graph.test.ts +347 -0
  207. package/src/test/unit/goal-progress.test.ts +538 -0
  208. package/src/test/unit/goal-status.test.ts +270 -0
  209. package/src/test/unit/goal-store.test.ts +318 -0
  210. package/src/test/unit/goal-subgoal-task.test.ts +72 -0
  211. package/src/test/unit/goal-tools-registry.test.ts +243 -0
  212. package/src/test/unit/handoff-export.test.ts +295 -0
  213. package/src/test/unit/invoke-discovered-tool.test.ts +216 -0
  214. package/src/test/unit/logging.test.ts +177 -5
  215. package/src/test/unit/post-task-cleanup.test.ts +53 -0
  216. package/src/test/unit/reconcile-tasks.test.ts +551 -0
  217. package/src/test/unit/release-gate.test.ts +314 -0
  218. package/src/test/unit/safe-views.test.ts +184 -0
  219. package/src/test/unit/schema-drift-check.test.ts +258 -0
  220. package/src/test/unit/subgoal-sync.test.ts +236 -0
  221. package/src/test/unit/tool-invocation-guard.test.ts +542 -0
  222. package/src/test/unit/tool-usage-stats.test.ts +384 -0
  223. package/src/test/unit/toolSearch.test.ts +756 -0
  224. package/src/test/unit/worktree-manager.test.ts +247 -0
  225. package/src/tools/auditTask.ts +831 -402
  226. package/src/tools/cancelTask.ts +1 -1
  227. package/src/tools/checkReleaseGate.ts +45 -0
  228. package/src/tools/createTask.ts +64 -6
  229. package/src/tools/diagnoseTask.ts +460 -0
  230. package/src/tools/discardWorktree.ts +42 -0
  231. package/src/tools/discoverTools.ts +51 -0
  232. package/src/tools/explainTool.ts +26 -0
  233. package/src/tools/getTaskSummary.ts +2 -1
  234. package/src/tools/goalSubgoalTask.ts +170 -0
  235. package/src/tools/healthCheck.ts +4 -1
  236. package/src/tools/invokeDiscoveredTool.ts +163 -0
  237. package/src/tools/listTasks.ts +16 -2
  238. package/src/tools/mergeWorktree.ts +42 -0
  239. package/src/tools/reconcileTasks.ts +464 -0
  240. package/src/tools/registry.ts +618 -1
  241. package/src/tools/safeStatus.ts +73 -2
  242. package/src/tools/safeViews.ts +271 -0
  243. package/src/tools/schemaDriftCheck.ts +120 -0
  244. package/src/tools/toolCatalog.ts +34 -11
  245. package/src/tools/toolRegistry.ts +786 -0
  246. package/src/tools/toolSearch.ts +464 -0
  247. package/src/tools/toolUsageStats.ts +151 -0
  248. package/src/tools/waitForTask.ts +1 -0
  249. package/src/version.ts +2 -2
  250. package/src/watcherStatus.ts +97 -1
  251. package/ui/colors_and_type.css +141 -0
  252. package/ui/pages/audit.html +743 -0
  253. package/ui/pages/dashboard.html +1154 -0
  254. package/ui/pages/direct-sessions.html +652 -0
  255. package/ui/pages/logs.html +502 -0
  256. package/ui/pages/task-detail.html +1229 -0
  257. package/ui/pages/tasks.html +702 -0
  258. package/ui/pages/workspace.html +947 -0
  259. package/ui/partials/project-shell.html +362 -0
  260. package/ui/vendor/lucide.js +12 -0
  261. package/ui/vendor/tailwindcss-browser.js +947 -0
@@ -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 {};