"""Harness: per-profile gateway start/stop inside the container. Phase 4 wires `hermes -p gateway start/stop` through the s6 ServiceManager dispatch path inside the container — so the lifecycle commands now bring up an s6-supervised gateway rather than refusing with the pre-Phase-4 informational message. These tests were marked ``xfail(strict=True)`` through Phase 0–3 and flip to plain ``test_…`` once Phase 4 lands (now). NB: The harness profile has no model/auth configured. Depending on how the gateway run script handles missing config, the supervised process may either spin up successfully (and svstat reports ``up``) or exit fast and get throttled by s6 (and svstat reports ``down …, want up``). Both states are valid "user asked for gateway up" results — what we assert is the *want* intent the lifecycle command set, NOT the supervised process's health. ``s6-svc -u`` records ``want up`` in the supervise/status file regardless of the run-script outcome. Every ``docker exec`` here runs as the unprivileged ``hermes`` user (via :func:`docker_exec_sh` in conftest); see the conftest module docstring. """ from __future__ import annotations import subprocess import time from tests.docker.conftest import docker_exec_sh PROFILE = "test-harness-profile" def _sh( container: str, command: str, timeout: int = 30, ) -> subprocess.CompletedProcess[str]: return docker_exec_sh(container, command, timeout=timeout) def _svstat(container: str) -> str: """Returns the raw s6-svstat output for the test profile's slot. /command/s6-svstat is called by absolute path because /command/ isn't on PATH for docker-exec sessions.""" r = _sh(container, f"/command/s6-svstat /run/service/gateway-{PROFILE}") return r.stdout if r.returncode == 0 else "" def _svstat_wants_up(container: str) -> bool: """Read the slot's want-state from s6-svstat output. s6-svstat formats the output to elide redundancies — when the service is currently up AND s6 wants it up, the literal token ``want up`` doesn't appear (it's implicit from the leading ``up``). When the service is down but s6 wants it back up, ``, want up`` appears explicitly. So a comprehensive "is the want-intent set to up" check has to accept both spellings. """ state = _svstat(container) if not state: return False head = state.split()[0] if state.split() else "" if head == "up": # Currently up implies wanted-up unless ``want down`` is set. return "want down" not in state # Currently down — ``want up`` only shows up when explicitly set. return "want up" in state def test_profile_create_then_gateway_start( built_image: str, container_name: str, ) -> None: subprocess.run( ["docker", "run", "-d", "--name", container_name, built_image, "sleep", "120"], check=True, capture_output=True, timeout=30, ) time.sleep(3) r = _sh(container_name, f"hermes profile create {PROFILE}") assert r.returncode == 0, f"profile create failed: {r.stderr}" # Profile create's s6-register hook should have produced a service slot. r = _sh(container_name, f"test -d /run/service/gateway-{PROFILE}") assert r.returncode == 0, "s6 service slot not created on profile create" r = _sh(container_name, f"hermes -p {PROFILE} gateway start", timeout=60) assert r.returncode == 0, ( f"gateway start failed: stderr={r.stderr!r} stdout={r.stdout!r}" ) # After start, s6's intent is "up" — even if the supervised gateway # process spin-fails (no model/auth in the test profile), the # supervision-state contract holds. See ``_svstat_wants_up`` for # why we accept both ``up …`` (currently up) and ``down …, want # up`` (down but s6 wants up). time.sleep(2) assert _svstat_wants_up(container_name), ( f"slot want-state is not up after gateway start: " f"{_svstat(container_name)!r}" ) r = _sh(container_name, f"hermes -p {PROFILE} gateway stop", timeout=30) assert r.returncode == 0 time.sleep(2) assert not _svstat_wants_up(container_name), ( f"slot want-state still up after gateway stop: " f"{_svstat(container_name)!r}" ) def test_profile_delete_stops_gateway( built_image: str, container_name: str, ) -> None: """Deleting a profile should stop its gateway and remove the s6 service slot.""" subprocess.run( ["docker", "run", "-d", "--name", container_name, built_image, "sleep", "120"], check=True, capture_output=True, timeout=30, ) time.sleep(3) _sh(container_name, f"hermes profile create {PROFILE}") _sh(container_name, f"hermes -p {PROFILE} gateway start", timeout=60) time.sleep(3) r = _sh( container_name, f"hermes profile delete {PROFILE} --yes", timeout=30, ) assert r.returncode == 0, f"profile delete failed: {r.stderr}" time.sleep(2) # Service slot should be gone. r = _sh(container_name, f"test -d /run/service/gateway-{PROFILE}") assert r.returncode != 0, "s6 service slot still present after profile delete"