Files
hermes-webui/tests/test_issue693_system_health_panel.py
nesquena-hermes f9acf464e9 ci: run test suite in 3 parallel shards + make suite shard-safe
Completes the test-sharding half of #3197 (Docker-cache half shipped v0.51.177).
Adds pytest-shard 3-way split to tests.yml (3 shards x 3 Python = 9 jobs,
fail-fast: false). pytest-shard is 0-indexed so the matrix uses [0,1,2] — the
original #3197 used [1,2,3] which would have crashed the out-of-range job and
silently skipped shard 0's tests.

Made the suite shard-safe by fixing 4 cross-test state-pollution bugs that
passed sequentially but failed when sharded:
- test_onboarding_mvp: reset onboarding_completed flag (settings.json) in the
  autouse fixture; the config-cleanup only cleared config.yaml/.env.
- test_issue693_system_health_panel: invalidate the process-wide password-hash
  cache before/after so a prior test's "no password" cache doesn't defeat the
  auth-gate assertion.
- test_auth_session_persistence: assert against auth._SESSIONS_FILE (where auth
  actually writes) instead of a local _TEST_STATE path that only matched under a
  lucky import order.
- test_profile_env_isolation (root cause of the worst leak): stop deleting +
  re-importing api.profiles under a temp HERMES_BASE_HOME — that swapped the
  module object and poisoned the cached _DEFAULT_HERMES_HOME for every later
  test (broke test_title_aux_routing's load_config). Now points the cached path
  via monkeypatch.setattr (auto-restored, no module swap).
- conftest: autouse fixture restores HERMES_HOME/HERMES_BASE_HOME after each
  test as defense-in-depth against future switch_profile leaks.

Verified: all 3 shards green (6912 passed, 0 failed); full sequential run still
green (6957 passed, 0 failed). Slowest shard ~70s vs ~180s sequential.
2026-05-30 21:09:25 +00:00

196 lines
7.8 KiB
Python

"""Regression coverage for #693 live VPS host resource health panel."""
from __future__ import annotations
import json
import pathlib
from types import SimpleNamespace
from urllib.parse import urlparse
REPO_ROOT = pathlib.Path(__file__).parent.parent
UI_JS = (REPO_ROOT / "static" / "ui.js").read_text(encoding="utf-8")
PANELS_JS = (REPO_ROOT / "static" / "panels.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")
AUTH_PY = (REPO_ROOT / "api" / "auth.py").read_text(encoding="utf-8")
class _FakeHandler:
def __init__(self):
self.status = None
self.sent_headers = []
self.body = bytearray()
self.wfile = self
self.headers = {}
def send_response(self, status):
self.status = status
def send_header(self, name, value):
self.sent_headers.append((name, value))
def end_headers(self):
pass
def write(self, data):
self.body.extend(data)
def json_body(self):
return json.loads(bytes(self.body).decode("utf-8"))
def test_system_health_payload_normalizes_safe_aggregate_metrics(monkeypatch):
from api import system_health
monkeypatch.setattr(system_health, "_cpu_percent", lambda: 17.345)
monkeypatch.setattr(
system_health,
"_memory_usage",
lambda: {"used_bytes": 4_000, "total_bytes": 10_000, "percent": 40.0},
)
monkeypatch.setattr(
system_health,
"_disk_usage",
lambda: {"used_bytes": 55_500, "total_bytes": 100_000, "percent": 55.5},
)
payload = system_health.build_system_health_payload()
assert payload["status"] == "ok"
assert payload["available"] is True
assert payload["cpu"] == {"percent": 17.3}
assert payload["memory"] == {"used_bytes": 4000, "total_bytes": 10000, "percent": 40.0}
assert payload["disk"] == {"used_bytes": 55500, "total_bytes": 100000, "percent": 55.5}
assert payload["checked_at"]
rendered = repr(payload)
for private_fragment in ("/home/", "/Users/", "mount", "path", "argv", "command", "env", "token"):
assert private_fragment not in rendered
def test_system_health_payload_partial_and_unavailable_are_graceful(monkeypatch):
from api import system_health
def boom():
raise RuntimeError("private /home/user/path should not leak")
monkeypatch.setattr(system_health, "_cpu_percent", boom)
monkeypatch.setattr(system_health, "_memory_usage", boom)
monkeypatch.setattr(
system_health,
"_disk_usage",
lambda: {"used_bytes": 1, "total_bytes": 4, "percent": 25.0},
)
partial = system_health.build_system_health_payload()
assert partial["status"] == "partial"
assert partial["available"] is True
assert partial["disk"]["percent"] == 25.0
assert partial["cpu"] is None
assert partial["memory"] is None
assert {e["metric"] for e in partial["errors"]} == {"cpu", "memory"}
assert "/home/user" not in repr(partial)
monkeypatch.setattr(system_health, "_disk_usage", boom)
unavailable = system_health.build_system_health_payload()
assert unavailable["status"] == "unavailable"
assert unavailable["available"] is False
assert unavailable["cpu"] is None
assert unavailable["memory"] is None
assert unavailable["disk"] is None
assert "/home/user" not in repr(unavailable)
def test_system_health_route_registered_and_auth_gated(monkeypatch):
assert 'parsed.path == "/api/system/health"' in ROUTES_PY
assert "build_system_health_payload()" in ROUTES_PY
assert '"/api/system/health"' not in AUTH_PY, "system metrics must not be public"
monkeypatch.setenv("HERMES_WEBUI_PASSWORD", "test-password")
from api import auth as _auth
from api.auth import check_auth
# The password hash is cached process-wide (PBKDF2 is ~1s). A prior test may
# have populated the cache with "no password" (None), so the env var we just
# set would be ignored on the fast path. Invalidate before AND after so this
# test sees its own password and doesn't leak the test-password cache to the
# next test — required for order-independence under sharded/random runs.
_auth._invalidate_password_hash_cache()
handler = _FakeHandler()
try:
assert check_auth(handler, SimpleNamespace(path="/api/system/health", query="")) is False
assert handler.status in (302, 401)
finally:
monkeypatch.delenv("HERMES_WEBUI_PASSWORD", raising=False)
_auth._invalidate_password_hash_cache()
def test_system_health_route_returns_only_sanitized_payload(monkeypatch):
from api import routes
monkeypatch.setattr(
routes,
"build_system_health_payload",
lambda: {
"status": "ok",
"available": True,
"checked_at": "2026-05-05T00:00:00+00:00",
"cpu": {"percent": 12.0},
"memory": {"used_bytes": 1, "total_bytes": 2, "percent": 50.0},
"disk": {"used_bytes": 3, "total_bytes": 4, "percent": 75.0},
"errors": [],
},
)
handler = _FakeHandler()
assert routes.handle_get(handler, urlparse("http://example.test/api/system/health")) is True
payload = handler.json_body()
assert payload["cpu"]["percent"] == 12.0
assert set(payload) == {"status", "available", "checked_at", "cpu", "memory", "disk", "errors"}
def test_system_health_panel_markup_and_styles_live_under_insights_not_top_chrome():
top_shell = INDEX_HTML[: INDEX_HTML.index('<div class="layout">')]
assert 'id="systemHealthPanel"' not in top_shell
assert 'aria-label="Host resource health"' not in top_shell
assert 'function _renderSystemHealthPanel()' in PANELS_JS
assert 'id="systemHealthPanel"' in PANELS_JS
assert 'aria-label="Host resource health"' in PANELS_JS
assert 'System health' in PANELS_JS
assert 'Current VPS resource usage' in PANELS_JS
assert PANELS_JS.index('_renderSystemHealthPanel()') < PANELS_JS.index('_renderLlmWikiStatus(wikiStatus)')
assert 'data-system-health-metric="cpu"' in PANELS_JS
assert 'data-system-health-metric="memory"' in PANELS_JS
assert 'data-system-health-metric="disk"' in PANELS_JS
assert ".system-health-panel.insights-card" in STYLE_CSS
assert ".system-health-bar-fill" in STYLE_CSS
assert ".system-health-panel.unavailable" in STYLE_CSS
assert "@media(max-width:640px)" in STYLE_CSS and ".system-health-panel.insights-card" in STYLE_CSS
def test_system_health_frontend_polls_visible_and_renders_progress_labels():
assert "const SYSTEM_HEALTH_INTERVAL_MS=5000" in UI_JS
assert "api('/api/system/health',{timeoutToast:false})" in UI_JS
assert "document.visibilityState !== 'visible'" in UI_JS
assert "document.querySelector('main.main.showing-insights')" in UI_JS
assert "document.addEventListener('visibilitychange',_syncSystemHealthMonitorVisibility)" in UI_JS
assert "typeof _syncSystemHealthMonitorVisibility === 'function'" in PANELS_JS
assert "function renderSystemHealth(payload)" in UI_JS
assert "setSystemHealthUnavailable" in UI_JS
assert "data-system-health-metric" in PANELS_JS
assert "CPU" in PANELS_JS and "RAM" in PANELS_JS and "Disk" in PANELS_JS
assert "aria-valuenow" in UI_JS
assert "style.width=`${percent}%`" in UI_JS
def test_system_health_backend_uses_no_shell_or_private_process_sources():
src = (REPO_ROOT / "api" / "system_health.py").read_text(encoding="utf-8")
assert "import subprocess" not in src
assert "import psutil" not in src
assert "os.environ" not in src
assert "ps aux" not in src
assert "/proc/self/environ" not in src
for private_field in ("argv", "cmdline", "username", "mountpoint"):
assert private_field not in src