"""Tests for the Stage F card layout: four info cards, no status header. The dashboard used to open with six status-header tiles *and* four compact cards that restated them (runner state lived in three places at once). Stage F keeps the cards and deletes the tiles: each card is now the single owner of its subject, enriched with what only the tiles used to show (CPU%/RAM/disk with their gauges) plus what neither had (hostname, kernel, CPU model, and the ``vmware-host.service`` row). Health is still never color-only: every gauge is paired with a text level (``ok``/``elevated``/``critical``/``unknown``) from :func:`_resource_levels`. """ from __future__ import annotations import shutil from collections import namedtuple from pathlib import Path from typing import Any import pytest from fastapi.testclient import TestClient import ci_orchestrator.web.app as app_module from ci_orchestrator.config import BackendConfig, CargoCacheConfig, Config, Paths from ci_orchestrator.operations.cache import CacheStatus from ci_orchestrator.operations.host import DiskStatus, HostResources, VmwareModuleHealth from ci_orchestrator.web.app import create_app, get_backend _DiskUsage = namedtuple("_DiskUsage", ["total", "used", "free"]) _RESOURCES = HostResources( cpu_pct=42.0, ncpu=16, mem_total_gb=62.5, mem_used_gb=20.0, uptime_sec=93784, load1=0.5, load5=0.75, load15=1.0, cpu_model="Intel(R) Core(TM) i9-13900K CPU @ 3.00GHz", kernel="7.0.0-28-generic", hostname="ci-host", ) _VMWARE_OK = VmwareModuleHealth( vmmon_loaded=True, vmnet_loaded=True, host_service_active=True, host_service_sub_state="running", ) _CACHE = CacheStatus( root=Path("/var/lib/ci/cache/cargo"), total_bytes=1024, entry_count=7, oldest_age_days=4, retention_days=30, ) def _make_config(root: Path) -> Config: return Config( paths=Paths( root=root, templates=root / "templates", build_vms=root / "build-vms", artifacts=root / "artifacts", keys=root / "keys", ), backend=BackendConfig(), cargo_cache=CargoCacheConfig( enabled=True, root=root / "cache" / "cargo", retention_days=30 ), ) class _FakeBackend: def list_running(self) -> list[Any]: return [] def get_ip(self, handle: Any, timeout: float = 0.0) -> str | None: return None def _client(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> TestClient: gb = 1024**3 monkeypatch.setattr( shutil, "disk_usage", lambda _p: _DiskUsage(total=100 * gb, used=40 * gb, free=60 * gb) ) monkeypatch.setattr(shutil, "which", lambda _name: None) monkeypatch.setattr(app_module, "host_resources", lambda: _RESOURCES) monkeypatch.setattr(app_module, "vmware_module_health", lambda: _VMWARE_OK) app = create_app(_make_config(tmp_path)) app.dependency_overrides[get_backend] = lambda: _FakeBackend() return TestClient(app) # ── _resource_levels: thresholds behind the card gauges ────────────────── _DISK_OK = DiskStatus(path=Path("/"), total_gb=100.0, free_gb=60.0, used_pct=40.0) _DISK_ELEVATED = DiskStatus(path=Path("/"), total_gb=100.0, free_gb=25.0, used_pct=75.0) _DISK_CRITICAL = DiskStatus(path=Path("/"), total_gb=100.0, free_gb=5.0, used_pct=90.0) def test_resource_levels_without_a_snapshot_is_unknown_not_false_green() -> None: levels = app_module._resource_levels(resources=None, disk=None) assert levels.cpu_class == "tile-unknown" assert levels.cpu_label == "unknown" assert levels.mem_class == "tile-unknown" assert levels.disk_class == "tile-unknown" assert levels.mem_pct is None def test_resource_levels_healthy_snapshot_is_ok() -> None: levels = app_module._resource_levels(resources=_RESOURCES, disk=_DISK_OK) assert (levels.cpu_class, levels.cpu_label) == ("tile-ok", "ok") assert (levels.mem_class, levels.mem_label) == ("tile-ok", "ok") assert (levels.disk_class, levels.disk_label) == ("tile-ok", "ok") assert levels.mem_pct == pytest.approx(32.0) def test_resource_levels_disk_above_elevated_is_warn() -> None: levels = app_module._resource_levels(resources=_RESOURCES, disk=_DISK_ELEVATED) assert (levels.disk_class, levels.disk_label) == ("tile-warn", "elevated") def test_resource_levels_disk_above_critical_is_bad() -> None: levels = app_module._resource_levels(resources=_RESOURCES, disk=_DISK_CRITICAL) assert (levels.disk_class, levels.disk_label) == ("tile-bad", "critical") def test_resource_levels_busy_cpu_and_full_memory_are_bad() -> None: busy = HostResources( cpu_pct=95.0, ncpu=16, mem_total_gb=62.5, mem_used_gb=61.0, uptime_sec=1, load1=0.0, load5=0.0, load15=0.0, ) levels = app_module._resource_levels(resources=busy, disk=_DISK_OK) assert (levels.cpu_class, levels.cpu_label) == ("tile-bad", "critical") assert (levels.mem_class, levels.mem_label) == ("tile-bad", "critical") # ── the status header is gone ──────────────────────────────────────────── def test_dashboard_has_no_status_header(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: resp = _client(tmp_path, monkeypatch).get("/") assert resp.status_code == 200 assert 'id="status-header"' not in resp.text assert "stat-tile" not in resp.text @pytest.mark.parametrize("route", ["/status-header", "/platform"]) def test_retired_routes_are_gone( route: str, tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: assert _client(tmp_path, monkeypatch).get(route).status_code == 404 def test_all_four_info_cards_share_the_same_dl_grammar( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: """Host and Cargo cache were always a
; VMware and Runners used to be a bordered at a different font-size with uppercase headers -- a visibly different style for cards sitting in the same row. All four read the same shape now: no other card renders a
.""" client = _client(tmp_path, monkeypatch) for route in ("/host", "/vmware", "/runners", "/cache"): resp = client.get(route) assert resp.status_code == 200 assert "
" in resp.text assert "
" not in resp.text def test_dashboard_renders_the_four_info_cards( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: resp = _client(tmp_path, monkeypatch).get("/") for card_id in ('id="host-card"', 'id="vmware-card"', 'id="runner-card"', 'id="cache-card"'): assert card_id in resp.text # ── /host: the enriched card ───────────────────────────────────────────── def test_host_card_pairs_identity_onto_two_rows( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: """Stage G: hostname+kernel share a row, CPU model+core count share a row. Four one-datum rows made the card twice as tall as its three neighbours for four facts that never change while the page is open. """ resp = _client(tmp_path, monkeypatch).get("/host") assert resp.status_code == 200 assert "
System
" in resp.text assert "ci-host" in resp.text assert "7.0.0-28-generic" in resp.text assert "
CPU
" in resp.text assert "16 cores" in resp.text for retired in ("
Hostname
", "
Kernel
", "
Cores
"): assert retired not in resp.text def test_host_card_shortens_the_cpu_model_but_keeps_the_full_one( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: """A one-quarter-width track cannot hold the raw ``/proc/cpuinfo`` name.""" resp = _client(tmp_path, monkeypatch).get("/host") assert "Intel Core i9-13900K" in resp.text assert 'title="Intel(R) Core(TM) i9-13900K CPU @ 3.00GHz"' in resp.text def test_host_card_holds_five_definition_rows( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: """System, CPU, CPU load, Memory, Disk -- the height parity Stage G buys.""" resp = _client(tmp_path, monkeypatch).get("/host") assert resp.text.count("
") == 5 def test_host_card_shows_cpu_ram_and_disk_with_gauges( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: """The three readouts that only existed in the retired tiles.""" resp = _client(tmp_path, monkeypatch).get("/host") assert "42%" in resp.text # cpu_pct assert "20.0" in resp.text # memory used GB assert "60 GB free" in resp.text # disk assert resp.text.count("gauge-fill") == 3 def test_host_card_drops_the_visible_level_strip( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: """The check-icon + level word row is gone from the card body.""" resp = _client(tmp_path, monkeypatch).get("/host") assert "resource-level" not in resp.text def test_host_card_keeps_the_level_word_in_every_gauge_label( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: """Health is still not color-only: the level rides each gauge's accessible name, so a screen reader states it even though no strip is drawn.""" resp = _client(tmp_path, monkeypatch).get("/host") assert 'aria-label="CPU 42 percent used, ok"' in resp.text assert 'aria-label="Memory 32 percent used, ok"' in resp.text assert 'aria-label="Disk 40 percent used, ok"' in resp.text def test_host_card_moves_uptime_into_the_card_header( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: """Uptime rides the header bar, spelled out -- a bare duration there reads as nothing in particular, and out of the ``
`` it has no ``
``.""" resp = _client(tmp_path, monkeypatch).get("/host") assert "head-meta" in resp.text assert "uptime 26h 03m" in resp.text # 93784s assert "
Uptime
" not in resp.text def test_host_card_folds_the_load_average_under_the_cpu_gauge( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: """Load average belongs to the CPU readout, like "GB free" to the disk.""" resp = _client(tmp_path, monkeypatch).get("/host") assert "0.50 / 0.75 / 1.00" in resp.text assert "
Load avg
" not in resp.text def test_host_card_degrades_when_the_probe_fails( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: def _boom() -> Any: raise RuntimeError("boom") client = _client(tmp_path, monkeypatch) monkeypatch.setattr(app_module, "host_resources", _boom) resp = client.get("/host") assert resp.status_code == 200 assert "unavailable" in resp.text.lower() # ── /vmware, /runners, /cache ──────────────────────────────────────────── def test_vmware_card_lists_both_modules_and_the_service( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: resp = _client(tmp_path, monkeypatch).get("/vmware") assert resp.status_code == 200 assert "vmmon" in resp.text assert "vmnet" in resp.text assert "vmware-host.service" in resp.text def test_runners_card_lists_every_unit(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: resp = _client(tmp_path, monkeypatch).get("/runners") assert resp.status_code == 200 assert "act-runner.service" in resp.text assert "act-runner-rust.service" in resp.text def test_cache_card_shows_size_entries_and_retention( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: monkeypatch.setattr(app_module, "_fetch_cache_status", lambda _config: _CACHE) resp = _client(tmp_path, monkeypatch).get("/cache") assert resp.status_code == 200 assert "/var/lib/ci/cache/cargo" in resp.text assert "1.0 KB" in resp.text assert "7" in resp.text # entry count assert "30 day(s)" in resp.text