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
2 changes: 1 addition & 1 deletion synodic_client/application/update_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -440,8 +440,8 @@ def _apply_update(self, *, silent: bool = False) -> None:
# the next launch.
sync_startup(sys.executable, auto_start=self._store.config.auto_start)

self._pending_version = None
self._client.apply_update_on_exit(restart=True, silent=silent)
self._pending_version = None
logger.info('Update scheduled — restarting application')
self._app.quit()
except Exception as e:
Expand Down
7 changes: 6 additions & 1 deletion synodic_client/updater.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,12 @@ def check_for_update(self) -> UpdateInfo:
latest_version=latest,
_velopack_info=velopack_info,
)
self._state = UpdateState.UPDATE_AVAILABLE
# Only advance to UPDATE_AVAILABLE if we haven't already
# moved past it. A periodic re-check that discovers the
# same release must not regress DOWNLOADED → UPDATE_AVAILABLE,
# which would cause apply_update_on_exit() to reject the update.
if self._state not in (UpdateState.DOWNLOADED, UpdateState.APPLYING, UpdateState.APPLIED):
self._state = UpdateState.UPDATE_AVAILABLE
logger.info('Update available: %s -> %s', self._current_version, latest)
else:
self._update_info = UpdateInfo(
Expand Down
32 changes: 32 additions & 0 deletions tests/unit/test_updater.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,38 @@ def test_check_non_404_http_error_is_failed(updater: Updater) -> None:
assert info.available is False
assert updater.state == UpdateState.FAILED

@staticmethod
def test_check_preserves_downloaded_state(updater: Updater) -> None:
"""Re-checking after download must not regress DOWNLOADED → UPDATE_AVAILABLE.

Regression test: when the periodic auto-check timer fires between
download completion and the user clicking "Restart Now", the state
was incorrectly reset to UPDATE_AVAILABLE, causing apply_update_on_exit
to reject the update with "No downloaded update to apply".
"""
mock_target = MagicMock(spec=velopack.VelopackAsset)
mock_target.Version = '2.0.0'
mock_velopack_info = MagicMock(spec=velopack.UpdateInfo)
mock_velopack_info.TargetFullRelease = mock_target

mock_manager = MagicMock(spec=velopack.UpdateManager)
mock_manager.check_for_updates.return_value = mock_velopack_info

# Simulate: download already completed
updater._state = UpdateState.DOWNLOADED
updater._update_info = UpdateInfo(
available=True,
current_version=Version('1.0.0'),
latest_version=Version('2.0.0'),
_velopack_info=mock_velopack_info,
)

with patch.object(updater, '_get_velopack_manager', return_value=mock_manager):
info = updater.check_for_update()

assert info.available is True
assert updater.state == UpdateState.DOWNLOADED


class TestUpdaterDownloadUpdate:
"""Tests for download_update method."""
Expand Down
Loading