patchwarden 0.6.4 → 1.5.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 (254) hide show
  1. package/README.en.md +54 -20
  2. package/README.md +48 -21
  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 +212 -3
  8. package/dist/direct/directSessionStore.d.ts +2 -0
  9. package/dist/direct/directVerification.js +7 -0
  10. package/dist/doctor.js +32 -1
  11. package/dist/goal/acceptanceEngine.d.ts +62 -0
  12. package/dist/goal/acceptanceEngine.js +103 -0
  13. package/dist/goal/acceptanceTemplate.d.ts +10 -0
  14. package/dist/goal/acceptanceTemplate.js +104 -0
  15. package/dist/goal/goalGraph.d.ts +58 -0
  16. package/dist/goal/goalGraph.js +189 -0
  17. package/dist/goal/goalProgress.d.ts +81 -0
  18. package/dist/goal/goalProgress.js +216 -0
  19. package/dist/goal/goalStatus.d.ts +60 -0
  20. package/dist/goal/goalStatus.js +137 -0
  21. package/dist/goal/goalStore.d.ts +79 -0
  22. package/dist/goal/goalStore.js +211 -0
  23. package/dist/goal/handoffExport.d.ts +34 -0
  24. package/dist/goal/handoffExport.js +183 -0
  25. package/dist/goal/subgoalSync.d.ts +40 -0
  26. package/dist/goal/subgoalSync.js +72 -0
  27. package/dist/goal/worktreeManager.d.ts +96 -0
  28. package/dist/goal/worktreeManager.js +292 -0
  29. package/dist/logging.d.ts +44 -0
  30. package/dist/logging.js +68 -0
  31. package/dist/policy/projectPolicy.d.ts +55 -0
  32. package/dist/policy/projectPolicy.js +286 -0
  33. package/dist/release/releaseGate.d.ts +99 -0
  34. package/dist/release/releaseGate.js +475 -0
  35. package/dist/runner/postTaskCleanup.d.ts +13 -0
  36. package/dist/runner/postTaskCleanup.js +147 -0
  37. package/dist/runner/runTask.js +50 -10
  38. package/dist/security/discoveryTokenStore.d.ts +62 -0
  39. package/dist/security/discoveryTokenStore.js +100 -0
  40. package/dist/security/toolInvocationGuard.d.ts +35 -0
  41. package/dist/security/toolInvocationGuard.js +153 -0
  42. package/dist/smoke-test.js +18 -10
  43. package/dist/taskRuntime.d.ts +17 -0
  44. package/dist/test/unit/acceptance-engine.test.d.ts +1 -0
  45. package/dist/test/unit/acceptance-engine.test.js +228 -0
  46. package/dist/test/unit/agent-router.test.d.ts +1 -0
  47. package/dist/test/unit/agent-router.test.js +287 -0
  48. package/dist/test/unit/audit-checks.test.d.ts +1 -0
  49. package/dist/test/unit/audit-checks.test.js +350 -0
  50. package/dist/test/unit/diagnose-task.test.d.ts +1 -0
  51. package/dist/test/unit/diagnose-task.test.js +457 -0
  52. package/dist/test/unit/discovery-token-store.test.d.ts +1 -0
  53. package/dist/test/unit/discovery-token-store.test.js +139 -0
  54. package/dist/test/unit/evidence-pack.test.d.ts +1 -0
  55. package/dist/test/unit/evidence-pack.test.js +130 -0
  56. package/dist/test/unit/goal-graph.test.d.ts +1 -0
  57. package/dist/test/unit/goal-graph.test.js +298 -0
  58. package/dist/test/unit/goal-progress.test.d.ts +1 -0
  59. package/dist/test/unit/goal-progress.test.js +381 -0
  60. package/dist/test/unit/goal-status.test.d.ts +1 -0
  61. package/dist/test/unit/goal-status.test.js +215 -0
  62. package/dist/test/unit/goal-store.test.d.ts +1 -0
  63. package/dist/test/unit/goal-store.test.js +253 -0
  64. package/dist/test/unit/goal-subgoal-task.test.d.ts +1 -0
  65. package/dist/test/unit/goal-subgoal-task.test.js +55 -0
  66. package/dist/test/unit/goal-tools-registry.test.d.ts +1 -0
  67. package/dist/test/unit/goal-tools-registry.test.js +190 -0
  68. package/dist/test/unit/handoff-export.test.d.ts +1 -0
  69. package/dist/test/unit/handoff-export.test.js +263 -0
  70. package/dist/test/unit/invoke-discovered-tool.test.d.ts +1 -0
  71. package/dist/test/unit/invoke-discovered-tool.test.js +167 -0
  72. package/dist/test/unit/logging.test.js +127 -5
  73. package/dist/test/unit/post-task-cleanup.test.d.ts +1 -0
  74. package/dist/test/unit/post-task-cleanup.test.js +48 -0
  75. package/dist/test/unit/project-policy-release-mode.test.d.ts +1 -0
  76. package/dist/test/unit/project-policy-release-mode.test.js +125 -0
  77. package/dist/test/unit/reconcile-tasks.test.d.ts +1 -0
  78. package/dist/test/unit/reconcile-tasks.test.js +456 -0
  79. package/dist/test/unit/release-gate.test.d.ts +1 -0
  80. package/dist/test/unit/release-gate.test.js +242 -0
  81. package/dist/test/unit/run-task-loop.test.d.ts +1 -0
  82. package/dist/test/unit/run-task-loop.test.js +380 -0
  83. package/dist/test/unit/safe-views.test.d.ts +1 -0
  84. package/dist/test/unit/safe-views.test.js +171 -0
  85. package/dist/test/unit/schema-drift-check.test.d.ts +1 -0
  86. package/dist/test/unit/schema-drift-check.test.js +176 -0
  87. package/dist/test/unit/subgoal-sync.test.d.ts +1 -0
  88. package/dist/test/unit/subgoal-sync.test.js +183 -0
  89. package/dist/test/unit/tool-invocation-guard.test.d.ts +1 -0
  90. package/dist/test/unit/tool-invocation-guard.test.js +432 -0
  91. package/dist/test/unit/tool-usage-stats.test.d.ts +1 -0
  92. package/dist/test/unit/tool-usage-stats.test.js +300 -0
  93. package/dist/test/unit/toolSearch.test.d.ts +1 -0
  94. package/dist/test/unit/toolSearch.test.js +571 -0
  95. package/dist/test/unit/worktree-manager.test.d.ts +1 -0
  96. package/dist/test/unit/worktree-manager.test.js +176 -0
  97. package/dist/tools/auditTask.d.ts +103 -1
  98. package/dist/tools/auditTask.js +461 -8
  99. package/dist/tools/cancelTask.js +1 -1
  100. package/dist/tools/checkReleaseGate.d.ts +21 -0
  101. package/dist/tools/checkReleaseGate.js +22 -0
  102. package/dist/tools/createTask.d.ts +18 -2
  103. package/dist/tools/createTask.js +29 -1
  104. package/dist/tools/diagnoseTask.d.ts +45 -0
  105. package/dist/tools/diagnoseTask.js +347 -0
  106. package/dist/tools/discardWorktree.d.ts +24 -0
  107. package/dist/tools/discardWorktree.js +27 -0
  108. package/dist/tools/discoverTools.d.ts +11 -0
  109. package/dist/tools/discoverTools.js +39 -0
  110. package/dist/tools/evidencePack.d.ts +39 -0
  111. package/dist/tools/evidencePack.js +168 -0
  112. package/dist/tools/explainTool.d.ts +11 -0
  113. package/dist/tools/explainTool.js +21 -0
  114. package/dist/tools/getTaskSummary.js +2 -1
  115. package/dist/tools/goalSubgoalTask.d.ts +51 -0
  116. package/dist/tools/goalSubgoalTask.js +110 -0
  117. package/dist/tools/healthCheck.d.ts +1 -0
  118. package/dist/tools/healthCheck.js +5 -1
  119. package/dist/tools/invokeDiscoveredTool.d.ts +37 -0
  120. package/dist/tools/invokeDiscoveredTool.js +112 -0
  121. package/dist/tools/listTasks.d.ts +3 -1
  122. package/dist/tools/listTasks.js +14 -1
  123. package/dist/tools/mergeWorktree.d.ts +24 -0
  124. package/dist/tools/mergeWorktree.js +27 -0
  125. package/dist/tools/recommendAgentForTask.d.ts +19 -0
  126. package/dist/tools/recommendAgentForTask.js +56 -0
  127. package/dist/tools/reconcileTasks.d.ts +41 -0
  128. package/dist/tools/reconcileTasks.js +352 -0
  129. package/dist/tools/registry.js +926 -2
  130. package/dist/tools/releaseMode.d.ts +50 -0
  131. package/dist/tools/releaseMode.js +370 -0
  132. package/dist/tools/runDirectVerificationBundle.d.ts +26 -0
  133. package/dist/tools/runDirectVerificationBundle.js +64 -0
  134. package/dist/tools/runTaskLoop.d.ts +57 -0
  135. package/dist/tools/runTaskLoop.js +417 -0
  136. package/dist/tools/runVerification.d.ts +4 -0
  137. package/dist/tools/runVerification.js +4 -0
  138. package/dist/tools/safeStatus.d.ts +31 -1
  139. package/dist/tools/safeStatus.js +43 -1
  140. package/dist/tools/safeViews.d.ts +262 -0
  141. package/dist/tools/safeViews.js +252 -0
  142. package/dist/tools/schemaDriftCheck.d.ts +46 -0
  143. package/dist/tools/schemaDriftCheck.js +80 -0
  144. package/dist/tools/taskLineage.d.ts +91 -0
  145. package/dist/tools/taskLineage.js +175 -0
  146. package/dist/tools/toolCatalog.d.ts +4 -3
  147. package/dist/tools/toolCatalog.js +39 -11
  148. package/dist/tools/toolRegistry.d.ts +61 -0
  149. package/dist/tools/toolRegistry.js +834 -0
  150. package/dist/tools/toolSearch.d.ts +110 -0
  151. package/dist/tools/toolSearch.js +331 -0
  152. package/dist/tools/toolUsageStats.d.ts +41 -0
  153. package/dist/tools/toolUsageStats.js +116 -0
  154. package/dist/tools/waitForTask.js +1 -0
  155. package/dist/version.d.ts +2 -2
  156. package/dist/version.js +2 -2
  157. package/dist/watcherStatus.d.ts +29 -0
  158. package/dist/watcherStatus.js +92 -1
  159. package/docs/chatgpt-usage.md +31 -0
  160. package/docs/control-center/README.md +9 -0
  161. package/package.json +3 -2
  162. package/scripts/checks/control-center-smoke.js +87 -0
  163. package/scripts/checks/control-smoke.js +2 -2
  164. package/scripts/checks/doctor-smoke.js +1 -0
  165. package/scripts/checks/lifecycle-smoke.js +13 -9
  166. package/scripts/checks/mcp-manifest-check.js +65 -6
  167. package/scripts/checks/mcp-smoke.js +60 -9
  168. package/scripts/checks/watcher-supervisor-smoke.js +2 -2
  169. package/src/agents/agentRouter.ts +149 -0
  170. package/src/config.ts +9 -3
  171. package/src/controlCenter.ts +215 -4
  172. package/src/direct/directSessionStore.ts +2 -0
  173. package/src/direct/directVerification.ts +7 -0
  174. package/src/doctor.ts +36 -1
  175. package/src/goal/acceptanceEngine.ts +160 -0
  176. package/src/goal/acceptanceTemplate.ts +121 -0
  177. package/src/goal/goalGraph.ts +225 -0
  178. package/src/goal/goalProgress.ts +301 -0
  179. package/src/goal/goalStatus.ts +234 -0
  180. package/src/goal/goalStore.ts +306 -0
  181. package/src/goal/handoffExport.ts +211 -0
  182. package/src/goal/subgoalSync.ts +82 -0
  183. package/src/goal/worktreeManager.ts +404 -0
  184. package/src/logging.ts +91 -0
  185. package/src/policy/projectPolicy.ts +344 -0
  186. package/src/release/releaseGate.ts +567 -0
  187. package/src/runner/postTaskCleanup.ts +154 -0
  188. package/src/runner/runTask.ts +49 -10
  189. package/src/security/discoveryTokenStore.ts +157 -0
  190. package/src/security/toolInvocationGuard.ts +251 -0
  191. package/src/smoke-test.ts +15 -7
  192. package/src/taskRuntime.ts +17 -0
  193. package/src/test/unit/acceptance-engine.test.ts +261 -0
  194. package/src/test/unit/agent-router.test.ts +342 -0
  195. package/src/test/unit/audit-checks.test.ts +567 -0
  196. package/src/test/unit/diagnose-task.test.ts +544 -0
  197. package/src/test/unit/discovery-token-store.test.ts +181 -0
  198. package/src/test/unit/evidence-pack.test.ts +142 -0
  199. package/src/test/unit/goal-graph.test.ts +347 -0
  200. package/src/test/unit/goal-progress.test.ts +538 -0
  201. package/src/test/unit/goal-status.test.ts +270 -0
  202. package/src/test/unit/goal-store.test.ts +318 -0
  203. package/src/test/unit/goal-subgoal-task.test.ts +72 -0
  204. package/src/test/unit/goal-tools-registry.test.ts +243 -0
  205. package/src/test/unit/handoff-export.test.ts +295 -0
  206. package/src/test/unit/invoke-discovered-tool.test.ts +216 -0
  207. package/src/test/unit/logging.test.ts +177 -5
  208. package/src/test/unit/post-task-cleanup.test.ts +53 -0
  209. package/src/test/unit/project-policy-release-mode.test.ts +156 -0
  210. package/src/test/unit/reconcile-tasks.test.ts +551 -0
  211. package/src/test/unit/release-gate.test.ts +314 -0
  212. package/src/test/unit/run-task-loop.test.ts +425 -0
  213. package/src/test/unit/safe-views.test.ts +184 -0
  214. package/src/test/unit/schema-drift-check.test.ts +260 -0
  215. package/src/test/unit/subgoal-sync.test.ts +236 -0
  216. package/src/test/unit/tool-invocation-guard.test.ts +542 -0
  217. package/src/test/unit/tool-usage-stats.test.ts +384 -0
  218. package/src/test/unit/toolSearch.test.ts +756 -0
  219. package/src/test/unit/worktree-manager.test.ts +247 -0
  220. package/src/tools/auditTask.ts +831 -402
  221. package/src/tools/cancelTask.ts +1 -1
  222. package/src/tools/checkReleaseGate.ts +45 -0
  223. package/src/tools/createTask.ts +64 -6
  224. package/src/tools/diagnoseTask.ts +460 -0
  225. package/src/tools/discardWorktree.ts +42 -0
  226. package/src/tools/discoverTools.ts +51 -0
  227. package/src/tools/evidencePack.ts +205 -0
  228. package/src/tools/explainTool.ts +26 -0
  229. package/src/tools/getTaskSummary.ts +2 -1
  230. package/src/tools/goalSubgoalTask.ts +170 -0
  231. package/src/tools/healthCheck.ts +4 -1
  232. package/src/tools/invokeDiscoveredTool.ts +163 -0
  233. package/src/tools/listTasks.ts +16 -2
  234. package/src/tools/listWorkspace.ts +71 -71
  235. package/src/tools/mergeWorktree.ts +42 -0
  236. package/src/tools/recommendAgentForTask.ts +79 -0
  237. package/src/tools/reconcileTasks.ts +464 -0
  238. package/src/tools/registry.ts +1093 -73
  239. package/src/tools/releaseMode.ts +450 -0
  240. package/src/tools/runDirectVerificationBundle.ts +98 -0
  241. package/src/tools/runTaskLoop.ts +526 -0
  242. package/src/tools/runVerification.ts +8 -0
  243. package/src/tools/safeStatus.ts +73 -2
  244. package/src/tools/safeViews.ts +273 -0
  245. package/src/tools/schemaDriftCheck.ts +120 -0
  246. package/src/tools/taskLineage.ts +300 -0
  247. package/src/tools/toolCatalog.ts +40 -11
  248. package/src/tools/toolRegistry.ts +896 -0
  249. package/src/tools/toolSearch.ts +464 -0
  250. package/src/tools/toolUsageStats.ts +151 -0
  251. package/src/tools/waitForTask.ts +1 -0
  252. package/src/version.ts +2 -2
  253. package/src/watcherStatus.ts +97 -1
  254. package/ui/pages/dashboard.html +143 -2
@@ -0,0 +1,381 @@
1
+ import { describe, it, beforeEach, afterEach } from "node:test";
2
+ import { strict as assert } from "node:assert";
3
+ import { mkdtempSync, rmSync, mkdirSync, writeFileSync, } from "node:fs";
4
+ import { join } from "node:path";
5
+ import { tmpdir } from "node:os";
6
+ import { createGoal, readGoalStatus, writeGoalStatus, } from "../../goal/goalStore.js";
7
+ import { addSubgoal, updateSubgoalStatus, linkTaskToSubgoal, } from "../../goal/goalStatus.js";
8
+ import { acceptSubgoal, rejectSubgoal, summarizeGoalProgress, } from "../../goal/goalProgress.js";
9
+ import { PatchWardenError } from "../../errors.js";
10
+ // ── Helpers ───────────────────────────────────────────────────────
11
+ let tempDir;
12
+ beforeEach(() => {
13
+ tempDir = mkdtempSync(join(tmpdir(), "pw-goalprogress-"));
14
+ });
15
+ afterEach(() => {
16
+ rmSync(tempDir, { recursive: true, force: true });
17
+ });
18
+ /**
19
+ * 在 {workspaceRoot}/.patchwarden/tasks/{taskId}/status.json 创建任务状态文件。
20
+ */
21
+ function createTestTask(workspaceRoot, taskId, status) {
22
+ const taskDir = join(workspaceRoot, ".patchwarden", "tasks", taskId);
23
+ mkdirSync(taskDir, { recursive: true });
24
+ writeFileSync(join(taskDir, "status.json"), JSON.stringify({ task_id: taskId, status }), "utf-8");
25
+ }
26
+ /**
27
+ * 创建 goal 并添加一个 subgoal,可选地设置状态和关联 task。
28
+ */
29
+ function setupGoalWithSubgoal(workspaceRoot, options = {}) {
30
+ const { goal_id } = createGoal("repo", options.title ?? "Test Goal", "description", workspaceRoot);
31
+ let status = readGoalStatus(goal_id, workspaceRoot);
32
+ const addResult = addSubgoal(status, options.subgoalTitle ?? "Subgoal 1", options.dependsOn ?? []);
33
+ status = addResult.goalStatus;
34
+ const subgoalId = addResult.subgoalId;
35
+ // 关联 task
36
+ for (const taskId of options.taskIds ?? []) {
37
+ status = linkTaskToSubgoal(status, subgoalId, taskId);
38
+ }
39
+ // 推进到目标状态
40
+ const targetStatus = options.subgoalStatus ?? "ready";
41
+ if (targetStatus !== "ready") {
42
+ if (targetStatus === "running" || targetStatus === "done_by_agent" ||
43
+ targetStatus === "accepted" || targetStatus === "rejected" || targetStatus === "needs_fix") {
44
+ status = updateSubgoalStatus(status, subgoalId, "running");
45
+ if (targetStatus === "done_by_agent" || targetStatus === "accepted" ||
46
+ targetStatus === "rejected" || targetStatus === "needs_fix") {
47
+ status = updateSubgoalStatus(status, subgoalId, "done_by_agent");
48
+ if (targetStatus === "accepted") {
49
+ status = updateSubgoalStatus(status, subgoalId, "accepted");
50
+ }
51
+ else if (targetStatus === "rejected") {
52
+ status = updateSubgoalStatus(status, subgoalId, "rejected", {
53
+ rejected_reason: "pre-set rejected",
54
+ });
55
+ }
56
+ else if (targetStatus === "needs_fix") {
57
+ status = updateSubgoalStatus(status, subgoalId, "needs_fix");
58
+ }
59
+ }
60
+ }
61
+ }
62
+ writeGoalStatus(goal_id, status, workspaceRoot);
63
+ return { goal_id, subgoal_id: subgoalId };
64
+ }
65
+ // ── Tests ─────────────────────────────────────────────────────────
66
+ describe("goalProgress", () => {
67
+ describe("acceptSubgoal", () => {
68
+ it("所有关联 task 为 accepted → 成功,subgoal.status 变为 accepted", () => {
69
+ const { goal_id, subgoal_id } = setupGoalWithSubgoal(tempDir, {
70
+ subgoalStatus: "done_by_agent",
71
+ taskIds: ["task-001", "task-002"],
72
+ });
73
+ createTestTask(tempDir, "task-001", "accepted");
74
+ createTestTask(tempDir, "task-002", "accepted");
75
+ const result = acceptSubgoal(goal_id, subgoal_id, tempDir);
76
+ assert.equal(result.subgoal_id, subgoal_id);
77
+ assert.equal(result.status, "accepted");
78
+ assert.ok(result.accepted_at.length > 0);
79
+ // 验证已持久化
80
+ const status = readGoalStatus(goal_id, tempDir);
81
+ const sub = status.subgoals.find((s) => s.id === subgoal_id);
82
+ assert.equal(sub.status, "accepted");
83
+ assert.equal(sub.accepted_at, result.accepted_at);
84
+ });
85
+ it("有未 accepted 的 task(done_by_agent)→ 抛 subgoal_not_ready,detail 含 unaccepted_tasks", () => {
86
+ const { goal_id, subgoal_id } = setupGoalWithSubgoal(tempDir, {
87
+ subgoalStatus: "done_by_agent",
88
+ taskIds: ["task-001", "task-002"],
89
+ });
90
+ createTestTask(tempDir, "task-001", "accepted");
91
+ createTestTask(tempDir, "task-002", "done_by_agent");
92
+ assert.throws(() => acceptSubgoal(goal_id, subgoal_id, tempDir), (err) => {
93
+ assert.ok(err instanceof PatchWardenError);
94
+ assert.equal(err.reason, "subgoal_not_ready");
95
+ const details = err.details;
96
+ assert.ok(Array.isArray(details.unaccepted_tasks));
97
+ assert.equal(details.unaccepted_tasks.length, 1);
98
+ assert.equal(details.unaccepted_tasks[0].task_id, "task-002");
99
+ assert.equal(details.unaccepted_tasks[0].current_status, "done_by_agent");
100
+ return true;
101
+ });
102
+ // 验证状态未改变
103
+ const status = readGoalStatus(goal_id, tempDir);
104
+ const sub = status.subgoals.find((s) => s.id === subgoal_id);
105
+ assert.equal(sub.status, "done_by_agent");
106
+ });
107
+ it("无关联 task(task_ids 为空)→ 抛 subgoal_not_ready", () => {
108
+ const { goal_id, subgoal_id } = setupGoalWithSubgoal(tempDir, {
109
+ subgoalStatus: "done_by_agent",
110
+ taskIds: [],
111
+ });
112
+ assert.throws(() => acceptSubgoal(goal_id, subgoal_id, tempDir), (err) => {
113
+ assert.ok(err instanceof PatchWardenError);
114
+ assert.equal(err.reason, "subgoal_not_ready");
115
+ return true;
116
+ });
117
+ });
118
+ it("subgoal 不存在 → 抛 subgoal_not_found", () => {
119
+ const { goal_id } = setupGoalWithSubgoal(tempDir, {
120
+ subgoalStatus: "done_by_agent",
121
+ taskIds: ["task-001"],
122
+ });
123
+ createTestTask(tempDir, "task-001", "accepted");
124
+ assert.throws(() => acceptSubgoal(goal_id, "subgoal-999", tempDir), (err) => {
125
+ assert.ok(err instanceof PatchWardenError);
126
+ assert.equal(err.reason, "subgoal_not_found");
127
+ return true;
128
+ });
129
+ });
130
+ it("goal 不存在 → 抛 goal_not_found", () => {
131
+ assert.throws(() => acceptSubgoal("goal_nonexistent", "subgoal-001", tempDir), (err) => {
132
+ assert.ok(err instanceof PatchWardenError);
133
+ assert.equal(err.reason, "goal_not_found");
134
+ return true;
135
+ });
136
+ });
137
+ it("task status.json 不存在 → 视为未 accepted,抛 subgoal_not_ready", () => {
138
+ const { goal_id, subgoal_id } = setupGoalWithSubgoal(tempDir, {
139
+ subgoalStatus: "done_by_agent",
140
+ taskIds: ["task-missing"],
141
+ });
142
+ // 不创建 task 文件
143
+ assert.throws(() => acceptSubgoal(goal_id, subgoal_id, tempDir), (err) => {
144
+ assert.ok(err instanceof PatchWardenError);
145
+ assert.equal(err.reason, "subgoal_not_ready");
146
+ const details = err.details;
147
+ assert.equal(details.unaccepted_tasks[0].current_status, "missing");
148
+ return true;
149
+ });
150
+ });
151
+ });
152
+ describe("rejectSubgoal", () => {
153
+ it("从 running 状态 reject → 成功,status 变为 rejected,rejected_reason 记录", () => {
154
+ const { goal_id, subgoal_id } = setupGoalWithSubgoal(tempDir, {
155
+ subgoalStatus: "running",
156
+ });
157
+ const result = rejectSubgoal(goal_id, subgoal_id, "Tests failed", tempDir);
158
+ assert.equal(result.subgoal_id, subgoal_id);
159
+ assert.equal(result.status, "rejected");
160
+ assert.equal(result.rejected_reason, "Tests failed");
161
+ // 验证已持久化
162
+ const status = readGoalStatus(goal_id, tempDir);
163
+ const sub = status.subgoals.find((s) => s.id === subgoal_id);
164
+ assert.equal(sub.status, "rejected");
165
+ assert.equal(sub.rejected_reason, "Tests failed");
166
+ });
167
+ it("从 ready 状态 reject → 成功", () => {
168
+ const { goal_id, subgoal_id } = setupGoalWithSubgoal(tempDir, {
169
+ subgoalStatus: "ready",
170
+ });
171
+ const result = rejectSubgoal(goal_id, subgoal_id, "Not needed", tempDir);
172
+ assert.equal(result.status, "rejected");
173
+ assert.equal(result.rejected_reason, "Not needed");
174
+ const status = readGoalStatus(goal_id, tempDir);
175
+ const sub = status.subgoals.find((s) => s.id === subgoal_id);
176
+ assert.equal(sub.status, "rejected");
177
+ });
178
+ it("从 done_by_agent 状态 reject → 成功", () => {
179
+ const { goal_id, subgoal_id } = setupGoalWithSubgoal(tempDir, {
180
+ subgoalStatus: "done_by_agent",
181
+ });
182
+ const result = rejectSubgoal(goal_id, subgoal_id, "Quality issues", tempDir);
183
+ assert.equal(result.status, "rejected");
184
+ assert.equal(result.rejected_reason, "Quality issues");
185
+ const status = readGoalStatus(goal_id, tempDir);
186
+ const sub = status.subgoals.find((s) => s.id === subgoal_id);
187
+ assert.equal(sub.status, "rejected");
188
+ });
189
+ it("从 needs_fix 状态 reject → 成功", () => {
190
+ const { goal_id, subgoal_id } = setupGoalWithSubgoal(tempDir, {
191
+ subgoalStatus: "needs_fix",
192
+ });
193
+ const result = rejectSubgoal(goal_id, subgoal_id, "Abandoning fix", tempDir);
194
+ assert.equal(result.status, "rejected");
195
+ assert.equal(result.rejected_reason, "Abandoning fix");
196
+ });
197
+ it("从 accepted 状态 reject → 抛 invalid_status_transition", () => {
198
+ const { goal_id, subgoal_id } = setupGoalWithSubgoal(tempDir, {
199
+ subgoalStatus: "accepted",
200
+ });
201
+ assert.throws(() => rejectSubgoal(goal_id, subgoal_id, "Try to reject", tempDir), (err) => {
202
+ assert.ok(err instanceof PatchWardenError);
203
+ assert.equal(err.reason, "invalid_status_transition");
204
+ const details = err.details;
205
+ assert.equal(details.from_status, "accepted");
206
+ assert.equal(details.to_status, "rejected");
207
+ return true;
208
+ });
209
+ // 验证状态未改变
210
+ const status = readGoalStatus(goal_id, tempDir);
211
+ const sub = status.subgoals.find((s) => s.id === subgoal_id);
212
+ assert.equal(sub.status, "accepted");
213
+ });
214
+ it("从 rejected 状态 reject → 抛 invalid_status_transition", () => {
215
+ const { goal_id, subgoal_id } = setupGoalWithSubgoal(tempDir, {
216
+ subgoalStatus: "rejected",
217
+ });
218
+ assert.throws(() => rejectSubgoal(goal_id, subgoal_id, "Reject again", tempDir), (err) => {
219
+ assert.ok(err instanceof PatchWardenError);
220
+ assert.equal(err.reason, "invalid_status_transition");
221
+ const details = err.details;
222
+ assert.equal(details.from_status, "rejected");
223
+ return true;
224
+ });
225
+ });
226
+ it("subgoal 不存在 → 抛 subgoal_not_found", () => {
227
+ const { goal_id } = setupGoalWithSubgoal(tempDir);
228
+ assert.throws(() => rejectSubgoal(goal_id, "subgoal-999", "reason", tempDir), (err) => {
229
+ assert.ok(err instanceof PatchWardenError);
230
+ assert.equal(err.reason, "subgoal_not_found");
231
+ return true;
232
+ });
233
+ });
234
+ it("goal 不存在 → 抛 goal_not_found", () => {
235
+ assert.throws(() => rejectSubgoal("goal_nonexistent", "subgoal-001", "reason", tempDir), (err) => {
236
+ assert.ok(err instanceof PatchWardenError);
237
+ assert.equal(err.reason, "goal_not_found");
238
+ return true;
239
+ });
240
+ });
241
+ });
242
+ describe("summarizeGoalProgress", () => {
243
+ it("空 subgoals → total=0, completion_rate=0%", () => {
244
+ const { goal_id } = createGoal("repo", "Empty Goal", "desc", tempDir);
245
+ const summary = summarizeGoalProgress(goal_id, tempDir);
246
+ assert.equal(summary.goal_id, goal_id);
247
+ assert.equal(summary.title, "Empty Goal");
248
+ assert.equal(summary.total, 0);
249
+ assert.equal(summary.accepted, 0);
250
+ assert.equal(summary.rejected, 0);
251
+ assert.equal(summary.running, 0);
252
+ assert.equal(summary.ready, 0);
253
+ assert.equal(summary.needs_fix, 0);
254
+ assert.equal(summary.done_by_agent, 0);
255
+ assert.equal(summary.completion_rate, "0%");
256
+ assert.deepEqual(summary.blocked_subgoals, []);
257
+ assert.deepEqual(summary.risks, []);
258
+ });
259
+ it("混合状态 → 正确计数", () => {
260
+ const { goal_id } = createGoal("repo", "Mixed Goal", "desc", tempDir);
261
+ let status = readGoalStatus(goal_id, tempDir);
262
+ // subgoal-001: accepted
263
+ status = addSubgoal(status, "S1").goalStatus;
264
+ status = updateSubgoalStatus(status, "subgoal-001", "running");
265
+ status = updateSubgoalStatus(status, "subgoal-001", "done_by_agent");
266
+ status = updateSubgoalStatus(status, "subgoal-001", "accepted");
267
+ // subgoal-002: running
268
+ status = addSubgoal(status, "S2").goalStatus;
269
+ status = updateSubgoalStatus(status, "subgoal-002", "running");
270
+ // subgoal-003: ready
271
+ status = addSubgoal(status, "S3").goalStatus;
272
+ // subgoal-004: rejected
273
+ status = addSubgoal(status, "S4").goalStatus;
274
+ status = updateSubgoalStatus(status, "subgoal-004", "running");
275
+ status = updateSubgoalStatus(status, "subgoal-004", "done_by_agent");
276
+ status = updateSubgoalStatus(status, "subgoal-004", "rejected", { rejected_reason: "bad" });
277
+ // subgoal-005: needs_fix
278
+ status = addSubgoal(status, "S5").goalStatus;
279
+ status = updateSubgoalStatus(status, "subgoal-005", "running");
280
+ status = updateSubgoalStatus(status, "subgoal-005", "done_by_agent");
281
+ status = updateSubgoalStatus(status, "subgoal-005", "needs_fix");
282
+ // subgoal-006: done_by_agent
283
+ status = addSubgoal(status, "S6").goalStatus;
284
+ status = updateSubgoalStatus(status, "subgoal-006", "running");
285
+ status = updateSubgoalStatus(status, "subgoal-006", "done_by_agent");
286
+ writeGoalStatus(goal_id, status, tempDir);
287
+ const summary = summarizeGoalProgress(goal_id, tempDir);
288
+ assert.equal(summary.total, 6);
289
+ assert.equal(summary.accepted, 1);
290
+ assert.equal(summary.rejected, 1);
291
+ assert.equal(summary.running, 1);
292
+ assert.equal(summary.ready, 1);
293
+ assert.equal(summary.needs_fix, 1);
294
+ assert.equal(summary.done_by_agent, 1);
295
+ // 1/6 = 16.67% → round to 17%
296
+ assert.equal(summary.completion_rate, "17%");
297
+ });
298
+ it("有 blocked subgoal → blocked_subgoals 非空", () => {
299
+ const { goal_id } = createGoal("repo", "Blocked Goal", "desc", tempDir);
300
+ let status = readGoalStatus(goal_id, tempDir);
301
+ // subgoal-001: running(未 accepted,作为阻塞源)
302
+ status = addSubgoal(status, "Blocker").goalStatus;
303
+ status = updateSubgoalStatus(status, "subgoal-001", "running");
304
+ // subgoal-002: ready 但依赖 subgoal-001(未 accepted)→ blocked
305
+ status = addSubgoal(status, "Blocked", ["subgoal-001"]).goalStatus;
306
+ writeGoalStatus(goal_id, status, tempDir);
307
+ const summary = summarizeGoalProgress(goal_id, tempDir);
308
+ assert.equal(summary.blocked_subgoals.length, 1);
309
+ assert.equal(summary.blocked_subgoals[0].subgoal_id, "subgoal-002");
310
+ assert.equal(summary.blocked_subgoals[0].title, "Blocked");
311
+ assert.deepEqual(summary.blocked_subgoals[0].blocked_by, ["subgoal-001"]);
312
+ });
313
+ it("有 needs_fix → risks 非空", () => {
314
+ const { goal_id } = createGoal("repo", "Risk Goal", "desc", tempDir);
315
+ let status = readGoalStatus(goal_id, tempDir);
316
+ status = addSubgoal(status, "NeedsFix").goalStatus;
317
+ status = updateSubgoalStatus(status, "subgoal-001", "running");
318
+ status = updateSubgoalStatus(status, "subgoal-001", "done_by_agent");
319
+ status = updateSubgoalStatus(status, "subgoal-001", "needs_fix");
320
+ writeGoalStatus(goal_id, status, tempDir);
321
+ const summary = summarizeGoalProgress(goal_id, tempDir);
322
+ assert.equal(summary.risks.length, 1);
323
+ assert.equal(summary.risks[0].subgoal_id, "subgoal-001");
324
+ assert.equal(summary.risks[0].title, "NeedsFix");
325
+ assert.equal(summary.risks[0].status, "needs_fix");
326
+ assert.equal(summary.risks[0].reason, "needs_fix");
327
+ });
328
+ it("有 running → risks 非空", () => {
329
+ const { goal_id } = createGoal("repo", "Running Goal", "desc", tempDir);
330
+ let status = readGoalStatus(goal_id, tempDir);
331
+ status = addSubgoal(status, "Running").goalStatus;
332
+ status = updateSubgoalStatus(status, "subgoal-001", "running");
333
+ writeGoalStatus(goal_id, status, tempDir);
334
+ const summary = summarizeGoalProgress(goal_id, tempDir);
335
+ assert.equal(summary.risks.length, 1);
336
+ assert.equal(summary.risks[0].subgoal_id, "subgoal-001");
337
+ assert.equal(summary.risks[0].status, "running");
338
+ assert.equal(summary.risks[0].reason, "running");
339
+ });
340
+ it("全部 accepted → completion_rate=100%", () => {
341
+ const { goal_id } = createGoal("repo", "All Accepted", "desc", tempDir);
342
+ let status = readGoalStatus(goal_id, tempDir);
343
+ status = addSubgoal(status, "S1").goalStatus;
344
+ status = updateSubgoalStatus(status, "subgoal-001", "running");
345
+ status = updateSubgoalStatus(status, "subgoal-001", "done_by_agent");
346
+ status = updateSubgoalStatus(status, "subgoal-001", "accepted");
347
+ status = addSubgoal(status, "S2").goalStatus;
348
+ status = updateSubgoalStatus(status, "subgoal-002", "running");
349
+ status = updateSubgoalStatus(status, "subgoal-002", "done_by_agent");
350
+ status = updateSubgoalStatus(status, "subgoal-002", "accepted");
351
+ writeGoalStatus(goal_id, status, tempDir);
352
+ const summary = summarizeGoalProgress(goal_id, tempDir);
353
+ assert.equal(summary.total, 2);
354
+ assert.equal(summary.accepted, 2);
355
+ assert.equal(summary.completion_rate, "100%");
356
+ assert.equal(summary.risks.length, 0);
357
+ assert.equal(summary.blocked_subgoals.length, 0);
358
+ });
359
+ it("goal 不存在 → 抛 goal_not_found", () => {
360
+ assert.throws(() => summarizeGoalProgress("goal_nonexistent", tempDir), (err) => {
361
+ assert.ok(err instanceof PatchWardenError);
362
+ assert.equal(err.reason, "goal_not_found");
363
+ return true;
364
+ });
365
+ });
366
+ it("completion_rate 取整正确(1/3 → 33%)", () => {
367
+ const { goal_id } = createGoal("repo", "Third Goal", "desc", tempDir);
368
+ let status = readGoalStatus(goal_id, tempDir);
369
+ // 1 accepted, 2 ready
370
+ status = addSubgoal(status, "S1").goalStatus;
371
+ status = updateSubgoalStatus(status, "subgoal-001", "running");
372
+ status = updateSubgoalStatus(status, "subgoal-001", "done_by_agent");
373
+ status = updateSubgoalStatus(status, "subgoal-001", "accepted");
374
+ status = addSubgoal(status, "S2").goalStatus;
375
+ status = addSubgoal(status, "S3").goalStatus;
376
+ writeGoalStatus(goal_id, status, tempDir);
377
+ const summary = summarizeGoalProgress(goal_id, tempDir);
378
+ assert.equal(summary.completion_rate, "33%");
379
+ });
380
+ });
381
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,215 @@
1
+ import { describe, it } from "node:test";
2
+ import { strict as assert } from "node:assert";
3
+ import { createInitialGoalStatus, addSubgoal, updateSubgoalStatus, linkTaskToSubgoal, } from "../../goal/goalStatus.js";
4
+ import { PatchWardenError } from "../../errors.js";
5
+ // ── Helpers ───────────────────────────────────────────────────────
6
+ function makeInitialStatus() {
7
+ return createInitialGoalStatus("goal_test_001", "Test Goal", "/repo/test");
8
+ }
9
+ // ── Tests ─────────────────────────────────────────────────────────
10
+ describe("goalStatus", () => {
11
+ describe("createInitialGoalStatus", () => {
12
+ it("返回正确的初始字段", () => {
13
+ const status = createInitialGoalStatus("goal_20260101_my_goal", "My Goal", "/repo/path");
14
+ assert.equal(status.goal_id, "goal_20260101_my_goal");
15
+ assert.equal(status.title, "My Goal");
16
+ assert.equal(status.status, "active");
17
+ assert.equal(status.repo_path, "/repo/path");
18
+ assert.ok(status.created_at.length > 0);
19
+ assert.equal(status.created_at, status.updated_at);
20
+ assert.deepEqual(status.subgoals, []);
21
+ });
22
+ });
23
+ describe("addSubgoal", () => {
24
+ it("成功生成 subgoal-001", () => {
25
+ const initial = makeInitialStatus();
26
+ const { goalStatus, subgoalId } = addSubgoal(initial, "First subgoal");
27
+ assert.equal(subgoalId, "subgoal-001");
28
+ assert.equal(goalStatus.subgoals.length, 1);
29
+ const sub = goalStatus.subgoals[0];
30
+ assert.equal(sub.id, "subgoal-001");
31
+ assert.equal(sub.title, "First subgoal");
32
+ assert.equal(sub.status, "ready");
33
+ assert.deepEqual(sub.depends_on, []);
34
+ assert.deepEqual(sub.task_ids, []);
35
+ });
36
+ it("多个 subgoal 编号递增", () => {
37
+ const initial = makeInitialStatus();
38
+ const r1 = addSubgoal(initial, "A");
39
+ const r2 = addSubgoal(r1.goalStatus, "B");
40
+ const r3 = addSubgoal(r2.goalStatus, "C");
41
+ assert.equal(r1.subgoalId, "subgoal-001");
42
+ assert.equal(r2.subgoalId, "subgoal-002");
43
+ assert.equal(r3.subgoalId, "subgoal-003");
44
+ assert.equal(r3.goalStatus.subgoals.length, 3);
45
+ });
46
+ it("三位补零到 010", () => {
47
+ let status = makeInitialStatus();
48
+ for (let i = 1; i <= 10; i++) {
49
+ const r = addSubgoal(status, `Sub ${i}`);
50
+ status = r.goalStatus;
51
+ if (i === 10) {
52
+ assert.equal(r.subgoalId, "subgoal-010");
53
+ }
54
+ }
55
+ });
56
+ it("无效依赖抛出 invalid_dependency", () => {
57
+ const initial = makeInitialStatus();
58
+ assert.throws(() => addSubgoal(initial, "Bad dep", ["subgoal-999"]), (err) => {
59
+ assert.ok(err instanceof PatchWardenError);
60
+ assert.equal(err.reason, "invalid_dependency");
61
+ return true;
62
+ });
63
+ });
64
+ it("有效依赖成功添加", () => {
65
+ const initial = makeInitialStatus();
66
+ const r1 = addSubgoal(initial, "Parent");
67
+ const r2 = addSubgoal(r1.goalStatus, "Child", ["subgoal-001"]);
68
+ assert.equal(r2.subgoalId, "subgoal-002");
69
+ assert.deepEqual(r2.goalStatus.subgoals[1].depends_on, ["subgoal-001"]);
70
+ });
71
+ it("不修改原 goalStatus(不可变)", () => {
72
+ const initial = makeInitialStatus();
73
+ const originalLen = initial.subgoals.length;
74
+ addSubgoal(initial, "New");
75
+ assert.equal(initial.subgoals.length, originalLen);
76
+ });
77
+ it("更新 updated_at", () => {
78
+ const initial = makeInitialStatus();
79
+ const originalUpdatedAt = initial.updated_at;
80
+ const { goalStatus } = addSubgoal(initial, "New");
81
+ // updated_at 应该是新的 ISO 时间字符串
82
+ assert.ok(goalStatus.updated_at >= originalUpdatedAt);
83
+ });
84
+ });
85
+ describe("updateSubgoalStatus", () => {
86
+ it("合法转换 ready → running", () => {
87
+ let status = makeInitialStatus();
88
+ const r = addSubgoal(status, "Sub");
89
+ status = r.goalStatus;
90
+ const updated = updateSubgoalStatus(status, "subgoal-001", "running");
91
+ assert.equal(updated.subgoals[0].status, "running");
92
+ });
93
+ it("合法完整链路 ready → running → done_by_agent → accepted", () => {
94
+ let status = makeInitialStatus();
95
+ status = addSubgoal(status, "Sub").goalStatus;
96
+ status = updateSubgoalStatus(status, "subgoal-001", "running");
97
+ status = updateSubgoalStatus(status, "subgoal-001", "done_by_agent");
98
+ status = updateSubgoalStatus(status, "subgoal-001", "accepted");
99
+ assert.equal(status.subgoals[0].status, "accepted");
100
+ assert.ok(status.subgoals[0].accepted_at !== undefined);
101
+ assert.ok(status.subgoals[0].accepted_at.length > 0);
102
+ });
103
+ it("done_by_agent → rejected 需要 reason", () => {
104
+ let status = makeInitialStatus();
105
+ status = addSubgoal(status, "Sub").goalStatus;
106
+ status = updateSubgoalStatus(status, "subgoal-001", "running");
107
+ status = updateSubgoalStatus(status, "subgoal-001", "done_by_agent");
108
+ // 缺少 reason 抛错
109
+ assert.throws(() => updateSubgoalStatus(status, "subgoal-001", "rejected"), (err) => {
110
+ assert.ok(err instanceof PatchWardenError);
111
+ assert.equal(err.reason, "invalid_status_transition");
112
+ return true;
113
+ });
114
+ // 提供 reason 成功
115
+ const updated = updateSubgoalStatus(status, "subgoal-001", "rejected", {
116
+ rejected_reason: "Tests failed",
117
+ });
118
+ assert.equal(updated.subgoals[0].status, "rejected");
119
+ assert.equal(updated.subgoals[0].rejected_reason, "Tests failed");
120
+ });
121
+ it("合法分支 done_by_agent → needs_fix → running", () => {
122
+ let status = makeInitialStatus();
123
+ status = addSubgoal(status, "Sub").goalStatus;
124
+ status = updateSubgoalStatus(status, "subgoal-001", "running");
125
+ status = updateSubgoalStatus(status, "subgoal-001", "done_by_agent");
126
+ status = updateSubgoalStatus(status, "subgoal-001", "needs_fix");
127
+ assert.equal(status.subgoals[0].status, "needs_fix");
128
+ status = updateSubgoalStatus(status, "subgoal-001", "running");
129
+ assert.equal(status.subgoals[0].status, "running");
130
+ });
131
+ it("非法转换 ready → accepted 抛错", () => {
132
+ const initial = makeInitialStatus();
133
+ const status = addSubgoal(initial, "Sub").goalStatus;
134
+ assert.throws(() => updateSubgoalStatus(status, "subgoal-001", "accepted"), (err) => {
135
+ assert.ok(err instanceof PatchWardenError);
136
+ assert.equal(err.reason, "invalid_status_transition");
137
+ return true;
138
+ });
139
+ });
140
+ it("非法转换 running → ready 抛错", () => {
141
+ let status = makeInitialStatus();
142
+ status = addSubgoal(status, "Sub").goalStatus;
143
+ status = updateSubgoalStatus(status, "subgoal-001", "running");
144
+ assert.throws(() => updateSubgoalStatus(status, "subgoal-001", "ready"), (err) => {
145
+ assert.ok(err instanceof PatchWardenError);
146
+ assert.equal(err.reason, "invalid_status_transition");
147
+ return true;
148
+ });
149
+ });
150
+ it("终态 accepted → running 抛错", () => {
151
+ let status = makeInitialStatus();
152
+ status = addSubgoal(status, "Sub").goalStatus;
153
+ status = updateSubgoalStatus(status, "subgoal-001", "running");
154
+ status = updateSubgoalStatus(status, "subgoal-001", "done_by_agent");
155
+ status = updateSubgoalStatus(status, "subgoal-001", "accepted");
156
+ assert.throws(() => updateSubgoalStatus(status, "subgoal-001", "running"), (err) => {
157
+ assert.ok(err instanceof PatchWardenError);
158
+ assert.equal(err.reason, "invalid_status_transition");
159
+ return true;
160
+ });
161
+ });
162
+ it("subgoal 不存在抛出 subgoal_not_found", () => {
163
+ const initial = makeInitialStatus();
164
+ assert.throws(() => updateSubgoalStatus(initial, "subgoal-999", "running"), (err) => {
165
+ assert.ok(err instanceof PatchWardenError);
166
+ assert.equal(err.reason, "subgoal_not_found");
167
+ return true;
168
+ });
169
+ });
170
+ it("不修改原 goalStatus(不可变)", () => {
171
+ let status = makeInitialStatus();
172
+ status = addSubgoal(status, "Sub").goalStatus;
173
+ const originalStatus = status.subgoals[0].status;
174
+ updateSubgoalStatus(status, "subgoal-001", "running");
175
+ assert.equal(status.subgoals[0].status, originalStatus);
176
+ });
177
+ });
178
+ describe("linkTaskToSubgoal", () => {
179
+ it("成功追加 taskId", () => {
180
+ let status = makeInitialStatus();
181
+ status = addSubgoal(status, "Sub").goalStatus;
182
+ const updated = linkTaskToSubgoal(status, "subgoal-001", "task_abc");
183
+ assert.deepEqual(updated.subgoals[0].task_ids, ["task_abc"]);
184
+ });
185
+ it("追加多个 taskId", () => {
186
+ let status = makeInitialStatus();
187
+ status = addSubgoal(status, "Sub").goalStatus;
188
+ status = linkTaskToSubgoal(status, "subgoal-001", "task_a");
189
+ status = linkTaskToSubgoal(status, "subgoal-001", "task_b");
190
+ assert.deepEqual(status.subgoals[0].task_ids, ["task_a", "task_b"]);
191
+ });
192
+ it("去重:相同 taskId 不重复添加", () => {
193
+ let status = makeInitialStatus();
194
+ status = addSubgoal(status, "Sub").goalStatus;
195
+ status = linkTaskToSubgoal(status, "subgoal-001", "task_a");
196
+ status = linkTaskToSubgoal(status, "subgoal-001", "task_a");
197
+ assert.deepEqual(status.subgoals[0].task_ids, ["task_a"]);
198
+ });
199
+ it("subgoal 不存在抛出 subgoal_not_found", () => {
200
+ const initial = makeInitialStatus();
201
+ assert.throws(() => linkTaskToSubgoal(initial, "subgoal-999", "task_a"), (err) => {
202
+ assert.ok(err instanceof PatchWardenError);
203
+ assert.equal(err.reason, "subgoal_not_found");
204
+ return true;
205
+ });
206
+ });
207
+ it("不修改原 goalStatus(不可变)", () => {
208
+ let status = makeInitialStatus();
209
+ status = addSubgoal(status, "Sub").goalStatus;
210
+ const originalLen = status.subgoals[0].task_ids.length;
211
+ linkTaskToSubgoal(status, "subgoal-001", "task_a");
212
+ assert.equal(status.subgoals[0].task_ids.length, originalLen);
213
+ });
214
+ });
215
+ });
@@ -0,0 +1 @@
1
+ export {};