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
166 lines
7.2 KiB
Python
166 lines
7.2 KiB
Python
"""Ticket #17 prefactor inventory: central identity + rename surfaces.
|
|
|
|
Seams under test (per spec Testing Decisions):
|
|
- Seam 1 (Python behaviour suite): central constants hold current values and
|
|
every previously duplicated consumer resolves through the shared definition.
|
|
- Seam 2 (offline policy test): rename/retained inventories enumerate files
|
|
the rename ticket must flip (or deliberately keep) without network access.
|
|
|
|
No user-visible behaviour, name, path, or string changes: all asserted values
|
|
are the pre-rename Voculinux/voculinux literals.
|
|
"""
|
|
|
|
import ast
|
|
from pathlib import Path
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parents[1]
|
|
SRC_ROOT = REPO_ROOT / "src" / "vocalinux"
|
|
|
|
|
|
def _load_identity():
|
|
from vocalinux import app_identity
|
|
|
|
return app_identity
|
|
|
|
|
|
def test_central_values_are_current_names():
|
|
"""Shared definition still carries the pre-rename literals."""
|
|
identity = _load_identity()
|
|
assert identity.APP_NAME == "Voculinux"
|
|
assert identity.APP_ID == "voculinux"
|
|
assert identity.APP_DIR_NAME == "voculinux"
|
|
assert identity.CLI_PROG == "voculinux"
|
|
assert identity.BUS_NAME == "com.voculinux.Voculinux"
|
|
assert identity.OBJECT_PATH == "/com/voculinux/Voculinux"
|
|
assert identity.INTERFACE_NAME == "com.voculinux.Voculinux"
|
|
assert identity.DESKTOP_FILENAME == "voculinux.desktop"
|
|
assert identity.DESKTOP_ICON_NAME == "voculinux"
|
|
assert identity.ENGINE_NAME == "voculinux"
|
|
assert identity.ENGINE_LONGNAME == "Voculinux"
|
|
assert identity.COMPONENT_NAME == "org.freedesktop.IBus.Voculinux"
|
|
assert identity.IBUS_DIR_NAME == "voculinux-ibus"
|
|
assert identity.UPDATE_CHECKER_USER_AGENT == "Voculinux-UpdateChecker"
|
|
assert identity.RESOURCE_DIR_NAME == "voculinux"
|
|
assert identity.VERIFICATION_STAMP_NAME == ".voculinux_verified"
|
|
assert identity.LOG_FILE_PREFIX == "voculinux_logs_"
|
|
assert identity.PORTAL_TOKEN_PREFIX == "voculinux_"
|
|
assert identity.ENV_FORCE_BACKEND == "VOCULINUX_FORCE_BACKEND"
|
|
assert identity.ENV_INJECT_MODIFIER_WAIT == "VOCULINUX_INJECT_MODIFIER_WAIT"
|
|
assert identity.ENV_YDOTOOL_KEY_DELAY == "VOCULINUX_YDOTOOL_KEY_DELAY"
|
|
|
|
|
|
def _imports_app_identity(path: Path) -> bool:
|
|
tree = ast.parse(path.read_text(encoding="utf-8"))
|
|
for node in ast.walk(tree):
|
|
if isinstance(node, ast.ImportFrom):
|
|
if node.module and node.module.endswith("app_identity"):
|
|
return True
|
|
return False
|
|
|
|
|
|
def test_consumers_resolve_through_shared_definition():
|
|
"""Previously duplicated surfaces import the shared definition (AST, offline)."""
|
|
consumers = [
|
|
SRC_ROOT / "utils" / "paths.py",
|
|
SRC_ROOT / "ui" / "notifications.py",
|
|
SRC_ROOT / "ui" / "tray_indicator.py",
|
|
SRC_ROOT / "dbus_service.py",
|
|
SRC_ROOT / "ui" / "autostart_manager.py",
|
|
SRC_ROOT / "text_injection" / "ibus_engine.py",
|
|
SRC_ROOT / "utils" / "update_checker.py",
|
|
SRC_ROOT / "utils" / "resource_manager.py",
|
|
SRC_ROOT / "main.py",
|
|
SRC_ROOT / "text_injection" / "text_injector.py",
|
|
SRC_ROOT / "text_injection" / "remote_desktop_portal.py",
|
|
SRC_ROOT / "ui" / "logging_dialog.py",
|
|
SRC_ROOT / "utils" / "model_checksums.py",
|
|
SRC_ROOT / "speech_recognition" / "recognition_manager.py",
|
|
SRC_ROOT / "ui" / "settings_dialog.py",
|
|
SRC_ROOT / "ui" / "dictation_overlay.py",
|
|
SRC_ROOT / "ui" / "audio_feedback.py",
|
|
SRC_ROOT / "ui" / "keyboard_backends" / "__init__.py",
|
|
]
|
|
missing = [str(p) for p in consumers if not p.exists()]
|
|
assert not missing, f"consumer files missing: {missing}"
|
|
not_centralised = [str(p) for p in consumers if not _imports_app_identity(p)]
|
|
assert not_centralised == [], f"consumers not resolving through app_identity: {not_centralised}"
|
|
|
|
|
|
def test_lightweight_consumers_match_shared_values_at_runtime():
|
|
"""Runtime check where importing the consumer needs no display server."""
|
|
identity = _load_identity()
|
|
from vocalinux.ui import autostart_manager, notifications
|
|
from vocalinux.utils import paths
|
|
|
|
assert paths.APP_DIR_NAME == identity.APP_DIR_NAME
|
|
assert autostart_manager.DESKTOP_FILENAME == identity.DESKTOP_FILENAME
|
|
assert notifications.APP_NAME == identity.APP_NAME
|
|
|
|
|
|
def test_source_pinned_literals_reserved_for_rename():
|
|
"""Literals existing source-shape tests pin stay literal until the rename.
|
|
|
|
``tests/test_settings_dialog.py`` (About page) and
|
|
``tests/test_dictation_pad.py`` (shelving) read module source or exec
|
|
function bodies in isolation, so ``settings_about.py`` and
|
|
``dictation_pad.py`` keep their current literals. They are recorded here
|
|
as checked rename surfaces for ticket #18 rather than centralised twice.
|
|
"""
|
|
about = (SRC_ROOT / "ui" / "settings_about.py").read_text(encoding="utf-8")
|
|
for literal in (
|
|
'VOCULINUX_SITE_URL = "https://voculinux.com"',
|
|
'title="Voculinux"',
|
|
'get_icon_path("voculinux")',
|
|
):
|
|
assert literal in about, literal
|
|
pad = (SRC_ROOT / "ui" / "dictation_pad.py").read_text(encoding="utf-8")
|
|
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()
|
|
assert identity.RENAME_SURFACES, "rename inventory must not be empty"
|
|
missing = [rel for rel in identity.RENAME_SURFACES if not (REPO_ROOT / rel).exists()]
|
|
assert missing == [], f"rename inventory entries missing on disk: {missing}"
|
|
|
|
|
|
def test_retained_inventory_records_internal_names():
|
|
"""Internal module name and upstream URLs stay, by record not by accident."""
|
|
identity = _load_identity()
|
|
assert any("src/vocalinux" in entry for entry in identity.RETAINED_SURFACES)
|
|
assert any("github.com/VocaHQ/vocalinux" in entry for entry in identity.RETAINED_SURFACES)
|
|
# The retained Python package must still import under its current name.
|
|
import vocalinux
|
|
|
|
assert vocalinux.__name__ == "vocalinux"
|
|
|
|
|
|
def test_legacy_names_recorded_and_unclaimed():
|
|
"""The old install identity is recorded for the one-time import only.
|
|
|
|
No compatibility alias: the legacy D-Bus name differs from the claimed
|
|
one, and no source claims it.
|
|
"""
|
|
identity = _load_identity()
|
|
assert identity.LEGACY_APP_DIR_NAME == "vocalinux"
|
|
assert identity.LEGACY_APP_DIR_NAME != identity.APP_DIR_NAME
|
|
assert identity.LEGACY_BUS_NAME == "com.vocalinux.Vocalinux"
|
|
assert identity.LEGACY_BUS_NAME != identity.BUS_NAME
|
|
service = (SRC_ROOT / "dbus_service.py").read_text(encoding="utf-8")
|
|
assert identity.LEGACY_BUS_NAME not in service
|