From e071328b5ff48be39fe70be979620455a9418691 Mon Sep 17 00:00:00 2001 From: Xavier Karma Date: Fri, 6 Nov 2026 18:25:22 +0530 Subject: [PATCH] test(rename): tighten the naming-sweep allowlist to retained surfaces The allowlist accepted any line that mentioned upstream, legacy, cmdline, a dotted name or a path segment, so a user-facing old name could pass by sharing a line with one of those words. Each rule is now scoped to a retained surface: upstream repository slugs and labelled upstream prose, dotted references to modules that exist under src/vocalinux, import and -m forms, retained class names, legacy import sources in the files that implement or test the import, quoted package names in tooling and test paths only, and the pre-rename guard in the Void container harness. The sweep skips the .git worktree pointer file, and the allowlist itself has behaviour tests: user-facing names are denied and retained surfaces are kept. --- tests/test_rename_sweep.py | 248 +++++++++++++++++++++++-------------- 1 file changed, 156 insertions(+), 92 deletions(-) diff --git a/tests/test_rename_sweep.py b/tests/test_rename_sweep.py index c07a725..8e916ff 100644 --- a/tests/test_rename_sweep.py +++ b/tests/test_rename_sweep.py @@ -1,24 +1,34 @@ -"""Ticket #18 + #24: offline naming-sweep — no old-name strings on surfaces. +"""Ticket #18 + #24: offline naming-sweep: no old-name strings on surfaces. The fork takes its own name everywhere a user can see it. Anything still -spelling the old name must be on the allowlist below: +spelling the old name must match a retained surface below. Each rule is +scoped to what the surface is, not to a word that happens to co-occur: -- upstream provenance (``VocaHQ/vocalinux`` URLs), -- the retained internal Python module name (``src/vocalinux`` paths, - ``vocalinux.*`` imports/attributes, ``-m vocalinux.main``), -- retained internal class names (``VocalinuxEngine*``, ``VocalinuxDBusService``), -- one-time-legacy import sources (old config/dictionary/bus literals), +- upstream provenance: ``VocaHQ/vocalinux`` and ``jatinkrmalik/vocalinux`` + slugs, and prose that labels the old name as upstream or as the fork's + origin ("Vocalinux (upstream)", "a hard fork of Vocalinux"); +- the retained internal Python package (ticket #17): ``src/vocalinux`` paths, + dotted module references to modules that exist under ``src/vocalinux``, + import and ``-m`` forms, ``vocalinux/.py`` module paths, and the + retained CamelCase class names (``VocalinuxEngine``, ...); +- the package name as a quoted tooling or test identifier (package lists, + mock module names, fixture trees), only in tooling and test paths; +- the one-time legacy import sources (ticket #18), only in the files that + implement or test the import, and only on lines that say so; +- pre-rename guards that name the old files they reject (ticket #28); - generated or historical files (``uv.lock``, ``requirements/*.txt``, - ``CHANGELOG.md``), -- web upstream surfaces (ticket #24): upstream GitHub/raw/nix flake URLs, - nix flake API identifiers, and the nixos-page fork-status note that names - the upstream flake as installing Vocalinux rather than Voculinux. + ``CHANGELOG.md``) and the git worktree pointer file ``.git``; +- web upstream surfaces (ticket #24): the nix flake snippets on the nixos page. + +``tests/test_rename_sweep.py`` also checks the allowlist itself, so a loosened +rule fails here before it lets an old name onto a user-visible surface. """ import os import re REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +SRC_PACKAGE = os.path.join(REPO_ROOT, "src", "vocalinux") OLD_NAME = re.compile(r"vocalinux", re.IGNORECASE) @@ -34,7 +44,9 @@ SKIP_DIRS = { "out", } SKIP_SUFFIXES = (".egg-info",) -SKIP_FILES = {"uv.lock", "CHANGELOG.md"} +#: ``.git`` is a file in linked worktrees (``gitdir: .../worktrees/...``), so it +#: is skipped by name in addition to being a skipped directory. +SKIP_FILES = {"uv.lock", "CHANGELOG.md", ".git"} SKIP_PREFIXES = ("requirements/",) BINARY_EXTS = ( ".wav", @@ -60,98 +72,119 @@ BINARY_EXTS = ( #: (matched in _allowed) may still spell the old name there. WEB_OWNED = {"web/src/app/page.tsx", "web/package.json"} +#: Top-level modules and packages of the retained package, read from the tree +#: so a dotted reference to a module that does not exist is not allowlisted. +RETAINED_MODULE_HEADS = frozenset( + [name[:-3] for name in os.listdir(SRC_PACKAGE) if name.endswith(".py")] + + [ + name + for name in os.listdir(SRC_PACKAGE) + if os.path.isdir(os.path.join(SRC_PACKAGE, name)) and not name.startswith("__") + ] +) - {"__init__"} + +_UPSTREAM_SLUG = re.compile(r"(?:VocaHQ|jatinkrmalik)/vocalinux", re.IGNORECASE) +_UPSTREAM_LABEL = re.compile( + r"upstream[^\n]{0,40}vocalinux|vocalinux[^\n]{0,40}upstream|fork of vocalinux", + re.IGNORECASE, +) +_DOTTED_MODULE = re.compile( + r"\bvocalinux\.(?:__\w+__|" + + "|".join(sorted(RETAINED_MODULE_HEADS, key=len, reverse=True)) + + r")\b" +) +_IMPORT_FORM = re.compile(r"(?:^|[\s\"'])(?:from|import)\s+vocalinux\b|-m\s+vocalinux\b") +#: Prose that names the retained package as such (ticket #17's inventory). +_RETAINED_PROSE = re.compile( + r"retained[^\n]{0,60}vocalinux|vocalinux[^\n]{0,60}(?:retained|import package|module name)" + r"|(?:import package|module name)[^\n]{0,60}vocalinux", + re.IGNORECASE, +) +#: Prose that names the old install as the source of the one-time import. +_LEGACY_PROSE = re.compile(r"\b(?:legacy|previous|old) Vocalinux\b", re.IGNORECASE) +_MODULE_PATH = re.compile( + r"src/vocalinux\b" + r"|site-packages/vocalinux\b" + r"|\"src\"\s*/\s*\"vocalinux\"" + r"|\bvocalinux/[\w./-]*\.py\b" + r"|\"vocalinux/" + r"|\bsite\s*/\s*\"vocalinux\"" + r"|\"platform/vocalinux\"" +) +_CAMEL_CLASS = re.compile(r"Vocalinux[A-Z]\w*") +#: Quoted package names in tooling and tests (package lists, mock targets, +#: ``"vocalinux."`` prefixes), and scanner regex literals in web tests. +_PACKAGE_SEGMENT = re.compile(r"[\"']vocalinux(?:[\"']|\.)|/vocalinux/[a-z]*") +_TOOLING_PREFIXES = ( + "tests/", + "web/src/lib/__tests__/", + "scripts/", + "snap/local/", + ".flake8", + "MANIFEST.in", + "install.sh", + "install.d/", + "uninstall.sh", + "pyproject.toml", + "justfile", +) +#: Files that implement or test the one-time legacy import (ticket #18), and the +#: pre-XDG compatibility path the custom dictionary keeps for stored settings. +_LEGACY_SOURCE_FILES = frozenset( + { + "src/vocalinux/legacy_import.py", + "src/vocalinux/app_identity.py", + "src/vocalinux/custom_dictionary.py", + "tests/test_legacy_import.py", + "tests/test_app_identity.py", + "tests/test_custom_dictionary.py", + "tests/test_rename_sweep.py", + } +) +_LEGACY_LINE = re.compile( + r"legacy|previous|pre-XDG|\.config/vocalinux|\.config\"?\s*/\s*\"vocalinux\"" + r"|\.local/share/vocalinux", + re.IGNORECASE, +) +#: Pre-rename file names a packaging guard rejects (ticket #28). +_PRE_RENAME_GUARD = re.compile(r"usr/share/(?:applications|icons/[^\s\"]*)/vocalinux") +_NIX_FLAKE_IDENTIFIER = re.compile( + r"packages\.\.|apps\.\.|nixosModules\.|homeManagerModules\.|programs\." + r"|\bflake\b|\binputs?\b|outputs = \{|nixpkgs, |pkgs\.vocalinux|nix run|nix profile" + r"|github:VocaHQ", + re.IGNORECASE, +) + def _allowed(path: str, line: str) -> bool: - """Return True when an old-name match is an allowlisted surface.""" - low = line.lower() - if "vocahq/" in low or "jatinkrmalik/vocalinux" in low: + """Return True when an old-name match is an allowlisted retained surface.""" + if path == "tests/test_rename_sweep.py": + # This file defines the allowlist and quotes the old name to do so. return True - if "vocalinux" in low and "voculinux" in low: - # Migration-compat shims that name both installs at once. + if _UPSTREAM_SLUG.search(line) or _UPSTREAM_LABEL.search(line): return True - if "upstream" in low: - # Upstream-provenance mentions that explicitly label the old name - # as upstream (fork-status notes, provenance cards). + if _MODULE_PATH.search(line) or _DOTTED_MODULE.search(line): return True - if "/{name}/vocalinux" in line: - # Upstream repo-name template (retained provenance). + if _IMPORT_FORM.search(line) or _CAMEL_CLASS.search(line) or _RETAINED_PROSE.search(line): return True - if path == "snap/local/check_runtime.py" and '"vocalinux"' in line: - # Smoke imports resolve the retained import package. - return True - if re.search(r'vocalinux\.["a-z_]', line): - return True - if line.strip().rstrip(",").lstrip("/ ") == '"vocalinux"': - # Retained module-dir path segment element in tests and tooling - # (single-line and os.path.join element forms). - return True - if 'site / "vocalinux"' in line or '"platform/vocalinux"' in line: - # Fixture module trees named after the retained import package. - return True - if '"vocalinux/' in line: - # Fixture module file paths under the retained package name. - return True - if "/vocalinux/" in line or "src/vocalinux" in line: - return True - if "-m vocalinux" in line or "import vocalinux" in line: - return True - if "VocalinuxEngine" in line or "VocalinuxDBusService" in line: - return True - if "cmdline" in line: - return True - if " - vocalinux" in line: - # Log logger names keep the retained module path. - return True - if path in ( - "src/vocalinux/legacy_import.py", - "tests/test_legacy_import.py", - ): - return True - if "LEGACY" in line or "legacy" in line: - return True - if "~/.config/vocalinux" in line or '".config" / "vocalinux"' in line: - # Documented one-time-import source (ticket #18). - return True - if "from vocalinux" in line: - return True - if '"src"' in line and '"vocalinux"' in line: - # Retained module-dir path segments in tests and tooling. + if _LEGACY_PROSE.search(line): return True if path == "web/src/app/install/nixos/page.tsx": - # The Nix flake is upstream's: flake URL, input/output names, module - # paths, and programs.* options spell the retained upstream API. - # The page carries a fork-status note stating those snippets install - # Vocalinux (upstream), not Voculinux. + # The Nix flake is upstream's: flake URL, input and output names and + # programs.* options spell the retained upstream API. The page carries + # a fork-status note saying those snippets install Vocalinux, not + # Voculinux, so only flake-shaped lines are allowed here. + return bool(_NIX_FLAKE_IDENTIFIER.search(line)) + if path in _LEGACY_SOURCE_FILES and _LEGACY_LINE.search(line): return True - if path == "tests/test_rename_sweep.py": + if path.startswith(_TOOLING_PREFIXES) and _PACKAGE_SEGMENT.search(line): return True - if path == "pyproject.toml" and re.search(r'"vocalinux[."\'\]]', line): + if path == "packaging/void/container-main.sh" and _PRE_RENAME_GUARD.search(line): return True - if path == "pyproject.toml" and "vocalinux.version" in line: - return True - if 'PKG_NAME="vocalinux"' in line: - # Installer probes the retained import package, not the distribution. - return True - if path == "packaging/void/srcpkgs/voculinux/template" and "vocalinux-${version}" in line: - # Pre-rename release asset root (ticket #27); dropped at the first - # post-rename release bump. - return True - if path == "tests/test_void_app_template.py" and '"vocalinux"' in line: - # The app-template policy test asserts the old name is gone. - return True - if path == "tests/test_void_app_template.py" and "usr/bin/vocalinux" in line: - # Pre-rename sdist launchers the template promotes at install time - # (ticket #28); dropped at the first post-rename release bump. - return True - if path == "packaging/void/container-main.sh" and ( - "usr/share/applications/vocalinux" in line or 'version("vocalinux")' in line + if path == "src/vocalinux/utils/update_checker.py" and ( + 'casefold() == "vocalinux"' in line or '"/{name}/vocalinux"' in line ): - # Pre-rename sdist compat (ticket #28): the payload guard names the - # old file names it rejects, and the import smoke queries the old - # distribution name. Dropped at the first post-rename release bump. - return True - if '== "vocalinux"' in line or '.startswith("vocalinux' in line: - # Retained module identity comparisons in test isolation helpers. + # Upstream repository identity the update checker recognises. return True return False @@ -194,3 +227,34 @@ def test_owned_web_files_carry_new_name() -> None: with open(os.path.join(REPO_ROOT, rel), encoding="utf-8") as handle: text = handle.read() assert "Voculinux" in text or "voculinux" in text, rel + + +def test_allowlist_denies_user_facing_old_names() -> None: + """A user-facing old name is never allowlisted, wherever it sits.""" + denied = [ + ("web/src/app/page.tsx", "

Install vocalinux today

"), + ("README.md", "Launch vocalinux from your app menu."), + ("docs/UPDATE.md", "Your settings live in ~/.local/share/vocalinux."), + ("src/vocalinux/ui/tray_indicator.py", 'TRAY_TITLE = "vocalinux"'), + ("src/vocalinux/ui/tray_indicator.py", 'ICON = "vocalinux-microphone"'), + ("packaging/void/srcpkgs/voculinux/template", "pkgname=vocalinux"), + ("docs/INSTALL.md", "Open vocalinux.desktop from the menu."), + ] + for path, line in denied: + assert not _allowed(path, line), f"{path}: {line!r} must not be allowlisted" + + +def test_allowlist_keeps_the_retained_surfaces() -> None: + """Upstream provenance, the retained package and the legacy import stay allowed.""" + kept = [ + ("README.md", "Upstream: https://github.com/VocaHQ/vocalinux/issues"), + ("web/src/app/page.tsx", "Voculinux is a hard fork of Vocalinux for Void Linux."), + ("web/src/app/install/[distro]/page.tsx", "installs Vocalinux (upstream), not Voculinux."), + ("src/vocalinux/ui/tray_indicator.py", "from vocalinux.utils import paths"), + ("src/vocalinux/app_identity.py", 'LEGACY_BUS_NAME: str = "com.vocalinux.Vocalinux"'), + ("tests/test_app_identity.py", "from vocalinux import app_identity"), + ("packaging/void/container-main.sh", 'grep -E "usr/share/applications/vocalinux"'), + ("install.sh", 'local PKG_NAME="vocalinux"'), + ] + for path, line in kept: + assert _allowed(path, line), f"{path}: {line!r} is a retained surface"