bugs.test.ts 10.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310
  1. import { test, expect } from "bun:test"
  2. import { $ } from "bun"
  3. import { Snapshot } from "../../src/snapshot"
  4. import { Instance } from "../../src/project/instance"
  5. import path from "path"
  6. async function bootstrap() {
  7. const dir = await $`mktemp -d`.text().then((t) => t.trim())
  8. const unique = Math.random().toString(36).slice(2)
  9. const aContent = `A${unique}`
  10. const bContent = `B${unique}`
  11. await Bun.write(`${dir}/a.txt`, aContent)
  12. await Bun.write(`${dir}/b.txt`, bContent)
  13. await $`git init`.cwd(dir).quiet()
  14. await $`git add .`.cwd(dir).quiet()
  15. await $`git commit -m init`.cwd(dir).quiet()
  16. return {
  17. [Symbol.asyncDispose]: async () => {
  18. await $`rm -rf ${dir}`.quiet()
  19. },
  20. dir,
  21. aContent,
  22. bContent,
  23. }
  24. }
  25. test("BUG: revert fails with absolute paths outside worktree", async () => {
  26. await using tmp = await bootstrap()
  27. await Instance.provide(tmp.dir, async () => {
  28. const before = await Snapshot.track()
  29. expect(before).toBeTruthy()
  30. await Bun.write(`${tmp.dir}/new.txt`, "NEW")
  31. const patch = await Snapshot.patch(before!)
  32. // Bug: The revert function tries to checkout files using absolute paths
  33. // but git checkout expects relative paths from the worktree
  34. // This will fail when the file path contains the full absolute path
  35. await expect(Snapshot.revert([patch])).resolves.toBeUndefined()
  36. // The file should be deleted but won't be due to git checkout failure
  37. expect(await Bun.file(`${tmp.dir}/new.txt`).exists()).toBe(false)
  38. })
  39. })
  40. test("BUG: filenames with special git characters break operations", async () => {
  41. await using tmp = await bootstrap()
  42. await Instance.provide(tmp.dir, async () => {
  43. const before = await Snapshot.track()
  44. expect(before).toBeTruthy()
  45. // Create files with characters that need escaping in git
  46. const problematicFiles = [
  47. `${tmp.dir}/"quotes".txt`,
  48. `${tmp.dir}/'apostrophe'.txt`,
  49. `${tmp.dir}/file\nwith\nnewline.txt`,
  50. `${tmp.dir}/file\twith\ttab.txt`,
  51. `${tmp.dir}/file with $ dollar.txt`,
  52. `${tmp.dir}/file with \` backtick.txt`,
  53. ]
  54. for (const file of problematicFiles) {
  55. try {
  56. await Bun.write(file, "content")
  57. } catch (e) {
  58. // Some filenames might not be valid on the filesystem
  59. }
  60. }
  61. const patch = await Snapshot.patch(before!)
  62. // The patch should handle these special characters correctly
  63. // but git commands may fail or produce unexpected results
  64. for (const file of patch.files) {
  65. if (problematicFiles.some((pf) => file.includes(path.basename(pf)))) {
  66. // These files with special characters may not be handled correctly
  67. console.log("Found problematic file in patch:", file)
  68. }
  69. }
  70. // Reverting these files will likely fail
  71. await Snapshot.revert([patch])
  72. // Check if files were actually removed (they likely won't be)
  73. for (const file of problematicFiles) {
  74. try {
  75. const exists = await Bun.file(file).exists()
  76. if (exists) {
  77. console.log("File with special chars still exists after revert:", file)
  78. }
  79. } catch {}
  80. }
  81. })
  82. })
  83. test("BUG: race condition in concurrent track calls", async () => {
  84. await using tmp = await bootstrap()
  85. await Instance.provide(tmp.dir, async () => {
  86. // Create initial state
  87. await Bun.write(`${tmp.dir}/file1.txt`, "initial1")
  88. const hash1 = await Snapshot.track()
  89. // Start multiple concurrent modifications and tracks
  90. const promises = []
  91. for (let i = 0; i < 10; i++) {
  92. promises.push(
  93. (async () => {
  94. await Bun.write(`${tmp.dir}/file${i}.txt`, `content${i}`)
  95. const hash = await Snapshot.track()
  96. return hash
  97. })(),
  98. )
  99. }
  100. const hashes = await Promise.all(promises)
  101. // Bug: Multiple concurrent track() calls may interfere with each other
  102. // because they all run `git add .` and `git write-tree` without locking
  103. // This can lead to inconsistent state
  104. // All hashes should be different (since files are different)
  105. // but due to race conditions, some might be the same
  106. const uniqueHashes = new Set(hashes)
  107. console.log(`Got ${uniqueHashes.size} unique hashes out of ${hashes.length} operations`)
  108. // This assertion might fail due to race conditions
  109. expect(uniqueHashes.size).toBe(hashes.length)
  110. })
  111. })
  112. test("BUG: restore doesn't handle modified files correctly", async () => {
  113. await using tmp = await bootstrap()
  114. await Instance.provide(tmp.dir, async () => {
  115. const before = await Snapshot.track()
  116. expect(before).toBeTruthy()
  117. // Modify existing file
  118. await Bun.write(`${tmp.dir}/a.txt`, "MODIFIED")
  119. // Add new file
  120. await Bun.write(`${tmp.dir}/new.txt`, "NEW")
  121. // Delete existing file
  122. await $`rm ${tmp.dir}/b.txt`.quiet()
  123. // Restore to original state
  124. await Snapshot.restore(before!)
  125. // Check restoration
  126. expect(await Bun.file(`${tmp.dir}/a.txt`).text()).toBe(tmp.aContent)
  127. expect(await Bun.file(`${tmp.dir}/b.txt`).text()).toBe(tmp.bContent)
  128. // Bug: restore uses checkout-index -a which only restores tracked files
  129. // It doesn't remove untracked files that were added after the snapshot
  130. expect(await Bun.file(`${tmp.dir}/new.txt`).exists()).toBe(false) // This will fail
  131. })
  132. })
  133. test("BUG: patch with spaces in filenames not properly escaped", async () => {
  134. await using tmp = await bootstrap()
  135. await Instance.provide(tmp.dir, async () => {
  136. const before = await Snapshot.track()
  137. expect(before).toBeTruthy()
  138. // Create file with spaces
  139. const fileWithSpaces = `${tmp.dir}/file with many spaces.txt`
  140. await Bun.write(fileWithSpaces, "content")
  141. const patch = await Snapshot.patch(before!)
  142. expect(patch.files).toContain(fileWithSpaces)
  143. // Try to revert - this might fail due to improper escaping
  144. await Snapshot.revert([patch])
  145. // File should be removed but might not be due to escaping issues
  146. expect(await Bun.file(fileWithSpaces).exists()).toBe(false)
  147. })
  148. })
  149. test("BUG: init() recursive directory removal uses wrong method", async () => {
  150. // The init() function uses fs.rmdir() which is deprecated
  151. // and might not work correctly on all systems
  152. // It should use fs.rm() with recursive: true instead
  153. // This is more of a code quality issue than a functional bug
  154. // but could fail on certain node versions or systems
  155. })
  156. test("BUG: diff and patch don't handle binary files correctly", async () => {
  157. await using tmp = await bootstrap()
  158. await Instance.provide(tmp.dir, async () => {
  159. const before = await Snapshot.track()
  160. expect(before).toBeTruthy()
  161. // Create a binary file
  162. const binaryData = Buffer.from([
  163. 0x89,
  164. 0x50,
  165. 0x4e,
  166. 0x47,
  167. 0x0d,
  168. 0x0a,
  169. 0x1a,
  170. 0x0a, // PNG header
  171. 0x00,
  172. 0x00,
  173. 0x00,
  174. 0x0d,
  175. 0x49,
  176. 0x48,
  177. 0x44,
  178. 0x52,
  179. ])
  180. await Bun.write(`${tmp.dir}/image.png`, binaryData)
  181. // diff() returns text which won't handle binary files correctly
  182. const diff = await Snapshot.diff(before!)
  183. // Binary files should be indicated differently in diff
  184. // but the current implementation just returns text()
  185. console.log("Diff output for binary file:", diff)
  186. // The diff might contain binary data as text, which could cause issues
  187. expect(diff).toContain("image.png")
  188. })
  189. })
  190. test("BUG: revert with relative path from different cwd fails", async () => {
  191. await using tmp = await bootstrap()
  192. await Instance.provide(tmp.dir, async () => {
  193. const before = await Snapshot.track()
  194. expect(before).toBeTruthy()
  195. await $`mkdir -p ${tmp.dir}/subdir`.quiet()
  196. await Bun.write(`${tmp.dir}/subdir/file.txt`, "content")
  197. const patch = await Snapshot.patch(before!)
  198. // Change cwd to a different directory
  199. const originalCwd = process.cwd()
  200. process.chdir(tmp.dir)
  201. try {
  202. // The revert function uses Instance.worktree as cwd for git checkout
  203. // but the file paths in the patch are absolute
  204. // This mismatch can cause issues
  205. await Snapshot.revert([patch])
  206. expect(await Bun.file(`${tmp.dir}/subdir/file.txt`).exists()).toBe(false)
  207. } finally {
  208. process.chdir(originalCwd)
  209. }
  210. })
  211. })
  212. test("BUG: track without git init in Instance.worktree creates orphaned git dir", async () => {
  213. // Create a directory without git initialization
  214. const dir = await $`mktemp -d`.text().then((t) => t.trim())
  215. try {
  216. await Instance.provide(dir, async () => {
  217. // Track will create a git directory in Global.Path.data
  218. // but if the worktree doesn't have git, operations might fail
  219. const hash = await Snapshot.track()
  220. // This might return a hash even though the worktree isn't properly tracked
  221. console.log("Hash from non-git directory:", hash)
  222. if (hash) {
  223. // Try to use the hash - this might fail or produce unexpected results
  224. const patch = await Snapshot.patch(hash)
  225. console.log("Patch from non-git directory:", patch)
  226. }
  227. })
  228. } finally {
  229. await $`rm -rf ${dir}`.quiet()
  230. }
  231. })
  232. test("BUG: patch doesn't handle deleted files in snapshot correctly", async () => {
  233. await using tmp = await bootstrap()
  234. await Instance.provide(tmp.dir, async () => {
  235. // Track initial state
  236. const before = await Snapshot.track()
  237. expect(before).toBeTruthy()
  238. // Delete a file
  239. await $`rm ${tmp.dir}/a.txt`.quiet()
  240. // Track after deletion
  241. const after = await Snapshot.track()
  242. expect(after).toBeTruthy()
  243. // Now create a new file
  244. await Bun.write(`${tmp.dir}/new.txt`, "NEW")
  245. // Get patch from the state where a.txt was deleted
  246. // This should show that new.txt was added and a.txt is still missing
  247. const patch = await Snapshot.patch(after!)
  248. // But the patch might incorrectly include a.txt as a changed file
  249. // because git diff compares against the snapshot tree, not working directory
  250. console.log("Patch files:", patch.files)
  251. // The patch should only contain new.txt
  252. expect(patch.files).toContain(`${tmp.dir}/new.txt`)
  253. expect(patch.files).not.toContain(`${tmp.dir}/a.txt`)
  254. })
  255. })