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.
This commit is contained in:
2026-11-06 18:25:22 +05:30
parent bf8a6b7b56
commit e071328b5f
+156 -92
View File
@@ -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/<file>.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\.<system>\.|apps\.<system>\.|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", "<h1>Install vocalinux today</h1>"),
("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"