"""Offline policy guards for the Void package smoke gate (ticket #28). Spec #16 gives the packaged artefact the same treatment as the other distribution formats: one documented command runs lint, build, install and version/import smoke for every Void template from a clean checkout, using podman and the pinned container, with no host Void install. Seams under test (public interfaces, not internals): - ``packaging/void/smoke.sh`` — the gate command itself: exit status plus package/stage identification on stdout/stderr. - The ``void-smoke`` just recipe — how release work and CI call the gate, same shape as the other packaging gates. - ``packaging/void/README.md`` — the documented one-command entry. Independent sources of truth: ``packaging/void/srcpkgs/*/template`` owns the template set (a renamed or added template must not silently drop out), ``packaging/void/tool_checksums.txt`` owns the pins, and ``packaging/void/build.sh`` owns the per-package container flow the gate orchestrates. The gate must agree with all of them; the container run itself is the proof, these tests are the wiring guard. """ import re from pathlib import Path REPO_ROOT = Path(__file__).resolve().parents[1] VOID = REPO_ROOT / "packaging" / "void" SRC = VOID / "srcpkgs" SMOKE_SH = VOID / "smoke.sh" BUILD_SH = VOID / "build.sh" CONTAINER_MAIN = VOID / "container-main.sh" PINS = VOID / "tool_checksums.txt" JUSTFILE = REPO_ROOT / "justfile" README = VOID / "README.md" def _srcpkg_templates() -> list: """Every template directory under srcpkgs, in dependency build order.""" return sorted(p.name for p in SRC.iterdir() if (p / "template").is_file()) def _smoke_text() -> str: assert SMOKE_SH.is_file(), f"missing Void smoke gate: {SMOKE_SH}" return SMOKE_SH.read_text(encoding="utf-8") def _gate_packages() -> list: """The ordered package list the gate builds, parsed from smoke.sh.""" match = re.search(r'^PKGS="([^"]+)"', _smoke_text(), re.M) assert match, "smoke.sh must declare its ordered PKGS list" return match.group(1).split() def test_gate_builds_every_template_and_nothing_else() -> None: """A renamed or added template cannot silently drop out of the smoke.""" assert _gate_packages() and set(_gate_packages()) == set( _srcpkg_templates() ), f"gate covers {_gate_packages()}, srcpkgs holds {_srcpkg_templates()}" def test_gate_builds_the_app_last() -> None: """voculinux resolves the other templates from the local repository.""" assert ( _gate_packages()[-1] == "voculinux" ), f"the app must build last, gate order is {_gate_packages()}" def test_gate_delegates_to_the_per_package_harness() -> None: """One container flow implementation; the gate only orchestrates it.""" assert "build.sh" in _smoke_text(), "smoke.sh must call build.sh per package" def test_gate_stays_podman_only_with_pinned_inputs() -> None: """Podman, never Docker; pins live in tool_checksums.txt, not the gate.""" text = _smoke_text() assert "podman" in text, "the smoke gate must use podman" assert "docker" not in text.lower().replace("never docker", "") assert "ghcr.io" not in text, "the base image pin lives in tool_checksums.txt" assert PINS.is_file(), f"missing pins file: {PINS}" def test_gate_names_the_failing_package() -> None: """Output identifies which package failed, alongside the stage echoes.""" text = _smoke_text() assert "FAIL" in text, "the gate must report failures as FAIL" assert re.search( r"FAIL.*\$PKG|FAIL.*\$\{PKG", text ), "the FAIL report must name the failing package" def _case_branch(label: str) -> str: """Shell lines of one case branch in container-main.sh.""" text = CONTAINER_MAIN.read_text(encoding="utf-8") start = text.index(f"\n{label})") end = text.index(";;", start) return text[start:end] def test_harness_scopes_the_ggml_scan_to_the_binding() -> None: """Only the whisper binding ships libggml; leaves must not need it.""" assert "libggml-cpu" in _case_branch("python3-pywhispercpp") assert "libggml-cpu" not in _case_branch("*") def test_harness_smokes_the_leaf_packages() -> None: """PyAudio imports headless; pynput needs a display, so prove presence.""" assert "import pyaudio" in _case_branch("python3-PyAudio") branch = _case_branch("python3-pynput") assert "pynput" in branch assert "find_spec" in branch or "version(" in branch def test_harness_rejects_packages_without_a_smoke_recipe() -> None: """A future template falls into *) and must fail loudly, never pass mute.""" branch = _case_branch("*") assert "exit 1" in branch def test_just_recipe_calls_the_gate_like_other_packaging_gates() -> None: """`just void-smoke` runs the gate script with no shared-output step.""" recipe = re.search( r"^void-smoke:([^\n]*)\n((?:[ \t].*\n|\n)+)", JUSTFILE.read_text(encoding="utf-8"), re.MULTILINE, ) assert recipe, "justfile must define a void-smoke recipe" assert not recipe.group(1).strip(), "void-smoke must not take a shared-output prerequisite" assert "packaging/void/smoke.sh" in recipe.group( 2 ), "void-smoke must run packaging/void/smoke.sh" def test_readme_documents_the_one_command_gate() -> None: """AC1: one documented command from a clean checkout, no host Void.""" text = README.read_text(encoding="utf-8") assert "void-smoke" in text or "smoke.sh" in text, "README must document the smoke gate command" assert "podman" in text.lower(), "README must state the gate uses podman"