mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-12 08:51:53 +00:00
* fix(gateway): refuse to write service definitions with a temp-dir HERMES_HOME A test/E2E harness that exports HERMES_HOME=/tmp/... and touches any gateway service write path (install, start self-heal, restart's refresh_systemd_unit_if_needed) bakes the throwaway home into the production systemd unit / launchd plist. The gateway then restarts 'healthy' but pointed at an empty temp home — no platforms enabled, deaf to every message (live incident 2026-06-11: /tmp/hermes-e2e-41264 poisoned the unit during a PR-review E2E probe; the post-update restart produced a 7-hour zombie gateway). The existing safety belt only sniffed pytest-shaped markers (/pytest-of-, /hermes_test). Add a structural guard: _temp_home_in_service_definition() extracts HERMES_HOME from the generated systemd unit or launchd plist and refuses the write (with actionable guidance) when it resolves under tempfile.gettempdir(), /tmp, /var/tmp, or the macOS /private variants. Wired into all five write sites: systemd refresh + install, launchd refresh + install + start self-heal. * test: patch unit generator in install tests tripped by temp-home guard CI runs hermetic with HERMES_HOME under a tmp dir, so the real generate_systemd_unit() output now (correctly) trips the new temp-home write guard in three install tests. Patch the generator with synthetic non-temp content — same pattern the existing pytest-marker guard tests use.
134 lines
5.6 KiB
Python
134 lines
5.6 KiB
Python
"""Tests for gateway linger auto-enable behavior on headless Linux installs."""
|
|
|
|
from types import SimpleNamespace
|
|
|
|
import hermes_cli.gateway as gateway
|
|
|
|
|
|
class TestEnsureLingerEnabled:
|
|
def test_linger_already_enabled_via_file(self, monkeypatch, capsys):
|
|
monkeypatch.setattr(gateway, "is_linux", lambda: True)
|
|
monkeypatch.setattr(gateway, "is_termux", lambda: False)
|
|
monkeypatch.setattr("getpass.getuser", lambda: "testuser")
|
|
monkeypatch.setattr(gateway, "Path", lambda _path: SimpleNamespace(exists=lambda: True))
|
|
|
|
calls = []
|
|
monkeypatch.setattr(gateway.subprocess, "run", lambda *args, **kwargs: calls.append((args, kwargs)))
|
|
|
|
gateway._ensure_linger_enabled()
|
|
|
|
out = capsys.readouterr().out
|
|
assert "Systemd linger is enabled" in out
|
|
assert calls == []
|
|
|
|
def test_status_enabled_skips_enable(self, monkeypatch, capsys):
|
|
monkeypatch.setattr(gateway, "is_linux", lambda: True)
|
|
monkeypatch.setattr(gateway, "is_termux", lambda: False)
|
|
monkeypatch.setattr("getpass.getuser", lambda: "testuser")
|
|
monkeypatch.setattr(gateway, "Path", lambda _path: SimpleNamespace(exists=lambda: False))
|
|
monkeypatch.setattr(gateway, "get_systemd_linger_status", lambda: (True, ""))
|
|
|
|
calls = []
|
|
monkeypatch.setattr(gateway.subprocess, "run", lambda *args, **kwargs: calls.append((args, kwargs)))
|
|
|
|
gateway._ensure_linger_enabled()
|
|
|
|
out = capsys.readouterr().out
|
|
assert "Systemd linger is enabled" in out
|
|
assert calls == []
|
|
|
|
def test_loginctl_success_enables_linger(self, monkeypatch, capsys):
|
|
monkeypatch.setattr(gateway, "is_linux", lambda: True)
|
|
monkeypatch.setattr(gateway, "is_termux", lambda: False)
|
|
monkeypatch.setattr("getpass.getuser", lambda: "testuser")
|
|
monkeypatch.setattr(gateway, "Path", lambda _path: SimpleNamespace(exists=lambda: False))
|
|
monkeypatch.setattr(gateway, "get_systemd_linger_status", lambda: (False, ""))
|
|
monkeypatch.setattr("shutil.which", lambda name: "/usr/bin/loginctl")
|
|
|
|
run_calls = []
|
|
|
|
def fake_run(cmd, capture_output=False, text=False, check=False, **kwargs):
|
|
run_calls.append((cmd, capture_output, text, check))
|
|
return SimpleNamespace(returncode=0, stdout="", stderr="")
|
|
|
|
monkeypatch.setattr(gateway.subprocess, "run", fake_run)
|
|
|
|
gateway._ensure_linger_enabled()
|
|
|
|
out = capsys.readouterr().out
|
|
assert "Enabling linger" in out
|
|
assert "Linger enabled" in out
|
|
assert run_calls == [(["loginctl", "enable-linger", "testuser"], True, True, False)]
|
|
|
|
def test_missing_loginctl_shows_manual_guidance(self, monkeypatch, capsys):
|
|
monkeypatch.setattr(gateway, "is_linux", lambda: True)
|
|
monkeypatch.setattr(gateway, "is_termux", lambda: False)
|
|
monkeypatch.setattr("getpass.getuser", lambda: "testuser")
|
|
monkeypatch.setattr(gateway, "Path", lambda _path: SimpleNamespace(exists=lambda: False))
|
|
monkeypatch.setattr(gateway, "get_systemd_linger_status", lambda: (None, "loginctl not found"))
|
|
monkeypatch.setattr("shutil.which", lambda name: None)
|
|
|
|
calls = []
|
|
monkeypatch.setattr(gateway.subprocess, "run", lambda *args, **kwargs: calls.append((args, kwargs)))
|
|
|
|
gateway._ensure_linger_enabled()
|
|
|
|
out = capsys.readouterr().out
|
|
assert "sudo loginctl enable-linger testuser" in out
|
|
assert "loginctl not found" in out
|
|
assert calls == []
|
|
|
|
def test_loginctl_failure_shows_manual_guidance(self, monkeypatch, capsys):
|
|
monkeypatch.setattr(gateway, "is_linux", lambda: True)
|
|
monkeypatch.setattr(gateway, "is_termux", lambda: False)
|
|
monkeypatch.setattr("getpass.getuser", lambda: "testuser")
|
|
monkeypatch.setattr(gateway, "Path", lambda _path: SimpleNamespace(exists=lambda: False))
|
|
monkeypatch.setattr(gateway, "get_systemd_linger_status", lambda: (False, ""))
|
|
monkeypatch.setattr("shutil.which", lambda name: "/usr/bin/loginctl")
|
|
monkeypatch.setattr(
|
|
gateway.subprocess,
|
|
"run",
|
|
lambda *args, **kwargs: SimpleNamespace(returncode=1, stdout="", stderr="Permission denied"),
|
|
)
|
|
|
|
gateway._ensure_linger_enabled()
|
|
|
|
out = capsys.readouterr().out
|
|
assert "sudo loginctl enable-linger testuser" in out
|
|
assert "Permission denied" in out
|
|
|
|
|
|
def test_systemd_install_calls_linger_helper(monkeypatch, tmp_path, capsys):
|
|
unit_path = tmp_path / "systemd" / "user" / "hermes-gateway.service"
|
|
|
|
monkeypatch.setattr(gateway, "get_systemd_unit_path", lambda system=False: unit_path)
|
|
# Non-temp home so the temp-home write guard (which trips on the
|
|
# hermetic test HERMES_HOME) stays out of the way.
|
|
monkeypatch.setattr(
|
|
gateway,
|
|
"generate_systemd_unit",
|
|
lambda system=False, run_as_user=None: (
|
|
'[Service]\nEnvironment="HERMES_HOME=/home/alice/.hermes"\n'
|
|
),
|
|
)
|
|
|
|
calls = []
|
|
|
|
def fake_run(cmd, check=False, **kwargs):
|
|
calls.append((cmd, check))
|
|
return SimpleNamespace(returncode=0, stdout="", stderr="")
|
|
|
|
helper_calls = []
|
|
monkeypatch.setattr(gateway.subprocess, "run", fake_run)
|
|
monkeypatch.setattr(gateway, "_ensure_linger_enabled", lambda: helper_calls.append(True))
|
|
|
|
gateway.systemd_install(force=False)
|
|
|
|
out = capsys.readouterr().out
|
|
assert unit_path.exists()
|
|
assert [cmd for cmd, _ in calls] == [
|
|
["systemctl", "--user", "daemon-reload"],
|
|
["systemctl", "--user", "enable", gateway.get_service_name()],
|
|
]
|
|
assert helper_calls == [True]
|
|
assert "User service installed and enabled" in out
|