From 15c171db0099e513b574b70348d384262ef421fa Mon Sep 17 00:00:00 2001 From: Daniele21 Date: Thu, 17 Sep 2026 13:56:16 +0200 Subject: [PATCH] fix: align bundled runtime identity with product version --- src/local_asr_server/app_identity.py | 46 ++++++++++------- test/test_app_identity_product_version.py | 60 +++++++++++++++++++++++ 2 files changed, 89 insertions(+), 17 deletions(-) create mode 100644 test/test_app_identity_product_version.py diff --git a/src/local_asr_server/app_identity.py b/src/local_asr_server/app_identity.py index c0263a99..b1c47184 100644 --- a/src/local_asr_server/app_identity.py +++ b/src/local_asr_server/app_identity.py @@ -36,42 +36,54 @@ def as_health_payload(self) -> dict: } -def get_bundle_display_name() -> str: - """Return the visible bundle name from Info.plist when bundled.""" +def _bundle_info() -> dict: contents_dir = get_app_contents_dir() if contents_dir is None: - return APP_NAME + return {} info_plist = contents_dir / "Info.plist" try: with info_plist.open("rb") as f: payload = plistlib.load(f) except Exception: - return APP_NAME + return {} + return payload if isinstance(payload, dict) else {} + +def get_bundle_display_name() -> str: + """Return the visible bundle name from Info.plist when bundled.""" + payload = _bundle_info() value = payload.get("CFBundleDisplayName") or payload.get("CFBundleName") return str(value or APP_NAME) def get_bundle_identifier() -> str: - """Return the bundle identifier from Info.plist when bundled, falling back to paths.APP_BUNDLE_ID.""" - contents_dir = get_app_contents_dir() - if contents_dir is None: - return APP_BUNDLE_ID - - info_plist = contents_dir / "Info.plist" - try: - with info_plist.open("rb") as f: - payload = plistlib.load(f) - except Exception: - return APP_BUNDLE_ID - + """Return the bundle identifier from Info.plist when bundled.""" + payload = _bundle_info() value = payload.get("CFBundleIdentifier") return str(value or APP_BUNDLE_ID) +def get_bundle_version() -> str | None: + """Return the product version embedded in the macOS bundle, when available.""" + payload = _bundle_info() + value = payload.get("CFBundleShortVersionString") or payload.get("CFBundleVersion") + text = str(value or "").strip() + return text or None + + def get_app_version() -> str: - """Return the installed package version, falling back to the module version.""" + """Return the product identity version for bundled apps, package version otherwise. + + ClosedRoom's root VERSION owns the macOS product version and is embedded in + Info.plist at build time. The Python distribution has an independent package + version, so a frozen app must not expose that package version as its runtime + application identity. + """ + if is_bundled(): + bundle_version = get_bundle_version() + if bundle_version: + return bundle_version try: return metadata.version("local-asr-server") except metadata.PackageNotFoundError: diff --git a/test/test_app_identity_product_version.py b/test/test_app_identity_product_version.py new file mode 100644 index 00000000..1f34af84 --- /dev/null +++ b/test/test_app_identity_product_version.py @@ -0,0 +1,60 @@ +from __future__ import annotations + +import plistlib +import tempfile +import unittest +from pathlib import Path +from unittest.mock import patch + +from local_asr_server import app_identity + + +class AppIdentityProductVersionTests(unittest.TestCase): + def _contents_dir(self, payload: dict) -> tuple[tempfile.TemporaryDirectory[str], Path]: + tmp = tempfile.TemporaryDirectory() + contents = Path(tmp.name) / "Contents" + contents.mkdir(parents=True) + with (contents / "Info.plist").open("wb") as handle: + plistlib.dump(payload, handle) + return tmp, contents + + def test_bundled_identity_uses_product_bundle_version_not_python_package_version(self) -> None: + tmp, contents = self._contents_dir( + { + "CFBundleIdentifier": "com.closedroom.app", + "CFBundleName": "ClosedRoom", + "CFBundleDisplayName": "ClosedRoom", + "CFBundleVersion": "0.2.0", + "CFBundleShortVersionString": "0.2.0", + } + ) + self.addCleanup(tmp.cleanup) + + with ( + patch.object(app_identity, "get_app_contents_dir", return_value=contents), + patch.object(app_identity, "is_bundled", return_value=True), + patch.object(app_identity.metadata, "version", return_value="0.1.0"), + ): + identity = app_identity.get_app_identity() + + self.assertEqual(identity.version, "0.2.0") + self.assertEqual(identity.as_health_payload()["app_version"], "0.2.0") + self.assertNotEqual(identity.version, "0.1.0") + + def test_bundle_version_falls_back_to_cf_bundle_version(self) -> None: + tmp, contents = self._contents_dir({"CFBundleVersion": "0.2.1"}) + self.addCleanup(tmp.cleanup) + + with patch.object(app_identity, "get_app_contents_dir", return_value=contents): + self.assertEqual(app_identity.get_bundle_version(), "0.2.1") + + def test_non_bundled_identity_keeps_python_package_version(self) -> None: + with ( + patch.object(app_identity, "is_bundled", return_value=False), + patch.object(app_identity.metadata, "version", return_value="0.1.0"), + ): + self.assertEqual(app_identity.get_app_version(), "0.1.0") + + +if __name__ == "__main__": + unittest.main()