From eb4ff91c2dca4faba4b328e4e47d2eabfa79b847 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Mon, 20 Jul 2026 23:36:48 -0500 Subject: [PATCH] fix(core): compose successive patch updates (#38034) --- packages/core/src/tool/patch.ts | 52 +++++++++++++++------------ packages/core/test/tool-patch.test.ts | 16 +++++++++ 2 files changed, 46 insertions(+), 22 deletions(-) diff --git a/packages/core/src/tool/patch.ts b/packages/core/src/tool/patch.ts index 02574f000a..0cd9200ef0 100644 --- a/packages/core/src/tool/patch.ts +++ b/packages/core/src/tool/patch.ts @@ -111,6 +111,7 @@ export const Plugin = { } const prepared: Prepared[] = [] const targets: Target[] = [] + const updates = new Map() for (const hunk of hunks) { yield* Effect.gen(function* () { const target = resolveTarget(location, hunk.path) @@ -154,28 +155,34 @@ export const Plugin = { prepared.push({ ...hunk, target, before: original.replace(/^\uFEFF/, ""), after: "" }) return } - const stats = yield* fs.stat(target.canonical).pipe( - Effect.mapError( - (error) => - new ToolFailure({ - message: `patch verification failed: Failed to read file to update ${target.canonical}: ${error instanceof Error ? error.message : String(error)}`, - }), - ), - ) - if (stats.type === "Directory") { - return yield* new ToolFailure({ - message: `patch verification failed: Failed to read file to update ${target.canonical}: path is a directory`, - }) - } - const content = yield* fs.readFile(target.canonical).pipe( - Effect.mapError( - (error) => - new ToolFailure({ - message: `patch verification failed: Failed to read file to update ${target.canonical}: ${error instanceof Error ? error.message : String(error)}`, - }), - ), - ) - const original = new TextDecoder("utf-8", { ignoreBOM: true }).decode(content) + const previous = updates.get(target.canonical) + const original = + previous ?? + (yield* Effect.gen(function* () { + const stats = yield* fs.stat(target.canonical).pipe( + Effect.mapError( + (error) => + new ToolFailure({ + message: `patch verification failed: Failed to read file to update ${target.canonical}: ${error instanceof Error ? error.message : String(error)}`, + }), + ), + ) + if (stats.type === "Directory") { + return yield* new ToolFailure({ + message: `patch verification failed: Failed to read file to update ${target.canonical}: path is a directory`, + }) + } + return new TextDecoder("utf-8", { ignoreBOM: true }).decode( + yield* fs.readFile(target.canonical).pipe( + Effect.mapError( + (error) => + new ToolFailure({ + message: `patch verification failed: Failed to read file to update ${target.canonical}: ${error instanceof Error ? error.message : String(error)}`, + }), + ), + ), + ) + })) const before = original.replace(/^\uFEFF/, "") const update = yield* Effect.try({ try: () => Patch.derive(hunk.path, hunk.chunks, original), @@ -205,6 +212,7 @@ export const Plugin = { after: update.content, moveTarget, }) + if (!moveTarget) updates.set(target.canonical, Patch.joinBom(update.content, update.bom)) }).pipe(Effect.mapError((error) => (error instanceof ToolFailure ? error : fail(hunk.path, error)))) } diff --git a/packages/core/test/tool-patch.test.ts b/packages/core/test/tool-patch.test.ts index bdb24bf3b5..2f93197140 100644 --- a/packages/core/test/tool-patch.test.ts +++ b/packages/core/test/tool-patch.test.ts @@ -487,6 +487,22 @@ describe("PatchTool", () => { ), ) + it.live("applies successive update operations to one file", () => + withTempTool((directory, registry) => + Effect.gen(function* () { + const target = path.join(directory, "successive.txt") + yield* Effect.promise(() => fs.writeFile(target, "a\nb\n")) + yield* executeTool( + registry, + call( + "*** Begin Patch\n*** Update File: successive.txt\n@@\n-a\n+A\n*** Update File: successive.txt\n@@\n-b\n+B\n*** End Patch", + ), + ) + expect(yield* Effect.promise(() => fs.readFile(target, "utf8"))).toBe("A\nB\n") + }), + ), + ) + it.live("does not invent a first-line diff for BOM files", () => withTempTool((directory, registry) => Effect.gen(function* () {