mirror of
https://github.com/nesquena/hermes-webui.git
synced 2026-05-25 19:20:16 +00:00
213 lines
7.7 KiB
Python
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
|