11import { test , expect } from "bun:test"
22import { $ } from "bun"
33import fs from "fs/promises"
4+ import path from "path"
45import { Snapshot } from "../../src/snapshot"
56import { Instance } from "../../src/project/instance"
67import { Filesystem } from "../../src/util/filesystem"
78import { tmpdir } from "../fixture/fixture"
89
10+ // Git always outputs /-separated paths internally. Snapshot.patch() joins them
11+ // with path.join (which produces \ on Windows) then normalizes back to /.
12+ // This helper does the same for expected values so assertions match cross-platform.
13+ const fwd = ( ...parts : string [ ] ) => path . join ( ...parts ) . replaceAll ( "\\" , "/" )
14+
915async function bootstrap ( ) {
1016 return tmpdir ( {
1117 git : true ,
@@ -35,7 +41,7 @@ test("tracks deleted files correctly", async () => {
3541
3642 await $ `rm ${ tmp . path } /a.txt` . quiet ( )
3743
38- expect ( ( await Snapshot . patch ( before ! ) ) . files ) . toContain ( ` ${ tmp . path } / a.txt` )
44+ expect ( ( await Snapshot . patch ( before ! ) ) . files ) . toContain ( fwd ( tmp . path , " a.txt" ) )
3945 } ,
4046 } )
4147} )
@@ -143,7 +149,7 @@ test("binary file handling", async () => {
143149 await Filesystem . write ( `${ tmp . path } /image.png` , new Uint8Array ( [ 0x89 , 0x50 , 0x4e , 0x47 ] ) )
144150
145151 const patch = await Snapshot . patch ( before ! )
146- expect ( patch . files ) . toContain ( ` ${ tmp . path } / image.png` )
152+ expect ( patch . files ) . toContain ( fwd ( tmp . path , " image.png" ) )
147153
148154 await Snapshot . revert ( [ patch ] )
149155 expect (
@@ -164,9 +170,9 @@ test("symlink handling", async () => {
164170 const before = await Snapshot . track ( )
165171 expect ( before ) . toBeTruthy ( )
166172
167- await $ `ln -s ${ tmp . path } /a.txt ${ tmp . path } /link.txt`. quiet ( )
173+ await fs . symlink ( ` ${ tmp . path } /a.txt` , ` ${ tmp . path } /link.txt`, "file" )
168174
169- expect ( ( await Snapshot . patch ( before ! ) ) . files ) . toContain ( ` ${ tmp . path } / link.txt` )
175+ expect ( ( await Snapshot . patch ( before ! ) ) . files ) . toContain ( fwd ( tmp . path , " link.txt" ) )
170176 } ,
171177 } )
172178} )
@@ -181,7 +187,7 @@ test("large file handling", async () => {
181187
182188 await Filesystem . write ( `${ tmp . path } /large.txt` , "x" . repeat ( 1024 * 1024 ) )
183189
184- expect ( ( await Snapshot . patch ( before ! ) ) . files ) . toContain ( ` ${ tmp . path } / large.txt` )
190+ expect ( ( await Snapshot . patch ( before ! ) ) . files ) . toContain ( fwd ( tmp . path , " large.txt" ) )
185191 } ,
186192 } )
187193} )
@@ -222,9 +228,9 @@ test("special characters in filenames", async () => {
222228 await Filesystem . write ( `${ tmp . path } /file_with_underscores.txt` , "UNDERSCORES" )
223229
224230 const files = ( await Snapshot . patch ( before ! ) ) . files
225- expect ( files ) . toContain ( ` ${ tmp . path } / file with spaces.txt` )
226- expect ( files ) . toContain ( ` ${ tmp . path } / file-with-dashes.txt` )
227- expect ( files ) . toContain ( ` ${ tmp . path } / file_with_underscores.txt` )
231+ expect ( files ) . toContain ( fwd ( tmp . path , " file with spaces.txt" ) )
232+ expect ( files ) . toContain ( fwd ( tmp . path , " file-with-dashes.txt" ) )
233+ expect ( files ) . toContain ( fwd ( tmp . path , " file_with_underscores.txt" ) )
228234 } ,
229235 } )
230236} )
@@ -293,10 +299,10 @@ test("unicode filenames", async () => {
293299 expect ( before ) . toBeTruthy ( )
294300
295301 const unicodeFiles = [
296- { path : ` ${ tmp . path } / 文件.txt` , content : "chinese content" } ,
297- { path : ` ${ tmp . path } / 🚀rocket.txt` , content : "emoji content" } ,
298- { path : ` ${ tmp . path } / café.txt` , content : "accented content" } ,
299- { path : ` ${ tmp . path } / файл.txt` , content : "cyrillic content" } ,
302+ { path : fwd ( tmp . path , " 文件.txt" ) , content : "chinese content" } ,
303+ { path : fwd ( tmp . path , " 🚀rocket.txt" ) , content : "emoji content" } ,
304+ { path : fwd ( tmp . path , " café.txt" ) , content : "accented content" } ,
305+ { path : fwd ( tmp . path , " файл.txt" ) , content : "cyrillic content" } ,
300306 ]
301307
302308 for ( const file of unicodeFiles ) {
@@ -329,8 +335,8 @@ test.skip("unicode filenames modification and restore", async () => {
329335 await Instance . provide ( {
330336 directory : tmp . path ,
331337 fn : async ( ) => {
332- const chineseFile = ` ${ tmp . path } / 文件.txt`
333- const cyrillicFile = ` ${ tmp . path } / файл.txt`
338+ const chineseFile = fwd ( tmp . path , " 文件.txt" )
339+ const cyrillicFile = fwd ( tmp . path , " файл.txt" )
334340
335341 await Filesystem . write ( chineseFile , "original chinese" )
336342 await Filesystem . write ( cyrillicFile , "original cyrillic" )
@@ -362,7 +368,7 @@ test("unicode filenames in subdirectories", async () => {
362368 expect ( before ) . toBeTruthy ( )
363369
364370 await $ `mkdir -p "${ tmp . path } /目录/подкаталог"` . quiet ( )
365- const deepFile = ` ${ tmp . path } /目录/ подкаталог/ 文件.txt`
371+ const deepFile = fwd ( tmp . path , "目录" , " подкаталог" , " 文件.txt" )
366372 await Filesystem . write ( deepFile , "deep unicode content" )
367373
368374 const patch = await Snapshot . patch ( before ! )
@@ -388,7 +394,7 @@ test("very long filenames", async () => {
388394 expect ( before ) . toBeTruthy ( )
389395
390396 const longName = "a" . repeat ( 200 ) + ".txt"
391- const longFile = ` ${ tmp . path } / ${ longName } `
397+ const longFile = fwd ( tmp . path , longName )
392398
393399 await Filesystem . write ( longFile , "long filename content" )
394400
@@ -419,9 +425,9 @@ test("hidden files", async () => {
419425 await Filesystem . write ( `${ tmp . path } /.config` , "config content" )
420426
421427 const patch = await Snapshot . patch ( before ! )
422- expect ( patch . files ) . toContain ( ` ${ tmp . path } / .hidden` )
423- expect ( patch . files ) . toContain ( ` ${ tmp . path } / .gitignore` )
424- expect ( patch . files ) . toContain ( ` ${ tmp . path } / .config` )
428+ expect ( patch . files ) . toContain ( fwd ( tmp . path , " .hidden" ) )
429+ expect ( patch . files ) . toContain ( fwd ( tmp . path , " .gitignore" ) )
430+ expect ( patch . files ) . toContain ( fwd ( tmp . path , " .config" ) )
425431 } ,
426432 } )
427433} )
@@ -436,12 +442,12 @@ test("nested symlinks", async () => {
436442
437443 await $ `mkdir -p ${ tmp . path } /sub/dir` . quiet ( )
438444 await Filesystem . write ( `${ tmp . path } /sub/dir/target.txt` , "target content" )
439- await $ `ln -s ${ tmp . path } /sub/dir/target.txt ${ tmp . path } /sub/dir/link.txt`. quiet ( )
440- await $ `ln -s ${ tmp . path } /sub ${ tmp . path } /sub-link`. quiet ( )
445+ await fs . symlink ( ` ${ tmp . path } /sub/dir/target.txt` , ` ${ tmp . path } /sub/dir/link.txt`, "file" )
446+ await fs . symlink ( ` ${ tmp . path } /sub` , ` ${ tmp . path } /sub-link`, "dir" )
441447
442448 const patch = await Snapshot . patch ( before ! )
443- expect ( patch . files ) . toContain ( ` ${ tmp . path } / sub/ dir/ link.txt` )
444- expect ( patch . files ) . toContain ( ` ${ tmp . path } / sub-link` )
449+ expect ( patch . files ) . toContain ( fwd ( tmp . path , " sub" , " dir" , " link.txt" ) )
450+ expect ( patch . files ) . toContain ( fwd ( tmp . path , " sub-link" ) )
445451 } ,
446452 } )
447453} )
@@ -476,7 +482,7 @@ test("circular symlinks", async () => {
476482 expect ( before ) . toBeTruthy ( )
477483
478484 // Create circular symlink
479- await $ `ln -s ${ tmp . path } /circular ${ tmp . path } /circular`. quiet ( ) . nothrow ( )
485+ await fs . symlink ( ` ${ tmp . path } /circular` , ` ${ tmp . path } /circular`, "dir" ) . catch ( ( ) => { } )
480486
481487 const patch = await Snapshot . patch ( before ! )
482488 expect ( patch . files . length ) . toBeGreaterThanOrEqual ( 0 ) // Should not crash
@@ -499,11 +505,11 @@ test("gitignore changes", async () => {
499505 const patch = await Snapshot . patch ( before ! )
500506
501507 // Should track gitignore itself
502- expect ( patch . files ) . toContain ( ` ${ tmp . path } / .gitignore` )
508+ expect ( patch . files ) . toContain ( fwd ( tmp . path , " .gitignore" ) )
503509 // Should track normal files
504- expect ( patch . files ) . toContain ( ` ${ tmp . path } / normal.txt` )
510+ expect ( patch . files ) . toContain ( fwd ( tmp . path , " normal.txt" ) )
505511 // Should not track ignored files (git won't see them)
506- expect ( patch . files ) . not . toContain ( ` ${ tmp . path } / test.ignored` )
512+ expect ( patch . files ) . not . toContain ( fwd ( tmp . path , " test.ignored" ) )
507513 } ,
508514 } )
509515} )
@@ -523,8 +529,8 @@ test("git info exclude changes", async () => {
523529 await Bun . write ( `${ tmp . path } /normal.txt` , "normal content" )
524530
525531 const patch = await Snapshot . patch ( before ! )
526- expect ( patch . files ) . toContain ( ` ${ tmp . path } / normal.txt` )
527- expect ( patch . files ) . not . toContain ( ` ${ tmp . path } / ignored.txt` )
532+ expect ( patch . files ) . toContain ( fwd ( tmp . path , " normal.txt" ) )
533+ expect ( patch . files ) . not . toContain ( fwd ( tmp . path , " ignored.txt" ) )
528534
529535 const after = await Snapshot . track ( )
530536 const diffs = await Snapshot . diffFull ( before ! , after ! )
@@ -559,9 +565,9 @@ test("git info exclude keeps global excludes", async () => {
559565 await Bun . write ( `${ tmp . path } /normal.txt` , "normal content" )
560566
561567 const patch = await Snapshot . patch ( before ! )
562- expect ( patch . files ) . toContain ( ` ${ tmp . path } / normal.txt` )
563- expect ( patch . files ) . not . toContain ( ` ${ tmp . path } / global.tmp` )
564- expect ( patch . files ) . not . toContain ( ` ${ tmp . path } / info.tmp` )
568+ expect ( patch . files ) . toContain ( fwd ( tmp . path , " normal.txt" ) )
569+ expect ( patch . files ) . not . toContain ( fwd ( tmp . path , " global.tmp" ) )
570+ expect ( patch . files ) . not . toContain ( fwd ( tmp . path , " info.tmp" ) )
565571 } finally {
566572 if ( prev ) process . env . GIT_CONFIG_GLOBAL = prev
567573 else delete process . env . GIT_CONFIG_GLOBAL
@@ -610,7 +616,7 @@ test("snapshot state isolation between projects", async () => {
610616 const before1 = await Snapshot . track ( )
611617 await Filesystem . write ( `${ tmp1 . path } /project1.txt` , "project1 content" )
612618 const patch1 = await Snapshot . patch ( before1 ! )
613- expect ( patch1 . files ) . toContain ( ` ${ tmp1 . path } / project1.txt` )
619+ expect ( patch1 . files ) . toContain ( fwd ( tmp1 . path , " project1.txt" ) )
614620 } ,
615621 } )
616622
@@ -620,10 +626,10 @@ test("snapshot state isolation between projects", async () => {
620626 const before2 = await Snapshot . track ( )
621627 await Filesystem . write ( `${ tmp2 . path } /project2.txt` , "project2 content" )
622628 const patch2 = await Snapshot . patch ( before2 ! )
623- expect ( patch2 . files ) . toContain ( ` ${ tmp2 . path } / project2.txt` )
629+ expect ( patch2 . files ) . toContain ( fwd ( tmp2 . path , " project2.txt" ) )
624630
625631 // Ensure project1 files don't appear in project2
626- expect ( patch2 . files ) . not . toContain ( ` ${ tmp1 ?. path } / project1.txt` )
632+ expect ( patch2 . files ) . not . toContain ( fwd ( tmp1 ?. path ?? "" , " project1.txt" ) )
627633 } ,
628634 } )
629635} )
@@ -647,7 +653,7 @@ test("patch detects changes in secondary worktree", async () => {
647653 const before = await Snapshot . track ( )
648654 expect ( before ) . toBeTruthy ( )
649655
650- const worktreeFile = ` ${ worktreePath } / worktree.txt`
656+ const worktreeFile = fwd ( worktreePath , " worktree.txt" )
651657 await Filesystem . write ( worktreeFile , "worktree content" )
652658
653659 const patch = await Snapshot . patch ( before ! )
@@ -681,7 +687,7 @@ test("revert only removes files in invoking worktree", async () => {
681687 const before = await Snapshot . track ( )
682688 expect ( before ) . toBeTruthy ( )
683689
684- const worktreeFile = ` ${ worktreePath } / worktree.txt`
690+ const worktreeFile = fwd ( worktreePath , " worktree.txt" )
685691 await Filesystem . write ( worktreeFile , "worktree content" )
686692
687693 const patch = await Snapshot . patch ( before ! )
@@ -832,7 +838,7 @@ test("revert should not delete files that existed but were deleted in snapshot",
832838 await Filesystem . write ( `${ tmp . path } /a.txt` , "recreated content" )
833839
834840 const patch = await Snapshot . patch ( snapshot2 ! )
835- expect ( patch . files ) . toContain ( ` ${ tmp . path } / a.txt` )
841+ expect ( patch . files ) . toContain ( fwd ( tmp . path , " a.txt" ) )
836842
837843 await Snapshot . revert ( [ patch ] )
838844
@@ -861,8 +867,8 @@ test("revert preserves file that existed in snapshot when deleted then recreated
861867 await Filesystem . write ( `${ tmp . path } /newfile.txt` , "new" )
862868
863869 const patch = await Snapshot . patch ( snapshot ! )
864- expect ( patch . files ) . toContain ( ` ${ tmp . path } / existing.txt` )
865- expect ( patch . files ) . toContain ( ` ${ tmp . path } / newfile.txt` )
870+ expect ( patch . files ) . toContain ( fwd ( tmp . path , " existing.txt" ) )
871+ expect ( patch . files ) . toContain ( fwd ( tmp . path , " newfile.txt" ) )
866872
867873 await Snapshot . revert ( [ patch ] )
868874
0 commit comments