@@ -595,3 +595,219 @@ def test_fix_broken_tool_use_does_not_affect_normal_conversations(session_manage
595595
596596 # Should remain unchanged
597597 assert fixed_messages == messages
598+
599+
600+ # ============================================================================
601+ # Conditional Sync Tests
602+ # ============================================================================
603+
604+
605+ def test_sync_agent_skips_update_when_state_not_dirty_and_internal_state_unchanged (mock_repository ):
606+ """Test that sync_agent() skips update_agent() when state is not dirty and internal state unchanged."""
607+ session_manager = RepositorySessionManager (session_id = "test-session" , session_repository = mock_repository )
608+
609+ # Create and initialize agent
610+ agent = Agent (agent_id = "test-agent" , session_manager = session_manager )
611+
612+ # Track update_agent calls
613+ update_agent_calls = []
614+ original_update_agent = mock_repository .update_agent
615+
616+ def tracking_update_agent (session_id , session_agent ):
617+ update_agent_calls .append ((session_id , session_agent ))
618+ return original_update_agent (session_id , session_agent )
619+
620+ mock_repository .update_agent = tracking_update_agent
621+
622+ # First sync should update (to establish baseline)
623+ session_manager .sync_agent (agent )
624+ assert len (update_agent_calls ) == 1
625+
626+ # Clear tracking
627+ update_agent_calls .clear ()
628+
629+ # Second sync without changes should skip update
630+ session_manager .sync_agent (agent )
631+ assert len (update_agent_calls ) == 0
632+
633+
634+ def test_sync_agent_calls_update_when_state_is_dirty (mock_repository ):
635+ """Test that sync_agent() calls update_agent() when agent.state is dirty."""
636+ session_manager = RepositorySessionManager (session_id = "test-session" , session_repository = mock_repository )
637+
638+ # Create and initialize agent
639+ agent = Agent (agent_id = "test-agent" , session_manager = session_manager )
640+
641+ # Track update_agent calls
642+ update_agent_calls = []
643+ original_update_agent = mock_repository .update_agent
644+
645+ def tracking_update_agent (session_id , session_agent ):
646+ update_agent_calls .append ((session_id , session_agent ))
647+ return original_update_agent (session_id , session_agent )
648+
649+ mock_repository .update_agent = tracking_update_agent
650+
651+ # First sync to establish baseline
652+ session_manager .sync_agent (agent )
653+ update_agent_calls .clear ()
654+
655+ # Modify state (makes it dirty)
656+ agent .state .set ("key" , "value" )
657+
658+ # Sync should call update_agent because state is dirty
659+ session_manager .sync_agent (agent )
660+ assert len (update_agent_calls ) == 1
661+
662+
663+ def test_sync_agent_calls_update_when_internal_state_changed (mock_repository ):
664+ """Test that sync_agent() calls update_agent() when internal state (interrupt_state) is dirty."""
665+ session_manager = RepositorySessionManager (session_id = "test-session" , session_repository = mock_repository )
666+
667+ # Create and initialize agent
668+ agent = Agent (agent_id = "test-agent" , session_manager = session_manager )
669+
670+ # Track update_agent calls
671+ update_agent_calls = []
672+ original_update_agent = mock_repository .update_agent
673+
674+ def tracking_update_agent (session_id , session_agent ):
675+ update_agent_calls .append ((session_id , session_agent ))
676+ return original_update_agent (session_id , session_agent )
677+
678+ mock_repository .update_agent = tracking_update_agent
679+
680+ # First sync to establish baseline
681+ session_manager .sync_agent (agent )
682+ update_agent_calls .clear ()
683+
684+ # Modify internal state (activate interrupt state which sets dirty flag)
685+ agent ._interrupt_state .activate ()
686+
687+ # Sync should call update_agent because internal state is dirty
688+ session_manager .sync_agent (agent )
689+ assert len (update_agent_calls ) == 1
690+
691+
692+ def test_sync_agent_calls_update_when_conversation_manager_state_changed (mock_repository ):
693+ """Test that sync_agent() calls update_agent() when conversation manager state changed."""
694+ session_manager = RepositorySessionManager (session_id = "test-session" , session_repository = mock_repository )
695+
696+ # Create and initialize agent
697+ agent = Agent (agent_id = "test-agent" , session_manager = session_manager )
698+
699+ # Track update_agent calls
700+ update_agent_calls = []
701+ original_update_agent = mock_repository .update_agent
702+
703+ def tracking_update_agent (session_id , session_agent ):
704+ update_agent_calls .append ((session_id , session_agent ))
705+ return original_update_agent (session_id , session_agent )
706+
707+ mock_repository .update_agent = tracking_update_agent
708+
709+ # First sync to establish baseline
710+ session_manager .sync_agent (agent )
711+ update_agent_calls .clear ()
712+
713+ # Modify conversation manager state
714+ agent .conversation_manager .removed_message_count = 5
715+
716+ # Sync should call update_agent because conversation manager state changed
717+ session_manager .sync_agent (agent )
718+ assert len (update_agent_calls ) == 1
719+
720+
721+ def test_sync_agent_tracks_version_after_successful_sync (mock_repository ):
722+ """Test that sync_agent() tracks version after successful sync."""
723+ session_manager = RepositorySessionManager (session_id = "test-session" , session_repository = mock_repository )
724+
725+ # Create and initialize agent
726+ agent = Agent (agent_id = "test-agent" , session_manager = session_manager )
727+
728+ # First sync to establish baseline
729+ session_manager .sync_agent (agent )
730+ initial_version = agent .state ._get_version ()
731+
732+ # Modify state (increments version)
733+ agent .state .set ("key" , "value" )
734+ assert agent .state ._get_version () == initial_version + 1
735+
736+ # Track update_agent calls
737+ update_agent_calls = []
738+ original_update_agent = mock_repository .update_agent
739+
740+ def tracking_update_agent (session_id , session_agent ):
741+ update_agent_calls .append ((session_id , session_agent ))
742+ return original_update_agent (session_id , session_agent )
743+
744+ mock_repository .update_agent = tracking_update_agent
745+
746+ # Sync should update because version changed
747+ session_manager .sync_agent (agent )
748+ assert len (update_agent_calls ) == 1
749+
750+ # Second sync without changes should skip
751+ update_agent_calls .clear ()
752+ session_manager .sync_agent (agent )
753+ assert len (update_agent_calls ) == 0
754+
755+
756+ def test_sync_agent_retries_on_failure (mock_repository ):
757+ """Test that sync_agent() retries on next call if update_agent() fails."""
758+ session_manager = RepositorySessionManager (session_id = "test-session" , session_repository = mock_repository )
759+
760+ # Create and initialize agent
761+ agent = Agent (agent_id = "test-agent" , session_manager = session_manager )
762+
763+ # First sync to establish baseline
764+ session_manager .sync_agent (agent )
765+
766+ # Modify state (increments version)
767+ agent .state .set ("key" , "value" )
768+
769+ # Make update_agent fail
770+ def failing_update_agent (session_id , session_agent ):
771+ raise SessionException ("Update failed" )
772+
773+ mock_repository .update_agent = failing_update_agent
774+
775+ # Sync should fail
776+ with pytest .raises (SessionException , match = "Update failed" ):
777+ session_manager .sync_agent (agent )
778+
779+ # Restore working update_agent
780+ update_agent_calls = []
781+ original_update_agent = MockedSessionRepository .update_agent
782+
783+ def tracking_update_agent (self , session_id , session_agent ):
784+ update_agent_calls .append ((session_id , session_agent ))
785+ return original_update_agent (self , session_id , session_agent )
786+
787+ mock_repository .update_agent = lambda sid , sa : tracking_update_agent (mock_repository , sid , sa )
788+
789+ # Retry should work because version wasn't updated on failure
790+ session_manager .sync_agent (agent )
791+ assert len (update_agent_calls ) == 1
792+
793+
794+ def test_sync_agent_first_sync_always_updates (mock_repository ):
795+ """Test that the first sync_agent() call always updates (no previous state to compare)."""
796+ session_manager = RepositorySessionManager (session_id = "test-session" , session_repository = mock_repository )
797+
798+ # Create and initialize agent
799+ agent = Agent (agent_id = "test-agent" , session_manager = session_manager )
800+
801+ # Track update_agent calls
802+ update_agent_calls = []
803+ original_update_agent = mock_repository .update_agent
804+
805+ def tracking_update_agent (session_id , session_agent ):
806+ update_agent_calls .append ((session_id , session_agent ))
807+ return original_update_agent (session_id , session_agent )
808+
809+ mock_repository .update_agent = tracking_update_agent
810+
811+ # First sync should always update (no previous state)
812+ session_manager .sync_agent (agent )
813+ assert len (update_agent_calls ) == 1
0 commit comments