@@ -56,9 +56,8 @@ mock.module('../src/command', () => ({
5656 elideInfo : jest . fn ( )
5757} ) )
5858
59- const { downloadRelease, resolveLatestVersion } = await import (
60- '../src/releases'
61- )
59+ const { downloadRelease, resolveLatestVersion, toSemverCacheKey } =
60+ await import ( '../src/releases' )
6261const { default : buildOptions } = await import ( '../src/options' )
6362
6463describe ( 'resolveLatestVersion' , ( ) => {
@@ -116,6 +115,28 @@ describe('resolveLatestVersion', () => {
116115 } )
117116} )
118117
118+ describe ( 'toSemverCacheKey' , ( ) => {
119+ it ( 'should pass through valid semver' , ( ) => {
120+ expect ( toSemverCacheKey ( '1.0.0' ) ) . toBe ( '1.0.0' )
121+ expect ( toSemverCacheKey ( '1.0.0-beta10' ) ) . toBe ( '1.0.0-beta10' )
122+ expect ( toSemverCacheKey ( '1.0.0-alpha9' ) ) . toBe ( '1.0.0-alpha9' )
123+ expect ( toSemverCacheKey ( '2.1.3' ) ) . toBe ( '2.1.3' )
124+ } )
125+
126+ it ( 'should convert nightly tags to semver prerelease' , ( ) => {
127+ expect ( toSemverCacheKey ( 'nightly-20260328' ) ) . toBe ( '0.0.0-nightly.20260328' )
128+ expect ( toSemverCacheKey ( 'nightly-2026-03-28' ) ) . toBe (
129+ '0.0.0-nightly.20260328'
130+ )
131+ expect ( toSemverCacheKey ( 'nightly-20251231' ) ) . toBe ( '0.0.0-nightly.20251231' )
132+ } )
133+
134+ it ( 'should pass through unknown formats unchanged' , ( ) => {
135+ expect ( toSemverCacheKey ( 'dev' ) ) . toBe ( 'dev' )
136+ expect ( toSemverCacheKey ( 'custom-build' ) ) . toBe ( 'custom-build' )
137+ } )
138+ } )
139+
119140describe ( 'downloadRelease' , ( ) => {
120141 beforeEach ( ( ) => {
121142 debugMock . mockClear ( )
@@ -426,6 +447,59 @@ describe('downloadRelease', () => {
426447 expect ( downloadToolMock ) . not . toHaveBeenCalled ( )
427448 } )
428449
450+ it ( 'nightly versions should use semver cache keys' , async ( ) => {
451+ findMock . mockReturnValue ( '' )
452+ requestMock . mockResolvedValue ( {
453+ data : { tag_name : 'nightly-20260328' , name : 'Nightly' }
454+ } )
455+ const options = buildOptions ( {
456+ os : 'linux' ,
457+ arch : 'amd64' ,
458+ version : 'latest'
459+ } )
460+ await downloadRelease ( options )
461+
462+ // find should be called with the semver cache key, not the raw tag
463+ expect ( findMock ) . toHaveBeenCalledWith (
464+ 'elide' ,
465+ '0.0.0-nightly.20260328' ,
466+ 'amd64'
467+ )
468+ // cacheDir should also use the semver key
469+ expect ( cacheDirMock ) . toHaveBeenCalledWith (
470+ expect . any ( String ) ,
471+ 'elide' ,
472+ '0.0.0-nightly.20260328' ,
473+ 'amd64'
474+ )
475+ } )
476+
477+ it ( 'second run with nightly should hit cache' , async ( ) => {
478+ findMock . mockReturnValue (
479+ '/cache/tools/elide/0.0.0-nightly.20260328/amd64'
480+ )
481+ requestMock . mockResolvedValue ( {
482+ data : { tag_name : 'nightly-20260328' , name : 'Nightly' }
483+ } )
484+ const options = buildOptions ( {
485+ os : 'linux' ,
486+ arch : 'amd64' ,
487+ version : 'latest'
488+ } )
489+ const result = await downloadRelease ( options )
490+
491+ expect ( findMock ) . toHaveBeenCalledWith (
492+ 'elide' ,
493+ '0.0.0-nightly.20260328' ,
494+ 'amd64'
495+ )
496+ expect ( downloadToolMock ) . not . toHaveBeenCalled ( )
497+ expect ( result . cached ) . toBe ( true )
498+ expect ( result . elideBin ) . toBe (
499+ '/cache/tools/elide/0.0.0-nightly.20260328/amd64/bin'
500+ )
501+ } )
502+
429503 it ( 'different architectures should not share cache' , async ( ) => {
430504 findMock . mockImplementation (
431505 ( _name : string , _version : string , arch : string ) => {
0 commit comments