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.
- {synodic_client-0.0.1.dev29 → synodic_client-0.0.1.dev30}/PKG-INFO +3 -3
- {synodic_client-0.0.1.dev29 → synodic_client-0.0.1.dev30}/pyproject.toml +3 -3
- synodic_client-0.0.1.dev30/synodic_client/_version.py +1 -0
- {synodic_client-0.0.1.dev29 → synodic_client-0.0.1.dev30}/synodic_client/application/qt.py +6 -1
- {synodic_client-0.0.1.dev29 → synodic_client-0.0.1.dev30}/synodic_client/config.py +10 -1
- {synodic_client-0.0.1.dev29 → synodic_client-0.0.1.dev30}/synodic_client/resolution.py +10 -4
- {synodic_client-0.0.1.dev29 → synodic_client-0.0.1.dev30}/tests/unit/test_config.py +30 -0
- {synodic_client-0.0.1.dev29 → synodic_client-0.0.1.dev30}/tests/unit/test_resolution.py +74 -12
- {synodic_client-0.0.1.dev29 → synodic_client-0.0.1.dev30}/tests/unit/windows/test_startup.py +0 -71
- synodic_client-0.0.1.dev29/synodic_client/_version.py +0 -1
- {synodic_client-0.0.1.dev29 → synodic_client-0.0.1.dev30}/LICENSE.md +0 -0
- {synodic_client-0.0.1.dev29 → synodic_client-0.0.1.dev30}/README.md +0 -0
- {synodic_client-0.0.1.dev29 → synodic_client-0.0.1.dev30}/synodic_client/__init__.py +0 -0
- {synodic_client-0.0.1.dev29 → synodic_client-0.0.1.dev30}/synodic_client/__main__.py +0 -0
- {synodic_client-0.0.1.dev29 → synodic_client-0.0.1.dev30}/synodic_client/application/__init__.py +0 -0
- {synodic_client-0.0.1.dev29 → synodic_client-0.0.1.dev30}/synodic_client/application/bootstrap.py +0 -0
- {synodic_client-0.0.1.dev29 → synodic_client-0.0.1.dev30}/synodic_client/application/icon.py +0 -0
- {synodic_client-0.0.1.dev29 → synodic_client-0.0.1.dev30}/synodic_client/application/instance.py +0 -0
- {synodic_client-0.0.1.dev29 → synodic_client-0.0.1.dev30}/synodic_client/application/screen/__init__.py +0 -0
- {synodic_client-0.0.1.dev29 → synodic_client-0.0.1.dev30}/synodic_client/application/screen/action_card.py +0 -0
- {synodic_client-0.0.1.dev29 → synodic_client-0.0.1.dev30}/synodic_client/application/screen/card.py +0 -0
- {synodic_client-0.0.1.dev29 → synodic_client-0.0.1.dev30}/synodic_client/application/screen/install.py +0 -0
- {synodic_client-0.0.1.dev29 → synodic_client-0.0.1.dev30}/synodic_client/application/screen/log_panel.py +0 -0
- {synodic_client-0.0.1.dev29 → synodic_client-0.0.1.dev30}/synodic_client/application/screen/screen.py +0 -0
- {synodic_client-0.0.1.dev29 → synodic_client-0.0.1.dev30}/synodic_client/application/screen/spinner.py +0 -0
- {synodic_client-0.0.1.dev29 → synodic_client-0.0.1.dev30}/synodic_client/application/screen/tray.py +0 -0
- {synodic_client-0.0.1.dev29 → synodic_client-0.0.1.dev30}/synodic_client/application/theme.py +0 -0
- {synodic_client-0.0.1.dev29 → synodic_client-0.0.1.dev30}/synodic_client/application/uri.py +0 -0
- {synodic_client-0.0.1.dev29 → synodic_client-0.0.1.dev30}/synodic_client/cli.py +0 -0
- {synodic_client-0.0.1.dev29 → synodic_client-0.0.1.dev30}/synodic_client/client.py +0 -0
- {synodic_client-0.0.1.dev29 → synodic_client-0.0.1.dev30}/synodic_client/logging.py +0 -0
- {synodic_client-0.0.1.dev29 → synodic_client-0.0.1.dev30}/synodic_client/protocol.py +0 -0
- {synodic_client-0.0.1.dev29 → synodic_client-0.0.1.dev30}/synodic_client/py.typed +0 -0
- {synodic_client-0.0.1.dev29 → synodic_client-0.0.1.dev30}/synodic_client/startup.py +0 -0
- {synodic_client-0.0.1.dev29 → synodic_client-0.0.1.dev30}/synodic_client/updater.py +0 -0
- {synodic_client-0.0.1.dev29 → synodic_client-0.0.1.dev30}/tests/__init__.py +0 -0
- {synodic_client-0.0.1.dev29 → synodic_client-0.0.1.dev30}/tests/conftest.py +0 -0
- {synodic_client-0.0.1.dev29 → synodic_client-0.0.1.dev30}/tests/unit/__init__.py +0 -0
- {synodic_client-0.0.1.dev29 → synodic_client-0.0.1.dev30}/tests/unit/qt/__init__.py +0 -0
- {synodic_client-0.0.1.dev29 → synodic_client-0.0.1.dev30}/tests/unit/qt/conftest.py +0 -0
- {synodic_client-0.0.1.dev29 → synodic_client-0.0.1.dev30}/tests/unit/qt/test_action_card.py +0 -0
- {synodic_client-0.0.1.dev29 → synodic_client-0.0.1.dev30}/tests/unit/qt/test_install_preview.py +0 -0
- {synodic_client-0.0.1.dev29 → synodic_client-0.0.1.dev30}/tests/unit/qt/test_log_panel.py +0 -0
- {synodic_client-0.0.1.dev29 → synodic_client-0.0.1.dev30}/tests/unit/qt/test_logging.py +0 -0
- {synodic_client-0.0.1.dev29 → synodic_client-0.0.1.dev30}/tests/unit/test_cli.py +0 -0
- {synodic_client-0.0.1.dev29 → synodic_client-0.0.1.dev30}/tests/unit/test_client_updater.py +0 -0
- {synodic_client-0.0.1.dev29 → synodic_client-0.0.1.dev30}/tests/unit/test_client_version.py +0 -0
- {synodic_client-0.0.1.dev29 → synodic_client-0.0.1.dev30}/tests/unit/test_examples.py +0 -0
- {synodic_client-0.0.1.dev29 → synodic_client-0.0.1.dev30}/tests/unit/test_install.py +0 -0
- {synodic_client-0.0.1.dev29 → synodic_client-0.0.1.dev30}/tests/unit/test_updater.py +0 -0
- {synodic_client-0.0.1.dev29 → synodic_client-0.0.1.dev30}/tests/unit/test_uri.py +0 -0
- {synodic_client-0.0.1.dev29 → synodic_client-0.0.1.dev30}/tests/unit/windows/__init__.py +0 -0
- {synodic_client-0.0.1.dev29 → synodic_client-0.0.1.dev30}/tests/unit/windows/conftest.py +0 -0
- {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.
|
|
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.
|
|
11
|
+
Requires-Dist: porringer>=0.2.1.dev49
|
|
12
12
|
Requires-Dist: qasync>=0.28.0
|
|
13
|
-
Requires-Dist: velopack>=0.0.
|
|
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.
|
|
13
|
+
"porringer>=0.2.1.dev49",
|
|
14
14
|
"qasync>=0.28.0",
|
|
15
|
-
"velopack>=0.0.
|
|
15
|
+
"velopack>=0.0.1442.dev64255",
|
|
16
16
|
"typer>=0.24.1",
|
|
17
17
|
]
|
|
18
|
-
version = "0.0.1.
|
|
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(
|
|
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
|
|
36
|
-
|
|
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.
|
|
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
|
|
38
|
-
global_cfg = GlobalConfiguration(
|
|
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
|
|
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 == '/
|
|
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
|
|
65
|
-
global_cfg = GlobalConfiguration(
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
237
|
-
(system_dir / 'config.json').write_text(
|
|
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
|
{synodic_client-0.0.1.dev29 → synodic_client-0.0.1.dev30}/tests/unit/windows/test_startup.py
RENAMED
|
@@ -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'
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{synodic_client-0.0.1.dev29 → synodic_client-0.0.1.dev30}/synodic_client/application/__init__.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev29 → synodic_client-0.0.1.dev30}/synodic_client/application/bootstrap.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev29 → synodic_client-0.0.1.dev30}/synodic_client/application/icon.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev29 → synodic_client-0.0.1.dev30}/synodic_client/application/instance.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{synodic_client-0.0.1.dev29 → synodic_client-0.0.1.dev30}/synodic_client/application/screen/card.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{synodic_client-0.0.1.dev29 → synodic_client-0.0.1.dev30}/synodic_client/application/screen/tray.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev29 → synodic_client-0.0.1.dev30}/synodic_client/application/theme.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
|
{synodic_client-0.0.1.dev29 → synodic_client-0.0.1.dev30}/tests/unit/qt/test_install_preview.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.dev29 → synodic_client-0.0.1.dev30}/tests/unit/windows/test_protocol.py
RENAMED
|
File without changes
|