hermes-agent/tests/docker/test_profile_gateway.py
Ben 244d62ded3 test(docker): lock baseline behavior for Phase 0 harness
Tasks 0.2-0.6 of the s6-overlay supervision plan. Locks the
user-visible behavior we must preserve through the Phase 2 init-
system swap:

- test_main_invocation.py (Task 0.2): docker run <image> with no
  args, chat subcommand passthrough, bare executable passthrough,
  bash pattern, exit-code propagation
- test_tui_passthrough.py (Task 0.3): TTY allocation via docker -t
  using the host's script(1) for a PTY
- test_dashboard.py (Task 0.4): HERMES_DASHBOARD=1 opt-in,
  HERMES_DASHBOARD_PORT override
- test_profile_gateway.py (Task 0.5): per-profile gateway
  start/stop and profile-delete-stops-gateway. Both marked
  xfail(strict=True) because the current tini image refuses
  gateway lifecycle commands inside the container; Phase 4
  Task 4.3 flips them to passing.
- test_zombie_reaping.py (Task 0.6): PID 1 reaps orphaned
  zombies. tini does this today; s6-overlay's /init must
  continue to.

Refs: docs/plans/2026-05-07-s6-overlay-dynamic-subagent-gateways.md
2026-05-22 11:46:52 +10:00

97 lines
3.1 KiB
Python

"""Harness: per-profile gateway start/stop inside the container.
Phase 4 will change the *implementation* of these commands inside the
container — they'll talk to s6 instead of refusing. The user-visible
surface that should result is locked here.
NOTE: These tests are marked ``xfail(strict=True)`` until Phase 4 lands.
The current tini image deliberately refuses gateway start/stop inside
containers — ``pgrep`` finds nothing and the tests fail. After Phase 4
they should flip to passing automatically; ``strict=True`` means an
unexpected pass also fails the test, protecting against side-channel
fixes outside the planned Phase 4 mechanism.
"""
from __future__ import annotations
import subprocess
import time
import pytest
PROFILE = "test-harness-profile"
_PHASE4_REASON = (
"Phase 4 not yet landed: container-side `hermes gateway start` "
"currently exits 0 with an informational message instead of "
"spawning/supervising a gateway. Remove this marker after Task 4.3."
)
def _sh(
container: str, command: str, timeout: int = 30,
) -> subprocess.CompletedProcess[str]:
return subprocess.run(
["docker", "exec", container, "sh", "-c", command],
capture_output=True, text=True, timeout=timeout,
)
@pytest.mark.xfail(reason=_PHASE4_REASON, strict=True)
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}"
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}"
)
time.sleep(3)
r = _sh(container_name, f"pgrep -f 'gateway.*{PROFILE}'")
assert r.returncode == 0, "gateway process not running"
r = _sh(container_name, f"hermes -p {PROFILE} gateway stop", timeout=30)
assert r.returncode == 0
time.sleep(2)
r = _sh(container_name, f"pgrep -f 'gateway.*{PROFILE}'")
assert r.returncode != 0, "gateway process still running after stop"
@pytest.mark.xfail(reason=_PHASE4_REASON, strict=True)
def test_profile_delete_stops_gateway(
built_image: str, container_name: str,
) -> None:
"""Deleting a profile should stop its gateway if running."""
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
time.sleep(2)
r = _sh(container_name, f"pgrep -f 'gateway.*{PROFILE}'")
assert r.returncode != 0, "gateway still running after profile delete"