@@ -703,3 +703,107 @@ def test_boundary_text_in_tool_result_not_truncated():
703703
704704 assert not changed
705705 assert messages [0 ]["content" ][0 ]["toolResult" ]["content" ][0 ]["text" ] == boundary_text
706+
707+
708+ # ── protected_messages tests ──────────────────────────────────────────────
709+
710+
711+ def test_protected_messages_negative_raises ():
712+ """protected_messages must be non-negative."""
713+ with pytest .raises (ValueError , match = "non-negative" ):
714+ SlidingWindowConversationManager (protected_messages = - 1 )
715+
716+
717+ def test_protected_messages_zero_is_default ():
718+ """Default protected_messages=0 behaves identically to the original manager."""
719+ manager = SlidingWindowConversationManager (window_size = 2 , should_truncate_results = False )
720+ assert manager .protected_messages == 0
721+
722+
723+ def test_protected_messages_preserves_first_message_on_trim ():
724+ """When protected_messages=1, the first user message survives trimming."""
725+ manager = SlidingWindowConversationManager (window_size = 2 , should_truncate_results = False , protected_messages = 1 )
726+ agent = MagicMock ()
727+ agent .messages = [
728+ {"role" : "user" , "content" : [{"text" : "Generate the report" }]},
729+ {"role" : "assistant" , "content" : [{"text" : "Step 1" }]},
730+ {"role" : "user" , "content" : [{"text" : "Follow-up" }]},
731+ {"role" : "assistant" , "content" : [{"text" : "Step 2" }]},
732+ {"role" : "user" , "content" : [{"text" : "Another question" }]},
733+ ]
734+
735+ manager .apply_management (agent )
736+
737+ # The first message must still be present
738+ assert agent .messages [0 ]["content" ][0 ]["text" ] == "Generate the report"
739+ # And the conversation should end with the most recent messages
740+ assert agent .messages [- 1 ]["content" ][0 ]["text" ] == "Another question"
741+
742+
743+ def test_protected_messages_preserves_first_message_on_overflow ():
744+ """protected_messages=1 preserves the prompt even during context overflow (reduce_context with e)."""
745+ manager = SlidingWindowConversationManager (window_size = 2 , should_truncate_results = False , protected_messages = 1 )
746+ agent = MagicMock ()
747+ agent .messages = [
748+ {"role" : "user" , "content" : [{"text" : "Task prompt" }]},
749+ {"role" : "assistant" , "content" : [{"text" : "Calling tools" }]},
750+ {"role" : "user" , "content" : [{"text" : "Tool results" }]},
751+ {"role" : "assistant" , "content" : [{"text" : "More work" }]},
752+ {"role" : "user" , "content" : [{"text" : "More results" }]},
753+ ]
754+
755+ manager .reduce_context (agent , e = RuntimeError ("context overflow" ))
756+
757+ assert agent .messages [0 ]["content" ][0 ]["text" ] == "Task prompt"
758+
759+
760+ def test_protected_messages_multiple ():
761+ """protected_messages=2 preserves the first two messages."""
762+ manager = SlidingWindowConversationManager (window_size = 2 , should_truncate_results = False , protected_messages = 2 )
763+ agent = MagicMock ()
764+ agent .messages = [
765+ {"role" : "user" , "content" : [{"text" : "System context" }]},
766+ {"role" : "assistant" , "content" : [{"text" : "Acknowledged" }]},
767+ {"role" : "user" , "content" : [{"text" : "Question 1" }]},
768+ {"role" : "assistant" , "content" : [{"text" : "Answer 1" }]},
769+ {"role" : "user" , "content" : [{"text" : "Question 2" }]},
770+ ]
771+
772+ manager .apply_management (agent )
773+
774+ assert agent .messages [0 ]["content" ][0 ]["text" ] == "System context"
775+ assert agent .messages [1 ]["content" ][0 ]["text" ] == "Acknowledged"
776+
777+
778+ def test_protected_messages_no_trim_needed ():
779+ """When messages fit in the window, protected_messages has no effect."""
780+ manager = SlidingWindowConversationManager (window_size = 10 , should_truncate_results = False , protected_messages = 1 )
781+ agent = MagicMock ()
782+ agent .messages = [
783+ {"role" : "user" , "content" : [{"text" : "Hello" }]},
784+ {"role" : "assistant" , "content" : [{"text" : "Hi" }]},
785+ ]
786+
787+ manager .apply_management (agent )
788+
789+ assert len (agent .messages ) == 2
790+
791+
792+ def test_protected_messages_trim_index_skips_protected_region ():
793+ """The trim index must never fall within the protected region."""
794+ manager = SlidingWindowConversationManager (window_size = 3 , should_truncate_results = False , protected_messages = 1 )
795+ agent = MagicMock ()
796+ # 5 messages, window_size=3 → trim_index starts at 2
797+ # But protected_messages=1 means index 0 is protected
798+ agent .messages = [
799+ {"role" : "user" , "content" : [{"text" : "Important prompt" }]},
800+ {"role" : "assistant" , "content" : [{"text" : "Response 1" }]},
801+ {"role" : "user" , "content" : [{"text" : "Q2" }]},
802+ {"role" : "assistant" , "content" : [{"text" : "Response 2" }]},
803+ {"role" : "user" , "content" : [{"text" : "Q3" }]},
804+ ]
805+
806+ manager .apply_management (agent )
807+
808+ # First message must survive
809+ assert agent .messages [0 ]["content" ][0 ]["text" ] == "Important prompt"
0 commit comments