patch.test.ts 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266
  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(
  22. "patchText is required",
  23. )
  24. },
  25. })
  26. })
  27. test("should validate patch format", async () => {
  28. await Instance.provide({
  29. directory: "/tmp",
  30. fn: async () => {
  31. await expect(patchTool.execute({ patchText: "invalid patch" }, ctx)).rejects.toThrow(
  32. "Failed to parse patch",
  33. )
  34. },
  35. })
  36. })
  37. test("should handle empty patch", async () => {
  38. await Instance.provide({
  39. directory: "/tmp",
  40. fn: async () => {
  41. const emptyPatch = `*** Begin Patch
  42. *** End Patch`
  43. await expect(patchTool.execute({ patchText: emptyPatch }, ctx)).rejects.toThrow(
  44. "No file changes found in patch",
  45. )
  46. },
  47. })
  48. })
  49. test("should reject files outside working directory", async () => {
  50. await Instance.provide({
  51. directory: "/tmp",
  52. fn: async () => {
  53. const maliciousPatch = `*** Begin Patch
  54. *** Add File: /etc/passwd
  55. +malicious content
  56. *** End Patch`
  57. await expect(patchTool.execute({ patchText: maliciousPatch }, ctx)).rejects.toThrow(
  58. "is not in the current working directory",
  59. )
  60. },
  61. })
  62. })
  63. test("should handle simple add file operation", async () => {
  64. await using fixture = await tmpdir()
  65. await Instance.provide({
  66. directory: fixture.path,
  67. fn: async () => {
  68. const patchText = `*** Begin Patch
  69. *** Add File: test-file.txt
  70. +Hello World
  71. +This is a test file
  72. *** End Patch`
  73. const result = await patchTool.execute({ patchText }, ctx)
  74. expect(result.title).toContain("files changed")
  75. expect(result.metadata.diff).toBeDefined()
  76. expect(result.output).toContain("Patch applied successfully")
  77. // Verify file was created
  78. const filePath = path.join(fixture.path, "test-file.txt")
  79. const content = await fs.readFile(filePath, "utf-8")
  80. expect(content).toBe("Hello World\nThis is a test file")
  81. },
  82. })
  83. })
  84. test("should handle file with context update", async () => {
  85. await using fixture = await tmpdir()
  86. await Instance.provide({
  87. directory: fixture.path,
  88. fn: async () => {
  89. const patchText = `*** Begin Patch
  90. *** Add File: config.js
  91. +const API_KEY = "test-key"
  92. +const DEBUG = false
  93. +const VERSION = "1.0"
  94. *** End Patch`
  95. const result = await patchTool.execute({ patchText }, ctx)
  96. expect(result.title).toContain("files changed")
  97. expect(result.metadata.diff).toBeDefined()
  98. expect(result.output).toContain("Patch applied successfully")
  99. // Verify file was created with correct content
  100. const filePath = path.join(fixture.path, "config.js")
  101. const content = await fs.readFile(filePath, "utf-8")
  102. expect(content).toBe(
  103. 'const API_KEY = "test-key"\nconst DEBUG = false\nconst VERSION = "1.0"',
  104. )
  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
  149. .access(nestedPath)
  150. .then(() => true)
  151. .catch(() => false)
  152. expect(exists).toBe(true)
  153. const content = await fs.readFile(nestedPath, "utf-8")
  154. expect(content).toBe("Deep nested content")
  155. },
  156. })
  157. })
  158. test("should generate proper unified diff in metadata", async () => {
  159. await using fixture = await tmpdir()
  160. await Instance.provide({
  161. directory: fixture.path,
  162. fn: async () => {
  163. // First create a file with simple content
  164. const patchText1 = `*** Begin Patch
  165. *** Add File: test.txt
  166. +line 1
  167. +line 2
  168. +line 3
  169. *** End Patch`
  170. await patchTool.execute({ patchText: patchText1 }, ctx)
  171. // Now create an update patch
  172. const patchText2 = `*** Begin Patch
  173. *** Update File: test.txt
  174. @@
  175. line 1
  176. -line 2
  177. +line 2 updated
  178. line 3
  179. *** End Patch`
  180. const result = await patchTool.execute({ patchText: patchText2 }, ctx)
  181. expect(result.metadata.diff).toBeDefined()
  182. expect(result.metadata.diff).toContain("@@")
  183. expect(result.metadata.diff).toContain("-line 2")
  184. expect(result.metadata.diff).toContain("+line 2 updated")
  185. },
  186. })
  187. })
  188. test("should handle complex patch with multiple operations", async () => {
  189. await using fixture = await tmpdir()
  190. await Instance.provide({
  191. directory: fixture.path,
  192. fn: async () => {
  193. const patchText = `*** Begin Patch
  194. *** Add File: new.txt
  195. +This is a new file
  196. +with multiple lines
  197. *** Add File: existing.txt
  198. +old content
  199. +new line
  200. +more content
  201. *** Add File: config.json
  202. +{
  203. + "version": "1.0",
  204. + "debug": true
  205. +}
  206. *** End Patch`
  207. const result = await patchTool.execute({ patchText }, ctx)
  208. expect(result.title).toContain("3 files changed")
  209. expect(result.metadata.diff).toBeDefined()
  210. expect(result.output).toContain("Patch applied successfully")
  211. // Verify all files were created
  212. const newPath = path.join(fixture.path, "new.txt")
  213. const newContent = await fs.readFile(newPath, "utf-8")
  214. expect(newContent).toBe("This is a new file\nwith multiple lines")
  215. const existingPath = path.join(fixture.path, "existing.txt")
  216. const existingContent = await fs.readFile(existingPath, "utf-8")
  217. expect(existingContent).toBe("old content\nnew line\nmore content")
  218. const configPath = path.join(fixture.path, "config.json")
  219. const configContent = await fs.readFile(configPath, "utf-8")
  220. expect(configContent).toBe('{\n "version": "1.0",\n "debug": true\n}')
  221. },
  222. })
  223. })
  224. })