@@ -97,10 +97,37 @@ describe("cached assets", () => {
9797 expect ( stored ?. url ) . toBe ( "https://cdn/new.webp" ) ;
9898 } ) ;
9999
100- it ( "propagates not_found via cached null" , async ( ) => {
100+ it ( "retries when null is cached (treats null as miss) " , async ( ) => {
101101 const indexKey = ns ( "test" , "asset4" ) ;
102102 const lockKey = ns ( "lock" , "test" , "asset4" ) ;
103103
104+ // Pre-seed cache with null result (simulating previous failure)
105+ const { redis } = await import ( "@/lib/redis" ) ;
106+ await redis . set ( indexKey , {
107+ url : null ,
108+ expiresAtMs : Date . now ( ) + 1000 ,
109+ } ) ;
110+
111+ let produceCalled = false ;
112+ const result = await getOrCreateCachedAsset < { source : string } > ( {
113+ indexKey,
114+ lockKey,
115+ ttlSeconds : 60 ,
116+ produceAndUpload : async ( ) => {
117+ produceCalled = true ;
118+ return { url : "https://cdn/recovered.webp" } ;
119+ } ,
120+ } ) ;
121+
122+ // Should have retried and returned new URL
123+ expect ( result ) . toEqual ( { url : "https://cdn/recovered.webp" } ) ;
124+ expect ( produceCalled ) . toBe ( true ) ;
125+ } ) ;
126+
127+ it ( "writes null to cache to prevent concurrent retries" , async ( ) => {
128+ const indexKey = ns ( "test" , "asset5" ) ;
129+ const lockKey = ns ( "lock" , "test" , "asset5" ) ;
130+
104131 const result = await getOrCreateCachedAsset < { source : string } > ( {
105132 indexKey,
106133 lockKey,
@@ -109,6 +136,13 @@ describe("cached assets", () => {
109136 } ) ;
110137
111138 expect ( result ) . toEqual ( { url : null } ) ;
139+
140+ // Null should still be written to cache
141+ const { redis } = await import ( "@/lib/redis" ) ;
142+ const stored = ( await redis . get ( indexKey ) ) as {
143+ url ?: string | null ;
144+ } | null ;
145+ expect ( stored ?. url ) . toBe ( null ) ;
112146 } ) ;
113147
114148 it ( "schedules blob deletion with grace period beyond Redis TTL" , async ( ) => {
0 commit comments