From 57899ee7b784e070dca1e5c37281b2610ed1354e Mon Sep 17 00:00:00 2001 From: kevin9327 <5299031+kevin9327@users.noreply.github.com> Date: Sun, 13 Sep 2026 19:02:48 +0900 Subject: [PATCH] fix(runtime): keep apply_patch insertion-only hunks at the end of the file An insertion-only hunk records the original end of the file as its position, while later hunks can change how many lines precede it. Replacements were applied in patch order reversed rather than by position, so when such a hunk came before an edit that added or removed lines, its lines were written into the middle of the file and the tool still reported success. Sort replacements by position before applying them bottom-up, as Codex does. Co-Authored-By: Claude Opus 5 --- .../runtime/src/tools/built-in/apply-patch.ts | 3 +++ .../tests/tools/built-in/apply-patch.test.ts | 19 +++++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/packages/runtime/src/tools/built-in/apply-patch.ts b/packages/runtime/src/tools/built-in/apply-patch.ts index df22d5cd..70d47e94 100644 --- a/packages/runtime/src/tools/built-in/apply-patch.ts +++ b/packages/runtime/src/tools/built-in/apply-patch.ts @@ -258,6 +258,9 @@ function _applyHunks( lineIndex = position + hunk.oldLines.length; } + // Insertion-only hunks target the end of the file regardless of their order + // in the patch, so apply every replacement from the bottom up by position. + replacements.sort((left, right) => left[0] - right[0]); for (const [position, oldLength, newLines] of replacements.reverse()) { lines.splice(position, oldLength, ...newLines); } diff --git a/packages/runtime/tests/tools/built-in/apply-patch.test.ts b/packages/runtime/tests/tools/built-in/apply-patch.test.ts index 42bbf641..ac39a975 100644 --- a/packages/runtime/tests/tools/built-in/apply-patch.test.ts +++ b/packages/runtime/tests/tools/built-in/apply-patch.test.ts @@ -163,6 +163,25 @@ describe("apply_patch built-in", () => { expect(await fs.readFile(filePath, "utf8")).toBe("first\nsecond\n"); }); + test("keeps insertion-only hunks at the end when a later hunk grows the file", async () => { + const workspace = await createWorkspace(); + const filePath = path.join(workspace, "example.txt"); + const original = "alpha\nbeta\ngamma\ndelta\n"; + const expected = "alpha\nbeta one\nbeta two\ngamma\ndelta\nomega\n"; + const insertion = "@@\n+omega"; + const edit = "@@\n alpha\n-beta\n+beta one\n+beta two\n gamma"; + const patch = (...hunks: string[]) => + `*** Begin Patch\n*** Update File: example.txt\n${hunks.join("\n")}\n*** End Patch`; + + await fs.writeFile(filePath, original, "utf8"); + await applyPatch(patch(edit, insertion), workspace); + expect(await fs.readFile(filePath, "utf8")).toBe(expected); + + await fs.writeFile(filePath, original, "utf8"); + await applyPatch(patch(insertion, edit), workspace); + expect(await fs.readFile(filePath, "utf8")).toBe(expected); + }); + test("can remove the complete contents of a file", async () => { const workspace = await createWorkspace(); const filePath = path.join(workspace, "example.txt");