hermes-agent/tests/gateway/test_restart_service_detection.py
Kyzcreig bcec6c8d39 fix(test): hermetic env-detection tests — pin container/supervisor/HOME probes (#422)
The restart-routing, systemd-support, and subprocess-HOME tests asserted
branch behavior but left part of the real probe surface unmocked, so they
fail when the suite itself runs inside a container (self-hosted CI) or a
launchd-descended shell:

- /restart routing tests: the handler also consults the real /.dockerenv —
  extract the inline probe to gateway.restart.is_container_restart_context()
  (patchable seam, no behavior change) and pin it False; scrub ALL four
  supervisor env markers (ambient XPC_SERVICE_NAME on macOS flipped one).
- supports_systemd_services tests: pin shutil.which('systemctl') and
  is_container() so the test asserts the branch, not the host.
- copilot ACP real-HOME test: pin is_container() (auto mode prefers profile
  home in containers) and scrub ambient HERMES_REAL_HOME/TERMINAL_HOME_MODE.

97 tests green on macOS dev box AND inside a docker CI runner container.

Co-authored-by: Kyzcreig <9063726+Kyzcreig@users.noreply.github.com>
2026-07-29 18:55:10 -07:00

78 lines
3.3 KiB
Python

"""Tests for /restart service-manager detection (launchd vs interactive).
The /restart handler routes through ``request_restart(via_service=True)``
when a service manager supervises the gateway, so the process exits with
the service-restart code and the manager relaunches it. Under macOS
launchd the plist uses ``KeepAlive.SuccessfulExit=false`` — a clean exit 0
is treated as a deliberate stop and the gateway stays dead (#43475) — so
launchd must be detected here in the handler, not only at the exit-code
site (which never runs unless ``via_service=True`` is already set).
launchd sets ``XPC_SERVICE_NAME`` to the job label for processes it
spawns. Interactive macOS shells inherit ``XPC_SERVICE_NAME=0`` (a
truthy string), so the probe must treat ``"0"`` as not-under-launchd:
routing an unsupervised interactive gateway to the service path would
make it exit non-zero with nothing to revive it.
"""
from unittest.mock import MagicMock
import pytest
import gateway.run as gateway_run
from gateway.platforms.base import MessageEvent, MessageType
from gateway.restart import EXTERNAL_GATEWAY_SUPERVISOR_ENV
from tests.gateway.restart_test_helpers import make_restart_runner, make_restart_source
def _make_restart_event(update_id: int | None = 100) -> MessageEvent:
return MessageEvent(
text="/restart",
message_type=MessageType.TEXT,
source=make_restart_source(),
message_id="m1",
platform_update_id=update_id,
)
def _make_runner_with_mock_restart(tmp_path, monkeypatch):
monkeypatch.setattr(gateway_run, "_hermes_home", tmp_path)
monkeypatch.delenv("INVOCATION_ID", raising=False)
monkeypatch.delenv("XPC_SERVICE_NAME", raising=False)
monkeypatch.delenv("HERMES_S6_SUPERVISED_CHILD", raising=False)
monkeypatch.delenv(EXTERNAL_GATEWAY_SUPERVISOR_ENV, raising=False)
# Hermeticity: neutralize the real container probe — on a containerized
# CI runner /.dockerenv exists and would route every case via_service=True
# regardless of the env markers under test (the detection under test is
# the SUPERVISOR markers, not the runner's own containment).
monkeypatch.setattr(
"gateway.restart.is_container_restart_context", lambda: False
)
runner, _adapter = make_restart_runner()
runner.request_restart = MagicMock(return_value=True)
return runner
@pytest.mark.asyncio
async def test_restart_with_external_supervisor_marker_uses_service_path(
tmp_path, monkeypatch
):
"""Wrapped supervisors can retain restart ownership without native markers."""
runner = _make_runner_with_mock_restart(tmp_path, monkeypatch)
monkeypatch.setenv(EXTERNAL_GATEWAY_SUPERVISOR_ENV, "1")
await runner._handle_restart_command(_make_restart_event())
runner.request_restart.assert_called_once_with(detached=False, via_service=True)
@pytest.mark.asyncio
@pytest.mark.parametrize("value", ["", "0", "false", "off"])
async def test_false_external_supervisor_marker_keeps_detached_path(
value, tmp_path, monkeypatch
):
runner = _make_runner_with_mock_restart(tmp_path, monkeypatch)
monkeypatch.setenv(EXTERNAL_GATEWAY_SUPERVISOR_ENV, value)
await runner._handle_restart_command(_make_restart_event())
runner.request_restart.assert_called_once_with(detached=True, via_service=False)