@@ -548,4 +548,263 @@ describe("todo-continuation-enforcer", () => {
548548 // #then - no continuation (abort error detected)
549549 expect ( promptCalls ) . toHaveLength ( 0 )
550550 } )
551+
552+ test ( "should skip injection when abort detected via session.error event (event-based, primary)" , async ( ) => {
553+ // #given - session with incomplete todos
554+ const sessionID = "main-event-abort"
555+ setMainSession ( sessionID )
556+ mockMessages = [
557+ { info : { id : "msg-1" , role : "user" } } ,
558+ { info : { id : "msg-2" , role : "assistant" } } ,
559+ ]
560+
561+ const hook = createTodoContinuationEnforcer ( createMockPluginInput ( ) , { } )
562+
563+ // #when - abort error event fires
564+ await hook . handler ( {
565+ event : {
566+ type : "session.error" ,
567+ properties : { sessionID, error : { name : "MessageAbortedError" } } ,
568+ } ,
569+ } )
570+
571+ // #when - session goes idle immediately after
572+ await hook . handler ( {
573+ event : { type : "session.idle" , properties : { sessionID } } ,
574+ } )
575+
576+ await new Promise ( r => setTimeout ( r , 3000 ) )
577+
578+ // #then - no continuation (abort detected via event)
579+ expect ( promptCalls ) . toHaveLength ( 0 )
580+ } )
581+
582+ test ( "should skip injection when AbortError detected via session.error event" , async ( ) => {
583+ // #given - session with incomplete todos
584+ const sessionID = "main-event-abort-dom"
585+ setMainSession ( sessionID )
586+ mockMessages = [
587+ { info : { id : "msg-1" , role : "user" } } ,
588+ { info : { id : "msg-2" , role : "assistant" } } ,
589+ ]
590+
591+ const hook = createTodoContinuationEnforcer ( createMockPluginInput ( ) , { } )
592+
593+ // #when - AbortError event fires
594+ await hook . handler ( {
595+ event : {
596+ type : "session.error" ,
597+ properties : { sessionID, error : { name : "AbortError" } } ,
598+ } ,
599+ } )
600+
601+ // #when - session goes idle
602+ await hook . handler ( {
603+ event : { type : "session.idle" , properties : { sessionID } } ,
604+ } )
605+
606+ await new Promise ( r => setTimeout ( r , 3000 ) )
607+
608+ // #then - no continuation (abort detected via event)
609+ expect ( promptCalls ) . toHaveLength ( 0 )
610+ } )
611+
612+ test ( "should inject when abort flag is stale (>3s old)" , async ( ) => {
613+ // #given - session with incomplete todos and old abort timestamp
614+ const sessionID = "main-stale-abort"
615+ setMainSession ( sessionID )
616+ mockMessages = [
617+ { info : { id : "msg-1" , role : "user" } } ,
618+ { info : { id : "msg-2" , role : "assistant" } } ,
619+ ]
620+
621+ const hook = createTodoContinuationEnforcer ( createMockPluginInput ( ) , { } )
622+
623+ // #when - abort error fires
624+ await hook . handler ( {
625+ event : {
626+ type : "session.error" ,
627+ properties : { sessionID, error : { name : "MessageAbortedError" } } ,
628+ } ,
629+ } )
630+
631+ // #when - wait >3s then idle fires
632+ await new Promise ( r => setTimeout ( r , 3100 ) )
633+
634+ await hook . handler ( {
635+ event : { type : "session.idle" , properties : { sessionID } } ,
636+ } )
637+
638+ await new Promise ( r => setTimeout ( r , 3000 ) )
639+
640+ // #then - continuation injected (abort flag is stale)
641+ expect ( promptCalls . length ) . toBeGreaterThan ( 0 )
642+ } , 10000 )
643+
644+ test ( "should clear abort flag on user message activity" , async ( ) => {
645+ // #given - session with abort detected
646+ const sessionID = "main-clear-on-user"
647+ setMainSession ( sessionID )
648+ mockMessages = [
649+ { info : { id : "msg-1" , role : "user" } } ,
650+ { info : { id : "msg-2" , role : "assistant" } } ,
651+ ]
652+
653+ const hook = createTodoContinuationEnforcer ( createMockPluginInput ( ) , { } )
654+
655+ // #when - abort error fires
656+ await hook . handler ( {
657+ event : {
658+ type : "session.error" ,
659+ properties : { sessionID, error : { name : "MessageAbortedError" } } ,
660+ } ,
661+ } )
662+
663+ // #when - user sends new message (clears abort flag)
664+ await new Promise ( r => setTimeout ( r , 600 ) )
665+ await hook . handler ( {
666+ event : {
667+ type : "message.updated" ,
668+ properties : { info : { sessionID, role : "user" } } ,
669+ } ,
670+ } )
671+
672+ // #when - session goes idle
673+ await hook . handler ( {
674+ event : { type : "session.idle" , properties : { sessionID } } ,
675+ } )
676+
677+ await new Promise ( r => setTimeout ( r , 3000 ) )
678+
679+ // #then - continuation injected (abort flag was cleared by user activity)
680+ expect ( promptCalls . length ) . toBeGreaterThan ( 0 )
681+ } )
682+
683+ test ( "should clear abort flag on assistant message activity" , async ( ) => {
684+ // #given - session with abort detected
685+ const sessionID = "main-clear-on-assistant"
686+ setMainSession ( sessionID )
687+ mockMessages = [
688+ { info : { id : "msg-1" , role : "user" } } ,
689+ { info : { id : "msg-2" , role : "assistant" } } ,
690+ ]
691+
692+ const hook = createTodoContinuationEnforcer ( createMockPluginInput ( ) , { } )
693+
694+ // #when - abort error fires
695+ await hook . handler ( {
696+ event : {
697+ type : "session.error" ,
698+ properties : { sessionID, error : { name : "MessageAbortedError" } } ,
699+ } ,
700+ } )
701+
702+ // #when - assistant starts responding (clears abort flag)
703+ await hook . handler ( {
704+ event : {
705+ type : "message.updated" ,
706+ properties : { info : { sessionID, role : "assistant" } } ,
707+ } ,
708+ } )
709+
710+ // #when - session goes idle
711+ await hook . handler ( {
712+ event : { type : "session.idle" , properties : { sessionID } } ,
713+ } )
714+
715+ await new Promise ( r => setTimeout ( r , 3000 ) )
716+
717+ // #then - continuation injected (abort flag was cleared by assistant activity)
718+ expect ( promptCalls . length ) . toBeGreaterThan ( 0 )
719+ } )
720+
721+ test ( "should clear abort flag on tool execution" , async ( ) => {
722+ // #given - session with abort detected
723+ const sessionID = "main-clear-on-tool"
724+ setMainSession ( sessionID )
725+ mockMessages = [
726+ { info : { id : "msg-1" , role : "user" } } ,
727+ { info : { id : "msg-2" , role : "assistant" } } ,
728+ ]
729+
730+ const hook = createTodoContinuationEnforcer ( createMockPluginInput ( ) , { } )
731+
732+ // #when - abort error fires
733+ await hook . handler ( {
734+ event : {
735+ type : "session.error" ,
736+ properties : { sessionID, error : { name : "MessageAbortedError" } } ,
737+ } ,
738+ } )
739+
740+ // #when - tool executes (clears abort flag)
741+ await hook . handler ( {
742+ event : {
743+ type : "tool.execute.before" ,
744+ properties : { sessionID } ,
745+ } ,
746+ } )
747+
748+ // #when - session goes idle
749+ await hook . handler ( {
750+ event : { type : "session.idle" , properties : { sessionID } } ,
751+ } )
752+
753+ await new Promise ( r => setTimeout ( r , 3000 ) )
754+
755+ // #then - continuation injected (abort flag was cleared by tool execution)
756+ expect ( promptCalls . length ) . toBeGreaterThan ( 0 )
757+ } )
758+
759+ test ( "should use event-based detection even when API indicates no abort (event wins)" , async ( ) => {
760+ // #given - session with abort event but API shows no error
761+ const sessionID = "main-event-wins"
762+ setMainSession ( sessionID )
763+ mockMessages = [
764+ { info : { id : "msg-1" , role : "user" } } ,
765+ { info : { id : "msg-2" , role : "assistant" } } ,
766+ ]
767+
768+ const hook = createTodoContinuationEnforcer ( createMockPluginInput ( ) , { } )
769+
770+ // #when - abort error event fires (but API doesn't have it yet)
771+ await hook . handler ( {
772+ event : {
773+ type : "session.error" ,
774+ properties : { sessionID, error : { name : "MessageAbortedError" } } ,
775+ } ,
776+ } )
777+
778+ // #when - session goes idle
779+ await hook . handler ( {
780+ event : { type : "session.idle" , properties : { sessionID } } ,
781+ } )
782+
783+ await new Promise ( r => setTimeout ( r , 3000 ) )
784+
785+ // #then - no continuation (event-based detection wins over API)
786+ expect ( promptCalls ) . toHaveLength ( 0 )
787+ } )
788+
789+ test ( "should use API fallback when event is missed but API shows abort" , async ( ) => {
790+ // #given - session where event was missed but API shows abort
791+ const sessionID = "main-api-fallback"
792+ setMainSession ( sessionID )
793+ mockMessages = [
794+ { info : { id : "msg-1" , role : "user" } } ,
795+ { info : { id : "msg-2" , role : "assistant" , error : { name : "MessageAbortedError" } } } ,
796+ ]
797+
798+ const hook = createTodoContinuationEnforcer ( createMockPluginInput ( ) , { } )
799+
800+ // #when - session goes idle without prior session.error event
801+ await hook . handler ( {
802+ event : { type : "session.idle" , properties : { sessionID } } ,
803+ } )
804+
805+ await new Promise ( r => setTimeout ( r , 3000 ) )
806+
807+ // #then - no continuation (API fallback detected the abort)
808+ expect ( promptCalls ) . toHaveLength ( 0 )
809+ } )
551810} )
0 commit comments