synodic-client 0.0.1.dev29__tar.gz → 0.0.1.dev30__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 (54) hide show
  1. {synodic_client-0.0.1.dev29 → synodic_client-0.0.1.dev30}/PKG-INFO +3 -3
  2. {synodic_client-0.0.1.dev29 → synodic_client-0.0.1.dev30}/pyproject.toml +3 -3
  3. synodic_client-0.0.1.dev30/synodic_client/_version.py +1 -0
  4. {synodic_client-0.0.1.dev29 → synodic_client-0.0.1.dev30}/synodic_client/application/qt.py +6 -1
  5. {synodic_client-0.0.1.dev29 → synodic_client-0.0.1.dev30}/synodic_client/config.py +10 -1
  6. {synodic_client-0.0.1.dev29 → synodic_client-0.0.1.dev30}/synodic_client/resolution.py +10 -4
  7. {synodic_client-0.0.1.dev29 → synodic_client-0.0.1.dev30}/tests/unit/test_config.py +30 -0
  8. {synodic_client-0.0.1.dev29 → synodic_client-0.0.1.dev30}/tests/unit/test_resolution.py +74 -12
  9. {synodic_client-0.0.1.dev29 → synodic_client-0.0.1.dev30}/tests/unit/windows/test_startup.py +0 -71
  10. synodic_client-0.0.1.dev29/synodic_client/_version.py +0 -1
  11. {synodic_client-0.0.1.dev29 → synodic_client-0.0.1.dev30}/LICENSE.md +0 -0
  12. {synodic_client-0.0.1.dev29 → synodic_client-0.0.1.dev30}/README.md +0 -0
  13. {synodic_client-0.0.1.dev29 → synodic_client-0.0.1.dev30}/synodic_client/__init__.py +0 -0
  14. {synodic_client-0.0.1.dev29 → synodic_client-0.0.1.dev30}/synodic_client/__main__.py +0 -0
  15. {synodic_client-0.0.1.dev29 → synodic_client-0.0.1.dev30}/synodic_client/application/__init__.py +0 -0
  16. {synodic_client-0.0.1.dev29 → synodic_client-0.0.1.dev30}/synodic_client/application/bootstrap.py +0 -0
  17. {synodic_client-0.0.1.dev29 → synodic_client-0.0.1.dev30}/synodic_client/application/icon.py +0 -0
  18. {synodic_client-0.0.1.dev29 → synodic_client-0.0.1.dev30}/synodic_client/application/instance.py +0 -0
  19. {synodic_client-0.0.1.dev29 → synodic_client-0.0.1.dev30}/synodic_client/application/screen/__init__.py +0 -0
  20. {synodic_client-0.0.1.dev29 → synodic_client-0.0.1.dev30}/synodic_client/application/screen/action_card.py +0 -0
  21. {synodic_client-0.0.1.dev29 → synodic_client-0.0.1.dev30}/synodic_client/application/screen/card.py +0 -0
  22. {synodic_client-0.0.1.dev29 → synodic_client-0.0.1.dev30}/synodic_client/application/screen/install.py +0 -0
  23. {synodic_client-0.0.1.dev29 → synodic_client-0.0.1.dev30}/synodic_client/application/screen/log_panel.py +0 -0
  24. {synodic_client-0.0.1.dev29 → synodic_client-0.0.1.dev30}/synodic_client/application/screen/screen.py +0 -0
  25. {synodic_client-0.0.1.dev29 → synodic_client-0.0.1.dev30}/synodic_client/application/screen/spinner.py +0 -0
  26. {synodic_client-0.0.1.dev29 → synodic_client-0.0.1.dev30}/synodic_client/application/screen/tray.py +0 -0
  27. {synodic_client-0.0.1.dev29 → synodic_client-0.0.1.dev30}/synodic_client/application/theme.py +0 -0
  28. {synodic_client-0.0.1.dev29 → synodic_client-0.0.1.dev30}/synodic_client/application/uri.py +0 -0
  29. {synodic_client-0.0.1.dev29 → synodic_client-0.0.1.dev30}/synodic_client/cli.py +0 -0
  30. {synodic_client-0.0.1.dev29 → synodic_client-0.0.1.dev30}/synodic_client/client.py +0 -0
  31. {synodic_client-0.0.1.dev29 → synodic_client-0.0.1.dev30}/synodic_client/logging.py +0 -0
  32. {synodic_client-0.0.1.dev29 → synodic_client-0.0.1.dev30}/synodic_client/protocol.py +0 -0
  33. {synodic_client-0.0.1.dev29 → synodic_client-0.0.1.dev30}/synodic_client/py.typed +0 -0
  34. {synodic_client-0.0.1.dev29 → synodic_client-0.0.1.dev30}/synodic_client/startup.py +0 -0
  35. {synodic_client-0.0.1.dev29 → synodic_client-0.0.1.dev30}/synodic_client/updater.py +0 -0
  36. {synodic_client-0.0.1.dev29 → synodic_client-0.0.1.dev30}/tests/__init__.py +0 -0
  37. {synodic_client-0.0.1.dev29 → synodic_client-0.0.1.dev30}/tests/conftest.py +0 -0
  38. {synodic_client-0.0.1.dev29 → synodic_client-0.0.1.dev30}/tests/unit/__init__.py +0 -0
  39. {synodic_client-0.0.1.dev29 → synodic_client-0.0.1.dev30}/tests/unit/qt/__init__.py +0 -0
  40. {synodic_client-0.0.1.dev29 → synodic_client-0.0.1.dev30}/tests/unit/qt/conftest.py +0 -0
  41. {synodic_client-0.0.1.dev29 → synodic_client-0.0.1.dev30}/tests/unit/qt/test_action_card.py +0 -0
  42. {synodic_client-0.0.1.dev29 → synodic_client-0.0.1.dev30}/tests/unit/qt/test_install_preview.py +0 -0
  43. {synodic_client-0.0.1.dev29 → synodic_client-0.0.1.dev30}/tests/unit/qt/test_log_panel.py +0 -0
  44. {synodic_client-0.0.1.dev29 → synodic_client-0.0.1.dev30}/tests/unit/qt/test_logging.py +0 -0
  45. {synodic_client-0.0.1.dev29 → synodic_client-0.0.1.dev30}/tests/unit/test_cli.py +0 -0
  46. {synodic_client-0.0.1.dev29 → synodic_client-0.0.1.dev30}/tests/unit/test_client_updater.py +0 -0
  47. {synodic_client-0.0.1.dev29 → synodic_client-0.0.1.dev30}/tests/unit/test_client_version.py +0 -0
  48. {synodic_client-0.0.1.dev29 → synodic_client-0.0.1.dev30}/tests/unit/test_examples.py +0 -0
  49. {synodic_client-0.0.1.dev29 → synodic_client-0.0.1.dev30}/tests/unit/test_install.py +0 -0
  50. {synodic_client-0.0.1.dev29 → synodic_client-0.0.1.dev30}/tests/unit/test_updater.py +0 -0
  51. {synodic_client-0.0.1.dev29 → synodic_client-0.0.1.dev30}/tests/unit/test_uri.py +0 -0
  52. {synodic_client-0.0.1.dev29 → synodic_client-0.0.1.dev30}/tests/unit/windows/__init__.py +0 -0
  53. {synodic_client-0.0.1.dev29 → synodic_client-0.0.1.dev30}/tests/unit/windows/conftest.py +0 -0
  54. {synodic_client-0.0.1.dev29 → synodic_client-0.0.1.dev30}/tests/unit/windows/test_protocol.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: synodic_client
3
- Version: 0.0.1.dev29
3
+ Version: 0.0.1.dev30
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
@@ -8,9 +8,9 @@ Project-URL: repository, https://github.com/synodic/synodic-client
8
8
  Requires-Python: <3.15,>=3.14
9
9
  Requires-Dist: pyside6>=6.10.2
10
10
  Requires-Dist: packaging>=26.0
11
- Requires-Dist: porringer>=0.2.1.dev48
11
+ Requires-Dist: porringer>=0.2.1.dev49
12
12
  Requires-Dist: qasync>=0.28.0
13
- Requires-Dist: velopack>=0.0.1369.dev7516
13
+ Requires-Dist: velopack>=0.0.1442.dev64255
14
14
  Requires-Dist: typer>=0.24.1
15
15
  Description-Content-Type: text/markdown
16
16
 
@@ -10,12 +10,12 @@ requires-python = ">=3.14, <3.15"
10
10
  dependencies = [
11
11
  "pyside6>=6.10.2",
12
12
  "packaging>=26.0",
13
- "porringer>=0.2.1.dev48",
13
+ "porringer>=0.2.1.dev49",
14
14
  "qasync>=0.28.0",
15
- "velopack>=0.0.1369.dev7516",
15
+ "velopack>=0.0.1442.dev64255",
16
16
  "typer>=0.24.1",
17
17
  ]
18
- version = "0.0.1.dev29"
18
+ version = "0.0.1.dev30"
19
19
 
20
20
  [project.license]
21
21
  text = "LGPL-3.0-or-later"
@@ -0,0 +1 @@
1
+ __version__ = '0.0.1.dev30'
@@ -44,11 +44,16 @@ def _init_services(logger: logging.Logger) -> tuple[Client, API, GlobalConfigura
44
44
  update_config = resolve_update_config(config)
45
45
  client.initialize_updater(update_config)
46
46
 
47
+ cached_dirs = porringer.cache.list_directories()
48
+
47
49
  logger.info(
48
- 'Synodic Client v%s started (channel: %s, source: %s)',
50
+ 'Synodic Client v%s started (channel: %s, source: %s, '
51
+ 'config_fields_set: %s, cached_projects: %d)',
49
52
  client.version,
50
53
  update_config.channel.name,
51
54
  update_config.repo_url,
55
+ sorted(config.model_fields_set),
56
+ len(cached_dirs),
52
57
  )
53
58
 
54
59
  return client, porringer, config
@@ -194,6 +194,12 @@ def _load_global_config() -> GlobalConfiguration:
194
194
  def save_config(config: GlobalConfiguration) -> None:
195
195
  """Save configuration to the global (system) config directory.
196
196
 
197
+ Only fields that have been explicitly set (either loaded from the
198
+ existing config file or changed at runtime) are written. This
199
+ sparse serialisation ensures that build-time local-config values
200
+ do not leak into the user's global config and that future defaults
201
+ can take effect for fields the user has not customised.
202
+
197
203
  Args:
198
204
  config: The configuration to persist.
199
205
  """
@@ -202,7 +208,10 @@ def save_config(config: GlobalConfiguration) -> None:
202
208
  path = directory / _CONFIG_FILENAME
203
209
 
204
210
  try:
205
- path.write_text(config.model_dump_json(indent=2), encoding='utf-8')
211
+ path.write_text(
212
+ config.model_dump_json(indent=2, exclude_unset=True),
213
+ encoding='utf-8',
214
+ )
206
215
  logger.info('Saved config to %s', path)
207
216
  except Exception:
208
217
  logger.exception('Failed to save config to %s', path)
@@ -32,8 +32,13 @@ def merge_config(
32
32
  ) -> GlobalConfiguration:
33
33
  """Merge local overrides into a global configuration.
34
34
 
35
- Fields explicitly set (not None) in the local config override the
36
- corresponding global values.
35
+ Fields that the user has explicitly saved (present in the global
36
+ config file) take priority over local overrides. Local config
37
+ fields only fill in values the user has **not** set.
38
+
39
+ The returned object preserves the global config's
40
+ ``model_fields_set`` so that :func:`save_config` can use
41
+ ``exclude_unset=True`` to write only user-changed fields.
37
42
 
38
43
  Args:
39
44
  global_config: The user-scoped global configuration.
@@ -45,12 +50,13 @@ def merge_config(
45
50
  if local_config is None:
46
51
  return global_config
47
52
 
53
+ user_set = global_config.model_fields_set
48
54
  merged = global_config.model_dump()
49
55
  for field_name, value in local_config.model_dump().items():
50
- if value is not None:
56
+ if value is not None and field_name not in user_set:
51
57
  merged[field_name] = value
52
58
 
53
- return GlobalConfiguration.model_validate(merged)
59
+ return GlobalConfiguration.model_construct(_fields_set=set(user_set), **merged)
54
60
 
55
61
 
56
62
  def resolve_config() -> GlobalConfiguration:
@@ -165,6 +165,20 @@ class TestSaveConfig:
165
165
  assert data['update_source'] == '/my/releases'
166
166
  assert data['update_channel'] == 'stable'
167
167
 
168
+ @staticmethod
169
+ def test_sparse_serialization(tmp_path: Path) -> None:
170
+ """Verify save_config only writes user-set fields (exclude_unset)."""
171
+ config = GlobalConfiguration(update_channel='dev')
172
+
173
+ with patch('synodic_client.config.config_dir', return_value=tmp_path):
174
+ save_config(config)
175
+
176
+ data = json.loads((tmp_path / 'config.json').read_text(encoding='utf-8'))
177
+ # Only 'update_channel' should be in the file
178
+ assert data == {'update_channel': 'dev'}
179
+ assert 'update_source' not in data
180
+ assert 'auto_update_interval_minutes' not in data
181
+
168
182
  @staticmethod
169
183
  def test_creates_directory(tmp_path: Path) -> None:
170
184
  """Verify save_config creates the directory if missing."""
@@ -189,3 +203,19 @@ class TestSaveConfig:
189
203
 
190
204
  data = json.loads(config_path.read_text(encoding='utf-8'))
191
205
  assert data['update_source'] == 'http://new-source'
206
+
207
+ @staticmethod
208
+ def test_save_load_round_trip(tmp_path: Path) -> None:
209
+ """Verify saved config can be loaded back with correct fields_set."""
210
+ original = GlobalConfiguration(update_channel='dev', auto_start=False)
211
+
212
+ with patch('synodic_client.config.config_dir', return_value=tmp_path):
213
+ save_config(original)
214
+
215
+ data = json.loads((tmp_path / 'config.json').read_text(encoding='utf-8'))
216
+ loaded = GlobalConfiguration.model_validate(data)
217
+ assert loaded.update_channel == 'dev'
218
+ assert loaded.auto_start is False
219
+ # Only saved fields should be in model_fields_set
220
+ assert loaded.model_fields_set == {'update_channel', 'auto_start'}
221
+ assert loaded.update_source is None
@@ -34,8 +34,8 @@ class TestMergeConfig:
34
34
 
35
35
  @staticmethod
36
36
  def test_local_overrides_global() -> None:
37
- """Verify local fields override global values."""
38
- global_cfg = GlobalConfiguration(update_source='/system', update_channel='stable')
37
+ """Verify local fields fill in values the user hasn't set."""
38
+ global_cfg = GlobalConfiguration(update_channel='stable')
39
39
  local_cfg = LocalConfiguration(update_source='/local')
40
40
  result = merge_config(global_cfg, local_cfg)
41
41
  assert result.update_source == '/local'
@@ -52,21 +52,57 @@ class TestMergeConfig:
52
52
 
53
53
  @staticmethod
54
54
  def test_full_override() -> None:
55
- """Verify all local fields override when set."""
55
+ """Verify local fields are ignored when the user has saved both."""
56
56
  global_cfg = GlobalConfiguration(update_source='/system', update_channel='stable')
57
57
  local_cfg = LocalConfiguration(update_source='/local', update_channel='dev')
58
58
  result = merge_config(global_cfg, local_cfg)
59
- assert result.update_source == '/local'
59
+ assert result.update_source == '/system'
60
+ assert result.update_channel == 'stable'
61
+
62
+ @staticmethod
63
+ def test_user_saved_wins_over_local() -> None:
64
+ """Verify a user-saved field takes priority over local config."""
65
+ global_cfg = GlobalConfiguration(update_channel='dev')
66
+ local_cfg = LocalConfiguration(update_channel='stable')
67
+ result = merge_config(global_cfg, local_cfg)
68
+ assert result.update_channel == 'dev'
69
+
70
+ @staticmethod
71
+ def test_local_fills_unsaved_fields() -> None:
72
+ """Verify local config fills in fields the user hasn't saved."""
73
+ global_cfg = GlobalConfiguration(update_channel='dev')
74
+ local_cfg = LocalConfiguration(update_source='/local/releases')
75
+ result = merge_config(global_cfg, local_cfg)
60
76
  assert result.update_channel == 'dev'
77
+ assert result.update_source == '/local/releases'
78
+
79
+ @staticmethod
80
+ def test_preserves_model_fields_set() -> None:
81
+ """Verify merge preserves the global config's model_fields_set."""
82
+ global_cfg = GlobalConfiguration(update_channel='dev')
83
+ local_cfg = LocalConfiguration(update_source='/local')
84
+ result = merge_config(global_cfg, local_cfg)
85
+ # Only 'update_channel' was in the user's config file
86
+ assert result.model_fields_set == {'update_channel'}
87
+ # But the runtime value from local config is available
88
+ assert result.update_source == '/local'
61
89
 
62
90
  @staticmethod
63
91
  def test_local_overrides_plugin_auto_update() -> None:
64
- """Verify local plugin_auto_update overrides global."""
65
- global_cfg = GlobalConfiguration(plugin_auto_update={'pip': False})
92
+ """Verify local plugin_auto_update fills in when user hasn't set it."""
93
+ global_cfg = GlobalConfiguration()
66
94
  local_cfg = LocalConfiguration(plugin_auto_update={'pip': True, 'pipx': False})
67
95
  result = merge_config(global_cfg, local_cfg)
68
96
  assert result.plugin_auto_update == {'pip': True, 'pipx': False}
69
97
 
98
+ @staticmethod
99
+ def test_user_plugin_auto_update_wins() -> None:
100
+ """Verify user-saved plugin_auto_update wins over local."""
101
+ global_cfg = GlobalConfiguration(plugin_auto_update={'pip': False})
102
+ local_cfg = LocalConfiguration(plugin_auto_update={'pip': True, 'pipx': False})
103
+ result = merge_config(global_cfg, local_cfg)
104
+ assert result.plugin_auto_update == {'pip': False}
105
+
70
106
 
71
107
  class TestResolveAutoStart:
72
108
  """Tests for resolve_auto_start."""
@@ -169,7 +205,7 @@ class TestResolveConfig:
169
205
 
170
206
  @staticmethod
171
207
  def test_local_overrides_global_per_field(tmp_path: Path) -> None:
172
- """Verify local config overrides global on a per-field basis."""
208
+ """Verify local config fills in fields the user hasn't saved."""
173
209
  local_data = {'update_source': '/local/releases'}
174
210
  local_path = tmp_path / 'local' / 'config.json'
175
211
  local_path.parent.mkdir()
@@ -177,7 +213,8 @@ class TestResolveConfig:
177
213
 
178
214
  system_dir = tmp_path / 'system'
179
215
  system_dir.mkdir()
180
- system_data = {'update_source': '/system/releases', 'update_channel': 'stable'}
216
+ # User has only saved update_channel, not update_source
217
+ system_data = {'update_channel': 'stable'}
181
218
  (system_dir / 'config.json').write_text(json.dumps(system_data), encoding='utf-8')
182
219
 
183
220
  with (
@@ -186,7 +223,9 @@ class TestResolveConfig:
186
223
  ):
187
224
  config = resolve_config()
188
225
 
226
+ # Local fills in update_source since user didn't set it
189
227
  assert config.update_source == '/local/releases'
228
+ # User's saved update_channel is preserved
190
229
  assert config.update_channel == 'stable'
191
230
 
192
231
  @staticmethod
@@ -226,15 +265,15 @@ class TestResolveConfig:
226
265
 
227
266
  @staticmethod
228
267
  def test_portable_takes_precedence(tmp_path: Path) -> None:
229
- """Verify portable config values override system config."""
268
+ """Verify portable config fills in fields user hasn't saved."""
230
269
  portable_data = {'update_source': '/portable/releases', 'update_channel': 'dev'}
231
270
  portable_path = tmp_path / 'config.json'
232
271
  portable_path.write_text(json.dumps(portable_data), encoding='utf-8')
233
272
 
234
273
  system_dir = tmp_path / 'system'
235
274
  system_dir.mkdir()
236
- system_data = {'update_source': '/system/releases', 'update_channel': 'stable'}
237
- (system_dir / 'config.json').write_text(json.dumps(system_data), encoding='utf-8')
275
+ # User has NOT saved any config (empty file or missing)
276
+ (system_dir / 'config.json').write_text('{}', encoding='utf-8')
238
277
 
239
278
  with (
240
279
  patch('synodic_client.config._portable_config_path', return_value=portable_path),
@@ -245,6 +284,27 @@ class TestResolveConfig:
245
284
  assert config.update_source == '/portable/releases'
246
285
  assert config.update_channel == 'dev'
247
286
 
287
+ @staticmethod
288
+ def test_user_saved_wins_over_portable(tmp_path: Path) -> None:
289
+ """Verify user-saved values in global config win over portable."""
290
+ portable_data = {'update_source': '/portable/releases', 'update_channel': 'dev'}
291
+ portable_path = tmp_path / 'config.json'
292
+ portable_path.write_text(json.dumps(portable_data), encoding='utf-8')
293
+
294
+ system_dir = tmp_path / 'system'
295
+ system_dir.mkdir()
296
+ system_data = {'update_source': '/system/releases', 'update_channel': 'stable'}
297
+ (system_dir / 'config.json').write_text(json.dumps(system_data), encoding='utf-8')
298
+
299
+ with (
300
+ patch('synodic_client.config._portable_config_path', return_value=portable_path),
301
+ patch('synodic_client.config.config_dir', return_value=system_dir),
302
+ ):
303
+ config = resolve_config()
304
+
305
+ assert config.update_source == '/system/releases'
306
+ assert config.update_channel == 'stable'
307
+
248
308
 
249
309
  class TestResolveUpdateConfig:
250
310
  """Tests for resolve_update_config."""
@@ -339,7 +399,9 @@ class TestUpdateAndResolve:
339
399
  assert result.channel == UpdateChannel.DEVELOPMENT
340
400
  assert result.repo_url == '/my/source'
341
401
 
342
- # Verify file was saved
402
+ # Verify file was saved (sparse — only user-set fields)
343
403
  saved = json.loads((tmp_path / 'config.json').read_text(encoding='utf-8'))
344
404
  assert saved['update_source'] == '/my/source'
345
405
  assert saved['update_channel'] == 'dev'
406
+ # Unset fields should not appear in the sparse output
407
+ assert 'auto_update_interval_minutes' not in saved
@@ -3,8 +3,6 @@
3
3
  import winreg
4
4
  from unittest.mock import MagicMock, patch
5
5
 
6
- import pytest
7
-
8
6
  from synodic_client.startup import (
9
7
  RUN_KEY_PATH,
10
8
  STARTUP_VALUE_NAME,
@@ -13,9 +11,6 @@ from synodic_client.startup import (
13
11
  remove_startup,
14
12
  )
15
13
 
16
- _TEST_VALUE_NAME = f'{STARTUP_VALUE_NAME}_test'
17
- """Temporary value name used by integration tests to avoid clobbering the real registration."""
18
-
19
14
 
20
15
  class TestRegisterStartup:
21
16
  """Tests for register_startup."""
@@ -123,70 +118,4 @@ class TestIsStartupRegistered:
123
118
  patch.object(winreg, 'QueryValueEx', side_effect=FileNotFoundError),
124
119
  ):
125
120
  assert is_startup_registered() is False
126
-
127
-
128
- class TestStartupIntegration:
129
- """Integration tests that read/write real registry values under a test name."""
130
-
131
- @staticmethod
132
- def test_register_creates_valid_entry() -> None:
133
- """Register under a test value name, verify, then clean up."""
134
- test_exe = r'C:\test\synodic_test.exe'
135
-
136
- try:
137
- with patch('synodic_client.startup.STARTUP_VALUE_NAME', _TEST_VALUE_NAME):
138
- register_startup(test_exe)
139
-
140
- with winreg.OpenKey(winreg.HKEY_CURRENT_USER, RUN_KEY_PATH, 0, winreg.KEY_QUERY_VALUE) as key:
141
- value, reg_type = winreg.QueryValueEx(key, _TEST_VALUE_NAME)
142
- assert reg_type == winreg.REG_SZ
143
- assert test_exe in value
144
-
145
- finally:
146
- with patch('synodic_client.startup.STARTUP_VALUE_NAME', _TEST_VALUE_NAME):
147
- remove_startup()
148
-
149
- @staticmethod
150
- def test_remove_deletes_entry() -> None:
151
- """Register then remove under a test value name, verify it is gone."""
152
- with patch('synodic_client.startup.STARTUP_VALUE_NAME', _TEST_VALUE_NAME):
153
- register_startup(r'C:\test\synodic_test.exe')
154
- remove_startup()
155
-
156
- with (
157
- winreg.OpenKey(winreg.HKEY_CURRENT_USER, RUN_KEY_PATH, 0, winreg.KEY_QUERY_VALUE) as key,
158
- pytest.raises(FileNotFoundError),
159
- ):
160
- winreg.QueryValueEx(key, _TEST_VALUE_NAME)
161
-
162
- @staticmethod
163
- def test_register_is_idempotent() -> None:
164
- """Calling register twice with a different exe updates the value."""
165
- exe_v1 = r'C:\test\v1\synodic.exe'
166
- exe_v2 = r'C:\test\v2\synodic.exe'
167
-
168
- try:
169
- with patch('synodic_client.startup.STARTUP_VALUE_NAME', _TEST_VALUE_NAME):
170
- register_startup(exe_v1)
171
- register_startup(exe_v2)
172
-
173
- with winreg.OpenKey(winreg.HKEY_CURRENT_USER, RUN_KEY_PATH, 0, winreg.KEY_QUERY_VALUE) as key:
174
- value, _ = winreg.QueryValueEx(key, _TEST_VALUE_NAME)
175
- assert exe_v2 in value
176
- assert exe_v1 not in value
177
-
178
- finally:
179
- with patch('synodic_client.startup.STARTUP_VALUE_NAME', _TEST_VALUE_NAME):
180
- remove_startup()
181
-
182
- @staticmethod
183
- def test_is_startup_registered_reflects_state() -> None:
184
- """Verify is_startup_registered returns the correct state."""
185
- with patch('synodic_client.startup.STARTUP_VALUE_NAME', _TEST_VALUE_NAME):
186
- assert is_startup_registered() is False
187
-
188
- register_startup(r'C:\test\synodic_test.exe')
189
- assert is_startup_registered() is True
190
-
191
- remove_startup()
192
121
  assert is_startup_registered() is False
@@ -1 +0,0 @@
1
- __version__ = '0.0.1.dev29'