"""Fase 5 — the external handover package: one archive a stranger can deploy into their own Microsoft Foundry with minimal friction. **Why an archive and not "clone the repo".** The receiving party is not a contributor: they get a tree, set two environment variables and deploy it. The measured docker build context has always been ``git archive HEAD`` (Dockerfile header), so the package is that SAME tree — never a hand-curated copy, which would be the second copy that drifts (the kø-(p) rule applied to a deliverable). Three seams are gated here, and each one is a way the handover fails in the receiver's hands rather than in ours: 1. **Completeness** — a tree missing ``uv.lock`` resolves different versions than every measurement in this repo ran against; missing ``shared/`` gives a container with no example knowledge base. 2. **Exposure** — ``STATE.md``, ``*.local.md`` and ``.env`` must never leave this machine. The archive is built from tracked files only, so this is a property of the BUILDER; the control below proves the check looks for names that could actually appear. 3. **The deploy contract is written down** — the packaged ``model_map.json`` ships ``REPLACE-WITH-*`` placeholders and ``backends.py`` fail-fasts on them, so a receiver who sets only an endpoint gets a container that answers ``/readiness`` and fails every ``/invocations``. 4e measured that requirement and called it "et deploy-krav ingen rad hadde skrevet ned"; DEPLOY.md is that row, and this test is what keeps it written. """ from __future__ import annotations import subprocess import zipfile from pathlib import Path import pytest _REPO_ROOT = Path(__file__).resolve().parents[1] _SCRIPT = _REPO_ROOT / "scripts" / "make-handover-package.sh" # Deploy-critical members. Each one is load-bearing for a receiver, not decoration: # the two manifests, the entry point, the locked resolution, the packaged data and the example base. _REQUIRED_MEMBERS = ( "Dockerfile", "azure.yaml", "main.py", "pyproject.toml", "uv.lock", "DEPLOY.md", "src/portfolio_optimiser/hosting.py", "src/portfolio_optimiser/backends.py", "src/portfolio_optimiser/data/model_map.json", "shared/examples/bygg-energi-mikro/index.md", ) # Names that must NEVER reach a stranger. Local-only continuity, operator config, secrets. _FORBIDDEN_SUFFIXES = (".local.md", ".env") _FORBIDDEN_NAMES = ("STATE.md",) @pytest.fixture(scope="module") def package(tmp_path_factory: pytest.TempPathFactory) -> zipfile.ZipFile: """Build the real package by running the real script — the packaging config is itself a seam (the 4a precedent: an ``uv build`` in the fixture, never a simulated one).""" dest = tmp_path_factory.mktemp("handover") result = subprocess.run( [str(_SCRIPT), str(dest)], cwd=_REPO_ROOT, capture_output=True, text=True, check=False, ) assert result.returncode == 0, f"builder failed: {result.stderr}" archives = sorted(dest.glob("*.zip")) assert len(archives) == 1, f"expected exactly one archive, got {archives}" return zipfile.ZipFile(archives[0]) def test_package_carries_every_deploy_critical_file(package: zipfile.ZipFile) -> None: """Detach point: drop a member from the archive → RED. A receiver cannot supply what we omit.""" names = set(package.namelist()) missing = [m for m in _REQUIRED_MEMBERS if m not in names] assert not missing, f"handover package is missing {missing}" def test_package_leaks_no_local_or_secret_files(package: zipfile.ZipFile) -> None: """Detach point: build from the working tree instead of tracked files → RED (STATE.md appears). The control is the point: a filter that matched nothing would make this gate green forever, so we first prove the archive is populated and that the suffixes we forbid are ones the repo actually produces (``STATE.md`` exists on this machine, untracked-by-design).""" names = package.namelist() assert len(names) > 50, "archive suspiciously small — the check below would be vacuous" assert (_REPO_ROOT / "STATE.md").exists(), ( "control: STATE.md must exist locally, else this gate cannot discriminate" ) leaked = [ n for n in names if Path(n).name in _FORBIDDEN_NAMES or n.endswith(_FORBIDDEN_SUFFIXES) ] assert not leaked, f"handover package leaks local-only files: {leaked}" def test_deploy_doc_names_both_required_env_vars(package: zipfile.ZipFile) -> None: """Detach point: remove either variable from DEPLOY.md → RED. Line-anchored, not substring: ``PORTFOLIO_FOUNDRY_PROJECT_ENDPOINT`` CONTAINS ``FOUNDRY_PROJECT_ENDPOINT``, so a naive substring assert on the platform-injected name is satisfied by our own (the 08-09 defect class, measured twice before in this repo).""" doc = package.read("DEPLOY.md").decode("utf-8") lines = doc.splitlines() assert any("PORTFOLIO_MODEL_MAP" in line for line in lines), ( "DEPLOY.md must name PORTFOLIO_MODEL_MAP — without it the container fail-fasts on the " "REPLACE-WITH-* placeholders" ) # The injected name must appear on a line that is NOT merely our own name. injected_lines = [ line for line in lines if "FOUNDRY_PROJECT_ENDPOINT" in line.replace("PORTFOLIO_FOUNDRY_PROJECT_ENDPOINT", "") ] assert injected_lines, "DEPLOY.md must name the platform-injected FOUNDRY_PROJECT_ENDPOINT" def test_deploy_doc_states_the_placeholder_requirement(package: zipfile.ZipFile) -> None: """Detach point: drop the placeholder warning → RED. The packaged map ships REPLACE-WITH-*, so a receiver who is not told will deploy a container that fails every invocation.""" doc = package.read("DEPLOY.md").decode("utf-8") packaged_map = package.read("src/portfolio_optimiser/data/model_map.json").decode("utf-8") # Control: the requirement is only real while the packaged map actually ships placeholders. assert "REPLACE-WITH-" in packaged_map, ( "control: packaged model_map no longer has placeholders — this gate would be vacuous" ) assert "REPLACE-WITH-" in doc, ( "DEPLOY.md must state that the packaged deployment ids are placeholders" )