Merge branch 'master' into feat/issue-651-usage-cost-analytics
This commit is contained in:
@@ -0,0 +1,345 @@
|
||||
"""Tests for the system-health dashboard view (#639).
|
||||
|
||||
Covers the acceptance criteria directly: the page renders the health DTO
|
||||
fields (AC1), degraded dependencies are visible (AC2), stale runtime is warned
|
||||
prominently and never rendered as mutation-safe (AC3), healthy and degraded
|
||||
fixtures both render (AC4), and the shell carries a nav entry (AC5).
|
||||
"""
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
from webui.app import create_app
|
||||
from webui.deployment_boundary import scan_text_for_client_secrets
|
||||
from webui.layout import render_page
|
||||
from webui.nav import iter_nav_items
|
||||
from webui.system_health import (
|
||||
STATUS_DEGRADED,
|
||||
STATUS_DOWN,
|
||||
STATUS_OK,
|
||||
STATUS_SKIPPED,
|
||||
STATUS_UNPROVEN,
|
||||
DependencyProbe,
|
||||
StaleRuntime,
|
||||
SystemHealthSnapshot,
|
||||
VersionInfo,
|
||||
)
|
||||
from webui.system_health_views import render_system_health_page
|
||||
|
||||
DASHBOARD_PATH = "/system-health"
|
||||
|
||||
|
||||
def _version(*, known: bool = True) -> VersionInfo:
|
||||
return VersionInfo(
|
||||
git_sha="1c455b6ec0f9cb761fe6248de68c17e061fb5ecd" if known else None,
|
||||
git_describe="v0.4.1-12-g1c455b6" if known else None,
|
||||
control_plane_schema_version=4 if known else None,
|
||||
python_version="3.13.1",
|
||||
known=known,
|
||||
)
|
||||
|
||||
|
||||
def _parity(*, stale: bool = False, determinable: bool = True) -> StaleRuntime:
|
||||
if stale:
|
||||
return StaleRuntime(
|
||||
daemon_head="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
checkout_head="bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
||||
remote_head="cccccccccccccccccccccccccccccccccccccccc",
|
||||
stale=True,
|
||||
determinable=True,
|
||||
mutation_safe=False,
|
||||
reasons=("runtime, checkout, and remote commits disagree",),
|
||||
)
|
||||
if not determinable:
|
||||
return StaleRuntime(
|
||||
daemon_head=None,
|
||||
checkout_head=None,
|
||||
remote_head=None,
|
||||
stale=False,
|
||||
determinable=False,
|
||||
mutation_safe=False,
|
||||
reasons=("local checkout HEAD could not be read",),
|
||||
)
|
||||
return StaleRuntime(
|
||||
daemon_head="1c455b6ec0f9cb761fe6248de68c17e061fb5ecd",
|
||||
checkout_head="1c455b6ec0f9cb761fe6248de68c17e061fb5ecd",
|
||||
remote_head="1c455b6ec0f9cb761fe6248de68c17e061fb5ecd",
|
||||
stale=False,
|
||||
determinable=True,
|
||||
mutation_safe=True,
|
||||
reasons=(),
|
||||
)
|
||||
|
||||
|
||||
def _snapshot(
|
||||
*,
|
||||
status: str = STATUS_OK,
|
||||
ready: bool = True,
|
||||
readiness_complete: bool = True,
|
||||
readiness_reasons: tuple[str, ...] = (),
|
||||
dependencies: tuple[DependencyProbe, ...] | None = None,
|
||||
parity: StaleRuntime | None = None,
|
||||
namespaces: tuple[dict, ...] = (),
|
||||
probe_errors: tuple[str, ...] = (),
|
||||
version_known: bool = True,
|
||||
) -> SystemHealthSnapshot:
|
||||
if dependencies is None:
|
||||
dependencies = (
|
||||
DependencyProbe(
|
||||
name="control_plane_db",
|
||||
kind="sqlite",
|
||||
status=STATUS_OK,
|
||||
detail="schema version 4",
|
||||
required=True,
|
||||
latency_ms=1.25,
|
||||
metadata={"schema_version": 4},
|
||||
),
|
||||
)
|
||||
return SystemHealthSnapshot(
|
||||
status=status,
|
||||
ready=ready,
|
||||
readiness_complete=readiness_complete,
|
||||
readiness_reasons=readiness_reasons,
|
||||
service="mcp-control-plane-webui",
|
||||
mode="read-only",
|
||||
version=_version(known=version_known),
|
||||
started_at="2026-07-23T19:50:47+00:00",
|
||||
uptime_seconds=3661.5,
|
||||
timestamp="2026-07-23T20:51:48+00:00",
|
||||
deep_probes_requested=False,
|
||||
dependencies=dependencies,
|
||||
mcp_namespaces=namespaces,
|
||||
stale_runtime=parity if parity is not None else _parity(),
|
||||
probe_errors=probe_errors,
|
||||
)
|
||||
|
||||
|
||||
class TestHealthyRender(unittest.TestCase):
|
||||
"""AC1 / AC4 — every health DTO field reaches the page."""
|
||||
|
||||
def setUp(self):
|
||||
self.html = render_system_health_page(_snapshot())
|
||||
|
||||
def test_readiness_fields_render(self):
|
||||
self.assertIn("System health", self.html)
|
||||
self.assertIn("Ready", self.html)
|
||||
self.assertIn("mcp-control-plane-webui", self.html)
|
||||
self.assertIn("read-only", self.html)
|
||||
self.assertIn("2026-07-23T20:51:48+00:00", self.html)
|
||||
|
||||
def test_version_and_uptime_render(self):
|
||||
self.assertIn("1c455b6ec0f9cb761fe6248de68c17e061fb5ecd", self.html)
|
||||
self.assertIn("v0.4.1-12-g1c455b6", self.html)
|
||||
self.assertIn("3.13.1", self.html)
|
||||
self.assertIn("3661.500s", self.html)
|
||||
self.assertIn("1.02h", self.html)
|
||||
|
||||
def test_dependency_row_renders_with_latency(self):
|
||||
self.assertIn("control_plane_db", self.html)
|
||||
self.assertIn("sqlite", self.html)
|
||||
self.assertIn("schema version 4", self.html)
|
||||
self.assertIn("1.2 ms", self.html)
|
||||
|
||||
def test_healthy_page_shows_no_stale_warning(self):
|
||||
self.assertNotIn("Stale runtime:", self.html)
|
||||
self.assertNotIn("Staleness", self.html)
|
||||
|
||||
def test_unknown_version_is_labelled_not_faked(self):
|
||||
html = render_system_health_page(_snapshot(version_known=False))
|
||||
self.assertIn("unknown", html)
|
||||
self.assertIn("unresolved", html)
|
||||
|
||||
|
||||
class TestDegradedRender(unittest.TestCase):
|
||||
"""AC2 — a degraded or unrun dependency is visible, not swallowed."""
|
||||
|
||||
def setUp(self):
|
||||
self.deps = (
|
||||
DependencyProbe(
|
||||
name="control_plane_db",
|
||||
kind="sqlite",
|
||||
status=STATUS_OK,
|
||||
detail="schema version 4",
|
||||
required=True,
|
||||
latency_ms=0.9,
|
||||
),
|
||||
DependencyProbe(
|
||||
name="repository",
|
||||
kind="git",
|
||||
status=STATUS_DOWN,
|
||||
detail="repository root is not a git checkout",
|
||||
required=True,
|
||||
latency_ms=4.0,
|
||||
),
|
||||
DependencyProbe(
|
||||
name="gitea",
|
||||
kind="http",
|
||||
status=STATUS_SKIPPED,
|
||||
detail="deep probe not requested",
|
||||
required=False,
|
||||
),
|
||||
)
|
||||
self.html = render_system_health_page(
|
||||
_snapshot(
|
||||
status=STATUS_DEGRADED,
|
||||
ready=False,
|
||||
readiness_complete=False,
|
||||
readiness_reasons=("required dependency 'repository' is down",),
|
||||
dependencies=self.deps,
|
||||
)
|
||||
)
|
||||
|
||||
def test_degraded_banner_names_the_dependency(self):
|
||||
self.assertIn("Degraded dependencies:", self.html)
|
||||
self.assertIn("repository", self.html)
|
||||
|
||||
def test_not_run_probe_is_reported_separately(self):
|
||||
self.assertIn("Not probed:", self.html)
|
||||
self.assertIn("gitea", self.html)
|
||||
self.assertIn("not counted", self.html)
|
||||
|
||||
def test_not_ready_headline_and_reason(self):
|
||||
self.assertIn("Not ready", self.html)
|
||||
self.assertIn("required dependency 'repository' is down", self.html)
|
||||
|
||||
def test_degraded_status_badge_present(self):
|
||||
self.assertIn("badge-health-degraded", self.html)
|
||||
self.assertIn("badge-health-down", self.html)
|
||||
|
||||
def test_ready_but_incomplete_is_not_shown_as_plain_ready(self):
|
||||
html = render_system_health_page(
|
||||
_snapshot(ready=True, readiness_complete=False)
|
||||
)
|
||||
self.assertIn("Ready (incomplete evidence)", html)
|
||||
|
||||
|
||||
class TestStaleRuntimeWarning(unittest.TestCase):
|
||||
"""AC3 — staleness is prominent and never claims mutation safety."""
|
||||
|
||||
def test_stale_runtime_warns_and_denies_mutation_safety(self):
|
||||
html = render_system_health_page(_snapshot(parity=_parity(stale=True)))
|
||||
self.assertIn("Stale runtime:", html)
|
||||
self.assertIn("do not treat this runtime as mutation-safe", html)
|
||||
self.assertIn("<tr><th>Mutation safe</th><td>False</td></tr>", html)
|
||||
|
||||
def test_indeterminate_parity_is_not_reported_safe(self):
|
||||
html = render_system_health_page(
|
||||
_snapshot(parity=_parity(determinable=False))
|
||||
)
|
||||
self.assertIn("Staleness", html)
|
||||
self.assertIn("<tr><th>Mutation safe</th><td>False</td></tr>", html)
|
||||
self.assertIn("<tr><th>Determinable</th><td>False</td></tr>", html)
|
||||
|
||||
def test_healthy_parity_reports_mutation_safe_true(self):
|
||||
html = render_system_health_page(_snapshot())
|
||||
self.assertIn("<tr><th>Mutation safe</th><td>True</td></tr>", html)
|
||||
|
||||
|
||||
class TestNamespacesAndErrors(unittest.TestCase):
|
||||
def test_unproven_namespace_rows_render(self):
|
||||
html = render_system_health_page(
|
||||
_snapshot(
|
||||
namespaces=(
|
||||
{
|
||||
"namespace": "gitea-author",
|
||||
"required_tool": "gitea_lock_issue",
|
||||
"status": STATUS_UNPROVEN,
|
||||
"ide_namespace_proven": False,
|
||||
"reason": "the web console cannot invoke the IDE-managed MCP client",
|
||||
},
|
||||
)
|
||||
)
|
||||
)
|
||||
self.assertIn("gitea-author", html)
|
||||
self.assertIn("gitea_lock_issue", html)
|
||||
self.assertIn("badge-health-unproven", html)
|
||||
|
||||
def test_no_namespaces_degrades_gracefully(self):
|
||||
html = render_system_health_page(_snapshot(namespaces=()))
|
||||
self.assertIn("No MCP namespaces are declared.", html)
|
||||
|
||||
def test_probe_errors_render_when_present(self):
|
||||
html = render_system_health_page(
|
||||
_snapshot(probe_errors=("probe raised: disk offline",))
|
||||
)
|
||||
self.assertIn("Probe errors", html)
|
||||
self.assertIn("disk offline", html)
|
||||
|
||||
def test_probe_error_card_absent_when_clean(self):
|
||||
self.assertNotIn("Probe errors", render_system_health_page(_snapshot()))
|
||||
|
||||
|
||||
class TestReadOnlyAndRedaction(unittest.TestCase):
|
||||
def test_no_restart_or_kill_controls(self):
|
||||
html = render_system_health_page(_snapshot())
|
||||
self.assertNotIn("<button", html)
|
||||
self.assertNotIn("<form", html)
|
||||
self.assertNotIn("pkill", html)
|
||||
self.assertIn("read-only", html)
|
||||
|
||||
def test_recovery_points_at_sanctioned_path(self):
|
||||
html = render_system_health_page(_snapshot())
|
||||
self.assertIn("Reconnect the MCP client", html)
|
||||
self.assertIn("Never kill the daemon process manually", html)
|
||||
|
||||
def test_secret_shaped_detail_is_redacted(self):
|
||||
leaky = DependencyProbe(
|
||||
name="gitea",
|
||||
kind="http",
|
||||
status=STATUS_DOWN,
|
||||
detail="auth failed for token=ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789",
|
||||
required=False,
|
||||
latency_ms=12.0,
|
||||
)
|
||||
html = render_system_health_page(_snapshot(dependencies=(leaky,)))
|
||||
self.assertNotIn("ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", html)
|
||||
|
||||
def test_html_in_detail_is_escaped(self):
|
||||
hostile = DependencyProbe(
|
||||
name="repository",
|
||||
kind="git",
|
||||
status=STATUS_DOWN,
|
||||
detail="<script>alert(1)</script>",
|
||||
required=True,
|
||||
)
|
||||
html = render_system_health_page(_snapshot(dependencies=(hostile,)))
|
||||
self.assertNotIn("<script>", html)
|
||||
self.assertIn("<script>", html)
|
||||
|
||||
|
||||
class TestNavAndRoute(unittest.TestCase):
|
||||
"""AC5 — the shell links the dashboard, and the route serves it."""
|
||||
|
||||
def setUp(self):
|
||||
self.client = TestClient(create_app())
|
||||
|
||||
def test_nav_contains_system_health(self):
|
||||
self.assertIn(
|
||||
(DASHBOARD_PATH, "System health"),
|
||||
[(item.href, item.label) for item in iter_nav_items()],
|
||||
)
|
||||
|
||||
def test_rendered_shell_links_dashboard(self):
|
||||
page = render_page(title="Home", body_html="<p>x</p>")
|
||||
self.assertIn(f'href="{DASHBOARD_PATH}"', page)
|
||||
|
||||
def test_route_renders_dashboard(self):
|
||||
response = self.client.get(DASHBOARD_PATH)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIn("System health", response.text)
|
||||
self.assertIn("Stale-runtime parity", response.text)
|
||||
|
||||
def test_route_is_read_only(self):
|
||||
self.assertEqual(self.client.post(DASHBOARD_PATH).status_code, 405)
|
||||
|
||||
def test_live_page_leaks_no_client_secret(self):
|
||||
findings = scan_text_for_client_secrets(self.client.get(DASHBOARD_PATH).text)
|
||||
self.assertEqual(findings, [])
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user