@@ -10,6 +10,8 @@ vi.mock("fs/promises", () => ({
1010 chmod : vi . fn ( ) . mockResolvedValue ( undefined ) ,
1111 unlink : vi . fn ( ) . mockResolvedValue ( undefined ) ,
1212 rm : vi . fn ( ) . mockResolvedValue ( undefined ) ,
13+ readFile : vi . fn ( ) ,
14+ writeFile : vi . fn ( ) . mockResolvedValue ( undefined ) ,
1315} ) )
1416
1517// Mock fs (createWriteStream)
@@ -144,6 +146,8 @@ describe("semble-downloader", () => {
144146
145147 // fs.access resolves => file exists
146148 ; ( fs . access as any ) . mockResolvedValue ( undefined )
149+ // Version file matches current version
150+ ; ( fs . readFile as any ) . mockResolvedValue ( "v0.3.1" )
147151
148152 try {
149153 const result = await downloadSemble ( "/storage" )
@@ -168,6 +172,8 @@ describe("semble-downloader", () => {
168172
169173 // fs.access rejects => file not present
170174 ; ( fs . access as any ) . mockRejectedValue ( new Error ( "ENOENT" ) )
175+ // No version file exists
176+ ; ( fs . readFile as any ) . mockRejectedValue ( new Error ( "ENOENT" ) )
171177
172178 // Simulate successful download: pipe is called, then "finish" fires
173179 mockWriteStream . on . mockImplementation ( ( event : string , cb : ( ) => void ) => {
@@ -196,6 +202,12 @@ describe("semble-downloader", () => {
196202 expect . any ( Object ) ,
197203 )
198204 expect ( fs . chmod ) . toHaveBeenCalledWith ( path . join ( "/storage" , "semble" , "semble" ) , 0o755 )
205+ // Version file should be written
206+ expect ( fs . writeFile ) . toHaveBeenCalledWith (
207+ path . join ( "/storage" , "semble" , ".semble-version" ) ,
208+ "v0.3.1" ,
209+ "utf-8" ,
210+ )
199211 // Archive should be cleaned up
200212 expect ( fs . unlink ) . toHaveBeenCalledWith ( path . join ( "/storage" , "semble-linux-x64-fast.tar.gz" ) )
201213 } finally {
@@ -213,6 +225,8 @@ describe("semble-downloader", () => {
213225
214226 // fs.access resolves => file exists
215227 ; ( fs . access as any ) . mockResolvedValue ( undefined )
228+ // Version file matches
229+ ; ( fs . readFile as any ) . mockResolvedValue ( "v0.3.1" )
216230
217231 try {
218232 const result = await downloadSemble ( "/storage" )
@@ -234,6 +248,8 @@ describe("semble-downloader", () => {
234248
235249 // fs.access rejects => file not present
236250 ; ( fs . access as any ) . mockRejectedValue ( new Error ( "ENOENT" ) )
251+ // No version file
252+ ; ( fs . readFile as any ) . mockRejectedValue ( new Error ( "ENOENT" ) )
237253
238254 // Simulate HTTP error response
239255 mockResponse . statusCode = 404
@@ -257,6 +273,8 @@ describe("semble-downloader", () => {
257273
258274 // fs.access rejects => file not present
259275 ; ( fs . access as any ) . mockRejectedValue ( new Error ( "ENOENT" ) )
276+ // No version file
277+ ; ( fs . readFile as any ) . mockRejectedValue ( new Error ( "ENOENT" ) )
260278
261279 // First call returns a redirect, second call returns 200
262280 let callCount = 0
@@ -378,6 +396,8 @@ describe("semble-downloader", () => {
378396
379397 // fs.access rejects => file not present, triggering download
380398 ; ( fs . access as any ) . mockRejectedValue ( new Error ( "ENOENT" ) )
399+ // No version file
400+ ; ( fs . readFile as any ) . mockRejectedValue ( new Error ( "ENOENT" ) )
381401
382402 // Simulate successful download
383403 mockWriteStream . on . mockImplementation ( ( event : string , cb : ( ) => void ) => {
@@ -415,6 +435,8 @@ describe("semble-downloader", () => {
415435
416436 // fs.access rejects => file not present
417437 ; ( fs . access as any ) . mockRejectedValue ( new Error ( "ENOENT" ) )
438+ // No version file
439+ ; ( fs . readFile as any ) . mockRejectedValue ( new Error ( "ENOENT" ) )
418440
419441 // Simulate successful download
420442 mockWriteStream . on . mockImplementation ( ( event : string , cb : ( ) => void ) => {
@@ -436,4 +458,152 @@ describe("semble-downloader", () => {
436458 }
437459 } )
438460 } )
461+
462+ describe ( "downloadSemble - version tracking" , ( ) => {
463+ it ( "should re-download when installed version differs from SEMBLE_VERSION" , async ( ) => {
464+ const originalPlatform = Object . getOwnPropertyDescriptor ( process , "platform" )
465+ const originalArch = Object . getOwnPropertyDescriptor ( process , "arch" )
466+
467+ Object . defineProperty ( process , "platform" , { value : "linux" , configurable : true } )
468+ Object . defineProperty ( process , "arch" , { value : "x64" , configurable : true } )
469+
470+ // Version file has an old version
471+ ; ( fs . readFile as any ) . mockResolvedValue ( "v0.2.0" )
472+ // Binary doesn't matter — version mismatch forces re-download
473+ ; ( fs . access as any ) . mockRejectedValue ( new Error ( "ENOENT" ) )
474+
475+ // Simulate successful download
476+ mockWriteStream . on . mockImplementation ( ( event : string , cb : ( ) => void ) => {
477+ if ( event === "finish" ) {
478+ setImmediate ( cb )
479+ }
480+ } )
481+
482+ try {
483+ const result = await downloadSemble ( "/storage" )
484+
485+ expect ( result ) . toBe ( path . join ( "/storage" , "semble" , "semble" ) )
486+ // Should remove old installation
487+ expect ( fs . rm ) . toHaveBeenCalledWith ( path . join ( "/storage" , "semble" ) , {
488+ recursive : true ,
489+ force : true ,
490+ } )
491+ // Should download the new version
492+ expect ( https . get ) . toHaveBeenCalledWith ( expect . stringContaining ( "v0.3.1" ) , expect . any ( Function ) )
493+ // Should write the new version file
494+ expect ( fs . writeFile ) . toHaveBeenCalledWith (
495+ path . join ( "/storage" , "semble" , ".semble-version" ) ,
496+ "v0.3.1" ,
497+ "utf-8" ,
498+ )
499+ } finally {
500+ if ( originalPlatform ) Object . defineProperty ( process , "platform" , originalPlatform )
501+ if ( originalArch ) Object . defineProperty ( process , "arch" , originalArch )
502+ }
503+ } )
504+
505+ it ( "should skip download when installed version matches SEMBLE_VERSION and binary exists" , async ( ) => {
506+ const originalPlatform = Object . getOwnPropertyDescriptor ( process , "platform" )
507+ const originalArch = Object . getOwnPropertyDescriptor ( process , "arch" )
508+
509+ Object . defineProperty ( process , "platform" , { value : "linux" , configurable : true } )
510+ Object . defineProperty ( process , "arch" , { value : "x64" , configurable : true } )
511+
512+ // Version matches
513+ ; ( fs . readFile as any ) . mockResolvedValue ( "v0.3.1" )
514+ // Binary exists
515+ ; ( fs . access as any ) . mockResolvedValue ( undefined )
516+
517+ try {
518+ const result = await downloadSemble ( "/storage" )
519+
520+ expect ( result ) . toBe ( path . join ( "/storage" , "semble" , "semble" ) )
521+ // Should NOT download
522+ expect ( https . get ) . not . toHaveBeenCalled ( )
523+ // Should NOT remove the extract dir
524+ expect ( fs . rm ) . not . toHaveBeenCalled ( )
525+ } finally {
526+ if ( originalPlatform ) Object . defineProperty ( process , "platform" , originalPlatform )
527+ if ( originalArch ) Object . defineProperty ( process , "arch" , originalArch )
528+ }
529+ } )
530+
531+ it ( "should re-download when version matches but binary is missing" , async ( ) => {
532+ const originalPlatform = Object . getOwnPropertyDescriptor ( process , "platform" )
533+ const originalArch = Object . getOwnPropertyDescriptor ( process , "arch" )
534+
535+ Object . defineProperty ( process , "platform" , { value : "linux" , configurable : true } )
536+ Object . defineProperty ( process , "arch" , { value : "x64" , configurable : true } )
537+
538+ // Version matches
539+ ; ( fs . readFile as any ) . mockResolvedValue ( "v0.3.1" )
540+ // But binary is missing
541+ ; ( fs . access as any ) . mockRejectedValue ( new Error ( "ENOENT" ) )
542+
543+ // Simulate successful download
544+ mockWriteStream . on . mockImplementation ( ( event : string , cb : ( ) => void ) => {
545+ if ( event === "finish" ) {
546+ setImmediate ( cb )
547+ }
548+ } )
549+
550+ try {
551+ const result = await downloadSemble ( "/storage" )
552+
553+ expect ( result ) . toBe ( path . join ( "/storage" , "semble" , "semble" ) )
554+ // Should download since binary was missing
555+ expect ( https . get ) . toHaveBeenCalled ( )
556+ // Should write version file again
557+ expect ( fs . writeFile ) . toHaveBeenCalledWith (
558+ path . join ( "/storage" , "semble" , ".semble-version" ) ,
559+ "v0.3.1" ,
560+ "utf-8" ,
561+ )
562+ } finally {
563+ if ( originalPlatform ) Object . defineProperty ( process , "platform" , originalPlatform )
564+ if ( originalArch ) Object . defineProperty ( process , "arch" , originalArch )
565+ }
566+ } )
567+
568+ it ( "should download when no version file exists (first install)" , async ( ) => {
569+ const originalPlatform = Object . getOwnPropertyDescriptor ( process , "platform" )
570+ const originalArch = Object . getOwnPropertyDescriptor ( process , "arch" )
571+
572+ Object . defineProperty ( process , "platform" , { value : "linux" , configurable : true } )
573+ Object . defineProperty ( process , "arch" , { value : "x64" , configurable : true } )
574+
575+ // No version file
576+ ; ( fs . readFile as any ) . mockRejectedValue ( new Error ( "ENOENT" ) )
577+ // No binary
578+ ; ( fs . access as any ) . mockRejectedValue ( new Error ( "ENOENT" ) )
579+
580+ // Simulate successful download
581+ mockWriteStream . on . mockImplementation ( ( event : string , cb : ( ) => void ) => {
582+ if ( event === "finish" ) {
583+ setImmediate ( cb )
584+ }
585+ } )
586+
587+ try {
588+ const result = await downloadSemble ( "/storage" )
589+
590+ expect ( result ) . toBe ( path . join ( "/storage" , "semble" , "semble" ) )
591+ expect ( https . get ) . toHaveBeenCalled ( )
592+ // Should NOT try to rm the old dir (no previous version)
593+ expect ( fs . rm ) . not . toHaveBeenCalledWith (
594+ path . join ( "/storage" , "semble" ) ,
595+ expect . objectContaining ( { recursive : true } ) ,
596+ )
597+ // Should write version file
598+ expect ( fs . writeFile ) . toHaveBeenCalledWith (
599+ path . join ( "/storage" , "semble" , ".semble-version" ) ,
600+ "v0.3.1" ,
601+ "utf-8" ,
602+ )
603+ } finally {
604+ if ( originalPlatform ) Object . defineProperty ( process , "platform" , originalPlatform )
605+ if ( originalArch ) Object . defineProperty ( process , "arch" , originalArch )
606+ }
607+ } )
608+ } )
439609} )
0 commit comments