7008fb81b3
hermes update decides restart ownership from the live grandchild argv, not from an env marker. Newly generated plists now include the flag. The stderr_timestamp wrapper upgrades only historical Hermes gateway run shapes for stale plists and leaves arbitrary launchd children unmarked.
160 lines
5.0 KiB
Python
160 lines
5.0 KiB
Python
"""Tests for hermes_cli.stderr_timestamp."""
|
|
|
|
import re
|
|
import sys
|
|
|
|
from gateway.restart import EXTERNAL_GATEWAY_SUPERVISOR_ENV
|
|
from hermes_cli import stderr_timestamp
|
|
|
|
_STALE_GATEWAY_ARGV = [
|
|
sys.executable,
|
|
"-m",
|
|
"hermes_cli.main",
|
|
"gateway",
|
|
"run",
|
|
"--replace",
|
|
]
|
|
_LAUNCHD_ENV = {"PATH": "/usr/bin", "XPC_SERVICE_NAME": "ai.hermes.gateway-butler"}
|
|
|
|
|
|
def test_main_timestamps_each_stderr_line(tmp_path):
|
|
log_path = tmp_path / "gateway.error.log"
|
|
code = (
|
|
"import sys\n"
|
|
"sys.stderr.write('first failure\\n')\n"
|
|
"sys.stderr.write('second failure without newline\\n')\n"
|
|
"sys.stderr.write('2026-07-15 12:34:56,789 already timestamped')\n"
|
|
"sys.exit(7)\n"
|
|
)
|
|
|
|
rc = stderr_timestamp.main(
|
|
[
|
|
"--error-log",
|
|
str(log_path),
|
|
"--",
|
|
sys.executable,
|
|
"-c",
|
|
code,
|
|
]
|
|
)
|
|
|
|
assert rc == 7
|
|
lines = log_path.read_text(encoding="utf-8").splitlines()
|
|
timestamp = r"\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2},\d{3}"
|
|
assert len(lines) == 3
|
|
assert re.fullmatch(f"{timestamp} first failure", lines[0])
|
|
assert re.fullmatch(f"{timestamp} second failure without newline", lines[1])
|
|
assert lines[2] == "2026-07-15 12:34:56,789 already timestamped"
|
|
|
|
|
|
def test_prepare_upgrades_stale_gateway_argv_under_launchd():
|
|
upgraded = stderr_timestamp._prepare_child_command(
|
|
_STALE_GATEWAY_ARGV, _LAUNCHD_ENV
|
|
)
|
|
assert upgraded == [*_STALE_GATEWAY_ARGV, "--external-supervisor"]
|
|
|
|
|
|
def test_prepare_keeps_existing_external_supervisor_flag():
|
|
already = [*_STALE_GATEWAY_ARGV, "--external-supervisor"]
|
|
assert (
|
|
stderr_timestamp._prepare_child_command(already, _LAUNCHD_ENV) == already
|
|
)
|
|
|
|
|
|
def test_prepare_skips_arbitrary_command_under_launchd():
|
|
"""A generic wrapper must not mark random launchd children as the gateway."""
|
|
other = [sys.executable, "-c", "print('ok')"]
|
|
assert stderr_timestamp._prepare_child_command(other, _LAUNCHD_ENV) == other
|
|
|
|
|
|
def test_prepare_skips_interactive_xpc_zero_even_for_gateway_argv():
|
|
assert (
|
|
stderr_timestamp._prepare_child_command(
|
|
_STALE_GATEWAY_ARGV, {"PATH": "/usr/bin", "XPC_SERVICE_NAME": "0"}
|
|
)
|
|
== _STALE_GATEWAY_ARGV
|
|
)
|
|
assert (
|
|
stderr_timestamp._prepare_child_command(_STALE_GATEWAY_ARGV, {"PATH": "/usr/bin"})
|
|
== _STALE_GATEWAY_ARGV
|
|
)
|
|
|
|
|
|
def test_main_injects_flag_into_stale_gateway_child(tmp_path, monkeypatch):
|
|
"""Stale plist inner argv must grow --external-supervisor in the grandchild."""
|
|
monkeypatch.setenv("XPC_SERVICE_NAME", "ai.hermes.gateway-butler")
|
|
monkeypatch.delenv(EXTERNAL_GATEWAY_SUPERVISOR_ENV, raising=False)
|
|
log_path = tmp_path / "gateway.error.log"
|
|
marker_path = tmp_path / "argv.txt"
|
|
code = (
|
|
"import sys\n"
|
|
f"from pathlib import Path\n"
|
|
f"Path({str(marker_path)!r}).write_text("
|
|
"'\\n'.join(sys.argv[1:]), encoding='utf-8')\n"
|
|
)
|
|
stale = [sys.executable, "-c", code, "-m", "hermes_cli.main", "gateway", "run", "--replace"]
|
|
|
|
rc = stderr_timestamp.main(
|
|
["--error-log", str(log_path), "--", *stale]
|
|
)
|
|
|
|
assert rc == 0
|
|
recorded = marker_path.read_text(encoding="utf-8").splitlines()
|
|
assert recorded[-1] == "--external-supervisor"
|
|
assert "gateway" in recorded and "run" in recorded
|
|
|
|
|
|
def test_main_does_not_mark_arbitrary_launchd_child(tmp_path, monkeypatch):
|
|
monkeypatch.setenv("XPC_SERVICE_NAME", "ai.hermes.gateway-butler")
|
|
monkeypatch.delenv(EXTERNAL_GATEWAY_SUPERVISOR_ENV, raising=False)
|
|
log_path = tmp_path / "gateway.error.log"
|
|
marker_path = tmp_path / "marker.txt"
|
|
code = (
|
|
"import os\n"
|
|
f"from pathlib import Path\n"
|
|
f"Path({str(marker_path)!r}).write_text("
|
|
f"os.environ.get({EXTERNAL_GATEWAY_SUPERVISOR_ENV!r}, 'unset'), encoding='utf-8')\n"
|
|
)
|
|
|
|
rc = stderr_timestamp.main(
|
|
[
|
|
"--error-log",
|
|
str(log_path),
|
|
"--",
|
|
sys.executable,
|
|
"-c",
|
|
code,
|
|
]
|
|
)
|
|
|
|
assert rc == 0
|
|
assert marker_path.read_text(encoding="utf-8") == "unset"
|
|
|
|
|
|
def test_main_does_not_mark_unsupervised_child(tmp_path, monkeypatch):
|
|
"""Foreground/unsupervised starts must not inherit a fabricated marker."""
|
|
monkeypatch.setenv("XPC_SERVICE_NAME", "0")
|
|
monkeypatch.delenv(EXTERNAL_GATEWAY_SUPERVISOR_ENV, raising=False)
|
|
log_path = tmp_path / "gateway.error.log"
|
|
marker_path = tmp_path / "marker.txt"
|
|
code = (
|
|
"import os\n"
|
|
f"from pathlib import Path\n"
|
|
f"Path({str(marker_path)!r}).write_text("
|
|
f"os.environ.get({EXTERNAL_GATEWAY_SUPERVISOR_ENV!r}, 'unset'), encoding='utf-8')\n"
|
|
)
|
|
|
|
rc = stderr_timestamp.main(
|
|
[
|
|
"--error-log",
|
|
str(log_path),
|
|
"--",
|
|
sys.executable,
|
|
"-c",
|
|
code,
|
|
]
|
|
)
|
|
|
|
assert rc == 0
|
|
assert marker_path.read_text(encoding="utf-8") == "unset"
|