feat(5): overleveringspakka er KUN kjørbar Python — container-innpakningen fjernet

Operatørdirektiv 14.08 etter ekstern test: mottakeren skal få kjørbar Python, ikke en
Docker-innpakking. `Dockerfile` og `azure.yaml` er SLETTET fra treet.

Sømmen er valgt av den eksisterende invarianten, ikke av smak: pakka ER `git archive HEAD`,
så å ekskludere filene fra arkivet ville krevd en kurerings-mekanisme — den andre kopien av
«hva mottakeren får», fri til å drifte fra HEAD (kø-(p)). Fjerning holder arkivet ukurert og
gjør fraværet til en egenskap ved HEAD, som er det eneste en gate kan måle.

De to gatene som pinnet flaten er håndtert bevisst:
- 4e-rå-tekst-gaten (`--platform linux/amd64` + ÉN kopi av startkommandoen) er SLETTET, med
  et notat der den sto. En gate som pinner en fjernet flate kan bare bli grønn.
- handover-gatens `_REQUIRED_MEMBERS` er ikke bare fratatt de to navnene, men erstattet av en
  POSITIV fraværs-assert pluss en dokument-gate. Å kun slutte å KREVE dem ville gitt en gate
  som ikke kan skille «fjernet» fra «shippes fortsatt».

Startkommandoen har nå ÉN kopi igjen: DEPLOY.md-ens `python main.py`, som navngir inngangen
subprosess-testen faktisk kjører.
This commit is contained in:
Kjell Tore Guttormsen 2026-08-14 14:09:35 +02:00
commit 605c02aa62
10 changed files with 215 additions and 205 deletions

View file

@ -18,11 +18,11 @@ Load-bearing surface pinned here:
* The server is asyncio on the ONE loop (NG1: ``test_no_thread_or_process_path_exists_under_src``
ratchets src/ thread-free) these tests run client and server as coroutines on the SAME loop,
which only works because nothing in the server blocks it.
* Root ``main.py`` is the ONE process entry (Dockerfile CMD + azure.yaml point at it): the
* Root ``main.py`` is the ONE process entry (``python main.py``, the command DEPLOY.md prints): the
subprocess test is the ONLY test that catches a detached shim or a detached SIGTERM handler
(P4-presedensen: entry-point-mutasjoner fanges aldri av in-process-tester).
Fase 4e closes two gaps the above leaves open, and both are about things a stand-in cannot see:
Fase 4e closed two gaps the above leaves open, and both are about things a stand-in cannot see:
* **The whitelist composes with the REAL ``run_project``.** Every test above hands ``invoke`` a
stand-in that swallows ``**kwargs``, so the whitelist could name a field ``run_project`` does not
@ -33,14 +33,11 @@ Fase 4e closes two gaps the above leaves open, and both are about things a stand
PURPOSE (a caller must never choose the server's model client), so patching the factory the run
falls back to is the only injection point this surface leaves the same argument
``test_run_cli_loadbearing`` makes for ``main()``.
* **The deployment artifacts are raw-text-gated.** ``Dockerfile`` and ``azure.yaml`` are the two
files that decide whether the image the platform pulls can run at all, and NO test executes
them here (``docker build``/``azd deploy`` are operator-gated). A raw-text gate is therefore the
only mechanism available: it pins ``--platform linux/amd64`` (measured required spike §1.4;
an arm64 image built on this Intel-free-of-charge assumption would fail only in the cloud) and
the ONE-copy rule for the start command (the image's ``CMD``; ``azure.yaml`` carries no
``startupCommand`` to drift from it). Guard-tester leser kildefiler som TEKST reformulate
the prose around them, never the strings they pin.
* **The deployment artifacts were raw-text-gated** until 14.08, when the operator directive
after an external trial made the delivery runnable Python and the two artifacts were removed
from the tree. The gate is deleted, not weakened; see the note where it stood, below the
end-to-end test. The start command now has exactly one copy left, in DEPLOY.md, and
``tests/test_handover_package_loadbearing.py`` is what keeps it there.
"""
from __future__ import annotations
@ -381,8 +378,8 @@ def _blocking_get(url: str) -> tuple[int, str]:
def test_main_entrypoint_serves_and_stops_on_sigterm() -> None:
"""Root main.py is the ONE process entry (Dockerfile CMD + azure.yaml point at it): started
as a subprocess it must serve /readiness and exit 0 on SIGTERM. This is the only test that
"""Root main.py is the ONE process entry (``python main.py``, the command DEPLOY.md prints):
started as a subprocess it must serve /readiness and exit 0 on SIGTERM. This is the only test that
catches a shim that stops calling hosting.main() or a detached SIGTERM handler."""
with socket.socket() as probe:
probe.bind(("127.0.0.1", 0))
@ -415,7 +412,7 @@ def test_main_entrypoint_serves_and_stops_on_sigterm() -> None:
proc.wait()
# --- Fase 4e: the whitelist against the REAL run_project, and the deployment artifacts ----------
# --- Fase 4e: the whitelist against the REAL run_project -----------------------------------------
@pytest.fixture()
@ -491,51 +488,9 @@ async def test_invocations_answers_through_the_real_run_project(
assert _scripted_backend, "the scripted backend was never called — no real run happened"
def test_deployment_artifacts_pin_the_measured_platform_and_one_start_command() -> None:
"""RAW-TEXT GATE on the two files that decide whether the hosted image runs at all.
Neither is executed by any test: ``docker build`` and ``azd deploy`` are operator-gated, so a
regression in them is invisible to the whole suite until it fails in the cloud. What the gate
pins is exactly what was MEASURED, and nothing about the prose around it:
* ``--platform linux/amd64`` the platform requires x86_64 (spike §1.4). Dropping it makes the
image inherit the builder's architecture, which on an arm64 laptop yields an image that
builds green locally and cannot start in the cloud. The flag lives in the documented build
command (a Dockerfile cannot set the build platform for its own invocation), so pinning the
documented string is the only gate available and it is worth having precisely because
nothing else re-derives it.
* ONE copy of the start command: the image's ``CMD`` names ``main.py``, and ``azure.yaml``
declares NO ``startupCommand``. Two copies drift (-(p)-regelen); this is the pair that
keeps there being one.
``env:`` is checked for the same reason: ``FOUNDRY_PROJECT_ENDPOINT`` is injected by the
platform, and redeclaring it here could shadow the injected value which is the failure mode
Fase 4b's endpoint precedence exists to avoid, undone from the config side."""
dockerfile = _REPO_ROOT / "Dockerfile"
azure_yaml = _REPO_ROOT / "azure.yaml"
assert dockerfile.is_file(), "the hosted image has no build definition"
assert azure_yaml.is_file(), "azd has no project definition to deploy"
docker_text = dockerfile.read_text(encoding="utf-8")
azure_text = azure_yaml.read_text(encoding="utf-8")
assert "--platform linux/amd64" in docker_text, (
"the Dockerfile no longer names the measured build platform; an image built without it "
"inherits the builder's architecture and cannot start on the hosting platform"
)
assert "CMD" in docker_text and "main.py" in docker_text, (
"the image's CMD is the ONE copy of the start command and must name main.py"
)
# Line-anchored: a mention inside a comment is prose, a top-level key is a declaration.
azure_keys = [line.split(":")[0].strip() for line in azure_text.splitlines()]
assert "startupCommand" not in azure_keys, (
"azure.yaml declares a startupCommand — a SECOND copy of the start command, free to drift "
"from the image's CMD"
)
assert "env" not in azure_keys, (
"azure.yaml declares an env block — FOUNDRY_PROJECT_ENDPOINT is injected by the platform "
"and must never be redeclared here"
)
assert "host: azure.ai.agent" in azure_text and "kind: hosted" in azure_text, (
"azure.yaml no longer declares the hosted-agent host this whole entrypoint targets"
)
# The 4e raw-text gate on ``Dockerfile``/``azure.yaml`` lived here until 14.08. It pinned
# ``--platform linux/amd64`` and the one-copy rule for the image's ``CMD``. Both files were removed
# from the tree that day (operator directive: the delivery is runnable Python), and a gate that pins
# a surface we no longer ship is deleted with it rather than weakened into something that can only
# be green. What replaced it lives in ``tests/test_handover_package_loadbearing.py``: the package
# must carry NO container/azd wrapper and must document the Python start command.