Aiden Cline 3 kuukautta sitten
vanhempi
sitoutus
2ed18ea1fe

+ 25 - 9
packages/opencode/src/tool/apply_patch.ts

@@ -28,11 +28,15 @@ export const ApplyPatchTool = Tool.define("apply_patch", {
       const parseResult = Patch.parsePatch(params.patchText)
       const parseResult = Patch.parsePatch(params.patchText)
       hunks = parseResult.hunks
       hunks = parseResult.hunks
     } catch (error) {
     } catch (error) {
-      throw new Error(`Failed to parse patch: ${error}`)
+      throw new Error(`apply_patch verification failed: ${error}`)
     }
     }
 
 
     if (hunks.length === 0) {
     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
     // Validate file paths and check permissions
@@ -54,7 +58,8 @@ export const ApplyPatchTool = Tool.define("apply_patch", {
         case "add":
         case "add":
           if (hunk.type === "add") {
           if (hunk.type === "add") {
             const oldContent = ""
             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)
             const diff = createTwoFilesPatch(filePath, filePath, oldContent, newContent)
 
 
             fileChanges.push({
             fileChanges.push({
@@ -72,7 +77,7 @@ export const ApplyPatchTool = Tool.define("apply_patch", {
           // Check if file exists for update
           // Check if file exists for update
           const stats = await fs.stat(filePath).catch(() => null)
           const stats = await fs.stat(filePath).catch(() => null)
           if (!stats || stats.isDirectory()) {
           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)
           // 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)
             const fileUpdate = Patch.deriveNewContentsFromChunks(filePath, hunk.chunks)
             newContent = fileUpdate.content
             newContent = fileUpdate.content
           } catch (error) {
           } 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)
           const diff = createTwoFilesPatch(filePath, filePath, oldContent, newContent)
@@ -107,7 +112,9 @@ export const ApplyPatchTool = Tool.define("apply_patch", {
         case "delete":
         case "delete":
           // Check if file exists for deletion
           // Check if file exists for deletion
           await FileTime.assert(ctx.sessionID, filePath)
           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, "")
           const deleteDiff = createTwoFilesPatch(filePath, filePath, contentToDelete, "")
 
 
           fileChanges.push({
           fileChanges.push({
@@ -186,15 +193,24 @@ export const ApplyPatchTool = Tool.define("apply_patch", {
     }
     }
 
 
     // Generate output summary
     // 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 {
     return {
       title: summary,
       title: summary,
       metadata: {
       metadata: {
         diff: totalDiff,
         diff: totalDiff,
       },
       },
-      output: `Patch applied successfully. ${summary}:\n${relativePaths.map((p) => `  ${p}`).join("\n")}`,
+      output: summary,
     }
     }
   },
   },
 })
 })

+ 22 - 8
packages/opencode/test/tool/apply_patch.test.ts

@@ -3,6 +3,7 @@ import path from "path"
 import * as fs from "fs/promises"
 import * as fs from "fs/promises"
 import { ApplyPatchTool } from "../../src/tool/apply_patch"
 import { ApplyPatchTool } from "../../src/tool/apply_patch"
 import { Instance } from "../../src/project/instance"
 import { Instance } from "../../src/project/instance"
+import { FileTime } from "../../src/file/time"
 import { tmpdir } from "../fixture/fixture"
 import { tmpdir } from "../fixture/fixture"
 
 
 const baseCtx = {
 const baseCtx = {
@@ -50,13 +51,13 @@ describe("tool.apply_patch freeform", () => {
 
 
   test("rejects invalid patch format", async () => {
   test("rejects invalid patch format", async () => {
     const { ctx } = makeCtx()
     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 () => {
   test("rejects empty patch", async () => {
     const { ctx } = makeCtx()
     const { ctx } = makeCtx()
     const emptyPatch = "*** Begin Patch\n*** End Patch"
     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 () => {
   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")
         const deletePath = path.join(fixture.path, "delete.txt")
         await fs.writeFile(modifyPath, "line1\nline2\n", "utf-8")
         await fs.writeFile(modifyPath, "line1\nline2\n", "utf-8")
         await fs.writeFile(deletePath, "obsolete\n", "utf-8")
         await fs.writeFile(deletePath, "obsolete\n", "utf-8")
+        FileTime.read(ctx.sessionID, modifyPath)
+        FileTime.read(ctx.sessionID, deletePath)
 
 
         const patchText =
         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"
           "*** 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)
         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)
         expect(calls.length).toBe(1)
 
 
         const added = await fs.readFile(path.join(fixture.path, "nested", "new.txt"), "utf-8")
         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 () => {
       fn: async () => {
         const target = path.join(fixture.path, "multi.txt")
         const target = path.join(fixture.path, "multi.txt")
         await fs.writeFile(target, "line1\nline2\nline3\nline4\n", "utf-8")
         await fs.writeFile(target, "line1\nline2\nline3\nline4\n", "utf-8")
+        FileTime.read(ctx.sessionID, target)
 
 
         const patchText =
         const patchText =
           "*** Begin Patch\n*** Update File: multi.txt\n@@\n-line2\n+changed2\n@@\n-line4\n+changed4\n*** End Patch"
           "*** 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 () => {
       fn: async () => {
         const target = path.join(fixture.path, "insert_only.txt")
         const target = path.join(fixture.path, "insert_only.txt")
         await fs.writeFile(target, "alpha\nomega\n", "utf-8")
         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"
         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 () => {
       fn: async () => {
         const target = path.join(fixture.path, "no_newline.txt")
         const target = path.join(fixture.path, "no_newline.txt")
         await fs.writeFile(target, "no newline at end", "utf-8")
         await fs.writeFile(target, "no newline at end", "utf-8")
+        FileTime.read(ctx.sessionID, target)
 
 
         const patchText =
         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"
           "*** 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")
         const original = path.join(fixture.path, "old", "name.txt")
         await fs.mkdir(path.dirname(original), { recursive: true })
         await fs.mkdir(path.dirname(original), { recursive: true })
         await fs.writeFile(original, "old content\n", "utf-8")
         await fs.writeFile(original, "old content\n", "utf-8")
+        FileTime.read(ctx.sessionID, original)
 
 
         const patchText =
         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"
           "*** 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.mkdir(path.dirname(destination), { recursive: true })
         await fs.writeFile(original, "from\n", "utf-8")
         await fs.writeFile(original, "from\n", "utf-8")
         await fs.writeFile(destination, "existing\n", "utf-8")
         await fs.writeFile(destination, "existing\n", "utf-8")
+        FileTime.read(ctx.sessionID, original)
 
 
         const patchText =
         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"
           "*** 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 () => {
       fn: async () => {
         const patchText = "*** Begin Patch\n*** Update File: missing.txt\n@@\n-nope\n+better\n*** End Patch"
         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 () => {
       fn: async () => {
         const patchText = "*** Begin Patch\n*** Frobnicate File: foo\n*** End Patch"
         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 () => {
       fn: async () => {
         const target = path.join(fixture.path, "modify.txt")
         const target = path.join(fixture.path, "modify.txt")
         await fs.writeFile(target, "line1\nline2\n", "utf-8")
         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"
         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")
         expect(await fs.readFile(target, "utf-8")).toBe("line1\nline2\n")
       },
       },
     })
     })
@@ -320,6 +331,7 @@ describe("tool.apply_patch freeform", () => {
       fn: async () => {
       fn: async () => {
         const target = path.join(fixture.path, "tail.txt")
         const target = path.join(fixture.path, "tail.txt")
         await fs.writeFile(target, "alpha\nlast\n", "utf-8")
         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"
         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 () => {
       fn: async () => {
         const target = path.join(fixture.path, "two_chunks.txt")
         const target = path.join(fixture.path, "two_chunks.txt")
         await fs.writeFile(target, "a\nb\nc\nd\n", "utf-8")
         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"
         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 () => {
       fn: async () => {
         const target = path.join(fixture.path, "multi_ctx.txt")
         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")
         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"
         const patchText = "*** Begin Patch\n*** Update File: multi_ctx.txt\n@@ fn b\n-x=10\n+x=11\n*** End Patch"