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.
- package/README.en.md +54 -20
- package/README.md +48 -21
- package/dist/agents/agentRouter.d.ts +40 -0
- package/dist/agents/agentRouter.js +95 -0
- package/dist/config.d.ts +1 -1
- package/dist/config.js +6 -2
- package/dist/controlCenter.js +212 -3
- package/dist/direct/directSessionStore.d.ts +2 -0
- package/dist/direct/directVerification.js +7 -0
- package/dist/doctor.js +32 -1
- package/dist/goal/acceptanceEngine.d.ts +62 -0
- package/dist/goal/acceptanceEngine.js +103 -0
- package/dist/goal/acceptanceTemplate.d.ts +10 -0
- package/dist/goal/acceptanceTemplate.js +104 -0
- package/dist/goal/goalGraph.d.ts +58 -0
- package/dist/goal/goalGraph.js +189 -0
- package/dist/goal/goalProgress.d.ts +81 -0
- package/dist/goal/goalProgress.js +216 -0
- package/dist/goal/goalStatus.d.ts +60 -0
- package/dist/goal/goalStatus.js +137 -0
- package/dist/goal/goalStore.d.ts +79 -0
- package/dist/goal/goalStore.js +211 -0
- package/dist/goal/handoffExport.d.ts +34 -0
- package/dist/goal/handoffExport.js +183 -0
- package/dist/goal/subgoalSync.d.ts +40 -0
- package/dist/goal/subgoalSync.js +72 -0
- package/dist/goal/worktreeManager.d.ts +96 -0
- package/dist/goal/worktreeManager.js +292 -0
- package/dist/logging.d.ts +44 -0
- package/dist/logging.js +68 -0
- package/dist/policy/projectPolicy.d.ts +55 -0
- package/dist/policy/projectPolicy.js +286 -0
- package/dist/release/releaseGate.d.ts +99 -0
- package/dist/release/releaseGate.js +475 -0
- package/dist/runner/postTaskCleanup.d.ts +13 -0
- package/dist/runner/postTaskCleanup.js +147 -0
- package/dist/runner/runTask.js +50 -10
- package/dist/security/discoveryTokenStore.d.ts +62 -0
- package/dist/security/discoveryTokenStore.js +100 -0
- package/dist/security/toolInvocationGuard.d.ts +35 -0
- package/dist/security/toolInvocationGuard.js +153 -0
- package/dist/smoke-test.js +18 -10
- package/dist/taskRuntime.d.ts +17 -0
- package/dist/test/unit/acceptance-engine.test.d.ts +1 -0
- package/dist/test/unit/acceptance-engine.test.js +228 -0
- package/dist/test/unit/agent-router.test.d.ts +1 -0
- package/dist/test/unit/agent-router.test.js +287 -0
- package/dist/test/unit/audit-checks.test.d.ts +1 -0
- package/dist/test/unit/audit-checks.test.js +350 -0
- package/dist/test/unit/diagnose-task.test.d.ts +1 -0
- package/dist/test/unit/diagnose-task.test.js +457 -0
- package/dist/test/unit/discovery-token-store.test.d.ts +1 -0
- package/dist/test/unit/discovery-token-store.test.js +139 -0
- package/dist/test/unit/evidence-pack.test.d.ts +1 -0
- package/dist/test/unit/evidence-pack.test.js +130 -0
- package/dist/test/unit/goal-graph.test.d.ts +1 -0
- package/dist/test/unit/goal-graph.test.js +298 -0
- package/dist/test/unit/goal-progress.test.d.ts +1 -0
- package/dist/test/unit/goal-progress.test.js +381 -0
- package/dist/test/unit/goal-status.test.d.ts +1 -0
- package/dist/test/unit/goal-status.test.js +215 -0
- package/dist/test/unit/goal-store.test.d.ts +1 -0
- package/dist/test/unit/goal-store.test.js +253 -0
- package/dist/test/unit/goal-subgoal-task.test.d.ts +1 -0
- package/dist/test/unit/goal-subgoal-task.test.js +55 -0
- package/dist/test/unit/goal-tools-registry.test.d.ts +1 -0
- package/dist/test/unit/goal-tools-registry.test.js +190 -0
- package/dist/test/unit/handoff-export.test.d.ts +1 -0
- package/dist/test/unit/handoff-export.test.js +263 -0
- package/dist/test/unit/invoke-discovered-tool.test.d.ts +1 -0
- package/dist/test/unit/invoke-discovered-tool.test.js +167 -0
- package/dist/test/unit/logging.test.js +127 -5
- package/dist/test/unit/post-task-cleanup.test.d.ts +1 -0
- package/dist/test/unit/post-task-cleanup.test.js +48 -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/reconcile-tasks.test.d.ts +1 -0
- package/dist/test/unit/reconcile-tasks.test.js +456 -0
- package/dist/test/unit/release-gate.test.d.ts +1 -0
- package/dist/test/unit/release-gate.test.js +242 -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/safe-views.test.d.ts +1 -0
- package/dist/test/unit/safe-views.test.js +171 -0
- package/dist/test/unit/schema-drift-check.test.d.ts +1 -0
- package/dist/test/unit/schema-drift-check.test.js +176 -0
- package/dist/test/unit/subgoal-sync.test.d.ts +1 -0
- package/dist/test/unit/subgoal-sync.test.js +183 -0
- package/dist/test/unit/tool-invocation-guard.test.d.ts +1 -0
- package/dist/test/unit/tool-invocation-guard.test.js +432 -0
- package/dist/test/unit/tool-usage-stats.test.d.ts +1 -0
- package/dist/test/unit/tool-usage-stats.test.js +300 -0
- package/dist/test/unit/toolSearch.test.d.ts +1 -0
- package/dist/test/unit/toolSearch.test.js +571 -0
- package/dist/test/unit/worktree-manager.test.d.ts +1 -0
- package/dist/test/unit/worktree-manager.test.js +176 -0
- package/dist/tools/auditTask.d.ts +103 -1
- package/dist/tools/auditTask.js +461 -8
- package/dist/tools/cancelTask.js +1 -1
- package/dist/tools/checkReleaseGate.d.ts +21 -0
- package/dist/tools/checkReleaseGate.js +22 -0
- package/dist/tools/createTask.d.ts +18 -2
- package/dist/tools/createTask.js +29 -1
- package/dist/tools/diagnoseTask.d.ts +45 -0
- package/dist/tools/diagnoseTask.js +347 -0
- package/dist/tools/discardWorktree.d.ts +24 -0
- package/dist/tools/discardWorktree.js +27 -0
- package/dist/tools/discoverTools.d.ts +11 -0
- package/dist/tools/discoverTools.js +39 -0
- package/dist/tools/evidencePack.d.ts +39 -0
- package/dist/tools/evidencePack.js +168 -0
- package/dist/tools/explainTool.d.ts +11 -0
- package/dist/tools/explainTool.js +21 -0
- package/dist/tools/getTaskSummary.js +2 -1
- package/dist/tools/goalSubgoalTask.d.ts +51 -0
- package/dist/tools/goalSubgoalTask.js +110 -0
- package/dist/tools/healthCheck.d.ts +1 -0
- package/dist/tools/healthCheck.js +5 -1
- package/dist/tools/invokeDiscoveredTool.d.ts +37 -0
- package/dist/tools/invokeDiscoveredTool.js +112 -0
- package/dist/tools/listTasks.d.ts +3 -1
- package/dist/tools/listTasks.js +14 -1
- package/dist/tools/mergeWorktree.d.ts +24 -0
- package/dist/tools/mergeWorktree.js +27 -0
- package/dist/tools/recommendAgentForTask.d.ts +19 -0
- package/dist/tools/recommendAgentForTask.js +56 -0
- package/dist/tools/reconcileTasks.d.ts +41 -0
- package/dist/tools/reconcileTasks.js +352 -0
- package/dist/tools/registry.js +926 -2
- package/dist/tools/releaseMode.d.ts +50 -0
- package/dist/tools/releaseMode.js +370 -0
- 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/safeStatus.d.ts +31 -1
- package/dist/tools/safeStatus.js +43 -1
- package/dist/tools/safeViews.d.ts +262 -0
- package/dist/tools/safeViews.js +252 -0
- package/dist/tools/schemaDriftCheck.d.ts +46 -0
- package/dist/tools/schemaDriftCheck.js +80 -0
- package/dist/tools/taskLineage.d.ts +91 -0
- package/dist/tools/taskLineage.js +175 -0
- package/dist/tools/toolCatalog.d.ts +4 -3
- package/dist/tools/toolCatalog.js +39 -11
- package/dist/tools/toolRegistry.d.ts +61 -0
- package/dist/tools/toolRegistry.js +834 -0
- package/dist/tools/toolSearch.d.ts +110 -0
- package/dist/tools/toolSearch.js +331 -0
- package/dist/tools/toolUsageStats.d.ts +41 -0
- package/dist/tools/toolUsageStats.js +116 -0
- package/dist/tools/waitForTask.js +1 -0
- package/dist/version.d.ts +2 -2
- package/dist/version.js +2 -2
- package/dist/watcherStatus.d.ts +29 -0
- package/dist/watcherStatus.js +92 -1
- package/docs/chatgpt-usage.md +31 -0
- package/docs/control-center/README.md +9 -0
- package/package.json +3 -2
- package/scripts/checks/control-center-smoke.js +87 -0
- package/scripts/checks/control-smoke.js +2 -2
- package/scripts/checks/doctor-smoke.js +1 -0
- package/scripts/checks/lifecycle-smoke.js +13 -9
- package/scripts/checks/mcp-manifest-check.js +65 -6
- package/scripts/checks/mcp-smoke.js +60 -9
- package/scripts/checks/watcher-supervisor-smoke.js +2 -2
- package/src/agents/agentRouter.ts +149 -0
- package/src/config.ts +9 -3
- package/src/controlCenter.ts +215 -4
- package/src/direct/directSessionStore.ts +2 -0
- package/src/direct/directVerification.ts +7 -0
- package/src/doctor.ts +36 -1
- package/src/goal/acceptanceEngine.ts +160 -0
- package/src/goal/acceptanceTemplate.ts +121 -0
- package/src/goal/goalGraph.ts +225 -0
- package/src/goal/goalProgress.ts +301 -0
- package/src/goal/goalStatus.ts +234 -0
- package/src/goal/goalStore.ts +306 -0
- package/src/goal/handoffExport.ts +211 -0
- package/src/goal/subgoalSync.ts +82 -0
- package/src/goal/worktreeManager.ts +404 -0
- package/src/logging.ts +91 -0
- package/src/policy/projectPolicy.ts +344 -0
- package/src/release/releaseGate.ts +567 -0
- package/src/runner/postTaskCleanup.ts +154 -0
- package/src/runner/runTask.ts +49 -10
- package/src/security/discoveryTokenStore.ts +157 -0
- package/src/security/toolInvocationGuard.ts +251 -0
- package/src/smoke-test.ts +15 -7
- package/src/taskRuntime.ts +17 -0
- package/src/test/unit/acceptance-engine.test.ts +261 -0
- package/src/test/unit/agent-router.test.ts +342 -0
- package/src/test/unit/audit-checks.test.ts +567 -0
- package/src/test/unit/diagnose-task.test.ts +544 -0
- package/src/test/unit/discovery-token-store.test.ts +181 -0
- package/src/test/unit/evidence-pack.test.ts +142 -0
- package/src/test/unit/goal-graph.test.ts +347 -0
- package/src/test/unit/goal-progress.test.ts +538 -0
- package/src/test/unit/goal-status.test.ts +270 -0
- package/src/test/unit/goal-store.test.ts +318 -0
- package/src/test/unit/goal-subgoal-task.test.ts +72 -0
- package/src/test/unit/goal-tools-registry.test.ts +243 -0
- package/src/test/unit/handoff-export.test.ts +295 -0
- package/src/test/unit/invoke-discovered-tool.test.ts +216 -0
- package/src/test/unit/logging.test.ts +177 -5
- package/src/test/unit/post-task-cleanup.test.ts +53 -0
- package/src/test/unit/project-policy-release-mode.test.ts +156 -0
- package/src/test/unit/reconcile-tasks.test.ts +551 -0
- package/src/test/unit/release-gate.test.ts +314 -0
- package/src/test/unit/run-task-loop.test.ts +425 -0
- package/src/test/unit/safe-views.test.ts +184 -0
- package/src/test/unit/schema-drift-check.test.ts +260 -0
- package/src/test/unit/subgoal-sync.test.ts +236 -0
- package/src/test/unit/tool-invocation-guard.test.ts +542 -0
- package/src/test/unit/tool-usage-stats.test.ts +384 -0
- package/src/test/unit/toolSearch.test.ts +756 -0
- package/src/test/unit/worktree-manager.test.ts +247 -0
- package/src/tools/auditTask.ts +831 -402
- package/src/tools/cancelTask.ts +1 -1
- package/src/tools/checkReleaseGate.ts +45 -0
- package/src/tools/createTask.ts +64 -6
- package/src/tools/diagnoseTask.ts +460 -0
- package/src/tools/discardWorktree.ts +42 -0
- package/src/tools/discoverTools.ts +51 -0
- package/src/tools/evidencePack.ts +205 -0
- package/src/tools/explainTool.ts +26 -0
- package/src/tools/getTaskSummary.ts +2 -1
- package/src/tools/goalSubgoalTask.ts +170 -0
- package/src/tools/healthCheck.ts +4 -1
- package/src/tools/invokeDiscoveredTool.ts +163 -0
- package/src/tools/listTasks.ts +16 -2
- package/src/tools/listWorkspace.ts +71 -71
- package/src/tools/mergeWorktree.ts +42 -0
- package/src/tools/recommendAgentForTask.ts +79 -0
- package/src/tools/reconcileTasks.ts +464 -0
- package/src/tools/registry.ts +1093 -73
- package/src/tools/releaseMode.ts +450 -0
- package/src/tools/runDirectVerificationBundle.ts +98 -0
- package/src/tools/runTaskLoop.ts +526 -0
- package/src/tools/runVerification.ts +8 -0
- package/src/tools/safeStatus.ts +73 -2
- package/src/tools/safeViews.ts +273 -0
- package/src/tools/schemaDriftCheck.ts +120 -0
- package/src/tools/taskLineage.ts +300 -0
- package/src/tools/toolCatalog.ts +40 -11
- package/src/tools/toolRegistry.ts +896 -0
- package/src/tools/toolSearch.ts +464 -0
- package/src/tools/toolUsageStats.ts +151 -0
- package/src/tools/waitForTask.ts +1 -0
- package/src/version.ts +2 -2
- package/src/watcherStatus.ts +97 -1
- package/ui/pages/dashboard.html +143 -2
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { describe, it, beforeEach } from "node:test";
|
|
2
|
+
import { strict as assert } from "node:assert";
|
|
3
|
+
import {
|
|
4
|
+
invokeDiscoveredTool,
|
|
5
|
+
type InvokeDiscoveredToolContext,
|
|
6
|
+
type InvokeDiscoveredToolInput,
|
|
7
|
+
} from "../../tools/invokeDiscoveredTool.js";
|
|
8
|
+
import { issueToken, clearAllTokens } from "../../security/discoveryTokenStore.js";
|
|
9
|
+
import { computeSchemaDigest, type ToolRisk } from "../../tools/toolRegistry.js";
|
|
10
|
+
import type { ToolDef } from "../../tools/registry.js";
|
|
11
|
+
|
|
12
|
+
// ── Helpers ───────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
function makeToolDef(name: string): ToolDef {
|
|
15
|
+
return {
|
|
16
|
+
name,
|
|
17
|
+
description: `Mock ${name} tool`,
|
|
18
|
+
inputSchema: { type: "object", properties: {} },
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const MOCK_TOOLS: ToolDef[] = [
|
|
23
|
+
"safe_status",
|
|
24
|
+
"get_diff",
|
|
25
|
+
"save_plan",
|
|
26
|
+
"invoke_discovered_tool",
|
|
27
|
+
"__test_credential_tool",
|
|
28
|
+
].map(makeToolDef);
|
|
29
|
+
|
|
30
|
+
function makeContext(overrides: Partial<InvokeDiscoveredToolContext> = {}): InvokeDiscoveredToolContext {
|
|
31
|
+
return {
|
|
32
|
+
tools: MOCK_TOOLS,
|
|
33
|
+
profile: "full",
|
|
34
|
+
dispatch: async () => ({ ok: true }),
|
|
35
|
+
...overrides,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function issueTokenFor(toolName: string, risk: ToolRisk): string {
|
|
40
|
+
return issueToken({
|
|
41
|
+
toolName,
|
|
42
|
+
risk,
|
|
43
|
+
query: "test query",
|
|
44
|
+
schemaDigest: computeSchemaDigest({}),
|
|
45
|
+
profile: "full",
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function makeInput(overrides: Partial<InvokeDiscoveredToolInput> = {}): InvokeDiscoveredToolInput {
|
|
50
|
+
return {
|
|
51
|
+
toolName: "safe_status",
|
|
52
|
+
arguments: {},
|
|
53
|
+
discoveryToken: "",
|
|
54
|
+
...overrides,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ── Tests ─────────────────────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
describe("invokeDiscoveredTool", () => {
|
|
61
|
+
beforeEach(() => {
|
|
62
|
+
clearAllTokens();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe("合法调用", () => {
|
|
66
|
+
it("readonly 工具 + 有效 token + dispatch 成功 → ok=true", async () => {
|
|
67
|
+
const token = issueTokenFor("safe_status", "readonly");
|
|
68
|
+
const result = await invokeDiscoveredTool(
|
|
69
|
+
makeInput({
|
|
70
|
+
toolName: "safe_status",
|
|
71
|
+
arguments: { task_id: "t-1" },
|
|
72
|
+
discoveryToken: token,
|
|
73
|
+
}),
|
|
74
|
+
makeContext({
|
|
75
|
+
dispatch: async () => ({ status: "pending" }),
|
|
76
|
+
}),
|
|
77
|
+
);
|
|
78
|
+
assert.equal(result.ok, true);
|
|
79
|
+
assert.ok(result.result, "result should be present");
|
|
80
|
+
assert.deepEqual(result.result, { status: "pending" });
|
|
81
|
+
assert.equal(result.invocation_log_written, true);
|
|
82
|
+
assert.equal(result.error, undefined);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe("token 校验", () => {
|
|
87
|
+
it("token 不存在 → ok=false, reason=token_not_found", async () => {
|
|
88
|
+
const result = await invokeDiscoveredTool(
|
|
89
|
+
makeInput({
|
|
90
|
+
toolName: "safe_status",
|
|
91
|
+
arguments: {},
|
|
92
|
+
discoveryToken: "tok",
|
|
93
|
+
}),
|
|
94
|
+
makeContext(),
|
|
95
|
+
);
|
|
96
|
+
assert.equal(result.ok, false);
|
|
97
|
+
assert.equal(result.error?.reason, "token_not_found");
|
|
98
|
+
assert.equal(result.invocation_log_written, true);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("token toolName 不匹配 → ok=false, reason=token_tool_mismatch", async () => {
|
|
102
|
+
const token = issueTokenFor("safe_status", "readonly");
|
|
103
|
+
const result = await invokeDiscoveredTool(
|
|
104
|
+
makeInput({
|
|
105
|
+
toolName: "get_diff",
|
|
106
|
+
arguments: { task_id: "t-1" },
|
|
107
|
+
discoveryToken: token,
|
|
108
|
+
}),
|
|
109
|
+
makeContext(),
|
|
110
|
+
);
|
|
111
|
+
assert.equal(result.ok, false);
|
|
112
|
+
assert.equal(result.error?.reason, "token_tool_mismatch");
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
describe("toolMeta 查找", () => {
|
|
117
|
+
it("工具未注册 → ok=false, reason=tool_not_registered", async () => {
|
|
118
|
+
const token = issueTokenFor("safe_status", "readonly");
|
|
119
|
+
const result = await invokeDiscoveredTool(
|
|
120
|
+
makeInput({
|
|
121
|
+
toolName: "safe_status",
|
|
122
|
+
arguments: {},
|
|
123
|
+
discoveryToken: token,
|
|
124
|
+
}),
|
|
125
|
+
makeContext({
|
|
126
|
+
tools: [makeToolDef("get_diff")],
|
|
127
|
+
}),
|
|
128
|
+
);
|
|
129
|
+
assert.equal(result.ok, false);
|
|
130
|
+
assert.equal(result.error?.reason, "tool_not_registered");
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe("防递归", () => {
|
|
135
|
+
it("toolName=invoke_discovered_tool → ok=false, reason=recursive_invocation_blocked", async () => {
|
|
136
|
+
const token = issueTokenFor("invoke_discovered_tool", "command");
|
|
137
|
+
const result = await invokeDiscoveredTool(
|
|
138
|
+
makeInput({
|
|
139
|
+
toolName: "invoke_discovered_tool",
|
|
140
|
+
arguments: {},
|
|
141
|
+
discoveryToken: token,
|
|
142
|
+
}),
|
|
143
|
+
makeContext(),
|
|
144
|
+
);
|
|
145
|
+
assert.equal(result.ok, false);
|
|
146
|
+
assert.equal(result.error?.reason, "recursive_invocation_blocked");
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
describe("守卫校验", () => {
|
|
151
|
+
it("credential_sensitive 工具 → ok=false, reason=credential_sensitive_blocked", async () => {
|
|
152
|
+
const token = issueTokenFor("__test_credential_tool", "credential_sensitive");
|
|
153
|
+
const result = await invokeDiscoveredTool(
|
|
154
|
+
makeInput({
|
|
155
|
+
toolName: "__test_credential_tool",
|
|
156
|
+
arguments: {},
|
|
157
|
+
discoveryToken: token,
|
|
158
|
+
}),
|
|
159
|
+
makeContext(),
|
|
160
|
+
);
|
|
161
|
+
assert.equal(result.ok, false);
|
|
162
|
+
assert.equal(result.error?.reason, "credential_sensitive_blocked");
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("workspace_write 缺 assessmentId → ok=false, reason=assessment_required", async () => {
|
|
166
|
+
const token = issueTokenFor("save_plan", "workspace_write");
|
|
167
|
+
const result = await invokeDiscoveredTool(
|
|
168
|
+
makeInput({
|
|
169
|
+
toolName: "save_plan",
|
|
170
|
+
arguments: {},
|
|
171
|
+
discoveryToken: token,
|
|
172
|
+
}),
|
|
173
|
+
makeContext(),
|
|
174
|
+
);
|
|
175
|
+
assert.equal(result.ok, false);
|
|
176
|
+
assert.equal(result.error?.reason, "assessment_required");
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
describe("dispatch", () => {
|
|
181
|
+
it("dispatch 抛错 → ok=false, reason=dispatch_error", async () => {
|
|
182
|
+
const token = issueTokenFor("safe_status", "readonly");
|
|
183
|
+
const result = await invokeDiscoveredTool(
|
|
184
|
+
makeInput({
|
|
185
|
+
toolName: "safe_status",
|
|
186
|
+
arguments: {},
|
|
187
|
+
discoveryToken: token,
|
|
188
|
+
}),
|
|
189
|
+
makeContext({
|
|
190
|
+
dispatch: async () => {
|
|
191
|
+
throw new Error("boom");
|
|
192
|
+
},
|
|
193
|
+
}),
|
|
194
|
+
);
|
|
195
|
+
assert.equal(result.ok, false);
|
|
196
|
+
assert.equal(result.error?.reason, "dispatch_error");
|
|
197
|
+
assert.ok(result.error?.message.includes("boom"), `expected message to contain "boom", got "${result.error?.message}"`);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("成功调用 → invocation_log_written=true", async () => {
|
|
201
|
+
const token = issueTokenFor("safe_status", "readonly");
|
|
202
|
+
const result = await invokeDiscoveredTool(
|
|
203
|
+
makeInput({
|
|
204
|
+
toolName: "safe_status",
|
|
205
|
+
arguments: { task_id: "t-log" },
|
|
206
|
+
discoveryToken: token,
|
|
207
|
+
}),
|
|
208
|
+
makeContext({
|
|
209
|
+
dispatch: async () => ({ status: "done" }),
|
|
210
|
+
}),
|
|
211
|
+
);
|
|
212
|
+
assert.equal(result.ok, true);
|
|
213
|
+
assert.equal(result.invocation_log_written, true);
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
});
|
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
import { describe, it, beforeEach, afterEach } from "node:test";
|
|
2
2
|
import { strict as assert } from "node:assert";
|
|
3
|
-
import {
|
|
3
|
+
import { mkdtempSync, rmSync, readFileSync, existsSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import {
|
|
7
|
+
Logger,
|
|
8
|
+
logUnhandledError,
|
|
9
|
+
installGlobalHandlers,
|
|
10
|
+
computeArgumentsDigest,
|
|
11
|
+
logToolInvocation,
|
|
12
|
+
type InvocationLogEntry,
|
|
13
|
+
} from "../../logging.js";
|
|
4
14
|
|
|
5
15
|
// ── Test helpers ──────────────────────────────────────────────────
|
|
6
16
|
|
|
@@ -166,9 +176,10 @@ describe("Logger — verbose mode (PATCHWARDEN_VERBOSE_LOG)", () => {
|
|
|
166
176
|
delete process.env.PATCHWARDEN_VERBOSE_LOG;
|
|
167
177
|
|
|
168
178
|
const log = new Logger();
|
|
179
|
+
const fakeToken = "sk_" + "FAKE_TOKEN_VALUE_123456";
|
|
169
180
|
const sensitiveArgs = {
|
|
170
181
|
prompt: "fix the bug",
|
|
171
|
-
token:
|
|
182
|
+
token: fakeToken,
|
|
172
183
|
};
|
|
173
184
|
|
|
174
185
|
const entries = captureJson(() => {
|
|
@@ -182,16 +193,17 @@ describe("Logger — verbose mode (PATCHWARDEN_VERBOSE_LOG)", () => {
|
|
|
182
193
|
assert.equal(entry.args, undefined);
|
|
183
194
|
// The raw token must not appear anywhere in the serialized entry
|
|
184
195
|
const raw = JSON.stringify(entry);
|
|
185
|
-
assert.ok(!raw.includes(
|
|
196
|
+
assert.ok(!raw.includes(fakeToken), "raw token must not leak into log output");
|
|
186
197
|
});
|
|
187
198
|
|
|
188
199
|
it("logs sanitized args when PATCHWARDEN_VERBOSE_LOG=true", () => {
|
|
189
200
|
process.env.PATCHWARDEN_VERBOSE_LOG = "true";
|
|
190
201
|
|
|
191
202
|
const log = new Logger();
|
|
203
|
+
const fakeToken = "sk_" + "FAKE_TOKEN_VALUE_123456";
|
|
192
204
|
const sensitiveArgs = {
|
|
193
205
|
prompt: "fix the bug",
|
|
194
|
-
token:
|
|
206
|
+
token: fakeToken,
|
|
195
207
|
};
|
|
196
208
|
|
|
197
209
|
const entries = captureJson(() => {
|
|
@@ -205,7 +217,7 @@ describe("Logger — verbose mode (PATCHWARDEN_VERBOSE_LOG)", () => {
|
|
|
205
217
|
assert.ok(entry.args !== undefined, "args should be logged in verbose mode");
|
|
206
218
|
const argsStr = String(entry.args);
|
|
207
219
|
assert.ok(argsStr.includes("[REDACTED TOKEN]"), "sensitive token must be redacted");
|
|
208
|
-
assert.ok(!argsStr.includes(
|
|
220
|
+
assert.ok(!argsStr.includes(fakeToken), "raw token must not appear in args");
|
|
209
221
|
// non-sensitive content is preserved
|
|
210
222
|
assert.ok(argsStr.includes("fix the bug"), "non-sensitive args content is preserved");
|
|
211
223
|
});
|
|
@@ -323,3 +335,163 @@ describe("installGlobalHandlers", () => {
|
|
|
323
335
|
}
|
|
324
336
|
});
|
|
325
337
|
});
|
|
338
|
+
|
|
339
|
+
// ── computeArgumentsDigest ───────────────────────────────────────
|
|
340
|
+
|
|
341
|
+
describe("computeArgumentsDigest", () => {
|
|
342
|
+
it("returns the same digest for identical arguments (stability)", () => {
|
|
343
|
+
const args = { b: 2, a: 1, nested: { y: "yes", x: [1, 2] } };
|
|
344
|
+
const d1 = computeArgumentsDigest(args);
|
|
345
|
+
const d2 = computeArgumentsDigest(args);
|
|
346
|
+
assert.equal(d1, d2);
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
it("returns the same digest regardless of key order (stable serialization)", () => {
|
|
350
|
+
const d1 = computeArgumentsDigest({ a: 1, b: 2 });
|
|
351
|
+
const d2 = computeArgumentsDigest({ b: 2, a: 1 });
|
|
352
|
+
assert.equal(d1, d2);
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
it("returns different digests for different arguments", () => {
|
|
356
|
+
const d1 = computeArgumentsDigest({ a: 1 });
|
|
357
|
+
const d2 = computeArgumentsDigest({ a: 2 });
|
|
358
|
+
assert.notEqual(d1, d2);
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
it("returns a 'sha256:' prefixed digest", () => {
|
|
362
|
+
const digest = computeArgumentsDigest({ foo: "bar" });
|
|
363
|
+
assert.ok(
|
|
364
|
+
digest.startsWith("sha256:"),
|
|
365
|
+
`digest should start with "sha256:", got: ${digest}`,
|
|
366
|
+
);
|
|
367
|
+
// full format: sha256: + 64 lowercase hex chars
|
|
368
|
+
assert.match(digest, /^sha256:[a-f0-9]{64}$/);
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
it("does not leak raw argument values into the digest string", () => {
|
|
372
|
+
const fakeToken = "tok-value";
|
|
373
|
+
const sensitiveArgs = { token: fakeToken, prompt: "hello-world" };
|
|
374
|
+
const digest = computeArgumentsDigest(sensitiveArgs);
|
|
375
|
+
assert.ok(
|
|
376
|
+
!digest.includes(fakeToken),
|
|
377
|
+
"digest must not leak raw token value",
|
|
378
|
+
);
|
|
379
|
+
assert.ok(
|
|
380
|
+
!digest.includes("hello-world"),
|
|
381
|
+
"digest must not leak raw prompt value",
|
|
382
|
+
);
|
|
383
|
+
});
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
// ── logToolInvocation ────────────────────────────────────────────
|
|
387
|
+
|
|
388
|
+
describe("logToolInvocation", () => {
|
|
389
|
+
let tempDir: string;
|
|
390
|
+
|
|
391
|
+
beforeEach(() => {
|
|
392
|
+
tempDir = mkdtempSync(join(tmpdir(), "pw-invlog-"));
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
afterEach(() => {
|
|
396
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
function sampleEntry(overrides: Partial<InvocationLogEntry> = {}): InvocationLogEntry {
|
|
400
|
+
return {
|
|
401
|
+
timestamp: new Date().toISOString(),
|
|
402
|
+
toolName: "read_workspace_file",
|
|
403
|
+
discoveryToken: "tok",
|
|
404
|
+
risk: "workspace_read_sensitive",
|
|
405
|
+
profile: "full",
|
|
406
|
+
arguments_digest: "sha256:" + "0".repeat(64),
|
|
407
|
+
result: "ok",
|
|
408
|
+
duration_ms: 42,
|
|
409
|
+
...overrides,
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
it("writes an entry to invocation.log inside the provided logsDir", () => {
|
|
414
|
+
const logsDir = join(tempDir, ".patchwarden", "logs");
|
|
415
|
+
const entry = sampleEntry();
|
|
416
|
+
logToolInvocation(entry, { logsDir });
|
|
417
|
+
|
|
418
|
+
const logPath = join(logsDir, "invocation.log");
|
|
419
|
+
assert.ok(existsSync(logPath), "invocation.log should be created");
|
|
420
|
+
|
|
421
|
+
const content = readFileSync(logPath, "utf-8");
|
|
422
|
+
const lines = content.trim().split("\n");
|
|
423
|
+
assert.equal(lines.length, 1, "exactly one JSON line should be written");
|
|
424
|
+
|
|
425
|
+
const parsed = JSON.parse(lines[0]);
|
|
426
|
+
assert.equal(parsed.toolName, "read_workspace_file");
|
|
427
|
+
assert.equal(parsed.discoveryToken, "tok");
|
|
428
|
+
assert.equal(parsed.risk, "workspace_read_sensitive");
|
|
429
|
+
assert.equal(parsed.profile, "full");
|
|
430
|
+
assert.equal(parsed.result, "ok");
|
|
431
|
+
assert.equal(parsed.duration_ms, 42);
|
|
432
|
+
assert.match(parsed.arguments_digest, /^sha256:[a-f0-9]{64}$/);
|
|
433
|
+
assert.ok(!Number.isNaN(Date.parse(parsed.timestamp)));
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
it("appends multiple entries as JSON Lines", () => {
|
|
437
|
+
const logsDir = join(tempDir, "logs");
|
|
438
|
+
logToolInvocation(sampleEntry({ duration_ms: 1 }), { logsDir });
|
|
439
|
+
logToolInvocation(
|
|
440
|
+
sampleEntry({ duration_ms: 2, result: "error", error_code: "blocked" }),
|
|
441
|
+
{ logsDir },
|
|
442
|
+
);
|
|
443
|
+
|
|
444
|
+
const content = readFileSync(join(logsDir, "invocation.log"), "utf-8");
|
|
445
|
+
const lines = content.trim().split("\n");
|
|
446
|
+
assert.equal(lines.length, 2);
|
|
447
|
+
|
|
448
|
+
const first = JSON.parse(lines[0]);
|
|
449
|
+
const second = JSON.parse(lines[1]);
|
|
450
|
+
assert.equal(first.duration_ms, 1);
|
|
451
|
+
assert.equal(first.result, "ok");
|
|
452
|
+
assert.equal(second.duration_ms, 2);
|
|
453
|
+
assert.equal(second.result, "error");
|
|
454
|
+
assert.equal(second.error_code, "blocked");
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
it("includes allowedScope and error_code when provided", () => {
|
|
458
|
+
const logsDir = join(tempDir, "logs");
|
|
459
|
+
logToolInvocation(
|
|
460
|
+
sampleEntry({
|
|
461
|
+
allowedScope: ["src/", "docs/"],
|
|
462
|
+
result: "error",
|
|
463
|
+
error_code: "scope_violation",
|
|
464
|
+
}),
|
|
465
|
+
{ logsDir },
|
|
466
|
+
);
|
|
467
|
+
|
|
468
|
+
const parsed = JSON.parse(
|
|
469
|
+
readFileSync(join(logsDir, "invocation.log"), "utf-8").trim(),
|
|
470
|
+
);
|
|
471
|
+
assert.deepEqual(parsed.allowedScope, ["src/", "docs/"]);
|
|
472
|
+
assert.equal(parsed.error_code, "scope_violation");
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
it("omits error_code and allowedScope when not provided", () => {
|
|
476
|
+
const logsDir = join(tempDir, "logs");
|
|
477
|
+
logToolInvocation(sampleEntry(), { logsDir });
|
|
478
|
+
|
|
479
|
+
const parsed = JSON.parse(
|
|
480
|
+
readFileSync(join(logsDir, "invocation.log"), "utf-8").trim(),
|
|
481
|
+
);
|
|
482
|
+
assert.equal(parsed.error_code, undefined);
|
|
483
|
+
assert.equal(parsed.allowedScope, undefined);
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
it("does not throw when the log directory cannot be created", () => {
|
|
487
|
+
// Create a file, then attempt to use a path beneath it as logsDir.
|
|
488
|
+
// mkdirSync will fail with ENOTDIR because the parent is a file.
|
|
489
|
+
const blockingFile = join(tempDir, "blocker");
|
|
490
|
+
writeFileSync(blockingFile, "x", "utf-8");
|
|
491
|
+
const impossibleLogsDir = join(blockingFile, "subdir");
|
|
492
|
+
|
|
493
|
+
assert.doesNotThrow(() => {
|
|
494
|
+
logToolInvocation(sampleEntry(), { logsDir: impossibleLogsDir });
|
|
495
|
+
});
|
|
496
|
+
});
|
|
497
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { describe, it } from "node:test";
|
|
2
|
+
import { strict as assert } from "node:assert";
|
|
3
|
+
import { execFileSync } from "node:child_process";
|
|
4
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { tmpdir } from "node:os";
|
|
7
|
+
import { runPostTaskCleanup } from "../../runner/postTaskCleanup.js";
|
|
8
|
+
|
|
9
|
+
describe("postTaskCleanup", () => {
|
|
10
|
+
it("removes untracked low-risk artifacts and skips tracked or excluded paths", () => {
|
|
11
|
+
const root = mkdtempSync(join(tmpdir(), "pw-cleanup-"));
|
|
12
|
+
try {
|
|
13
|
+
execFileSync("git", ["init"], { cwd: root, stdio: "ignore" });
|
|
14
|
+
execFileSync("git", ["config", "user.email", "test@example.com"], { cwd: root, stdio: "ignore" });
|
|
15
|
+
execFileSync("git", ["config", "user.name", "PatchWarden Test"], { cwd: root, stdio: "ignore" });
|
|
16
|
+
|
|
17
|
+
mkdirSync(join(root, "tracked", "__pycache__"), { recursive: true });
|
|
18
|
+
writeFileSync(join(root, "tracked", "__pycache__", "keep.pyc"), "tracked", "utf-8");
|
|
19
|
+
execFileSync("git", ["add", "."], { cwd: root, stdio: "ignore" });
|
|
20
|
+
execFileSync("git", ["commit", "-m", "init"], { cwd: root, stdio: "ignore" });
|
|
21
|
+
|
|
22
|
+
mkdirSync(join(root, "backend", "__pycache__"), { recursive: true });
|
|
23
|
+
mkdirSync(join(root, ".venv", "__pycache__"), { recursive: true });
|
|
24
|
+
mkdirSync(join(root, ".patchwarden", "tasks", "old-task", "__pycache__"), { recursive: true });
|
|
25
|
+
mkdirSync(join(root, "node_modules", "pkg", "__pycache__"), { recursive: true });
|
|
26
|
+
mkdirSync(join(root, "docs", "__pycache__"), { recursive: true });
|
|
27
|
+
writeFileSync(join(root, "backend", "__pycache__", "drop.pyc"), "drop", "utf-8");
|
|
28
|
+
writeFileSync(join(root, ".venv", "__pycache__", "skip.pyc"), "skip", "utf-8");
|
|
29
|
+
writeFileSync(join(root, ".patchwarden", "tasks", "old-task", "__pycache__", "skip.pyc"), "skip", "utf-8");
|
|
30
|
+
writeFileSync(join(root, "node_modules", "pkg", "__pycache__", "skip.pyc"), "skip", "utf-8");
|
|
31
|
+
writeFileSync(join(root, "docs", "__pycache__", "skip.pyc"), "skip", "utf-8");
|
|
32
|
+
|
|
33
|
+
const taskDir = join(root, ".patchwarden", "tasks", "task-1");
|
|
34
|
+
mkdirSync(taskDir, { recursive: true });
|
|
35
|
+
const report = runPostTaskCleanup(root, taskDir);
|
|
36
|
+
|
|
37
|
+
assert.ok(report.removed.some((entry) => entry.path === "backend/__pycache__"));
|
|
38
|
+
assert.ok(report.skipped.some((entry) => entry.path === "tracked/__pycache__" && entry.skip_reason === "tracked_by_git"));
|
|
39
|
+
assert.equal(report.source_files_touched, 0);
|
|
40
|
+
assert.ok(!existsSync(join(root, "backend", "__pycache__")));
|
|
41
|
+
assert.ok(existsSync(join(root, "tracked", "__pycache__", "keep.pyc")));
|
|
42
|
+
assert.ok(existsSync(join(root, ".venv", "__pycache__", "skip.pyc")));
|
|
43
|
+
assert.ok(existsSync(join(root, ".patchwarden", "tasks", "old-task", "__pycache__", "skip.pyc")));
|
|
44
|
+
assert.ok(existsSync(join(root, "node_modules", "pkg", "__pycache__", "skip.pyc")));
|
|
45
|
+
assert.ok(existsSync(join(root, "docs", "__pycache__", "skip.pyc")));
|
|
46
|
+
|
|
47
|
+
const written = JSON.parse(readFileSync(join(taskDir, "post-task-cleanup.json"), "utf-8"));
|
|
48
|
+
assert.equal(written.enabled, true);
|
|
49
|
+
} finally {
|
|
50
|
+
rmSync(root, { recursive: true, force: true });
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
});
|
|
@@ -0,0 +1,156 @@
|
|
|
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
|
+
|
|
10
|
+
let tempDir: string;
|
|
11
|
+
let repoPath: string;
|
|
12
|
+
let prevConfigEnv: string | undefined;
|
|
13
|
+
|
|
14
|
+
function writeConfig(allowedTestCommands = ["npm test", "npm run build"]): void {
|
|
15
|
+
const configPath = join(tempDir, "patchwarden.config.json");
|
|
16
|
+
writeFileSync(
|
|
17
|
+
configPath,
|
|
18
|
+
JSON.stringify({
|
|
19
|
+
workspaceRoot: tempDir,
|
|
20
|
+
plansDir: ".patchwarden/plans",
|
|
21
|
+
tasksDir: ".patchwarden/tasks",
|
|
22
|
+
assessmentsDir: ".patchwarden/assessments",
|
|
23
|
+
allowedTestCommands,
|
|
24
|
+
agents: {},
|
|
25
|
+
defaultTaskTimeoutSeconds: 30,
|
|
26
|
+
maxTaskTimeoutSeconds: 120,
|
|
27
|
+
}),
|
|
28
|
+
"utf-8",
|
|
29
|
+
);
|
|
30
|
+
prevConfigEnv = process.env.PATCHWARDEN_CONFIG;
|
|
31
|
+
process.env.PATCHWARDEN_CONFIG = configPath;
|
|
32
|
+
reloadConfig(configPath);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function writePackage(version = "1.3.0"): void {
|
|
36
|
+
writeFileSync(
|
|
37
|
+
join(repoPath, "package.json"),
|
|
38
|
+
JSON.stringify({
|
|
39
|
+
name: "patchwarden-test",
|
|
40
|
+
version,
|
|
41
|
+
repository: { type: "git", url: "git+https://github.com/example/patchwarden-test.git" },
|
|
42
|
+
}),
|
|
43
|
+
"utf-8",
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
describe("project policy and release mode", () => {
|
|
48
|
+
beforeEach(() => {
|
|
49
|
+
tempDir = mkdtempSync(join(tmpdir(), "pw-policy-"));
|
|
50
|
+
repoPath = join(tempDir, "repo");
|
|
51
|
+
mkdirSync(repoPath, { recursive: true });
|
|
52
|
+
writePackage();
|
|
53
|
+
writeConfig();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
afterEach(() => {
|
|
57
|
+
if (prevConfigEnv === undefined) {
|
|
58
|
+
delete process.env.PATCHWARDEN_CONFIG;
|
|
59
|
+
} else {
|
|
60
|
+
process.env.PATCHWARDEN_CONFIG = prevConfigEnv;
|
|
61
|
+
}
|
|
62
|
+
reloadConfig();
|
|
63
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("returns safe defaults when project-policy.json is missing", () => {
|
|
67
|
+
const summary = getProjectPolicySummary(repoPath);
|
|
68
|
+
assert.equal(summary.exists, false);
|
|
69
|
+
assert.equal(summary.valid, true);
|
|
70
|
+
assert.equal(summary.effective_policy.release_mode.version_source, "package.json");
|
|
71
|
+
assert.equal(summary.release_readiness.version, "1.3.0");
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("parses BOM-prefixed policy JSON and reports allowlist issues without granting permission", () => {
|
|
75
|
+
mkdirSync(join(repoPath, ".patchwarden"), { recursive: true });
|
|
76
|
+
writeFileSync(
|
|
77
|
+
join(repoPath, ".patchwarden", "project-policy.json"),
|
|
78
|
+
"\uFEFF" + JSON.stringify({
|
|
79
|
+
allowed_commands: ["npm run build", "npm publish"],
|
|
80
|
+
release_mode: {
|
|
81
|
+
version_source: "package.json",
|
|
82
|
+
required_commands: ["npm run build", "not-allowed"],
|
|
83
|
+
},
|
|
84
|
+
}),
|
|
85
|
+
"utf-8",
|
|
86
|
+
);
|
|
87
|
+
const summary = getProjectPolicySummary(repoPath);
|
|
88
|
+
assert.equal(summary.exists, true);
|
|
89
|
+
assert.equal(summary.valid, false);
|
|
90
|
+
assert.ok(summary.issues.some((issue) => issue.code === "high_risk_command"));
|
|
91
|
+
assert.ok(summary.issues.some((issue) => issue.code === "command_not_allowlisted"));
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("rejects unsafe path patterns and sensitive protected paths block cleanup", () => {
|
|
95
|
+
mkdirSync(join(repoPath, ".patchwarden"), { recursive: true });
|
|
96
|
+
writeFileSync(
|
|
97
|
+
join(repoPath, ".patchwarden", "project-policy.json"),
|
|
98
|
+
JSON.stringify({
|
|
99
|
+
auto_cleanup: { enabled: true, patterns: ["../escape", ".git", "release_packages"], exclude: [] },
|
|
100
|
+
protected_paths: [".env", "release_packages"],
|
|
101
|
+
}),
|
|
102
|
+
"utf-8",
|
|
103
|
+
);
|
|
104
|
+
mkdirSync(join(repoPath, "release_packages"), { recursive: true });
|
|
105
|
+
const summary = getProjectPolicySummary(repoPath);
|
|
106
|
+
assert.equal(summary.valid, false);
|
|
107
|
+
assert.ok(summary.issues.some((issue) => issue.code === "unsafe_path_pattern"));
|
|
108
|
+
const cleanup = releaseCleanup({ repo_path: repoPath, dry_run: true });
|
|
109
|
+
const skipped = cleanup.summary.skipped as Array<{ path: string; reason: string }>;
|
|
110
|
+
assert.ok(skipped.some((entry) => entry.path === "release_packages" && entry.reason === "protected_or_sensitive"));
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("release_cleanup defaults to dry run and writes BOM-free JSON summary", () => {
|
|
114
|
+
mkdirSync(join(repoPath, "release_packages"), { recursive: true });
|
|
115
|
+
const result = releaseCleanup({ repo_path: repoPath });
|
|
116
|
+
assert.equal(result.mode, "release_cleanup");
|
|
117
|
+
assert.equal((result.summary as any).dry_run, true);
|
|
118
|
+
assert.equal(existsSync(join(repoPath, "release_packages")), true);
|
|
119
|
+
const reportPath = join(repoPath, String((result.summary as any).report_path));
|
|
120
|
+
const raw = readFileSync(reportPath, "utf-8");
|
|
121
|
+
assert.notEqual(raw.charCodeAt(0), 0xfeff);
|
|
122
|
+
assert.doesNotThrow(() => JSON.parse(raw));
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("release_cleanup honors auto_cleanup.enabled=false before deleting", () => {
|
|
126
|
+
mkdirSync(join(repoPath, ".patchwarden"), { recursive: true });
|
|
127
|
+
writeFileSync(
|
|
128
|
+
join(repoPath, ".patchwarden", "project-policy.json"),
|
|
129
|
+
JSON.stringify({
|
|
130
|
+
auto_cleanup: { enabled: false, patterns: ["release_packages"], exclude: [] },
|
|
131
|
+
}),
|
|
132
|
+
"utf-8",
|
|
133
|
+
);
|
|
134
|
+
mkdirSync(join(repoPath, "release_packages"), { recursive: true });
|
|
135
|
+
const result = releaseCleanup({ repo_path: repoPath, dry_run: false });
|
|
136
|
+
assert.equal((result.summary as any).cleanup_disabled_by_policy, true);
|
|
137
|
+
assert.deepEqual((result.summary as any).removed, []);
|
|
138
|
+
assert.equal(existsSync(join(repoPath, "release_packages")), true);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("release_verify fails when required metadata cannot be inferred", async () => {
|
|
142
|
+
writeFileSync(join(repoPath, "package.json"), JSON.stringify({ name: "", version: "" }), "utf-8");
|
|
143
|
+
const result = await releaseVerify({ repo_path: repoPath });
|
|
144
|
+
assert.equal(result.ok, false);
|
|
145
|
+
const stages = result.summary.stages as Record<string, string>;
|
|
146
|
+
assert.equal(stages.published_verified, "failed");
|
|
147
|
+
assert.equal(stages.github_release_verified, "failed");
|
|
148
|
+
assert.equal(stages.ci_verified, "failed");
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("release_prepare blocks commands not accepted by the existing command guard", () => {
|
|
152
|
+
const result = releasePrepare({ repo_path: repoPath, required_commands: ["npm run build", "npm run secret"] });
|
|
153
|
+
const commands = result.summary.commands as Array<{ command: string; status: string }>;
|
|
154
|
+
assert.equal(commands.find((entry) => entry.command === "npm run secret")?.status, "blocked");
|
|
155
|
+
});
|
|
156
|
+
});
|