synodic-client 0.0.1.dev70__tar.gz → 0.0.1.dev71__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.
Files changed (80) hide show
  1. {synodic_client-0.0.1.dev70 → synodic_client-0.0.1.dev71}/PKG-INFO +1 -1
  2. {synodic_client-0.0.1.dev70 → synodic_client-0.0.1.dev71}/pyproject.toml +1 -1
  3. {synodic_client-0.0.1.dev70 → synodic_client-0.0.1.dev71}/synodic_client/application/update_controller.py +15 -1
  4. {synodic_client-0.0.1.dev70 → synodic_client-0.0.1.dev71}/tests/unit/qt/test_update_controller.py +88 -0
  5. {synodic_client-0.0.1.dev70 → synodic_client-0.0.1.dev71}/LICENSE.md +0 -0
  6. {synodic_client-0.0.1.dev70 → synodic_client-0.0.1.dev71}/README.md +0 -0
  7. {synodic_client-0.0.1.dev70 → synodic_client-0.0.1.dev71}/synodic_client/__init__.py +0 -0
  8. {synodic_client-0.0.1.dev70 → synodic_client-0.0.1.dev71}/synodic_client/__main__.py +0 -0
  9. {synodic_client-0.0.1.dev70 → synodic_client-0.0.1.dev71}/synodic_client/application/__init__.py +0 -0
  10. {synodic_client-0.0.1.dev70 → synodic_client-0.0.1.dev71}/synodic_client/application/bootstrap.py +0 -0
  11. {synodic_client-0.0.1.dev70 → synodic_client-0.0.1.dev71}/synodic_client/application/config_store.py +0 -0
  12. {synodic_client-0.0.1.dev70 → synodic_client-0.0.1.dev71}/synodic_client/application/data.py +0 -0
  13. {synodic_client-0.0.1.dev70 → synodic_client-0.0.1.dev71}/synodic_client/application/icon.py +0 -0
  14. {synodic_client-0.0.1.dev70 → synodic_client-0.0.1.dev71}/synodic_client/application/init.py +0 -0
  15. {synodic_client-0.0.1.dev70 → synodic_client-0.0.1.dev71}/synodic_client/application/instance.py +0 -0
  16. {synodic_client-0.0.1.dev70 → synodic_client-0.0.1.dev71}/synodic_client/application/package_state.py +0 -0
  17. {synodic_client-0.0.1.dev70 → synodic_client-0.0.1.dev71}/synodic_client/application/qt.py +0 -0
  18. {synodic_client-0.0.1.dev70 → synodic_client-0.0.1.dev71}/synodic_client/application/schema.py +0 -0
  19. {synodic_client-0.0.1.dev70 → synodic_client-0.0.1.dev71}/synodic_client/application/screen/__init__.py +0 -0
  20. {synodic_client-0.0.1.dev70 → synodic_client-0.0.1.dev71}/synodic_client/application/screen/action_card.py +0 -0
  21. {synodic_client-0.0.1.dev70 → synodic_client-0.0.1.dev71}/synodic_client/application/screen/card.py +0 -0
  22. {synodic_client-0.0.1.dev70 → synodic_client-0.0.1.dev71}/synodic_client/application/screen/install.py +0 -0
  23. {synodic_client-0.0.1.dev70 → synodic_client-0.0.1.dev71}/synodic_client/application/screen/install_workers.py +0 -0
  24. {synodic_client-0.0.1.dev70 → synodic_client-0.0.1.dev71}/synodic_client/application/screen/log_panel.py +0 -0
  25. {synodic_client-0.0.1.dev70 → synodic_client-0.0.1.dev71}/synodic_client/application/screen/plugin_row.py +0 -0
  26. {synodic_client-0.0.1.dev70 → synodic_client-0.0.1.dev71}/synodic_client/application/screen/projects.py +0 -0
  27. {synodic_client-0.0.1.dev70 → synodic_client-0.0.1.dev71}/synodic_client/application/screen/schema.py +0 -0
  28. {synodic_client-0.0.1.dev70 → synodic_client-0.0.1.dev71}/synodic_client/application/screen/screen.py +0 -0
  29. {synodic_client-0.0.1.dev70 → synodic_client-0.0.1.dev71}/synodic_client/application/screen/settings.py +0 -0
  30. {synodic_client-0.0.1.dev70 → synodic_client-0.0.1.dev71}/synodic_client/application/screen/sidebar.py +0 -0
  31. {synodic_client-0.0.1.dev70 → synodic_client-0.0.1.dev71}/synodic_client/application/screen/spinner.py +0 -0
  32. {synodic_client-0.0.1.dev70 → synodic_client-0.0.1.dev71}/synodic_client/application/screen/tool_update_controller.py +0 -0
  33. {synodic_client-0.0.1.dev70 → synodic_client-0.0.1.dev71}/synodic_client/application/screen/tray.py +0 -0
  34. {synodic_client-0.0.1.dev70 → synodic_client-0.0.1.dev71}/synodic_client/application/screen/update_banner.py +0 -0
  35. {synodic_client-0.0.1.dev70 → synodic_client-0.0.1.dev71}/synodic_client/application/theme.py +0 -0
  36. {synodic_client-0.0.1.dev70 → synodic_client-0.0.1.dev71}/synodic_client/application/update_model.py +0 -0
  37. {synodic_client-0.0.1.dev70 → synodic_client-0.0.1.dev71}/synodic_client/application/uri.py +0 -0
  38. {synodic_client-0.0.1.dev70 → synodic_client-0.0.1.dev71}/synodic_client/application/workers.py +0 -0
  39. {synodic_client-0.0.1.dev70 → synodic_client-0.0.1.dev71}/synodic_client/cli.py +0 -0
  40. {synodic_client-0.0.1.dev70 → synodic_client-0.0.1.dev71}/synodic_client/client.py +0 -0
  41. {synodic_client-0.0.1.dev70 → synodic_client-0.0.1.dev71}/synodic_client/config.py +0 -0
  42. {synodic_client-0.0.1.dev70 → synodic_client-0.0.1.dev71}/synodic_client/logging.py +0 -0
  43. {synodic_client-0.0.1.dev70 → synodic_client-0.0.1.dev71}/synodic_client/protocol.py +0 -0
  44. {synodic_client-0.0.1.dev70 → synodic_client-0.0.1.dev71}/synodic_client/py.typed +0 -0
  45. {synodic_client-0.0.1.dev70 → synodic_client-0.0.1.dev71}/synodic_client/resolution.py +0 -0
  46. {synodic_client-0.0.1.dev70 → synodic_client-0.0.1.dev71}/synodic_client/schema.py +0 -0
  47. {synodic_client-0.0.1.dev70 → synodic_client-0.0.1.dev71}/synodic_client/startup.py +0 -0
  48. {synodic_client-0.0.1.dev70 → synodic_client-0.0.1.dev71}/synodic_client/subprocess_patch.py +0 -0
  49. {synodic_client-0.0.1.dev70 → synodic_client-0.0.1.dev71}/synodic_client/updater.py +0 -0
  50. {synodic_client-0.0.1.dev70 → synodic_client-0.0.1.dev71}/tests/__init__.py +0 -0
  51. {synodic_client-0.0.1.dev70 → synodic_client-0.0.1.dev71}/tests/conftest.py +0 -0
  52. {synodic_client-0.0.1.dev70 → synodic_client-0.0.1.dev71}/tests/unit/__init__.py +0 -0
  53. {synodic_client-0.0.1.dev70 → synodic_client-0.0.1.dev71}/tests/unit/qt/__init__.py +0 -0
  54. {synodic_client-0.0.1.dev70 → synodic_client-0.0.1.dev71}/tests/unit/qt/conftest.py +0 -0
  55. {synodic_client-0.0.1.dev70 → synodic_client-0.0.1.dev71}/tests/unit/qt/test_action_card.py +0 -0
  56. {synodic_client-0.0.1.dev70 → synodic_client-0.0.1.dev71}/tests/unit/qt/test_gather_packages.py +0 -0
  57. {synodic_client-0.0.1.dev70 → synodic_client-0.0.1.dev71}/tests/unit/qt/test_install_preview.py +0 -0
  58. {synodic_client-0.0.1.dev70 → synodic_client-0.0.1.dev71}/tests/unit/qt/test_log_panel.py +0 -0
  59. {synodic_client-0.0.1.dev70 → synodic_client-0.0.1.dev71}/tests/unit/qt/test_logging.py +0 -0
  60. {synodic_client-0.0.1.dev70 → synodic_client-0.0.1.dev71}/tests/unit/qt/test_preview_model.py +0 -0
  61. {synodic_client-0.0.1.dev70 → synodic_client-0.0.1.dev71}/tests/unit/qt/test_settings.py +0 -0
  62. {synodic_client-0.0.1.dev70 → synodic_client-0.0.1.dev71}/tests/unit/qt/test_sidebar.py +0 -0
  63. {synodic_client-0.0.1.dev70 → synodic_client-0.0.1.dev71}/tests/unit/qt/test_tray_window_show.py +0 -0
  64. {synodic_client-0.0.1.dev70 → synodic_client-0.0.1.dev71}/tests/unit/qt/test_update_banner.py +0 -0
  65. {synodic_client-0.0.1.dev70 → synodic_client-0.0.1.dev71}/tests/unit/qt/test_update_feedback.py +0 -0
  66. {synodic_client-0.0.1.dev70 → synodic_client-0.0.1.dev71}/tests/unit/test_cli.py +0 -0
  67. {synodic_client-0.0.1.dev70 → synodic_client-0.0.1.dev71}/tests/unit/test_client_updater.py +0 -0
  68. {synodic_client-0.0.1.dev70 → synodic_client-0.0.1.dev71}/tests/unit/test_client_version.py +0 -0
  69. {synodic_client-0.0.1.dev70 → synodic_client-0.0.1.dev71}/tests/unit/test_config.py +0 -0
  70. {synodic_client-0.0.1.dev70 → synodic_client-0.0.1.dev71}/tests/unit/test_examples.py +0 -0
  71. {synodic_client-0.0.1.dev70 → synodic_client-0.0.1.dev71}/tests/unit/test_init.py +0 -0
  72. {synodic_client-0.0.1.dev70 → synodic_client-0.0.1.dev71}/tests/unit/test_install.py +0 -0
  73. {synodic_client-0.0.1.dev70 → synodic_client-0.0.1.dev71}/tests/unit/test_resolution.py +0 -0
  74. {synodic_client-0.0.1.dev70 → synodic_client-0.0.1.dev71}/tests/unit/test_updater.py +0 -0
  75. {synodic_client-0.0.1.dev70 → synodic_client-0.0.1.dev71}/tests/unit/test_uri.py +0 -0
  76. {synodic_client-0.0.1.dev70 → synodic_client-0.0.1.dev71}/tests/unit/test_workers.py +0 -0
  77. {synodic_client-0.0.1.dev70 → synodic_client-0.0.1.dev71}/tests/unit/windows/__init__.py +0 -0
  78. {synodic_client-0.0.1.dev70 → synodic_client-0.0.1.dev71}/tests/unit/windows/conftest.py +0 -0
  79. {synodic_client-0.0.1.dev70 → synodic_client-0.0.1.dev71}/tests/unit/windows/test_protocol.py +0 -0
  80. {synodic_client-0.0.1.dev70 → synodic_client-0.0.1.dev71}/tests/unit/windows/test_startup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: synodic_client
3
- Version: 0.0.1.dev70
3
+ Version: 0.0.1.dev71
4
4
  Author-Email: Synodic Software <contact@synodic.software>
5
5
  License: LGPL-3.0-or-later
6
6
  Project-URL: homepage, https://github.com/synodic/synodic-client
@@ -15,7 +15,7 @@ dependencies = [
15
15
  "velopack>=0.0.1444.dev49733",
16
16
  "typer>=0.24.1",
17
17
  ]
18
- version = "0.0.1.dev70"
18
+ version = "0.0.1.dev71"
19
19
 
20
20
  [project.license]
21
21
  text = "LGPL-3.0-or-later"
@@ -236,7 +236,17 @@ class UpdateController:
236
236
  )
237
237
 
238
238
  def _reinitialize_updater(self, config: ResolvedConfig) -> None:
239
- """Re-derive update settings and restart the updater and timer."""
239
+ """Re-derive update settings and restart the updater and timer.
240
+
241
+ Cancels any in-flight check/download task and clears cached
242
+ state so the new updater starts with a clean slate.
243
+ """
244
+ if self._update_task is not None and not self._update_task.done():
245
+ self._update_task.cancel()
246
+ self._update_task = None
247
+ self._pending_version = None
248
+ self._failed_version = None
249
+
240
250
  update_cfg = resolve_update_config(config)
241
251
  self._client.initialize_updater(update_cfg)
242
252
  self._restart_auto_update_timer()
@@ -414,6 +424,10 @@ class UpdateController:
414
424
  if self._client.updater is None:
415
425
  return
416
426
 
427
+ if self._pending_version is None:
428
+ self._report_error('No downloaded update to apply — please check for updates again.', silent=silent)
429
+ return
430
+
417
431
  try:
418
432
  # Re-register the startup entry with the current exe path so
419
433
  # the registry value stays valid even if Velopack relocates
@@ -347,6 +347,7 @@ class TestApplyUpdate:
347
347
  def test_apply_update_calls_client_and_quits() -> None:
348
348
  """_apply_update should call client.apply_update_on_exit and app.quit."""
349
349
  ctrl, app, client, banner, model = _make_controller()
350
+ ctrl._pending_version = '2.0.0'
350
351
  ctrl._apply_update()
351
352
 
352
353
  client.apply_update_on_exit.assert_called_once_with(restart=True, silent=False)
@@ -366,6 +367,7 @@ class TestApplyUpdate:
366
367
  def test_apply_update_refreshes_startup_registry_when_frozen() -> None:
367
368
  """_apply_update should call sync_startup before quitting."""
368
369
  ctrl, app, client, banner, model = _make_controller()
370
+ ctrl._pending_version = '2.0.0'
369
371
 
370
372
  with (
371
373
  patch('synodic_client.application.update_controller.sync_startup') as mock_sync,
@@ -497,3 +499,89 @@ class TestPersistCheckTimestamp:
497
499
  mock_update.assert_called_once()
498
500
 
499
501
  assert len(spy.last_checked) == 1
502
+
503
+
504
+ # ---------------------------------------------------------------------------
505
+ # Reinitialise updater — stale state cleared
506
+ # ---------------------------------------------------------------------------
507
+
508
+
509
+ class TestReinitializeUpdater:
510
+ """Verify _reinitialize_updater clears stale state."""
511
+
512
+ @staticmethod
513
+ def test_reinit_resets_state_and_cancels_task() -> None:
514
+ """Reinitialising should clear pending/failed versions and cancel in-flight tasks."""
515
+ ctrl, _app, _client, _banner, _model = _make_controller()
516
+ ctrl._pending_version = '1.0.0'
517
+ ctrl._failed_version = '1.0.0'
518
+ fake_task = MagicMock()
519
+ fake_task.done.return_value = False
520
+ ctrl._update_task = fake_task
521
+
522
+ new_config = _make_config(update_channel='dev')
523
+ with patch('synodic_client.application.update_controller.resolve_update_config') as mock_ucfg:
524
+ mock_ucfg.return_value = MagicMock(
525
+ auto_update_interval_minutes=0,
526
+ channel=MagicMock(name='DEVELOPMENT'),
527
+ repo_url='https://example.com',
528
+ )
529
+ ctrl._reinitialize_updater(new_config)
530
+
531
+ assert ctrl._pending_version is None # pyrefly: ignore
532
+ assert ctrl._failed_version is None # pyrefly: ignore
533
+ fake_task.cancel.assert_called_once()
534
+
535
+ @staticmethod
536
+ def test_reinit_then_check_redownloads_same_version() -> None:
537
+ """After reinit, a check for the same version should download instead of showing ready."""
538
+ ctrl, _app, _client, _banner, _model = _make_controller(auto_apply=False)
539
+ ctrl._pending_version = '2.0.0'
540
+
541
+ new_config = _make_config(update_channel='dev')
542
+ with patch('synodic_client.application.update_controller.resolve_update_config') as mock_ucfg:
543
+ mock_ucfg.return_value = MagicMock(
544
+ auto_update_interval_minutes=0,
545
+ channel=MagicMock(name='DEVELOPMENT'),
546
+ repo_url='https://example.com',
547
+ )
548
+ ctrl._reinitialize_updater(new_config)
549
+
550
+ result = UpdateInfo(available=True, current_version=Version('1.0.0'), latest_version=Version('2.0.0'))
551
+ with patch.object(ctrl, '_start_download') as mock_dl:
552
+ ctrl._on_check_finished(result, silent=True)
553
+
554
+ mock_dl.assert_called_once_with('2.0.0', silent=True)
555
+
556
+
557
+ # ---------------------------------------------------------------------------
558
+ # Apply without pending download — guard
559
+ # ---------------------------------------------------------------------------
560
+
561
+
562
+ class TestApplyGuard:
563
+ """Verify _apply_update rejects apply when no download is pending."""
564
+
565
+ @staticmethod
566
+ def test_apply_without_pending_shows_error() -> None:
567
+ """Applying with no pending version should show an error, not crash."""
568
+ ctrl, app, client, banner, model = _make_controller()
569
+ ctrl._pending_version = None
570
+
571
+ ctrl._apply_update(silent=False)
572
+
573
+ assert banner.state.name == 'ERROR'
574
+ client.apply_update_on_exit.assert_not_called()
575
+ app.quit.assert_not_called()
576
+
577
+ @staticmethod
578
+ def test_apply_without_pending_silent_logs_only() -> None:
579
+ """Applying silently with no pending version should not show the banner."""
580
+ ctrl, app, client, banner, model = _make_controller()
581
+ ctrl._pending_version = None
582
+
583
+ ctrl._apply_update(silent=True)
584
+
585
+ assert banner.state.name == 'HIDDEN'
586
+ client.apply_update_on_exit.assert_not_called()
587
+ app.quit.assert_not_called()