patchwarden 1.1.0 → 1.5.1

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 (258) hide show
  1. package/README.en.md +83 -8
  2. package/README.md +78 -10
  3. package/dist/assessments/agentAssessor.d.ts +1 -1
  4. package/dist/assessments/agentAssessor.js +3 -3
  5. package/dist/assessments/assessmentStore.d.ts +1 -1
  6. package/dist/assessments/assessmentStore.js +2 -2
  7. package/dist/assessments/confirmCli.js +5 -4
  8. package/dist/config.d.ts +7 -0
  9. package/dist/config.js +34 -0
  10. package/dist/control/middleware/auth.d.ts +10 -0
  11. package/dist/control/middleware/auth.js +8 -0
  12. package/dist/control/middleware/static.d.ts +3 -0
  13. package/dist/control/middleware/static.js +65 -0
  14. package/dist/control/routes/audit.d.ts +15 -0
  15. package/dist/control/routes/audit.js +277 -0
  16. package/dist/control/routes/evidence.d.ts +4 -0
  17. package/dist/control/routes/evidence.js +96 -0
  18. package/dist/control/routes/lineage.d.ts +3 -0
  19. package/dist/control/routes/lineage.js +71 -0
  20. package/dist/control/routes/policy.d.ts +3 -0
  21. package/dist/control/routes/policy.js +81 -0
  22. package/dist/control/routes/process.d.ts +5 -0
  23. package/dist/control/routes/process.js +200 -0
  24. package/dist/control/routes/sessions.d.ts +22 -0
  25. package/dist/control/routes/sessions.js +224 -0
  26. package/dist/control/routes/status.d.ts +6 -0
  27. package/dist/control/routes/status.js +250 -0
  28. package/dist/control/routes/taskActions.d.ts +21 -0
  29. package/dist/control/routes/taskActions.js +233 -0
  30. package/dist/control/routes/tasks.d.ts +20 -0
  31. package/dist/control/routes/tasks.js +310 -0
  32. package/dist/control/routes/workspace.d.ts +15 -0
  33. package/dist/control/routes/workspace.js +193 -0
  34. package/dist/control/runtime.d.ts +91 -0
  35. package/dist/control/runtime.js +392 -0
  36. package/dist/control/server.d.ts +13 -0
  37. package/dist/control/server.js +479 -0
  38. package/dist/control/shared.d.ts +35 -0
  39. package/dist/control/shared.js +288 -0
  40. package/dist/controlCenter.d.ts +6 -0
  41. package/dist/controlCenter.js +7 -2001
  42. package/dist/direct/directGuards.js +30 -8
  43. package/dist/direct/directSessionStore.d.ts +2 -0
  44. package/dist/direct/directVerification.js +7 -0
  45. package/dist/doctor.d.ts +18 -1
  46. package/dist/doctor.js +579 -348
  47. package/dist/goal/goalReport.d.ts +54 -0
  48. package/dist/goal/goalReport.js +204 -0
  49. package/dist/goal/goalStatus.d.ts +6 -0
  50. package/dist/goal/specKitImport.d.ts +63 -0
  51. package/dist/goal/specKitImport.js +220 -0
  52. package/dist/goal/subgoalSync.js +2 -1
  53. package/dist/httpServer.js +15 -12
  54. package/dist/index.js +7 -4
  55. package/dist/logging.d.ts +7 -1
  56. package/dist/logging.js +8 -0
  57. package/dist/policy/projectPolicy.d.ts +55 -0
  58. package/dist/policy/projectPolicy.js +286 -0
  59. package/dist/runner/changeCapture.d.ts +3 -3
  60. package/dist/runner/changeCapture.js +63 -39
  61. package/dist/runner/cli.js +7 -6
  62. package/dist/runner/postTaskCleanup.js +26 -2
  63. package/dist/runner/runTask.js +245 -221
  64. package/dist/runner/simpleProcess.js +4 -4
  65. package/dist/runner/watch.js +17 -14
  66. package/dist/security/contentRedaction.d.ts +6 -0
  67. package/dist/security/contentRedaction.js +22 -0
  68. package/dist/smoke-test.js +261 -255
  69. package/dist/test/unit/apply-patch.test.d.ts +1 -0
  70. package/dist/test/unit/apply-patch.test.js +225 -0
  71. package/dist/test/unit/create-task.test.d.ts +1 -0
  72. package/dist/test/unit/create-task.test.js +197 -0
  73. package/dist/test/unit/direct-guards.test.js +124 -9
  74. package/dist/test/unit/evidence-pack.test.d.ts +1 -0
  75. package/dist/test/unit/evidence-pack.test.js +224 -0
  76. package/dist/test/unit/get-task-status.test.d.ts +1 -0
  77. package/dist/test/unit/get-task-status.test.js +174 -0
  78. package/dist/test/unit/get-task-summary.test.d.ts +1 -0
  79. package/dist/test/unit/get-task-summary.test.js +146 -0
  80. package/dist/test/unit/goal-report.test.d.ts +1 -0
  81. package/dist/test/unit/goal-report.test.js +159 -0
  82. package/dist/test/unit/goal-subgoal-task.test.js +6 -6
  83. package/dist/test/unit/goal-tools-registry.test.js +6 -4
  84. package/dist/test/unit/path-guard.test.js +24 -0
  85. package/dist/test/unit/project-policy-release-mode.test.d.ts +1 -0
  86. package/dist/test/unit/project-policy-release-mode.test.js +125 -0
  87. package/dist/test/unit/run-task-loop.test.d.ts +1 -0
  88. package/dist/test/unit/run-task-loop.test.js +380 -0
  89. package/dist/test/unit/schema-drift-check.test.js +10 -9
  90. package/dist/test/unit/spec-kit-import.test.d.ts +1 -0
  91. package/dist/test/unit/spec-kit-import.test.js +341 -0
  92. package/dist/test/unit/wait-for-task.test.d.ts +1 -0
  93. package/dist/test/unit/wait-for-task.test.js +144 -0
  94. package/dist/tools/auditTask.d.ts +8 -63
  95. package/dist/tools/auditTask.js +12 -8
  96. package/dist/tools/createDirectSession.d.ts +1 -1
  97. package/dist/tools/createDirectSession.js +2 -2
  98. package/dist/tools/createTask.d.ts +2 -2
  99. package/dist/tools/createTask.js +4 -4
  100. package/dist/tools/dispatch/coreDispatch.d.ts +9 -0
  101. package/dist/tools/dispatch/coreDispatch.js +282 -0
  102. package/dist/tools/dispatch/diagnosticDispatch.d.ts +14 -0
  103. package/dist/tools/dispatch/diagnosticDispatch.js +78 -0
  104. package/dist/tools/dispatch/directDispatch.d.ts +8 -0
  105. package/dist/tools/dispatch/directDispatch.js +115 -0
  106. package/dist/tools/dispatch/goalDispatch.d.ts +8 -0
  107. package/dist/tools/dispatch/goalDispatch.js +91 -0
  108. package/dist/tools/dispatch/releaseDispatch.d.ts +8 -0
  109. package/dist/tools/dispatch/releaseDispatch.js +45 -0
  110. package/dist/tools/dispatch/types.d.ts +23 -0
  111. package/dist/tools/dispatch/types.js +15 -0
  112. package/dist/tools/evidencePack.d.ts +45 -0
  113. package/dist/tools/evidencePack.js +375 -0
  114. package/dist/tools/finalizeDirectSession.d.ts +1 -1
  115. package/dist/tools/finalizeDirectSession.js +3 -3
  116. package/dist/tools/goalSubgoalTask.d.ts +1 -1
  117. package/dist/tools/goalSubgoalTask.js +2 -2
  118. package/dist/tools/healthCheck.js +3 -3
  119. package/dist/tools/recommendAgentForTask.d.ts +19 -0
  120. package/dist/tools/recommendAgentForTask.js +56 -0
  121. package/dist/tools/registry.d.ts +3 -3
  122. package/dist/tools/registry.js +333 -402
  123. package/dist/tools/releaseMode.d.ts +50 -0
  124. package/dist/tools/releaseMode.js +370 -0
  125. package/dist/tools/retryTask.d.ts +2 -2
  126. package/dist/tools/retryTask.js +2 -2
  127. package/dist/tools/runDirectVerificationBundle.d.ts +26 -0
  128. package/dist/tools/runDirectVerificationBundle.js +64 -0
  129. package/dist/tools/runTaskLoop.d.ts +57 -0
  130. package/dist/tools/runTaskLoop.js +417 -0
  131. package/dist/tools/runVerification.d.ts +4 -0
  132. package/dist/tools/runVerification.js +4 -0
  133. package/dist/tools/safeViews.d.ts +8 -2
  134. package/dist/tools/safeViews.js +4 -2
  135. package/dist/tools/taskLineage.d.ts +91 -0
  136. package/dist/tools/taskLineage.js +175 -0
  137. package/dist/tools/toolCatalog.d.ts +2 -2
  138. package/dist/tools/toolCatalog.js +6 -0
  139. package/dist/tools/toolRegistry.js +132 -0
  140. package/dist/version.d.ts +2 -2
  141. package/dist/version.js +2 -2
  142. package/docs/CODE_WIKI.md +983 -0
  143. package/docs/agentseal-integration.md +150 -0
  144. package/docs/architecture.md +63 -0
  145. package/docs/assets/patchwarden-oss-demo.gif +0 -0
  146. package/docs/chatgpt-usage.md +31 -0
  147. package/docs/control-center/README.md +9 -0
  148. package/docs/dashboard-overview.md +86 -0
  149. package/docs/demo.md +8 -0
  150. package/docs/direct-session-workflow.md +98 -0
  151. package/docs/evidence-pack-schema.md +215 -0
  152. package/docs/execution-plan-2026-07-09.md +315 -0
  153. package/docs/lineage-evidence-pack-workflow.md +127 -0
  154. package/docs/mcp-inspector-testing.md +200 -0
  155. package/docs/open-source-application.md +162 -0
  156. package/docs/opencode-worker.md +151 -0
  157. package/docs/openhands-worker.md +181 -0
  158. package/docs/release-evidence.md +72 -0
  159. package/docs/roadmap-execution-and-acceptance.md +365 -0
  160. package/docs/spec-kit-integration.md +131 -0
  161. package/docs/task-safe-review-workflow.md +98 -0
  162. package/docs/threat-model.md +79 -0
  163. package/docs/user-feedback.md +40 -0
  164. package/docs/why-patchwarden.md +110 -0
  165. package/package.json +3 -3
  166. package/scripts/checks/control-center-smoke.js +443 -0
  167. package/scripts/checks/control-smoke.js +2 -2
  168. package/scripts/checks/lifecycle-smoke.js +29 -23
  169. package/scripts/checks/mcp-manifest-check.js +12 -0
  170. package/scripts/checks/mcp-smoke.js +33 -7
  171. package/scripts/checks/watcher-supervisor-smoke.js +1 -1
  172. package/scripts/generate-demo-gif.py +320 -0
  173. package/src/assessments/agentAssessor.ts +3 -3
  174. package/src/assessments/assessmentStore.ts +2 -2
  175. package/src/assessments/confirmCli.ts +5 -4
  176. package/src/config.ts +37 -0
  177. package/src/control/middleware/auth.ts +17 -0
  178. package/src/control/middleware/static.ts +71 -0
  179. package/src/control/routes/audit.ts +321 -0
  180. package/src/control/routes/evidence.ts +107 -0
  181. package/src/control/routes/lineage.ts +92 -0
  182. package/src/control/routes/policy.ts +81 -0
  183. package/src/control/routes/process.ts +204 -0
  184. package/src/control/routes/sessions.ts +251 -0
  185. package/src/control/routes/status.ts +325 -0
  186. package/src/control/routes/taskActions.ts +248 -0
  187. package/src/control/routes/tasks.ts +323 -0
  188. package/src/control/routes/workspace.ts +203 -0
  189. package/src/control/runtime.ts +472 -0
  190. package/src/control/server.ts +471 -0
  191. package/src/control/shared.ts +294 -0
  192. package/src/controlCenter.ts +7 -2150
  193. package/src/direct/directGuards.ts +28 -7
  194. package/src/direct/directSessionStore.ts +2 -0
  195. package/src/direct/directVerification.ts +7 -0
  196. package/src/doctor.ts +741 -481
  197. package/src/goal/goalReport.ts +271 -0
  198. package/src/goal/goalStatus.ts +6 -0
  199. package/src/goal/specKitImport.ts +355 -0
  200. package/src/goal/subgoalSync.ts +4 -2
  201. package/src/httpServer.ts +17 -14
  202. package/src/index.ts +7 -4
  203. package/src/logging.ts +10 -1
  204. package/src/policy/projectPolicy.ts +344 -0
  205. package/src/runner/changeCapture.ts +70 -42
  206. package/src/runner/cli.ts +7 -6
  207. package/src/runner/postTaskCleanup.ts +26 -2
  208. package/src/runner/runTask.ts +325 -223
  209. package/src/runner/simpleProcess.ts +4 -4
  210. package/src/runner/watch.ts +17 -14
  211. package/src/security/contentRedaction.ts +29 -0
  212. package/src/smoke-test.ts +254 -252
  213. package/src/test/unit/apply-patch.test.ts +293 -0
  214. package/src/test/unit/create-task.test.ts +255 -0
  215. package/src/test/unit/direct-guards.test.ts +178 -8
  216. package/src/test/unit/evidence-pack.test.ts +251 -0
  217. package/src/test/unit/get-task-status.test.ts +203 -0
  218. package/src/test/unit/get-task-summary.test.ts +173 -0
  219. package/src/test/unit/goal-report.test.ts +189 -0
  220. package/src/test/unit/goal-subgoal-task.test.ts +6 -6
  221. package/src/test/unit/goal-tools-registry.test.ts +7 -5
  222. package/src/test/unit/path-guard.test.ts +30 -0
  223. package/src/test/unit/project-policy-release-mode.test.ts +156 -0
  224. package/src/test/unit/run-task-loop.test.ts +425 -0
  225. package/src/test/unit/schema-drift-check.test.ts +11 -9
  226. package/src/test/unit/spec-kit-import.test.ts +429 -0
  227. package/src/test/unit/wait-for-task.test.ts +176 -0
  228. package/src/tools/auditTask.ts +99 -59
  229. package/src/tools/createDirectSession.ts +3 -3
  230. package/src/tools/createTask.ts +7 -7
  231. package/src/tools/dispatch/coreDispatch.ts +374 -0
  232. package/src/tools/dispatch/diagnosticDispatch.ts +101 -0
  233. package/src/tools/dispatch/directDispatch.ts +167 -0
  234. package/src/tools/dispatch/goalDispatch.ts +127 -0
  235. package/src/tools/dispatch/releaseDispatch.ts +65 -0
  236. package/src/tools/dispatch/types.ts +24 -0
  237. package/src/tools/evidencePack.ts +490 -0
  238. package/src/tools/finalizeDirectSession.ts +4 -4
  239. package/src/tools/goalSubgoalTask.ts +2 -2
  240. package/src/tools/healthCheck.ts +3 -3
  241. package/src/tools/listWorkspace.ts +71 -71
  242. package/src/tools/recommendAgentForTask.ts +79 -0
  243. package/src/tools/registry.ts +352 -509
  244. package/src/tools/releaseMode.ts +450 -0
  245. package/src/tools/retryTask.ts +2 -2
  246. package/src/tools/runDirectVerificationBundle.ts +98 -0
  247. package/src/tools/runTaskLoop.ts +526 -0
  248. package/src/tools/runVerification.ts +8 -0
  249. package/src/tools/safeViews.ts +4 -2
  250. package/src/tools/taskLineage.ts +300 -0
  251. package/src/tools/toolCatalog.ts +6 -0
  252. package/src/tools/toolRegistry.ts +132 -0
  253. package/src/version.ts +2 -2
  254. package/ui/pages/audit.html +192 -3
  255. package/ui/pages/dashboard.html +1070 -14
  256. package/ui/pages/direct-sessions.html +505 -53
  257. package/ui/pages/task-detail.html +456 -438
  258. package/ui/pages/tasks.html +598 -61
@@ -0,0 +1,159 @@
1
+ import { describe, it, beforeEach, afterEach } from "node:test";
2
+ import { strict as assert } from "node:assert";
3
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
4
+ import { join, resolve } from "node:path";
5
+ import { tmpdir } from "node:os";
6
+ import { reloadConfig } from "../../config.js";
7
+ import { exportGoalReport } from "../../goal/goalReport.js";
8
+ import { getGoalDir, writeGoalStatus } from "../../goal/goalStore.js";
9
+ // ── Helpers ───────────────────────────────────────────────────────
10
+ function makeSubgoal(overrides) {
11
+ return {
12
+ title: "Subgoal " + overrides.id,
13
+ status: "ready",
14
+ depends_on: [],
15
+ task_ids: [],
16
+ ...overrides,
17
+ };
18
+ }
19
+ function makeGoalStatus(subgoals, overrides = {}) {
20
+ const now = "2026-07-10T00:00:00.000Z";
21
+ return {
22
+ goal_id: "goal_test_001",
23
+ title: "Test Goal",
24
+ status: "active",
25
+ repo_path: "/repo/test",
26
+ created_at: now,
27
+ updated_at: now,
28
+ subgoals,
29
+ ...overrides,
30
+ };
31
+ }
32
+ let tempDir;
33
+ let prevConfigEnv;
34
+ function writeConfig() {
35
+ const configPath = join(tempDir, "patchwarden.config.json");
36
+ writeFileSync(configPath, JSON.stringify({
37
+ workspaceRoot: tempDir,
38
+ tasksDir: ".patchwarden/tasks",
39
+ plansDir: ".patchwarden/plans",
40
+ assessmentsDir: ".patchwarden/assessments",
41
+ agents: {
42
+ codex: { command: "codex", args: [] },
43
+ opencode: { command: "opencode", args: [] },
44
+ },
45
+ allowedTestCommands: ["npm test", "npm run build"],
46
+ defaultTaskTimeoutSeconds: 30,
47
+ maxTaskTimeoutSeconds: 120,
48
+ }), "utf-8");
49
+ prevConfigEnv = process.env.PATCHWARDEN_CONFIG;
50
+ process.env.PATCHWARDEN_CONFIG = configPath;
51
+ reloadConfig();
52
+ }
53
+ /**
54
+ * 创建 goal 目录并写入 goal_status.json(writeGoalStatus 不自动创建目录)。
55
+ */
56
+ function seedGoalStatus(goalId, status, workspaceRoot) {
57
+ mkdirSync(getGoalDir(goalId, workspaceRoot), { recursive: true });
58
+ writeGoalStatus(goalId, status, workspaceRoot);
59
+ }
60
+ // ── Tests ─────────────────────────────────────────────────────────
61
+ describe("goalReport", () => {
62
+ beforeEach(() => {
63
+ tempDir = mkdtempSync(join(tmpdir(), "pw-goal-report-"));
64
+ writeConfig();
65
+ });
66
+ afterEach(() => {
67
+ if (prevConfigEnv === undefined)
68
+ delete process.env.PATCHWARDEN_CONFIG;
69
+ else
70
+ process.env.PATCHWARDEN_CONFIG = prevConfigEnv;
71
+ reloadConfig();
72
+ rmSync(tempDir, { recursive: true, force: true });
73
+ });
74
+ it("完成 Goal 报告:所有 subgoal accepted,completion_rate=100", () => {
75
+ const goal = makeGoalStatus([
76
+ makeSubgoal({
77
+ id: "subgoal-001",
78
+ title: "Feature A",
79
+ status: "accepted",
80
+ accepted_at: "2026-07-01T00:00:00.000Z",
81
+ }),
82
+ makeSubgoal({
83
+ id: "subgoal-002",
84
+ title: "Feature B",
85
+ status: "accepted",
86
+ accepted_at: "2026-07-02T00:00:00.000Z",
87
+ }),
88
+ ], { goal_id: "goal_done_001" });
89
+ seedGoalStatus("goal_done_001", goal, tempDir);
90
+ const report = exportGoalReport("goal_done_001", { workspaceRoot: tempDir });
91
+ assert.equal(report.completion_rate, 100);
92
+ assert.equal(report.goal_id, "goal_done_001");
93
+ assert.equal(report.subgoals.length, 2);
94
+ assert.equal(report.bounded, true);
95
+ assert.ok(existsSync(report.files.report_md));
96
+ assert.ok(existsSync(report.files.report_json));
97
+ });
98
+ it("未完成 Goal 报告:有 running/ready subgoal,报告含 '未完成'", () => {
99
+ const goal = makeGoalStatus([
100
+ makeSubgoal({
101
+ id: "subgoal-001",
102
+ title: "Done",
103
+ status: "accepted",
104
+ accepted_at: "2026-07-01T00:00:00.000Z",
105
+ }),
106
+ makeSubgoal({ id: "subgoal-002", title: "WIP", status: "running" }),
107
+ ]);
108
+ seedGoalStatus("goal_wip_001", goal, tempDir);
109
+ const report = exportGoalReport("goal_wip_001", { workspaceRoot: tempDir });
110
+ const md = readFileSync(report.files.report_md, "utf-8");
111
+ assert.ok(md.includes("未完成"), "REPORT.md should contain '未完成' annotation");
112
+ assert.ok(report.risks.some((r) => r.includes("未完成")), "risks array should contain '未完成' entry");
113
+ });
114
+ it("空 Goal 报告:无 subgoal,completion_rate=0,报告含 '无子目标'", () => {
115
+ const goal = makeGoalStatus([]);
116
+ seedGoalStatus("goal_empty_001", goal, tempDir);
117
+ const report = exportGoalReport("goal_empty_001", { workspaceRoot: tempDir });
118
+ assert.equal(report.completion_rate, 0);
119
+ const md = readFileSync(report.files.report_md, "utf-8");
120
+ assert.ok(md.includes("无子目标"), "REPORT.md should contain '无子目标' annotation");
121
+ });
122
+ it("双格式输出:REPORT.md 和 report.json 同时存在", () => {
123
+ const goal = makeGoalStatus([
124
+ makeSubgoal({
125
+ id: "subgoal-001",
126
+ title: "Task A",
127
+ status: "accepted",
128
+ accepted_at: "2026-07-01T00:00:00.000Z",
129
+ }),
130
+ ]);
131
+ seedGoalStatus("goal_dual_001", goal, tempDir);
132
+ const report = exportGoalReport("goal_dual_001", { workspaceRoot: tempDir });
133
+ const reportDir = resolve(tempDir, ".patchwarden", "goals", "goal_dual_001", "report");
134
+ const mdPath = join(reportDir, "REPORT.md");
135
+ const jsonPath = join(reportDir, "report.json");
136
+ assert.ok(existsSync(mdPath), "REPORT.md should exist");
137
+ assert.ok(existsSync(jsonPath), "report.json should exist");
138
+ assert.equal(report.files.report_md, mdPath);
139
+ assert.equal(report.files.report_json, jsonPath);
140
+ });
141
+ it("脱敏验证:subgoal title 含敏感信息,输出中不含原始值", () => {
142
+ const goal = makeGoalStatus([
143
+ makeSubgoal({
144
+ id: "subgoal-001",
145
+ title: "Deploy with token=abcd1234",
146
+ status: "accepted",
147
+ accepted_at: "2026-07-01T00:00:00.000Z",
148
+ }),
149
+ ]);
150
+ seedGoalStatus("goal_redact_001", goal, tempDir);
151
+ const report = exportGoalReport("goal_redact_001", { workspaceRoot: tempDir });
152
+ const md = readFileSync(report.files.report_md, "utf-8");
153
+ const json = readFileSync(report.files.report_json, "utf-8");
154
+ assert.ok(!md.includes("abcd1234"), "REPORT.md must not contain raw secret value");
155
+ assert.ok(!json.includes("abcd1234"), "report.json must not contain raw secret value");
156
+ assert.ok(md.includes("[REDACTED]"), "REPORT.md should contain redaction marker");
157
+ assert.ok(json.includes("[REDACTED]"), "report.json should contain redaction marker");
158
+ });
159
+ });
@@ -11,8 +11,8 @@ import { PatchWardenError } from "../../errors.js";
11
11
  // - goal_not_found:readGoalStatus 在默认工作区找不到 goal 即抛错
12
12
  describe("createSubgoalTask", () => {
13
13
  describe("错误路径", () => {
14
- it("execution_mode=assess_only 抛 invalid_execution_mode", () => {
15
- assert.throws(() => createSubgoalTask({
14
+ it("execution_mode=assess_only 抛 invalid_execution_mode", async () => {
15
+ await assert.rejects(() => createSubgoalTask({
16
16
  goal_id: "goal_nonexistent_test",
17
17
  subgoal_title: "Sub A",
18
18
  repo_path: "repo",
@@ -24,8 +24,8 @@ describe("createSubgoalTask", () => {
24
24
  return true;
25
25
  });
26
26
  });
27
- it("goal_id 不存在抛 goal_not_found", () => {
28
- assert.throws(() => createSubgoalTask({
27
+ it("goal_id 不存在抛 goal_not_found", async () => {
28
+ await assert.rejects(() => createSubgoalTask({
29
29
  goal_id: "goal_definitely_does_not_exist_99999",
30
30
  subgoal_title: "Sub A",
31
31
  repo_path: "repo",
@@ -37,10 +37,10 @@ describe("createSubgoalTask", () => {
37
37
  return true;
38
38
  });
39
39
  });
40
- it("assess_only 优先于 goal_not_found(在读取 goal 前校验)", () => {
40
+ it("assess_only 优先于 goal_not_found(在读取 goal 前校验)", async () => {
41
41
  // execution_mode=assess_only 应先抛 invalid_execution_mode,
42
42
  // 而不是去读不存在的 goal
43
- assert.throws(() => createSubgoalTask({
43
+ await assert.rejects(() => createSubgoalTask({
44
44
  goal_id: "goal_definitely_does_not_exist_99999",
45
45
  subgoal_title: "Sub A",
46
46
  repo_path: "repo",
@@ -6,7 +6,7 @@ import { tmpdir } from "node:os";
6
6
  import { getToolDefs, handleToolCall } from "../../tools/registry.js";
7
7
  import { reloadConfig } from "../../config.js";
8
8
  import { PatchWardenError } from "../../errors.js";
9
- // ── 9 个 Goal Session MCP 工具名 ──────────────────────────────────
9
+ // ── 11 个 Goal Session MCP 工具名 ─────────────────────────────────
10
10
  const GOAL_TOOL_NAMES = [
11
11
  "create_goal",
12
12
  "list_goals",
@@ -17,6 +17,8 @@ const GOAL_TOOL_NAMES = [
17
17
  "suggest_next_subgoal",
18
18
  "summarize_goal_progress",
19
19
  "export_handoff",
20
+ "export_goal_report",
21
+ "import_speckit_tasks",
20
22
  ];
21
23
  // ── 测试环境隔离:使用临时 workspaceRoot + full profile ───────────
22
24
  let tempDir;
@@ -56,21 +58,21 @@ afterEach(() => {
56
58
  // ── Tests ─────────────────────────────────────────────────────────
57
59
  describe("goal tools registry", () => {
58
60
  describe("getToolDefs — 工具定义", () => {
59
- it("包含全部 9 个 goal 工具", () => {
61
+ it("包含全部 11 个 goal 工具", () => {
60
62
  const tools = getToolDefs();
61
63
  const names = new Set(tools.map((t) => t.name));
62
64
  for (const name of GOAL_TOOL_NAMES) {
63
65
  assert.ok(names.has(name), `Missing tool definition: ${name}`);
64
66
  }
65
67
  });
66
- it("每个 goal 工具描述以 v0.8.0 开头且 inputSchema.type 为 object", () => {
68
+ it("每个 goal 工具描述非空且 inputSchema.type 为 object", () => {
67
69
  const tools = getToolDefs();
68
70
  const byName = new Map(tools.map((t) => [t.name, t]));
69
71
  for (const name of GOAL_TOOL_NAMES) {
70
72
  const tool = byName.get(name);
71
73
  assert.ok(tool, `Tool not found: ${name}`);
72
74
  assert.equal(tool.inputSchema.type, "object", `${name} schema type should be object`);
73
- assert.ok(tool.description.startsWith("v0.8.0:"), `${name} description should start with v0.8.0:`);
75
+ assert.ok(typeof tool.description === "string" && tool.description.length > 0, `${name} description should be a non-empty string`);
74
76
  }
75
77
  });
76
78
  it("create_goal required 字段为 title/goal_description/repo_path", () => {
@@ -69,6 +69,30 @@ describe("guardPath", () => {
69
69
  rmSync(target, { recursive: true, force: true });
70
70
  }
71
71
  });
72
+ it("rejects Windows junction escape", { skip: process.platform !== "win32" ? "Windows-only test" : undefined }, () => {
73
+ const target = mkdtempSync(join(tmpdir(), "pw-junction-target-"));
74
+ try {
75
+ const linkPath = join(tempDir, "escape-junction");
76
+ symlinkSync(target, linkPath, "junction");
77
+ assert.throws(() => guardPath("escape-junction/secret.txt", tempDir), PatchWardenError);
78
+ }
79
+ finally {
80
+ rmSync(target, { recursive: true, force: true });
81
+ }
82
+ });
83
+ it("rejects intermediate symlink escape (TOCTOU defense)", { skip: process.platform === "win32" ? "Windows symlink permissions unstable" : undefined }, () => {
84
+ const target = mkdtempSync(join(tmpdir(), "pw-toctou-target-"));
85
+ try {
86
+ const linkPath = join(tempDir, "escape-link");
87
+ symlinkSync(target, linkPath);
88
+ mkdirSync(join(target, "subdir"), { recursive: true });
89
+ writeFileSync(join(target, "subdir", "secret.txt"), "secret", "utf-8");
90
+ assert.throws(() => guardPath("escape-link/subdir/secret.txt", tempDir), PatchWardenError);
91
+ }
92
+ finally {
93
+ rmSync(target, { recursive: true, force: true });
94
+ }
95
+ });
72
96
  });
73
97
  describe("guardWorkspacePath", () => {
74
98
  let tempDir;
@@ -0,0 +1,125 @@
1
+ import { describe, it, beforeEach, afterEach } from "node:test";
2
+ import { strict as assert } from "node:assert";
3
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ import { tmpdir } from "node:os";
6
+ import { reloadConfig } from "../../config.js";
7
+ import { getProjectPolicySummary } from "../../policy/projectPolicy.js";
8
+ import { releaseCleanup, releasePrepare, releaseVerify } from "../../tools/releaseMode.js";
9
+ let tempDir;
10
+ let repoPath;
11
+ let prevConfigEnv;
12
+ function writeConfig(allowedTestCommands = ["npm test", "npm run build"]) {
13
+ const configPath = join(tempDir, "patchwarden.config.json");
14
+ writeFileSync(configPath, JSON.stringify({
15
+ workspaceRoot: tempDir,
16
+ plansDir: ".patchwarden/plans",
17
+ tasksDir: ".patchwarden/tasks",
18
+ assessmentsDir: ".patchwarden/assessments",
19
+ allowedTestCommands,
20
+ agents: {},
21
+ defaultTaskTimeoutSeconds: 30,
22
+ maxTaskTimeoutSeconds: 120,
23
+ }), "utf-8");
24
+ prevConfigEnv = process.env.PATCHWARDEN_CONFIG;
25
+ process.env.PATCHWARDEN_CONFIG = configPath;
26
+ reloadConfig(configPath);
27
+ }
28
+ function writePackage(version = "1.3.0") {
29
+ writeFileSync(join(repoPath, "package.json"), JSON.stringify({
30
+ name: "patchwarden-test",
31
+ version,
32
+ repository: { type: "git", url: "git+https://github.com/example/patchwarden-test.git" },
33
+ }), "utf-8");
34
+ }
35
+ describe("project policy and release mode", () => {
36
+ beforeEach(() => {
37
+ tempDir = mkdtempSync(join(tmpdir(), "pw-policy-"));
38
+ repoPath = join(tempDir, "repo");
39
+ mkdirSync(repoPath, { recursive: true });
40
+ writePackage();
41
+ writeConfig();
42
+ });
43
+ afterEach(() => {
44
+ if (prevConfigEnv === undefined) {
45
+ delete process.env.PATCHWARDEN_CONFIG;
46
+ }
47
+ else {
48
+ process.env.PATCHWARDEN_CONFIG = prevConfigEnv;
49
+ }
50
+ reloadConfig();
51
+ rmSync(tempDir, { recursive: true, force: true });
52
+ });
53
+ it("returns safe defaults when project-policy.json is missing", () => {
54
+ const summary = getProjectPolicySummary(repoPath);
55
+ assert.equal(summary.exists, false);
56
+ assert.equal(summary.valid, true);
57
+ assert.equal(summary.effective_policy.release_mode.version_source, "package.json");
58
+ assert.equal(summary.release_readiness.version, "1.3.0");
59
+ });
60
+ it("parses BOM-prefixed policy JSON and reports allowlist issues without granting permission", () => {
61
+ mkdirSync(join(repoPath, ".patchwarden"), { recursive: true });
62
+ writeFileSync(join(repoPath, ".patchwarden", "project-policy.json"), "\uFEFF" + JSON.stringify({
63
+ allowed_commands: ["npm run build", "npm publish"],
64
+ release_mode: {
65
+ version_source: "package.json",
66
+ required_commands: ["npm run build", "not-allowed"],
67
+ },
68
+ }), "utf-8");
69
+ const summary = getProjectPolicySummary(repoPath);
70
+ assert.equal(summary.exists, true);
71
+ assert.equal(summary.valid, false);
72
+ assert.ok(summary.issues.some((issue) => issue.code === "high_risk_command"));
73
+ assert.ok(summary.issues.some((issue) => issue.code === "command_not_allowlisted"));
74
+ });
75
+ it("rejects unsafe path patterns and sensitive protected paths block cleanup", () => {
76
+ mkdirSync(join(repoPath, ".patchwarden"), { recursive: true });
77
+ writeFileSync(join(repoPath, ".patchwarden", "project-policy.json"), JSON.stringify({
78
+ auto_cleanup: { enabled: true, patterns: ["../escape", ".git", "release_packages"], exclude: [] },
79
+ protected_paths: [".env", "release_packages"],
80
+ }), "utf-8");
81
+ mkdirSync(join(repoPath, "release_packages"), { recursive: true });
82
+ const summary = getProjectPolicySummary(repoPath);
83
+ assert.equal(summary.valid, false);
84
+ assert.ok(summary.issues.some((issue) => issue.code === "unsafe_path_pattern"));
85
+ const cleanup = releaseCleanup({ repo_path: repoPath, dry_run: true });
86
+ const skipped = cleanup.summary.skipped;
87
+ assert.ok(skipped.some((entry) => entry.path === "release_packages" && entry.reason === "protected_or_sensitive"));
88
+ });
89
+ it("release_cleanup defaults to dry run and writes BOM-free JSON summary", () => {
90
+ mkdirSync(join(repoPath, "release_packages"), { recursive: true });
91
+ const result = releaseCleanup({ repo_path: repoPath });
92
+ assert.equal(result.mode, "release_cleanup");
93
+ assert.equal(result.summary.dry_run, true);
94
+ assert.equal(existsSync(join(repoPath, "release_packages")), true);
95
+ const reportPath = join(repoPath, String(result.summary.report_path));
96
+ const raw = readFileSync(reportPath, "utf-8");
97
+ assert.notEqual(raw.charCodeAt(0), 0xfeff);
98
+ assert.doesNotThrow(() => JSON.parse(raw));
99
+ });
100
+ it("release_cleanup honors auto_cleanup.enabled=false before deleting", () => {
101
+ mkdirSync(join(repoPath, ".patchwarden"), { recursive: true });
102
+ writeFileSync(join(repoPath, ".patchwarden", "project-policy.json"), JSON.stringify({
103
+ auto_cleanup: { enabled: false, patterns: ["release_packages"], exclude: [] },
104
+ }), "utf-8");
105
+ mkdirSync(join(repoPath, "release_packages"), { recursive: true });
106
+ const result = releaseCleanup({ repo_path: repoPath, dry_run: false });
107
+ assert.equal(result.summary.cleanup_disabled_by_policy, true);
108
+ assert.deepEqual(result.summary.removed, []);
109
+ assert.equal(existsSync(join(repoPath, "release_packages")), true);
110
+ });
111
+ it("release_verify fails when required metadata cannot be inferred", async () => {
112
+ writeFileSync(join(repoPath, "package.json"), JSON.stringify({ name: "", version: "" }), "utf-8");
113
+ const result = await releaseVerify({ repo_path: repoPath });
114
+ assert.equal(result.ok, false);
115
+ const stages = result.summary.stages;
116
+ assert.equal(stages.published_verified, "failed");
117
+ assert.equal(stages.github_release_verified, "failed");
118
+ assert.equal(stages.ci_verified, "failed");
119
+ });
120
+ it("release_prepare blocks commands not accepted by the existing command guard", () => {
121
+ const result = releasePrepare({ repo_path: repoPath, required_commands: ["npm run build", "npm run secret"] });
122
+ const commands = result.summary.commands;
123
+ assert.equal(commands.find((entry) => entry.command === "npm run secret")?.status, "blocked");
124
+ });
125
+ });
@@ -0,0 +1 @@
1
+ export {};