/ tests / run_agent / test_session_reset_fix.py
test_session_reset_fix.py
  1  """Tests for session reset completeness (fixes #2635).
  2  
  3  /clear and /new must not carry stale state into the next session.
  4  Two fields were added after reset_session_state() was written and were
  5  therefore never cleared:
  6    - ContextCompressor._previous_summary
  7    - AIAgent._user_turn_count
  8  """
  9  import sys
 10  import types
 11  from pathlib import Path
 12  
 13  import pytest
 14  
 15  # Ensure repo root is importable
 16  sys.path.insert(0, str(Path(__file__).resolve().parent.parent.parent))
 17  
 18  # Stub out optional heavy dependencies not installed in the test environment
 19  sys.modules.setdefault("fire", types.SimpleNamespace(Fire=lambda *a, **k: None))
 20  sys.modules.setdefault("firecrawl", types.SimpleNamespace(Firecrawl=object))
 21  sys.modules.setdefault("fal_client", types.SimpleNamespace())
 22  
 23  from run_agent import AIAgent
 24  from agent.context_compressor import ContextCompressor
 25  
 26  
 27  def _make_minimal_agent() -> AIAgent:
 28      """Return an AIAgent constructed with the absolute minimum args.
 29  
 30      We pass dummy values that bypass network calls and filesystem access.
 31      The object is never used to make API calls — only its attributes and
 32      reset_session_state() are exercised.
 33      """
 34      agent = AIAgent.__new__(AIAgent)  # skip __init__ entirely
 35  
 36      # Seed the exact attributes that reset_session_state() writes
 37      agent.session_total_tokens = 0
 38      agent.session_input_tokens = 0
 39      agent.session_output_tokens = 0
 40      agent.session_prompt_tokens = 0
 41      agent.session_completion_tokens = 0
 42      agent.session_cache_read_tokens = 0
 43      agent.session_cache_write_tokens = 0
 44      agent.session_reasoning_tokens = 0
 45      agent.session_api_calls = 0
 46      agent.session_estimated_cost_usd = 0.0
 47      agent.session_cost_status = "unknown"
 48      agent.session_cost_source = "none"
 49  
 50      # The two fields under test
 51      agent._user_turn_count = 0
 52      agent.context_compressor = None  # will be set per-test as needed
 53  
 54      return agent
 55  
 56  
 57  class TestResetSessionState:
 58      """reset_session_state() must clear ALL session-scoped state."""
 59  
 60      def test_previous_summary_cleared_on_reset(self):
 61          """Compression summary from old session must not leak into new session."""
 62          agent = _make_minimal_agent()
 63          compressor = ContextCompressor.__new__(ContextCompressor)
 64          compressor._previous_summary = "Old session summary about unrelated topic"
 65          # Seed counter attributes that reset_session_state touches
 66          compressor.last_prompt_tokens = 100
 67          compressor.last_completion_tokens = 50
 68          compressor.last_total_tokens = 150
 69          compressor.compression_count = 3
 70          compressor._context_probed = True
 71  
 72          agent.context_compressor = compressor
 73  
 74          agent.reset_session_state()
 75  
 76          assert compressor._previous_summary is None, (
 77              "_previous_summary must be None after reset; got: "
 78              f"{compressor._previous_summary!r}"
 79          )
 80  
 81      def test_user_turn_count_cleared_on_reset(self):
 82          """Turn counter must reset to 0 on new session."""
 83          agent = _make_minimal_agent()
 84          agent._user_turn_count = 7  # simulates turns accumulated in previous session
 85          agent.context_compressor = None
 86  
 87          agent.reset_session_state()
 88  
 89          assert agent._user_turn_count == 0, (
 90              f"_user_turn_count must be 0 after reset; got: {agent._user_turn_count}"
 91          )
 92  
 93      def test_both_fields_cleared_together(self):
 94          """Both stale fields are cleared in a single reset_session_state() call."""
 95          agent = _make_minimal_agent()
 96          agent._user_turn_count = 3
 97  
 98          compressor = ContextCompressor.__new__(ContextCompressor)
 99          compressor._previous_summary = "Stale summary"
100          compressor.last_prompt_tokens = 0
101          compressor.last_completion_tokens = 0
102          compressor.last_total_tokens = 0
103          compressor.compression_count = 0
104          compressor._context_probed = False
105          agent.context_compressor = compressor
106  
107          agent.reset_session_state()
108  
109          assert agent._user_turn_count == 0
110          assert compressor._previous_summary is None
111  
112      def test_reset_without_compressor_does_not_raise(self):
113          """reset_session_state() must not raise when context_compressor is None."""
114          agent = _make_minimal_agent()
115          agent._user_turn_count = 2
116          agent.context_compressor = None
117  
118          # Must not raise
119          agent.reset_session_state()
120  
121          assert agent._user_turn_count == 0