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,342 @@
1
+ import { describe, it } from "node:test";
2
+ import { strict as assert } from "node:assert";
3
+ import {
4
+ routeAgent,
5
+ AGENT_ROUTING_RULES,
6
+ type AgentRouteInput,
7
+ } from "../../agents/agentRouter.js";
8
+ import { PatchWardenError } from "../../errors.js";
9
+
10
+ // ── Helpers ───────────────────────────────────────────────────────
11
+
12
+ const ALL_AGENTS = ["codex", "opencode", "claude", "patchwarden-direct", "patchwarden-audit"];
13
+
14
+ function scopeFiles(count: number): string[] {
15
+ return Array.from({ length: count }, (_, i) => `file_${i}.ts`);
16
+ }
17
+
18
+ // ── Tests ─────────────────────────────────────────────────────────
19
+
20
+ describe("routeAgent", () => {
21
+ // ── 1. 大规模代码修改 → opencode ────────────────────────────────
22
+
23
+ it("scope 15 个文件 → opencode, fallback=false, reason 含 'large scope'", () => {
24
+ const result = routeAgent({
25
+ goal: "大规模修改",
26
+ scope: scopeFiles(15),
27
+ configuredAgents: ALL_AGENTS,
28
+ });
29
+ assert.equal(result.recommended_agent, "opencode");
30
+ assert.equal(result.fallback, false);
31
+ assert.ok(result.reason.includes("large scope"), `reason should contain 'large scope', got: ${result.reason}`);
32
+ assert.ok(result.reason.includes("15"), `reason should contain file count '15', got: ${result.reason}`);
33
+ assert.ok(ALL_AGENTS.includes(result.recommended_agent));
34
+ });
35
+
36
+ // ── 2. 单文件修改 → patchwarden-direct ──────────────────────────
37
+
38
+ it("scope 1 个文件 → patchwarden-direct, fallback=false", () => {
39
+ const result = routeAgent({
40
+ goal: "修复一个 bug",
41
+ scope: ["src/index.ts"],
42
+ configuredAgents: ALL_AGENTS,
43
+ });
44
+ assert.equal(result.recommended_agent, "patchwarden-direct");
45
+ assert.equal(result.fallback, false);
46
+ assert.ok(ALL_AGENTS.includes(result.recommended_agent));
47
+ });
48
+
49
+ // ── 3. 文档整理 → claude ────────────────────────────────────────
50
+
51
+ it("goal 含 '更新 README 文档' → claude(若 configured)", () => {
52
+ const result = routeAgent({
53
+ goal: "更新 README 文档",
54
+ configuredAgents: ALL_AGENTS,
55
+ });
56
+ assert.equal(result.recommended_agent, "claude");
57
+ assert.equal(result.fallback, false);
58
+ assert.ok(result.reason.includes("documentation"), `reason should contain 'documentation', got: ${result.reason}`);
59
+ assert.ok(ALL_AGENTS.includes(result.recommended_agent));
60
+ });
61
+
62
+ it("goal 含文档关键词但 claude 未配置 → fallback 到 opencode", () => {
63
+ const configured = ["codex", "opencode"];
64
+ const result = routeAgent({
65
+ goal: "更新 README 文档",
66
+ configuredAgents: configured,
67
+ });
68
+ assert.equal(result.recommended_agent, "opencode");
69
+ assert.equal(result.fallback, true);
70
+ assert.ok(configured.includes(result.recommended_agent));
71
+ });
72
+
73
+ it("goal 含文档关键词但 claude 和 opencode 都未配置 → fallback 到第一个", () => {
74
+ const configured = ["codex"];
75
+ const result = routeAgent({
76
+ goal: "更新 README 文档",
77
+ configuredAgents: configured,
78
+ });
79
+ assert.equal(result.recommended_agent, "codex");
80
+ assert.equal(result.fallback, true);
81
+ assert.ok(configured.includes(result.recommended_agent));
82
+ });
83
+
84
+ // ── 4. 复杂推理和重构 → codex ───────────────────────────────────
85
+
86
+ it("goal 含 '跨模块重构' → codex", () => {
87
+ const result = routeAgent({
88
+ goal: "跨模块重构认证模块",
89
+ configuredAgents: ALL_AGENTS,
90
+ });
91
+ assert.equal(result.recommended_agent, "codex");
92
+ assert.equal(result.fallback, false);
93
+ assert.ok(result.reason.includes("refactor"), `reason should contain 'refactor', got: ${result.reason}`);
94
+ assert.ok(ALL_AGENTS.includes(result.recommended_agent));
95
+ });
96
+
97
+ it("goal 含 'redesign' → codex", () => {
98
+ const result = routeAgent({
99
+ goal: "Redesign the data layer",
100
+ configuredAgents: ALL_AGENTS,
101
+ });
102
+ assert.equal(result.recommended_agent, "codex");
103
+ assert.equal(result.fallback, false);
104
+ assert.ok(ALL_AGENTS.includes(result.recommended_agent));
105
+ });
106
+
107
+ // ── 5. 验收审计 → patchwarden-audit ─────────────────────────────
108
+
109
+ it("goal 含 '审计任务' → patchwarden-audit", () => {
110
+ const result = routeAgent({
111
+ goal: "执行验收审计任务",
112
+ configuredAgents: ALL_AGENTS,
113
+ });
114
+ assert.equal(result.recommended_agent, "patchwarden-audit");
115
+ assert.equal(result.fallback, false);
116
+ assert.ok(result.reason.includes("audit"), `reason should contain 'audit', got: ${result.reason}`);
117
+ assert.ok(ALL_AGENTS.includes(result.recommended_agent));
118
+ });
119
+
120
+ it("goal 含 'verify' → patchwarden-audit", () => {
121
+ const result = routeAgent({
122
+ goal: "verify the release artifacts",
123
+ configuredAgents: ALL_AGENTS,
124
+ });
125
+ assert.equal(result.recommended_agent, "patchwarden-audit");
126
+ assert.equal(result.fallback, false);
127
+ assert.ok(ALL_AGENTS.includes(result.recommended_agent));
128
+ });
129
+
130
+ // ── 6. 无匹配 → fallback 到 configuredAgents[0] ────────────────
131
+
132
+ it("无匹配(goal='普通任务', scope=['a.ts','b.ts'])→ fallback=true, recommended_agent=configuredAgents[0]", () => {
133
+ const configured = ["codex", "opencode"];
134
+ const result = routeAgent({
135
+ goal: "普通任务",
136
+ scope: ["a.ts", "b.ts"],
137
+ configuredAgents: configured,
138
+ });
139
+ assert.equal(result.recommended_agent, configured[0]);
140
+ assert.equal(result.fallback, true);
141
+ assert.ok(configured.includes(result.recommended_agent));
142
+ });
143
+
144
+ // ── 7. 白名单约束:推荐结果必须在 configuredAgents 中 ──────────
145
+
146
+ it("所有场景断言 result.recommended_agent 在 configuredAgents 中", () => {
147
+ const configured = ["codex", "opencode"];
148
+ const inputs: AgentRouteInput[] = [
149
+ { goal: "大规模修改", scope: scopeFiles(15), configuredAgents: configured },
150
+ { goal: "单文件", scope: ["a.ts"], configuredAgents: configured },
151
+ { goal: "审计任务", configuredAgents: configured },
152
+ { goal: "重构模块", configuredAgents: configured },
153
+ { goal: "更新文档", configuredAgents: configured },
154
+ { goal: "普通任务", scope: ["a.ts", "b.ts"], configuredAgents: configured },
155
+ ];
156
+ for (const input of inputs) {
157
+ const result = routeAgent(input);
158
+ assert.ok(
159
+ configured.includes(result.recommended_agent),
160
+ `recommended_agent "${result.recommended_agent}" not in configuredAgents [${configured.join(", ")}] for goal "${input.goal}"`
161
+ );
162
+ }
163
+ });
164
+
165
+ it("白名单约束:opencode 未配置时大规模 scope 回退到第一个 agent", () => {
166
+ const configured = ["codex"];
167
+ const result = routeAgent({
168
+ goal: "大规模修改",
169
+ scope: scopeFiles(15),
170
+ configuredAgents: configured,
171
+ });
172
+ assert.equal(result.recommended_agent, "codex");
173
+ assert.equal(result.fallback, true);
174
+ assert.ok(result.reason.includes("fallback"), `reason should contain 'fallback', got: ${result.reason}`);
175
+ assert.ok(configured.includes(result.recommended_agent));
176
+ });
177
+
178
+ it("白名单约束:patchwarden-direct 未配置时单文件 scope 回退", () => {
179
+ const configured = ["codex", "opencode"];
180
+ const result = routeAgent({
181
+ goal: "修复 bug",
182
+ scope: ["a.ts"],
183
+ configuredAgents: configured,
184
+ });
185
+ assert.equal(result.recommended_agent, "codex");
186
+ assert.equal(result.fallback, true);
187
+ assert.ok(configured.includes(result.recommended_agent));
188
+ });
189
+
190
+ it("白名单约束:patchwarden-audit 未配置时审计任务回退", () => {
191
+ const configured = ["codex", "opencode"];
192
+ const result = routeAgent({
193
+ goal: "审计任务",
194
+ configuredAgents: configured,
195
+ });
196
+ assert.equal(result.recommended_agent, "codex");
197
+ assert.equal(result.fallback, true);
198
+ assert.ok(configured.includes(result.recommended_agent));
199
+ });
200
+
201
+ it("白名单约束:codex 未配置时重构任务回退", () => {
202
+ const configured = ["opencode"];
203
+ const result = routeAgent({
204
+ goal: "跨模块重构",
205
+ configuredAgents: configured,
206
+ });
207
+ assert.equal(result.recommended_agent, "opencode");
208
+ assert.equal(result.fallback, true);
209
+ assert.ok(configured.includes(result.recommended_agent));
210
+ });
211
+
212
+ // ── 8. configuredAgents 为空 → 抛错 ────────────────────────────
213
+
214
+ it("configuredAgents 为空 → 抛 PatchWardenError(no_agent_configured)", () => {
215
+ assert.throws(
216
+ () => routeAgent({ goal: "任意任务", configuredAgents: [] }),
217
+ (err: unknown) => {
218
+ assert.ok(err instanceof PatchWardenError, "err should be PatchWardenError");
219
+ assert.equal((err as PatchWardenError).reason, "no_agent_configured");
220
+ return true;
221
+ }
222
+ );
223
+ });
224
+
225
+ // ── 9. 路由规则优先级验证 ───────────────────────────────────────
226
+
227
+ it("优先级:scope > 10 优先于文档关键词", () => {
228
+ // scope 15 个文件 + goal 含文档关键词 → opencode(规则1 优先于规则5)
229
+ const result = routeAgent({
230
+ goal: "更新文档",
231
+ scope: scopeFiles(15),
232
+ configuredAgents: ALL_AGENTS,
233
+ });
234
+ assert.equal(result.recommended_agent, "opencode");
235
+ assert.equal(result.fallback, false);
236
+ });
237
+
238
+ it("优先级:scope === 1 优先于审计关键词", () => {
239
+ // scope 1 个文件 + goal 含审计关键词 → patchwarden-direct(规则2 优先于规则3)
240
+ const result = routeAgent({
241
+ goal: "审计任务",
242
+ scope: ["a.ts"],
243
+ configuredAgents: ALL_AGENTS,
244
+ });
245
+ assert.equal(result.recommended_agent, "patchwarden-direct");
246
+ assert.equal(result.fallback, false);
247
+ });
248
+
249
+ it("优先级:审计关键词优先于重构关键词", () => {
250
+ // goal 同时含审计和重构关键词 → patchwarden-audit(规则3 优先于规则4)
251
+ const result = routeAgent({
252
+ goal: "审计重构模块",
253
+ configuredAgents: ALL_AGENTS,
254
+ });
255
+ assert.equal(result.recommended_agent, "patchwarden-audit");
256
+ assert.equal(result.fallback, false);
257
+ });
258
+
259
+ it("优先级:重构关键词优先于文档关键词", () => {
260
+ // goal 同时含重构和文档关键词 → codex(规则4 优先于规则5)
261
+ const result = routeAgent({
262
+ goal: "重构文档模块",
263
+ configuredAgents: ALL_AGENTS,
264
+ });
265
+ assert.equal(result.recommended_agent, "codex");
266
+ assert.equal(result.fallback, false);
267
+ });
268
+
269
+ // ── 10. inline_plan 文本特征匹配 ────────────────────────────────
270
+
271
+ it("inline_plan 含重构关键词 → codex", () => {
272
+ const result = routeAgent({
273
+ goal: "改进代码",
274
+ inline_plan: "需要 refactor 数据访问层",
275
+ configuredAgents: ALL_AGENTS,
276
+ });
277
+ assert.equal(result.recommended_agent, "codex");
278
+ assert.equal(result.fallback, false);
279
+ });
280
+
281
+ it("inline_plan 含审计关键词 → patchwarden-audit", () => {
282
+ const result = routeAgent({
283
+ goal: "检查工作",
284
+ inline_plan: "执行 audit 验收流程",
285
+ configuredAgents: ALL_AGENTS,
286
+ });
287
+ assert.equal(result.recommended_agent, "patchwarden-audit");
288
+ assert.equal(result.fallback, false);
289
+ });
290
+
291
+ // ── 11. AGENT_ROUTING_RULES 常量结构断言 ────────────────────────
292
+
293
+ it("AGENT_ROUTING_RULES 导出完整路由规则结构", () => {
294
+ assert.equal(AGENT_ROUTING_RULES.largeScope.agent, "opencode");
295
+ assert.equal(AGENT_ROUTING_RULES.largeScope.threshold, 10);
296
+
297
+ assert.equal(AGENT_ROUTING_RULES.singleFile.agent, "patchwarden-direct");
298
+
299
+ assert.equal(AGENT_ROUTING_RULES.audit.agent, "patchwarden-audit");
300
+ assert.ok(AGENT_ROUTING_RULES.audit.keywords.includes("审计"));
301
+ assert.ok(AGENT_ROUTING_RULES.audit.keywords.includes("audit"));
302
+ assert.ok(AGENT_ROUTING_RULES.audit.keywords.includes("验收"));
303
+ assert.ok(AGENT_ROUTING_RULES.audit.keywords.includes("verify"));
304
+
305
+ assert.equal(AGENT_ROUTING_RULES.refactor.agent, "codex");
306
+ assert.ok(AGENT_ROUTING_RULES.refactor.keywords.includes("重构"));
307
+ assert.ok(AGENT_ROUTING_RULES.refactor.keywords.includes("refactor"));
308
+ assert.ok(AGENT_ROUTING_RULES.refactor.keywords.includes("跨模块"));
309
+ assert.ok(AGENT_ROUTING_RULES.refactor.keywords.includes("redesign"));
310
+
311
+ assert.equal(AGENT_ROUTING_RULES.documentation.agent, "claude");
312
+ assert.equal(AGENT_ROUTING_RULES.documentation.fallbackAgent, "opencode");
313
+ assert.ok(AGENT_ROUTING_RULES.documentation.keywords.includes("文档"));
314
+ assert.ok(AGENT_ROUTING_RULES.documentation.keywords.includes("readme"));
315
+ assert.ok(AGENT_ROUTING_RULES.documentation.keywords.includes("changelog"));
316
+ assert.ok(AGENT_ROUTING_RULES.documentation.keywords.includes("doc"));
317
+ });
318
+
319
+ // ── 12. scope 边界值测试 ────────────────────────────────────────
320
+
321
+ it("scope 正好 10 个文件(threshold 边界)→ 不触发 large scope 规则", () => {
322
+ // threshold 是 10,> 10 才触发,所以 10 个文件不触发规则1
323
+ const result = routeAgent({
324
+ goal: "普通任务",
325
+ scope: scopeFiles(10),
326
+ configuredAgents: ALL_AGENTS,
327
+ });
328
+ // 10 个文件不触发规则1,也不触发规则2(不是1个),无关键词 → fallback
329
+ assert.equal(result.fallback, true);
330
+ assert.equal(result.recommended_agent, ALL_AGENTS[0]);
331
+ });
332
+
333
+ it("scope 正好 11 个文件 → 触发 large scope 规则", () => {
334
+ const result = routeAgent({
335
+ goal: "普通任务",
336
+ scope: scopeFiles(11),
337
+ configuredAgents: ALL_AGENTS,
338
+ });
339
+ assert.equal(result.recommended_agent, "opencode");
340
+ assert.equal(result.fallback, false);
341
+ });
342
+ });