patch.test.ts 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263
  1. import { describe, expect, test } from "bun:test"
  2. import path from "path"
  3. import { PatchTool } from "../../src/tool/patch"
  4. import { Log } from "../../src/util/log"
  5. import { Instance } from "../../src/project/instance"
  6. import { tmpdir } from "../fixture/fixture"
  7. import * as fs from "fs/promises"
  8. const ctx = {
  9. sessionID: "test",
  10. messageID: "",
  11. toolCallID: "",
  12. agent: "build",
  13. abort: AbortSignal.any([]),
  14. metadata: () => {},
  15. }
  16. const patchTool = await PatchTool.init()
  17. Log.init({ print: false })
  18. describe("tool.patch", () => {
  19. test("should validate required parameters", async () => {
  20. await Instance.provide({
  21. directory: "/tmp",
  22. fn: async () => {
  23. await expect(
  24. patchTool.execute({ patchText: "" }, ctx)
  25. ).rejects.toThrow("patchText is required")
  26. },
  27. })
  28. })
  29. test("should validate patch format", async () => {
  30. await Instance.provide({
  31. directory: "/tmp",
  32. fn: async () => {
  33. await expect(
  34. patchTool.execute({ patchText: "invalid patch" }, ctx)
  35. ).rejects.toThrow("Failed to parse patch")
  36. },
  37. })
  38. })
  39. test("should handle empty patch", async () => {
  40. await Instance.provide({
  41. directory: "/tmp",
  42. fn: async () => {
  43. const emptyPatch = `*** Begin Patch
  44. *** End Patch`
  45. await expect(
  46. patchTool.execute({ patchText: emptyPatch }, ctx)
  47. ).rejects.toThrow("No file changes found in patch")
  48. },
  49. })
  50. })
  51. test("should reject files outside working directory", async () => {
  52. await Instance.provide({
  53. directory: "/tmp",
  54. fn: async () => {
  55. const maliciousPatch = `*** Begin Patch
  56. *** Add File: /etc/passwd
  57. +malicious content
  58. *** End Patch`
  59. await expect(
  60. patchTool.execute({ patchText: maliciousPatch }, ctx)
  61. ).rejects.toThrow("is not in the current working directory")
  62. },
  63. })
  64. })
  65. test("should handle simple add file operation", async () => {
  66. await using fixture = await tmpdir()
  67. await Instance.provide({
  68. directory: fixture.path,
  69. fn: async () => {
  70. const patchText = `*** Begin Patch
  71. *** Add File: test-file.txt
  72. +Hello World
  73. +This is a test file
  74. *** End Patch`
  75. const result = await patchTool.execute({ patchText }, ctx)
  76. expect(result.title).toContain("files changed")
  77. expect(result.metadata.diff).toBeDefined()
  78. expect(result.output).toContain("Patch applied successfully")
  79. // Verify file was created
  80. const filePath = path.join(fixture.path, "test-file.txt")
  81. const content = await fs.readFile(filePath, "utf-8")
  82. expect(content).toBe("Hello World\nThis is a test file")
  83. },
  84. })
  85. })
  86. test("should handle file with context update", async () => {
  87. await using fixture = await tmpdir()
  88. await Instance.provide({
  89. directory: fixture.path,
  90. fn: async () => {
  91. const patchText = `*** Begin Patch
  92. *** Add File: config.js
  93. +const API_KEY = "test-key"
  94. +const DEBUG = false
  95. +const VERSION = "1.0"
  96. *** End Patch`
  97. const result = await patchTool.execute({ patchText }, ctx)
  98. expect(result.title).toContain("files changed")
  99. expect(result.metadata.diff).toBeDefined()
  100. expect(result.output).toContain("Patch applied successfully")
  101. // Verify file was created with correct content
  102. const filePath = path.join(fixture.path, "config.js")
  103. const content = await fs.readFile(filePath, "utf-8")
  104. expect(content).toBe("const API_KEY = \"test-key\"\nconst DEBUG = false\nconst VERSION = \"1.0\"")
  105. },
  106. })
  107. })
  108. test("should handle multiple file operations", async () => {
  109. await using fixture = await tmpdir()
  110. await Instance.provide({
  111. directory: fixture.path,
  112. fn: async () => {
  113. const patchText = `*** Begin Patch
  114. *** Add File: file1.txt
  115. +Content of file 1
  116. *** Add File: file2.txt
  117. +Content of file 2
  118. *** Add File: file3.txt
  119. +Content of file 3
  120. *** End Patch`
  121. const result = await patchTool.execute({ patchText }, ctx)
  122. expect(result.title).toContain("3 files changed")
  123. expect(result.metadata.diff).toBeDefined()
  124. expect(result.output).toContain("Patch applied successfully")
  125. // Verify all files were created
  126. for (let i = 1; i <= 3; i++) {
  127. const filePath = path.join(fixture.path, `file${i}.txt`)
  128. const content = await fs.readFile(filePath, "utf-8")
  129. expect(content).toBe(`Content of file ${i}`)
  130. }
  131. },
  132. })
  133. })
  134. test("should create parent directories when adding nested files", async () => {
  135. await using fixture = await tmpdir()
  136. await Instance.provide({
  137. directory: fixture.path,
  138. fn: async () => {
  139. const patchText = `*** Begin Patch
  140. *** Add File: deep/nested/file.txt
  141. +Deep nested content
  142. *** End Patch`
  143. const result = await patchTool.execute({ patchText }, ctx)
  144. expect(result.title).toContain("files changed")
  145. expect(result.output).toContain("Patch applied successfully")
  146. // Verify nested file was created
  147. const nestedPath = path.join(fixture.path, "deep", "nested", "file.txt")
  148. const exists = await fs.access(nestedPath).then(() => true).catch(() => false)
  149. expect(exists).toBe(true)
  150. const content = await fs.readFile(nestedPath, "utf-8")
  151. expect(content).toBe("Deep nested content")
  152. },
  153. })
  154. })
  155. test("should generate proper unified diff in metadata", async () => {
  156. await using fixture = await tmpdir()
  157. await Instance.provide({
  158. directory: fixture.path,
  159. fn: async () => {
  160. // First create a file with simple content
  161. const patchText1 = `*** Begin Patch
  162. *** Add File: test.txt
  163. +line 1
  164. +line 2
  165. +line 3
  166. *** End Patch`
  167. await patchTool.execute({ patchText: patchText1 }, ctx)
  168. // Now create an update patch
  169. const patchText2 = `*** Begin Patch
  170. *** Update File: test.txt
  171. @@
  172. line 1
  173. -line 2
  174. +line 2 updated
  175. line 3
  176. *** End Patch`
  177. const result = await patchTool.execute({ patchText: patchText2 }, ctx)
  178. expect(result.metadata.diff).toBeDefined()
  179. expect(result.metadata.diff).toContain("@@")
  180. expect(result.metadata.diff).toContain("-line 2")
  181. expect(result.metadata.diff).toContain("+line 2 updated")
  182. },
  183. })
  184. })
  185. test("should handle complex patch with multiple operations", async () => {
  186. await using fixture = await tmpdir()
  187. await Instance.provide({
  188. directory: fixture.path,
  189. fn: async () => {
  190. const patchText = `*** Begin Patch
  191. *** Add File: new.txt
  192. +This is a new file
  193. +with multiple lines
  194. *** Add File: existing.txt
  195. +old content
  196. +new line
  197. +more content
  198. *** Add File: config.json
  199. +{
  200. + "version": "1.0",
  201. + "debug": true
  202. +}
  203. *** End Patch`
  204. const result = await patchTool.execute({ patchText }, ctx)
  205. expect(result.title).toContain("3 files changed")
  206. expect(result.metadata.diff).toBeDefined()
  207. expect(result.output).toContain("Patch applied successfully")
  208. // Verify all files were created
  209. const newPath = path.join(fixture.path, "new.txt")
  210. const newContent = await fs.readFile(newPath, "utf-8")
  211. expect(newContent).toBe("This is a new file\nwith multiple lines")
  212. const existingPath = path.join(fixture.path, "existing.txt")
  213. const existingContent = await fs.readFile(existingPath, "utf-8")
  214. expect(existingContent).toBe("old content\nnew line\nmore content")
  215. const configPath = path.join(fixture.path, "config.json")
  216. const configContent = await fs.readFile(configPath, "utf-8")
  217. expect(configContent).toBe('{\n "version": "1.0",\n "debug": true\n}')
  218. },
  219. })
  220. })
  221. })