Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions synodic_client/application/screen/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
7 changes: 6 additions & 1 deletion synodic_client/application/screen/tool_update_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 5 additions & 2 deletions synodic_client/application/update_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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

Expand Down
43 changes: 43 additions & 0 deletions tests/unit/qt/test_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
54 changes: 54 additions & 0 deletions tests/unit/qt/test_update_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Loading