synodic-client 0.0.1.dev78__tar.gz → 0.0.1.dev79__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/PKG-INFO +1 -1
- {synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/pyproject.toml +1 -1
- {synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/synodic_client/application/bootstrap.py +16 -3
- {synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/synodic_client/application/update_controller.py +7 -1
- {synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/synodic_client/cli/update.py +8 -1
- {synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/synodic_client/resolution.py +1 -7
- {synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/synodic_client/updater.py +102 -15
- {synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/tests/unit/qt/test_update_controller.py +17 -0
- synodic_client-0.0.1.dev79/tests/unit/test_bootstrap.py +79 -0
- {synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/tests/unit/test_cli.py +28 -1
- {synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/tests/unit/test_resolution.py +4 -4
- {synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/tests/unit/test_updater.py +6 -6
- {synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/LICENSE.md +0 -0
- {synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/README.md +0 -0
- {synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/synodic_client/__init__.py +0 -0
- {synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/synodic_client/__main__.py +0 -0
- {synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/synodic_client/application/__init__.py +0 -0
- {synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/synodic_client/application/config_store.py +0 -0
- {synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/synodic_client/application/data.py +0 -0
- {synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/synodic_client/application/debug.py +0 -0
- {synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/synodic_client/application/icon.py +0 -0
- {synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/synodic_client/application/init.py +0 -0
- {synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/synodic_client/application/instance.py +0 -0
- {synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/synodic_client/application/package_state.py +0 -0
- {synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/synodic_client/application/qt.py +0 -0
- {synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/synodic_client/application/schema.py +0 -0
- {synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/synodic_client/application/screen/__init__.py +0 -0
- {synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/synodic_client/application/screen/action_card.py +0 -0
- {synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/synodic_client/application/screen/card.py +0 -0
- {synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/synodic_client/application/screen/install.py +0 -0
- {synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/synodic_client/application/screen/install_workers.py +0 -0
- {synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/synodic_client/application/screen/log_panel.py +0 -0
- {synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/synodic_client/application/screen/plugin_row.py +0 -0
- {synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/synodic_client/application/screen/projects.py +0 -0
- {synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/synodic_client/application/screen/schema.py +0 -0
- {synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/synodic_client/application/screen/screen.py +0 -0
- {synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/synodic_client/application/screen/settings.py +0 -0
- {synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/synodic_client/application/screen/sidebar.py +0 -0
- {synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/synodic_client/application/screen/spinner.py +0 -0
- {synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/synodic_client/application/screen/tool_update_controller.py +0 -0
- {synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/synodic_client/application/screen/tray.py +0 -0
- {synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/synodic_client/application/screen/update_banner.py +0 -0
- {synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/synodic_client/application/theme.py +0 -0
- {synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/synodic_client/application/update_model.py +0 -0
- {synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/synodic_client/application/uri.py +0 -0
- {synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/synodic_client/cli/__init__.py +0 -0
- {synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/synodic_client/cli/config.py +0 -0
- {synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/synodic_client/cli/context.py +0 -0
- {synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/synodic_client/cli/debug.py +0 -0
- {synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/synodic_client/cli/install.py +0 -0
- {synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/synodic_client/cli/output.py +0 -0
- {synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/synodic_client/cli/project.py +0 -0
- {synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/synodic_client/cli/tool.py +0 -0
- {synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/synodic_client/client.py +0 -0
- {synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/synodic_client/config.py +0 -0
- {synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/synodic_client/logging.py +0 -0
- {synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/synodic_client/operations/__init__.py +0 -0
- {synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/synodic_client/operations/bootstrap.py +0 -0
- {synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/synodic_client/operations/config.py +0 -0
- {synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/synodic_client/operations/install.py +0 -0
- {synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/synodic_client/operations/project.py +0 -0
- {synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/synodic_client/operations/schema.py +0 -0
- {synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/synodic_client/operations/tool.py +0 -0
- {synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/synodic_client/operations/update.py +0 -0
- {synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/synodic_client/protocol.py +0 -0
- {synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/synodic_client/py.typed +0 -0
- {synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/synodic_client/schema.py +0 -0
- {synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/synodic_client/startup.py +0 -0
- {synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/synodic_client/subprocess_patch.py +0 -0
- {synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/tests/__init__.py +0 -0
- {synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/tests/conftest.py +0 -0
- {synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/tests/unit/__init__.py +0 -0
- {synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/tests/unit/operations/__init__.py +0 -0
- {synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/tests/unit/operations/test_config.py +0 -0
- {synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/tests/unit/operations/test_install.py +0 -0
- {synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/tests/unit/operations/test_install_plan.py +0 -0
- {synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/tests/unit/operations/test_project.py +0 -0
- {synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/tests/unit/operations/test_tool.py +0 -0
- {synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/tests/unit/operations/test_update.py +0 -0
- {synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/tests/unit/qt/__init__.py +0 -0
- {synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/tests/unit/qt/conftest.py +0 -0
- {synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/tests/unit/qt/test_action_card.py +0 -0
- {synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/tests/unit/qt/test_gather_packages.py +0 -0
- {synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/tests/unit/qt/test_install_preview.py +0 -0
- {synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/tests/unit/qt/test_log_panel.py +0 -0
- {synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/tests/unit/qt/test_logging.py +0 -0
- {synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/tests/unit/qt/test_package_state.py +0 -0
- {synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/tests/unit/qt/test_preview_model.py +0 -0
- {synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/tests/unit/qt/test_settings.py +0 -0
- {synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/tests/unit/qt/test_sidebar.py +0 -0
- {synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/tests/unit/qt/test_tray_window_show.py +0 -0
- {synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/tests/unit/qt/test_update_banner.py +0 -0
- {synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/tests/unit/qt/test_update_feedback.py +0 -0
- {synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/tests/unit/test_client_updater.py +0 -0
- {synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/tests/unit/test_client_version.py +0 -0
- {synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/tests/unit/test_config.py +0 -0
- {synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/tests/unit/test_examples.py +0 -0
- {synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/tests/unit/test_init.py +0 -0
- {synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/tests/unit/test_install.py +0 -0
- {synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/tests/unit/test_uri.py +0 -0
- {synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/tests/unit/test_workers.py +0 -0
- {synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/tests/unit/windows/__init__.py +0 -0
- {synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/tests/unit/windows/conftest.py +0 -0
- {synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/tests/unit/windows/test_protocol.py +0 -0
- {synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/tests/unit/windows/test_startup.py +0 -0
{synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/synodic_client/application/bootstrap.py
RENAMED
|
@@ -8,9 +8,11 @@ timeouts (15–30 s) and must complete before the process is killed.
|
|
|
8
8
|
Import order matters:
|
|
9
9
|
1. stdlib + config (pure-Python, fast)
|
|
10
10
|
2. configure_logging() — now Qt-free
|
|
11
|
-
3.
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
3. sync_startup() — refresh Windows auto-startup registry **before**
|
|
12
|
+
Velopack, which may exit the process during post-update hooks
|
|
13
|
+
4. initialize_velopack() — hooks run with logging active
|
|
14
|
+
5. run_startup_preamble() — protocol, config seed, auto-startup
|
|
15
|
+
6. import qt.application — PySide6 / porringer loaded here
|
|
14
16
|
"""
|
|
15
17
|
|
|
16
18
|
import logging
|
|
@@ -47,6 +49,17 @@ def bootstrap() -> None:
|
|
|
47
49
|
logger = logging.getLogger(__name__)
|
|
48
50
|
logger.info('Bootstrap started (exe=%s, argv=%s)', sys.executable, sys.argv)
|
|
49
51
|
|
|
52
|
+
# Refresh the Windows auto-startup registry entry BEFORE Velopack
|
|
53
|
+
# initialisation. App.run() may exit the current process during
|
|
54
|
+
# post-update lifecycle hooks, so sync_startup must run first to
|
|
55
|
+
# ensure the registry path stays current after an update.
|
|
56
|
+
if not dev_mode:
|
|
57
|
+
from synodic_client.resolution import resolve_config
|
|
58
|
+
from synodic_client.startup import sync_startup
|
|
59
|
+
|
|
60
|
+
config = resolve_config()
|
|
61
|
+
sync_startup(sys.executable, auto_start=config.auto_start)
|
|
62
|
+
|
|
50
63
|
initialize_velopack()
|
|
51
64
|
|
|
52
65
|
if not dev_mode:
|
|
@@ -124,7 +124,13 @@ class UpdateController:
|
|
|
124
124
|
"""Cancel any in-flight task and start *coro* as the active task."""
|
|
125
125
|
if self._update_task is not None and not self._update_task.done():
|
|
126
126
|
self._update_task.cancel()
|
|
127
|
-
|
|
127
|
+
try:
|
|
128
|
+
self._update_task = asyncio.create_task(coro)
|
|
129
|
+
except RuntimeError:
|
|
130
|
+
# No running event loop yet (e.g. during early init).
|
|
131
|
+
# The periodic timer will retry once the loop is running.
|
|
132
|
+
coro.close()
|
|
133
|
+
logger.debug('Deferred update check — event loop not yet running')
|
|
128
134
|
|
|
129
135
|
# ------------------------------------------------------------------
|
|
130
136
|
# Config helpers
|
|
@@ -8,6 +8,7 @@ synodic-c update apply
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
10
|
import asyncio
|
|
11
|
+
import sys
|
|
11
12
|
from typing import Annotated
|
|
12
13
|
|
|
13
14
|
import typer
|
|
@@ -75,7 +76,13 @@ def update_apply(
|
|
|
75
76
|
"""Apply a downloaded self-update."""
|
|
76
77
|
from synodic_client.cli.context import get_services
|
|
77
78
|
from synodic_client.operations.update import apply_self_update
|
|
79
|
+
from synodic_client.startup import sync_startup
|
|
80
|
+
|
|
81
|
+
client, _, config = get_services()
|
|
82
|
+
|
|
83
|
+
# Refresh the Windows auto-startup registry entry before the update
|
|
84
|
+
# replaces the executable, so the path stays current.
|
|
85
|
+
sync_startup(sys.executable, auto_start=config.auto_start)
|
|
78
86
|
|
|
79
|
-
client, _, _ = get_services()
|
|
80
87
|
apply_self_update(client, restart=not no_restart, silent=silent)
|
|
81
88
|
typer.echo('Update applied.')
|
|
@@ -31,7 +31,6 @@ from synodic_client.schema import (
|
|
|
31
31
|
UpdateConfig,
|
|
32
32
|
UserConfig,
|
|
33
33
|
)
|
|
34
|
-
from synodic_client.updater import github_release_asset_url
|
|
35
34
|
|
|
36
35
|
logger = logging.getLogger(__name__)
|
|
37
36
|
|
|
@@ -171,14 +170,9 @@ def resolve_update_config(config: ResolvedConfig) -> UpdateConfig:
|
|
|
171
170
|
"""
|
|
172
171
|
channel = UpdateChannel.DEVELOPMENT if config.update_channel == 'dev' else UpdateChannel.STABLE
|
|
173
172
|
|
|
174
|
-
repo_url = github_release_asset_url(
|
|
175
|
-
config.update_source or GITHUB_REPO_URL,
|
|
176
|
-
channel,
|
|
177
|
-
)
|
|
178
|
-
|
|
179
173
|
return UpdateConfig(
|
|
180
174
|
channel=channel,
|
|
181
|
-
repo_url=
|
|
175
|
+
repo_url=config.update_source or GITHUB_REPO_URL,
|
|
182
176
|
auto_update_interval_minutes=config.auto_update_interval_minutes,
|
|
183
177
|
tool_update_interval_minutes=config.tool_update_interval_minutes,
|
|
184
178
|
)
|
|
@@ -8,8 +8,10 @@ For non-installed (development) environments, updates are not supported.
|
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
10
|
import contextlib
|
|
11
|
+
import json
|
|
11
12
|
import logging
|
|
12
13
|
import sys
|
|
14
|
+
import urllib.request
|
|
13
15
|
from collections.abc import Callable
|
|
14
16
|
from typing import Any
|
|
15
17
|
|
|
@@ -173,7 +175,19 @@ class Updater:
|
|
|
173
175
|
error='Not installed via Velopack',
|
|
174
176
|
)
|
|
175
177
|
|
|
176
|
-
|
|
178
|
+
try:
|
|
179
|
+
velopack_info = manager.check_for_updates()
|
|
180
|
+
except Exception as sdk_err:
|
|
181
|
+
if '404' in str(sdk_err):
|
|
182
|
+
logger.debug('SDK check failed with 404, trying manifest fallback: %s', sdk_err)
|
|
183
|
+
velopack_info = self._check_manifest_fallback()
|
|
184
|
+
else:
|
|
185
|
+
raise
|
|
186
|
+
|
|
187
|
+
if velopack_info is None:
|
|
188
|
+
# SDK returned no update; try the manual manifest fallback
|
|
189
|
+
# in case the SDK's GithubSource skipped prerelease entries.
|
|
190
|
+
velopack_info = self._check_manifest_fallback()
|
|
177
191
|
|
|
178
192
|
if velopack_info is not None:
|
|
179
193
|
latest = Version(velopack_info.TargetFullRelease.Version)
|
|
@@ -202,20 +216,6 @@ class Updater:
|
|
|
202
216
|
return self._update_info
|
|
203
217
|
|
|
204
218
|
except Exception as e:
|
|
205
|
-
if '404' in str(e):
|
|
206
|
-
channel = self._config.channel_name
|
|
207
|
-
msg = (
|
|
208
|
-
f"No releases found for the '{channel}' channel. "
|
|
209
|
-
"Try switching to the 'Development' channel in Settings \u2192 Channel."
|
|
210
|
-
)
|
|
211
|
-
logger.debug('No releases for channel %s: %s', channel, e)
|
|
212
|
-
self._state = UpdateState.NO_UPDATE
|
|
213
|
-
return UpdateInfo(
|
|
214
|
-
available=False,
|
|
215
|
-
current_version=self._current_version,
|
|
216
|
-
error=msg,
|
|
217
|
-
)
|
|
218
|
-
|
|
219
219
|
logger.exception('Failed to check for updates')
|
|
220
220
|
self._state = UpdateState.FAILED
|
|
221
221
|
return UpdateInfo(
|
|
@@ -224,6 +224,93 @@ class Updater:
|
|
|
224
224
|
error=str(e),
|
|
225
225
|
)
|
|
226
226
|
|
|
227
|
+
def _check_manifest_fallback(self) -> Any:
|
|
228
|
+
"""Download the release manifest directly and check for updates.
|
|
229
|
+
|
|
230
|
+
The Velopack SDK's ``GithubSource`` handler cannot discover
|
|
231
|
+
updates from prerelease GitHub Releases. This fallback
|
|
232
|
+
downloads ``releases.{channel}.json`` via Python's stdlib and
|
|
233
|
+
constructs a ``velopack.UpdateInfo`` when a newer version
|
|
234
|
+
exists.
|
|
235
|
+
|
|
236
|
+
Returns:
|
|
237
|
+
A ``velopack.UpdateInfo`` if an update is available,
|
|
238
|
+
``None`` otherwise.
|
|
239
|
+
"""
|
|
240
|
+
asset_base = github_release_asset_url(self._config.repo_url, self._config.channel)
|
|
241
|
+
manifest_url = f'{asset_base}/releases.{self._config.channel_name}.json'
|
|
242
|
+
logger.debug('Manifest fallback: fetching %s', manifest_url)
|
|
243
|
+
|
|
244
|
+
try:
|
|
245
|
+
req = urllib.request.Request(manifest_url, headers={'User-Agent': 'synodic-client'})
|
|
246
|
+
with urllib.request.urlopen(req, timeout=30) as resp: # noqa: S310 — URL is derived from a known repo constant
|
|
247
|
+
data = json.loads(resp.read())
|
|
248
|
+
except Exception:
|
|
249
|
+
logger.debug('Manifest fallback failed for %s', manifest_url, exc_info=True)
|
|
250
|
+
return None
|
|
251
|
+
|
|
252
|
+
current_semver = pep440_to_semver(str(self._current_version))
|
|
253
|
+
best: dict[str, Any] | None = None
|
|
254
|
+
best_ver: str | None = None
|
|
255
|
+
|
|
256
|
+
for asset in data.get('Assets', []):
|
|
257
|
+
if asset.get('Type') != 'Full':
|
|
258
|
+
continue
|
|
259
|
+
ver = asset.get('Version', '')
|
|
260
|
+
if not ver:
|
|
261
|
+
continue
|
|
262
|
+
# Simple semver comparison via packaging.version (accepts
|
|
263
|
+
# semver pre-release tags like ``0.1.0-dev.79``).
|
|
264
|
+
try:
|
|
265
|
+
if Version(ver) > Version(current_semver):
|
|
266
|
+
if best_ver is None or Version(ver) > Version(best_ver):
|
|
267
|
+
best = asset
|
|
268
|
+
best_ver = ver
|
|
269
|
+
except Exception:
|
|
270
|
+
continue
|
|
271
|
+
|
|
272
|
+
if best is None:
|
|
273
|
+
logger.debug('Manifest fallback: no newer version found')
|
|
274
|
+
return None
|
|
275
|
+
|
|
276
|
+
logger.debug('Manifest fallback: found %s', best_ver)
|
|
277
|
+
|
|
278
|
+
target = velopack.VelopackAsset(
|
|
279
|
+
PackageId=best['PackageId'],
|
|
280
|
+
Version=best['Version'],
|
|
281
|
+
Type=best['Type'],
|
|
282
|
+
FileName=best['FileName'],
|
|
283
|
+
SHA1=best.get('SHA1', ''),
|
|
284
|
+
SHA256=best.get('SHA256', ''),
|
|
285
|
+
Size=best.get('Size', 0),
|
|
286
|
+
NotesMarkdown='',
|
|
287
|
+
NotesHtml='',
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
# Collect matching delta assets for the same version.
|
|
291
|
+
deltas = []
|
|
292
|
+
for asset in data.get('Assets', []):
|
|
293
|
+
if asset.get('Type') == 'Delta' and asset.get('Version') == best['Version']:
|
|
294
|
+
deltas.append(
|
|
295
|
+
velopack.VelopackAsset(
|
|
296
|
+
PackageId=asset['PackageId'],
|
|
297
|
+
Version=asset['Version'],
|
|
298
|
+
Type=asset['Type'],
|
|
299
|
+
FileName=asset['FileName'],
|
|
300
|
+
SHA1=asset.get('SHA1', ''),
|
|
301
|
+
SHA256=asset.get('SHA256', ''),
|
|
302
|
+
Size=asset.get('Size', 0),
|
|
303
|
+
NotesMarkdown='',
|
|
304
|
+
NotesHtml='',
|
|
305
|
+
)
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
return velopack.UpdateInfo(
|
|
309
|
+
TargetFullRelease=target,
|
|
310
|
+
DeltasToTarget=deltas,
|
|
311
|
+
IsDowngrade=False,
|
|
312
|
+
)
|
|
313
|
+
|
|
227
314
|
def download_update(self, progress_callback: Callable[[int], None] | None = None) -> bool:
|
|
228
315
|
"""Download the update.
|
|
229
316
|
|
{synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/tests/unit/qt/test_update_controller.py
RENAMED
|
@@ -40,6 +40,7 @@ class ModelSpy:
|
|
|
40
40
|
def _make_controller(
|
|
41
41
|
*,
|
|
42
42
|
auto_apply: bool = True,
|
|
43
|
+
auto_start: bool = True,
|
|
43
44
|
auto_update_interval_minutes: int = 0,
|
|
44
45
|
is_user_active: bool = False,
|
|
45
46
|
) -> tuple[UpdateController, MagicMock, MagicMock, UpdateBanner, UpdateModel]:
|
|
@@ -49,6 +50,7 @@ def _make_controller(
|
|
|
49
50
|
"""
|
|
50
51
|
config = make_resolved_config(
|
|
51
52
|
auto_apply=auto_apply,
|
|
53
|
+
auto_start=auto_start,
|
|
52
54
|
auto_update_interval_minutes=auto_update_interval_minutes,
|
|
53
55
|
)
|
|
54
56
|
|
|
@@ -352,6 +354,21 @@ class TestApplyUpdate:
|
|
|
352
354
|
client.apply_update_on_exit.assert_called_once()
|
|
353
355
|
app.quit.assert_called_once()
|
|
354
356
|
|
|
357
|
+
@staticmethod
|
|
358
|
+
def test_apply_update_passes_auto_start_false_from_config() -> None:
|
|
359
|
+
"""sync_startup receives auto_start=False when config says so."""
|
|
360
|
+
ctrl, app, client, banner, model = _make_controller(auto_start=False)
|
|
361
|
+
ctrl._pending_version = '2.0.0'
|
|
362
|
+
|
|
363
|
+
with (
|
|
364
|
+
patch('synodic_client.application.update_controller.sync_startup') as mock_sync,
|
|
365
|
+
patch('synodic_client.application.update_controller.sys') as mock_sys,
|
|
366
|
+
):
|
|
367
|
+
mock_sys.executable = r'C:\app\synodic.exe'
|
|
368
|
+
ctrl._apply_update()
|
|
369
|
+
|
|
370
|
+
mock_sync.assert_called_once_with(r'C:\app\synodic.exe', auto_start=False)
|
|
371
|
+
|
|
355
372
|
|
|
356
373
|
# ---------------------------------------------------------------------------
|
|
357
374
|
# Settings changed → immediate check
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Tests for the bootstrap entry point startup-sync ordering."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import importlib
|
|
6
|
+
import sys
|
|
7
|
+
from unittest.mock import MagicMock, patch
|
|
8
|
+
|
|
9
|
+
_MODULE = 'synodic_client.application.bootstrap'
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _run_bootstrap(*, argv: list[str]) -> None:
|
|
13
|
+
"""Import (or reload) the bootstrap module, triggering ``bootstrap()``.
|
|
14
|
+
|
|
15
|
+
The module-level ``bootstrap()`` call runs on every import/reload,
|
|
16
|
+
so all patches must be in place before calling this.
|
|
17
|
+
"""
|
|
18
|
+
# Ensure sys.argv is set for the bootstrap function
|
|
19
|
+
with patch.object(sys, 'argv', argv):
|
|
20
|
+
if _MODULE in sys.modules:
|
|
21
|
+
importlib.reload(sys.modules[_MODULE])
|
|
22
|
+
else:
|
|
23
|
+
importlib.import_module(_MODULE)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class TestBootstrapStartupSync:
|
|
27
|
+
"""Verify sync_startup runs before initialize_velopack in bootstrap."""
|
|
28
|
+
|
|
29
|
+
@staticmethod
|
|
30
|
+
def test_sync_startup_called_before_velopack_init() -> None:
|
|
31
|
+
"""sync_startup must execute before initialize_velopack.
|
|
32
|
+
|
|
33
|
+
Velopack's App.run() may exit the process during post-update
|
|
34
|
+
hooks, so the startup registry must already be refreshed.
|
|
35
|
+
"""
|
|
36
|
+
call_order: list[str] = []
|
|
37
|
+
|
|
38
|
+
def _record_sync(*args: object, **kwargs: object) -> None:
|
|
39
|
+
call_order.append('sync_startup')
|
|
40
|
+
|
|
41
|
+
def _record_velopack() -> None:
|
|
42
|
+
call_order.append('initialize_velopack')
|
|
43
|
+
|
|
44
|
+
mock_config = MagicMock(auto_start=True)
|
|
45
|
+
|
|
46
|
+
with (
|
|
47
|
+
patch('synodic_client.config.set_dev_mode'),
|
|
48
|
+
patch('synodic_client.logging.configure_logging'),
|
|
49
|
+
patch('synodic_client.subprocess_patch.apply'),
|
|
50
|
+
patch('synodic_client.updater.initialize_velopack', side_effect=_record_velopack),
|
|
51
|
+
patch('synodic_client.resolution.resolve_config', return_value=mock_config),
|
|
52
|
+
patch('synodic_client.startup.sync_startup', side_effect=_record_sync) as mock_sync,
|
|
53
|
+
patch('synodic_client.application.init.run_startup_preamble'),
|
|
54
|
+
patch('synodic_client.application.qt.application'),
|
|
55
|
+
patch('synodic_client.protocol.extract_uri_from_args', return_value=None),
|
|
56
|
+
):
|
|
57
|
+
_run_bootstrap(argv=[r'C:\app\synodic.exe'])
|
|
58
|
+
|
|
59
|
+
assert call_order == ['sync_startup', 'initialize_velopack']
|
|
60
|
+
mock_sync.assert_called_once()
|
|
61
|
+
assert mock_sync.call_args.kwargs['auto_start'] is True
|
|
62
|
+
|
|
63
|
+
@staticmethod
|
|
64
|
+
def test_sync_startup_skipped_in_dev_mode() -> None:
|
|
65
|
+
"""sync_startup is not called when --dev flag is passed."""
|
|
66
|
+
with (
|
|
67
|
+
patch('synodic_client.config.set_dev_mode'),
|
|
68
|
+
patch('synodic_client.logging.configure_logging'),
|
|
69
|
+
patch('synodic_client.subprocess_patch.apply'),
|
|
70
|
+
patch('synodic_client.updater.initialize_velopack'),
|
|
71
|
+
patch('synodic_client.resolution.resolve_config') as mock_resolve,
|
|
72
|
+
patch('synodic_client.startup.sync_startup') as mock_sync,
|
|
73
|
+
patch('synodic_client.application.qt.application'),
|
|
74
|
+
patch('synodic_client.protocol.extract_uri_from_args', return_value=None),
|
|
75
|
+
):
|
|
76
|
+
_run_bootstrap(argv=[r'C:\app\synodic.exe', '--dev'])
|
|
77
|
+
|
|
78
|
+
mock_resolve.assert_not_called()
|
|
79
|
+
mock_sync.assert_not_called()
|
|
@@ -295,15 +295,42 @@ class TestUpdateCli:
|
|
|
295
295
|
@staticmethod
|
|
296
296
|
def test_update_apply() -> None:
|
|
297
297
|
"""Update apply calls apply_self_update."""
|
|
298
|
+
mock_config = MagicMock(auto_start=True)
|
|
298
299
|
with (
|
|
299
|
-
patch('synodic_client.cli.context.get_services', return_value=(MagicMock(), None,
|
|
300
|
+
patch('synodic_client.cli.context.get_services', return_value=(MagicMock(), None, mock_config)),
|
|
300
301
|
patch('synodic_client.operations.update.apply_self_update') as mock_apply,
|
|
302
|
+
patch('synodic_client.startup.sync_startup'),
|
|
301
303
|
):
|
|
302
304
|
result = runner.invoke(app, ['update', 'apply'])
|
|
303
305
|
assert result.exit_code == 0
|
|
304
306
|
assert 'applied' in result.output.lower()
|
|
305
307
|
mock_apply.assert_called_once()
|
|
306
308
|
|
|
309
|
+
@staticmethod
|
|
310
|
+
def test_update_apply_calls_sync_startup_before_apply() -> None:
|
|
311
|
+
"""sync_startup is called with the config's auto_start before apply_self_update."""
|
|
312
|
+
call_order: list[str] = []
|
|
313
|
+
mock_config = MagicMock(auto_start=False)
|
|
314
|
+
|
|
315
|
+
def _record_sync(*args: object, **kwargs: object) -> None:
|
|
316
|
+
call_order.append('sync_startup')
|
|
317
|
+
|
|
318
|
+
def _record_apply(*args: object, **kwargs: object) -> None:
|
|
319
|
+
call_order.append('apply_self_update')
|
|
320
|
+
|
|
321
|
+
with (
|
|
322
|
+
patch('synodic_client.cli.context.get_services', return_value=(MagicMock(), None, mock_config)),
|
|
323
|
+
patch('synodic_client.startup.sync_startup', side_effect=_record_sync) as mock_sync,
|
|
324
|
+
patch('synodic_client.operations.update.apply_self_update', side_effect=_record_apply),
|
|
325
|
+
):
|
|
326
|
+
result = runner.invoke(app, ['update', 'apply'])
|
|
327
|
+
assert result.exit_code == 0
|
|
328
|
+
|
|
329
|
+
mock_sync.assert_called_once()
|
|
330
|
+
# auto_start=False should be forwarded from config
|
|
331
|
+
assert mock_sync.call_args.kwargs['auto_start'] is False
|
|
332
|
+
assert call_order == ['sync_startup', 'apply_self_update']
|
|
333
|
+
|
|
307
334
|
|
|
308
335
|
# ---------------------------------------------------------------------------
|
|
309
336
|
# Debug subcommands
|
|
@@ -364,17 +364,17 @@ class TestResolveUpdateConfig:
|
|
|
364
364
|
|
|
365
365
|
@staticmethod
|
|
366
366
|
def test_default_source_dev() -> None:
|
|
367
|
-
"""Verify default dev source uses GitHub
|
|
367
|
+
"""Verify default dev source uses raw GitHub repo URL."""
|
|
368
368
|
config = _make_resolved(update_channel='dev')
|
|
369
369
|
result = resolve_update_config(config)
|
|
370
|
-
assert result.repo_url ==
|
|
370
|
+
assert result.repo_url == GITHUB_REPO_URL
|
|
371
371
|
|
|
372
372
|
@staticmethod
|
|
373
373
|
def test_default_source_stable() -> None:
|
|
374
|
-
"""Verify default stable source uses GitHub
|
|
374
|
+
"""Verify default stable source uses raw GitHub repo URL."""
|
|
375
375
|
config = _make_resolved(update_channel='stable')
|
|
376
376
|
result = resolve_update_config(config)
|
|
377
|
-
assert result.repo_url ==
|
|
377
|
+
assert result.repo_url == GITHUB_REPO_URL
|
|
378
378
|
|
|
379
379
|
@staticmethod
|
|
380
380
|
def test_default_auto_update_interval() -> None:
|
|
@@ -227,18 +227,18 @@ class TestUpdaterCheckForUpdate:
|
|
|
227
227
|
|
|
228
228
|
@staticmethod
|
|
229
229
|
def test_check_404_returns_friendly_message(updater: Updater) -> None:
|
|
230
|
-
"""Verify a 404 from GitHub
|
|
230
|
+
"""Verify a 404 from GitHub falls back to manifest check gracefully."""
|
|
231
231
|
mock_manager = MagicMock(spec=velopack.UpdateManager)
|
|
232
232
|
mock_manager.check_for_updates.side_effect = RuntimeError('Network error: Http error: http status: 404')
|
|
233
233
|
|
|
234
|
-
with
|
|
234
|
+
with (
|
|
235
|
+
patch.object(updater, '_get_velopack_manager', return_value=mock_manager),
|
|
236
|
+
patch.object(updater, '_check_manifest_fallback', return_value=None),
|
|
237
|
+
):
|
|
235
238
|
info = updater.check_for_update()
|
|
236
239
|
|
|
237
240
|
assert info.available is False
|
|
238
|
-
|
|
239
|
-
assert 'No releases found' in info.error
|
|
240
|
-
assert updater._config.channel_name in info.error
|
|
241
|
-
# A missing channel is informational, not a hard failure
|
|
241
|
+
# Fallback returned None, so no error — just no update.
|
|
242
242
|
assert updater.state == UpdateState.NO_UPDATE
|
|
243
243
|
|
|
244
244
|
@staticmethod
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/synodic_client/application/__init__.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/synodic_client/application/config_store.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/synodic_client/application/data.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/synodic_client/application/debug.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/synodic_client/application/icon.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/synodic_client/application/init.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/synodic_client/application/instance.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/synodic_client/application/schema.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/synodic_client/application/screen/card.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/synodic_client/application/screen/tray.py
RENAMED
|
File without changes
|
|
File without changes
|
{synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/synodic_client/application/theme.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/synodic_client/application/update_model.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/synodic_client/operations/__init__.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/synodic_client/operations/bootstrap.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/synodic_client/operations/config.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/synodic_client/operations/install.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/synodic_client/operations/project.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/synodic_client/operations/schema.py
RENAMED
|
File without changes
|
|
File without changes
|
{synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/synodic_client/operations/update.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/synodic_client/subprocess_patch.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/tests/unit/operations/test_config.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/tests/unit/operations/test_install.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/tests/unit/operations/test_install_plan.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/tests/unit/operations/test_project.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/tests/unit/operations/test_tool.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/tests/unit/operations/test_update.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/tests/unit/qt/test_gather_packages.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/tests/unit/qt/test_install_preview.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/tests/unit/qt/test_package_state.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/tests/unit/qt/test_preview_model.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/tests/unit/qt/test_tray_window_show.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/tests/unit/qt/test_update_banner.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/tests/unit/qt/test_update_feedback.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/tests/unit/windows/test_protocol.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev78 → synodic_client-0.0.1.dev79}/tests/unit/windows/test_startup.py
RENAMED
|
File without changes
|