@@ -326,5 +326,85 @@ public void DeviceReceivedHandlerThrowingDoesNotBreakCachePopulation()
326326 client . Stop ( ) ;
327327 }
328328 }
329+
330+ /// <summary>Pins the behaviour expressed by the test name: device received fires for all subscribers when one throws.</summary>
331+ [ Test ]
332+ public void DeviceReceivedFiresForAllSubscribersWhenOneThrows ( )
333+ {
334+ // Wire two subscribers: a throwing one FIRST, a recording one SECOND.
335+ // The recording subscriber must still receive every device because
336+ // RaiseDeviceReceived iterates the invocation list with per-delegate
337+ // try/catch, not a single try/catch over the multicast.
338+ var client = new MTConnectHttpClient ( Hostname , Port ) ;
339+
340+ client . DeviceReceived += ( _ , _ ) => throw new InvalidOperationException ( "first handler throws" ) ;
341+
342+ var recorded = new List < IDevice > ( ) ;
343+ var recordedLock = new object ( ) ;
344+ client . DeviceReceived += ( _ , device ) =>
345+ {
346+ lock ( recordedLock ) { recorded . Add ( device ) ; }
347+ } ;
348+
349+ using var probeSignal = new ManualResetEventSlim ( false ) ;
350+ client . ProbeReceived += ( _ , _ ) => probeSignal . Set ( ) ;
351+
352+ try
353+ {
354+ client . Start ( ) ;
355+
356+ Assert . That ( probeSignal . Wait ( ProbeWaitTimeoutMs ) , Is . True , "ProbeReceived did not fire within the timeout" ) ;
357+
358+ List < IDevice > snapshot ;
359+ lock ( recordedLock ) { snapshot = new List < IDevice > ( recorded ) ; }
360+
361+ Assert . That ( snapshot . Count , Is . EqualTo ( ExpectedDocumentEntryCount ) ,
362+ "subscribers after a throwing one must still be invoked" ) ;
363+ }
364+ finally
365+ {
366+ client . Stop ( ) ;
367+ }
368+ }
369+
370+ /// <summary>Pins the behaviour expressed by the test name: internal error handler throwing does not break device received fan out.</summary>
371+ [ Test ]
372+ public void InternalErrorHandlerThrowingDoesNotBreakDeviceReceivedFanOut ( )
373+ {
374+ // The fix routes swallowed DeviceReceived faults through InternalError.
375+ // If InternalError ITSELF throws, that secondary fault must also be
376+ // swallowed so the rest of the DeviceReceived fan-out continues.
377+ var client = new MTConnectHttpClient ( Hostname , Port ) ;
378+
379+ client . InternalError += ( _ , _ ) => throw new InvalidOperationException ( "InternalError throws" ) ;
380+ client . DeviceReceived += ( _ , _ ) => throw new InvalidOperationException ( "first DeviceReceived throws" ) ;
381+
382+ var recorded = new List < IDevice > ( ) ;
383+ var recordedLock = new object ( ) ;
384+ client . DeviceReceived += ( _ , device ) =>
385+ {
386+ lock ( recordedLock ) { recorded . Add ( device ) ; }
387+ } ;
388+
389+ using var probeSignal = new ManualResetEventSlim ( false ) ;
390+ client . ProbeReceived += ( _ , _ ) => probeSignal . Set ( ) ;
391+
392+ try
393+ {
394+ client . Start ( ) ;
395+
396+ Assert . That ( probeSignal . Wait ( ProbeWaitTimeoutMs ) , Is . True , "ProbeReceived did not fire within the timeout" ) ;
397+
398+ List < IDevice > snapshot ;
399+ lock ( recordedLock ) { snapshot = new List < IDevice > ( recorded ) ; }
400+
401+ Assert . That ( snapshot . Count , Is . EqualTo ( ExpectedDocumentEntryCount ) ,
402+ "InternalError throwing must not break the DeviceReceived fan-out" ) ;
403+ }
404+ finally
405+ {
406+ client . Stop ( ) ;
407+ }
408+ }
329409 }
330410}
0 commit comments