From ebd12dd473bda32e0abfbbac3dd4765ce70af130 Mon Sep 17 00:00:00 2001 From: Asher Norland Date: Mon, 9 Mar 2026 10:51:56 -0700 Subject: [PATCH] Fix Config Updates --- synodic_client/application/screen/settings.py | 9 ++++ .../screen/tool_update_controller.py | 7 ++- .../application/update_controller.py | 7 ++- tests/unit/qt/test_settings.py | 43 +++++++++++++++ tests/unit/qt/test_update_controller.py | 54 +++++++++++++++++++ 5 files changed, 117 insertions(+), 3 deletions(-) diff --git a/synodic_client/application/screen/settings.py b/synodic_client/application/screen/settings.py index 3e8e083..eafbbb9 100644 --- a/synodic_client/application/screen/settings.py +++ b/synodic_client/application/screen/settings.py @@ -305,6 +305,15 @@ def reset_check_updates_button(self) -> None: """Re-enable the *Check for Updates* button after a check completes.""" self._check_updates_btn.setEnabled(True) + def update_config(self, config: ResolvedConfig) -> None: + """Replace the internal config snapshot without emitting signals. + + Called by controllers that persist timestamps so that the next + :meth:`sync_from_config` sees fresh data instead of the stale + snapshot captured at construction time. + """ + self._config = config + def set_last_checked(self, timestamp: str) -> None: """Update the *last updated* label from an ISO 8601 timestamp.""" relative = _format_relative_time(timestamp) diff --git a/synodic_client/application/screen/tool_update_controller.py b/synodic_client/application/screen/tool_update_controller.py index 1f0e3c3..bce34b0 100644 --- a/synodic_client/application/screen/tool_update_controller.py +++ b/synodic_client/application/screen/tool_update_controller.py @@ -382,7 +382,12 @@ def _on_tool_update_finished( for pkg_name in result.updated_packages: key = f'{plugin_name}/{pkg_name}' if plugin_name else pkg_name existing[key] = now - update_user_config(last_tool_updates=existing) + resolved = update_user_config(last_tool_updates=existing) + # Refresh the config on the tools view so the next rebuild + # picks up the updated timestamps instead of stale data. + tools_view_ref = self._window.tools_view + if tools_view_ref is not None: + tools_view_ref._config = resolved # Clear updating state on widgets tools_view = self._window.tools_view diff --git a/synodic_client/application/update_controller.py b/synodic_client/application/update_controller.py index d01a3bc..45759ce 100644 --- a/synodic_client/application/update_controller.py +++ b/synodic_client/application/update_controller.py @@ -132,7 +132,8 @@ def _can_auto_apply(self) -> bool: def _persist_check_timestamp(self) -> None: """Persist the current time as *last_client_update* and refresh the label.""" ts = datetime.now(UTC).isoformat() - update_user_config(last_client_update=ts) + resolved = update_user_config(last_client_update=ts) + self._settings_window.update_config(resolved) self._settings_window.set_last_checked(ts) def _report_error(self, message: str, *, silent: bool) -> None: @@ -329,7 +330,9 @@ def _on_download_finished(self, success: bool, version: str) -> None: # Persist the client-update timestamp (actual update downloaded) ts = datetime.now(UTC).isoformat() - update_user_config(last_client_update=ts) + resolved = update_user_config(last_client_update=ts) + self._settings_window.update_config(resolved) + self._settings_window.set_last_checked(ts) self._pending_version = version diff --git a/tests/unit/qt/test_settings.py b/tests/unit/qt/test_settings.py index a87dfac..d2b2223 100644 --- a/tests/unit/qt/test_settings.py +++ b/tests/unit/qt/test_settings.py @@ -404,3 +404,46 @@ def test_set_checking() -> None: window.set_checking() assert window._check_updates_btn.isEnabled() is False assert window._update_status_label.text() == 'Checking\u2026' + + +# --------------------------------------------------------------------------- +# update_config — silent config refresh +# --------------------------------------------------------------------------- + + +class TestUpdateConfig: + """Verify that update_config refreshes _config without emitting signals.""" + + @staticmethod + def test_update_config_replaces_internal_config() -> None: + """update_config should replace _config with the new snapshot.""" + window = _make_window(_make_config(last_client_update=None)) + new_config = _make_config(last_client_update='2026-03-09T12:00:00+00:00') + + window.update_config(new_config) + + assert window._config is new_config + assert window._config.last_client_update == '2026-03-09T12:00:00+00:00' + + @staticmethod + def test_update_config_does_not_emit_settings_changed() -> None: + """update_config must NOT emit settings_changed to avoid circular reinit.""" + window = _make_window() + signal_spy = MagicMock() + window.settings_changed.connect(signal_spy) + + window.update_config(_make_config(update_channel='dev')) + + signal_spy.assert_not_called() + + @staticmethod + def test_sync_after_update_config_uses_new_timestamp() -> None: + """sync_from_config after update_config should display the refreshed timestamp.""" + window = _make_window(_make_config(last_client_update=None)) + assert window._last_client_update_label.text() == '' + + new_config = _make_config(last_client_update='2026-03-09T12:00:00+00:00') + window.update_config(new_config) + window.sync_from_config() + + assert 'Last updated:' in window._last_client_update_label.text() diff --git a/tests/unit/qt/test_update_controller.py b/tests/unit/qt/test_update_controller.py index 2a195a6..1904b77 100644 --- a/tests/unit/qt/test_update_controller.py +++ b/tests/unit/qt/test_update_controller.py @@ -414,3 +414,57 @@ def test_check_error_no_banner_when_silent() -> None: ctrl._on_check_error('timeout', silent=True) assert banner.state.name == 'HIDDEN' + + +# --------------------------------------------------------------------------- +# Timestamp sync — _persist_check_timestamp updates config +# --------------------------------------------------------------------------- + + +class TestPersistCheckTimestamp: + """Verify _persist_check_timestamp syncs the settings config.""" + + @staticmethod + def test_persist_updates_settings_config() -> None: + """_persist_check_timestamp should call update_config on the settings window.""" + ctrl, _app, _client, _banner, settings = _make_controller() + + fake_resolved = _make_config(last_client_update='2026-03-09T00:00:00+00:00') + with patch( + 'synodic_client.application.update_controller.update_user_config', + return_value=fake_resolved, + ): + ctrl._persist_check_timestamp() + + settings.update_config.assert_called_once_with(fake_resolved) + settings.set_last_checked.assert_called_once() + + @staticmethod + def test_on_check_finished_success_syncs_config() -> None: + """A successful check should persist timestamp AND sync settings config.""" + ctrl, _app, _client, _banner, settings = _make_controller() + result = UpdateInfo(available=False, current_version=Version('1.0.0')) + + fake_resolved = _make_config(last_client_update='2026-03-09T00:00:00+00:00') + with patch( + 'synodic_client.application.update_controller.update_user_config', + return_value=fake_resolved, + ): + ctrl._on_check_finished(result, silent=True) + + settings.update_config.assert_called_once_with(fake_resolved) + + @staticmethod + def test_download_finished_syncs_config_and_label() -> None: + """_on_download_finished should sync config and update the label.""" + ctrl, _app, _client, _banner, settings = _make_controller(auto_apply=False) + + fake_resolved = _make_config(last_client_update='2026-03-09T00:00:00+00:00') + with patch( + 'synodic_client.application.update_controller.update_user_config', + return_value=fake_resolved, + ): + ctrl._on_download_finished(True, '2.0.0') + + settings.update_config.assert_called_once_with(fake_resolved) + settings.set_last_checked.assert_called_once()