@@ -4012,6 +4012,92 @@ test('getBatchRetryContainers should match raw containers by fallback fullName w
40124012 expect ( trigger . batchRetryBuffer . get ( 'undefined_container1' ) ) . toBe ( currentContainer ) ;
40134013} ) ;
40144014
4015+ test ( 'getBatchRetryContainers should evict stale retry-buffer entries before reuse' , ( ) => {
4016+ trigger . configuration = {
4017+ threshold : 'all' ,
4018+ once : true ,
4019+ mode : 'batch' ,
4020+ } ;
4021+
4022+ const currentContainer = {
4023+ id : 'stale-id' ,
4024+ watcher : 'local' ,
4025+ name : 'container1' ,
4026+ updateAvailable : true ,
4027+ updateKind : { kind : 'tag' , semverDiff : 'major' } ,
4028+ } as any ;
4029+
4030+ trigger . batchRetryBuffer . set ( 'stale-id' , currentContainer ) ;
4031+ ( trigger as any ) . batchRetryBufferUpdatedAt = new Map ( [ [ 'stale-id' , 1_000 ] ] ) ;
4032+ ( trigger as any ) . bufferEntryRetentionMs = 100 ;
4033+ storeContainer . getContainersRaw . mockReturnValue ( [ currentContainer ] ) ;
4034+ const nowSpy = vi . spyOn ( Date , 'now' ) . mockReturnValue ( 1_101 ) ;
4035+
4036+ try {
4037+ const retryContainers = ( trigger as any ) . getBatchRetryContainers ( [ ] ) ;
4038+
4039+ expect ( retryContainers ) . toEqual ( [ ] ) ;
4040+ expect ( trigger . batchRetryBuffer . size ) . toBe ( 0 ) ;
4041+ } finally {
4042+ nowSpy . mockRestore ( ) ;
4043+ }
4044+ } ) ;
4045+
4046+ test ( 'handleContainerReports should cap retry-buffer growth by evicting the oldest entries' , async ( ) => {
4047+ trigger . configuration = {
4048+ threshold : 'all' ,
4049+ once : true ,
4050+ mode : 'batch' ,
4051+ } ;
4052+ ( trigger as any ) . batchRetryBufferMaxEntries = 2 ;
4053+ trigger . triggerBatch = vi . fn ( ) . mockRejectedValue ( new Error ( 'SMTP timeout' ) ) ;
4054+
4055+ const nowSpy = vi
4056+ . spyOn ( Date , 'now' )
4057+ . mockReturnValueOnce ( 1_000 )
4058+ . mockReturnValueOnce ( 1_001 )
4059+ . mockReturnValueOnce ( 1_002 ) ;
4060+
4061+ try {
4062+ await trigger . handleContainerReports ( [
4063+ {
4064+ changed : true ,
4065+ container : {
4066+ id : 'c1' ,
4067+ name : 'c1' ,
4068+ watcher : 'local' ,
4069+ updateAvailable : true ,
4070+ updateKind : { kind : 'tag' , semverDiff : 'major' } ,
4071+ } ,
4072+ } ,
4073+ {
4074+ changed : true ,
4075+ container : {
4076+ id : 'c2' ,
4077+ name : 'c2' ,
4078+ watcher : 'local' ,
4079+ updateAvailable : true ,
4080+ updateKind : { kind : 'tag' , semverDiff : 'major' } ,
4081+ } ,
4082+ } ,
4083+ {
4084+ changed : true ,
4085+ container : {
4086+ id : 'c3' ,
4087+ name : 'c3' ,
4088+ watcher : 'local' ,
4089+ updateAvailable : true ,
4090+ updateKind : { kind : 'tag' , semverDiff : 'major' } ,
4091+ } ,
4092+ } ,
4093+ ] as any ) ;
4094+
4095+ expect ( [ ...trigger . batchRetryBuffer . keys ( ) ] ) . toEqual ( [ 'c2' , 'c3' ] ) ;
4096+ } finally {
4097+ nowSpy . mockRestore ( ) ;
4098+ }
4099+ } ) ;
4100+
40154101test ( 'handleContainerReports should use fallback fullName keys for retry and digest cleanup when notification keys are missing' , async ( ) => {
40164102 trigger . configuration = {
40174103 threshold : 'all' ,
@@ -4370,6 +4456,37 @@ describe('digest mode', () => {
43704456 triggerBatchSpy . mockRestore ( ) ;
43714457 } ) ;
43724458
4459+ test ( 'bufferContainerForDigest should cap digest-buffer growth by evicting the oldest entries' , ( ) => {
4460+ ( trigger as any ) . digestBufferMaxEntries = 2 ;
4461+ const nowSpy = vi
4462+ . spyOn ( Date , 'now' )
4463+ . mockReturnValueOnce ( 1_000 )
4464+ . mockReturnValueOnce ( 1_001 )
4465+ . mockReturnValueOnce ( 1_002 ) ;
4466+
4467+ try {
4468+ ( trigger as any ) . bufferContainerForDigest ( {
4469+ id : 'c1' ,
4470+ name : 'app-1' ,
4471+ watcher : 'test' ,
4472+ } ) ;
4473+ ( trigger as any ) . bufferContainerForDigest ( {
4474+ id : 'c2' ,
4475+ name : 'app-2' ,
4476+ watcher : 'test' ,
4477+ } ) ;
4478+ ( trigger as any ) . bufferContainerForDigest ( {
4479+ id : 'c3' ,
4480+ name : 'app-3' ,
4481+ watcher : 'test' ,
4482+ } ) ;
4483+
4484+ expect ( [ ...trigger . digestBuffer . keys ( ) ] ) . toEqual ( [ 'c2' , 'c3' ] ) ;
4485+ } finally {
4486+ nowSpy . mockRestore ( ) ;
4487+ }
4488+ } ) ;
4489+
43734490 test ( 'handleContainerReportDigest should return early when report is not eligible for simple handling' , async ( ) => {
43744491 await trigger . register ( 'trigger' , 'test' , 'digest-trigger' , {
43754492 ...configurationValid ,
@@ -4560,6 +4677,36 @@ describe('digest mode', () => {
45604677 triggerBatchSpy . mockRestore ( ) ;
45614678 } ) ;
45624679
4680+ test ( 'flushDigestBuffer should evict stale buffered entries before dispatch' , async ( ) => {
4681+ await trigger . register ( 'trigger' , 'test' , 'digest-trigger' , {
4682+ ...configurationValid ,
4683+ mode : 'digest' ,
4684+ } ) ;
4685+
4686+ const staleContainer = {
4687+ id : 'c1' ,
4688+ name : 'app' ,
4689+ watcher : 'test' ,
4690+ updateAvailable : true ,
4691+ updateKind : { kind : 'tag' , localValue : '1.0' , remoteValue : '2.0' } ,
4692+ } as any ;
4693+ trigger . digestBuffer . set ( 'c1' , staleContainer ) ;
4694+ ( trigger as any ) . digestBufferUpdatedAt = new Map ( [ [ 'c1' , 1_000 ] ] ) ;
4695+ ( trigger as any ) . bufferEntryRetentionMs = 100 ;
4696+ const nowSpy = vi . spyOn ( Date , 'now' ) . mockReturnValue ( 1_101 ) ;
4697+
4698+ try {
4699+ const triggerBatchSpy = vi . spyOn ( trigger , 'triggerBatch' ) . mockResolvedValue ( undefined ) ;
4700+ await trigger . flushDigestBuffer ( ) ;
4701+
4702+ expect ( trigger . digestBuffer . size ) . toBe ( 0 ) ;
4703+ expect ( triggerBatchSpy ) . not . toHaveBeenCalled ( ) ;
4704+ triggerBatchSpy . mockRestore ( ) ;
4705+ } finally {
4706+ nowSpy . mockRestore ( ) ;
4707+ }
4708+ } ) ;
4709+
45634710 test ( 'flushDigestBuffer should return early when a digest flush is already in progress' , async ( ) => {
45644711 await trigger . register ( 'trigger' , 'test' , 'digest-trigger' , {
45654712 ...configurationValid ,
0 commit comments