From d08cacb0b4e724d324e279ca739b5fc89d2bbb45 Mon Sep 17 00:00:00 2001 From: soubarnak Date: Fri, 6 Nov 2026 09:01:15 +0530 Subject: [PATCH] fix(identity): share one model-download user agent definition Both model-download call sites built the User-Agent string separately: recognition_manager used a hardcoded literal while settings_dialog interpolated APP_ID. Add app_identity.user_agent() and use it in both, with a test pinning the shared definition. Fixes #17 --- src/vocalinux/app_identity.py | 6 ++++++ .../speech_recognition/recognition_manager.py | 4 ++-- src/vocalinux/ui/settings_dialog.py | 4 ++-- tests/test_app_identity.py | 14 ++++++++++++++ 4 files changed, 24 insertions(+), 4 deletions(-) diff --git a/src/vocalinux/app_identity.py b/src/vocalinux/app_identity.py index 697e676..4a97bc8 100644 --- a/src/vocalinux/app_identity.py +++ b/src/vocalinux/app_identity.py @@ -42,6 +42,12 @@ IBUS_TEXTDOMAIN: str = "voculinux" UPDATE_CHECKER_USER_AGENT: str = "Voculinux-UpdateChecker" + +def user_agent(version: str) -> str: + """Outbound HTTP User-Agent for model downloads.""" + return f"{APP_ID}/{version}" + + RESOURCE_DIR_NAME: str = "voculinux" VERIFICATION_STAMP_NAME: str = ".voculinux_verified" diff --git a/src/vocalinux/speech_recognition/recognition_manager.py b/src/vocalinux/speech_recognition/recognition_manager.py index 92cf09d..edb5c84 100644 --- a/src/vocalinux/speech_recognition/recognition_manager.py +++ b/src/vocalinux/speech_recognition/recognition_manager.py @@ -27,7 +27,7 @@ if TYPE_CHECKING: import numpy as np import requests -from ..app_identity import APP_NAME, RESOURCE_DIR_NAME +from ..app_identity import APP_NAME, RESOURCE_DIR_NAME, user_agent from ..audio.capture import ( # noqa: F401 (re-exported for compatibility) _STICKY_LOCK_MIN_MEAN_SQUARE, CaptureStalledError, @@ -2731,7 +2731,7 @@ class SpeechRecognitionManager: url, stream=True, timeout=self._MODEL_DOWNLOAD_TIMEOUT, - headers={"User-Agent": f"voculinux/{__version__}"}, + headers={"User-Agent": user_agent(__version__)}, proxies=requests_proxies(), ) except requests.exceptions.RequestException as e: diff --git a/src/vocalinux/ui/settings_dialog.py b/src/vocalinux/ui/settings_dialog.py index ab64f9f..5ec4393 100644 --- a/src/vocalinux/ui/settings_dialog.py +++ b/src/vocalinux/ui/settings_dialog.py @@ -36,7 +36,7 @@ gi.require_version("Gdk", "3.0") # Need GLib for idle_add from gi.repository import Gdk, GLib, GObject, Gtk, Pango # noqa: E402 -from ..app_identity import APP_ID, APP_NAME, RESOURCE_DIR_NAME # noqa: E402 +from ..app_identity import APP_NAME, RESOURCE_DIR_NAME, user_agent # noqa: E402 from ..audio.pulse_sources import ( # noqa: E402 PULSE_INDEX_BASE, PulseSource, @@ -6461,7 +6461,7 @@ class SettingsDialog(AboutSectionMixin): try: response = requests.get( "https://huggingface.co/api/models/ggerganov/whisper.cpp", - headers={"User-Agent": f"{APP_ID}/{__version__}"}, + headers={"User-Agent": user_agent(__version__)}, timeout=10, proxies=requests_proxies(config), ) diff --git a/tests/test_app_identity.py b/tests/test_app_identity.py index 3edb2fb..8c629e1 100644 --- a/tests/test_app_identity.py +++ b/tests/test_app_identity.py @@ -117,6 +117,20 @@ def test_source_pinned_literals_reserved_for_rename(): assert 'window.set_title("Voculinux Dictation Pad")' in pad +def test_model_download_user_agent_is_shared_definition(): + """Model-download User-Agent strings resolve through one shared helper.""" + identity = _load_identity() + assert identity.user_agent("9.9.9") == f"{identity.APP_ID}/9.9.9" + for rel in ( + SRC_ROOT / "speech_recognition" / "recognition_manager.py", + SRC_ROOT / "ui" / "settings_dialog.py", + ): + source = rel.read_text(encoding="utf-8") + assert "user_agent(__version__)" in source, rel + assert 'f"voculinux/{__version__}"' not in source, rel + assert 'f"{APP_ID}/{__version__}"' not in source, rel + + def test_rename_inventory_files_exist(): """Every rename surface the rename ticket must flip is a real checked file.""" identity = _load_identity()