@@ -158,6 +158,89 @@ describe("registry", () => {
158158 rmSync ( dir , { recursive : true , force : true } )
159159 }
160160 } )
161+
162+ it ( "persists verified models in the browser Cache API (download once)" , async ( ) => {
163+ const { createHash } = await import ( "node:crypto" )
164+ const { createServer } = await import ( "node:http" )
165+ const sha = createHash ( "sha256" ) . update ( fixtureZip ) . digest ( "hex" )
166+ let channelUp = true
167+ let indexBody = ""
168+ const server = createServer ( ( req , res ) => {
169+ if ( req . url === "/index.yaml" ) {
170+ res . writeHead ( 200 , { "content-type" : "text/yaml" } )
171+ res . end ( indexBody )
172+ return
173+ }
174+ if ( req . url === "/index.yaml.sha256" ) {
175+ const digest = createHash ( "sha256" ) . update ( indexBody ) . digest ( "hex" )
176+ res . writeHead ( 200 )
177+ res . end ( `${ digest } index.yaml\n` )
178+ return
179+ }
180+ if ( req . url === "/tiny.zip" ) {
181+ if ( ! channelUp ) {
182+ res . writeHead ( 404 )
183+ res . end ( "gone" )
184+ return
185+ }
186+ res . writeHead ( 200 )
187+ res . end ( fixtureZip )
188+ return
189+ }
190+ res . writeHead ( 404 )
191+ res . end ( )
192+ } )
193+ await new Promise < void > ( ( r ) => server . listen ( 0 , "127.0.0.1" , r ) )
194+ const port = ( server . address ( ) as { port : number } ) . port
195+ const indexUrl = `http://127.0.0.1:${ port } /index.yaml`
196+ indexBody = `version: 1\nmodels:\n tiny-1.0:\n filename: tiny.zip\n url: http://127.0.0.1:${ port } /tiny.zip\n sha256: ${ sha } \n`
197+
198+ const store = new Map < string , Response > ( )
199+ const fakeCache = {
200+ async match ( req : RequestInfo ) {
201+ const key = String ( req instanceof Request ? req . url : req )
202+ return store . get ( key ) ?. clone ( )
203+ } ,
204+ async put ( req : RequestInfo , res : Response ) {
205+ const key = String ( req instanceof Request ? req . url : req )
206+ store . set ( key , res . clone ( ) )
207+ } ,
208+ async delete ( req : RequestInfo ) {
209+ const key = String ( req instanceof Request ? req . url : req )
210+ return store . delete ( key )
211+ } ,
212+ }
213+ const g = globalThis as Record < string , unknown >
214+ g [ "caches" ] = { open : async ( ) => fakeCache }
215+ // simulate a browser host: no Node fs, so the Cache API path runs
216+ const versions = process . versions as { node ?: string }
217+ const realNode = versions . node
218+ delete versions . node
219+ try {
220+ process . env [ "SECRYST_CACHE" ] = undefined
221+ const first = await resolve ( "tiny-1.0" , indexUrl )
222+ expect ( [ ...first . bytes ] ) . toEqual ( [ ...fixtureZip ] )
223+ expect ( store . size ) . toBe ( 1 )
224+
225+ // channel dies; the cached copy serves, still sha-verified
226+ channelUp = false
227+ const second = await resolve ( "tiny-1.0" , indexUrl )
228+ expect ( [ ...second . bytes ] ) . toEqual ( [ ...fixtureZip ] )
229+
230+ // a corrupted cache entry falls through to a fresh download
231+ channelUp = true
232+ const key = store . keys ( ) . next ( ) . value as string
233+ store . set ( key , new Response ( new Uint8Array ( [ 1 , 2 , 3 ] ) ) )
234+ const third = await resolve ( "tiny-1.0" , indexUrl )
235+ expect ( [ ...third . bytes ] ) . toEqual ( [ ...fixtureZip ] )
236+ } finally {
237+ if ( realNode !== undefined ) versions . node = realNode
238+ delete g [ "caches" ]
239+ delete process . env [ "SECRYST_CACHE" ]
240+ await new Promise < void > ( ( r ) => server . close ( ( ) => r ( ) ) )
241+ }
242+ } )
243+
161244 it ( "DEFAULT_INDEX_URL pins a GitHub Release asset, never raw" , async ( ) => {
162245 const { DEFAULT_INDEX_URL } = await import ( "../src/ml/imf/registry.js" )
163246 expect ( DEFAULT_INDEX_URL ) . toMatch (
0 commit comments