patchwarden 1.5.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 +43 -2
- package/README.md +43 -2
- 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 -2197
- package/dist/direct/directGuards.js +30 -8
- 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/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 +257 -251
- 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.js +95 -1
- 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/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 +6 -0
- package/dist/tools/evidencePack.js +213 -6
- 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/registry.d.ts +3 -3
- package/dist/tools/registry.js +60 -503
- package/dist/tools/retryTask.d.ts +2 -2
- package/dist/tools/retryTask.js +2 -2
- package/dist/tools/runTaskLoop.js +4 -4
- package/dist/tools/safeViews.d.ts +2 -2
- package/dist/tools/safeViews.js +2 -2
- package/dist/tools/toolRegistry.js +22 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- 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/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 +2 -2
- package/scripts/checks/control-center-smoke.js +356 -0
- package/scripts/checks/lifecycle-smoke.js +29 -23
- package/scripts/checks/mcp-smoke.js +3 -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 -2347
- package/src/direct/directGuards.ts +28 -7
- 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/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 +252 -250
- 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 +110 -1
- 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/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 +291 -6
- package/src/tools/finalizeDirectSession.ts +4 -4
- package/src/tools/goalSubgoalTask.ts +2 -2
- package/src/tools/healthCheck.ts +3 -3
- package/src/tools/registry.ts +68 -628
- package/src/tools/retryTask.ts +2 -2
- package/src/tools/runTaskLoop.ts +4 -4
- package/src/tools/safeViews.ts +2 -2
- package/src/tools/toolRegistry.ts +22 -0
- package/src/version.ts +1 -1
- package/ui/pages/audit.html +192 -3
- package/ui/pages/dashboard.html +959 -44
- 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 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { describe, it, beforeEach, afterEach } from "node:test";
|
|
2
|
+
import { strict as assert } from "node:assert";
|
|
3
|
+
import { mkdtempSync, rmSync, mkdirSync, writeFileSync, readFileSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { createHash } from "node:crypto";
|
|
7
|
+
import { applyPatch } from "../../tools/applyPatch.js";
|
|
8
|
+
import { createDirectSession } from "../../direct/directSessionStore.js";
|
|
9
|
+
import { reloadConfig } from "../../config.js";
|
|
10
|
+
import { PatchWardenError } from "../../errors.js";
|
|
11
|
+
// ── Helpers ────────────────────────────────────────────────────────
|
|
12
|
+
function sha256(content) {
|
|
13
|
+
return createHash("sha256").update(content, "utf-8").digest("hex");
|
|
14
|
+
}
|
|
15
|
+
function makeSnapshot() {
|
|
16
|
+
return {
|
|
17
|
+
captured_at: new Date().toISOString(),
|
|
18
|
+
is_git: false,
|
|
19
|
+
head: null,
|
|
20
|
+
status: "",
|
|
21
|
+
workspace_dirty: false,
|
|
22
|
+
files: {},
|
|
23
|
+
dirty_paths: [],
|
|
24
|
+
warnings: [],
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function bootstrapWorkspace(prefix, maxPatchBytes) {
|
|
28
|
+
const tempDir = mkdtempSync(join(tmpdir(), prefix));
|
|
29
|
+
const repoPath = join(tempDir, "my-repo");
|
|
30
|
+
mkdirSync(repoPath, { recursive: true });
|
|
31
|
+
mkdirSync(join(repoPath, "src"), { recursive: true });
|
|
32
|
+
const configPath = join(tempDir, "patchwarden.config.json");
|
|
33
|
+
writeFileSync(configPath, JSON.stringify({
|
|
34
|
+
workspaceRoot: tempDir,
|
|
35
|
+
agents: { codex: { command: "codex", args: ["exec", "{prompt}"] } },
|
|
36
|
+
allowedTestCommands: ["npm test"],
|
|
37
|
+
directMaxPatchBytes: maxPatchBytes,
|
|
38
|
+
directMaxFileBytes: 500_000,
|
|
39
|
+
}), "utf-8");
|
|
40
|
+
process.env.PATCHWARDEN_CONFIG = configPath;
|
|
41
|
+
reloadConfig();
|
|
42
|
+
const session = createDirectSession({
|
|
43
|
+
repo_path: "my-repo",
|
|
44
|
+
resolved_repo_path: repoPath,
|
|
45
|
+
title: "applyPatch test",
|
|
46
|
+
snapshot: makeSnapshot(),
|
|
47
|
+
});
|
|
48
|
+
return { tempDir, repoPath, sessionId: session.session_id };
|
|
49
|
+
}
|
|
50
|
+
function teardownWorkspace(tempDir) {
|
|
51
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
52
|
+
delete process.env.PATCHWARDEN_CONFIG;
|
|
53
|
+
reloadConfig();
|
|
54
|
+
}
|
|
55
|
+
// ── Success cases ──────────────────────────────────────────────────
|
|
56
|
+
describe("applyPatch success cases", () => {
|
|
57
|
+
let tempDir;
|
|
58
|
+
let repoPath;
|
|
59
|
+
let sessionId;
|
|
60
|
+
beforeEach(() => {
|
|
61
|
+
const ws = bootstrapWorkspace("pw-applypatch-ok-", 200_000);
|
|
62
|
+
tempDir = ws.tempDir;
|
|
63
|
+
repoPath = ws.repoPath;
|
|
64
|
+
sessionId = ws.sessionId;
|
|
65
|
+
});
|
|
66
|
+
afterEach(() => {
|
|
67
|
+
teardownWorkspace(tempDir);
|
|
68
|
+
});
|
|
69
|
+
it("applies replace_exact operation", () => {
|
|
70
|
+
const filePath = join(repoPath, "src", "main.ts");
|
|
71
|
+
const original = "hello world";
|
|
72
|
+
writeFileSync(filePath, original, "utf-8");
|
|
73
|
+
const operations = [
|
|
74
|
+
{ type: "replace_exact", old_text: "world", new_text: "there" },
|
|
75
|
+
];
|
|
76
|
+
const result = applyPatch({
|
|
77
|
+
session_id: sessionId,
|
|
78
|
+
path: "src/main.ts",
|
|
79
|
+
expected_sha256: sha256(original),
|
|
80
|
+
operations,
|
|
81
|
+
});
|
|
82
|
+
assert.equal(result.path, "src/main.ts");
|
|
83
|
+
assert.equal(result.before_sha256, sha256(original));
|
|
84
|
+
assert.equal(result.after_sha256, sha256("hello there"));
|
|
85
|
+
assert.equal(result.operations_applied, 1);
|
|
86
|
+
assert.equal(result.bytes_changed, 0);
|
|
87
|
+
assert.ok(result.next_action.length > 0);
|
|
88
|
+
assert.equal(readFileSync(filePath, "utf-8"), "hello there");
|
|
89
|
+
});
|
|
90
|
+
it("applies insert_before operation", () => {
|
|
91
|
+
const filePath = join(repoPath, "src", "main.ts");
|
|
92
|
+
const original = "hello world";
|
|
93
|
+
writeFileSync(filePath, original, "utf-8");
|
|
94
|
+
const operations = [
|
|
95
|
+
{ type: "insert_before", old_text: "world", new_text: "beautiful " },
|
|
96
|
+
];
|
|
97
|
+
const result = applyPatch({
|
|
98
|
+
session_id: sessionId,
|
|
99
|
+
path: "src/main.ts",
|
|
100
|
+
expected_sha256: sha256(original),
|
|
101
|
+
operations,
|
|
102
|
+
});
|
|
103
|
+
assert.equal(readFileSync(filePath, "utf-8"), "hello beautiful world");
|
|
104
|
+
assert.equal(result.after_sha256, sha256("hello beautiful world"));
|
|
105
|
+
assert.equal(result.operations_applied, 1);
|
|
106
|
+
assert.equal(result.bytes_changed, 10);
|
|
107
|
+
});
|
|
108
|
+
it("applies insert_after operation", () => {
|
|
109
|
+
const filePath = join(repoPath, "src", "main.ts");
|
|
110
|
+
const original = "hello world";
|
|
111
|
+
writeFileSync(filePath, original, "utf-8");
|
|
112
|
+
const operations = [
|
|
113
|
+
{ type: "insert_after", old_text: "hello", new_text: " beautiful" },
|
|
114
|
+
];
|
|
115
|
+
const result = applyPatch({
|
|
116
|
+
session_id: sessionId,
|
|
117
|
+
path: "src/main.ts",
|
|
118
|
+
expected_sha256: sha256(original),
|
|
119
|
+
operations,
|
|
120
|
+
});
|
|
121
|
+
assert.equal(readFileSync(filePath, "utf-8"), "hello beautiful world");
|
|
122
|
+
assert.equal(result.after_sha256, sha256("hello beautiful world"));
|
|
123
|
+
assert.equal(result.operations_applied, 1);
|
|
124
|
+
assert.equal(result.bytes_changed, 10);
|
|
125
|
+
});
|
|
126
|
+
it("applies replace_whole_file operation", () => {
|
|
127
|
+
const filePath = join(repoPath, "src", "main.ts");
|
|
128
|
+
const original = "hello world";
|
|
129
|
+
writeFileSync(filePath, original, "utf-8");
|
|
130
|
+
const newContent = "completely new content";
|
|
131
|
+
const operations = [
|
|
132
|
+
{ type: "replace_whole_file", new_text: newContent },
|
|
133
|
+
];
|
|
134
|
+
const result = applyPatch({
|
|
135
|
+
session_id: sessionId,
|
|
136
|
+
path: "src/main.ts",
|
|
137
|
+
expected_sha256: sha256(original),
|
|
138
|
+
operations,
|
|
139
|
+
});
|
|
140
|
+
assert.equal(readFileSync(filePath, "utf-8"), newContent);
|
|
141
|
+
assert.equal(result.after_sha256, sha256(newContent));
|
|
142
|
+
assert.equal(result.operations_applied, 1);
|
|
143
|
+
assert.equal(result.bytes_changed, 11);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
// ── Rejection cases ────────────────────────────────────────────────
|
|
147
|
+
describe("applyPatch rejection cases", () => {
|
|
148
|
+
let tempDir;
|
|
149
|
+
let repoPath;
|
|
150
|
+
let sessionId;
|
|
151
|
+
beforeEach(() => {
|
|
152
|
+
const ws = bootstrapWorkspace("pw-applypatch-rej-", 200_000);
|
|
153
|
+
tempDir = ws.tempDir;
|
|
154
|
+
repoPath = ws.repoPath;
|
|
155
|
+
sessionId = ws.sessionId;
|
|
156
|
+
});
|
|
157
|
+
afterEach(() => {
|
|
158
|
+
teardownWorkspace(tempDir);
|
|
159
|
+
});
|
|
160
|
+
it("rejects when expected_sha256 does not match", () => {
|
|
161
|
+
const filePath = join(repoPath, "src", "main.ts");
|
|
162
|
+
writeFileSync(filePath, "hello world", "utf-8");
|
|
163
|
+
assert.throws(() => applyPatch({
|
|
164
|
+
session_id: sessionId,
|
|
165
|
+
path: "src/main.ts",
|
|
166
|
+
expected_sha256: "0".repeat(64),
|
|
167
|
+
operations: [{ type: "replace_whole_file", new_text: "new" }],
|
|
168
|
+
}), (err) => err instanceof PatchWardenError && err.reason === "file_hash_mismatch");
|
|
169
|
+
// File content must remain unchanged after rejection
|
|
170
|
+
assert.equal(readFileSync(filePath, "utf-8"), "hello world");
|
|
171
|
+
});
|
|
172
|
+
it("rejects binary files (e.g. .png)", () => {
|
|
173
|
+
const filePath = join(repoPath, "src", "image.png");
|
|
174
|
+
writeFileSync(filePath, "fake png content", "utf-8");
|
|
175
|
+
assert.throws(() => applyPatch({
|
|
176
|
+
session_id: sessionId,
|
|
177
|
+
path: "src/image.png",
|
|
178
|
+
expected_sha256: sha256("fake png content"),
|
|
179
|
+
operations: [{ type: "replace_whole_file", new_text: "new" }],
|
|
180
|
+
}), (err) => err instanceof PatchWardenError && err.reason === "binary_file_blocked");
|
|
181
|
+
// File content must remain unchanged after rejection
|
|
182
|
+
assert.equal(readFileSync(filePath, "utf-8"), "fake png content");
|
|
183
|
+
});
|
|
184
|
+
it("rejects paths outside workspaceRoot", () => {
|
|
185
|
+
assert.throws(() => applyPatch({
|
|
186
|
+
session_id: sessionId,
|
|
187
|
+
path: "../../../etc/passwd",
|
|
188
|
+
expected_sha256: sha256("dummy"),
|
|
189
|
+
operations: [{ type: "replace_whole_file", new_text: "new" }],
|
|
190
|
+
}), PatchWardenError);
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
// ── Patch size limit ───────────────────────────────────────────────
|
|
194
|
+
describe("applyPatch patch size limit", () => {
|
|
195
|
+
let tempDir;
|
|
196
|
+
let repoPath;
|
|
197
|
+
let sessionId;
|
|
198
|
+
beforeEach(() => {
|
|
199
|
+
const ws = bootstrapWorkspace("pw-applypatch-size-", 100);
|
|
200
|
+
tempDir = ws.tempDir;
|
|
201
|
+
repoPath = ws.repoPath;
|
|
202
|
+
sessionId = ws.sessionId;
|
|
203
|
+
});
|
|
204
|
+
afterEach(() => {
|
|
205
|
+
teardownWorkspace(tempDir);
|
|
206
|
+
});
|
|
207
|
+
it("rejects patches exceeding directMaxPatchBytes", () => {
|
|
208
|
+
const filePath = join(repoPath, "src", "main.ts");
|
|
209
|
+
writeFileSync(filePath, "hello world", "utf-8");
|
|
210
|
+
// Build a patch whose JSON.stringify length exceeds the 100-byte limit
|
|
211
|
+
const bigText = "x".repeat(80);
|
|
212
|
+
const operations = [
|
|
213
|
+
{ type: "replace_whole_file", new_text: bigText },
|
|
214
|
+
];
|
|
215
|
+
assert.ok(JSON.stringify(operations).length > 100);
|
|
216
|
+
assert.throws(() => applyPatch({
|
|
217
|
+
session_id: sessionId,
|
|
218
|
+
path: "src/main.ts",
|
|
219
|
+
expected_sha256: sha256("hello world"),
|
|
220
|
+
operations,
|
|
221
|
+
}), (err) => err instanceof PatchWardenError && err.reason === "patch_too_large");
|
|
222
|
+
// File content must remain unchanged after rejection
|
|
223
|
+
assert.equal(readFileSync(filePath, "utf-8"), "hello world");
|
|
224
|
+
});
|
|
225
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { describe, it, beforeEach, afterEach } from "node:test";
|
|
2
|
+
import { strict as assert } from "node:assert";
|
|
3
|
+
import { mkdtempSync, rmSync, mkdirSync, writeFileSync, existsSync, readFileSync, readdirSync, } from "node:fs";
|
|
4
|
+
import { join, dirname } from "node:path";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { reloadConfig } from "../../config.js";
|
|
7
|
+
import { createTask } from "../../tools/createTask.js";
|
|
8
|
+
import { savePlan } from "../../tools/savePlan.js";
|
|
9
|
+
import { PatchWardenError } from "../../errors.js";
|
|
10
|
+
// ── Helpers ───────────────────────────────────────────────────────
|
|
11
|
+
let tempDir;
|
|
12
|
+
let prevConfigEnv;
|
|
13
|
+
function writeConfig(workspaceRoot) {
|
|
14
|
+
const configPath = join(tempDir, "patchwarden.config.json");
|
|
15
|
+
writeFileSync(configPath, JSON.stringify({
|
|
16
|
+
workspaceRoot,
|
|
17
|
+
tasksDir: ".patchwarden/tasks",
|
|
18
|
+
plansDir: ".patchwarden/plans",
|
|
19
|
+
assessmentsDir: ".patchwarden/assessments",
|
|
20
|
+
directSessionsDir: ".patchwarden/direct-sessions",
|
|
21
|
+
agents: {
|
|
22
|
+
codex: { command: "codex", args: ["exec", "{prompt}"] },
|
|
23
|
+
},
|
|
24
|
+
allowedTestCommands: ["npm test", "npm run build"],
|
|
25
|
+
defaultTaskTimeoutSeconds: 60,
|
|
26
|
+
maxTaskTimeoutSeconds: 300,
|
|
27
|
+
watcherStaleSeconds: 30,
|
|
28
|
+
}), "utf-8");
|
|
29
|
+
prevConfigEnv = process.env.PATCHWARDEN_CONFIG;
|
|
30
|
+
process.env.PATCHWARDEN_CONFIG = configPath;
|
|
31
|
+
reloadConfig();
|
|
32
|
+
}
|
|
33
|
+
/** Write a fresh watcher heartbeat so readWatcherStatus reports healthy. */
|
|
34
|
+
function writeFreshHeartbeat(workspaceRoot) {
|
|
35
|
+
const heartbeatPath = join(workspaceRoot, ".patchwarden", "watcher-heartbeat.json");
|
|
36
|
+
mkdirSync(dirname(heartbeatPath), { recursive: true });
|
|
37
|
+
writeFileSync(heartbeatPath, JSON.stringify({
|
|
38
|
+
status: "running",
|
|
39
|
+
pid: process.pid,
|
|
40
|
+
last_heartbeat_at: new Date().toISOString(),
|
|
41
|
+
instance_id: "test-instance",
|
|
42
|
+
}), "utf-8");
|
|
43
|
+
}
|
|
44
|
+
describe("createTask", () => {
|
|
45
|
+
beforeEach(() => {
|
|
46
|
+
tempDir = mkdtempSync(join(tmpdir(), "pw-createtask-"));
|
|
47
|
+
});
|
|
48
|
+
afterEach(() => {
|
|
49
|
+
if (prevConfigEnv === undefined)
|
|
50
|
+
delete process.env.PATCHWARDEN_CONFIG;
|
|
51
|
+
else
|
|
52
|
+
process.env.PATCHWARDEN_CONFIG = prevConfigEnv;
|
|
53
|
+
reloadConfig();
|
|
54
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
55
|
+
});
|
|
56
|
+
// ── 1. inline_plan source ──────────────────────────────────────
|
|
57
|
+
it("creates a task from inline_plan, writes status.json, and allocates a task directory", async () => {
|
|
58
|
+
writeConfig(tempDir);
|
|
59
|
+
mkdirSync(join(tempDir, "my-repo"), { recursive: true });
|
|
60
|
+
writeFreshHeartbeat(tempDir);
|
|
61
|
+
const result = await createTask({
|
|
62
|
+
inline_plan: "## Goal\nAdd a hello-world print statement to main.ts.",
|
|
63
|
+
agent: "codex",
|
|
64
|
+
repo_path: "my-repo",
|
|
65
|
+
});
|
|
66
|
+
const out = result;
|
|
67
|
+
assert.ok(out.task_id.startsWith("task_"), `task_id should start with "task_", got: ${out.task_id}`);
|
|
68
|
+
assert.equal(out.status, "pending");
|
|
69
|
+
assert.equal(out.plan_source, "inline");
|
|
70
|
+
assert.equal(out.agent, "codex");
|
|
71
|
+
assert.equal(out.execution_blocked, false);
|
|
72
|
+
assert.ok(existsSync(out.path), "task directory should exist");
|
|
73
|
+
assert.ok(existsSync(join(out.path, "status.json")), "status.json should exist");
|
|
74
|
+
const status = JSON.parse(readFileSync(join(out.path, "status.json"), "utf-8"));
|
|
75
|
+
assert.equal(status.task_id, out.task_id);
|
|
76
|
+
assert.equal(status.status, "pending");
|
|
77
|
+
assert.equal(status.phase, "queued");
|
|
78
|
+
assert.equal(status.agent, "codex");
|
|
79
|
+
assert.equal(status.plan_source, "inline");
|
|
80
|
+
assert.equal(status.repo_path, "my-repo");
|
|
81
|
+
});
|
|
82
|
+
// ── 2. template source ─────────────────────────────────────────
|
|
83
|
+
it("creates a task from a built-in template (feature_small)", async () => {
|
|
84
|
+
writeConfig(tempDir);
|
|
85
|
+
mkdirSync(join(tempDir, "my-repo"), { recursive: true });
|
|
86
|
+
writeFreshHeartbeat(tempDir);
|
|
87
|
+
const result = await createTask({
|
|
88
|
+
template: "feature_small",
|
|
89
|
+
goal: "Add a utility function for string truncation.",
|
|
90
|
+
agent: "codex",
|
|
91
|
+
repo_path: "my-repo",
|
|
92
|
+
});
|
|
93
|
+
const out = result;
|
|
94
|
+
assert.ok(out.task_id.startsWith("task_"));
|
|
95
|
+
assert.equal(out.plan_source, "template");
|
|
96
|
+
assert.equal(out.template, "feature_small");
|
|
97
|
+
assert.ok(existsSync(join(out.path, "status.json")));
|
|
98
|
+
const status = JSON.parse(readFileSync(join(out.path, "status.json"), "utf-8"));
|
|
99
|
+
assert.equal(status.template, "feature_small");
|
|
100
|
+
assert.equal(status.plan_source, "template");
|
|
101
|
+
});
|
|
102
|
+
// ── 3. plan_id source ──────────────────────────────────────────
|
|
103
|
+
it("creates a task from a previously saved plan_id", async () => {
|
|
104
|
+
writeConfig(tempDir);
|
|
105
|
+
mkdirSync(join(tempDir, "my-repo"), { recursive: true });
|
|
106
|
+
writeFreshHeartbeat(tempDir);
|
|
107
|
+
const saved = savePlan({
|
|
108
|
+
title: "Pre-saved plan",
|
|
109
|
+
content: "## Goal\nDo something safe and repo-local.",
|
|
110
|
+
});
|
|
111
|
+
const result = await createTask({
|
|
112
|
+
plan_id: saved.plan_id,
|
|
113
|
+
agent: "codex",
|
|
114
|
+
repo_path: "my-repo",
|
|
115
|
+
});
|
|
116
|
+
const out = result;
|
|
117
|
+
assert.equal(out.plan_id, saved.plan_id);
|
|
118
|
+
assert.equal(out.plan_source, "saved");
|
|
119
|
+
assert.ok(existsSync(join(out.path, "status.json")));
|
|
120
|
+
const status = JSON.parse(readFileSync(join(out.path, "status.json"), "utf-8"));
|
|
121
|
+
assert.equal(status.plan_id, saved.plan_id);
|
|
122
|
+
assert.equal(status.plan_source, "saved");
|
|
123
|
+
});
|
|
124
|
+
// ── 4. assess_only mode ────────────────────────────────────────
|
|
125
|
+
it("returns a risk assessment instead of creating a task in assess_only mode", async () => {
|
|
126
|
+
writeConfig(tempDir);
|
|
127
|
+
mkdirSync(join(tempDir, "my-repo"), { recursive: true });
|
|
128
|
+
const result = await createTask({
|
|
129
|
+
inline_plan: "## Goal\nAdd a print statement to main.ts.",
|
|
130
|
+
agent: "codex",
|
|
131
|
+
repo_path: "my-repo",
|
|
132
|
+
execution_mode: "assess_only",
|
|
133
|
+
});
|
|
134
|
+
const out = result;
|
|
135
|
+
assert.ok(out.assessment_id, "assessment_id should be present");
|
|
136
|
+
assert.ok(out.assessment_short_id, "assessment_short_id should be present");
|
|
137
|
+
assert.equal(out.decision, "allow");
|
|
138
|
+
assert.equal(out.risk_level, "low");
|
|
139
|
+
assert.ok(out.next_tool_call, "next_tool_call should be present for allow decision");
|
|
140
|
+
assert.equal(out.next_tool_call.name, "create_task");
|
|
141
|
+
assert.equal(out.next_tool_call.arguments.execution_mode, "execute");
|
|
142
|
+
assert.equal(out.next_tool_call.arguments.assessment_id, out.assessment_id);
|
|
143
|
+
// No task directory should be created in assess_only mode
|
|
144
|
+
assert.equal("task_id" in out, false, "assess_only output should not contain task_id");
|
|
145
|
+
const tasksDir = join(tempDir, ".patchwarden", "tasks");
|
|
146
|
+
if (existsSync(tasksDir)) {
|
|
147
|
+
const taskEntries = readdirSync(tasksDir).filter((name) => name.startsWith("task_"));
|
|
148
|
+
assert.equal(taskEntries.length, 0, "no task directory should be created in assess_only mode");
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
// ── 5. agent not configured ────────────────────────────────────
|
|
152
|
+
it("throws PatchWardenError (agent_not_configured) when the agent is not registered", async () => {
|
|
153
|
+
writeConfig(tempDir);
|
|
154
|
+
mkdirSync(join(tempDir, "my-repo"), { recursive: true });
|
|
155
|
+
await assert.rejects(() => createTask({
|
|
156
|
+
inline_plan: "## Goal\nDo something.",
|
|
157
|
+
agent: "ghost-agent",
|
|
158
|
+
repo_path: "my-repo",
|
|
159
|
+
}), (err) => {
|
|
160
|
+
assert.ok(err instanceof PatchWardenError, "should throw PatchWardenError");
|
|
161
|
+
assert.equal(err.reason, "agent_not_configured");
|
|
162
|
+
return true;
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
// ── 6. repo_path escapes workspace ─────────────────────────────
|
|
166
|
+
it("throws workspace_path_escape when repo_path is outside workspaceRoot", async () => {
|
|
167
|
+
writeConfig(tempDir);
|
|
168
|
+
mkdirSync(join(tempDir, "my-repo"), { recursive: true });
|
|
169
|
+
await assert.rejects(() => createTask({
|
|
170
|
+
inline_plan: "## Goal\nDo something.",
|
|
171
|
+
agent: "codex",
|
|
172
|
+
repo_path: "../outside-workspace",
|
|
173
|
+
}), (err) => {
|
|
174
|
+
assert.ok(err instanceof PatchWardenError, "should throw PatchWardenError");
|
|
175
|
+
assert.equal(err.reason, "workspace_path_escape");
|
|
176
|
+
return true;
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
// ── 7. watcher not running ─────────────────────────────────────
|
|
180
|
+
it("returns execution_blocked: true when watcher is not running", async () => {
|
|
181
|
+
writeConfig(tempDir);
|
|
182
|
+
mkdirSync(join(tempDir, "my-repo"), { recursive: true });
|
|
183
|
+
// No watcher heartbeat file → watcher status is "missing"
|
|
184
|
+
const result = await createTask({
|
|
185
|
+
inline_plan: "## Goal\nDo something.",
|
|
186
|
+
agent: "codex",
|
|
187
|
+
repo_path: "my-repo",
|
|
188
|
+
});
|
|
189
|
+
const out = result;
|
|
190
|
+
assert.equal(out.execution_blocked, true);
|
|
191
|
+
assert.equal(out.watcher.available, false);
|
|
192
|
+
assert.equal(out.watcher.status, "missing");
|
|
193
|
+
assert.equal(out.next_tool_call.name, "health_check");
|
|
194
|
+
// Task is still created (queued) despite being blocked
|
|
195
|
+
assert.ok(existsSync(join(out.path, "status.json")));
|
|
196
|
+
});
|
|
197
|
+
});
|
|
@@ -3,8 +3,9 @@ import { strict as assert } from "node:assert";
|
|
|
3
3
|
import { mkdtempSync, rmSync, mkdirSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { join, resolve } from "node:path";
|
|
5
5
|
import { tmpdir } from "node:os";
|
|
6
|
-
import { guardDirectPath, guardDirectWritePath, guardDirectReadPath, isBinaryFile } from "../../direct/directGuards.js";
|
|
6
|
+
import { guardDirectPath, guardDirectWritePath, guardDirectReadPath, guardDirectPatchSize, guardDirectFileSize, isBinaryFile } from "../../direct/directGuards.js";
|
|
7
7
|
import { PatchWardenError } from "../../errors.js";
|
|
8
|
+
import { reloadConfig } from "../../config.js";
|
|
8
9
|
function makeConfig(workspaceRoot) {
|
|
9
10
|
return {
|
|
10
11
|
workspaceRoot,
|
|
@@ -130,26 +131,65 @@ describe("guardDirectPatchSize", () => {
|
|
|
130
131
|
let tempDir;
|
|
131
132
|
beforeEach(() => {
|
|
132
133
|
tempDir = mkdtempSync(join(tmpdir(), "pw-patchsize-"));
|
|
133
|
-
|
|
134
|
+
// Write a real config file so getConfig() picks it up
|
|
135
|
+
const configPath = join(tempDir, "patchwarden.config.json");
|
|
136
|
+
writeFileSync(configPath, JSON.stringify({
|
|
137
|
+
workspaceRoot: tempDir,
|
|
138
|
+
agents: { codex: { command: "codex", args: ["exec", "{prompt}"] } },
|
|
139
|
+
allowedTestCommands: ["npm test"],
|
|
140
|
+
directMaxPatchBytes: 1000,
|
|
141
|
+
directMaxFileBytes: 5000,
|
|
142
|
+
}), "utf-8");
|
|
143
|
+
process.env.PATCHWARDEN_CONFIG = configPath;
|
|
144
|
+
reloadConfig();
|
|
134
145
|
});
|
|
135
146
|
afterEach(() => {
|
|
136
147
|
rmSync(tempDir, { recursive: true, force: true });
|
|
137
148
|
delete process.env.PATCHWARDEN_CONFIG;
|
|
149
|
+
reloadConfig();
|
|
138
150
|
});
|
|
139
151
|
it("allows patches within size limit", () => {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
// We'll test it indirectly by checking it doesn't throw for small sizes
|
|
143
|
-
// when config is loaded with defaults
|
|
144
|
-
// Skip if no config available
|
|
152
|
+
assert.doesNotThrow(() => guardDirectPatchSize(500));
|
|
153
|
+
assert.doesNotThrow(() => guardDirectPatchSize(1000));
|
|
145
154
|
});
|
|
146
155
|
it("rejects patches exceeding size limit", () => {
|
|
147
|
-
|
|
156
|
+
assert.throws(() => guardDirectPatchSize(1001), (err) => err instanceof PatchWardenError && err.reason === "patch_too_large");
|
|
157
|
+
assert.throws(() => guardDirectPatchSize(999999), (err) => err instanceof PatchWardenError && err.reason === "patch_too_large");
|
|
158
|
+
});
|
|
159
|
+
it("allows zero-size patch", () => {
|
|
160
|
+
assert.doesNotThrow(() => guardDirectPatchSize(0));
|
|
148
161
|
});
|
|
149
162
|
});
|
|
150
163
|
describe("guardDirectFileSize", () => {
|
|
164
|
+
let tempDir;
|
|
165
|
+
beforeEach(() => {
|
|
166
|
+
tempDir = mkdtempSync(join(tmpdir(), "pw-filesize-"));
|
|
167
|
+
const configPath = join(tempDir, "patchwarden.config.json");
|
|
168
|
+
writeFileSync(configPath, JSON.stringify({
|
|
169
|
+
workspaceRoot: tempDir,
|
|
170
|
+
agents: { codex: { command: "codex", args: ["exec", "{prompt}"] } },
|
|
171
|
+
allowedTestCommands: ["npm test"],
|
|
172
|
+
directMaxPatchBytes: 1000,
|
|
173
|
+
directMaxFileBytes: 5000,
|
|
174
|
+
}), "utf-8");
|
|
175
|
+
process.env.PATCHWARDEN_CONFIG = configPath;
|
|
176
|
+
reloadConfig();
|
|
177
|
+
});
|
|
178
|
+
afterEach(() => {
|
|
179
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
180
|
+
delete process.env.PATCHWARDEN_CONFIG;
|
|
181
|
+
reloadConfig();
|
|
182
|
+
});
|
|
151
183
|
it("allows files within size limit", () => {
|
|
152
|
-
|
|
184
|
+
assert.doesNotThrow(() => guardDirectFileSize(100));
|
|
185
|
+
assert.doesNotThrow(() => guardDirectFileSize(5000));
|
|
186
|
+
});
|
|
187
|
+
it("rejects files exceeding size limit", () => {
|
|
188
|
+
assert.throws(() => guardDirectFileSize(5001), (err) => err instanceof PatchWardenError && err.reason === "file_too_large");
|
|
189
|
+
assert.throws(() => guardDirectFileSize(9999999), (err) => err instanceof PatchWardenError && err.reason === "file_too_large");
|
|
190
|
+
});
|
|
191
|
+
it("allows zero-size file", () => {
|
|
192
|
+
assert.doesNotThrow(() => guardDirectFileSize(0));
|
|
153
193
|
});
|
|
154
194
|
});
|
|
155
195
|
describe("isBinaryFile", () => {
|
|
@@ -210,4 +250,79 @@ describe("isBinaryFile", () => {
|
|
|
210
250
|
it("returns false for non-existent files without binary extension", () => {
|
|
211
251
|
assert.equal(isBinaryFile(join(tempDir, "nonexistent.txt")), false);
|
|
212
252
|
});
|
|
253
|
+
// ── Adversarial: null byte beyond 8KB read window ──
|
|
254
|
+
it("detects null byte at position 8200 (beyond old 8KB window)", () => {
|
|
255
|
+
// Previously isBinaryFile only read the first 8192 bytes, so a null byte
|
|
256
|
+
// at position 8200 would bypass detection. Now we scan up to 1 MB.
|
|
257
|
+
const stealthBinary = join(tempDir, "stealth.txt");
|
|
258
|
+
const buffer = Buffer.alloc(8200, 0x41); // 8200 bytes of 'A'
|
|
259
|
+
buffer[8199] = 0; // null byte just beyond the old 8KB read window
|
|
260
|
+
writeFileSync(stealthBinary, buffer);
|
|
261
|
+
assert.equal(isBinaryFile(stealthBinary), true);
|
|
262
|
+
});
|
|
263
|
+
it("detects null byte at exactly position 8192 (first byte beyond old window)", () => {
|
|
264
|
+
const stealthBinary = join(tempDir, "boundary-plus.txt");
|
|
265
|
+
const buffer = Buffer.alloc(8193, 0x41);
|
|
266
|
+
buffer[8192] = 0; // first byte beyond the old 8KB read window
|
|
267
|
+
writeFileSync(stealthBinary, buffer);
|
|
268
|
+
assert.equal(isBinaryFile(stealthBinary), true);
|
|
269
|
+
});
|
|
270
|
+
it("detects null byte at 100KB offset", () => {
|
|
271
|
+
const deepBinary = join(tempDir, "deep.txt");
|
|
272
|
+
const buffer = Buffer.alloc(102400, 0x41);
|
|
273
|
+
buffer[102399] = 0;
|
|
274
|
+
writeFileSync(deepBinary, buffer);
|
|
275
|
+
assert.equal(isBinaryFile(deepBinary), true);
|
|
276
|
+
});
|
|
277
|
+
it("detects null byte at 1MB offset (scan limit boundary)", () => {
|
|
278
|
+
const limitBinary = join(tempDir, "limit.txt");
|
|
279
|
+
const buffer = Buffer.alloc(1_048_576, 0x41);
|
|
280
|
+
buffer[1_048_575] = 0;
|
|
281
|
+
writeFileSync(limitBinary, buffer);
|
|
282
|
+
assert.equal(isBinaryFile(limitBinary), true);
|
|
283
|
+
});
|
|
284
|
+
it("does not scan beyond 1MB limit", () => {
|
|
285
|
+
// A null byte just past the 1MB scan limit should NOT be detected.
|
|
286
|
+
// This documents the remaining scan boundary.
|
|
287
|
+
const overLimit = join(tempDir, "over-limit.txt");
|
|
288
|
+
const buffer = Buffer.alloc(1_048_577, 0x41);
|
|
289
|
+
buffer[1_048_576] = 0; // first byte beyond scan limit
|
|
290
|
+
writeFileSync(overLimit, buffer);
|
|
291
|
+
assert.equal(isBinaryFile(overLimit), false);
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
// ── Adversarial: Windows backslash prefix bypass attempts ──
|
|
295
|
+
describe("guardDirectWritePath adversarial separator tests", () => {
|
|
296
|
+
let tempDir;
|
|
297
|
+
let repoPath;
|
|
298
|
+
beforeEach(() => {
|
|
299
|
+
tempDir = mkdtempSync(join(tmpdir(), "pw-directsep-"));
|
|
300
|
+
repoPath = join(tempDir, "my-repo");
|
|
301
|
+
mkdirSync(repoPath, { recursive: true });
|
|
302
|
+
mkdirSync(join(repoPath, "src"), { recursive: true });
|
|
303
|
+
});
|
|
304
|
+
afterEach(() => {
|
|
305
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
306
|
+
});
|
|
307
|
+
it("blocks node_modules with backslash prefix", () => {
|
|
308
|
+
assert.throws(() => guardDirectWritePath("node_modules\\evil\\index.js", repoPath, tempDir), (err) => err instanceof PatchWardenError && err.reason === "blocked_artifact_path");
|
|
309
|
+
});
|
|
310
|
+
it("blocks dist with backslash prefix", () => {
|
|
311
|
+
assert.throws(() => guardDirectWritePath("dist\\main.js", repoPath, tempDir), (err) => err instanceof PatchWardenError && err.reason === "blocked_artifact_path");
|
|
312
|
+
});
|
|
313
|
+
it("blocks release with backslash prefix", () => {
|
|
314
|
+
assert.throws(() => guardDirectWritePath("release\\app.exe", repoPath, tempDir), (err) => err instanceof PatchWardenError && err.reason === "blocked_artifact_path");
|
|
315
|
+
});
|
|
316
|
+
it("blocks nested node_modules with mixed separators", () => {
|
|
317
|
+
assert.throws(() => guardDirectWritePath("src\\node_modules/evil\\index.js", repoPath, tempDir), (err) => err instanceof PatchWardenError && err.reason === "blocked_artifact_path");
|
|
318
|
+
});
|
|
319
|
+
it("blocks .patchwarden with backslash prefix", () => {
|
|
320
|
+
assert.throws(() => guardDirectWritePath(".patchwarden\\tasks\\evil.json", repoPath, tempDir), (err) => err instanceof PatchWardenError && err.reason === "internal_patchwarden_path_blocked");
|
|
321
|
+
});
|
|
322
|
+
it("blocks .env with backslash prefix", () => {
|
|
323
|
+
assert.throws(() => guardDirectWritePath("path\\to\\.env", repoPath, tempDir), (err) => err instanceof PatchWardenError && err.reason === "sensitive_path_blocked");
|
|
324
|
+
});
|
|
325
|
+
it("blocks config.json with backslash path", () => {
|
|
326
|
+
assert.throws(() => guardDirectWritePath("subdir\\config.json", repoPath, tempDir), (err) => err instanceof PatchWardenError && err.reason === "sensitive_path_blocked");
|
|
327
|
+
});
|
|
213
328
|
});
|