diff --git a/pdm.lock b/pdm.lock index 45b8fe0..5113050 100644 --- a/pdm.lock +++ b/pdm.lock @@ -5,7 +5,7 @@ groups = ["default", "build", "dev", "lint", "test"] strategy = ["inherit_metadata"] lock_version = "4.5.0" -content_hash = "sha256:f07f57cb6c2748e33a2352195ca8fdf838b74c3bcabb338a75519eeff09874e1" +content_hash = "sha256:d83e9bf82339b08c1aeabb99f878d82f4bf3606ee7f616bada987d44b6a38160" [[metadata.targets]] requires_python = ">=3.14,<3.15" @@ -336,7 +336,7 @@ files = [ [[package]] name = "porringer" -version = "0.2.1.dev48" +version = "0.2.1.dev49" requires_python = ">=3.14" summary = "" groups = ["default"] @@ -349,8 +349,8 @@ dependencies = [ "userpath>=1.9.2", ] files = [ - {file = "porringer-0.2.1.dev48-py3-none-any.whl", hash = "sha256:fb8faa9d5a4e45c93a024eb5e5311368cb80423df33d117d96751dd80096eb0d"}, - {file = "porringer-0.2.1.dev48.tar.gz", hash = "sha256:30e93bf4c404165700c5436c9e7ee7d4def51171d49ba8a03449d5fa06f90618"}, + {file = "porringer-0.2.1.dev49-py3-none-any.whl", hash = "sha256:f162456b180b1d90a58d649bd7b38b07b7a3b7520b71f8b7009ddae533e10521"}, + {file = "porringer-0.2.1.dev49.tar.gz", hash = "sha256:731ba5b3bf4c9461636246a21de1e416d8e029392bc0740ea1656c7a7d91ece9"}, ] [[package]] @@ -765,25 +765,25 @@ files = [ [[package]] name = "velopack" -version = "0.0.1369.dev7516" +version = "0.0.1442.dev64255" requires_python = ">=3.8" summary = "Installer and automatic update framework for cross-platform desktop applications" groups = ["default"] files = [ - {file = "velopack-0.0.1369.dev7516-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:414877bf22205c11e276662191905885a59f9a1f6e8d2f4b7f5bc9654abf448f"}, - {file = "velopack-0.0.1369.dev7516-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:1188540fe9d8afbbeb82d44969e825eab24b177bb99cdf4265b44db428d2450f"}, - {file = "velopack-0.0.1369.dev7516-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:66ab86bdb6d5e4b5495bd27aa48aec09d51a7c7ee8553a2fee263b7ccdbfaa3f"}, - {file = "velopack-0.0.1369.dev7516-cp37-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:aad06b05693ef0f5ad4a05352d3dbc3919ce65fe5efa4df0375344f6cd331792"}, - {file = "velopack-0.0.1369.dev7516-cp37-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7aa87f0ed9b9643939b98114bbbb105d318e6142dede77e50c63ee0aa6a45310"}, - {file = "velopack-0.0.1369.dev7516-cp37-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:87e58d8faf981ea5643fa9aa94bee37b816195091a72f3b77915d54a2f144fc8"}, - {file = "velopack-0.0.1369.dev7516-cp37-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99dad522b9505d18c3409050309a24418ca2bc71b06a67753488c02905581874"}, - {file = "velopack-0.0.1369.dev7516-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d476d40f6dc04aa62a4ff1f6a3e70a63fac0651e946ce51a8f6d614d9e4c09c2"}, - {file = "velopack-0.0.1369.dev7516-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:3c6c8a3f17e1c3cd4152cd41c9ff7ffa845a22fe244ab24be76b9b4f7963dfee"}, - {file = "velopack-0.0.1369.dev7516-cp37-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:86c0b28eda29296955d34e91c7da7776af130ff8eec49772f7eba3984cdf4fb2"}, - {file = "velopack-0.0.1369.dev7516-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:66949bcc82d2d63bda6e4576ab9d3b90ab55ea7323581c7693a55131b7aa0c84"}, - {file = "velopack-0.0.1369.dev7516-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6047d2f80c559252b093f32311822e7392dbfef3fe5cbf099653c1f5b85c8bf4"}, - {file = "velopack-0.0.1369.dev7516-cp37-abi3-win32.whl", hash = "sha256:fe0da2d522eba0c925a780618695058f943d621704a1312b464ace7bdaab9847"}, - {file = "velopack-0.0.1369.dev7516-cp37-abi3-win_amd64.whl", hash = "sha256:1cfa675600923096d19e31f9368209a676e8eae1410632bef59b140a3521c16b"}, - {file = "velopack-0.0.1369.dev7516-cp37-abi3-win_arm64.whl", hash = "sha256:a8135e422e4fd30c09d89049b927bd6a78bb6f4a724d3888ccd43afe7739d49a"}, - {file = "velopack-0.0.1369.dev7516.tar.gz", hash = "sha256:b8db23570043050c68400742f1cac10c185be3cffb3b378d08761e6f522cdc4a"}, + {file = "velopack-0.0.1442.dev64255-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:8a29f8de20f807517deb9d7b6839acc4982c4f77ed00eb8a9ed33f6df33f9170"}, + {file = "velopack-0.0.1442.dev64255-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:3ae4ae56916d8f3378e2ec90b3f3871b62c9db7f9fc2f7aedb9744f486938a65"}, + {file = "velopack-0.0.1442.dev64255-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:912484fa5622554af79c5799c831bf0c0ef58d3af53365ad8b69ba3df6802705"}, + {file = "velopack-0.0.1442.dev64255-cp37-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:358bba9a56efb7f06d5b16ef96b92020a2d89e9e7fbc1339082b5fb6ee47da76"}, + {file = "velopack-0.0.1442.dev64255-cp37-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:93b32b688f27f6d46c6ae3a5246befa23f3d670aa6f1bf98e9afdb5f7b53d0d9"}, + {file = "velopack-0.0.1442.dev64255-cp37-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2fcab425cf5360783cddef2a7e53e34d66917c8dbf3e3808f064f83d0d19865b"}, + {file = "velopack-0.0.1442.dev64255-cp37-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75e960883efa8f2b184f399416bbd85d679491c3dd13ba896de311d928ea74b0"}, + {file = "velopack-0.0.1442.dev64255-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4039decb048315ac858cb551ad083fee43a36410286abb90f245e7794961de4a"}, + {file = "velopack-0.0.1442.dev64255-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9321b68d42bcac050aae36fbdbf64243f7c4e2ad666d39788f27406889cf1ac7"}, + {file = "velopack-0.0.1442.dev64255-cp37-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:c21ee8c6360fad3a71800afd183270bcd1adead868818ebb76228e4b4b314953"}, + {file = "velopack-0.0.1442.dev64255-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:8d031bb79d0ad3cae8d77bd99125b61355f7c8a157596c2d068ad848cb764dea"}, + {file = "velopack-0.0.1442.dev64255-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:df7aac8b54767b87a82eb3ce51b7e97c86c707f18b1edd29332dc4f6fc578c70"}, + {file = "velopack-0.0.1442.dev64255-cp37-abi3-win32.whl", hash = "sha256:04c2465bcd3b0fc55b16e03690aa9ee75c3de20945c5aa4a3cfbece782fdb02d"}, + {file = "velopack-0.0.1442.dev64255-cp37-abi3-win_amd64.whl", hash = "sha256:7f910a3df65c57bc9cdcc2ab2d1457c27a3a8ea8bbe8d838bbc78a5f0d282a69"}, + {file = "velopack-0.0.1442.dev64255-cp37-abi3-win_arm64.whl", hash = "sha256:b1c3999bcee7f4a6f638b75423e46dfc3d2ee9e0065a79c22ae6724042736839"}, + {file = "velopack-0.0.1442.dev64255.tar.gz", hash = "sha256:50da944cb6ebac090ed77fb0d4ad55434af2710ba9181a2c5a2447363e9e4473"}, ] diff --git a/pyproject.toml b/pyproject.toml index 164fa80..894939c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,9 +14,9 @@ requires-python = ">=3.14, <3.15" dependencies = [ "pyside6>=6.10.2", "packaging>=26.0", - "porringer>=0.2.1.dev48", + "porringer>=0.2.1.dev49", "qasync>=0.28.0", - "velopack>=0.0.1369.dev7516", + "velopack>=0.0.1442.dev64255", "typer>=0.24.1", ] diff --git a/synodic_client/application/qt.py b/synodic_client/application/qt.py index f74afab..523f563 100644 --- a/synodic_client/application/qt.py +++ b/synodic_client/application/qt.py @@ -44,11 +44,16 @@ def _init_services(logger: logging.Logger) -> tuple[Client, API, GlobalConfigura update_config = resolve_update_config(config) client.initialize_updater(update_config) + cached_dirs = porringer.cache.list_directories() + logger.info( - 'Synodic Client v%s started (channel: %s, source: %s)', + 'Synodic Client v%s started (channel: %s, source: %s, ' + 'config_fields_set: %s, cached_projects: %d)', client.version, update_config.channel.name, update_config.repo_url, + sorted(config.model_fields_set), + len(cached_dirs), ) return client, porringer, config diff --git a/synodic_client/config.py b/synodic_client/config.py index 695a2aa..de9c646 100644 --- a/synodic_client/config.py +++ b/synodic_client/config.py @@ -194,6 +194,12 @@ def _load_global_config() -> GlobalConfiguration: def save_config(config: GlobalConfiguration) -> None: """Save configuration to the global (system) config directory. + Only fields that have been explicitly set (either loaded from the + existing config file or changed at runtime) are written. This + sparse serialisation ensures that build-time local-config values + do not leak into the user's global config and that future defaults + can take effect for fields the user has not customised. + Args: config: The configuration to persist. """ @@ -202,7 +208,10 @@ def save_config(config: GlobalConfiguration) -> None: path = directory / _CONFIG_FILENAME try: - path.write_text(config.model_dump_json(indent=2), encoding='utf-8') + path.write_text( + config.model_dump_json(indent=2, exclude_unset=True), + encoding='utf-8', + ) logger.info('Saved config to %s', path) except Exception: logger.exception('Failed to save config to %s', path) diff --git a/synodic_client/resolution.py b/synodic_client/resolution.py index 3a96fd3..35265c3 100644 --- a/synodic_client/resolution.py +++ b/synodic_client/resolution.py @@ -32,8 +32,13 @@ def merge_config( ) -> GlobalConfiguration: """Merge local overrides into a global configuration. - Fields explicitly set (not None) in the local config override the - corresponding global values. + Fields that the user has explicitly saved (present in the global + config file) take priority over local overrides. Local config + fields only fill in values the user has **not** set. + + The returned object preserves the global config's + ``model_fields_set`` so that :func:`save_config` can use + ``exclude_unset=True`` to write only user-changed fields. Args: global_config: The user-scoped global configuration. @@ -45,12 +50,13 @@ def merge_config( if local_config is None: return global_config + user_set = global_config.model_fields_set merged = global_config.model_dump() for field_name, value in local_config.model_dump().items(): - if value is not None: + if value is not None and field_name not in user_set: merged[field_name] = value - return GlobalConfiguration.model_validate(merged) + return GlobalConfiguration.model_construct(_fields_set=set(user_set), **merged) def resolve_config() -> GlobalConfiguration: diff --git a/tests/unit/test_config.py b/tests/unit/test_config.py index 7017655..5b2d628 100644 --- a/tests/unit/test_config.py +++ b/tests/unit/test_config.py @@ -165,6 +165,20 @@ def test_creates_file(tmp_path: Path) -> None: assert data['update_source'] == '/my/releases' assert data['update_channel'] == 'stable' + @staticmethod + def test_sparse_serialization(tmp_path: Path) -> None: + """Verify save_config only writes user-set fields (exclude_unset).""" + config = GlobalConfiguration(update_channel='dev') + + with patch('synodic_client.config.config_dir', return_value=tmp_path): + save_config(config) + + data = json.loads((tmp_path / 'config.json').read_text(encoding='utf-8')) + # Only 'update_channel' should be in the file + assert data == {'update_channel': 'dev'} + assert 'update_source' not in data + assert 'auto_update_interval_minutes' not in data + @staticmethod def test_creates_directory(tmp_path: Path) -> None: """Verify save_config creates the directory if missing.""" @@ -189,3 +203,19 @@ def test_overwrites_existing(tmp_path: Path) -> None: data = json.loads(config_path.read_text(encoding='utf-8')) assert data['update_source'] == 'http://new-source' + + @staticmethod + def test_save_load_round_trip(tmp_path: Path) -> None: + """Verify saved config can be loaded back with correct fields_set.""" + original = GlobalConfiguration(update_channel='dev', auto_start=False) + + with patch('synodic_client.config.config_dir', return_value=tmp_path): + save_config(original) + + data = json.loads((tmp_path / 'config.json').read_text(encoding='utf-8')) + loaded = GlobalConfiguration.model_validate(data) + assert loaded.update_channel == 'dev' + assert loaded.auto_start is False + # Only saved fields should be in model_fields_set + assert loaded.model_fields_set == {'update_channel', 'auto_start'} + assert loaded.update_source is None diff --git a/tests/unit/test_resolution.py b/tests/unit/test_resolution.py index 83e6cf1..d5ff46c 100644 --- a/tests/unit/test_resolution.py +++ b/tests/unit/test_resolution.py @@ -34,8 +34,8 @@ def test_returns_global_when_no_local() -> None: @staticmethod def test_local_overrides_global() -> None: - """Verify local fields override global values.""" - global_cfg = GlobalConfiguration(update_source='/system', update_channel='stable') + """Verify local fields fill in values the user hasn't set.""" + global_cfg = GlobalConfiguration(update_channel='stable') local_cfg = LocalConfiguration(update_source='/local') result = merge_config(global_cfg, local_cfg) assert result.update_source == '/local' @@ -52,21 +52,57 @@ def test_local_none_fields_do_not_override() -> None: @staticmethod def test_full_override() -> None: - """Verify all local fields override when set.""" + """Verify local fields are ignored when the user has saved both.""" global_cfg = GlobalConfiguration(update_source='/system', update_channel='stable') local_cfg = LocalConfiguration(update_source='/local', update_channel='dev') result = merge_config(global_cfg, local_cfg) - assert result.update_source == '/local' + assert result.update_source == '/system' + assert result.update_channel == 'stable' + + @staticmethod + def test_user_saved_wins_over_local() -> None: + """Verify a user-saved field takes priority over local config.""" + global_cfg = GlobalConfiguration(update_channel='dev') + local_cfg = LocalConfiguration(update_channel='stable') + result = merge_config(global_cfg, local_cfg) + assert result.update_channel == 'dev' + + @staticmethod + def test_local_fills_unsaved_fields() -> None: + """Verify local config fills in fields the user hasn't saved.""" + global_cfg = GlobalConfiguration(update_channel='dev') + local_cfg = LocalConfiguration(update_source='/local/releases') + result = merge_config(global_cfg, local_cfg) assert result.update_channel == 'dev' + assert result.update_source == '/local/releases' + + @staticmethod + def test_preserves_model_fields_set() -> None: + """Verify merge preserves the global config's model_fields_set.""" + global_cfg = GlobalConfiguration(update_channel='dev') + local_cfg = LocalConfiguration(update_source='/local') + result = merge_config(global_cfg, local_cfg) + # Only 'update_channel' was in the user's config file + assert result.model_fields_set == {'update_channel'} + # But the runtime value from local config is available + assert result.update_source == '/local' @staticmethod def test_local_overrides_plugin_auto_update() -> None: - """Verify local plugin_auto_update overrides global.""" - global_cfg = GlobalConfiguration(plugin_auto_update={'pip': False}) + """Verify local plugin_auto_update fills in when user hasn't set it.""" + global_cfg = GlobalConfiguration() local_cfg = LocalConfiguration(plugin_auto_update={'pip': True, 'pipx': False}) result = merge_config(global_cfg, local_cfg) assert result.plugin_auto_update == {'pip': True, 'pipx': False} + @staticmethod + def test_user_plugin_auto_update_wins() -> None: + """Verify user-saved plugin_auto_update wins over local.""" + global_cfg = GlobalConfiguration(plugin_auto_update={'pip': False}) + local_cfg = LocalConfiguration(plugin_auto_update={'pip': True, 'pipx': False}) + result = merge_config(global_cfg, local_cfg) + assert result.plugin_auto_update == {'pip': False} + class TestResolveAutoStart: """Tests for resolve_auto_start.""" @@ -169,7 +205,7 @@ def test_returns_defaults_on_corrupt_json(tmp_path: Path) -> None: @staticmethod def test_local_overrides_global_per_field(tmp_path: Path) -> None: - """Verify local config overrides global on a per-field basis.""" + """Verify local config fills in fields the user hasn't saved.""" local_data = {'update_source': '/local/releases'} local_path = tmp_path / 'local' / 'config.json' local_path.parent.mkdir() @@ -177,7 +213,8 @@ def test_local_overrides_global_per_field(tmp_path: Path) -> None: system_dir = tmp_path / 'system' system_dir.mkdir() - system_data = {'update_source': '/system/releases', 'update_channel': 'stable'} + # User has only saved update_channel, not update_source + system_data = {'update_channel': 'stable'} (system_dir / 'config.json').write_text(json.dumps(system_data), encoding='utf-8') with ( @@ -186,7 +223,9 @@ def test_local_overrides_global_per_field(tmp_path: Path) -> None: ): config = resolve_config() + # Local fills in update_source since user didn't set it assert config.update_source == '/local/releases' + # User's saved update_channel is preserved assert config.update_channel == 'stable' @staticmethod @@ -226,15 +265,15 @@ def test_falls_back_to_global_on_corrupt_portable(tmp_path: Path) -> None: @staticmethod def test_portable_takes_precedence(tmp_path: Path) -> None: - """Verify portable config values override system config.""" + """Verify portable config fills in fields user hasn't saved.""" portable_data = {'update_source': '/portable/releases', 'update_channel': 'dev'} portable_path = tmp_path / 'config.json' portable_path.write_text(json.dumps(portable_data), encoding='utf-8') system_dir = tmp_path / 'system' system_dir.mkdir() - system_data = {'update_source': '/system/releases', 'update_channel': 'stable'} - (system_dir / 'config.json').write_text(json.dumps(system_data), encoding='utf-8') + # User has NOT saved any config (empty file or missing) + (system_dir / 'config.json').write_text('{}', encoding='utf-8') with ( patch('synodic_client.config._portable_config_path', return_value=portable_path), @@ -245,6 +284,27 @@ def test_portable_takes_precedence(tmp_path: Path) -> None: assert config.update_source == '/portable/releases' assert config.update_channel == 'dev' + @staticmethod + def test_user_saved_wins_over_portable(tmp_path: Path) -> None: + """Verify user-saved values in global config win over portable.""" + portable_data = {'update_source': '/portable/releases', 'update_channel': 'dev'} + portable_path = tmp_path / 'config.json' + portable_path.write_text(json.dumps(portable_data), encoding='utf-8') + + system_dir = tmp_path / 'system' + system_dir.mkdir() + system_data = {'update_source': '/system/releases', 'update_channel': 'stable'} + (system_dir / 'config.json').write_text(json.dumps(system_data), encoding='utf-8') + + with ( + patch('synodic_client.config._portable_config_path', return_value=portable_path), + patch('synodic_client.config.config_dir', return_value=system_dir), + ): + config = resolve_config() + + assert config.update_source == '/system/releases' + assert config.update_channel == 'stable' + class TestResolveUpdateConfig: """Tests for resolve_update_config.""" @@ -339,7 +399,9 @@ def test_saves_and_resolves(tmp_path: Path) -> None: assert result.channel == UpdateChannel.DEVELOPMENT assert result.repo_url == '/my/source' - # Verify file was saved + # Verify file was saved (sparse — only user-set fields) saved = json.loads((tmp_path / 'config.json').read_text(encoding='utf-8')) assert saved['update_source'] == '/my/source' assert saved['update_channel'] == 'dev' + # Unset fields should not appear in the sparse output + assert 'auto_update_interval_minutes' not in saved diff --git a/tests/unit/windows/test_startup.py b/tests/unit/windows/test_startup.py index c2300dc..f221819 100644 --- a/tests/unit/windows/test_startup.py +++ b/tests/unit/windows/test_startup.py @@ -3,8 +3,6 @@ import winreg from unittest.mock import MagicMock, patch -import pytest - from synodic_client.startup import ( RUN_KEY_PATH, STARTUP_VALUE_NAME, @@ -13,9 +11,6 @@ remove_startup, ) -_TEST_VALUE_NAME = f'{STARTUP_VALUE_NAME}_test' -"""Temporary value name used by integration tests to avoid clobbering the real registration.""" - class TestRegisterStartup: """Tests for register_startup.""" @@ -123,70 +118,4 @@ def test_returns_false_when_missing() -> None: patch.object(winreg, 'QueryValueEx', side_effect=FileNotFoundError), ): assert is_startup_registered() is False - - -class TestStartupIntegration: - """Integration tests that read/write real registry values under a test name.""" - - @staticmethod - def test_register_creates_valid_entry() -> None: - """Register under a test value name, verify, then clean up.""" - test_exe = r'C:\test\synodic_test.exe' - - try: - with patch('synodic_client.startup.STARTUP_VALUE_NAME', _TEST_VALUE_NAME): - register_startup(test_exe) - - with winreg.OpenKey(winreg.HKEY_CURRENT_USER, RUN_KEY_PATH, 0, winreg.KEY_QUERY_VALUE) as key: - value, reg_type = winreg.QueryValueEx(key, _TEST_VALUE_NAME) - assert reg_type == winreg.REG_SZ - assert test_exe in value - - finally: - with patch('synodic_client.startup.STARTUP_VALUE_NAME', _TEST_VALUE_NAME): - remove_startup() - - @staticmethod - def test_remove_deletes_entry() -> None: - """Register then remove under a test value name, verify it is gone.""" - with patch('synodic_client.startup.STARTUP_VALUE_NAME', _TEST_VALUE_NAME): - register_startup(r'C:\test\synodic_test.exe') - remove_startup() - - with ( - winreg.OpenKey(winreg.HKEY_CURRENT_USER, RUN_KEY_PATH, 0, winreg.KEY_QUERY_VALUE) as key, - pytest.raises(FileNotFoundError), - ): - winreg.QueryValueEx(key, _TEST_VALUE_NAME) - - @staticmethod - def test_register_is_idempotent() -> None: - """Calling register twice with a different exe updates the value.""" - exe_v1 = r'C:\test\v1\synodic.exe' - exe_v2 = r'C:\test\v2\synodic.exe' - - try: - with patch('synodic_client.startup.STARTUP_VALUE_NAME', _TEST_VALUE_NAME): - register_startup(exe_v1) - register_startup(exe_v2) - - with winreg.OpenKey(winreg.HKEY_CURRENT_USER, RUN_KEY_PATH, 0, winreg.KEY_QUERY_VALUE) as key: - value, _ = winreg.QueryValueEx(key, _TEST_VALUE_NAME) - assert exe_v2 in value - assert exe_v1 not in value - - finally: - with patch('synodic_client.startup.STARTUP_VALUE_NAME', _TEST_VALUE_NAME): - remove_startup() - - @staticmethod - def test_is_startup_registered_reflects_state() -> None: - """Verify is_startup_registered returns the correct state.""" - with patch('synodic_client.startup.STARTUP_VALUE_NAME', _TEST_VALUE_NAME): - assert is_startup_registered() is False - - register_startup(r'C:\test\synodic_test.exe') - assert is_startup_registered() is True - - remove_startup() assert is_startup_registered() is False diff --git a/tool/scripts/package.py b/tool/scripts/package.py index 805c61b..f11e550 100644 --- a/tool/scripts/package.py +++ b/tool/scripts/package.py @@ -92,6 +92,8 @@ def main( str(ICON_FILE), '--channel', velopack_channel, + '--shortcutLocations', + 'StartMenuRoot', '-o', str(OUTPUT_DIR), ],