@@ -376,6 +376,165 @@ void testConcurrentDispatchFromMultipleThreads() throws Exception {
376376 }
377377
378378 // Helper methods to dispatch events using reflection
379+ // ====================================================================
380+ // EventErrorHandler tests
381+ // ====================================================================
382+
383+ @ Test
384+ void testDefaultErrorHandlerLogsException () {
385+ // Without a custom error handler, exceptions should be logged (no crash)
386+ Logger sessionLogger = Logger .getLogger (CopilotSession .class .getName ());
387+ Level originalLevel = sessionLogger .getLevel ();
388+ sessionLogger .setLevel (Level .OFF );
389+
390+ try {
391+ session .on (AssistantMessageEvent .class , msg -> {
392+ throw new RuntimeException ("boom" );
393+ });
394+
395+ assertDoesNotThrow (() -> dispatchEvent (createAssistantMessageEvent ("Test" )));
396+ } finally {
397+ sessionLogger .setLevel (originalLevel );
398+ }
399+ }
400+
401+ @ Test
402+ void testCustomEventErrorHandlerReceivesEventAndException () {
403+ var capturedEvents = new ArrayList <AbstractSessionEvent >();
404+ var capturedExceptions = new ArrayList <Exception >();
405+
406+ session .setEventErrorHandler ((event , exception ) -> {
407+ capturedEvents .add (event );
408+ capturedExceptions .add (exception );
409+ });
410+
411+ var thrownException = new RuntimeException ("test error" );
412+ session .on (AssistantMessageEvent .class , msg -> {
413+ throw thrownException ;
414+ });
415+
416+ var event = createAssistantMessageEvent ("Hello" );
417+ dispatchEvent (event );
418+
419+ assertEquals (1 , capturedEvents .size ());
420+ assertSame (event , capturedEvents .get (0 ));
421+ assertEquals (1 , capturedExceptions .size ());
422+ assertSame (thrownException , capturedExceptions .get (0 ));
423+ }
424+
425+ @ Test
426+ void testCustomErrorHandlerReplacesDefaultLogging () {
427+ var errorCount = new AtomicInteger (0 );
428+
429+ session .setEventErrorHandler ((event , exception ) -> {
430+ errorCount .incrementAndGet ();
431+ });
432+
433+ session .on (AssistantMessageEvent .class , msg -> {
434+ throw new RuntimeException ("error 1" );
435+ });
436+ session .on (AssistantMessageEvent .class , msg -> {
437+ throw new RuntimeException ("error 2" );
438+ });
439+
440+ dispatchEvent (createAssistantMessageEvent ("Test" ));
441+
442+ // Both handler errors should be reported to the custom error handler
443+ assertEquals (2 , errorCount .get ());
444+ }
445+
446+ @ Test
447+ void testErrorHandlerItselfThrowingDoesNotBreakDispatch () {
448+ var received = new ArrayList <String >();
449+
450+ Logger sessionLogger = Logger .getLogger (CopilotSession .class .getName ());
451+ Level originalLevel = sessionLogger .getLevel ();
452+ sessionLogger .setLevel (Level .OFF );
453+
454+ try {
455+ session .setEventErrorHandler ((event , exception ) -> {
456+ throw new RuntimeException ("error handler also broke" );
457+ });
458+
459+ // First handler throws
460+ session .on (AssistantMessageEvent .class , msg -> {
461+ throw new RuntimeException ("handler error" );
462+ });
463+
464+ // Second handler should still execute
465+ session .on (AssistantMessageEvent .class , msg -> {
466+ received .add (msg .getData ().getContent ());
467+ });
468+
469+ assertDoesNotThrow (() -> dispatchEvent (createAssistantMessageEvent ("Still works" )));
470+ assertEquals (1 , received .size ());
471+ assertEquals ("Still works" , received .get (0 ));
472+ } finally {
473+ sessionLogger .setLevel (originalLevel );
474+ }
475+ }
476+
477+ @ Test
478+ void testSetEventErrorHandlerToNullRestoresDefaultBehavior () {
479+ var errorCount = new AtomicInteger (0 );
480+
481+ // Set custom handler
482+ session .setEventErrorHandler ((event , exception ) -> {
483+ errorCount .incrementAndGet ();
484+ });
485+
486+ session .on (AssistantMessageEvent .class , msg -> {
487+ throw new RuntimeException ("error" );
488+ });
489+
490+ dispatchEvent (createAssistantMessageEvent ("Test1" ));
491+ assertEquals (1 , errorCount .get ());
492+
493+ // Reset to null (restore default logging)
494+ session .setEventErrorHandler (null );
495+
496+ // Suppress default logging for the next dispatch
497+ Logger sessionLogger = Logger .getLogger (CopilotSession .class .getName ());
498+ Level originalLevel = sessionLogger .getLevel ();
499+ sessionLogger .setLevel (Level .OFF );
500+
501+ try {
502+ dispatchEvent (createAssistantMessageEvent ("Test2" ));
503+ } finally {
504+ sessionLogger .setLevel (originalLevel );
505+ }
506+
507+ // Custom handler should NOT have been called again
508+ assertEquals (1 , errorCount .get ());
509+ }
510+
511+ @ Test
512+ void testErrorHandlerReceivesCorrectEventType () {
513+ var capturedEvents = new ArrayList <AbstractSessionEvent >();
514+
515+ session .setEventErrorHandler ((event , exception ) -> {
516+ capturedEvents .add (event );
517+ });
518+
519+ session .on (event -> {
520+ throw new RuntimeException ("always fails" );
521+ });
522+
523+ var msgEvent = createAssistantMessageEvent ("msg" );
524+ var idleEvent = createSessionIdleEvent ();
525+
526+ dispatchEvent (msgEvent );
527+ dispatchEvent (idleEvent );
528+
529+ assertEquals (2 , capturedEvents .size ());
530+ assertInstanceOf (AssistantMessageEvent .class , capturedEvents .get (0 ));
531+ assertInstanceOf (SessionIdleEvent .class , capturedEvents .get (1 ));
532+ }
533+
534+ // ====================================================================
535+ // Helper methods
536+ // ====================================================================
537+
379538 private void dispatchEvent (AbstractSessionEvent event ) {
380539 try {
381540 Method dispatchMethod = CopilotSession .class .getDeclaredMethod ("dispatchEvent" , AbstractSessionEvent .class );
0 commit comments