synodic-client 0.0.1.dev57__tar.gz → 0.0.1.dev62__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.dev57 → synodic_client-0.0.1.dev62}/PKG-INFO +1 -1
- {synodic_client-0.0.1.dev57 → synodic_client-0.0.1.dev62}/pyproject.toml +1 -1
- {synodic_client-0.0.1.dev57 → synodic_client-0.0.1.dev62}/synodic_client/application/init.py +10 -6
- {synodic_client-0.0.1.dev57 → synodic_client-0.0.1.dev62}/synodic_client/application/screen/screen.py +4 -9
- {synodic_client-0.0.1.dev57 → synodic_client-0.0.1.dev62}/synodic_client/application/screen/settings.py +26 -6
- {synodic_client-0.0.1.dev57 → synodic_client-0.0.1.dev62}/synodic_client/application/screen/spinner.py +1 -0
- {synodic_client-0.0.1.dev57 → synodic_client-0.0.1.dev62}/synodic_client/application/screen/tool_update_controller.py +6 -1
- {synodic_client-0.0.1.dev57 → synodic_client-0.0.1.dev62}/synodic_client/application/update_controller.py +5 -2
- {synodic_client-0.0.1.dev57 → synodic_client-0.0.1.dev62}/synodic_client/logging.py +7 -6
- {synodic_client-0.0.1.dev57 → synodic_client-0.0.1.dev62}/tests/unit/qt/test_gather_packages.py +4 -10
- {synodic_client-0.0.1.dev57 → synodic_client-0.0.1.dev62}/tests/unit/qt/test_logging.py +4 -5
- {synodic_client-0.0.1.dev57 → synodic_client-0.0.1.dev62}/tests/unit/qt/test_settings.py +68 -4
- {synodic_client-0.0.1.dev57 → synodic_client-0.0.1.dev62}/tests/unit/qt/test_update_controller.py +54 -0
- {synodic_client-0.0.1.dev57 → synodic_client-0.0.1.dev62}/tests/unit/test_init.py +24 -2
- {synodic_client-0.0.1.dev57 → synodic_client-0.0.1.dev62}/LICENSE.md +0 -0
- {synodic_client-0.0.1.dev57 → synodic_client-0.0.1.dev62}/README.md +0 -0
- {synodic_client-0.0.1.dev57 → synodic_client-0.0.1.dev62}/synodic_client/__init__.py +0 -0
- {synodic_client-0.0.1.dev57 → synodic_client-0.0.1.dev62}/synodic_client/__main__.py +0 -0
- {synodic_client-0.0.1.dev57 → synodic_client-0.0.1.dev62}/synodic_client/application/__init__.py +0 -0
- {synodic_client-0.0.1.dev57 → synodic_client-0.0.1.dev62}/synodic_client/application/bootstrap.py +0 -0
- {synodic_client-0.0.1.dev57 → synodic_client-0.0.1.dev62}/synodic_client/application/data.py +0 -0
- {synodic_client-0.0.1.dev57 → synodic_client-0.0.1.dev62}/synodic_client/application/icon.py +0 -0
- {synodic_client-0.0.1.dev57 → synodic_client-0.0.1.dev62}/synodic_client/application/instance.py +0 -0
- {synodic_client-0.0.1.dev57 → synodic_client-0.0.1.dev62}/synodic_client/application/qt.py +0 -0
- {synodic_client-0.0.1.dev57 → synodic_client-0.0.1.dev62}/synodic_client/application/schema.py +0 -0
- {synodic_client-0.0.1.dev57 → synodic_client-0.0.1.dev62}/synodic_client/application/screen/__init__.py +0 -0
- {synodic_client-0.0.1.dev57 → synodic_client-0.0.1.dev62}/synodic_client/application/screen/action_card.py +0 -0
- {synodic_client-0.0.1.dev57 → synodic_client-0.0.1.dev62}/synodic_client/application/screen/card.py +0 -0
- {synodic_client-0.0.1.dev57 → synodic_client-0.0.1.dev62}/synodic_client/application/screen/install.py +0 -0
- {synodic_client-0.0.1.dev57 → synodic_client-0.0.1.dev62}/synodic_client/application/screen/install_workers.py +0 -0
- {synodic_client-0.0.1.dev57 → synodic_client-0.0.1.dev62}/synodic_client/application/screen/log_panel.py +0 -0
- {synodic_client-0.0.1.dev57 → synodic_client-0.0.1.dev62}/synodic_client/application/screen/plugin_row.py +0 -0
- {synodic_client-0.0.1.dev57 → synodic_client-0.0.1.dev62}/synodic_client/application/screen/projects.py +0 -0
- {synodic_client-0.0.1.dev57 → synodic_client-0.0.1.dev62}/synodic_client/application/screen/schema.py +0 -0
- {synodic_client-0.0.1.dev57 → synodic_client-0.0.1.dev62}/synodic_client/application/screen/sidebar.py +0 -0
- {synodic_client-0.0.1.dev57 → synodic_client-0.0.1.dev62}/synodic_client/application/screen/tray.py +0 -0
- {synodic_client-0.0.1.dev57 → synodic_client-0.0.1.dev62}/synodic_client/application/screen/update_banner.py +0 -0
- {synodic_client-0.0.1.dev57 → synodic_client-0.0.1.dev62}/synodic_client/application/theme.py +0 -0
- {synodic_client-0.0.1.dev57 → synodic_client-0.0.1.dev62}/synodic_client/application/uri.py +0 -0
- {synodic_client-0.0.1.dev57 → synodic_client-0.0.1.dev62}/synodic_client/application/workers.py +0 -0
- {synodic_client-0.0.1.dev57 → synodic_client-0.0.1.dev62}/synodic_client/cli.py +0 -0
- {synodic_client-0.0.1.dev57 → synodic_client-0.0.1.dev62}/synodic_client/client.py +0 -0
- {synodic_client-0.0.1.dev57 → synodic_client-0.0.1.dev62}/synodic_client/config.py +0 -0
- {synodic_client-0.0.1.dev57 → synodic_client-0.0.1.dev62}/synodic_client/protocol.py +0 -0
- {synodic_client-0.0.1.dev57 → synodic_client-0.0.1.dev62}/synodic_client/py.typed +0 -0
- {synodic_client-0.0.1.dev57 → synodic_client-0.0.1.dev62}/synodic_client/resolution.py +0 -0
- {synodic_client-0.0.1.dev57 → synodic_client-0.0.1.dev62}/synodic_client/schema.py +0 -0
- {synodic_client-0.0.1.dev57 → synodic_client-0.0.1.dev62}/synodic_client/startup.py +0 -0
- {synodic_client-0.0.1.dev57 → synodic_client-0.0.1.dev62}/synodic_client/updater.py +0 -0
- {synodic_client-0.0.1.dev57 → synodic_client-0.0.1.dev62}/tests/__init__.py +0 -0
- {synodic_client-0.0.1.dev57 → synodic_client-0.0.1.dev62}/tests/conftest.py +0 -0
- {synodic_client-0.0.1.dev57 → synodic_client-0.0.1.dev62}/tests/unit/__init__.py +0 -0
- {synodic_client-0.0.1.dev57 → synodic_client-0.0.1.dev62}/tests/unit/qt/__init__.py +0 -0
- {synodic_client-0.0.1.dev57 → synodic_client-0.0.1.dev62}/tests/unit/qt/conftest.py +0 -0
- {synodic_client-0.0.1.dev57 → synodic_client-0.0.1.dev62}/tests/unit/qt/test_action_card.py +0 -0
- {synodic_client-0.0.1.dev57 → synodic_client-0.0.1.dev62}/tests/unit/qt/test_install_preview.py +0 -0
- {synodic_client-0.0.1.dev57 → synodic_client-0.0.1.dev62}/tests/unit/qt/test_log_panel.py +0 -0
- {synodic_client-0.0.1.dev57 → synodic_client-0.0.1.dev62}/tests/unit/qt/test_preview_model.py +0 -0
- {synodic_client-0.0.1.dev57 → synodic_client-0.0.1.dev62}/tests/unit/qt/test_sidebar.py +0 -0
- {synodic_client-0.0.1.dev57 → synodic_client-0.0.1.dev62}/tests/unit/qt/test_tray_window_show.py +0 -0
- {synodic_client-0.0.1.dev57 → synodic_client-0.0.1.dev62}/tests/unit/qt/test_update_banner.py +0 -0
- {synodic_client-0.0.1.dev57 → synodic_client-0.0.1.dev62}/tests/unit/qt/test_update_feedback.py +0 -0
- {synodic_client-0.0.1.dev57 → synodic_client-0.0.1.dev62}/tests/unit/test_cli.py +0 -0
- {synodic_client-0.0.1.dev57 → synodic_client-0.0.1.dev62}/tests/unit/test_client_updater.py +0 -0
- {synodic_client-0.0.1.dev57 → synodic_client-0.0.1.dev62}/tests/unit/test_client_version.py +0 -0
- {synodic_client-0.0.1.dev57 → synodic_client-0.0.1.dev62}/tests/unit/test_config.py +0 -0
- {synodic_client-0.0.1.dev57 → synodic_client-0.0.1.dev62}/tests/unit/test_examples.py +0 -0
- {synodic_client-0.0.1.dev57 → synodic_client-0.0.1.dev62}/tests/unit/test_install.py +0 -0
- {synodic_client-0.0.1.dev57 → synodic_client-0.0.1.dev62}/tests/unit/test_resolution.py +0 -0
- {synodic_client-0.0.1.dev57 → synodic_client-0.0.1.dev62}/tests/unit/test_updater.py +0 -0
- {synodic_client-0.0.1.dev57 → synodic_client-0.0.1.dev62}/tests/unit/test_uri.py +0 -0
- {synodic_client-0.0.1.dev57 → synodic_client-0.0.1.dev62}/tests/unit/test_workers.py +0 -0
- {synodic_client-0.0.1.dev57 → synodic_client-0.0.1.dev62}/tests/unit/windows/__init__.py +0 -0
- {synodic_client-0.0.1.dev57 → synodic_client-0.0.1.dev62}/tests/unit/windows/conftest.py +0 -0
- {synodic_client-0.0.1.dev57 → synodic_client-0.0.1.dev62}/tests/unit/windows/test_protocol.py +0 -0
- {synodic_client-0.0.1.dev57 → synodic_client-0.0.1.dev62}/tests/unit/windows/test_startup.py +0 -0
{synodic_client-0.0.1.dev57 → synodic_client-0.0.1.dev62}/synodic_client/application/init.py
RENAMED
|
@@ -53,12 +53,16 @@ def run_startup_preamble(exe_path: str | None = None) -> None:
|
|
|
53
53
|
# Seed user config from the build config (one-time propagation).
|
|
54
54
|
seed_user_config_from_build()
|
|
55
55
|
|
|
56
|
-
|
|
56
|
+
frozen = getattr(sys, 'frozen', False)
|
|
57
|
+
|
|
58
|
+
if frozen:
|
|
59
|
+
register_protocol(exe_path)
|
|
57
60
|
|
|
58
61
|
config = resolve_config()
|
|
59
|
-
if
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
62
|
+
if frozen:
|
|
63
|
+
if config.auto_start:
|
|
64
|
+
register_startup(exe_path)
|
|
65
|
+
else:
|
|
66
|
+
remove_startup()
|
|
63
67
|
|
|
64
|
-
logger.info('Startup preamble complete (auto_start=%s)', config.auto_start)
|
|
68
|
+
logger.info('Startup preamble complete (auto_start=%s, frozen=%s)', config.auto_start, frozen)
|
|
@@ -315,11 +315,10 @@ class ToolsView(QWidget):
|
|
|
315
315
|
pkg_tasks: dict[str, asyncio.Task] = {}
|
|
316
316
|
for plugin in updatable_plugins:
|
|
317
317
|
if plugin.name in runtime_probed:
|
|
318
|
-
#
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
)
|
|
318
|
+
# Runtime-probed plugins show only per-runtime
|
|
319
|
+
# packages; venv/project packages belong in
|
|
320
|
+
# ProjectsView and are intentionally skipped here.
|
|
321
|
+
continue
|
|
323
322
|
else:
|
|
324
323
|
pkg_tasks[plugin.name] = tg.create_task(
|
|
325
324
|
self._gather_packages(
|
|
@@ -390,10 +389,6 @@ class ToolsView(QWidget):
|
|
|
390
389
|
for plugin in kind_buckets[kind]:
|
|
391
390
|
if plugin.name in data.runtime_packages:
|
|
392
391
|
self._build_runtime_sections(plugin, data, auto_update_map)
|
|
393
|
-
# Also emit venv packages (if any) as a separate
|
|
394
|
-
# provider header without a runtime tag.
|
|
395
|
-
if data.packages_map.get(plugin.name):
|
|
396
|
-
self._build_plugin_section(plugin, data, auto_update_map)
|
|
397
392
|
else:
|
|
398
393
|
self._build_plugin_section(plugin, data, auto_update_map)
|
|
399
394
|
|
|
@@ -120,6 +120,7 @@ class SettingsWindow(QMainWindow):
|
|
|
120
120
|
|
|
121
121
|
scroll.setWidget(container)
|
|
122
122
|
self.setCentralWidget(scroll)
|
|
123
|
+
self._scroll_content = container
|
|
123
124
|
|
|
124
125
|
def _build_updates_section(self) -> CardFrame:
|
|
125
126
|
"""Construct the *Updates* settings card."""
|
|
@@ -221,6 +222,9 @@ class SettingsWindow(QMainWindow):
|
|
|
221
222
|
card = CardFrame('Startup')
|
|
222
223
|
self._auto_start_check = QCheckBox('Start with Windows')
|
|
223
224
|
self._auto_start_check.toggled.connect(self._on_auto_start_changed)
|
|
225
|
+
if not getattr(sys, 'frozen', False):
|
|
226
|
+
self._auto_start_check.setEnabled(False)
|
|
227
|
+
self._auto_start_check.setToolTip('Auto-start is only available for installed builds')
|
|
224
228
|
card.content_layout.addWidget(self._auto_start_check)
|
|
225
229
|
return card
|
|
226
230
|
|
|
@@ -301,6 +305,15 @@ class SettingsWindow(QMainWindow):
|
|
|
301
305
|
"""Re-enable the *Check for Updates* button after a check completes."""
|
|
302
306
|
self._check_updates_btn.setEnabled(True)
|
|
303
307
|
|
|
308
|
+
def update_config(self, config: ResolvedConfig) -> None:
|
|
309
|
+
"""Replace the internal config snapshot without emitting signals.
|
|
310
|
+
|
|
311
|
+
Called by controllers that persist timestamps so that the next
|
|
312
|
+
:meth:`sync_from_config` sees fresh data instead of the stale
|
|
313
|
+
snapshot captured at construction time.
|
|
314
|
+
"""
|
|
315
|
+
self._config = config
|
|
316
|
+
|
|
304
317
|
def set_last_checked(self, timestamp: str) -> None:
|
|
305
318
|
"""Update the *last updated* label from an ISO 8601 timestamp."""
|
|
306
319
|
relative = _format_relative_time(timestamp)
|
|
@@ -314,8 +327,14 @@ class SettingsWindow(QMainWindow):
|
|
|
314
327
|
def show(self) -> None:
|
|
315
328
|
"""Sync controls from config, size to content, then show the window."""
|
|
316
329
|
self.sync_from_config()
|
|
317
|
-
#
|
|
318
|
-
|
|
330
|
+
# QScrollArea doesn't propagate its content's sizeHint, so
|
|
331
|
+
# adjustSize() only reaches the minimum. Compute the ideal
|
|
332
|
+
# height from the content widget directly.
|
|
333
|
+
content_hint = self._scroll_content.sizeHint()
|
|
334
|
+
margins = self._scroll_content.layout().contentsMargins()
|
|
335
|
+
ideal_w = max(content_hint.width() + margins.left() + margins.right(), self.minimumWidth())
|
|
336
|
+
ideal_h = max(content_hint.height() + margins.top() + margins.bottom(), self.minimumHeight())
|
|
337
|
+
self.resize(ideal_w, ideal_h)
|
|
319
338
|
super().show()
|
|
320
339
|
self.raise_()
|
|
321
340
|
self.activateWindow()
|
|
@@ -388,10 +407,11 @@ class SettingsWindow(QMainWindow):
|
|
|
388
407
|
|
|
389
408
|
def _on_auto_start_changed(self, checked: bool) -> None:
|
|
390
409
|
self._config = update_user_config(auto_start=checked)
|
|
391
|
-
if
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
410
|
+
if getattr(sys, 'frozen', False):
|
|
411
|
+
if checked:
|
|
412
|
+
register_startup(sys.executable)
|
|
413
|
+
else:
|
|
414
|
+
remove_startup()
|
|
395
415
|
self.settings_changed.emit(self._config)
|
|
396
416
|
|
|
397
417
|
def _on_debug_logging_changed(self, checked: bool) -> None:
|
|
@@ -128,6 +128,7 @@ class SpinnerWidget(QWidget):
|
|
|
128
128
|
# Auto-overlay: track parent geometry via event filter
|
|
129
129
|
if parent is not None:
|
|
130
130
|
self.setAutoFillBackground(True)
|
|
131
|
+
self.setStyleSheet('background: palette(window);')
|
|
131
132
|
self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
|
|
132
133
|
parent.installEventFilter(self)
|
|
133
134
|
self.setGeometry(parent.rect())
|
|
@@ -382,7 +382,12 @@ class ToolUpdateOrchestrator:
|
|
|
382
382
|
for pkg_name in result.updated_packages:
|
|
383
383
|
key = f'{plugin_name}/{pkg_name}' if plugin_name else pkg_name
|
|
384
384
|
existing[key] = now
|
|
385
|
-
update_user_config(last_tool_updates=existing)
|
|
385
|
+
resolved = update_user_config(last_tool_updates=existing)
|
|
386
|
+
# Refresh the config on the tools view so the next rebuild
|
|
387
|
+
# picks up the updated timestamps instead of stale data.
|
|
388
|
+
tools_view_ref = self._window.tools_view
|
|
389
|
+
if tools_view_ref is not None:
|
|
390
|
+
tools_view_ref._config = resolved
|
|
386
391
|
|
|
387
392
|
# Clear updating state on widgets
|
|
388
393
|
tools_view = self._window.tools_view
|
|
@@ -132,7 +132,8 @@ class UpdateController:
|
|
|
132
132
|
def _persist_check_timestamp(self) -> None:
|
|
133
133
|
"""Persist the current time as *last_client_update* and refresh the label."""
|
|
134
134
|
ts = datetime.now(UTC).isoformat()
|
|
135
|
-
update_user_config(last_client_update=ts)
|
|
135
|
+
resolved = update_user_config(last_client_update=ts)
|
|
136
|
+
self._settings_window.update_config(resolved)
|
|
136
137
|
self._settings_window.set_last_checked(ts)
|
|
137
138
|
|
|
138
139
|
def _report_error(self, message: str, *, silent: bool) -> None:
|
|
@@ -329,7 +330,9 @@ class UpdateController:
|
|
|
329
330
|
|
|
330
331
|
# Persist the client-update timestamp (actual update downloaded)
|
|
331
332
|
ts = datetime.now(UTC).isoformat()
|
|
332
|
-
update_user_config(last_client_update=ts)
|
|
333
|
+
resolved = update_user_config(last_client_update=ts)
|
|
334
|
+
self._settings_window.update_config(resolved)
|
|
335
|
+
self._settings_window.set_last_checked(ts)
|
|
333
336
|
|
|
334
337
|
self._pending_version = version
|
|
335
338
|
|
|
@@ -6,15 +6,14 @@ log-level switching via :func:`set_debug_level`.
|
|
|
6
6
|
|
|
7
7
|
import logging
|
|
8
8
|
import sys
|
|
9
|
-
import tempfile
|
|
10
9
|
from logging.handlers import RotatingFileHandler
|
|
11
10
|
from pathlib import Path
|
|
12
11
|
|
|
13
|
-
from synodic_client.config import is_dev_mode
|
|
12
|
+
from synodic_client.config import config_dir, is_dev_mode
|
|
14
13
|
|
|
15
14
|
_LOG_FILENAME = 'synodic.log'
|
|
16
15
|
_LOG_FILENAME_DEV = 'synodic-dev.log'
|
|
17
|
-
_MAX_BYTES =
|
|
16
|
+
_MAX_BYTES = 1_048_576 # 1 MB
|
|
18
17
|
_BACKUP_COUNT = 3
|
|
19
18
|
_FORMAT = '%(asctime)s [%(levelname)s] %(name)s: %(message)s'
|
|
20
19
|
|
|
@@ -24,13 +23,13 @@ _debug_active: bool = False
|
|
|
24
23
|
def log_path() -> Path:
|
|
25
24
|
"""Return the path to the application log file.
|
|
26
25
|
|
|
27
|
-
The file lives
|
|
28
|
-
|
|
26
|
+
The file lives under ``config_dir() / 'logs'`` so that agents and
|
|
27
|
+
developers can find it at a deterministic, well-known location.
|
|
29
28
|
|
|
30
29
|
Returns:
|
|
31
30
|
Path to the log file.
|
|
32
31
|
"""
|
|
33
|
-
return
|
|
32
|
+
return config_dir() / 'logs' / (_LOG_FILENAME_DEV if is_dev_mode() else _LOG_FILENAME)
|
|
34
33
|
|
|
35
34
|
|
|
36
35
|
class EagerRotatingFileHandler(RotatingFileHandler):
|
|
@@ -68,6 +67,8 @@ def configure_logging(*, debug: bool = False) -> None:
|
|
|
68
67
|
|
|
69
68
|
logging.basicConfig(level=logging.INFO)
|
|
70
69
|
|
|
70
|
+
log_path().parent.mkdir(parents=True, exist_ok=True)
|
|
71
|
+
|
|
71
72
|
handler = EagerRotatingFileHandler(
|
|
72
73
|
str(log_path()),
|
|
73
74
|
maxBytes=_MAX_BYTES,
|
{synodic_client-0.0.1.dev57 → synodic_client-0.0.1.dev62}/tests/unit/qt/test_gather_packages.py
RENAMED
|
@@ -922,7 +922,6 @@ class TestRuntimePluginSupport:
|
|
|
922
922
|
|
|
923
923
|
# Expected widget counts (avoids PLR2004)
|
|
924
924
|
_EXPECTED_RUNTIME_PROVIDERS = 2
|
|
925
|
-
_EXPECTED_RUNTIME_PROVIDERS_WITH_VENV = 3
|
|
926
925
|
_EXPECTED_DEFAULT_RT_PACKAGES = 2
|
|
927
926
|
_EXPECTED_NON_DEFAULT_RT_PACKAGES = 1
|
|
928
927
|
|
|
@@ -1050,8 +1049,8 @@ class TestPerRuntimeDisplay:
|
|
|
1050
1049
|
assert len(non_default_rows) == _EXPECTED_NON_DEFAULT_RT_PACKAGES
|
|
1051
1050
|
assert non_default_rows[0]._package_name == 'django'
|
|
1052
1051
|
|
|
1053
|
-
def
|
|
1054
|
-
"""Venv packages appear in
|
|
1052
|
+
def test_venv_packages_excluded_for_runtime_probed_plugin(self) -> None:
|
|
1053
|
+
"""Venv packages must not appear in ToolsView for runtime-probed plugins."""
|
|
1055
1054
|
view = ToolsView(_make_porringer(), _make_config())
|
|
1056
1055
|
default_exe = Path('C:/Python314/python.exe')
|
|
1057
1056
|
plugin = self._pip_plugin()
|
|
@@ -1078,13 +1077,8 @@ class TestPerRuntimeDisplay:
|
|
|
1078
1077
|
view._build_widget_tree(data)
|
|
1079
1078
|
|
|
1080
1079
|
providers = [w for w in view._section_widgets if isinstance(w, PluginProviderHeader)]
|
|
1081
|
-
# 2 runtime providers
|
|
1082
|
-
assert len(providers) ==
|
|
1083
|
-
|
|
1084
|
-
# The last provider should NOT have a runtime tag
|
|
1085
|
-
last_provider = providers[-1]
|
|
1086
|
-
runtime_tags = [w for w in last_provider.findChildren(QLabel) if 'Python' in w.text()]
|
|
1087
|
-
assert len(runtime_tags) == 0, 'Venv provider should not have a runtime tag'
|
|
1080
|
+
# Only the 2 runtime providers — no extra venv provider
|
|
1081
|
+
assert len(providers) == _EXPECTED_RUNTIME_PROVIDERS
|
|
1088
1082
|
|
|
1089
1083
|
def test_runtime_tag_uses_default_style(self) -> None:
|
|
1090
1084
|
"""The default runtime tag uses the green highlight style."""
|
|
@@ -2,12 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
4
|
import sys
|
|
5
|
-
import tempfile
|
|
6
5
|
from pathlib import Path
|
|
7
6
|
from unittest.mock import patch
|
|
8
7
|
|
|
9
8
|
from synodic_client.application.screen.settings import SettingsWindow
|
|
10
|
-
from synodic_client.config import set_dev_mode
|
|
9
|
+
from synodic_client.config import config_dir, set_dev_mode
|
|
11
10
|
from synodic_client.logging import (
|
|
12
11
|
EagerRotatingFileHandler,
|
|
13
12
|
configure_logging,
|
|
@@ -19,10 +18,10 @@ class TestLogPath:
|
|
|
19
18
|
"""Tests for log_path()."""
|
|
20
19
|
|
|
21
20
|
@staticmethod
|
|
22
|
-
def
|
|
23
|
-
"""log_path() should resolve inside
|
|
21
|
+
def test_returns_path_in_config_logs_dir() -> None:
|
|
22
|
+
"""log_path() should resolve inside config_dir() / 'logs'."""
|
|
24
23
|
path = log_path()
|
|
25
|
-
assert path.parent ==
|
|
24
|
+
assert path.parent == config_dir() / 'logs'
|
|
26
25
|
|
|
27
26
|
@staticmethod
|
|
28
27
|
def test_filename() -> None:
|
|
@@ -272,8 +272,8 @@ class TestSettingsCallbacks:
|
|
|
272
272
|
signal_spy.assert_called_once_with(new_config)
|
|
273
273
|
|
|
274
274
|
@staticmethod
|
|
275
|
-
def
|
|
276
|
-
"""Enabling auto-start calls register_startup."""
|
|
275
|
+
def test_auto_start_registers_startup_when_frozen() -> None:
|
|
276
|
+
"""Enabling auto-start calls register_startup in frozen builds."""
|
|
277
277
|
config = _make_config()
|
|
278
278
|
window = _make_window(config)
|
|
279
279
|
|
|
@@ -282,14 +282,15 @@ class TestSettingsCallbacks:
|
|
|
282
282
|
patch('synodic_client.application.screen.settings.update_user_config', return_value=new_config),
|
|
283
283
|
patch('synodic_client.application.screen.settings.register_startup') as mock_register,
|
|
284
284
|
patch('synodic_client.application.screen.settings.is_startup_registered', return_value=False),
|
|
285
|
+
patch('synodic_client.application.screen.settings.getattr', return_value=True),
|
|
285
286
|
):
|
|
286
287
|
window._auto_start_check.setChecked(True)
|
|
287
288
|
|
|
288
289
|
mock_register.assert_called_once()
|
|
289
290
|
|
|
290
291
|
@staticmethod
|
|
291
|
-
def
|
|
292
|
-
"""Disabling auto-start calls remove_startup."""
|
|
292
|
+
def test_auto_start_removes_startup_when_frozen() -> None:
|
|
293
|
+
"""Disabling auto-start calls remove_startup in frozen builds."""
|
|
293
294
|
config = _make_config(auto_start=True)
|
|
294
295
|
window = _make_window(config)
|
|
295
296
|
# Manually set initial state without triggering signals
|
|
@@ -301,11 +302,31 @@ class TestSettingsCallbacks:
|
|
|
301
302
|
with (
|
|
302
303
|
patch('synodic_client.application.screen.settings.update_user_config', return_value=new_config),
|
|
303
304
|
patch('synodic_client.application.screen.settings.remove_startup') as mock_remove,
|
|
305
|
+
patch('synodic_client.application.screen.settings.getattr', return_value=True),
|
|
304
306
|
):
|
|
305
307
|
window._auto_start_check.setChecked(False)
|
|
306
308
|
|
|
307
309
|
mock_remove.assert_called_once()
|
|
308
310
|
|
|
311
|
+
@staticmethod
|
|
312
|
+
def test_auto_start_skips_registry_when_not_frozen() -> None:
|
|
313
|
+
"""Auto-start toggle persists config but skips registry in non-frozen builds."""
|
|
314
|
+
config = _make_config()
|
|
315
|
+
window = _make_window(config)
|
|
316
|
+
|
|
317
|
+
new_config = _make_config(auto_start=True)
|
|
318
|
+
with (
|
|
319
|
+
patch('synodic_client.application.screen.settings.update_user_config', return_value=new_config),
|
|
320
|
+
patch('synodic_client.application.screen.settings.register_startup') as mock_register,
|
|
321
|
+
patch('synodic_client.application.screen.settings.remove_startup') as mock_remove,
|
|
322
|
+
patch('synodic_client.application.screen.settings.is_startup_registered', return_value=False),
|
|
323
|
+
patch('synodic_client.application.screen.settings.getattr', return_value=False),
|
|
324
|
+
):
|
|
325
|
+
window._auto_start_check.setChecked(True)
|
|
326
|
+
|
|
327
|
+
mock_register.assert_not_called()
|
|
328
|
+
mock_remove.assert_not_called()
|
|
329
|
+
|
|
309
330
|
|
|
310
331
|
# ---------------------------------------------------------------------------
|
|
311
332
|
# sync_from_config does not emit signals
|
|
@@ -383,3 +404,46 @@ class TestCheckForUpdatesButton:
|
|
|
383
404
|
window.set_checking()
|
|
384
405
|
assert window._check_updates_btn.isEnabled() is False
|
|
385
406
|
assert window._update_status_label.text() == 'Checking\u2026'
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
# ---------------------------------------------------------------------------
|
|
410
|
+
# update_config — silent config refresh
|
|
411
|
+
# ---------------------------------------------------------------------------
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
class TestUpdateConfig:
|
|
415
|
+
"""Verify that update_config refreshes _config without emitting signals."""
|
|
416
|
+
|
|
417
|
+
@staticmethod
|
|
418
|
+
def test_update_config_replaces_internal_config() -> None:
|
|
419
|
+
"""update_config should replace _config with the new snapshot."""
|
|
420
|
+
window = _make_window(_make_config(last_client_update=None))
|
|
421
|
+
new_config = _make_config(last_client_update='2026-03-09T12:00:00+00:00')
|
|
422
|
+
|
|
423
|
+
window.update_config(new_config)
|
|
424
|
+
|
|
425
|
+
assert window._config is new_config
|
|
426
|
+
assert window._config.last_client_update == '2026-03-09T12:00:00+00:00'
|
|
427
|
+
|
|
428
|
+
@staticmethod
|
|
429
|
+
def test_update_config_does_not_emit_settings_changed() -> None:
|
|
430
|
+
"""update_config must NOT emit settings_changed to avoid circular reinit."""
|
|
431
|
+
window = _make_window()
|
|
432
|
+
signal_spy = MagicMock()
|
|
433
|
+
window.settings_changed.connect(signal_spy)
|
|
434
|
+
|
|
435
|
+
window.update_config(_make_config(update_channel='dev'))
|
|
436
|
+
|
|
437
|
+
signal_spy.assert_not_called()
|
|
438
|
+
|
|
439
|
+
@staticmethod
|
|
440
|
+
def test_sync_after_update_config_uses_new_timestamp() -> None:
|
|
441
|
+
"""sync_from_config after update_config should display the refreshed timestamp."""
|
|
442
|
+
window = _make_window(_make_config(last_client_update=None))
|
|
443
|
+
assert window._last_client_update_label.text() == ''
|
|
444
|
+
|
|
445
|
+
new_config = _make_config(last_client_update='2026-03-09T12:00:00+00:00')
|
|
446
|
+
window.update_config(new_config)
|
|
447
|
+
window.sync_from_config()
|
|
448
|
+
|
|
449
|
+
assert 'Last updated:' in window._last_client_update_label.text()
|
{synodic_client-0.0.1.dev57 → synodic_client-0.0.1.dev62}/tests/unit/qt/test_update_controller.py
RENAMED
|
@@ -414,3 +414,57 @@ class TestCheckError:
|
|
|
414
414
|
ctrl._on_check_error('timeout', silent=True)
|
|
415
415
|
|
|
416
416
|
assert banner.state.name == 'HIDDEN'
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
# ---------------------------------------------------------------------------
|
|
420
|
+
# Timestamp sync — _persist_check_timestamp updates config
|
|
421
|
+
# ---------------------------------------------------------------------------
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
class TestPersistCheckTimestamp:
|
|
425
|
+
"""Verify _persist_check_timestamp syncs the settings config."""
|
|
426
|
+
|
|
427
|
+
@staticmethod
|
|
428
|
+
def test_persist_updates_settings_config() -> None:
|
|
429
|
+
"""_persist_check_timestamp should call update_config on the settings window."""
|
|
430
|
+
ctrl, _app, _client, _banner, settings = _make_controller()
|
|
431
|
+
|
|
432
|
+
fake_resolved = _make_config(last_client_update='2026-03-09T00:00:00+00:00')
|
|
433
|
+
with patch(
|
|
434
|
+
'synodic_client.application.update_controller.update_user_config',
|
|
435
|
+
return_value=fake_resolved,
|
|
436
|
+
):
|
|
437
|
+
ctrl._persist_check_timestamp()
|
|
438
|
+
|
|
439
|
+
settings.update_config.assert_called_once_with(fake_resolved)
|
|
440
|
+
settings.set_last_checked.assert_called_once()
|
|
441
|
+
|
|
442
|
+
@staticmethod
|
|
443
|
+
def test_on_check_finished_success_syncs_config() -> None:
|
|
444
|
+
"""A successful check should persist timestamp AND sync settings config."""
|
|
445
|
+
ctrl, _app, _client, _banner, settings = _make_controller()
|
|
446
|
+
result = UpdateInfo(available=False, current_version=Version('1.0.0'))
|
|
447
|
+
|
|
448
|
+
fake_resolved = _make_config(last_client_update='2026-03-09T00:00:00+00:00')
|
|
449
|
+
with patch(
|
|
450
|
+
'synodic_client.application.update_controller.update_user_config',
|
|
451
|
+
return_value=fake_resolved,
|
|
452
|
+
):
|
|
453
|
+
ctrl._on_check_finished(result, silent=True)
|
|
454
|
+
|
|
455
|
+
settings.update_config.assert_called_once_with(fake_resolved)
|
|
456
|
+
|
|
457
|
+
@staticmethod
|
|
458
|
+
def test_download_finished_syncs_config_and_label() -> None:
|
|
459
|
+
"""_on_download_finished should sync config and update the label."""
|
|
460
|
+
ctrl, _app, _client, _banner, settings = _make_controller(auto_apply=False)
|
|
461
|
+
|
|
462
|
+
fake_resolved = _make_config(last_client_update='2026-03-09T00:00:00+00:00')
|
|
463
|
+
with patch(
|
|
464
|
+
'synodic_client.application.update_controller.update_user_config',
|
|
465
|
+
return_value=fake_resolved,
|
|
466
|
+
):
|
|
467
|
+
ctrl._on_download_finished(True, '2.0.0')
|
|
468
|
+
|
|
469
|
+
settings.update_config.assert_called_once_with(fake_resolved)
|
|
470
|
+
settings.set_last_checked.assert_called_once()
|
|
@@ -28,6 +28,7 @@ class TestRunStartupPreamble:
|
|
|
28
28
|
patch(f'{_MODULE}.resolve_config') as mock_resolve,
|
|
29
29
|
patch(f'{_MODULE}.register_startup'),
|
|
30
30
|
patch(f'{_MODULE}.remove_startup'),
|
|
31
|
+
patch(f'{_MODULE}.getattr', return_value=True),
|
|
31
32
|
):
|
|
32
33
|
mock_resolve.return_value = MagicMock(auto_start=True)
|
|
33
34
|
run_startup_preamble(r'C:\app\synodic.exe')
|
|
@@ -38,13 +39,14 @@ class TestRunStartupPreamble:
|
|
|
38
39
|
|
|
39
40
|
@staticmethod
|
|
40
41
|
def test_registers_startup_when_auto_start_true() -> None:
|
|
41
|
-
"""register_startup is called when auto_start is True."""
|
|
42
|
+
"""register_startup is called when auto_start is True and frozen."""
|
|
42
43
|
with (
|
|
43
44
|
patch(f'{_MODULE}.seed_user_config_from_build'),
|
|
44
45
|
patch(f'{_MODULE}.register_protocol'),
|
|
45
46
|
patch(f'{_MODULE}.resolve_config') as mock_resolve,
|
|
46
47
|
patch(f'{_MODULE}.register_startup') as mock_register,
|
|
47
48
|
patch(f'{_MODULE}.remove_startup') as mock_remove,
|
|
49
|
+
patch(f'{_MODULE}.getattr', return_value=True),
|
|
48
50
|
):
|
|
49
51
|
mock_resolve.return_value = MagicMock(auto_start=True)
|
|
50
52
|
run_startup_preamble(r'C:\app\synodic.exe')
|
|
@@ -54,13 +56,14 @@ class TestRunStartupPreamble:
|
|
|
54
56
|
|
|
55
57
|
@staticmethod
|
|
56
58
|
def test_removes_startup_when_auto_start_false() -> None:
|
|
57
|
-
"""remove_startup is called when auto_start is False."""
|
|
59
|
+
"""remove_startup is called when auto_start is False and frozen."""
|
|
58
60
|
with (
|
|
59
61
|
patch(f'{_MODULE}.seed_user_config_from_build'),
|
|
60
62
|
patch(f'{_MODULE}.register_protocol'),
|
|
61
63
|
patch(f'{_MODULE}.resolve_config') as mock_resolve,
|
|
62
64
|
patch(f'{_MODULE}.register_startup') as mock_register,
|
|
63
65
|
patch(f'{_MODULE}.remove_startup') as mock_remove,
|
|
66
|
+
patch(f'{_MODULE}.getattr', return_value=True),
|
|
64
67
|
):
|
|
65
68
|
mock_resolve.return_value = MagicMock(auto_start=False)
|
|
66
69
|
run_startup_preamble(r'C:\app\synodic.exe')
|
|
@@ -78,6 +81,7 @@ class TestRunStartupPreamble:
|
|
|
78
81
|
patch(f'{_MODULE}.register_startup') as mock_register,
|
|
79
82
|
patch(f'{_MODULE}.remove_startup'),
|
|
80
83
|
patch(f'{_MODULE}.sys') as mock_sys,
|
|
84
|
+
patch(f'{_MODULE}.getattr', return_value=True),
|
|
81
85
|
):
|
|
82
86
|
mock_sys.executable = r'C:\Python\python.exe'
|
|
83
87
|
mock_resolve.return_value = MagicMock(auto_start=True)
|
|
@@ -86,6 +90,24 @@ class TestRunStartupPreamble:
|
|
|
86
90
|
mock_proto.assert_called_once_with(r'C:\Python\python.exe')
|
|
87
91
|
mock_register.assert_called_once_with(r'C:\Python\python.exe')
|
|
88
92
|
|
|
93
|
+
@staticmethod
|
|
94
|
+
def test_skips_registry_when_not_frozen() -> None:
|
|
95
|
+
"""Protocol and startup registration are skipped in non-frozen builds."""
|
|
96
|
+
with (
|
|
97
|
+
patch(f'{_MODULE}.seed_user_config_from_build'),
|
|
98
|
+
patch(f'{_MODULE}.register_protocol') as mock_proto,
|
|
99
|
+
patch(f'{_MODULE}.resolve_config') as mock_resolve,
|
|
100
|
+
patch(f'{_MODULE}.register_startup') as mock_register,
|
|
101
|
+
patch(f'{_MODULE}.remove_startup') as mock_remove,
|
|
102
|
+
patch(f'{_MODULE}.getattr', return_value=False),
|
|
103
|
+
):
|
|
104
|
+
mock_resolve.return_value = MagicMock(auto_start=True)
|
|
105
|
+
run_startup_preamble(r'C:\Python\python.exe')
|
|
106
|
+
|
|
107
|
+
mock_proto.assert_not_called()
|
|
108
|
+
mock_register.assert_not_called()
|
|
109
|
+
mock_remove.assert_not_called()
|
|
110
|
+
|
|
89
111
|
@staticmethod
|
|
90
112
|
def test_idempotent_on_second_call() -> None:
|
|
91
113
|
"""A second call is a no-op; the preamble runs only once."""
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{synodic_client-0.0.1.dev57 → synodic_client-0.0.1.dev62}/synodic_client/application/__init__.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev57 → synodic_client-0.0.1.dev62}/synodic_client/application/bootstrap.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev57 → synodic_client-0.0.1.dev62}/synodic_client/application/data.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev57 → synodic_client-0.0.1.dev62}/synodic_client/application/icon.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev57 → synodic_client-0.0.1.dev62}/synodic_client/application/instance.py
RENAMED
|
File without changes
|
|
File without changes
|
{synodic_client-0.0.1.dev57 → synodic_client-0.0.1.dev62}/synodic_client/application/schema.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{synodic_client-0.0.1.dev57 → synodic_client-0.0.1.dev62}/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
|
{synodic_client-0.0.1.dev57 → synodic_client-0.0.1.dev62}/synodic_client/application/screen/tray.py
RENAMED
|
File without changes
|
|
File without changes
|
{synodic_client-0.0.1.dev57 → synodic_client-0.0.1.dev62}/synodic_client/application/theme.py
RENAMED
|
File without changes
|
|
File without changes
|
{synodic_client-0.0.1.dev57 → synodic_client-0.0.1.dev62}/synodic_client/application/workers.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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{synodic_client-0.0.1.dev57 → synodic_client-0.0.1.dev62}/tests/unit/qt/test_install_preview.py
RENAMED
|
File without changes
|
|
File without changes
|
{synodic_client-0.0.1.dev57 → synodic_client-0.0.1.dev62}/tests/unit/qt/test_preview_model.py
RENAMED
|
File without changes
|
|
File without changes
|
{synodic_client-0.0.1.dev57 → synodic_client-0.0.1.dev62}/tests/unit/qt/test_tray_window_show.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev57 → synodic_client-0.0.1.dev62}/tests/unit/qt/test_update_banner.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev57 → synodic_client-0.0.1.dev62}/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
|
|
File without changes
|
|
File without changes
|
{synodic_client-0.0.1.dev57 → synodic_client-0.0.1.dev62}/tests/unit/windows/test_protocol.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev57 → synodic_client-0.0.1.dev62}/tests/unit/windows/test_startup.py
RENAMED
|
File without changes
|