diff --git a/tests/conftest.py b/tests/conftest.py index bac6334869..acaca8d8c5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1428,6 +1428,14 @@ def _live_system_guard(request, monkeypatch): "daemon-reload", "try-restart", "reload-or-restart", ) _PROCESS_KILLERS = ("pkill", "killall", "taskkill", "skill", "fuser") + _CONTAINER_RUNTIMES = ("docker", "podman", "nerdctl") + + def _first_token_basename(cmd_str: str) -> str: + try: + tokens = _shlex.split(cmd_str) + except ValueError: + tokens = cmd_str.split() + return tokens[0].rsplit("/", 1)[-1].lower() if tokens else "" # Shell/launcher executables whose arguments are themselves commands — # argv[0]-only scanning must not exempt what they wrap. _WRAPPER_COMMANDS = ( @@ -1563,7 +1571,14 @@ def _live_system_guard(request, monkeypatch): # sibling refactor moved the spawn seam and left tests patching the # facade. The canonical matcher, never an argv substring. from gateway.status import _gateway_command_subcommand - if not lookalike_ok and _gateway_command_subcommand(cmd_str) in ("run", "start", "restart"): + # A gateway launched INSIDE a container (`docker exec … hermes gateway start`) cannot + # reach the host's systemd unit or webhook port; tests/docker/ exists to exercise it. + in_container = _first_token_basename(cmd_str) in _CONTAINER_RUNTIMES + if ( + not lookalike_ok + and not in_container + and _gateway_command_subcommand(cmd_str) in ("run", "start", "restart") + ): raise RuntimeError( f"tests/conftest.py live-system guard: blocked " f"subprocess.{name}({cmd!r}) — this would spawn a REAL " diff --git a/tests/test_live_system_guard.py b/tests/test_live_system_guard.py index 83e98565f9..ce51a08cce 100644 --- a/tests/test_live_system_guard.py +++ b/tests/test_live_system_guard.py @@ -37,3 +37,29 @@ def test_wrapped_killer_command_is_still_blocked(): def test_env_wrapped_killer_command_is_still_blocked(): with pytest.raises(RuntimeError, match="live-system guard"): subprocess.run(["env", "GUARD_TEST=1", "pkill", "-f", "hermes-guard-regression-nomatch"]) + + +def test_gateway_start_inside_a_container_exec_is_not_blocked(): + """``docker exec hermes gateway start`` launches the gateway INSIDE the container, + where it cannot reach the host's systemd unit or webhook port; tests/docker/ depends on it. + The binary is a stub so the argv stays exact without needing a Docker daemon.""" + import os + import stat + + stub_dir = os.path.join(os.environ["HERMES_HOME"], "stub-bin") + os.makedirs(stub_dir, exist_ok=True) + stub = os.path.join(stub_dir, "docker") + with open(stub, "w") as fh: + fh.write("#!/bin/sh\nexit 0\n") + os.chmod(stub, os.stat(stub).st_mode | stat.S_IXUSR) + result = subprocess.run( + [stub, "exec", "-u", "hermes", "ctr", "sh", "-c", "hermes -p prof gateway start"], + capture_output=True, + text=True, + ) + assert result.returncode == 0 + + +def test_gateway_start_on_the_host_is_still_blocked(): + with pytest.raises(RuntimeError, match="REAL.*gateway runtime"): + subprocess.run(["python", "-m", "hermes_cli.main", "gateway", "start"])