patchwarden 0.4.0 → 0.6.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/PatchWarden.cmd +51 -0
- package/README.en.md +1022 -0
- package/README.md +897 -358
- package/dist/assessments/agentAssessor.d.ts +15 -0
- package/dist/assessments/agentAssessor.js +293 -0
- package/dist/assessments/assessmentStore.d.ts +133 -0
- package/dist/assessments/assessmentStore.js +238 -0
- package/dist/assessments/confirmCli.d.ts +9 -0
- package/dist/assessments/confirmCli.js +26 -0
- package/dist/config.d.ts +22 -1
- package/dist/config.js +156 -3
- package/dist/direct/directAudit.d.ts +23 -0
- package/dist/direct/directAudit.js +309 -0
- package/dist/direct/directGuards.d.ts +20 -0
- package/dist/direct/directGuards.js +137 -0
- package/dist/direct/directPatch.d.ts +17 -0
- package/dist/direct/directPatch.js +113 -0
- package/dist/direct/directSessionStore.d.ts +63 -0
- package/dist/direct/directSessionStore.js +192 -0
- package/dist/direct/directVerification.d.ts +12 -0
- package/dist/direct/directVerification.js +96 -0
- package/dist/doctor.js +73 -5
- package/dist/logging.d.ts +52 -0
- package/dist/logging.js +123 -0
- package/dist/runner/agentInvocation.d.ts +23 -0
- package/dist/runner/agentInvocation.js +106 -0
- package/dist/runner/changeCapture.d.ts +70 -0
- package/dist/runner/changeCapture.js +278 -6
- package/dist/runner/runTask.js +271 -65
- package/dist/runner/simpleProcess.d.ts +21 -0
- package/dist/runner/simpleProcess.js +206 -0
- package/dist/runner/watch.js +17 -2
- package/dist/security/commandGuard.d.ts +2 -1
- package/dist/security/commandGuard.js +25 -3
- package/dist/security/riskEngine.d.ts +27 -0
- package/dist/security/riskEngine.js +118 -0
- package/dist/security/runtimeGuard.d.ts +6 -0
- package/dist/security/runtimeGuard.js +28 -0
- package/dist/smoke-test.js +1353 -7
- package/dist/test/unit/android-doctor.test.d.ts +1 -0
- package/dist/test/unit/android-doctor.test.js +118 -0
- package/dist/test/unit/chinese-path.test.d.ts +1 -0
- package/dist/test/unit/chinese-path.test.js +91 -0
- package/dist/test/unit/command-guard.test.d.ts +1 -0
- package/dist/test/unit/command-guard.test.js +160 -0
- package/dist/test/unit/direct-guards.test.d.ts +1 -0
- package/dist/test/unit/direct-guards.test.js +213 -0
- package/dist/test/unit/logging.test.d.ts +1 -0
- package/dist/test/unit/logging.test.js +275 -0
- package/dist/test/unit/path-guard.test.d.ts +1 -0
- package/dist/test/unit/path-guard.test.js +109 -0
- package/dist/test/unit/safe-status.test.d.ts +1 -0
- package/dist/test/unit/safe-status.test.js +165 -0
- package/dist/test/unit/sensitive-guard.test.d.ts +1 -0
- package/dist/test/unit/sensitive-guard.test.js +104 -0
- package/dist/test/unit/sync-file.test.d.ts +1 -0
- package/dist/test/unit/sync-file.test.js +154 -0
- package/dist/test/unit/watcher-status.test.d.ts +1 -0
- package/dist/test/unit/watcher-status.test.js +169 -0
- package/dist/tools/androidDoctor.d.ts +38 -0
- package/dist/tools/androidDoctor.js +391 -0
- package/dist/tools/applyPatch.d.ts +16 -0
- package/dist/tools/applyPatch.js +41 -0
- package/dist/tools/auditSession.d.ts +5 -0
- package/dist/tools/auditSession.js +12 -0
- package/dist/tools/auditTask.d.ts +7 -0
- package/dist/tools/auditTask.js +105 -7
- package/dist/tools/createDirectSession.d.ts +14 -0
- package/dist/tools/createDirectSession.js +54 -0
- package/dist/tools/createTask.d.ts +48 -1
- package/dist/tools/createTask.js +298 -47
- package/dist/tools/finalizeDirectSession.d.ts +19 -0
- package/dist/tools/finalizeDirectSession.js +84 -0
- package/dist/tools/getTaskSummary.d.ts +50 -0
- package/dist/tools/getTaskSummary.js +79 -2
- package/dist/tools/healthCheck.d.ts +27 -12
- package/dist/tools/healthCheck.js +44 -6
- package/dist/tools/readWorkspaceFile.d.ts +7 -1
- package/dist/tools/readWorkspaceFile.js +48 -2
- package/dist/tools/registry.js +270 -19
- package/dist/tools/runVerification.d.ts +16 -0
- package/dist/tools/runVerification.js +32 -0
- package/dist/tools/safeStatus.d.ts +19 -0
- package/dist/tools/safeStatus.js +72 -0
- package/dist/tools/savePlan.d.ts +1 -0
- package/dist/tools/savePlan.js +38 -7
- package/dist/tools/searchWorkspace.d.ts +19 -0
- package/dist/tools/searchWorkspace.js +205 -0
- package/dist/tools/syncFile.d.ts +18 -0
- package/dist/tools/syncFile.js +65 -0
- package/dist/tools/taskOutputs.d.ts +2 -2
- package/dist/tools/taskTemplates.js +2 -1
- package/dist/tools/toolCatalog.d.ts +4 -3
- package/dist/tools/toolCatalog.js +34 -5
- package/dist/tools/waitForTask.d.ts +2 -2
- package/dist/tools/waitForTask.js +1 -1
- package/dist/version.d.ts +2 -2
- package/dist/version.js +2 -2
- package/dist/watcherStatus.d.ts +1 -0
- package/dist/watcherStatus.js +96 -4
- package/docs/chatgpt-usage.md +101 -0
- package/docs/performance-notes.md +55 -0
- package/docs/release-checklist.md +14 -0
- package/docs/release-v0.6.0.md +71 -0
- package/docs/release-v0.6.1.md +75 -0
- package/examples/config.example.json +5 -0
- package/examples/openai-tunnel/README.md +11 -4
- package/examples/openai-tunnel/chatgpt-test-prompt.md +18 -14
- package/examples/openai-tunnel/tunnel-client.example.yaml +8 -0
- package/package.json +13 -8
- package/scripts/brand-check.js +58 -12
- package/scripts/control-smoke.js +206 -0
- package/scripts/http-mcp-smoke.js +10 -2
- package/scripts/launchers/Check-PatchWarden-Health.cmd +6 -0
- package/scripts/launchers/Reset-PatchWarden-Tunnel-Key.cmd +6 -0
- package/scripts/launchers/Restart-PatchWarden.cmd +6 -0
- package/scripts/launchers/Start-PatchWarden-Direct-Tunnel.cmd +7 -0
- package/scripts/launchers/Start-PatchWarden-Tunnel.cmd +7 -0
- package/scripts/lifecycle-smoke.js +402 -8
- package/scripts/manage-patchwarden.ps1 +639 -0
- package/scripts/mcp-manifest-check.js +123 -58
- package/scripts/mcp-smoke.js +160 -2
- package/scripts/pack-clean.js +160 -5
- package/scripts/package-manifest-check.js +78 -0
- package/scripts/patchwarden-mcp-direct.cmd +7 -0
- package/scripts/patchwarden-mcp-stdio.cmd +1 -1
- package/scripts/restart-patchwarden.ps1 +5 -4
- package/scripts/start-patchwarden-tunnel.ps1 +261 -30
- package/scripts/tunnel-supervisor-smoke.js +36 -4
- package/scripts/unit-tests.js +36 -0
- package/scripts/watcher-supervisor-smoke.js +10 -6
- package/src/assessments/agentAssessor.ts +324 -0
- package/src/assessments/assessmentStore.ts +426 -0
- package/src/assessments/confirmCli.ts +29 -0
- package/src/config.ts +178 -4
- package/src/direct/directAudit.ts +400 -0
- package/src/direct/directGuards.ts +279 -0
- package/src/direct/directPatch.ts +258 -0
- package/src/direct/directSessionStore.ts +345 -0
- package/src/direct/directVerification.ts +138 -0
- package/src/doctor.ts +103 -7
- package/src/logging.ts +152 -0
- package/src/runner/agentInvocation.ts +125 -0
- package/src/runner/changeCapture.ts +352 -6
- package/src/runner/runTask.ts +279 -63
- package/src/runner/simpleProcess.ts +223 -0
- package/src/runner/watch.ts +18 -2
- package/src/security/commandGuard.ts +46 -4
- package/src/security/riskEngine.ts +160 -0
- package/src/security/runtimeGuard.ts +41 -0
- package/src/smoke-test.ts +1291 -4
- package/src/test/unit/android-doctor.test.ts +158 -0
- package/src/test/unit/chinese-path.test.ts +106 -0
- package/src/test/unit/command-guard.test.ts +221 -0
- package/src/test/unit/direct-guards.test.ts +297 -0
- package/src/test/unit/logging.test.ts +325 -0
- package/src/test/unit/path-guard.test.ts +150 -0
- package/src/test/unit/safe-status.test.ts +187 -0
- package/src/test/unit/sensitive-guard.test.ts +124 -0
- package/src/test/unit/sync-file.test.ts +231 -0
- package/src/test/unit/watcher-status.test.ts +190 -0
- package/src/tools/androidDoctor.ts +424 -0
- package/src/tools/applyPatch.ts +86 -0
- package/src/tools/auditSession.ts +28 -0
- package/src/tools/auditTask.ts +111 -7
- package/src/tools/createDirectSession.ts +113 -0
- package/src/tools/createTask.ts +405 -55
- package/src/tools/finalizeDirectSession.ts +144 -0
- package/src/tools/getTaskSummary.ts +133 -2
- package/src/tools/healthCheck.ts +45 -6
- package/src/tools/readWorkspaceFile.ts +85 -2
- package/src/tools/registry.ts +305 -19
- package/src/tools/runVerification.ts +58 -0
- package/src/tools/safeStatus.ts +96 -0
- package/src/tools/savePlan.ts +57 -7
- package/src/tools/searchWorkspace.ts +275 -0
- package/src/tools/syncFile.ts +122 -0
- package/src/tools/taskTemplates.ts +2 -1
- package/src/tools/toolCatalog.ts +37 -6
- package/src/tools/waitForTask.ts +3 -3
- package/src/version.ts +2 -2
- package/src/watcherStatus.ts +101 -4
- package/tsconfig.json +18 -17
- package/Check-PatchWarden-Health.cmd +0 -6
- package/Reset-PatchWarden-Tunnel-Key.cmd +0 -6
- package/Restart-PatchWarden.cmd +0 -19
- package/Start-PatchWarden-Tunnel.cmd +0 -7
- package/docs/release-v0.3.0.md +0 -43
- package/docs/release-v0.4.0.md +0 -74
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { describe, it, beforeEach, afterEach } from "node:test";
|
|
2
|
+
import { strict as assert } from "node:assert";
|
|
3
|
+
import { mkdtempSync, rmSync, mkdirSync, writeFileSync, symlinkSync } from "node:fs";
|
|
4
|
+
import { join, resolve, sep } from "node:path";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { guardPath, guardWorkspacePath } from "../../security/pathGuard.js";
|
|
7
|
+
import { PatchWardenError } from "../../errors.js";
|
|
8
|
+
|
|
9
|
+
describe("guardPath", () => {
|
|
10
|
+
let tempDir: string;
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
tempDir = mkdtempSync(join(tmpdir(), "pw-pathguard-"));
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("allows paths inside workspace", () => {
|
|
21
|
+
const result = guardPath("src/main.ts", tempDir);
|
|
22
|
+
assert.ok(result.startsWith(tempDir));
|
|
23
|
+
assert.ok(result.includes("src"));
|
|
24
|
+
assert.ok(result.includes("main.ts"));
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("allows root workspace path", () => {
|
|
28
|
+
const result = guardPath(".", tempDir);
|
|
29
|
+
assert.equal(result, resolve(tempDir));
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("allows empty path as workspace root", () => {
|
|
33
|
+
const result = guardPath("", tempDir);
|
|
34
|
+
assert.equal(result, resolve(tempDir));
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("rejects path traversal with ..", () => {
|
|
38
|
+
assert.throws(
|
|
39
|
+
() => guardPath("../../../etc/passwd", tempDir),
|
|
40
|
+
PatchWardenError
|
|
41
|
+
);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("rejects path traversal nested with ..", () => {
|
|
45
|
+
assert.throws(
|
|
46
|
+
() => guardPath("src/../../../etc/passwd", tempDir),
|
|
47
|
+
PatchWardenError
|
|
48
|
+
);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("rejects absolute path outside workspace", () => {
|
|
52
|
+
const outside = process.platform === "win32" ? "C:\\Windows\\System32" : "/etc";
|
|
53
|
+
assert.throws(
|
|
54
|
+
() => guardPath(outside, tempDir),
|
|
55
|
+
PatchWardenError
|
|
56
|
+
);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("allows absolute path inside workspace", () => {
|
|
60
|
+
const insidePath = join(tempDir, "src", "main.ts");
|
|
61
|
+
mkdirSync(join(tempDir, "src"), { recursive: true });
|
|
62
|
+
writeFileSync(insidePath, "test", "utf-8");
|
|
63
|
+
const result = guardPath(insidePath, tempDir);
|
|
64
|
+
assert.equal(result, resolve(insidePath));
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("enforces allowedPrefix", () => {
|
|
68
|
+
mkdirSync(join(tempDir, "allowed"), { recursive: true });
|
|
69
|
+
mkdirSync(join(tempDir, "forbidden"), { recursive: true });
|
|
70
|
+
// Path inside allowed prefix works
|
|
71
|
+
const okResult = guardPath("allowed/file.ts", tempDir, "allowed");
|
|
72
|
+
assert.ok(okResult.includes("allowed"));
|
|
73
|
+
// Path outside allowed prefix fails
|
|
74
|
+
assert.throws(
|
|
75
|
+
() => guardPath("forbidden/file.ts", tempDir, "allowed"),
|
|
76
|
+
PatchWardenError
|
|
77
|
+
);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("handles Windows mixed separators", () => {
|
|
81
|
+
const mixedPath = "src\\subdir/file.ts";
|
|
82
|
+
const result = guardPath(mixedPath, tempDir);
|
|
83
|
+
assert.ok(result.startsWith(tempDir));
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("rejects symlink escape", { skip: process.platform === "win32" ? "Windows symlink permissions unstable" : undefined }, () => {
|
|
87
|
+
const target = mkdtempSync(join(tmpdir(), "pw-symlink-target-"));
|
|
88
|
+
try {
|
|
89
|
+
const linkPath = join(tempDir, "escape-link");
|
|
90
|
+
symlinkSync(target, linkPath);
|
|
91
|
+
assert.throws(
|
|
92
|
+
() => guardPath("escape-link/secret.txt", tempDir),
|
|
93
|
+
PatchWardenError
|
|
94
|
+
);
|
|
95
|
+
} finally {
|
|
96
|
+
rmSync(target, { recursive: true, force: true });
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe("guardWorkspacePath", () => {
|
|
102
|
+
let tempDir: string;
|
|
103
|
+
|
|
104
|
+
beforeEach(() => {
|
|
105
|
+
tempDir = mkdtempSync(join(tmpdir(), "pw-wspath-"));
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
afterEach(() => {
|
|
109
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("allows relative path inside workspace", () => {
|
|
113
|
+
const result = guardWorkspacePath("my-repo", tempDir);
|
|
114
|
+
assert.equal(result, resolve(tempDir, "my-repo"));
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("allows absolute path inside workspace", () => {
|
|
118
|
+
const absPath = join(tempDir, "my-repo");
|
|
119
|
+
const result = guardWorkspacePath(absPath, tempDir);
|
|
120
|
+
assert.equal(result, resolve(absPath));
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("rejects path outside workspace", () => {
|
|
124
|
+
assert.throws(
|
|
125
|
+
() => guardWorkspacePath("../../../etc", tempDir),
|
|
126
|
+
PatchWardenError
|
|
127
|
+
);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("rejects different drive letter on Windows", { skip: process.platform !== "win32" ? "Windows-only test" : undefined }, () => {
|
|
131
|
+
// If workspace is on C:, a D: path should be rejected
|
|
132
|
+
const wsDrive = tempDir.match(/^([A-Za-z]):/);
|
|
133
|
+
if (!wsDrive) return;
|
|
134
|
+
const otherDrive = wsDrive[1].toLowerCase() === "c" ? "D" : "C";
|
|
135
|
+
assert.throws(
|
|
136
|
+
() => guardWorkspacePath(`${otherDrive}:\\some\\repo`, tempDir),
|
|
137
|
+
(err: unknown) => err instanceof PatchWardenError && err.reason === "workspace_path_escape"
|
|
138
|
+
);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("defaults to workspace root for empty input", () => {
|
|
142
|
+
const result = guardWorkspacePath("", tempDir);
|
|
143
|
+
assert.equal(result, resolve(tempDir));
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("defaults to workspace root for dot input", () => {
|
|
147
|
+
const result = guardWorkspacePath(".", tempDir);
|
|
148
|
+
assert.equal(result, resolve(tempDir));
|
|
149
|
+
});
|
|
150
|
+
});
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { describe, it, beforeEach, afterEach } from "node:test";
|
|
2
|
+
import { strict as assert } from "node:assert";
|
|
3
|
+
import { mkdtempSync, rmSync, mkdirSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { join, resolve } from "node:path";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { safeStatus } from "../../tools/safeStatus.js";
|
|
7
|
+
import type { PatchWardenConfig } from "../../config.js";
|
|
8
|
+
|
|
9
|
+
function makeConfig(workspaceRoot: string): PatchWardenConfig {
|
|
10
|
+
return {
|
|
11
|
+
workspaceRoot,
|
|
12
|
+
plansDir: ".patchwarden/plans",
|
|
13
|
+
tasksDir: ".patchwarden/tasks",
|
|
14
|
+
assessmentsDir: ".patchwarden/assessments",
|
|
15
|
+
assessmentTtlSeconds: 3600,
|
|
16
|
+
agents: { codex: { command: "codex", args: ["exec", "{prompt}"] } },
|
|
17
|
+
allowedTestCommands: ["npm test"],
|
|
18
|
+
repoAllowedTestCommands: {},
|
|
19
|
+
maxReadFileBytes: 200_000,
|
|
20
|
+
defaultTaskTimeoutSeconds: 900,
|
|
21
|
+
maxTaskTimeoutSeconds: 3600,
|
|
22
|
+
watcherStaleSeconds: 30,
|
|
23
|
+
directSessionsDir: ".patchwarden/direct-sessions",
|
|
24
|
+
directSessionTtlSeconds: 3600,
|
|
25
|
+
directMaxPatchBytes: 200_000,
|
|
26
|
+
directMaxFileBytes: 500_000,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe("safeStatus", () => {
|
|
31
|
+
let tempDir: string;
|
|
32
|
+
let config: PatchWardenConfig;
|
|
33
|
+
let tasksDir: string;
|
|
34
|
+
|
|
35
|
+
beforeEach(() => {
|
|
36
|
+
tempDir = mkdtempSync(join(tmpdir(), "pw-safestatus-"));
|
|
37
|
+
config = makeConfig(tempDir);
|
|
38
|
+
tasksDir = join(tempDir, ".patchwarden", "tasks");
|
|
39
|
+
mkdirSync(tasksDir, { recursive: true });
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
afterEach(() => {
|
|
43
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
function writeTaskStatus(taskId: string, status: Record<string, unknown>) {
|
|
47
|
+
const taskDir = join(tasksDir, taskId);
|
|
48
|
+
mkdirSync(taskDir, { recursive: true });
|
|
49
|
+
writeFileSync(join(taskDir, "status.json"), JSON.stringify(status, null, 2), "utf-8");
|
|
50
|
+
return taskDir;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function writeTaskRuntime(taskDir: string, runtime: Record<string, unknown>) {
|
|
54
|
+
writeFileSync(join(taskDir, "runtime.json"), JSON.stringify(runtime, null, 2), "utf-8");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
it("returns not_found for non-existent task", () => {
|
|
58
|
+
const result = safeStatus("non-existent-task", config);
|
|
59
|
+
assert.equal(result.task_id, "non-existent-task");
|
|
60
|
+
assert.equal(result.status, "not_found");
|
|
61
|
+
assert.equal(result.phase, null);
|
|
62
|
+
assert.equal(result.error_code, null);
|
|
63
|
+
assert.equal(result.error_summary, null);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("returns correct status for pending task", () => {
|
|
67
|
+
writeTaskStatus("task-pending-001", {
|
|
68
|
+
task_id: "task-pending-001",
|
|
69
|
+
status: "pending",
|
|
70
|
+
phase: "queued",
|
|
71
|
+
created_at: "2026-06-24T10:00:00Z",
|
|
72
|
+
updated_at: "2026-06-24T10:00:01Z",
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const result = safeStatus("task-pending-001", config);
|
|
76
|
+
assert.equal(result.task_id, "task-pending-001");
|
|
77
|
+
assert.equal(result.status, "pending");
|
|
78
|
+
assert.equal(result.created_at, "2026-06-24T10:00:00Z");
|
|
79
|
+
assert.equal(result.error_code, null);
|
|
80
|
+
assert.equal(result.error_summary, null);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("returns correct status for running task", () => {
|
|
84
|
+
const taskDir = writeTaskStatus("task-running-001", {
|
|
85
|
+
task_id: "task-running-001",
|
|
86
|
+
status: "running",
|
|
87
|
+
phase: "executing_agent",
|
|
88
|
+
created_at: "2026-06-24T10:00:00Z",
|
|
89
|
+
started_at: "2026-06-24T10:00:05Z",
|
|
90
|
+
updated_at: "2026-06-24T10:00:10Z",
|
|
91
|
+
});
|
|
92
|
+
writeTaskRuntime(taskDir, {
|
|
93
|
+
phase: "executing_agent",
|
|
94
|
+
last_heartbeat_at: "2026-06-24T10:00:12Z",
|
|
95
|
+
current_command: "codex exec",
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const result = safeStatus("task-running-001", config);
|
|
99
|
+
assert.equal(result.status, "running");
|
|
100
|
+
assert.equal(result.phase, "executing_agent");
|
|
101
|
+
assert.equal(result.current_command, "codex exec");
|
|
102
|
+
assert.equal(result.last_heartbeat_at, "2026-06-24T10:00:12Z");
|
|
103
|
+
assert.equal(result.started_at, "2026-06-24T10:00:05Z");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("returns correct status for done task", () => {
|
|
107
|
+
writeTaskStatus("task-done-001", {
|
|
108
|
+
task_id: "task-done-001",
|
|
109
|
+
status: "done",
|
|
110
|
+
phase: "completed",
|
|
111
|
+
created_at: "2026-06-24T10:00:00Z",
|
|
112
|
+
started_at: "2026-06-24T10:00:05Z",
|
|
113
|
+
finished_at: "2026-06-24T10:05:00Z",
|
|
114
|
+
verify_status: "passed",
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const result = safeStatus("task-done-001", config);
|
|
118
|
+
assert.equal(result.status, "done");
|
|
119
|
+
assert.equal(result.phase, "completed");
|
|
120
|
+
assert.equal(result.finished_at, "2026-06-24T10:05:00Z");
|
|
121
|
+
assert.equal(result.verify_status, "passed");
|
|
122
|
+
assert.equal(result.error_code, null);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("returns correct status for failed task", () => {
|
|
126
|
+
writeTaskStatus("task-failed-001", {
|
|
127
|
+
task_id: "task-failed-001",
|
|
128
|
+
status: "failed_verification",
|
|
129
|
+
phase: "failed_verification",
|
|
130
|
+
created_at: "2026-06-24T10:00:00Z",
|
|
131
|
+
started_at: "2026-06-24T10:00:05Z",
|
|
132
|
+
finished_at: "2026-06-24T10:05:00Z",
|
|
133
|
+
error: 'Verification command "npm test" exited with code 1.',
|
|
134
|
+
verify_status: "failed",
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
const result = safeStatus("task-failed-001", config);
|
|
138
|
+
assert.equal(result.status, "failed_verification");
|
|
139
|
+
assert.equal(result.error_code, "failed_verification");
|
|
140
|
+
assert.ok(result.error_summary);
|
|
141
|
+
assert.ok(result.error_summary!.includes("npm test"));
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("does not return diff or file content", () => {
|
|
145
|
+
const taskDir = writeTaskStatus("task-safe-001", {
|
|
146
|
+
task_id: "task-safe-001",
|
|
147
|
+
status: "done",
|
|
148
|
+
phase: "completed",
|
|
149
|
+
created_at: "2026-06-24T10:00:00Z",
|
|
150
|
+
});
|
|
151
|
+
writeFileSync(join(taskDir, "result.md"), "# Secret result content", "utf-8");
|
|
152
|
+
writeFileSync(join(taskDir, "git.diff"), "secret diff content", "utf-8");
|
|
153
|
+
writeFileSync(join(taskDir, "test.log"), "secret test log", "utf-8");
|
|
154
|
+
|
|
155
|
+
const result = safeStatus("task-safe-001", config);
|
|
156
|
+
const resultStr = JSON.stringify(result);
|
|
157
|
+
assert.ok(!resultStr.includes("Secret result content"));
|
|
158
|
+
assert.ok(!resultStr.includes("secret diff content"));
|
|
159
|
+
assert.ok(!resultStr.includes("secret test log"));
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("truncates long error messages", () => {
|
|
163
|
+
const longError = "x".repeat(300);
|
|
164
|
+
writeTaskStatus("task-longerror-001", {
|
|
165
|
+
task_id: "task-longerror-001",
|
|
166
|
+
status: "failed",
|
|
167
|
+
phase: "failed",
|
|
168
|
+
error: longError,
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
const result = safeStatus("task-longerror-001", config);
|
|
172
|
+
assert.ok(result.error_summary!.length < 300);
|
|
173
|
+
assert.ok(result.error_summary!.endsWith("..."));
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("returns artifact_status when present", () => {
|
|
177
|
+
writeTaskStatus("task-artifact-001", {
|
|
178
|
+
task_id: "task-artifact-001",
|
|
179
|
+
status: "done",
|
|
180
|
+
phase: "completed",
|
|
181
|
+
artifact_status: "collected",
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
const result = safeStatus("task-artifact-001", config);
|
|
185
|
+
assert.equal(result.artifact_status, "collected");
|
|
186
|
+
});
|
|
187
|
+
});
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { describe, it } from "node:test";
|
|
2
|
+
import { strict as assert } from "node:assert";
|
|
3
|
+
import { isSensitivePath, guardSensitivePath } from "../../security/sensitiveGuard.js";
|
|
4
|
+
import { PatchWardenError } from "../../errors.js";
|
|
5
|
+
|
|
6
|
+
describe("isSensitivePath", () => {
|
|
7
|
+
it("blocks .env files", () => {
|
|
8
|
+
assert.equal(isSensitivePath(".env"), true);
|
|
9
|
+
assert.equal(isSensitivePath("path/to/.env"), true);
|
|
10
|
+
assert.equal(isSensitivePath(".env.production"), true);
|
|
11
|
+
assert.equal(isSensitivePath(".env.local"), true);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("blocks .ENV case insensitive", () => {
|
|
15
|
+
assert.equal(isSensitivePath(".ENV"), true);
|
|
16
|
+
assert.equal(isSensitivePath("path/to/.ENV"), true);
|
|
17
|
+
assert.equal(isSensitivePath(".Env.Production"), true);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("blocks config.json case insensitive", () => {
|
|
21
|
+
assert.equal(isSensitivePath("config.json"), true);
|
|
22
|
+
assert.equal(isSensitivePath("Config.json"), true);
|
|
23
|
+
assert.equal(isSensitivePath("CONFIG.JSON"), true);
|
|
24
|
+
assert.equal(isSensitivePath("path/to/Config.json"), true);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("blocks SSH keys", () => {
|
|
28
|
+
assert.equal(isSensitivePath("id_rsa"), true);
|
|
29
|
+
assert.equal(isSensitivePath("id_ed25519"), true);
|
|
30
|
+
assert.equal(isSensitivePath("id_ecdsa"), true);
|
|
31
|
+
assert.equal(isSensitivePath("path/to/id_rsa"), true);
|
|
32
|
+
assert.equal(isSensitivePath(".ssh/id_rsa"), true);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("blocks credentials and tokens", () => {
|
|
36
|
+
assert.equal(isSensitivePath("credentials"), true);
|
|
37
|
+
assert.equal(isSensitivePath("path/to/credentials.json"), true);
|
|
38
|
+
assert.equal(isSensitivePath("token.txt"), true);
|
|
39
|
+
// access_token does NOT match because the pattern requires token at start or after /
|
|
40
|
+
assert.equal(isSensitivePath("access_token"), false);
|
|
41
|
+
assert.equal(isSensitivePath("path/to/token"), true);
|
|
42
|
+
assert.equal(isSensitivePath(".netrc"), true);
|
|
43
|
+
assert.equal(isSensitivePath(".npmrc"), true);
|
|
44
|
+
assert.equal(isSensitivePath(".pypirc"), true);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("blocks private key files", () => {
|
|
48
|
+
assert.equal(isSensitivePath("server.pem"), true);
|
|
49
|
+
assert.equal(isSensitivePath("private.key"), true);
|
|
50
|
+
assert.equal(isSensitivePath("cert.pfx"), true);
|
|
51
|
+
assert.equal(isSensitivePath("cert.p12"), true);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("blocks browser data files", () => {
|
|
55
|
+
assert.equal(isSensitivePath("cookies"), true);
|
|
56
|
+
assert.equal(isSensitivePath("cookies.db"), true);
|
|
57
|
+
assert.equal(isSensitivePath("Web Data"), true);
|
|
58
|
+
assert.equal(isSensitivePath("Login Data"), true);
|
|
59
|
+
assert.equal(isSensitivePath("Local State"), true);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("blocks .git-credentials", () => {
|
|
63
|
+
assert.equal(isSensitivePath(".git-credentials"), true);
|
|
64
|
+
assert.equal(isSensitivePath("path/to/.git-credentials"), true);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("blocks docker and kube config", () => {
|
|
68
|
+
assert.equal(isSensitivePath(".docker/config.json"), true);
|
|
69
|
+
assert.equal(isSensitivePath(".kube/config"), true);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("allows .patchwarden safe prefix", () => {
|
|
73
|
+
assert.equal(isSensitivePath(".patchwarden/tasks/task-001/status.json"), false);
|
|
74
|
+
assert.equal(isSensitivePath(".patchwarden/config.json"), false);
|
|
75
|
+
assert.equal(isSensitivePath(".patchwarden"), false);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("allows non-sensitive files", () => {
|
|
79
|
+
assert.equal(isSensitivePath("src/main.ts"), false);
|
|
80
|
+
assert.equal(isSensitivePath("README.md"), false);
|
|
81
|
+
assert.equal(isSensitivePath("package.json"), false);
|
|
82
|
+
assert.equal(isSensitivePath("docs/guide.md"), false);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("handles Windows backslash paths", () => {
|
|
86
|
+
assert.equal(isSensitivePath("path\\to\\.env"), true);
|
|
87
|
+
assert.equal(isSensitivePath("path\\to\\config.json"), true);
|
|
88
|
+
assert.equal(isSensitivePath(".patchwarden\\tasks\\status.json"), false);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("handles null byte in path", () => {
|
|
92
|
+
// config.json with null byte appended — the $ anchor means this won't match
|
|
93
|
+
// because the string doesn't end with "config.json"
|
|
94
|
+
const nullPath = "config.json\x00.txt";
|
|
95
|
+
assert.equal(isSensitivePath(nullPath), false);
|
|
96
|
+
// But plain config.json still matches
|
|
97
|
+
assert.equal(isSensitivePath("config.json"), true);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("handles Unicode lookalike characters", () => {
|
|
101
|
+
// Full-width dot (U+FF0E) should not match .env pattern
|
|
102
|
+
// This is expected behavior — Unicode lookalikes are NOT matched
|
|
103
|
+
const fullWidthPath = "\uFF0Eenv";
|
|
104
|
+
assert.equal(isSensitivePath(fullWidthPath), false);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
describe("guardSensitivePath", () => {
|
|
109
|
+
it("throws PatchWardenError for sensitive paths", () => {
|
|
110
|
+
assert.throws(
|
|
111
|
+
() => guardSensitivePath(".env"),
|
|
112
|
+
(err: unknown) => err instanceof PatchWardenError && err.reason === "sensitive_path_blocked"
|
|
113
|
+
);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("does not throw for non-sensitive paths", () => {
|
|
117
|
+
assert.doesNotThrow(() => guardSensitivePath("src/main.ts"));
|
|
118
|
+
assert.doesNotThrow(() => guardSensitivePath("README.md"));
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("does not throw for .patchwarden paths", () => {
|
|
122
|
+
assert.doesNotThrow(() => guardSensitivePath(".patchwarden/tasks/status.json"));
|
|
123
|
+
});
|
|
124
|
+
});
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { describe, it, beforeEach, afterEach } from "node:test";
|
|
2
|
+
import { strict as assert } from "node:assert";
|
|
3
|
+
import { mkdtempSync, rmSync, mkdirSync, writeFileSync, readFileSync, existsSync } from "node:fs";
|
|
4
|
+
import { join, resolve } from "node:path";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { syncFile } from "../../tools/syncFile.js";
|
|
7
|
+
import { computeFileSha256 } from "../../direct/directPatch.js";
|
|
8
|
+
import type { PatchWardenConfig } from "../../config.js";
|
|
9
|
+
|
|
10
|
+
function makeConfig(workspaceRoot: string): PatchWardenConfig {
|
|
11
|
+
return {
|
|
12
|
+
workspaceRoot,
|
|
13
|
+
plansDir: ".patchwarden/plans",
|
|
14
|
+
tasksDir: ".patchwarden/tasks",
|
|
15
|
+
assessmentsDir: ".patchwarden/assessments",
|
|
16
|
+
assessmentTtlSeconds: 3600,
|
|
17
|
+
agents: { codex: { command: "codex", args: ["exec", "{prompt}"] } },
|
|
18
|
+
allowedTestCommands: ["npm test"],
|
|
19
|
+
repoAllowedTestCommands: {},
|
|
20
|
+
maxReadFileBytes: 200_000,
|
|
21
|
+
defaultTaskTimeoutSeconds: 900,
|
|
22
|
+
maxTaskTimeoutSeconds: 3600,
|
|
23
|
+
watcherStaleSeconds: 30,
|
|
24
|
+
directSessionsDir: ".patchwarden/direct-sessions",
|
|
25
|
+
directSessionTtlSeconds: 3600,
|
|
26
|
+
directMaxPatchBytes: 200_000,
|
|
27
|
+
directMaxFileBytes: 500_000,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe("syncFile", () => {
|
|
32
|
+
let tempDir: string;
|
|
33
|
+
let config: PatchWardenConfig;
|
|
34
|
+
let repoPath: string;
|
|
35
|
+
let sessionsDir: string;
|
|
36
|
+
let sessionId: string;
|
|
37
|
+
|
|
38
|
+
beforeEach(() => {
|
|
39
|
+
tempDir = mkdtempSync(join(tmpdir(), "pw-syncfile-"));
|
|
40
|
+
config = makeConfig(tempDir);
|
|
41
|
+
repoPath = join(tempDir, "my-repo");
|
|
42
|
+
sessionsDir = join(tempDir, ".patchwarden", "direct-sessions");
|
|
43
|
+
sessionId = "test-session-001";
|
|
44
|
+
|
|
45
|
+
mkdirSync(repoPath, { recursive: true });
|
|
46
|
+
mkdirSync(join(sessionsDir, sessionId), { recursive: true });
|
|
47
|
+
|
|
48
|
+
writeFileSync(
|
|
49
|
+
join(sessionsDir, sessionId, "session.json"),
|
|
50
|
+
JSON.stringify({
|
|
51
|
+
session_id: sessionId,
|
|
52
|
+
repo_path: repoPath,
|
|
53
|
+
status: "active",
|
|
54
|
+
created_at: new Date().toISOString(),
|
|
55
|
+
}),
|
|
56
|
+
"utf-8"
|
|
57
|
+
);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
afterEach(() => {
|
|
61
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("copies a file from source to target", () => {
|
|
65
|
+
const sourceContent = "console.log('hello world');";
|
|
66
|
+
mkdirSync(join(repoPath, "mobile_app"), { recursive: true });
|
|
67
|
+
writeFileSync(join(repoPath, "mobile_app", "app.js"), sourceContent, "utf-8");
|
|
68
|
+
|
|
69
|
+
const result = syncFile(sessionId, "mobile_app/app.js", "windows_app/app/app.js", undefined, config);
|
|
70
|
+
|
|
71
|
+
assert.equal(result.changed, true);
|
|
72
|
+
assert.equal(result.copied_bytes, sourceContent.length);
|
|
73
|
+
assert.equal(result.before_target_sha256, null);
|
|
74
|
+
assert.ok(result.after_target_sha256);
|
|
75
|
+
|
|
76
|
+
const targetContent = readFileSync(join(repoPath, "windows_app", "app", "app.js"), "utf-8");
|
|
77
|
+
assert.equal(targetContent, sourceContent);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("returns changed=false when target already has same content", () => {
|
|
81
|
+
const sourceContent = "same content";
|
|
82
|
+
mkdirSync(join(repoPath, "src"), { recursive: true });
|
|
83
|
+
mkdirSync(join(repoPath, "dst"), { recursive: true });
|
|
84
|
+
writeFileSync(join(repoPath, "src", "file.ts"), sourceContent, "utf-8");
|
|
85
|
+
writeFileSync(join(repoPath, "dst", "file.ts"), sourceContent, "utf-8");
|
|
86
|
+
|
|
87
|
+
const result = syncFile(sessionId, "src/file.ts", "dst/file.ts", undefined, config);
|
|
88
|
+
|
|
89
|
+
assert.equal(result.changed, false);
|
|
90
|
+
assert.ok(result.before_target_sha256);
|
|
91
|
+
assert.equal(result.before_target_sha256, result.after_target_sha256);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("returns changed=true when target has different content", () => {
|
|
95
|
+
mkdirSync(join(repoPath, "src"), { recursive: true });
|
|
96
|
+
mkdirSync(join(repoPath, "dst"), { recursive: true });
|
|
97
|
+
writeFileSync(join(repoPath, "src", "file.ts"), "new content", "utf-8");
|
|
98
|
+
writeFileSync(join(repoPath, "dst", "file.ts"), "old content", "utf-8");
|
|
99
|
+
|
|
100
|
+
const result = syncFile(sessionId, "src/file.ts", "dst/file.ts", undefined, config);
|
|
101
|
+
|
|
102
|
+
assert.equal(result.changed, true);
|
|
103
|
+
assert.notEqual(result.before_target_sha256, result.after_target_sha256);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("rejects source path outside repo", () => {
|
|
107
|
+
mkdirSync(join(repoPath, "src"), { recursive: true });
|
|
108
|
+
writeFileSync(join(repoPath, "src", "file.ts"), "content", "utf-8");
|
|
109
|
+
|
|
110
|
+
assert.throws(
|
|
111
|
+
() => syncFile(sessionId, "../../../etc/passwd", "dst/file.ts", undefined, config),
|
|
112
|
+
Error
|
|
113
|
+
);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("rejects target path outside repo", () => {
|
|
117
|
+
mkdirSync(join(repoPath, "src"), { recursive: true });
|
|
118
|
+
writeFileSync(join(repoPath, "src", "file.ts"), "content", "utf-8");
|
|
119
|
+
|
|
120
|
+
assert.throws(
|
|
121
|
+
() => syncFile(sessionId, "src/file.ts", "../../../etc/evil", undefined, config),
|
|
122
|
+
Error
|
|
123
|
+
);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("rejects sensitive source files", () => {
|
|
127
|
+
writeFileSync(join(repoPath, ".env"), "SECRET=abc123", "utf-8");
|
|
128
|
+
|
|
129
|
+
assert.throws(
|
|
130
|
+
() => syncFile(sessionId, ".env", "dst/.env", undefined, config),
|
|
131
|
+
Error
|
|
132
|
+
);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("rejects sensitive target files", () => {
|
|
136
|
+
mkdirSync(join(repoPath, "src"), { recursive: true });
|
|
137
|
+
writeFileSync(join(repoPath, "src", "file.ts"), "content", "utf-8");
|
|
138
|
+
|
|
139
|
+
assert.throws(
|
|
140
|
+
() => syncFile(sessionId, "src/file.ts", "config.json", undefined, config),
|
|
141
|
+
Error
|
|
142
|
+
);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("rejects writing to node_modules", () => {
|
|
146
|
+
mkdirSync(join(repoPath, "src"), { recursive: true });
|
|
147
|
+
writeFileSync(join(repoPath, "src", "file.ts"), "content", "utf-8");
|
|
148
|
+
|
|
149
|
+
assert.throws(
|
|
150
|
+
() => syncFile(sessionId, "src/file.ts", "node_modules/evil/index.js", undefined, config),
|
|
151
|
+
Error
|
|
152
|
+
);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("rejects writing to dist", () => {
|
|
156
|
+
mkdirSync(join(repoPath, "src"), { recursive: true });
|
|
157
|
+
writeFileSync(join(repoPath, "src", "file.ts"), "content", "utf-8");
|
|
158
|
+
|
|
159
|
+
assert.throws(
|
|
160
|
+
() => syncFile(sessionId, "src/file.ts", "dist/main.js", undefined, config),
|
|
161
|
+
Error
|
|
162
|
+
);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("rejects non-existent session", () => {
|
|
166
|
+
mkdirSync(join(repoPath, "src"), { recursive: true });
|
|
167
|
+
writeFileSync(join(repoPath, "src", "file.ts"), "content", "utf-8");
|
|
168
|
+
|
|
169
|
+
assert.throws(
|
|
170
|
+
() => syncFile("non-existent-session", "src/file.ts", "dst/file.ts", undefined, config),
|
|
171
|
+
Error
|
|
172
|
+
);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("rejects non-existent source file", () => {
|
|
176
|
+
assert.throws(
|
|
177
|
+
() => syncFile(sessionId, "non-existent/file.ts", "dst/file.ts", undefined, config),
|
|
178
|
+
Error
|
|
179
|
+
);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("validates expected_source_sha256", () => {
|
|
183
|
+
mkdirSync(join(repoPath, "src"), { recursive: true });
|
|
184
|
+
writeFileSync(join(repoPath, "src", "file.ts"), "content", "utf-8");
|
|
185
|
+
|
|
186
|
+
const correctHash = computeFileSha256(join(repoPath, "src", "file.ts"));
|
|
187
|
+
|
|
188
|
+
const result = syncFile(sessionId, "src/file.ts", "dst/file.ts", {
|
|
189
|
+
expected_source_sha256: correctHash,
|
|
190
|
+
}, config);
|
|
191
|
+
assert.equal(result.source_sha256, correctHash);
|
|
192
|
+
|
|
193
|
+
assert.throws(
|
|
194
|
+
() => syncFile(sessionId, "src/file.ts", "dst2/file.ts", {
|
|
195
|
+
expected_source_sha256: "wronghash",
|
|
196
|
+
}, config),
|
|
197
|
+
Error
|
|
198
|
+
);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it("validates expected_target_sha256", () => {
|
|
202
|
+
mkdirSync(join(repoPath, "src"), { recursive: true });
|
|
203
|
+
mkdirSync(join(repoPath, "dst"), { recursive: true });
|
|
204
|
+
writeFileSync(join(repoPath, "src", "file.ts"), "new content", "utf-8");
|
|
205
|
+
writeFileSync(join(repoPath, "dst", "file.ts"), "old content", "utf-8");
|
|
206
|
+
|
|
207
|
+
const correctTargetHash = computeFileSha256(join(repoPath, "dst", "file.ts"));
|
|
208
|
+
|
|
209
|
+
const result = syncFile(sessionId, "src/file.ts", "dst/file.ts", {
|
|
210
|
+
expected_target_sha256: correctTargetHash,
|
|
211
|
+
}, config);
|
|
212
|
+
assert.equal(result.before_target_sha256, correctTargetHash);
|
|
213
|
+
|
|
214
|
+
assert.throws(
|
|
215
|
+
() => syncFile(sessionId, "src/file.ts", "dst/file.ts", {
|
|
216
|
+
expected_target_sha256: "wronghash",
|
|
217
|
+
}, config),
|
|
218
|
+
Error
|
|
219
|
+
);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it("creates target directory if it doesn't exist", () => {
|
|
223
|
+
mkdirSync(join(repoPath, "src"), { recursive: true });
|
|
224
|
+
writeFileSync(join(repoPath, "src", "file.ts"), "content", "utf-8");
|
|
225
|
+
|
|
226
|
+
const result = syncFile(sessionId, "src/file.ts", "deep/nested/path/file.ts", undefined, config);
|
|
227
|
+
|
|
228
|
+
assert.equal(result.changed, true);
|
|
229
|
+
assert.ok(existsSync(join(repoPath, "deep", "nested", "path", "file.ts")));
|
|
230
|
+
});
|
|
231
|
+
});
|