"""Behavior contracts for pm's bionic target + DebPackage + stage_only. The lock VALUES are bumped by review; these tests pin the relationships: the linux-arm64-bionic rows exist and agree with their suppliers' shapes, DebPackage extraction is hardened, and cross-target staging never touches this host's installed facts. """ from __future__ import annotations import json from pathlib import Path import pytest REPO_ROOT = Path(__file__).resolve().parents[2] @pytest.fixture(scope="module") def lock(): return json.loads((REPO_ROOT / "pm" / "lock.json").read_text(encoding="utf-8")) def test_all_targets_includes_bionic(): from pm.store import ALL_TARGETS assert "linux-arm64-bionic" in ALL_TARGETS assert ALL_TARGETS.count("linux-arm64-bionic") == 1 def _assert_pinned_bionic_row(lock, pkg, url_suffix_re): """The bionic rows are DELIBERATE explicit pins: the version axis follows main (the desktop artifacts), while the termux/TUR suppliers rotate or lag, so the bionic row pins whatever the supplier actually ships today. The stage path consumes the row's url+sha256 directly. The contract: the row exists, is https, matches the supplier's filename shape, and fetch_url AGREES with the pinned row for this target.""" import re as _re row = lock["packages"][pkg]["artifacts"].get("linux-arm64-bionic") assert row, f"{pkg} has no linux-arm64-bionic artifact" assert row["url"].startswith("https://"), f"{pkg} bionic row not https" m = _re.search(url_suffix_re, row["url"]) assert m, f"{pkg} bionic url shape: {row['url']}" assert _re.fullmatch(r"[0-9a-f]{64}", row["sha256"]) from pm.registry import get_package cls = get_package(pkg) # fetch_url must reproduce the pinned row exactly for this target # (the pinned version is recovered from the URL itself). assert cls.fetch_url(m.group("ver"), "linux-arm64-bionic") == row["url"] def test_python_bionic_row_matches_supplier(lock): """The Python package definition and lock must agree on the termux-main artifact.""" _assert_pinned_bionic_row(lock, "python", r"/p/python/python_(?P[0-9.]+(?:-[0-9]+)?)_aarch64\.deb$") def test_node_bionic_row_matches_supplier(lock): """The node bionic row is an explicit pin of the termux-main nodejs .deb; the row and Nodejs.fetch_url(bionic arm) must agree.""" _assert_pinned_bionic_row(lock, "node", r"nodejs_(?P[0-9.]+)-1_aarch64\.deb$") def test_termux_docker_row_pins_digest(lock): row = lock["packages"]["termux-docker"]["artifacts"]["linux-arm64-bionic"] version = lock["packages"]["termux-docker"]["version"] assert version.startswith("sha256:") assert len(version) == 7 + 64 assert row["url"] == f"docker://termux/termux-docker@{version}" def test_python_bionic_pin_is_independent_of_desktop_build_version(lock): from pm.registry import get_package py = get_package("python") package = lock["packages"]["python"] assert py.fetch_url(package["version"], "linux-arm64-bionic") == package["artifacts"]["linux-arm64-bionic"]["url"] assert py.deb_package == "python" def test_uv_bionic_row_matches_supplier(lock): """The uv bionic row is an explicit pin of the termux-main pool .deb; the row and Uv.fetch_url(bionic arm) must agree.""" _assert_pinned_bionic_row(lock, "uv", r"/u/uv/uv_(?P[0-9.]+)_aarch64\.deb$") @pytest.mark.parametrize("name,main,on_path", [ ("python", None, True), ("uv", "bin/uv", False), ("node", "bin/node", True), ]) def test_registered_bionic_stage_preserves_host_facts(tmp_path, monkeypatch, lock, name, main, on_path): import hashlib from pm import paths from pm.install import stage_only from pm.lock import Lockfile from pm.package import InstallError from pm.registry import get_package from pm.store import Store from tests.termux_fixtures import build_deb target = "linux-arm64-bionic" monkeypatch.setenv("HERMES_RUNTIME_DIR", str(tmp_path / "runtime")) monkeypatch.setattr(paths, "lockfile_path", lambda: tmp_path / "lock.json") facts = paths.facts_path() facts.parent.mkdir(parents=True, exist_ok=True) facts.write_bytes(b'{"sentinel": "host state must not change"}') before = facts.read_bytes() if main is None: # Archive filename is the independent supplier authority, not main_rel(). version = lock["packages"]["python"]["artifacts"][target]["url"].rsplit("/", 1)[1].split("_")[1] main = "bin/python" + ".".join(version.split(".")[:2]) relative = "data/data/com.termux/files/usr/" + main package = get_package(name) store = Store(paths.store_root()) def archive(files): deb = tmp_path / "fixture.deb" build_deb(deb, {"Package": name, "Version": "1.0"}, files) digest = hashlib.sha256(deb.read_bytes()).hexdigest() lock = Lockfile(paths.lockfile_path()) lock.set_pin(name, "1.0", {target: {"url": "https://example.test/fixture.deb", "sha256": digest}}) lock.save() cached = store.entry(f"fetch-{digest}") cached.mkdir(parents=True) (cached / "fixture.deb").write_bytes(deb.read_bytes()) def no_exec(*args, **kwargs): pytest.fail(f"cross-target staging executed foreign bytes: {args}") monkeypatch.setattr("pm.packages.subprocess.run", no_exec) archive({relative: b"bionic-payload"}) entry = stage_only(name, target) assert (entry / relative).read_bytes() == b"bionic-payload" assert package.binary(entry, target) == entry / relative assert package.env(entry, target).get("PATH") == ([str((entry / relative).parent)] if on_path else None) assert stage_only(name, target) == entry archive({"unrelated": b"not the main executable"}) with pytest.raises(InstallError, match="missing"): stage_only(name, target) assert (entry / relative).read_bytes() == b"bionic-payload" assert facts.read_bytes() == before def test_deb_rejects_traversal_before_touching_outside(tmp_path): from pm.package import DebPackage, InstallError from tests.termux_fixtures import build_deb sentinel = tmp_path / "escape" sentinel.write_bytes(b"owned outside extraction") deb = tmp_path / "evil.deb" build_deb(deb, {"Package": "evil"}, {"../escape": b"overwrite"}) with pytest.raises(InstallError, match="unsafe|escape|traversal"): DebPackage().unpack(deb, tmp_path / "staged", "linux-arm64-bionic") assert sentinel.read_bytes() == b"owned outside extraction"