@@ -32,11 +32,20 @@ vi.mock("fs/promises", () => ({
3232 readdir : vi . fn ( ) . mockResolvedValue ( [ ] ) ,
3333} ) )
3434
35+ vi . mock ( "proper-lockfile" , ( ) => ( {
36+ lock : vi . fn ( ) . mockResolvedValue ( vi . fn ( ) . mockResolvedValue ( undefined ) ) ,
37+ } ) )
38+
3539// Mock fs (createWriteStream and createReadStream for checksum verification)
40+ let closeHandler : ( ( ) => void ) | undefined
3641const mockWriteStream = {
3742 on : vi . fn ( ) ,
3843 close : vi . fn ( ) ,
3944}
45+ const onWriteStreamEvent = ( event : string , callback : ( ) => void ) => {
46+ if ( event === "finish" ) setImmediate ( callback )
47+ if ( event === "close" ) closeHandler = callback
48+ }
4049vi . mock ( "fs" , ( ) => ( {
4150 createWriteStream : vi . fn ( ( ) => mockWriteStream ) ,
4251 createReadStream : vi . fn ( ( ) => {
@@ -103,8 +112,9 @@ describe("SEMBLE_SHA256 checksum fixture", () => {
103112describe ( "semble-downloader" , ( ) => {
104113 beforeEach ( ( ) => {
105114 vi . clearAllMocks ( )
106- mockWriteStream . on = vi . fn ( )
107- mockWriteStream . close = vi . fn ( )
115+ closeHandler = undefined
116+ mockWriteStream . on = vi . fn ( onWriteStreamEvent )
117+ mockWriteStream . close = vi . fn ( ( ) => closeHandler ?.( ) )
108118
109119 // Restore the default https.get mock so tests that override it don't leak
110120 ; ( https . get as any ) . mockImplementation ( ( _url : string , callback : ( res : any ) => void ) => {
@@ -223,13 +233,6 @@ describe("semble-downloader", () => {
223233 // No version file exists
224234 ; ( fs . readFile as any ) . mockRejectedValue ( new Error ( "ENOENT" ) )
225235
226- // Simulate successful download: pipe is called, then "finish" fires
227- mockWriteStream . on . mockImplementation ( ( event : string , cb : ( ) => void ) => {
228- if ( event === "finish" ) {
229- setImmediate ( cb )
230- }
231- } )
232-
233236 try {
234237 const result = await downloadSemble ( "/storage" )
235238
@@ -263,12 +266,14 @@ describe("semble-downloader", () => {
263266 )
264267 // Version file should be written
265268 expect ( fs . writeFile ) . toHaveBeenCalledWith (
266- path . join ( "/storage" , "semble" , ".semble-version" ) ,
269+ path . join ( "/storage" , "semble.new " , ".semble-version" ) ,
267270 "v0.4.1" ,
268271 "utf-8" ,
269272 )
270273 // Archive should be cleaned up (version-prefixed local cache path)
271- expect ( fs . unlink ) . toHaveBeenCalledWith ( path . join ( "/storage" , "v0.4.1-semble-linux-x64-fast.tar.gz" ) )
274+ expect ( fs . rm ) . toHaveBeenCalledWith ( path . join ( "/storage" , "v0.4.1-semble-linux-x64-fast.tar.gz" ) , {
275+ force : true ,
276+ } )
272277 } finally {
273278 if ( originalPlatform ) Object . defineProperty ( process , "platform" , originalPlatform )
274279 if ( originalArch ) Object . defineProperty ( process , "arch" , originalArch )
@@ -325,7 +330,9 @@ describe("semble-downloader", () => {
325330
326331 try {
327332 await expect ( downloadSemble ( "/storage" ) ) . rejects . toThrow ( "Failed to download semble" )
328- expect ( fs . unlink ) . toHaveBeenCalledWith ( path . join ( "/storage" , "v0.4.1-semble-linux-arm64-fast.tar.gz" ) )
333+ expect ( fs . rm ) . toHaveBeenCalledWith ( path . join ( "/storage" , "v0.4.1-semble-linux-arm64-fast.tar.gz" ) , {
334+ force : true ,
335+ } )
329336 // Should clean up staging directory, not the original
330337 expect ( fs . rm ) . toHaveBeenCalledWith ( path . join ( "/storage" , "semble.new" ) , {
331338 recursive : true ,
@@ -374,12 +381,6 @@ describe("semble-downloader", () => {
374381 } )
375382
376383 // Simulate successful download on the second response
377- mockWriteStream . on . mockImplementation ( ( event : string , cb : ( ) => void ) => {
378- if ( event === "finish" ) {
379- setImmediate ( cb )
380- }
381- } )
382-
383384 try {
384385 const result = await downloadSemble ( "/storage" )
385386
@@ -542,12 +543,6 @@ describe("semble-downloader", () => {
542543 ; ( fs . readFile as any ) . mockRejectedValue ( new Error ( "ENOENT" ) )
543544
544545 // Simulate successful download
545- mockWriteStream . on . mockImplementation ( ( event : string , cb : ( ) => void ) => {
546- if ( event === "finish" ) {
547- setImmediate ( cb )
548- }
549- } )
550-
551546 try {
552547 const result = await downloadSemble ( "/storage" )
553548
@@ -582,14 +577,8 @@ describe("semble-downloader", () => {
582577 ; ( fs . readFile as any ) . mockRejectedValue ( new Error ( "ENOENT" ) )
583578
584579 // Simulate successful download
585- mockWriteStream . on . mockImplementation ( ( event : string , cb : ( ) => void ) => {
586- if ( event === "finish" ) {
587- setImmediate ( cb )
588- }
589- } )
590-
591580 // Archive cleanup fails but should not throw (only archive removal after extraction)
592- ; ( fs . unlink as any ) . mockRejectedValue ( new Error ( "unlink cleanup failed" ) )
581+ ; ( fs . rm as any ) . mockRejectedValueOnce ( new Error ( "archive cleanup failed" ) )
593582
594583 try {
595584 const result = await downloadSemble ( "/storage" )
@@ -617,12 +606,6 @@ describe("semble-downloader", () => {
617606 ; ( fs . access as any ) . mockResolvedValue ( undefined )
618607
619608 // Simulate successful download
620- mockWriteStream . on . mockImplementation ( ( event : string , cb : ( ) => void ) => {
621- if ( event === "finish" ) {
622- setImmediate ( cb )
623- }
624- } )
625-
626609 try {
627610 const result = await downloadSemble ( "/storage" )
628611
@@ -641,7 +624,7 @@ describe("semble-downloader", () => {
641624 expect ( https . get ) . toHaveBeenCalledWith ( expect . stringContaining ( "v0.4.1" ) , expect . any ( Function ) )
642625 // Should write the new version file
643626 expect ( fs . writeFile ) . toHaveBeenCalledWith (
644- path . join ( "/storage" , "semble" , ".semble-version" ) ,
627+ path . join ( "/storage" , "semble.new " , ".semble-version" ) ,
645628 "v0.4.1" ,
646629 "utf-8" ,
647630 )
@@ -673,12 +656,6 @@ describe("semble-downloader", () => {
673656 ; ( fs . access as any ) . mockResolvedValue ( undefined )
674657
675658 // Simulate successful download
676- mockWriteStream . on . mockImplementation ( ( event : string , cb : ( ) => void ) => {
677- if ( event === "finish" ) {
678- setImmediate ( cb )
679- }
680- } )
681-
682659 try {
683660 const result = await downloadSemble ( "/storage" )
684661
@@ -700,19 +677,23 @@ describe("semble-downloader", () => {
700677 )
701678 // The stale archive is removed before the fresh download to guarantee
702679 // a clean package is verified against the new checksum.
703- expect ( fs . unlink ) . toHaveBeenCalledWith ( versionedArchive )
680+ expect ( fs . rm ) . toHaveBeenCalledWith ( versionedArchive , { force : true } )
704681 // The prior-version archive (v0.4.0-*) is swept by cleanupStaleArchives
705682 // after a successful install, so a version upgrade doesn't accumulate
706683 // orphaned packages on disk.
707- expect ( fs . unlink ) . toHaveBeenCalledWith ( path . join ( "/storage" , "v0.4.0-semble-linux-x64-fast.tar.gz" ) )
684+ expect ( fs . rm ) . toHaveBeenCalledWith ( path . join ( "/storage" , "v0.4.0-semble-linux-x64-fast.tar.gz" ) , {
685+ force : true ,
686+ } )
708687 // The legacy unversioned archive (pre-v0.4.0 cache layout) is also
709688 // swept, covering the v0.3.1 → v0.4.1 upgrade path.
710- expect ( fs . unlink ) . toHaveBeenCalledWith ( path . join ( "/storage" , "semble-linux-x64-fast.tar.gz" ) )
689+ expect ( fs . rm ) . toHaveBeenCalledWith ( path . join ( "/storage" , "semble-linux-x64-fast.tar.gz" ) , {
690+ force : true ,
691+ } )
711692 // Unrelated files in the storage dir must not be touched.
712- expect ( fs . unlink ) . not . toHaveBeenCalledWith ( path . join ( "/storage" , "unrelated-file.txt" ) )
693+ expect ( fs . rm ) . not . toHaveBeenCalledWith ( path . join ( "/storage" , "unrelated-file.txt" ) , expect . anything ( ) )
713694 // The new version file is recorded
714695 expect ( fs . writeFile ) . toHaveBeenCalledWith (
715- path . join ( "/storage" , "semble" , ".semble-version" ) ,
696+ path . join ( "/storage" , "semble.new " , ".semble-version" ) ,
716697 "v0.4.1" ,
717698 "utf-8" ,
718699 )
@@ -769,12 +750,6 @@ describe("semble-downloader", () => {
769750 } )
770751
771752 // Simulate successful download
772- mockWriteStream . on . mockImplementation ( ( event : string , cb : ( ) => void ) => {
773- if ( event === "finish" ) {
774- setImmediate ( cb )
775- }
776- } )
777-
778753 try {
779754 const result = await downloadSemble ( "/storage" )
780755
@@ -789,7 +764,7 @@ describe("semble-downloader", () => {
789764 )
790765 // Should write version file again
791766 expect ( fs . writeFile ) . toHaveBeenCalledWith (
792- path . join ( "/storage" , "semble" , ".semble-version" ) ,
767+ path . join ( "/storage" , "semble.new " , ".semble-version" ) ,
793768 "v0.4.1" ,
794769 "utf-8" ,
795770 )
@@ -812,12 +787,6 @@ describe("semble-downloader", () => {
812787 ; ( fs . access as any ) . mockResolvedValue ( undefined )
813788
814789 // Simulate successful download
815- mockWriteStream . on . mockImplementation ( ( event : string , cb : ( ) => void ) => {
816- if ( event === "finish" ) {
817- setImmediate ( cb )
818- }
819- } )
820-
821790 try {
822791 const result = await downloadSemble ( "/storage" )
823792
@@ -831,7 +800,7 @@ describe("semble-downloader", () => {
831800 )
832801 // Should write version file
833802 expect ( fs . writeFile ) . toHaveBeenCalledWith (
834- path . join ( "/storage" , "semble" , ".semble-version" ) ,
803+ path . join ( "/storage" , "semble.new " , ".semble-version" ) ,
835804 "v0.4.1" ,
836805 "utf-8" ,
837806 )
@@ -856,12 +825,6 @@ describe("semble-downloader", () => {
856825 // readdir rejects — exercises the catch block in cleanupStaleArchives
857826 ; ( fs . readdir as any ) . mockRejectedValue ( new Error ( "EACCES" ) )
858827
859- mockWriteStream . on . mockImplementation ( ( event : string , cb : ( ) => void ) => {
860- if ( event === "finish" ) {
861- setImmediate ( cb )
862- }
863- } )
864-
865828 try {
866829 const result = await downloadSemble ( "/storage" )
867830
@@ -892,29 +855,27 @@ describe("semble-downloader", () => {
892855 "unrelated.txt" ,
893856 ] )
894857
895- mockWriteStream . on . mockImplementation ( ( event : string , cb : ( ) => void ) => {
896- if ( event === "finish" ) {
897- setImmediate ( cb )
898- }
899- } )
900-
901858 try {
902859 await downloadSemble ( "/storage" )
903860
904861 const currentArchive = path . join ( "/storage" , "v0.4.1-semble-linux-x64-fast.tar.gz" )
905862 // Stale versioned + legacy unversioned archives are swept
906- expect ( fs . unlink ) . toHaveBeenCalledWith ( path . join ( "/storage" , "v0.4.0-semble-linux-x64-fast.tar.gz" ) )
907- expect ( fs . unlink ) . toHaveBeenCalledWith ( path . join ( "/storage" , "semble-linux-x64-fast.tar.gz" ) )
863+ expect ( fs . rm ) . toHaveBeenCalledWith ( path . join ( "/storage" , "v0.4.0-semble-linux-x64-fast.tar.gz" ) , {
864+ force : true ,
865+ } )
866+ expect ( fs . rm ) . toHaveBeenCalledWith ( path . join ( "/storage" , "semble-linux-x64-fast.tar.gz" ) , {
867+ force : true ,
868+ } )
908869 // The current archive is never swept by cleanupStaleArchives (it is
909870 // excluded by the currentArchivePath guard). It is unlinked only by
910871 // the pre-download partial-archive cleanup and the post-install
911872 // archive cleanup steps. unrelated.txt is never touched.
912- expect ( fs . unlink ) . not . toHaveBeenCalledWith ( path . join ( "/storage" , "unrelated.txt" ) )
873+ expect ( fs . rm ) . not . toHaveBeenCalledWith ( path . join ( "/storage" , "unrelated.txt" ) , expect . anything ( ) )
913874 // Sanity: the current archive path is never passed to the stale sweep.
914875 // It is unlinked exactly twice (pre-download cleanup + post-install
915876 // archive cleanup), never via cleanupStaleArchives.
916- const currentUnlinks = ( fs . unlink as any ) . mock . calls . filter ( ( c : any [ ] ) => c [ 0 ] === currentArchive )
917- expect ( currentUnlinks . length ) . toBe ( 2 )
877+ const currentRemovals = ( fs . rm as any ) . mock . calls . filter ( ( c : any [ ] ) => c [ 0 ] === currentArchive )
878+ expect ( currentRemovals . length ) . toBe ( 2 )
918879 } finally {
919880 if ( originalPlatform ) Object . defineProperty ( process , "platform" , originalPlatform )
920881 if ( originalArch ) Object . defineProperty ( process , "arch" , originalArch )
0 commit comments