@@ -100,6 +100,20 @@ async def test_suppressed_notice_does_not_toast(
100100 assert shell ._update_toast_shown_version is None
101101
102102
103+ def test_update_toast_dedupes_repeated_notices (runtime : Runtime , tmp_path : Path , _toasts ):
104+ """The periodic loop re-surfaces outcomes; each notice text toasts once."""
105+ shell = _make_shell (runtime , tmp_path )
106+
107+ shell ._update_toast ("Update available: 9.9.9" , style = "bold" )
108+ shell ._update_toast ("Update available: 9.9.9" , style = "bold" )
109+ shell ._update_toast ("Update available: 10.0.0" , style = "bold" )
110+
111+ assert [msg for msg , _ in _toasts ] == [
112+ "Update available: 9.9.9" ,
113+ "Update available: 10.0.0" ,
114+ ]
115+
116+
103117@pytest .mark .asyncio
104118async def test_periodic_update_check_runs_immediately_then_at_interval (monkeypatch ):
105119 checks : list [str ] = []
@@ -125,6 +139,34 @@ async def sleep(delay: float) -> None:
125139 ]
126140
127141
142+ @pytest .mark .asyncio
143+ async def test_periodic_update_check_recovers_after_failing_check (monkeypatch ):
144+ checks : list [str ] = []
145+ sleeps : list [float ] = []
146+
147+ async def check () -> None :
148+ checks .append ("check" )
149+ if len (checks ) == 1 :
150+ raise RuntimeError ("transient check failure" )
151+
152+ async def sleep (delay : float ) -> None :
153+ sleeps .append (delay )
154+ if len (sleeps ) == 2 :
155+ raise asyncio .CancelledError
156+
157+ monkeypatch .setattr (shell_module .asyncio , "sleep" , sleep )
158+
159+ with pytest .raises (asyncio .CancelledError ):
160+ await shell_module ._periodic_update_check (check )
161+
162+ # The first failure is swallowed (logged) and the loop keeps re-checking.
163+ assert checks == ["check" , "check" ]
164+ assert sleeps == [
165+ shell_module .AUTO_UPDATE_CHECK_INTERVAL_SECONDS ,
166+ shell_module .AUTO_UPDATE_CHECK_INTERVAL_SECONDS ,
167+ ]
168+
169+
128170def test_startup_scheduler_uses_periodic_loop_in_all_scheduling_branches (
129171 runtime : Runtime , tmp_path : Path , monkeypatch
130172):
@@ -133,13 +175,20 @@ def test_startup_scheduler_uses_periodic_loop_in_all_scheduling_branches(
133175 shell = _make_shell (runtime , tmp_path )
134176 scheduled_checks = []
135177
178+ def fake_periodic (check ):
179+ scheduled_checks .append (check )
180+
181+ async def _noop () -> None :
182+ return None
183+
184+ return _noop ()
185+
136186 def capture (coro ):
137- assert coro .cr_code .co_name == "_periodic_update_check"
138- assert coro .cr_frame is not None
139- scheduled_checks .append (coro .cr_frame .f_locals ["check" ])
140187 coro .close ()
141188 return None
142189
190+ monkeypatch .setattr (shell_module , "_periodic_update_check" , fake_periodic )
191+
143192 monkeypatch .delenv ("PYTHINKER_CLI_NO_AUTO_UPDATE" , raising = False )
144193 monkeypatch .setattr (shell , "_start_background_task" , capture )
145194
0 commit comments