From 2ed18ea1fe117dca548794b49cd75680213d1ee2 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Sat, 17 Jan 2026 20:48:09 -0600 Subject: [PATCH] wip --- packages/opencode/src/tool/apply_patch.ts | 34 ++++++++++++++----- .../opencode/test/tool/apply_patch.test.ts | 30 +++++++++++----- 2 files changed, 47 insertions(+), 17 deletions(-) diff --git a/packages/opencode/src/tool/apply_patch.ts b/packages/opencode/src/tool/apply_patch.ts index 5043e01472..9df0c099f5 100644 --- a/packages/opencode/src/tool/apply_patch.ts +++ b/packages/opencode/src/tool/apply_patch.ts @@ -28,11 +28,15 @@ export const ApplyPatchTool = Tool.define("apply_patch", { const parseResult = Patch.parsePatch(params.patchText) hunks = parseResult.hunks } catch (error) { - throw new Error(`Failed to parse patch: ${error}`) + throw new Error(`apply_patch verification failed: ${error}`) } if (hunks.length === 0) { - throw new Error("No file changes found in patch") + const normalized = params.patchText.replace(/\r\n/g, "\n").replace(/\r/g, "\n").trim() + if (normalized === "*** Begin Patch\n*** End Patch") { + throw new Error("patch rejected: empty patch") + } + throw new Error("apply_patch verification failed: no hunks found") } // Validate file paths and check permissions @@ -54,7 +58,8 @@ export const ApplyPatchTool = Tool.define("apply_patch", { case "add": if (hunk.type === "add") { const oldContent = "" - const newContent = hunk.contents + const newContent = + hunk.contents.length === 0 || hunk.contents.endsWith("\n") ? hunk.contents : `${hunk.contents}\n` const diff = createTwoFilesPatch(filePath, filePath, oldContent, newContent) fileChanges.push({ @@ -72,7 +77,7 @@ export const ApplyPatchTool = Tool.define("apply_patch", { // Check if file exists for update const stats = await fs.stat(filePath).catch(() => null) if (!stats || stats.isDirectory()) { - throw new Error(`File not found or is directory: ${filePath}`) + throw new Error(`apply_patch verification failed: Failed to read file to update: ${filePath}`) } // Read file and update time tracking (like edit tool does) @@ -85,7 +90,7 @@ export const ApplyPatchTool = Tool.define("apply_patch", { const fileUpdate = Patch.deriveNewContentsFromChunks(filePath, hunk.chunks) newContent = fileUpdate.content } catch (error) { - throw new Error(`Failed to apply update to ${filePath}: ${error}`) + throw new Error(`apply_patch verification failed: ${error}`) } const diff = createTwoFilesPatch(filePath, filePath, oldContent, newContent) @@ -107,7 +112,9 @@ export const ApplyPatchTool = Tool.define("apply_patch", { case "delete": // Check if file exists for deletion await FileTime.assert(ctx.sessionID, filePath) - const contentToDelete = await fs.readFile(filePath, "utf-8") + const contentToDelete = await fs.readFile(filePath, "utf-8").catch((error) => { + throw new Error(`apply_patch verification failed: ${error}`) + }) const deleteDiff = createTwoFilesPatch(filePath, filePath, contentToDelete, "") fileChanges.push({ @@ -186,15 +193,24 @@ export const ApplyPatchTool = Tool.define("apply_patch", { } // Generate output summary - const relativePaths = changedFiles.map((filePath) => path.relative(Instance.worktree, filePath)) - const summary = `${fileChanges.length} files changed` + const summaryLines = fileChanges.map((change) => { + if (change.type === "add") { + return `A ${path.relative(Instance.worktree, change.filePath)}` + } + if (change.type === "delete") { + return `D ${path.relative(Instance.worktree, change.filePath)}` + } + const target = change.movePath ?? change.filePath + return `M ${path.relative(Instance.worktree, target)}` + }) + const summary = `Success. Updated the following files:\n${summaryLines.join("\n")}` return { title: summary, metadata: { diff: totalDiff, }, - output: `Patch applied successfully. ${summary}:\n${relativePaths.map((p) => ` ${p}`).join("\n")}`, + output: summary, } }, }) diff --git a/packages/opencode/test/tool/apply_patch.test.ts b/packages/opencode/test/tool/apply_patch.test.ts index d65cb1479f..631d28d9e0 100644 --- a/packages/opencode/test/tool/apply_patch.test.ts +++ b/packages/opencode/test/tool/apply_patch.test.ts @@ -3,6 +3,7 @@ import path from "path" import * as fs from "fs/promises" import { ApplyPatchTool } from "../../src/tool/apply_patch" import { Instance } from "../../src/project/instance" +import { FileTime } from "../../src/file/time" import { tmpdir } from "../fixture/fixture" const baseCtx = { @@ -50,13 +51,13 @@ describe("tool.apply_patch freeform", () => { test("rejects invalid patch format", async () => { const { ctx } = makeCtx() - await expect(execute({ patchText: "invalid patch" }, ctx)).rejects.toThrow("Failed to parse patch") + await expect(execute({ patchText: "invalid patch" }, ctx)).rejects.toThrow("apply_patch verification failed") }) test("rejects empty patch", async () => { const { ctx } = makeCtx() const emptyPatch = "*** Begin Patch\n*** End Patch" - await expect(execute({ patchText: emptyPatch }, ctx)).rejects.toThrow("No file changes found in patch") + await expect(execute({ patchText: emptyPatch }, ctx)).rejects.toThrow("patch rejected: empty patch") }) test("applies add/update/delete in one patch", async () => { @@ -70,15 +71,17 @@ describe("tool.apply_patch freeform", () => { const deletePath = path.join(fixture.path, "delete.txt") await fs.writeFile(modifyPath, "line1\nline2\n", "utf-8") await fs.writeFile(deletePath, "obsolete\n", "utf-8") + FileTime.read(ctx.sessionID, modifyPath) + FileTime.read(ctx.sessionID, deletePath) const patchText = "*** Begin Patch\n*** Add File: nested/new.txt\n+created\n*** Delete File: delete.txt\n*** Update File: modify.txt\n@@\n-line2\n+changed\n*** End Patch" const result = await execute({ patchText }, ctx) - expect(result.title).toContain("files changed") - expect(result.output).toContain("Patch applied successfully") - expect(result.metadata.diff).toContain("diff") + expect(result.title).toContain("Success. Updated the following files") + expect(result.output).toContain("Success. Updated the following files") + expect(result.metadata.diff).toContain("Index:") expect(calls.length).toBe(1) const added = await fs.readFile(path.join(fixture.path, "nested", "new.txt"), "utf-8") @@ -98,6 +101,7 @@ describe("tool.apply_patch freeform", () => { fn: async () => { const target = path.join(fixture.path, "multi.txt") await fs.writeFile(target, "line1\nline2\nline3\nline4\n", "utf-8") + FileTime.read(ctx.sessionID, target) const patchText = "*** Begin Patch\n*** Update File: multi.txt\n@@\n-line2\n+changed2\n@@\n-line4\n+changed4\n*** End Patch" @@ -118,6 +122,7 @@ describe("tool.apply_patch freeform", () => { fn: async () => { const target = path.join(fixture.path, "insert_only.txt") await fs.writeFile(target, "alpha\nomega\n", "utf-8") + FileTime.read(ctx.sessionID, target) const patchText = "*** Begin Patch\n*** Update File: insert_only.txt\n@@\n alpha\n+beta\n omega\n*** End Patch" @@ -137,6 +142,7 @@ describe("tool.apply_patch freeform", () => { fn: async () => { const target = path.join(fixture.path, "no_newline.txt") await fs.writeFile(target, "no newline at end", "utf-8") + FileTime.read(ctx.sessionID, target) const patchText = "*** Begin Patch\n*** Update File: no_newline.txt\n@@\n-no newline at end\n+first line\n+second line\n*** End Patch" @@ -160,6 +166,7 @@ describe("tool.apply_patch freeform", () => { const original = path.join(fixture.path, "old", "name.txt") await fs.mkdir(path.dirname(original), { recursive: true }) await fs.writeFile(original, "old content\n", "utf-8") + FileTime.read(ctx.sessionID, original) const patchText = "*** Begin Patch\n*** Update File: old/name.txt\n*** Move to: renamed/dir/name.txt\n@@\n-old content\n+new content\n*** End Patch" @@ -186,6 +193,7 @@ describe("tool.apply_patch freeform", () => { await fs.mkdir(path.dirname(destination), { recursive: true }) await fs.writeFile(original, "from\n", "utf-8") await fs.writeFile(destination, "existing\n", "utf-8") + FileTime.read(ctx.sessionID, original) const patchText = "*** Begin Patch\n*** Update File: old/name.txt\n*** Move to: renamed/dir/name.txt\n@@\n-from\n+new\n*** End Patch" @@ -225,7 +233,9 @@ describe("tool.apply_patch freeform", () => { fn: async () => { const patchText = "*** Begin Patch\n*** Update File: missing.txt\n@@\n-nope\n+better\n*** End Patch" - await expect(execute({ patchText }, ctx)).rejects.toThrow("File not found or is directory") + await expect(execute({ patchText }, ctx)).rejects.toThrow( + "apply_patch verification failed: Failed to read file to update", + ) }, }) }) @@ -270,7 +280,7 @@ describe("tool.apply_patch freeform", () => { fn: async () => { const patchText = "*** Begin Patch\n*** Frobnicate File: foo\n*** End Patch" - await expect(execute({ patchText }, ctx)).rejects.toThrow("Failed to parse patch") + await expect(execute({ patchText }, ctx)).rejects.toThrow("apply_patch verification failed") }, }) }) @@ -284,10 +294,11 @@ describe("tool.apply_patch freeform", () => { fn: async () => { const target = path.join(fixture.path, "modify.txt") await fs.writeFile(target, "line1\nline2\n", "utf-8") + FileTime.read(ctx.sessionID, target) const patchText = "*** Begin Patch\n*** Update File: modify.txt\n@@\n-missing\n+changed\n*** End Patch" - await expect(execute({ patchText }, ctx)).rejects.toThrow("Failed to apply update") + await expect(execute({ patchText }, ctx)).rejects.toThrow("apply_patch verification failed") expect(await fs.readFile(target, "utf-8")).toBe("line1\nline2\n") }, }) @@ -320,6 +331,7 @@ describe("tool.apply_patch freeform", () => { fn: async () => { const target = path.join(fixture.path, "tail.txt") await fs.writeFile(target, "alpha\nlast\n", "utf-8") + FileTime.read(ctx.sessionID, target) const patchText = "*** Begin Patch\n*** Update File: tail.txt\n@@\n-last\n+end\n*** End of File\n*** End Patch" @@ -338,6 +350,7 @@ describe("tool.apply_patch freeform", () => { fn: async () => { const target = path.join(fixture.path, "two_chunks.txt") await fs.writeFile(target, "a\nb\nc\nd\n", "utf-8") + FileTime.read(ctx.sessionID, target) const patchText = "*** Begin Patch\n*** Update File: two_chunks.txt\n@@\n-b\n+B\n\n-d\n+D\n*** End Patch" @@ -356,6 +369,7 @@ describe("tool.apply_patch freeform", () => { fn: async () => { const target = path.join(fixture.path, "multi_ctx.txt") await fs.writeFile(target, "fn a\nx=10\ny=2\nfn b\nx=10\ny=20\n", "utf-8") + FileTime.read(ctx.sessionID, target) const patchText = "*** Begin Patch\n*** Update File: multi_ctx.txt\n@@ fn b\n-x=10\n+x=11\n*** End Patch"