@@ -97,11 +97,11 @@ describe("cached assets", () => {
9797 expect ( stored ?. url ) . toBe ( "https://cdn/new.webp" ) ;
9898 } ) ;
9999
100- it ( "retries when null is cached (treats null as miss )" , async ( ) => {
100+ it ( "retries transient failures ( null without notFound flag )" , 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)
104+ // Pre-seed cache with null result WITHOUT notFound (simulating transient failure)
105105 const { redis } = await import ( "@/lib/redis" ) ;
106106 await redis . set ( indexKey , {
107107 url : null ,
@@ -124,25 +124,55 @@ describe("cached assets", () => {
124124 expect ( produceCalled ) . toBe ( true ) ;
125125 } ) ;
126126
127- it ( "writes null to cache to prevent concurrent retries " , async ( ) => {
127+ it ( "does NOT retry permanent not found (null with notFound=true) " , async ( ) => {
128128 const indexKey = ns ( "test" , "asset5" ) ;
129129 const lockKey = ns ( "lock" , "test" , "asset5" ) ;
130130
131+ // Pre-seed cache with permanent not found
132+ const { redis } = await import ( "@/lib/redis" ) ;
133+ await redis . set ( indexKey , {
134+ url : null ,
135+ notFound : true ,
136+ expiresAtMs : Date . now ( ) + 1000 ,
137+ } ) ;
138+
139+ let produceCalled = false ;
140+ const result = await getOrCreateCachedAsset < { source : string } > ( {
141+ indexKey,
142+ lockKey,
143+ ttlSeconds : 60 ,
144+ produceAndUpload : async ( ) => {
145+ produceCalled = true ;
146+ return { url : "https://cdn/should-not-call.webp" } ;
147+ } ,
148+ } ) ;
149+
150+ // Should NOT retry and return null immediately
151+ expect ( result ) . toEqual ( { url : null } ) ;
152+ expect ( produceCalled ) . toBe ( false ) ;
153+ } ) ;
154+
155+ it ( "caches notFound flag when returned by producer" , async ( ) => {
156+ const indexKey = ns ( "test" , "asset6" ) ;
157+ const lockKey = ns ( "lock" , "test" , "asset6" ) ;
158+
131159 const result = await getOrCreateCachedAsset < { source : string } > ( {
132160 indexKey,
133161 lockKey,
134162 ttlSeconds : 60 ,
135- produceAndUpload : async ( ) => ( { url : null } ) ,
163+ produceAndUpload : async ( ) => ( { url : null , notFound : true } ) ,
136164 } ) ;
137165
138166 expect ( result ) . toEqual ( { url : null } ) ;
139167
140- // Null should still be written to cache
168+ // notFound flag should be stored in cache
141169 const { redis } = await import ( "@/lib/redis" ) ;
142170 const stored = ( await redis . get ( indexKey ) ) as {
143171 url ?: string | null ;
172+ notFound ?: boolean ;
144173 } | null ;
145174 expect ( stored ?. url ) . toBe ( null ) ;
175+ expect ( stored ?. notFound ) . toBe ( true ) ;
146176 } ) ;
147177
148178 it ( "schedules blob deletion with grace period beyond Redis TTL" , async ( ) => {
0 commit comments