/ tests / gateway / test_session_model_reset.py
test_session_model_reset.py
  1  """Tests that /new (and its /reset alias) clears session-scoped overrides."""
  2  from datetime import datetime
  3  from types import SimpleNamespace
  4  from unittest.mock import AsyncMock, MagicMock
  5  
  6  import pytest
  7  
  8  from gateway.config import GatewayConfig, Platform, PlatformConfig
  9  from gateway.platforms.base import MessageEvent
 10  from gateway.session import SessionEntry, SessionSource, build_session_key
 11  
 12  
 13  def _make_source() -> SessionSource:
 14      return SessionSource(
 15          platform=Platform.TELEGRAM,
 16          user_id="u1",
 17          chat_id="c1",
 18          user_name="tester",
 19          chat_type="dm",
 20      )
 21  
 22  
 23  def _make_event(text: str) -> MessageEvent:
 24      return MessageEvent(text=text, source=_make_source(), message_id="m1")
 25  
 26  
 27  def _make_runner():
 28      from gateway.run import GatewayRunner
 29  
 30      runner = object.__new__(GatewayRunner)
 31      runner.config = GatewayConfig(
 32          platforms={Platform.TELEGRAM: PlatformConfig(enabled=True, token="***")}
 33      )
 34      adapter = MagicMock()
 35      adapter.send = AsyncMock()
 36      runner.adapters = {Platform.TELEGRAM: adapter}
 37      runner._voice_mode = {}
 38      runner.hooks = SimpleNamespace(emit=AsyncMock(), loaded_hooks=False)
 39      runner._session_model_overrides = {}
 40      runner._session_reasoning_overrides = {}
 41      runner._pending_model_notes = {}
 42      runner._background_tasks = set()
 43  
 44      session_key = build_session_key(_make_source())
 45      session_entry = SessionEntry(
 46          session_key=session_key,
 47          session_id="sess-1",
 48          created_at=datetime.now(),
 49          updated_at=datetime.now(),
 50          platform=Platform.TELEGRAM,
 51          chat_type="dm",
 52      )
 53      runner.session_store = MagicMock()
 54      runner.session_store.get_or_create_session.return_value = session_entry
 55      runner.session_store.reset_session.return_value = session_entry
 56      runner.session_store._entries = {session_key: session_entry}
 57      runner.session_store._generate_session_key.return_value = session_key
 58      runner._running_agents = {}
 59      runner._pending_messages = {}
 60      runner._pending_approvals = {}
 61      runner._session_db = None
 62      runner._agent_cache_lock = None  # disables _evict_cached_agent lock path
 63      runner._is_user_authorized = lambda _source: True
 64      runner._format_session_info = lambda: ""
 65  
 66      return runner
 67  
 68  
 69  @pytest.mark.asyncio
 70  async def test_new_command_clears_session_model_override():
 71      """/new must remove the session-scoped model override for that session."""
 72      runner = _make_runner()
 73      session_key = build_session_key(_make_source())
 74  
 75      # Simulate a prior /model switch stored as a session override
 76      runner._session_model_overrides[session_key] = {
 77          "model": "gpt-4o",
 78          "provider": "openai",
 79          "api_key": "***",
 80          "base_url": "",
 81          "api_mode": "openai",
 82      }
 83      runner._session_reasoning_overrides[session_key] = {"enabled": True, "effort": "high"}
 84      runner._pending_model_notes[session_key] = "[Note: switched to gpt-4o.]"
 85  
 86      await runner._handle_reset_command(_make_event("/new"))
 87  
 88      assert session_key not in runner._session_model_overrides
 89      assert session_key not in runner._session_reasoning_overrides
 90      assert session_key not in runner._pending_model_notes
 91  
 92  
 93  @pytest.mark.asyncio
 94  async def test_new_command_no_override_is_noop():
 95      """/new with no prior model override must not raise."""
 96      runner = _make_runner()
 97      session_key = build_session_key(_make_source())
 98  
 99      assert session_key not in runner._session_model_overrides
100      assert session_key not in runner._session_reasoning_overrides
101  
102      await runner._handle_reset_command(_make_event("/new"))
103  
104      assert session_key not in runner._session_model_overrides
105      assert session_key not in runner._session_reasoning_overrides
106  
107  
108  @pytest.mark.asyncio
109  async def test_new_command_only_clears_own_session():
110      """/new must only clear the override for the session that triggered it."""
111      runner = _make_runner()
112      session_key = build_session_key(_make_source())
113      other_key = "other_session_key"
114  
115      runner._session_model_overrides[session_key] = {
116          "model": "gpt-4o",
117          "provider": "openai",
118          "api_key": "sk-test",
119          "base_url": "",
120          "api_mode": "openai",
121      }
122      runner._session_model_overrides[other_key] = {
123          "model": "claude-sonnet-4-6",
124          "provider": "anthropic",
125          "api_key": "***",
126          "base_url": "",
127          "api_mode": "anthropic",
128      }
129      runner._session_reasoning_overrides[session_key] = {"enabled": True, "effort": "high"}
130      runner._session_reasoning_overrides[other_key] = {"enabled": True, "effort": "low"}
131      runner._pending_model_notes[session_key] = "[Note: switched to gpt-4o.]"
132      runner._pending_model_notes[other_key] = "[Note: switched to claude-sonnet-4-6.]"
133  
134      await runner._handle_reset_command(_make_event("/new"))
135  
136      assert session_key not in runner._session_model_overrides
137      assert other_key in runner._session_model_overrides
138      assert session_key not in runner._session_reasoning_overrides
139      assert other_key in runner._session_reasoning_overrides
140      assert session_key not in runner._pending_model_notes
141      assert other_key in runner._pending_model_notes