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.
- package/README.en.md +83 -8
- package/README.md +78 -10
- package/dist/assessments/agentAssessor.d.ts +1 -1
- package/dist/assessments/agentAssessor.js +3 -3
- package/dist/assessments/assessmentStore.d.ts +1 -1
- package/dist/assessments/assessmentStore.js +2 -2
- package/dist/assessments/confirmCli.js +5 -4
- package/dist/config.d.ts +7 -0
- package/dist/config.js +34 -0
- package/dist/control/middleware/auth.d.ts +10 -0
- package/dist/control/middleware/auth.js +8 -0
- package/dist/control/middleware/static.d.ts +3 -0
- package/dist/control/middleware/static.js +65 -0
- package/dist/control/routes/audit.d.ts +15 -0
- package/dist/control/routes/audit.js +277 -0
- package/dist/control/routes/evidence.d.ts +4 -0
- package/dist/control/routes/evidence.js +96 -0
- package/dist/control/routes/lineage.d.ts +3 -0
- package/dist/control/routes/lineage.js +71 -0
- package/dist/control/routes/policy.d.ts +3 -0
- package/dist/control/routes/policy.js +81 -0
- package/dist/control/routes/process.d.ts +5 -0
- package/dist/control/routes/process.js +200 -0
- package/dist/control/routes/sessions.d.ts +22 -0
- package/dist/control/routes/sessions.js +224 -0
- package/dist/control/routes/status.d.ts +6 -0
- package/dist/control/routes/status.js +250 -0
- package/dist/control/routes/taskActions.d.ts +21 -0
- package/dist/control/routes/taskActions.js +233 -0
- package/dist/control/routes/tasks.d.ts +20 -0
- package/dist/control/routes/tasks.js +310 -0
- package/dist/control/routes/workspace.d.ts +15 -0
- package/dist/control/routes/workspace.js +193 -0
- package/dist/control/runtime.d.ts +91 -0
- package/dist/control/runtime.js +392 -0
- package/dist/control/server.d.ts +13 -0
- package/dist/control/server.js +479 -0
- package/dist/control/shared.d.ts +35 -0
- package/dist/control/shared.js +288 -0
- package/dist/controlCenter.d.ts +6 -0
- package/dist/controlCenter.js +7 -2001
- package/dist/direct/directGuards.js +30 -8
- package/dist/direct/directSessionStore.d.ts +2 -0
- package/dist/direct/directVerification.js +7 -0
- package/dist/doctor.d.ts +18 -1
- package/dist/doctor.js +579 -348
- package/dist/goal/goalReport.d.ts +54 -0
- package/dist/goal/goalReport.js +204 -0
- package/dist/goal/goalStatus.d.ts +6 -0
- package/dist/goal/specKitImport.d.ts +63 -0
- package/dist/goal/specKitImport.js +220 -0
- package/dist/goal/subgoalSync.js +2 -1
- package/dist/httpServer.js +15 -12
- package/dist/index.js +7 -4
- package/dist/logging.d.ts +7 -1
- package/dist/logging.js +8 -0
- package/dist/policy/projectPolicy.d.ts +55 -0
- package/dist/policy/projectPolicy.js +286 -0
- package/dist/runner/changeCapture.d.ts +3 -3
- package/dist/runner/changeCapture.js +63 -39
- package/dist/runner/cli.js +7 -6
- package/dist/runner/postTaskCleanup.js +26 -2
- package/dist/runner/runTask.js +245 -221
- package/dist/runner/simpleProcess.js +4 -4
- package/dist/runner/watch.js +17 -14
- package/dist/security/contentRedaction.d.ts +6 -0
- package/dist/security/contentRedaction.js +22 -0
- package/dist/smoke-test.js +261 -255
- package/dist/test/unit/apply-patch.test.d.ts +1 -0
- package/dist/test/unit/apply-patch.test.js +225 -0
- package/dist/test/unit/create-task.test.d.ts +1 -0
- package/dist/test/unit/create-task.test.js +197 -0
- package/dist/test/unit/direct-guards.test.js +124 -9
- package/dist/test/unit/evidence-pack.test.d.ts +1 -0
- package/dist/test/unit/evidence-pack.test.js +224 -0
- package/dist/test/unit/get-task-status.test.d.ts +1 -0
- package/dist/test/unit/get-task-status.test.js +174 -0
- package/dist/test/unit/get-task-summary.test.d.ts +1 -0
- package/dist/test/unit/get-task-summary.test.js +146 -0
- package/dist/test/unit/goal-report.test.d.ts +1 -0
- package/dist/test/unit/goal-report.test.js +159 -0
- package/dist/test/unit/goal-subgoal-task.test.js +6 -6
- package/dist/test/unit/goal-tools-registry.test.js +6 -4
- package/dist/test/unit/path-guard.test.js +24 -0
- package/dist/test/unit/project-policy-release-mode.test.d.ts +1 -0
- package/dist/test/unit/project-policy-release-mode.test.js +125 -0
- package/dist/test/unit/run-task-loop.test.d.ts +1 -0
- package/dist/test/unit/run-task-loop.test.js +380 -0
- package/dist/test/unit/schema-drift-check.test.js +10 -9
- package/dist/test/unit/spec-kit-import.test.d.ts +1 -0
- package/dist/test/unit/spec-kit-import.test.js +341 -0
- package/dist/test/unit/wait-for-task.test.d.ts +1 -0
- package/dist/test/unit/wait-for-task.test.js +144 -0
- package/dist/tools/auditTask.d.ts +8 -63
- package/dist/tools/auditTask.js +12 -8
- package/dist/tools/createDirectSession.d.ts +1 -1
- package/dist/tools/createDirectSession.js +2 -2
- package/dist/tools/createTask.d.ts +2 -2
- package/dist/tools/createTask.js +4 -4
- package/dist/tools/dispatch/coreDispatch.d.ts +9 -0
- package/dist/tools/dispatch/coreDispatch.js +282 -0
- package/dist/tools/dispatch/diagnosticDispatch.d.ts +14 -0
- package/dist/tools/dispatch/diagnosticDispatch.js +78 -0
- package/dist/tools/dispatch/directDispatch.d.ts +8 -0
- package/dist/tools/dispatch/directDispatch.js +115 -0
- package/dist/tools/dispatch/goalDispatch.d.ts +8 -0
- package/dist/tools/dispatch/goalDispatch.js +91 -0
- package/dist/tools/dispatch/releaseDispatch.d.ts +8 -0
- package/dist/tools/dispatch/releaseDispatch.js +45 -0
- package/dist/tools/dispatch/types.d.ts +23 -0
- package/dist/tools/dispatch/types.js +15 -0
- package/dist/tools/evidencePack.d.ts +45 -0
- package/dist/tools/evidencePack.js +375 -0
- package/dist/tools/finalizeDirectSession.d.ts +1 -1
- package/dist/tools/finalizeDirectSession.js +3 -3
- package/dist/tools/goalSubgoalTask.d.ts +1 -1
- package/dist/tools/goalSubgoalTask.js +2 -2
- package/dist/tools/healthCheck.js +3 -3
- package/dist/tools/recommendAgentForTask.d.ts +19 -0
- package/dist/tools/recommendAgentForTask.js +56 -0
- package/dist/tools/registry.d.ts +3 -3
- package/dist/tools/registry.js +333 -402
- package/dist/tools/releaseMode.d.ts +50 -0
- package/dist/tools/releaseMode.js +370 -0
- package/dist/tools/retryTask.d.ts +2 -2
- package/dist/tools/retryTask.js +2 -2
- package/dist/tools/runDirectVerificationBundle.d.ts +26 -0
- package/dist/tools/runDirectVerificationBundle.js +64 -0
- package/dist/tools/runTaskLoop.d.ts +57 -0
- package/dist/tools/runTaskLoop.js +417 -0
- package/dist/tools/runVerification.d.ts +4 -0
- package/dist/tools/runVerification.js +4 -0
- package/dist/tools/safeViews.d.ts +8 -2
- package/dist/tools/safeViews.js +4 -2
- package/dist/tools/taskLineage.d.ts +91 -0
- package/dist/tools/taskLineage.js +175 -0
- package/dist/tools/toolCatalog.d.ts +2 -2
- package/dist/tools/toolCatalog.js +6 -0
- package/dist/tools/toolRegistry.js +132 -0
- package/dist/version.d.ts +2 -2
- package/dist/version.js +2 -2
- package/docs/CODE_WIKI.md +983 -0
- package/docs/agentseal-integration.md +150 -0
- package/docs/architecture.md +63 -0
- package/docs/assets/patchwarden-oss-demo.gif +0 -0
- package/docs/chatgpt-usage.md +31 -0
- package/docs/control-center/README.md +9 -0
- package/docs/dashboard-overview.md +86 -0
- package/docs/demo.md +8 -0
- package/docs/direct-session-workflow.md +98 -0
- package/docs/evidence-pack-schema.md +215 -0
- package/docs/execution-plan-2026-07-09.md +315 -0
- package/docs/lineage-evidence-pack-workflow.md +127 -0
- package/docs/mcp-inspector-testing.md +200 -0
- package/docs/open-source-application.md +162 -0
- package/docs/opencode-worker.md +151 -0
- package/docs/openhands-worker.md +181 -0
- package/docs/release-evidence.md +72 -0
- package/docs/roadmap-execution-and-acceptance.md +365 -0
- package/docs/spec-kit-integration.md +131 -0
- package/docs/task-safe-review-workflow.md +98 -0
- package/docs/threat-model.md +79 -0
- package/docs/user-feedback.md +40 -0
- package/docs/why-patchwarden.md +110 -0
- package/package.json +3 -3
- package/scripts/checks/control-center-smoke.js +443 -0
- package/scripts/checks/control-smoke.js +2 -2
- package/scripts/checks/lifecycle-smoke.js +29 -23
- package/scripts/checks/mcp-manifest-check.js +12 -0
- package/scripts/checks/mcp-smoke.js +33 -7
- package/scripts/checks/watcher-supervisor-smoke.js +1 -1
- package/scripts/generate-demo-gif.py +320 -0
- package/src/assessments/agentAssessor.ts +3 -3
- package/src/assessments/assessmentStore.ts +2 -2
- package/src/assessments/confirmCli.ts +5 -4
- package/src/config.ts +37 -0
- package/src/control/middleware/auth.ts +17 -0
- package/src/control/middleware/static.ts +71 -0
- package/src/control/routes/audit.ts +321 -0
- package/src/control/routes/evidence.ts +107 -0
- package/src/control/routes/lineage.ts +92 -0
- package/src/control/routes/policy.ts +81 -0
- package/src/control/routes/process.ts +204 -0
- package/src/control/routes/sessions.ts +251 -0
- package/src/control/routes/status.ts +325 -0
- package/src/control/routes/taskActions.ts +248 -0
- package/src/control/routes/tasks.ts +323 -0
- package/src/control/routes/workspace.ts +203 -0
- package/src/control/runtime.ts +472 -0
- package/src/control/server.ts +471 -0
- package/src/control/shared.ts +294 -0
- package/src/controlCenter.ts +7 -2150
- package/src/direct/directGuards.ts +28 -7
- package/src/direct/directSessionStore.ts +2 -0
- package/src/direct/directVerification.ts +7 -0
- package/src/doctor.ts +741 -481
- package/src/goal/goalReport.ts +271 -0
- package/src/goal/goalStatus.ts +6 -0
- package/src/goal/specKitImport.ts +355 -0
- package/src/goal/subgoalSync.ts +4 -2
- package/src/httpServer.ts +17 -14
- package/src/index.ts +7 -4
- package/src/logging.ts +10 -1
- package/src/policy/projectPolicy.ts +344 -0
- package/src/runner/changeCapture.ts +70 -42
- package/src/runner/cli.ts +7 -6
- package/src/runner/postTaskCleanup.ts +26 -2
- package/src/runner/runTask.ts +325 -223
- package/src/runner/simpleProcess.ts +4 -4
- package/src/runner/watch.ts +17 -14
- package/src/security/contentRedaction.ts +29 -0
- package/src/smoke-test.ts +254 -252
- package/src/test/unit/apply-patch.test.ts +293 -0
- package/src/test/unit/create-task.test.ts +255 -0
- package/src/test/unit/direct-guards.test.ts +178 -8
- package/src/test/unit/evidence-pack.test.ts +251 -0
- package/src/test/unit/get-task-status.test.ts +203 -0
- package/src/test/unit/get-task-summary.test.ts +173 -0
- package/src/test/unit/goal-report.test.ts +189 -0
- package/src/test/unit/goal-subgoal-task.test.ts +6 -6
- package/src/test/unit/goal-tools-registry.test.ts +7 -5
- package/src/test/unit/path-guard.test.ts +30 -0
- package/src/test/unit/project-policy-release-mode.test.ts +156 -0
- package/src/test/unit/run-task-loop.test.ts +425 -0
- package/src/test/unit/schema-drift-check.test.ts +11 -9
- package/src/test/unit/spec-kit-import.test.ts +429 -0
- package/src/test/unit/wait-for-task.test.ts +176 -0
- package/src/tools/auditTask.ts +99 -59
- package/src/tools/createDirectSession.ts +3 -3
- package/src/tools/createTask.ts +7 -7
- package/src/tools/dispatch/coreDispatch.ts +374 -0
- package/src/tools/dispatch/diagnosticDispatch.ts +101 -0
- package/src/tools/dispatch/directDispatch.ts +167 -0
- package/src/tools/dispatch/goalDispatch.ts +127 -0
- package/src/tools/dispatch/releaseDispatch.ts +65 -0
- package/src/tools/dispatch/types.ts +24 -0
- package/src/tools/evidencePack.ts +490 -0
- package/src/tools/finalizeDirectSession.ts +4 -4
- package/src/tools/goalSubgoalTask.ts +2 -2
- package/src/tools/healthCheck.ts +3 -3
- package/src/tools/listWorkspace.ts +71 -71
- package/src/tools/recommendAgentForTask.ts +79 -0
- package/src/tools/registry.ts +352 -509
- package/src/tools/releaseMode.ts +450 -0
- package/src/tools/retryTask.ts +2 -2
- package/src/tools/runDirectVerificationBundle.ts +98 -0
- package/src/tools/runTaskLoop.ts +526 -0
- package/src/tools/runVerification.ts +8 -0
- package/src/tools/safeViews.ts +4 -2
- package/src/tools/taskLineage.ts +300 -0
- package/src/tools/toolCatalog.ts +6 -0
- package/src/tools/toolRegistry.ts +132 -0
- package/src/version.ts +2 -2
- package/ui/pages/audit.html +192 -3
- package/ui/pages/dashboard.html +1070 -14
- package/ui/pages/direct-sessions.html +505 -53
- package/ui/pages/task-detail.html +456 -438
- 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.
|
|
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.
|
|
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.
|
|
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
|
+
// ── 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("包含全部
|
|
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
|
|
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
|
|
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 @@
|
|
|
1
|
+
export {};
|
|
@@ -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 {};
|