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,247 @@
1
+ import { describe, it, before, after } from "node:test";
2
+ import { strict as assert } from "node:assert";
3
+ import {
4
+ mkdtempSync,
5
+ rmSync,
6
+ writeFileSync,
7
+ readFileSync,
8
+ existsSync,
9
+ readdirSync,
10
+ unlinkSync,
11
+ } from "node:fs";
12
+ import { join } from "node:path";
13
+ import { tmpdir } from "node:os";
14
+ import { execSync } from "node:child_process";
15
+ import {
16
+ createWorktree,
17
+ mergeWorktree,
18
+ discardWorktree,
19
+ readWorktreeStatus,
20
+ getWorktreesDir,
21
+ type WorktreeStatus,
22
+ } from "../../goal/worktreeManager.js";
23
+ import { PatchWardenError } from "../../errors.js";
24
+
25
+ // ── Test fixtures ─────────────────────────────────────────────────
26
+
27
+ /**
28
+ * 在 dir 创建一个真实的 git 仓库:git init + 配置 user + 初始 commit。
29
+ * createWorktree 要求 workspaceRoot 是一个至少有一个 commit 的 git 仓库。
30
+ */
31
+ function initGitRepo(dir: string): void {
32
+ execSync("git init", { cwd: dir, stdio: "ignore" });
33
+ execSync('git config user.email "test@patchwarden.local"', { cwd: dir, stdio: "ignore" });
34
+ execSync('git config user.name "PatchWarden Test"', { cwd: dir, stdio: "ignore" });
35
+ execSync('git config commit.gpgsign false', { cwd: dir, stdio: "ignore" });
36
+ writeFileSync(join(dir, "README.md"), "init\n", "utf-8");
37
+ execSync("git add README.md", { cwd: dir, stdio: "ignore" });
38
+ execSync("git commit -m init", { cwd: dir, stdio: "ignore" });
39
+ }
40
+
41
+ function gitBranchList(cwd: string): string {
42
+ return execSync("git branch --list", { cwd, stdio: ["pipe", "pipe", "ignore"] }).toString("utf-8");
43
+ }
44
+
45
+ function assertPatchWardenError(fn: () => void, reason: string): void {
46
+ try {
47
+ fn();
48
+ assert.fail(`expected PatchWardenError("${reason}") to be thrown`);
49
+ } catch (err) {
50
+ assert.ok(err instanceof PatchWardenError, `expected PatchWardenError, got: ${err}`);
51
+ assert.equal((err as PatchWardenError).reason, reason);
52
+ }
53
+ }
54
+
55
+ // ── Tests ─────────────────────────────────────────────────────────
56
+
57
+ describe("worktreeManager", () => {
58
+ let workspaceRoot: string;
59
+
60
+ before(() => {
61
+ workspaceRoot = mkdtempSync(join(tmpdir(), "pw-wt-"));
62
+ initGitRepo(workspaceRoot);
63
+ });
64
+
65
+ after(() => {
66
+ rmSync(workspaceRoot, { recursive: true, force: true });
67
+ });
68
+
69
+ // ── 1. createWorktree 成功 ──
70
+ it("createWorktree creates an active worktree with valid status.json", () => {
71
+ const result = createWorktree("goal_20260101_demo", "subgoal-001", workspaceRoot);
72
+
73
+ assert.ok(result.worktreeId.startsWith("wt_"), `worktreeId should start with "wt_", got: ${result.worktreeId}`);
74
+ assert.ok(result.branch.startsWith("pw-goal_20260101_demo-subgoal-001"), `branch should be pw-<goal>-<subgoal>, got: ${result.branch}`);
75
+ assert.ok(existsSync(result.worktreePath), "worktreePath should exist");
76
+
77
+ const statusFile = join(result.worktreePath, "worktree_status.json");
78
+ assert.ok(existsSync(statusFile), "worktree_status.json should exist");
79
+
80
+ // 原子写:不应残留 .tmp 文件
81
+ assert.ok(!existsSync(statusFile + ".tmp"), "status.json.tmp should not remain after atomic write");
82
+
83
+ // JSON 合法且字段正确
84
+ const status = JSON.parse(readFileSync(statusFile, "utf-8")) as WorktreeStatus;
85
+ assert.equal(status.worktree_id, result.worktreeId);
86
+ assert.equal(status.goal_id, "goal_20260101_demo");
87
+ assert.equal(status.subgoal_id, "subgoal-001");
88
+ assert.equal(status.status, "active");
89
+ assert.equal(status.branch, result.branch);
90
+ assert.equal(status.path, result.worktreePath);
91
+ assert.ok(typeof status.created_at === "string" && status.created_at.length > 0);
92
+
93
+ // worktree 内应检出主仓库的文件(README.md)
94
+ assert.ok(existsSync(join(result.worktreePath, "README.md")), "worktree should check out README.md");
95
+
96
+ // branch 应在 git 中存在
97
+ assert.ok(gitBranchList(workspaceRoot).includes(result.branch), "branch should exist in git");
98
+
99
+ // readWorktreeStatus 应返回一致的状态
100
+ const readBack = readWorktreeStatus(result.worktreeId, workspaceRoot);
101
+ assert.ok(readBack !== null);
102
+ assert.equal(readBack.status, "active");
103
+ });
104
+
105
+ // ── 2. createWorktree git 命令失败清理 ──
106
+ it("createWorktree cleans up and throws when git fails (non-git workspaceRoot)", () => {
107
+ const nonGitDir = mkdtempSync(join(tmpdir(), "pw-wt-nogit-"));
108
+ try {
109
+ // 非 git 目录 → git worktree add 失败
110
+ assertPatchWardenError(
111
+ () => createWorktree("goal_x", "subgoal-001", nonGitDir),
112
+ "worktree_create_failed"
113
+ );
114
+
115
+ // 不应残留任何 worktree 目录
116
+ const worktreesDir = getWorktreesDir(nonGitDir);
117
+ if (existsSync(worktreesDir)) {
118
+ // 若 _workspacetrees 被创建,应为空(无半成品 worktree)
119
+ const entries = readdirSync(worktreesDir);
120
+ assert.equal(entries.length, 0, "no leftover worktree directories should remain after failure");
121
+ }
122
+ } finally {
123
+ rmSync(nonGitDir, { recursive: true, force: true });
124
+ }
125
+ });
126
+
127
+ // ── 3. mergeWorktree 成功 ──
128
+ it("mergeWorktree merges the branch back and marks status merged", () => {
129
+ const created = createWorktree("goal_merge", "subgoal-001", workspaceRoot);
130
+
131
+ // 在 worktree 中产生一个新 commit,使 merge 有实际内容
132
+ writeFileSync(join(created.worktreePath, "feature.txt"), "merged feature\n", "utf-8");
133
+ execSync("git add feature.txt", { cwd: created.worktreePath, stdio: "ignore" });
134
+ execSync("git commit -m feature", { cwd: created.worktreePath, stdio: "ignore" });
135
+
136
+ const result = mergeWorktree(created.worktreeId, workspaceRoot);
137
+ assert.equal(result.status, "merged");
138
+
139
+ // 主工作区应出现 feature.txt(fast-forward 合并)
140
+ assert.ok(existsSync(join(workspaceRoot, "feature.txt")), "feature.txt should appear in main workspace after merge");
141
+
142
+ // status 应更新为 merged,merged_at 非空
143
+ const status = readWorktreeStatus(created.worktreeId, workspaceRoot);
144
+ assert.ok(status !== null);
145
+ assert.equal(status.status, "merged");
146
+ assert.ok(typeof status.merged_at === "string" && status.merged_at.length > 0, "merged_at should be set");
147
+ });
148
+
149
+ // ── 4. discardWorktree 成功 ──
150
+ it("discardWorktree removes worktree, deletes branch, and archives status", () => {
151
+ const created = createWorktree("goal_discard", "subgoal-001", workspaceRoot);
152
+ const branch = created.branch;
153
+ const worktreePath = created.worktreePath;
154
+
155
+ const result = discardWorktree(created.worktreeId, workspaceRoot);
156
+ assert.equal(result.status, "discarded");
157
+
158
+ // worktree 目录应被移除
159
+ assert.ok(!existsSync(worktreePath), "worktree directory should be removed after discard");
160
+
161
+ // 临时 branch 应被删除
162
+ assert.ok(!gitBranchList(workspaceRoot).includes(branch), "temporary branch should be deleted after discard");
163
+
164
+ // 归档文件应存在且 status="discarded"
165
+ const archiveFile = join(workspaceRoot, ".patchwarden", "worktree-archive", `${created.worktreeId}.json`);
166
+ assert.ok(existsSync(archiveFile), "archived status file should exist");
167
+ const archived = JSON.parse(readFileSync(archiveFile, "utf-8")) as WorktreeStatus;
168
+ assert.equal(archived.status, "discarded");
169
+ assert.ok(typeof archived.discarded_at === "string" && archived.discarded_at.length > 0, "discarded_at should be set");
170
+ assert.equal(archived.worktree_id, created.worktreeId);
171
+ });
172
+
173
+ // ── 5. 路径逃逸拦截 ──
174
+ it("blocks path escape via worktreeId containing ..", () => {
175
+ // worktreeId 在 read/merge/discard 中由调用方提供,构造逃逸路径
176
+ assertPatchWardenError(
177
+ () => readWorktreeStatus("../../../outside", workspaceRoot),
178
+ "workspace_path_escape"
179
+ );
180
+ assertPatchWardenError(
181
+ () => mergeWorktree("../../../outside", workspaceRoot),
182
+ "workspace_path_escape"
183
+ );
184
+ assertPatchWardenError(
185
+ () => discardWorktree("../../../outside", workspaceRoot),
186
+ "workspace_path_escape"
187
+ );
188
+ });
189
+
190
+ // ── 6. 敏感路径拦截 ──
191
+ it("blocks sensitive worktreeId via guardSensitivePath", () => {
192
+ // "credentials" 命中 sensitiveGuard 的敏感模式
193
+ assertPatchWardenError(
194
+ () => readWorktreeStatus("credentials", workspaceRoot),
195
+ "sensitive_path_blocked"
196
+ );
197
+ assertPatchWardenError(
198
+ () => mergeWorktree("credentials", workspaceRoot),
199
+ "sensitive_path_blocked"
200
+ );
201
+ assertPatchWardenError(
202
+ () => discardWorktree("credentials", workspaceRoot),
203
+ "sensitive_path_blocked"
204
+ );
205
+ });
206
+
207
+ // ── 7. readWorktreeStatus 返回 null 当 status 不存在 ──
208
+ it("readWorktreeStatus returns null when status file does not exist", () => {
209
+ // 创建一个 worktree 目录但不含 status 文件
210
+ const created = createWorktree("goal_missing", "subgoal-001", workspaceRoot);
211
+ // 手动删除 status 文件模拟缺失
212
+ unlinkSync(join(created.worktreePath, "worktree_status.json"));
213
+ assert.equal(readWorktreeStatus(created.worktreeId, workspaceRoot), null);
214
+ // 清理:删除该 worktree(避免污染后续测试)
215
+ try {
216
+ execSync(`git worktree remove --force "${created.worktreePath}"`, { cwd: workspaceRoot, stdio: "ignore" });
217
+ } catch { /* ignore */ }
218
+ try {
219
+ execSync(`git branch -D ${created.branch}`, { cwd: workspaceRoot, stdio: "ignore" });
220
+ } catch { /* ignore */ }
221
+ });
222
+
223
+ // ── 8. 非法状态转换:merge/discard 已 merged 的 worktree 抛错 ──
224
+ it("rejects merge/discard on non-active worktree", () => {
225
+ // 复用 #3 已 merged 的 worktree(goal_merge/subgoal-001)
226
+ // 先找到它的 worktreeId:扫描 _workspacetrees 目录
227
+ const worktreesDir = getWorktreesDir(workspaceRoot);
228
+ let mergedId: string | null = null;
229
+ for (const entry of readdirSync(worktreesDir)) {
230
+ const s = readWorktreeStatus(entry, workspaceRoot);
231
+ if (s && s.status === "merged" && s.goal_id === "goal_merge") {
232
+ mergedId = entry;
233
+ break;
234
+ }
235
+ }
236
+ assert.ok(mergedId !== null, "should find the merged worktree from the merge test");
237
+
238
+ assertPatchWardenError(
239
+ () => mergeWorktree(mergedId as string, workspaceRoot),
240
+ "invalid_worktree_state"
241
+ );
242
+ assertPatchWardenError(
243
+ () => discardWorktree(mergedId as string, workspaceRoot),
244
+ "invalid_worktree_state"
245
+ );
246
+ });
247
+ });