@@ -441,6 +441,89 @@ describeIfFetchMock('RelayerV2Provider - TFHEPkeParams Caching', () => {
441441 expect ( mockTFHEPkeParamsFetch ) . toHaveBeenCalledTimes ( 1 ) ;
442442 } ) ;
443443
444+ it ( 'FIFO eviction: 17th provider evicts first provider cache entry' , async ( ) => {
445+ const BASE_URL = 'https://test-relayer.net/eviction' ;
446+
447+ // Register 17 unique /keyurl mocks
448+ for ( let n = 0 ; n < 17 ; n ++ ) {
449+ fetchMock . get (
450+ `${ BASE_URL } /provider-${ String ( n ) . padStart ( 2 , '0' ) } /keyurl` ,
451+ relayerV1ResponseGetKeyUrl ,
452+ ) ;
453+ }
454+
455+ // Create 17 providers
456+ const providers = Array . from ( { length : 17 } , ( _ , n ) =>
457+ createRelayerProvider (
458+ `${ BASE_URL } /provider-${ String ( n ) . padStart ( 2 , '0' ) } ` ,
459+ 1 ,
460+ ) ,
461+ ) ;
462+
463+ // Fetch all 17 — fills the cache to 16 and evicts provider-00 on provider-16
464+ for ( const provider of providers ) {
465+ await provider . fetchTFHEPkeParams ( ) ;
466+ }
467+
468+ expect ( mockTFHEPkeParamsFetch ) . toHaveBeenCalledTimes ( 17 ) ;
469+
470+ // provider-01 is still in cache (only provider-00 was evicted by provider-16)
471+ await providers [ 1 ] . fetchTFHEPkeParams ( ) ;
472+ expect ( mockTFHEPkeParamsFetch ) . toHaveBeenCalledTimes ( 17 ) ;
473+
474+ // Re-fetch provider-00 — it was evicted, so spy should be called again
475+ await providers [ 0 ] . fetchTFHEPkeParams ( ) ;
476+ expect ( mockTFHEPkeParamsFetch ) . toHaveBeenCalledTimes ( 18 ) ;
477+ } ) ;
478+
479+ it ( 'failed fetch restores evicted entry — cache size stays at 16' , async ( ) => {
480+ const BASE_URL = 'https://test-relayer.net/eviction-restore' ;
481+ let provider16CallCount = 0 ;
482+
483+ // Register successful mocks for provider-00..provider-15
484+ for ( let n = 0 ; n < 16 ; n ++ ) {
485+ fetchMock . get (
486+ `${ BASE_URL } /provider-${ String ( n ) . padStart ( 2 , '0' ) } /keyurl` ,
487+ relayerV1ResponseGetKeyUrl ,
488+ ) ;
489+ }
490+ // provider-16: first call fails, subsequent calls succeed
491+ fetchMock . get ( `${ BASE_URL } /provider-16/keyurl` , ( ) => {
492+ provider16CallCount ++ ;
493+ if ( provider16CallCount === 1 ) {
494+ return { status : 500 } ;
495+ }
496+ return relayerV1ResponseGetKeyUrl ;
497+ } ) ;
498+
499+ const providers = Array . from ( { length : 17 } , ( _ , n ) =>
500+ createRelayerProvider (
501+ `${ BASE_URL } /provider-${ String ( n ) . padStart ( 2 , '0' ) } ` ,
502+ 1 ,
503+ ) ,
504+ ) ;
505+
506+ // Fill cache with 16 entries (provider-00..provider-15)
507+ for ( let n = 0 ; n < 16 ; n ++ ) {
508+ await providers [ n ] . fetchTFHEPkeParams ( ) ;
509+ }
510+ expect ( mockTFHEPkeParamsFetch ) . toHaveBeenCalledTimes ( 16 ) ;
511+
512+ // Fetch provider-16 — evicts provider-00, but the keyurl request fails.
513+ // The evicted entry must be restored so the cache doesn't silently shrink.
514+ await expect ( providers [ 16 ] . fetchTFHEPkeParams ( ) ) . rejects . toThrow ( ) ;
515+ // TFHEPkeParams.fetch was never called for the failed fetch
516+ expect ( mockTFHEPkeParamsFetch ) . toHaveBeenCalledTimes ( 16 ) ;
517+
518+ // provider-00 must be restored in cache — cache hit, spy count stays at 16
519+ await providers [ 0 ] . fetchTFHEPkeParams ( ) ;
520+ expect ( mockTFHEPkeParamsFetch ) . toHaveBeenCalledTimes ( 16 ) ;
521+
522+ // provider-16 must NOT be in cache — retry triggers a network call
523+ await providers [ 16 ] . fetchTFHEPkeParams ( ) ;
524+ expect ( mockTFHEPkeParamsFetch ) . toHaveBeenCalledTimes ( 17 ) ;
525+ } ) ;
526+
444527 it ( 'caches separately for different relayer URLs' , async ( ) => {
445528 const testRelayerUrlV2 = TEST_CONFIG . v2 . fhevmInstanceConfig . relayerUrl ;
446529 let fetchCount1 = 0 ;
0 commit comments