@@ -642,6 +642,215 @@ describe('EventStore', () => {
642642 } ) ;
643643 } ) ;
644644
645+ describe ( 'toClientState mapping (via session_state_changed events)' , ( ) => {
646+ const sid = 'client-state-test' ;
647+
648+ beforeEach ( ( ) => {
649+ store . upsertSession ( { sessionId : sid } ) ;
650+ } ) ;
651+
652+ it ( 'maps CREATED to idle' , ( ) => {
653+ store . setSessionState ( sid , 'CREATED' , { force : true } ) ;
654+ const events = store . getSessionEvents ( sid ) ;
655+ const stateEvent = events . find ( ( e ) => e . type === 'session_state_changed' ) ;
656+ expect ( stateEvent ?. payload . state ) . toBe ( 'idle' ) ;
657+ expect ( stateEvent ?. payload . internalState ) . toBe ( 'CREATED' ) ;
658+ } ) ;
659+
660+ it ( 'maps STARTING to running' , ( ) => {
661+ store . setSessionState ( sid , 'STARTING' , { force : true } ) ;
662+ const events = store . getSessionEvents ( sid ) ;
663+ const stateEvent = events . find ( ( e ) => e . type === 'session_state_changed' ) ;
664+ expect ( stateEvent ?. payload . state ) . toBe ( 'running' ) ;
665+ } ) ;
666+
667+ it ( 'maps ACTIVE to running' , ( ) => {
668+ store . setSessionState ( sid , 'ACTIVE' , { force : true } ) ;
669+ const events = store . getSessionEvents ( sid ) ;
670+ const stateEvent = events . find ( ( e ) => e . type === 'session_state_changed' ) ;
671+ expect ( stateEvent ?. payload . state ) . toBe ( 'running' ) ;
672+ } ) ;
673+
674+ it ( 'maps DETACHED to idle' , ( ) => {
675+ store . setSessionState ( sid , 'DETACHED' , { force : true } ) ;
676+ const events = store . getSessionEvents ( sid ) ;
677+ const stateEvent = events . find ( ( e ) => e . type === 'session_state_changed' ) ;
678+ expect ( stateEvent ?. payload . state ) . toBe ( 'idle' ) ;
679+ } ) ;
680+
681+ it ( 'maps SUSPENDED to idle' , ( ) => {
682+ store . setSessionState ( sid , 'SUSPENDED' , { force : true } ) ;
683+ const events = store . getSessionEvents ( sid ) ;
684+ const stateEvent = events . find ( ( e ) => e . type === 'session_state_changed' ) ;
685+ expect ( stateEvent ?. payload . state ) . toBe ( 'idle' ) ;
686+ } ) ;
687+
688+ it ( 'maps CLOSING to idle' , ( ) => {
689+ store . setSessionState ( sid , 'CLOSING' , { force : true } ) ;
690+ const events = store . getSessionEvents ( sid ) ;
691+ const stateEvent = events . find ( ( e ) => e . type === 'session_state_changed' ) ;
692+ expect ( stateEvent ?. payload . state ) . toBe ( 'idle' ) ;
693+ } ) ;
694+
695+ it ( 'maps ENDED to idle' , ( ) => {
696+ store . setSessionState ( sid , 'ENDED' , { force : true } ) ;
697+ const events = store . getSessionEvents ( sid ) ;
698+ const stateEvent = events . find ( ( e ) => e . type === 'session_state_changed' ) ;
699+ expect ( stateEvent ?. payload . state ) . toBe ( 'idle' ) ;
700+ } ) ;
701+
702+ it ( 'includes timestamp in event payload' , ( ) => {
703+ const before = Date . now ( ) ;
704+ store . setSessionState ( sid , 'ACTIVE' , { force : true } ) ;
705+ const events = store . getSessionEvents ( sid ) ;
706+ const stateEvent = events . find ( ( e ) => e . type === 'session_state_changed' ) ;
707+ expect ( stateEvent ?. payload . timestamp ) . toBeGreaterThanOrEqual ( before ) ;
708+ } ) ;
709+ } ) ;
710+
711+ describe ( 'setSessionState syncs is_active' , ( ) => {
712+ const sid = 'is-active-sync-test' ;
713+
714+ beforeEach ( ( ) => {
715+ store . upsertSession ( { sessionId : sid } ) ;
716+ } ) ;
717+
718+ it ( 'sets is_active=true for ACTIVE' , ( ) => {
719+ store . setSessionState ( sid , 'ACTIVE' , { force : true } ) ;
720+ expect ( store . getSession ( sid ) ! . isActive ) . toBe ( true ) ;
721+ } ) ;
722+
723+ it ( 'sets is_active=true for DETACHED' , ( ) => {
724+ store . setSessionState ( sid , 'DETACHED' , { force : true } ) ;
725+ expect ( store . getSession ( sid ) ! . isActive ) . toBe ( true ) ;
726+ } ) ;
727+
728+ it ( 'sets is_active=true for SUSPENDED' , ( ) => {
729+ store . setSessionState ( sid , 'SUSPENDED' , { force : true } ) ;
730+ expect ( store . getSession ( sid ) ! . isActive ) . toBe ( true ) ;
731+ } ) ;
732+
733+ it ( 'sets is_active=false for ENDED' , ( ) => {
734+ store . setSessionState ( sid , 'ENDED' , { force : true } ) ;
735+ expect ( store . getSession ( sid ) ! . isActive ) . toBe ( false ) ;
736+ } ) ;
737+
738+ it ( 'sets is_active=false for CLOSING' , ( ) => {
739+ store . setSessionState ( sid , 'CLOSING' , { force : true } ) ;
740+ expect ( store . getSession ( sid ) ! . isActive ) . toBe ( false ) ;
741+ } ) ;
742+
743+ it ( 'sets is_active=true for CREATED' , ( ) => {
744+ store . setSessionState ( sid , 'CREATED' , { force : true } ) ;
745+ expect ( store . getSession ( sid ) ! . isActive ) . toBe ( true ) ;
746+ } ) ;
747+
748+ it ( 'sets is_active=true for STARTING' , ( ) => {
749+ store . setSessionState ( sid , 'STARTING' , { force : true } ) ;
750+ expect ( store . getSession ( sid ) ! . isActive ) . toBe ( true ) ;
751+ } ) ;
752+ } ) ;
753+
754+ describe ( 'recoverStaleSessions' , ( ) => {
755+ it ( 'transitions ACTIVE sessions to ENDED' , ( ) => {
756+ store . upsertSession ( { sessionId : 'active-1' } ) ;
757+ store . setSessionState ( 'active-1' , 'ACTIVE' , { force : true } ) ;
758+
759+ const count = store . recoverStaleSessions ( ) ;
760+
761+ expect ( count ) . toBe ( 1 ) ;
762+ expect ( store . getSessionState ( 'active-1' ) ) . toBe ( 'ENDED' ) ;
763+ } ) ;
764+
765+ it ( 'transitions STARTING sessions to ENDED' , ( ) => {
766+ store . upsertSession ( { sessionId : 'starting-1' } ) ;
767+ store . setSessionState ( 'starting-1' , 'STARTING' , { force : true } ) ;
768+
769+ store . recoverStaleSessions ( ) ;
770+
771+ expect ( store . getSessionState ( 'starting-1' ) ) . toBe ( 'ENDED' ) ;
772+ } ) ;
773+
774+ it ( 'transitions DETACHED sessions to ENDED' , ( ) => {
775+ store . upsertSession ( { sessionId : 'detached-1' } ) ;
776+ store . setSessionState ( 'detached-1' , 'DETACHED' , { force : true } ) ;
777+
778+ store . recoverStaleSessions ( ) ;
779+
780+ expect ( store . getSessionState ( 'detached-1' ) ) . toBe ( 'ENDED' ) ;
781+ } ) ;
782+
783+ it ( 'transitions SUSPENDED sessions to ENDED' , ( ) => {
784+ store . upsertSession ( { sessionId : 'suspended-1' } ) ;
785+ store . setSessionState ( 'suspended-1' , 'SUSPENDED' , { force : true } ) ;
786+
787+ store . recoverStaleSessions ( ) ;
788+
789+ expect ( store . getSessionState ( 'suspended-1' ) ) . toBe ( 'ENDED' ) ;
790+ } ) ;
791+
792+ it ( 'transitions CLOSING sessions to ENDED' , ( ) => {
793+ store . upsertSession ( { sessionId : 'closing-1' } ) ;
794+ store . setSessionState ( 'closing-1' , 'CLOSING' , { force : true } ) ;
795+
796+ store . recoverStaleSessions ( ) ;
797+
798+ expect ( store . getSessionState ( 'closing-1' ) ) . toBe ( 'ENDED' ) ;
799+ } ) ;
800+
801+ it ( 'does not touch ENDED sessions' , ( ) => {
802+ store . upsertSession ( { sessionId : 'ended-1' } ) ;
803+ store . setSessionState ( 'ended-1' , 'ENDED' , { force : true } ) ;
804+
805+ const count = store . recoverStaleSessions ( ) ;
806+
807+ expect ( count ) . toBe ( 0 ) ;
808+ expect ( store . getSessionState ( 'ended-1' ) ) . toBe ( 'ENDED' ) ;
809+ } ) ;
810+
811+ it ( 'does not touch CREATED sessions' , ( ) => {
812+ store . upsertSession ( { sessionId : 'created-1' } ) ;
813+ store . setSessionState ( 'created-1' , 'CREATED' , { force : true } ) ;
814+
815+ const count = store . recoverStaleSessions ( ) ;
816+
817+ expect ( count ) . toBe ( 0 ) ;
818+ expect ( store . getSessionState ( 'created-1' ) ) . toBe ( 'CREATED' ) ;
819+ } ) ;
820+
821+ it ( 'returns correct count for multiple stale sessions' , ( ) => {
822+ store . upsertSession ( { sessionId : 'stale-1' } ) ;
823+ store . upsertSession ( { sessionId : 'stale-2' } ) ;
824+ store . upsertSession ( { sessionId : 'ok-1' } ) ;
825+ store . setSessionState ( 'stale-1' , 'ACTIVE' , { force : true } ) ;
826+ store . setSessionState ( 'stale-2' , 'DETACHED' , { force : true } ) ;
827+ store . setSessionState ( 'ok-1' , 'ENDED' , { force : true } ) ;
828+
829+ const count = store . recoverStaleSessions ( ) ;
830+
831+ expect ( count ) . toBe ( 2 ) ;
832+ } ) ;
833+
834+ it ( 'emits session_state_changed events for recovered sessions' , ( ) => {
835+ store . upsertSession ( { sessionId : 'recover-1' } ) ;
836+ store . setSessionState ( 'recover-1' , 'ACTIVE' , { force : true } ) ;
837+
838+ // Clear events from setup
839+ const beforeCount = store . getSessionEvents ( 'recover-1' ) . length ;
840+
841+ store . recoverStaleSessions ( ) ;
842+
843+ const events = store . getSessionEvents ( 'recover-1' ) ;
844+ // Should have new session_state_changed event from recovery
845+ const recoveryEvent = events
846+ . slice ( beforeCount )
847+ . find ( ( e ) => e . type === 'session_state_changed' ) ;
848+ expect ( recoveryEvent ) . toBeDefined ( ) ;
849+ expect ( recoveryEvent ?. payload . state ) . toBe ( 'idle' ) ;
850+ expect ( recoveryEvent ?. payload . internalState ) . toBe ( 'ENDED' ) ;
851+ } ) ;
852+ } ) ;
853+
645854 describe ( 'close' , ( ) => {
646855 it ( 'is safe to call multiple times' , ( ) => {
647856 store . close ( ) ;
0 commit comments