patch.test.ts 7.5 KB

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