88- Haiku skips effort keyboard (not supported)
99- Opus shows "max" effort, Sonnet does not
1010- _current_model_label returns correct labels
11+ - Regression: model: callback data prefix is never rewritten as effort:
12+ - force_new_session is always set on model change
1113"""
1214
1315from unittest .mock import AsyncMock , MagicMock
1719
1820from src .bot .handlers .command import (
1921 _EFFORT_BY_MODEL ,
20- _MODELS ,
22+ _MODEL_FAMILIES ,
2123 _current_model_label ,
2224 _handle_model_selection ,
2325 model_command ,
@@ -85,7 +87,7 @@ async def test_model_command_shows_keyboard(update, context):
8587@pytest .mark .asyncio
8688async def test_model_command_shows_current_override (update , context ):
8789 """When an override is active, /model should show it."""
88- context .user_data ["model_override" ] = _MODELS [ "sonnet" ]
90+ context .user_data ["model_override" ] = "sonnet"
8991 await model_command (update , context )
9092
9193 text = update .message .reply_text .call_args .args [0 ]
@@ -99,27 +101,27 @@ async def test_model_command_shows_current_override(update, context):
99101
100102@pytest .mark .asyncio
101103async def test_select_opus_sets_override (callback_query , context ):
102- """Selecting Opus sets model_override and force_new_session."""
104+ """Selecting Opus sets model_override to short alias and force_new_session."""
103105 await _handle_model_selection (callback_query , "model:opus" , context )
104106
105- assert context .user_data ["model_override" ] == _MODELS [ "opus" ]
107+ assert context .user_data ["model_override" ] == "opus"
106108 assert context .user_data ["force_new_session" ] is True
107109
108110
109111@pytest .mark .asyncio
110112async def test_select_sonnet_sets_override (callback_query , context ):
111- """Selecting Sonnet sets the correct model ID ."""
113+ """Selecting Sonnet sets the correct short alias ."""
112114 await _handle_model_selection (callback_query , "model:sonnet" , context )
113115
114- assert context .user_data ["model_override" ] == _MODELS [ "sonnet" ]
116+ assert context .user_data ["model_override" ] == "sonnet"
115117
116118
117119@pytest .mark .asyncio
118120async def test_select_haiku_skips_effort (callback_query , context ):
119121 """Selecting Haiku should not show effort keyboard (not supported)."""
120122 await _handle_model_selection (callback_query , "model:haiku" , context )
121123
122- assert context .user_data ["model_override" ] == _MODELS [ "haiku" ]
124+ assert context .user_data ["model_override" ] == "haiku"
123125 # Final message, no reply_markup (no effort keyboard)
124126 call_kwargs = callback_query .edit_message_text .call_args
125127 assert "reply_markup" not in call_kwargs .kwargs or call_kwargs .kwargs .get ("reply_markup" ) is None
@@ -156,7 +158,7 @@ async def test_select_sonnet_shows_effort_without_max(callback_query, context):
156158@pytest .mark .asyncio
157159async def test_default_clears_overrides (callback_query , context ):
158160 """Selecting 'default' clears model, effort, and forces new session."""
159- context .user_data ["model_override" ] = _MODELS [ "opus" ]
161+ context .user_data ["model_override" ] = "opus"
160162 context .user_data ["effort_override" ] = "high"
161163
162164 await _handle_model_selection (callback_query , "model:default" , context )
@@ -174,7 +176,7 @@ async def test_default_clears_overrides(callback_query, context):
174176@pytest .mark .asyncio
175177async def test_effort_sets_override (callback_query , context ):
176178 """Selecting an effort level stores it in user_data."""
177- context .user_data ["model_override" ] = _MODELS [ "opus" ]
179+ context .user_data ["model_override" ] = "opus"
178180
179181 await _handle_model_selection (callback_query , "effort:high" , context )
180182
@@ -184,7 +186,7 @@ async def test_effort_sets_override(callback_query, context):
184186@pytest .mark .asyncio
185187async def test_effort_skip_keeps_existing (callback_query , context ):
186188 """Selecting 'skip' should not set effort_override."""
187- context .user_data ["model_override" ] = _MODELS [ "sonnet" ]
189+ context .user_data ["model_override" ] = "sonnet"
188190
189191 await _handle_model_selection (callback_query , "effort:skip" , context )
190192
@@ -201,6 +203,44 @@ async def test_model_switch_clears_stale_effort(callback_query, context):
201203 assert "effort_override" not in context .user_data
202204
203205
206+ # ---------------------------------------------------------------------------
207+ # Regression: callback data prefix integrity (closure-bug guard)
208+ # ---------------------------------------------------------------------------
209+
210+
211+ @pytest .mark .asyncio
212+ async def test_model_callback_sets_model_not_effort (callback_query , context ):
213+ """model: callback must set model_override, not effort_override.
214+
215+ Regression guard: a shared closure capturing 'action' from outer scope
216+ could silently rewrite 'model:opus' as 'effort:opus'. This test would
217+ have caught that.
218+ """
219+ await _handle_model_selection (callback_query , "model:sonnet" , context )
220+
221+ assert context .user_data .get ("model_override" ) == "sonnet"
222+ assert "effort_override" not in context .user_data
223+
224+
225+ @pytest .mark .asyncio
226+ async def test_effort_callback_sets_effort_not_model (callback_query , context ):
227+ """effort: callback must set effort_override and not overwrite model_override."""
228+ context .user_data ["model_override" ] = "sonnet"
229+
230+ await _handle_model_selection (callback_query , "effort:high" , context )
231+
232+ assert context .user_data .get ("effort_override" ) == "high"
233+ assert context .user_data .get ("model_override" ) == "sonnet" # unchanged
234+
235+
236+ @pytest .mark .asyncio
237+ async def test_force_new_session_set_on_model_switch (callback_query , context ):
238+ """force_new_session must be True after any model switch."""
239+ await _handle_model_selection (callback_query , "model:opus" , context )
240+
241+ assert context .user_data .get ("force_new_session" ) is True
242+
243+
204244# ---------------------------------------------------------------------------
205245# Label helper
206246# ---------------------------------------------------------------------------
@@ -222,13 +262,13 @@ def test_label_default_with_server_model():
222262
223263def test_label_with_model_and_effort ():
224264 ctx = MagicMock ()
225- ctx .user_data = {"model_override" : _MODELS [ "sonnet" ] , "effort_override" : "medium" }
265+ ctx .user_data = {"model_override" : "sonnet" , "effort_override" : "medium" }
226266 assert _current_model_label (ctx ) == "Sonnet | effort=medium"
227267
228268
229269def test_label_model_only ():
230270 ctx = MagicMock ()
231- ctx .user_data = {"model_override" : _MODELS [ "opus" ] }
271+ ctx .user_data = {"model_override" : "opus" }
232272 assert _current_model_label (ctx ) == "Opus"
233273
234274
@@ -248,3 +288,9 @@ def test_sonnet_has_no_max():
248288
249289def test_opus_has_max ():
250290 assert "max" in _EFFORT_BY_MODEL ["opus" ]
291+
292+
293+ def test_model_families_contains_expected ():
294+ assert "opus" in _MODEL_FAMILIES
295+ assert "sonnet" in _MODEL_FAMILIES
296+ assert "haiku" in _MODEL_FAMILIES
0 commit comments