@@ -36,6 +36,32 @@ async def _run_update(app: SimpleNamespace, args: str) -> None:
3636 await cast (Awaitable [None ], shell_slash .update_command (cast (Shell , app ), args ))
3737
3838
39+ def _fake_choices (
40+ monkeypatch : pytest .MonkeyPatch , selections : dict [str , str ]
41+ ) -> dict [str , dict [str , object ]]:
42+ """Drive the real menu/picker by faking the interactive ``ChoiceInput`` boundary.
43+
44+ ``selections`` maps a prompt's ``message`` to the option key the user "picks".
45+ Returns a dict recording each prompt's constructor kwargs so tests can assert
46+ the observable choice boundary (offered options and default cursor) instead of
47+ patching the private helpers that build them.
48+ """
49+ import prompt_toolkit .shortcuts .choice_input as choice_input
50+
51+ recorded : dict [str , dict [str , object ]] = {}
52+
53+ class _FakeChoiceInput :
54+ def __init__ (self , * , message : object , ** kwargs : object ) -> None :
55+ self ._message = str (message )
56+ recorded [self ._message ] = {"message" : self ._message , ** kwargs }
57+
58+ async def prompt_async (self ) -> str :
59+ return selections [self ._message ]
60+
61+ monkeypatch .setattr (choice_input , "ChoiceInput" , _FakeChoiceInput )
62+ return recorded
63+
64+
3965@pytest .fixture (autouse = True )
4066def _no_override (monkeypatch : pytest .MonkeyPatch ) -> None :
4167 # The suite runs from a source checkout, where the override would otherwise
@@ -142,51 +168,66 @@ async def test_update_auto_requires_config_file(
142168async def test_bare_update_menu_check_runs_update_flow (
143169 runtime : Runtime , tmp_path : Path , monkeypatch : pytest .MonkeyPatch
144170) -> None :
171+ runtime .config .auto_update = True
145172 app = _make_shell_app (runtime , tmp_path )
146173 monkeypatch .setattr (shell_slash .console , "print" , Mock ())
147- monkeypatch . setattr ( shell_slash , "_prompt_update_action" , AsyncMock ( return_value = "check" ))
174+ # Stub only the public update-flow boundary; the real menu still runs.
148175 run_prompt = AsyncMock (return_value = update_module .UpdateResult .UP_TO_DATE )
149176 monkeypatch .setattr (update_module , "run_update_prompt" , run_prompt )
150- auto_toggle = AsyncMock ()
151- monkeypatch .setattr (shell_slash , "_auto_update_toggle" , auto_toggle )
177+ recorded = _fake_choices (monkeypatch , {"Update" : "check" })
152178
153179 await _run_update (app , "" )
154180
181+ # Picking "check" runs the update flow and leaves the auto setting untouched.
155182 run_prompt .assert_awaited_once ()
156- auto_toggle .assert_not_called ()
183+ assert runtime .config .auto_update is True
184+ assert recorded ["Update" ]["default" ] == "check"
157185
158186
159187@pytest .mark .asyncio
160- async def test_bare_update_menu_auto_routes_to_toggle (
188+ async def test_bare_update_menu_auto_persists_chosen_state (
161189 runtime : Runtime , tmp_path : Path , monkeypatch : pytest .MonkeyPatch
162190) -> None :
191+ monkeypatch .delenv ("PYTHINKER_AUTO_UPDATE" , raising = False )
192+ config_path = (tmp_path / "config.toml" ).resolve ()
193+ _seed_config_file (config_path , auto_update = False )
194+ runtime .config .source_file = config_path
195+ runtime .config .auto_update = False
163196 app = _make_shell_app (runtime , tmp_path )
164197 monkeypatch .setattr (shell_slash .console , "print" , Mock ())
165- monkeypatch .setattr (shell_slash , "_prompt_update_action" , AsyncMock (return_value = "auto" ))
166198 run_prompt = AsyncMock (return_value = update_module .UpdateResult .UP_TO_DATE )
167199 monkeypatch .setattr (update_module , "run_update_prompt" , run_prompt )
168- auto_toggle = AsyncMock ()
169- monkeypatch . setattr ( shell_slash , "_auto_update_toggle" , auto_toggle )
200+ # Drive the whole menu -> toggle -> picker chain via the input boundary.
201+ recorded = _fake_choices ( monkeypatch , { "Update" : "auto" , "Auto-update on startup" : "on" } )
170202
171203 await _run_update (app , "" )
172204
173- auto_toggle .assert_awaited_once_with (app , [])
205+ # The chosen state is persisted and mirrored live; the update flow is skipped.
206+ assert load_config (config_path ).auto_update is True
207+ assert runtime .config .auto_update is True
174208 run_prompt .assert_not_called ()
209+ # The picker's cursor defaults to the current (off) state.
210+ assert recorded ["Auto-update on startup" ]["default" ] == "off"
175211
176212
177213@pytest .mark .asyncio
178214async def test_bare_update_menu_cancel_is_noop (
179215 runtime : Runtime , tmp_path : Path , monkeypatch : pytest .MonkeyPatch
180216) -> None :
217+ runtime .config .auto_update = False
181218 app = _make_shell_app (runtime , tmp_path )
219+ save_mock = Mock ()
220+ monkeypatch .setattr (shell_slash , "save_config" , save_mock )
182221 monkeypatch .setattr (shell_slash .console , "print" , Mock ())
183- monkeypatch .setattr (shell_slash , "_prompt_update_action" , AsyncMock (return_value = None ))
184222 run_prompt = AsyncMock (return_value = update_module .UpdateResult .UP_TO_DATE )
185223 monkeypatch .setattr (update_module , "run_update_prompt" , run_prompt )
224+ _fake_choices (monkeypatch , {"Update" : "cancel" })
186225
187226 await _run_update (app , "" )
188227
189228 run_prompt .assert_not_called ()
229+ save_mock .assert_not_called ()
230+ assert runtime .config .auto_update is False
190231
191232
192233@pytest .mark .asyncio
@@ -200,14 +241,12 @@ async def test_update_auto_no_args_opens_picker_and_persists(
200241 runtime .config .auto_update = False
201242 app = _make_shell_app (runtime , tmp_path )
202243 monkeypatch .setattr (shell_slash .console , "print" , Mock ())
203-
204- picker = AsyncMock (return_value = True )
205- monkeypatch .setattr (shell_slash , "_prompt_auto_update_selection" , picker )
244+ recorded = _fake_choices (monkeypatch , {"Auto-update on startup" : "on" })
206245
207246 await _run_update (app , "auto" )
208247
209- # The picker is consulted with the current value as its default cursor ...
210- assert picker . call_args . kwargs == { "current" : False }
248+ # The picker's cursor defaults to the current value...
249+ assert recorded [ "Auto-update on startup" ][ "default" ] == "off"
211250 # ...and the chosen state is persisted and mirrored live.
212251 assert load_config (config_path ).auto_update is True
213252 assert runtime .config .auto_update is True
@@ -222,11 +261,7 @@ async def test_update_auto_no_args_cancel_is_noop(
222261 save_mock = Mock ()
223262 monkeypatch .setattr (shell_slash , "save_config" , save_mock )
224263 monkeypatch .setattr (shell_slash .console , "print" , Mock ())
225- monkeypatch .setattr (
226- shell_slash ,
227- "_prompt_auto_update_selection" ,
228- AsyncMock (return_value = None ),
229- )
264+ _fake_choices (monkeypatch , {"Auto-update on startup" : "cancel" })
230265
231266 await _run_update (app , "auto" )
232267
0 commit comments