

























1+import os from "node:os";
2+import path from "node:path";
3+import { describe, expect, it } from "vitest";
4+import { extractApplyPatchTargetPaths } from "./apply-patch-paths.js";
5+6+const defaultCwd = process.cwd();
7+const cwdPath = (...segments: string[]) => path.join(defaultCwd, ...segments);
8+9+describe("extractApplyPatchTargetPaths", () => {
10+it("returns an empty array for non-string input", () => {
11+expect(extractApplyPatchTargetPaths(undefined)).toEqual([]);
12+expect(extractApplyPatchTargetPaths(null)).toEqual([]);
13+expect(extractApplyPatchTargetPaths(42)).toEqual([]);
14+expect(extractApplyPatchTargetPaths({})).toEqual([]);
15+expect(extractApplyPatchTargetPaths({ input: 7 })).toEqual([]);
16+});
17+18+it("returns an empty array for an empty patch", () => {
19+expect(extractApplyPatchTargetPaths("")).toEqual([]);
20+expect(extractApplyPatchTargetPaths({ input: "" })).toEqual([]);
21+});
22+23+it("extracts Add File markers from the envelope payload", () => {
24+const patch = [
25+"*** Begin Patch",
26+"*** Add File: src/new.ts",
27+"+export const a = 1;",
28+"*** End Patch",
29+].join("\n");
30+expect(extractApplyPatchTargetPaths(patch)).toEqual([cwdPath("src/new.ts")]);
31+});
32+33+it("extracts Update File and Delete File markers", () => {
34+const patch = [
35+"*** Begin Patch",
36+"*** Update File: a.ts",
37+"@@",
38+" context",
39+"+added",
40+"*** Delete File: b.ts",
41+"*** End Patch",
42+].join("\n");
43+expect(extractApplyPatchTargetPaths(patch)).toEqual([cwdPath("a.ts"), cwdPath("b.ts")]);
44+});
45+46+it("includes the Move to: target paired with an Update File", () => {
47+const patch = [
48+"*** Begin Patch",
49+"*** Update File: old/path.ts",
50+"*** Move to: new/path.ts",
51+"@@",
52+" context",
53+"+added",
54+"*** End Patch",
55+].join("\n");
56+expect(extractApplyPatchTargetPaths(patch)).toEqual([
57+cwdPath("old/path.ts"),
58+cwdPath("new/path.ts"),
59+]);
60+});
61+62+it("tolerates blank lines between Update File and Move to", () => {
63+const patch = [
64+"*** Begin Patch",
65+"*** Update File: a.ts",
66+"",
67+"*** Move to: b.ts",
68+"*** End Patch",
69+].join("\n");
70+expect(extractApplyPatchTargetPaths(patch)).toEqual([cwdPath("a.ts"), cwdPath("b.ts")]);
71+});
72+73+it("accepts the wrapper object form used by the apply_patch tool", () => {
74+const patch = ["*** Begin Patch", "*** Add File: foo.ts", "+x", "*** End Patch"].join("\n");
75+expect(extractApplyPatchTargetPaths({ input: patch })).toEqual([cwdPath("foo.ts")]);
76+});
77+78+it("de-duplicates repeated paths within a single envelope", () => {
79+const patch = [
80+"*** Begin Patch",
81+"*** Add File: same.ts",
82+"+a",
83+"*** Update File: same.ts",
84+"@@",
85+"+b",
86+"*** End Patch",
87+].join("\n");
88+expect(extractApplyPatchTargetPaths(patch)).toEqual([cwdPath("same.ts")]);
89+});
90+91+it("normalizes derived paths before de-duplicating them", () => {
92+const patch = [
93+"*** Begin Patch",
94+"*** Add File: safe/../secret.ts",
95+"+x",
96+"*** Update File: ./src//old.ts",
97+"*** Move to: src/temp/../renamed.ts",
98+"@@",
99+"+y",
100+"*** Delete File: secret.ts",
101+"*** End Patch",
102+].join("\n");
103+expect(extractApplyPatchTargetPaths(patch)).toEqual([
104+cwdPath("secret.ts"),
105+cwdPath("src/old.ts"),
106+cwdPath("src/renamed.ts"),
107+]);
108+});
109+110+it("preserves POSIX backslashes to match apply_patch execution", () => {
111+const patch = [
112+"*** Begin Patch",
113+String.raw`*** Add File: src\windows\path.ts`,
114+"+x",
115+String.raw`*** Add File: safe\evil.ts`,
116+"*** End Patch",
117+].join("\n");
118+expect(extractApplyPatchTargetPaths(patch)).toEqual([
119+path.resolve(defaultCwd, String.raw`src\windows\path.ts`),
120+path.resolve(defaultCwd, String.raw`safe\evil.ts`),
121+]);
122+expect(extractApplyPatchTargetPaths(patch)).not.toContain(cwdPath("safe", "evil.ts"));
123+});
124+125+it("handles CRLF line endings", () => {
126+const patch = ["*** Begin Patch", "*** Add File: crlf.ts", "+x", "*** End Patch"].join("\r\n");
127+expect(extractApplyPatchTargetPaths(patch)).toEqual([cwdPath("crlf.ts")]);
128+});
129+130+it("matches indented hunk headers the same way as the apply_patch executor", () => {
131+const patch = [
132+" *** Begin Patch",
133+" *** Add File: src/new.ts",
134+"+x",
135+" *** Delete File: src/dead.ts",
136+" *** Update File: src/old.ts",
137+" *** Move to: src/renamed.ts",
138+"@@",
139+"-old",
140+"+new",
141+" *** End Patch",
142+].join("\n");
143+expect(extractApplyPatchTargetPaths(patch)).toEqual([
144+cwdPath("src/new.ts"),
145+cwdPath("src/dead.ts"),
146+cwdPath("src/old.ts"),
147+cwdPath("src/renamed.ts"),
148+]);
149+});
150+151+it("matches single-space-indented top-level headers the same way as the executor", () => {
152+const patch = [
153+"*** Begin Patch",
154+" *** Add File: src/new.ts",
155+"+x",
156+" *** Delete File: src/dead.ts",
157+"*** End Patch",
158+].join("\n");
159+expect(extractApplyPatchTargetPaths(patch)).toEqual([
160+cwdPath("src/new.ts"),
161+cwdPath("src/dead.ts"),
162+]);
163+});
164+165+it("finds top-level markers after an update hunk", () => {
166+const patch = [
167+"*** Begin Patch",
168+"*** Update File: src/old.ts",
169+"@@",
170+"-old",
171+"+new",
172+"*** Delete File: src/dead.ts",
173+"*** End Patch",
174+].join("\n");
175+expect(extractApplyPatchTargetPaths(patch)).toEqual([
176+cwdPath("src/old.ts"),
177+cwdPath("src/dead.ts"),
178+]);
179+});
180+181+it("ignores markers outside of the envelope grammar", () => {
182+expect(
183+extractApplyPatchTargetPaths(
184+["nothing here", "*** Random Marker: x", "+a", "context"].join("\n"),
185+),
186+).toEqual([]);
187+});
188+189+it("ignores marker-like context and body lines inside update hunks", () => {
190+const patch = [
191+"*** Begin Patch",
192+"*** Update File: real.ts",
193+"@@",
194+" *** Add File: fake-context.ts",
195+" *** Delete File: fake-indented-context.ts",
196+"-*** Delete File: fake-remove.ts",
197+"+*** Add File: fake-add.ts",
198+"*** End Patch",
199+].join("\n");
200+expect(extractApplyPatchTargetPaths(patch)).toEqual([cwdPath("real.ts")]);
201+});
202+203+it("can resolve paths with the same cwd semantics as apply_patch execution", () => {
204+const cwd = path.join(os.tmpdir(), "openclaw-derived-paths");
205+const patch = [
206+"*** Begin Patch",
207+"*** Add File: @src/../resolved.ts",
208+"+x",
209+"*** Update File: ~/renamed-source.ts",
210+"*** Move to: /tmp/openclaw-target.ts",
211+"@@",
212+"+y",
213+"*** End Patch",
214+].join("\n");
215+expect(extractApplyPatchTargetPaths(patch, { cwd })).toEqual([
216+path.join(cwd, "resolved.ts"),
217+path.join(os.homedir(), "renamed-source.ts"),
218+path.join("/tmp", "openclaw-target.ts"),
219+]);
220+});
221+222+it("defaults missing cwd to apply_patch process cwd semantics", () => {
223+const patch = [
224+"*** Begin Patch",
225+"*** Add File: @src/../resolved.ts",
226+"+x",
227+"*** Update File: ~/source.ts",
228+"*** Move to: src/moved.ts",
229+"@@",
230+"+y",
231+"*** End Patch",
232+].join("\n");
233+expect(extractApplyPatchTargetPaths(patch)).toEqual([
234+cwdPath("resolved.ts"),
235+path.join(os.homedir(), "source.ts"),
236+cwdPath("src/moved.ts"),
237+]);
238+});
239+240+it("skips sandbox paths the bridge rejects", () => {
241+const patch = [
242+"*** Begin Patch",
243+"*** Add File: /workspace/src/ok.ts",
244+"+x",
245+"*** Add File: /outside.ts",
246+"+y",
247+"*** End Patch",
248+].join("\n");
249+expect(
250+extractApplyPatchTargetPaths(patch, {
251+cwd: "/workspace",
252+sandbox: {
253+root: "/workspace",
254+bridge: {
255+resolvePath: ({ filePath }: { filePath: string }) => {
256+if (filePath === "/outside.ts") {
257+throw new Error("Path escapes sandbox root");
258+}
259+return {
260+containerPath: filePath,
261+hostPath: filePath.replace("/workspace", "/host/workspace"),
262+relativePath: filePath.replace("/workspace/", ""),
263+};
264+},
265+} as never,
266+},
267+}),
268+).toEqual(["/host/workspace/src/ok.ts"]);
269+});
270+271+it("does not require the begin/end envelope markers to be present", () => {
272+const patch = ["*** Add File: loose.ts", "+x"].join("\n");
273+expect(extractApplyPatchTargetPaths(patch)).toEqual([cwdPath("loose.ts")]);
274+});
275+});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。