@@ -168,6 +168,28 @@ void filtersOnDeleteEvents() {
168168 verify (eventHandlerMock , never ()).handleEvent (any ());
169169 }
170170
171+ @ Test
172+ void deletePropagatesEventWhenTempCacheReturnsDeleteEvent () {
173+ var resource = testDeployment ();
174+ when (temporaryResourceCache .onDeleteEvent (resource , false ))
175+ .thenReturn (
176+ Optional .of (new GenericResourceEvent (ResourceAction .DELETED , resource , null , false )));
177+
178+ informerEventSource .onDelete (resource , false );
179+
180+ verify (eventHandlerMock , times (1 )).handleEvent (any ());
181+ }
182+
183+ @ Test
184+ void deleteDoesNotPropagateWhenTempCacheReturnsEmpty () {
185+ var resource = testDeployment ();
186+ when (temporaryResourceCache .onDeleteEvent (resource , false )).thenReturn (Optional .empty ());
187+
188+ informerEventSource .onDelete (resource , false );
189+
190+ verify (eventHandlerMock , never ()).handleEvent (any ());
191+ }
192+
171193 @ RepeatedTest (REPEAT_COUNT )
172194 void handlesPrevResourceVersionForUpdate () {
173195 withRealTemporaryResourceCache ();
@@ -187,13 +209,7 @@ void handlesPrevResourceVersionForUpdate() {
187209 void handlesPrevResourceVersionForUpdateInCaseOfException () {
188210 withRealTemporaryResourceCache ();
189211
190- CountDownLatch latch =
191- EventFilterTestUtils .sendForEventFilteringUpdate (
192- informerEventSource ,
193- testDeployment (),
194- r -> {
195- throw new KubernetesClientException ("fake" );
196- });
212+ CountDownLatch latch = sendForExceptionThrowingUpdate ();
197213 informerEventSource .onUpdate (
198214 deploymentWithResourceVersion (1 ), deploymentWithResourceVersion (2 ));
199215 latch .countDown ();
@@ -202,6 +218,99 @@ void handlesPrevResourceVersionForUpdateInCaseOfException() {
202218 expectNoActiveUpdates ();
203219 }
204220
221+ @ RepeatedTest (REPEAT_COUNT )
222+ void failedUpdate_withNoEventsDuringWindow_propagatesNothing () {
223+ // No event arrives between start and the thrown exception. doneEventFilterModify
224+ // sees an empty filter window with no own writes — summary must be empty.
225+ withRealTemporaryResourceCache ();
226+
227+ CountDownLatch latch = sendForExceptionThrowingUpdate ();
228+ latch .countDown ();
229+
230+ assertNoEventProduced ();
231+ expectNoActiveUpdates ();
232+ assertThat (temporaryResourceCache .getResources ()).isEmpty ();
233+ }
234+
235+ @ RepeatedTest (REPEAT_COUNT )
236+ void failedUpdate_withMultipleEventsDuringWindow_synthesizesSummary () {
237+ // Multiple foreign updates arrive while we are about to fail. Since no own write
238+ // happened, every related event is foreign and must be folded into one summary
239+ // event spanning first.previous → last.resource.
240+ withRealTemporaryResourceCache ();
241+
242+ CountDownLatch latch = sendForExceptionThrowingUpdate ();
243+ informerEventSource .onUpdate (
244+ deploymentWithResourceVersion (1 ), deploymentWithResourceVersion (2 ));
245+ informerEventSource .onUpdate (
246+ deploymentWithResourceVersion (2 ), deploymentWithResourceVersion (3 ));
247+ latch .countDown ();
248+
249+ expectHandleAddEvent (3 , 1 );
250+ expectNoActiveUpdates ();
251+ }
252+
253+ @ RepeatedTest (REPEAT_COUNT )
254+ void failedUpdate_withDeleteEventDuringWindow_propagatesDelete () {
255+ // delete arrives during the (failing) filter window — must surface as DELETE.
256+ withRealTemporaryResourceCache ();
257+
258+ CountDownLatch latch = sendForExceptionThrowingUpdate ();
259+ informerEventSource .onDelete (deploymentWithResourceVersion (2 ), false );
260+ latch .countDown ();
261+
262+ expectHandleDeleteEvent (2 );
263+ expectNoActiveUpdates ();
264+ }
265+
266+ @ RepeatedTest (REPEAT_COUNT )
267+ void failedUpdate_withUpdateThenDelete_propagatesDelete () {
268+ // Update followed by delete inside a failing filter window: last event is DELETE,
269+ // so the summary must surface the delete (not a synthesized update).
270+ withRealTemporaryResourceCache ();
271+
272+ CountDownLatch latch = sendForExceptionThrowingUpdate ();
273+ informerEventSource .onUpdate (
274+ deploymentWithResourceVersion (1 ), deploymentWithResourceVersion (2 ));
275+ informerEventSource .onDelete (deploymentWithResourceVersion (3 ), false );
276+ latch .countDown ();
277+
278+ expectHandleDeleteEvent (3 );
279+ expectNoActiveUpdates ();
280+ }
281+
282+ @ RepeatedTest (REPEAT_COUNT )
283+ void failedUpdate_doesNotPopulateTempCache () {
284+ // putResource is only called from handleRecentResourceUpdate, which never runs
285+ // when updateMethod throws. The temp cache must therefore stay empty.
286+ withRealTemporaryResourceCache ();
287+
288+ CountDownLatch latch = sendForExceptionThrowingUpdate ();
289+ informerEventSource .onUpdate (
290+ deploymentWithResourceVersion (1 ), deploymentWithResourceVersion (2 ));
291+ latch .countDown ();
292+
293+ expectHandleAddEvent (2 , 1 );
294+ expectNoActiveUpdates ();
295+ assertThat (temporaryResourceCache .getResources ()).isEmpty ();
296+ }
297+
298+ @ RepeatedTest (REPEAT_COUNT )
299+ void eventReceivedAfterFailedUpdate_isPropagatedNormally () {
300+ // After the exception unwinds and the filter window is fully closed, subsequent
301+ // events must propagate via the regular non-filtered path.
302+ withRealTemporaryResourceCache ();
303+
304+ CountDownLatch latch = sendForExceptionThrowingUpdate ();
305+ latch .countDown ();
306+ expectNoActiveUpdates ();
307+
308+ informerEventSource .onUpdate (
309+ deploymentWithResourceVersion (1 ), deploymentWithResourceVersion (2 ));
310+
311+ expectHandleAddEvent (2 , 1 );
312+ }
313+
205314 @ RepeatedTest (REPEAT_COUNT )
206315 void handlesPrevResourceVersionForUpdateInCaseOfMultipleUpdates () {
207316 withRealTemporaryResourceCache ();
@@ -597,6 +706,15 @@ private CountDownLatch sendForEventFilteringUpdate(Deployment deployment, int re
597706 informerEventSource , deployment , r -> withResourceVersion (deployment , resourceVersion ));
598707 }
599708
709+ private CountDownLatch sendForExceptionThrowingUpdate () {
710+ return EventFilterTestUtils .sendForEventFilteringUpdate (
711+ informerEventSource ,
712+ testDeployment (),
713+ r -> {
714+ throw new KubernetesClientException ("fake" );
715+ });
716+ }
717+
600718 private void withRealTemporaryResourceCache () {
601719 var mes = mock (ManagedInformerEventSource .class );
602720 var mim = mock (InformerManager .class );
0 commit comments