




















@@ -11,7 +11,7 @@ import {
1111createRebindableDirectoryAlias,
1212withRealpathSymlinkRebindRace,
1313} from "../test-utils/symlink-rebind-race.js";
14-import { applyPatch } from "./apply-patch.js";
14+import { applyPatch, createApplyPatchTool } from "./apply-patch.js";
1515import type { SandboxFsBridge } from "./sandbox/fs-bridge.js";
16161717async function withTempDir<T>(fn: (dir: string) => Promise<T>) {
@@ -43,15 +43,16 @@ function createMemoryPatchSandbox(initialFiles: Record<string, string> = {}) {
4343const files = new Map<string, string>(
4444Object.entries(initialFiles).map(([filePath, contents]) => [`/sandbox/${filePath}`, contents]),
4545);
46+const writeFile = vi.fn(async ({ filePath, data }) => {
47+files.set(filePath, Buffer.isBuffer(data) ? data.toString("utf8") : data);
48+});
4649const bridge: SandboxFsBridge = {
4750resolvePath: ({ filePath }) => ({
4851relativePath: filePath,
4952containerPath: `/sandbox/${filePath}`,
5053}),
5154readFile: async ({ filePath }) => Buffer.from(files.get(filePath) ?? "", "utf8"),
52-writeFile: async ({ filePath, data }) => {
53-files.set(filePath, Buffer.isBuffer(data) ? data.toString("utf8") : data);
54-},
55+ writeFile,
5556remove: async ({ filePath }) => {
5657files.delete(filePath);
5758},
@@ -72,6 +73,8 @@ function createMemoryPatchSandbox(initialFiles: Record<string, string> = {}) {
7273};
7374return {
7475 files,
76+ bridge,
77+ writeFile,
7578options: {
7679cwd: "/local/workspace",
7780sandbox: {
@@ -155,6 +158,85 @@ describe("applyPatch", () => {
155158expect(result.summary.modified).toEqual(["source.txt"]);
156159});
157160161+it("returns a terminal no-op without rewriting unchanged update hunks", async () => {
162+const memory = createMemoryPatchSandbox({
163+"source.txt": "foo\nbar\n",
164+});
165+const patch = `*** Begin Patch
166+*** Update File: source.txt
167+@@
168+ foo
169+-bar
170++bar
171+*** End Patch`;
172+173+const result = await applyPatch(patch, memory.options);
174+175+expect(result.noOp).toBe(true);
176+expect(result.text).toBe("No changes made to source.txt.");
177+expect(result.summary).toEqual({ added: [], modified: [], deleted: [] });
178+expect(memory.files.get("/sandbox/source.txt")).toBe("foo\nbar\n");
179+expect(memory.writeFile.mock.calls).toHaveLength(0);
180+181+const tool = createApplyPatchTool(memory.options);
182+const toolResult = await tool.execute("call-no-op", { input: patch }, undefined);
183+expect(toolResult.terminate).toBe(true);
184+});
185+186+it("preserves line endings and EOF state for no-op update hunks", async () => {
187+const patch = `*** Begin Patch
188+*** Update File: source.txt
189+@@
190+ foo
191+-bar
192++bar
193+*** End Patch`;
194+for (const initial of ["foo\r\nbar\r\n", "foo\nbar"]) {
195+const memory = createMemoryPatchSandbox({ "source.txt": initial });
196+197+const result = await applyPatch(patch, memory.options);
198+199+expect(result.noOp).toBe(true);
200+expect(memory.files.get("/sandbox/source.txt")).toBe(initial);
201+expect(memory.writeFile.mock.calls).toHaveLength(0);
202+}
203+});
204+205+it("applies a real deletion of the sole blank line", async () => {
206+const memory = createMemoryPatchSandbox({ "source.txt": "\n" });
207+const patch = `*** Begin Patch
208+*** Update File: source.txt
209+@@
210+-
211+*** End Patch`;
212+213+const result = await applyPatch(patch, memory.options);
214+215+expect(result.noOp).toBeUndefined();
216+expect(memory.files.get("/sandbox/source.txt")).toBe("");
217+expect(memory.writeFile.mock.calls).toHaveLength(1);
218+});
219+220+it("preserves formatting for same-path move no-op hunks", async () => {
221+const patch = `*** Begin Patch
222+*** Update File: source.txt
223+*** Move to: ./source.txt
224+@@
225+ foo
226+-bar
227++bar
228+*** End Patch`;
229+for (const initial of ["foo\r\nbar\r\n", "foo\nbar"]) {
230+const memory = createMemoryPatchSandbox({ "source.txt": initial });
231+232+const result = await applyPatch(patch, memory.options);
233+234+expect(result.noOp).toBe(true);
235+expect(memory.files.get("/sandbox/source.txt")).toBe(initial);
236+expect(memory.writeFile.mock.calls).toHaveLength(0);
237+}
238+});
239+158240it("applies context-only insertions at the requested context", async () => {
159241const memory = createMemoryPatchSandbox({
160242"source.txt": "alpha\nanchor\nomega\n",
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。