patch.test.ts 7.7 KB

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