Skip to content

Commit 3bfacc5

Browse files
committed
test(update): assert observable outcomes instead of patching private helpers
Address review: the /update menu and auto-update picker tests patched the private _prompt_update_action / _auto_update_toggle / _prompt_auto_update_selection helpers and asserted internal call wiring. Drive the real helpers by faking only the interactive ChoiceInput boundary (and stubbing the public run_update_prompt seam), and assert observable results — persisted/unchanged config, the update flow running or being skipped, and the picker's default cursor reflecting the current state.
1 parent 3c03c18 commit 3bfacc5

1 file changed

Lines changed: 55 additions & 20 deletions

File tree

tests/ui_and_conv/test_update_auto_slash.py

Lines changed: 55 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -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)
4066
def _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(
142168
async 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
178214
async 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

Comments
 (0)