Files
hermes-webui/tests/test_issue716_agent_heartbeat.py
T
2026-05-08 16:07:48 +00:00

213 lines
7.7 KiB
Python

"""Regression coverage for #716 Hermes agent/gateway heartbeat monitor."""
from __future__ import annotations
import json
import pathlib
import sys
import types
REPO_ROOT = pathlib.Path(__file__).parent.parent
UI_JS = (REPO_ROOT / "static" / "ui.js").read_text(encoding="utf-8")
INDEX_HTML = (REPO_ROOT / "static" / "index.html").read_text(encoding="utf-8")
STYLE_CSS = (REPO_ROOT / "static" / "style.css").read_text(encoding="utf-8")
ROUTES_PY = (REPO_ROOT / "api" / "routes.py").read_text(encoding="utf-8")
class _FakeGatewayStatus:
def __init__(self, runtime_status, running_pid):
self._runtime_status = runtime_status
self._running_pid = running_pid
def read_runtime_status(self):
return self._runtime_status
def get_running_pid(self, cleanup_stale=False):
assert cleanup_stale is False
return self._running_pid
class _PathSensitiveGatewayStatus:
_RUNTIME_STATUS_FILE = "gateway_state.json"
def __init__(self, root_home: pathlib.Path):
self.root_home = root_home
self.runtime_pid_path = None
self.running_pid_path = None
def read_runtime_status(self, pid_path=None):
self.runtime_pid_path = pathlib.Path(pid_path) if pid_path is not None else None
if self.runtime_pid_path:
base = self.runtime_pid_path.parent
else:
base = self.root_home / "profiles" / "troubleshooting"
path = base / self._RUNTIME_STATUS_FILE
if not path.exists():
return None
return json.loads(path.read_text(encoding="utf-8"))
def get_running_pid(self, pid_path=None, cleanup_stale=False):
assert cleanup_stale is False
self.running_pid_path = pathlib.Path(pid_path) if pid_path is not None else None
if self.running_pid_path == self.root_home / "gateway.pid":
return 98765
return None
def _runtime_status(**overrides):
payload = {
"gateway_state": "running",
"updated_at": "2026-05-04T12:00:00+00:00",
"active_agents": 2,
"platforms": {
"discord": {"state": "connected"},
"telegram": {"state": "starting"},
},
# Sensitive/raw process fields that must never reach the browser.
"pid": 12345,
"argv": ["hermes", "gateway", "--token", "secret-token"],
"command": "hermes gateway --token secret-token",
"executable": "/home/user/.hermes/hermes-agent/venv/bin/python",
"env": {"API_KEY": "secret"},
}
payload.update(overrides)
return payload
def test_agent_health_uses_root_gateway_state_when_hermes_home_is_profile(monkeypatch, tmp_path):
from api import agent_health
root_home = tmp_path / "root-home"
profile_home = root_home / "profiles" / "troubleshooting"
profile_home.mkdir(parents=True)
(root_home / "gateway.pid").write_text(json.dumps({"pid": 98765}), encoding="utf-8")
(root_home / "gateway_state.json").write_text(json.dumps(_runtime_status()), encoding="utf-8")
fake_gateway_status = _PathSensitiveGatewayStatus(root_home)
monkeypatch.setenv("HERMES_HOME", str(profile_home))
monkeypatch.setitem(
sys.modules,
"hermes_constants",
types.SimpleNamespace(get_default_hermes_root=lambda: root_home),
)
monkeypatch.setattr(agent_health, "_gateway_status_module", lambda: fake_gateway_status)
payload = agent_health.build_agent_health_payload()
assert payload["alive"] is True
assert payload["details"]["state"] == "alive"
assert fake_gateway_status.runtime_pid_path == root_home / "gateway.pid"
assert fake_gateway_status.running_pid_path == root_home / "gateway.pid"
def test_agent_health_payload_alive_uses_safe_runtime_details(monkeypatch):
from api import agent_health
monkeypatch.setattr(
agent_health,
"_gateway_status_module",
lambda: _FakeGatewayStatus(_runtime_status(), running_pid=12345),
)
payload = agent_health.build_agent_health_payload()
assert payload["alive"] is True
assert payload["checked_at"]
assert payload["details"] == {
"state": "alive",
"gateway_state": "running",
"updated_at": "2026-05-04T12:00:00+00:00",
"active_agents": 2,
"platform_count": 2,
"platform_states": {"connected": 1, "starting": 1},
}
rendered = repr(payload)
assert "secret-token" not in rendered
assert "API_KEY" not in rendered
assert "argv" not in rendered
assert "command" not in rendered
assert "executable" not in rendered
assert "pid" not in payload["details"]
def test_agent_health_payload_down_when_gateway_metadata_exists_but_no_process(monkeypatch):
from api import agent_health
monkeypatch.setattr(
agent_health,
"_gateway_status_module",
lambda: _FakeGatewayStatus(_runtime_status(gateway_state="stale"), running_pid=None),
)
payload = agent_health.build_agent_health_payload()
assert payload["alive"] is False
assert payload["details"]["state"] == "down"
assert payload["details"]["reason"] == "gateway_not_running"
assert payload["details"]["gateway_state"] == "stale"
def test_agent_health_payload_unknown_when_gateway_is_not_configured(monkeypatch):
from api import agent_health
monkeypatch.setattr(
agent_health,
"_gateway_status_module",
lambda: _FakeGatewayStatus(runtime_status=None, running_pid=None),
)
payload = agent_health.build_agent_health_payload()
assert payload["alive"] is None
assert payload["details"] == {"state": "unknown", "reason": "gateway_not_configured"}
def test_agent_health_route_is_registered_with_tri_state_payload_shape():
assert 'parsed.path == "/api/health/agent"' in ROUTES_PY
assert "build_agent_health_payload()" in ROUTES_PY
src = (REPO_ROOT / "api" / "agent_health.py").read_text(encoding="utf-8")
assert '"alive"' in src
assert '"checked_at"' in src
assert '"details"' in src
def test_agent_health_banner_markup_and_styles_exist():
assert 'id="agentHealthBanner"' in INDEX_HTML
assert 'role="alert"' in INDEX_HTML
assert 'aria-live="assertive"' in INDEX_HTML
assert 'onclick="dismissAgentHealthAlert()"' in INDEX_HTML
assert ".agent-health-banner" in STYLE_CSS
assert ".agent-health-banner.visible" in STYLE_CSS
assert ".agent-health-dismiss" in STYLE_CSS
def test_agent_health_frontend_polls_only_visible_and_distinguishes_states():
assert "const AGENT_HEALTH_INTERVAL_MS=30000" in UI_JS
assert "api('/api/health/agent')" in UI_JS
assert "document.visibilityState !== 'visible'" in UI_JS
assert "document.addEventListener('visibilitychange',_syncAgentHealthMonitorVisibility)" in UI_JS
assert "if(payload.alive === true)" in UI_JS
assert "if(payload.alive === false)" in UI_JS
assert "if(payload.alive == null)" in UI_JS
assert "_showAgentHealthAlert(payload)" in UI_JS
assert "_hideAgentHealthAlert()" in UI_JS
def test_agent_health_dismiss_persists_until_recovery():
assert "const AGENT_HEALTH_DISMISSED_KEY='agent-health-dismissed'" in UI_JS
assert "localStorage.setItem(AGENT_HEALTH_DISMISSED_KEY,'1')" in UI_JS
assert "localStorage.removeItem(AGENT_HEALTH_DISMISSED_KEY)" in UI_JS
assert "function dismissAgentHealthAlert()" in UI_JS
assert "if(_agentHealthDismissed()) return;" in UI_JS
assert "_setAgentHealthDismissed(false)" in UI_JS
def test_agent_health_backend_does_not_use_shell_or_expose_raw_process_fields():
src = (REPO_ROOT / "api" / "agent_health.py").read_text(encoding="utf-8")
assert "import subprocess" not in src
assert "import psutil" not in src
for private_field in ("argv", "command", "executable", "env"):
assert f'details["{private_field}"]' not in src
assert f"details['{private_field}']" not in src