diff --git a/tests/test_native_packaging.py b/tests/test_native_packaging.py index 89430f4..6eeb9bc 100644 --- a/tests/test_native_packaging.py +++ b/tests/test_native_packaging.py @@ -322,10 +322,12 @@ def test_wayland_input_group_is_documented() -> None: already listed in /etc/group: a fresh Wayland install leaves the default hotkey dead unless the docs name the step.""" install_md = (REPO_ROOT / "docs" / "INSTALL.md").read_text(encoding="utf-8") - section = install_md.split("## Distro packages", 1)[1].split("\n## ", 1)[0] + section = install_md.split("### Wayland hotkeys need the input group", 1)[1].split("\n## ", 1)[ + 0 + ] assert ( "usermod -aG input" in section - ), "INSTALL.md's .deb/.rpm section must document the Wayland input-group step" + ), "INSTALL.md's Wayland hotkey section must document the input-group step" release = RELEASE_YML.read_text(encoding="utf-8") assert ( "usermod -aG input" in release diff --git a/tests/test_rename_sweep.py b/tests/test_rename_sweep.py index 91ba498..48d557a 100644 --- a/tests/test_rename_sweep.py +++ b/tests/test_rename_sweep.py @@ -20,6 +20,12 @@ scoped to what the surface is, not to a word that happens to co-occur: ``CHANGELOG.md``) and the git worktree pointer file ``.git``; - web upstream surfaces (ticket #24): the nix flake snippets on the nixos page. +The Void release cleanup adds the fork surfaces (``FORK_SURFACES``): README, +the support and security docs, the install docs, the About page and the Void +install message. On those, upstream references are not allowlisted by pattern +at all. An upstream URL, handle or host is a regression unless the line is +exactly the one credit line recorded for that file. + ``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. """ @@ -83,6 +89,44 @@ RETAINED_MODULE_HEADS = frozenset( ] ) - {"__init__"} +#: The one upstream credit per fork surface, matched as whole stripped lines. +CREDIT_LINE = "Voculinux is a hard fork of Vocalinux by VocaHQ: https://github.com/VocaHQ/vocalinux" +FORK_SURFACE_CREDITS: dict[str, frozenset[str]] = { + "README.md": frozenset({CREDIT_LINE}), + "CONTRIBUTING.md": frozenset({CREDIT_LINE}), + "src/vocalinux/ui/settings_about.py": frozenset( + { + 'UPSTREAM_REPO_URL = "https://github.com/VocaHQ/vocalinux"', + 'UPSTREAM_CREDIT_LINE = f"Voculinux is a hard fork of Vocalinux by VocaHQ: {UPSTREAM_REPO_URL}"', + } + ), +} +FORK_SURFACES = frozenset( + { + "README.md", + "SECURITY.md", + "SUPPORT.md", + "CONTRIBUTING.md", + "docs/UPDATE.md", + "docs/INSTALL.md", + "docs/TROUBLESHOOTING.md", + "docs/DISTRO_COMPATIBILITY.md", + "src/vocalinux/ui/settings_about.py", + "src/vocalinux/ui/settings_dialog.py", + "packaging/void/srcpkgs/voculinux/INSTALL.msg", + } +) +#: Upstream hosts, handles and URL forms. Plain words such as "Flathub" are not +#: listed, so a sentence that says a channel is not published stays legal. +UPSTREAM_REFERENCE = re.compile( + r"VocaHQ|vocahq|jatinkrmalik|raw\.githubusercontent|discord\.gg|(?\.|apps\.\.|nixosModules\.|homeManagerModules\.|programs\." - r"|\bflake\b|\binputs?\b|outputs = \{|nixpkgs, |pkgs\.vocalinux|nix run|nix profile" - r"|github:VocaHQ", - re.IGNORECASE, + + +WEB_REBRAND_TEST = "web/src/lib/__tests__/voculinux-rebrand.test.ts" +WEB_REBRAND_SCANNER_LINES = frozenset( + { + "const OLD_NAME = /Vocalinux/;", + 'expect(home).not.toContain("Then launch vocalinux");', + } ) +def _fork_surface_allowed(path: str, line: str) -> bool: + """Fork surfaces allow the exact credit, retained module references and legacy prose.""" + if line.strip() in FORK_SURFACE_CREDITS.get(path, frozenset()): + return True + if UPSTREAM_REFERENCE.search(line): + return False + return bool( + _MODULE_PATH.search(line) + or _DOTTED_MODULE.search(line) + or _IMPORT_FORM.search(line) + or _CAMEL_CLASS.search(line) + or _RETAINED_PROSE.search(line) + or _LEGACY_PROSE.search(line) + ) + + def _allowed(path: str, line: str) -> bool: """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 path in FORK_SURFACES: + return _fork_surface_allowed(path, line) + if path == WEB_REBRAND_TEST and line.strip() in WEB_REBRAND_SCANNER_LINES: + # The web rebrand test names the old word only to scan for it. + return True if _UPSTREAM_SLUG.search(line) or _UPSTREAM_LABEL.search(line): return True if _MODULE_PATH.search(line) or _DOTTED_MODULE.search(line): @@ -169,12 +237,6 @@ def _allowed(path: str, line: str) -> bool: return True 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 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.startswith(_TOOLING_PREFIXES) and _PACKAGE_SEGMENT.search(line): @@ -221,6 +283,28 @@ def test_naming_sweep_no_old_name_outside_allowlist() -> None: ), f"{len(offenders)} old-name string(s) outside the allowlist:\n" + "\n".join(offenders[:60]) +def test_fork_surfaces_name_no_upstream_but_the_one_credit() -> None: + """A fork surface may name upstream only on its recorded credit line.""" + offenders: list[str] = [] + for rel in sorted(FORK_SURFACES): + with open(os.path.join(REPO_ROOT, rel), encoding="utf-8") as handle: + text = handle.read() + credits = FORK_SURFACE_CREDITS.get(rel, frozenset()) + for number, line in enumerate(text.splitlines(), 1): + if UPSTREAM_REFERENCE.search(line) and line.strip() not in credits: + offenders.append(f"{rel}:{number}:{line.strip()[:160]}") + assert offenders == [], "upstream reference outside the credit:\n" + "\n".join(offenders) + + +def test_fork_surfaces_carry_their_credit_exactly_once() -> None: + """The credit is present, once, on every file that records one.""" + for rel, credits in sorted(FORK_SURFACE_CREDITS.items()): + with open(os.path.join(REPO_ROOT, rel), encoding="utf-8") as handle: + lines = [line.strip() for line in handle.read().splitlines()] + for credit in credits: + assert lines.count(credit) == 1, f"{rel}: credit line must appear exactly once" + + def test_owned_web_files_carry_new_name() -> None: """The two web rename surfaces name the fork (URLs aside).""" for rel in sorted(WEB_OWNED): @@ -232,6 +316,20 @@ def test_owned_web_files_carry_new_name() -> None: def test_allowlist_denies_user_facing_old_names() -> None: """A user-facing old name is never allowlisted, wherever it sits.""" denied = [ + ("README.md", "Upstream: https://github.com/VocaHQ/vocalinux/issues"), + ("README.md", "Join us on [Discord](https://discord.gg/t6muquAJbm)."), + ("docs/INSTALL.md", "flatpak install vocahq com.voculinux.Voculinux"), + ("SUPPORT.md", "| [Discussions](https://github.com/VocaHQ/vocalinux/discussions) |"), + ("SECURITY.md", "Email hello@vocahq.com for security reports."), + ("src/vocalinux/ui/settings_about.py", 'VOCAHQ_X_URL = "https://x.com/vocahq"'), + ( + "src/vocalinux/ui/settings_dialog.py", + 'MODEL_SELECTION_DOC_URL = "https://github.com/VocaHQ/vocalinux/x"', + ), + ( + "packaging/void/srcpkgs/voculinux/INSTALL.msg", + "Report bugs at github.com/VocaHQ/vocalinux", + ), ("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."), @@ -247,7 +345,12 @@ def test_allowlist_denies_user_facing_old_names() -> None: 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"), + ("README.md", CREDIT_LINE), + ("CONTRIBUTING.md", CREDIT_LINE), + ( + "src/vocalinux/ui/settings_about.py", + 'UPSTREAM_REPO_URL = "https://github.com/VocaHQ/vocalinux"', + ), ("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"), diff --git a/tests/test_snap_packaging.py b/tests/test_snap_packaging.py index 451f200..2b0e44b 100644 --- a/tests/test_snap_packaging.py +++ b/tests/test_snap_packaging.py @@ -59,24 +59,20 @@ def test_snap_puts_gnome_platform_first_on_ld_library_path() -> None: def test_snap_docs_tell_users_to_connect_hardware_observe() -> None: """Hotkeys need hardware-observe for /proc/bus/input/devices (#857).""" - readme = (REPO_ROOT / "README.md").read_text(encoding="utf-8") - install = (REPO_ROOT / "docs" / "INSTALL.md").read_text(encoding="utf-8") + # Only the recipe still names the Snap plugs; the fork docs carry no Snap install text. snapcraft = SNAPCRAFT_YAML.read_text(encoding="utf-8") - for text in (readme, install, snapcraft): + for text in (snapcraft,): assert "sudo snap connect voculinux:raw-input" in text assert "sudo snap connect voculinux:hardware-observe" in text def test_snap_docs_explain_typing_without_uinput_plug() -> None: """The Store denied the uinput grant; docs must point at the portal, not a connect.""" - readme = (REPO_ROOT / "README.md").read_text(encoding="utf-8") - install = (REPO_ROOT / "docs" / "INSTALL.md").read_text(encoding="utf-8") - update = (REPO_ROOT / "docs" / "UPDATE.md").read_text(encoding="utf-8") - changelog = (REPO_ROOT / "web" / "src" / "app" / "changelog" / "page.tsx").read_text( - encoding="utf-8" - ) + # The Snap channel is not published for Voculinux, so the recipe is the one + # surface that still describes the plug set. The fork docs no longer carry + # Snap install text. snapcraft = SNAPCRAFT_YAML.read_text(encoding="utf-8") - for text in (readme, install, update, changelog, snapcraft): + for text in (snapcraft,): uinput_lines = "\n".join(line for line in text.splitlines() if "uinput" in line.lower()) lowered = uinput_lines.lower() assert "uinput" in lowered