test: fence upstream references on the fork surfaces
CI / security (pull_request) Failing after 1m14s
CI / lint (pull_request) Successful in 22s
CI / ai-review (pull_request) Successful in 7s

The naming sweep forbids upstream hosts, handles and URLs on the fork
surfaces except each file's recorded credit line, matched exactly. Packaging
tests read the surfaces that still exist: the Wayland hotkey section and the
Snap recipe.
This commit is contained in:
2026-11-06 21:25:42 +05:30
parent 4edccb6caa
commit c166a67822
3 changed files with 125 additions and 24 deletions
+4 -2
View File
@@ -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
+115 -12
View File
@@ -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|(?<![\w.])x\.com/"
r"|mailto:|codeberg|vocamac\.com|vocawin\.com|vocaphone\.|vocagateway\.|flathub\.org"
r"|dl\.flathub"
r"|snapcraft\.io|aur\.archlinux|pypi\.org/project/voculinux|github:|star-history"
r"|contrib\.rocks"
r"|shields\.io/(?:aur|pypi|discord|github)",
re.IGNORECASE,
)
_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",
@@ -148,19 +192,43 @@ _LEGACY_LINE = re.compile(
)
#: 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,
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 <code>vocalinux</code>");',
}
)
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", "<h1>Install vocalinux today</h1>"),
("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"),
+6 -10
View File
@@ -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