feat(web): keep the four info cards, enrich them, drop the status-header tiles
The dashboard used to open with six status-header tiles *and* four compact cards restating them (runner state lived in three places at once). The tiles are gone; each card is now the single owner of its subject, enriched with what only the tiles had (CPU%/RAM/disk with gauges) plus what neither had (hostname, kernel, full CPU model, the vmware-host.service row). All four cards share the same <dl> grammar and font scale now -- VMware and Runners were a bordered <table> at a different size/case, which read as a visibly different style sitting next to Host and Cargo cache's plain label/value lists. operations/host.py also gains delete_template()/TemplateDeleteResult, used by the template-delete route in the next commit.
This commit is contained in:
@@ -40,6 +40,19 @@ _MEMINFO = (
|
||||
_UPTIME = "123456.78 987654.32\n"
|
||||
_LOADAVG = "0.50 0.75 1.00 3/456 7890\n"
|
||||
|
||||
# Two cores' worth of /proc/cpuinfo -- the parser takes the FIRST "model name".
|
||||
_CPUINFO = (
|
||||
"processor\t: 0\n"
|
||||
"vendor_id\t: GenuineIntel\n"
|
||||
"model name\t: Intel(R) Core(TM) i9-13900K CPU @ 3.00GHz\n"
|
||||
"cache size\t: 36864 KB\n"
|
||||
"\n"
|
||||
"processor\t: 1\n"
|
||||
"model name\t: Intel(R) Core(TM) i9-13900K CPU @ 3.00GHz\n"
|
||||
)
|
||||
_OSRELEASE = "7.0.0-28-generic\n"
|
||||
_HOSTNAME = "ci-host\n"
|
||||
|
||||
|
||||
def _fake_reader(calls: dict[str, int]) -> Any:
|
||||
def _read(path: str) -> str:
|
||||
@@ -52,6 +65,12 @@ def _fake_reader(calls: dict[str, int]) -> Any:
|
||||
return _UPTIME
|
||||
if path == "/proc/loadavg":
|
||||
return _LOADAVG
|
||||
if path == "/proc/cpuinfo":
|
||||
return _CPUINFO
|
||||
if path == "/proc/sys/kernel/osrelease":
|
||||
return _OSRELEASE
|
||||
if path == "/proc/sys/kernel/hostname":
|
||||
return _HOSTNAME
|
||||
raise AssertionError(f"unexpected proc path: {path}")
|
||||
|
||||
return _read
|
||||
@@ -113,6 +132,61 @@ def test_host_resources_uptime_and_loadavg(monkeypatch: pytest.MonkeyPatch) -> N
|
||||
assert result.load15 == pytest.approx(1.00)
|
||||
|
||||
|
||||
# ── host_resources: static identity fields (CPU model, kernel, hostname) ─────
|
||||
|
||||
|
||||
def test_host_resources_reports_cpu_model_kernel_and_hostname(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
_install_fake_proc(monkeypatch)
|
||||
|
||||
result = host_resources()
|
||||
|
||||
assert result.cpu_model == "Intel(R) Core(TM) i9-13900K CPU @ 3.00GHz"
|
||||
assert result.kernel == "7.0.0-28-generic"
|
||||
assert result.hostname == "ci-host"
|
||||
|
||||
|
||||
def test_host_resources_cpu_model_is_empty_when_cpuinfo_has_no_model_name(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""Not every architecture prints ``model name`` (e.g. arm64) -- degrade, don't crash."""
|
||||
calls: dict[str, int] = {}
|
||||
inner = _fake_reader(calls)
|
||||
|
||||
def _read(path: str) -> str:
|
||||
if path == "/proc/cpuinfo":
|
||||
return "processor\t: 0\nBogoMIPS\t: 50.00\n"
|
||||
return str(inner(path))
|
||||
|
||||
monkeypatch.setattr(host_module, "_read_proc", _read)
|
||||
monkeypatch.setattr(host_module.time, "sleep", lambda _s: None)
|
||||
|
||||
assert host_resources().cpu_model == ""
|
||||
|
||||
|
||||
def test_host_resources_unreadable_static_fields_do_not_zero_live_metrics(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""A missing ``/proc/cpuinfo`` must not cost us CPU%/RAM/load as well."""
|
||||
calls: dict[str, int] = {}
|
||||
inner = _fake_reader(calls)
|
||||
|
||||
def _read(path: str) -> str:
|
||||
if path in ("/proc/cpuinfo", "/proc/sys/kernel/osrelease", "/proc/sys/kernel/hostname"):
|
||||
raise OSError("gone")
|
||||
return str(inner(path))
|
||||
|
||||
monkeypatch.setattr(host_module, "_read_proc", _read)
|
||||
monkeypatch.setattr(host_module.time, "sleep", lambda _s: None)
|
||||
|
||||
result = host_resources()
|
||||
|
||||
assert result.cpu_pct == pytest.approx(50.0)
|
||||
assert result.load1 == pytest.approx(0.50)
|
||||
assert (result.cpu_model, result.kernel, result.hostname) == ("", "", "")
|
||||
|
||||
|
||||
# ── host_resources: degrade to zeros on OSError, never raise ────────────────
|
||||
|
||||
|
||||
@@ -134,6 +208,9 @@ def test_host_resources_degrades_to_zeros_on_oserror(monkeypatch: pytest.MonkeyP
|
||||
load1=0.0,
|
||||
load5=0.0,
|
||||
load15=0.0,
|
||||
cpu_model="",
|
||||
kernel="",
|
||||
hostname="",
|
||||
)
|
||||
|
||||
|
||||
@@ -144,6 +221,8 @@ def test_host_resources_degrades_to_zeros_when_second_stat_sample_fails(
|
||||
calls = {"n": 0}
|
||||
|
||||
def _flaky(path: str) -> str:
|
||||
if path.startswith("/proc/sys/kernel/") or path == "/proc/cpuinfo":
|
||||
return "" # static identity reads happen before the live-metric block
|
||||
if path != "/proc/stat":
|
||||
raise AssertionError("should not reach meminfo after stat blows up")
|
||||
calls["n"] += 1
|
||||
|
||||
+33
-128
@@ -20,8 +20,6 @@ import ci_orchestrator.web.app as app_module
|
||||
from ci_orchestrator.backends.errors import BackendNotAvailable
|
||||
from ci_orchestrator.backends.protocol import VmHandle
|
||||
from ci_orchestrator.config import BackendConfig, CargoCacheConfig, Config, Paths
|
||||
from ci_orchestrator.operations.cache import CacheStatus
|
||||
from ci_orchestrator.operations.host import DiskStatus, RunnerStatus
|
||||
from ci_orchestrator.operations.vms import LiveVm
|
||||
from ci_orchestrator.web.app import create_app, get_backend
|
||||
|
||||
@@ -182,7 +180,7 @@ def test_get_backend_degrades_to_none_when_vmrun_missing(
|
||||
assert "unavailable" in resp.text.lower()
|
||||
|
||||
|
||||
# ── /cache ────────────────────────────────────────────────────────────────
|
||||
# ── /cache, /runners ──────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_cache_route_renders_status(tmp_path: Path) -> None:
|
||||
@@ -210,14 +208,9 @@ def test_cache_route_degrades_when_operations_raises(
|
||||
assert resp.status_code == 200
|
||||
|
||||
|
||||
# ── /host ─────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_runners_route_renders_runner_status(
|
||||
def test_runners_route_renders_every_unit(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
# Stage E: the /runners partial carries only the runner statuses -- the
|
||||
# disk card was folded into the status-header Disk tile.
|
||||
_quiet_host(monkeypatch)
|
||||
app = create_app(_make_config(tmp_path))
|
||||
client = TestClient(app)
|
||||
@@ -230,9 +223,10 @@ def test_runners_route_renders_runner_status(
|
||||
assert "act-runner-rust.service" in resp.text
|
||||
|
||||
|
||||
def test_runners_route_degrades_when_disk_status_raises(
|
||||
def test_host_route_degrades_when_disk_status_raises(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
"""Stage F put the disk gauge in the Host card, so this is its degrade path."""
|
||||
def _boom(_path: Path) -> Any:
|
||||
raise RuntimeError("boom")
|
||||
|
||||
@@ -241,9 +235,10 @@ def test_runners_route_degrades_when_disk_status_raises(
|
||||
app = create_app(_make_config(tmp_path))
|
||||
client = TestClient(app)
|
||||
|
||||
resp = client.get("/runners")
|
||||
resp = client.get("/host")
|
||||
|
||||
assert resp.status_code == 200
|
||||
assert "unknown" in resp.text
|
||||
|
||||
|
||||
def test_runners_route_degrades_when_runner_status_raises(
|
||||
@@ -274,131 +269,22 @@ def test_static_mount_serves_app_css(tmp_path: Path) -> None:
|
||||
assert resp.status_code == 200
|
||||
|
||||
|
||||
# ── _health_summary (A3) ─────────────────────────────────────────────────
|
||||
# ── dashboard composition (Stage F) ──────────────────────────────────────
|
||||
|
||||
_ACTIVE_RUNNER = RunnerStatus(name="act-runner.service", active=True, sub_state="running")
|
||||
_DEAD_RUNNER = RunnerStatus(name="act-runner-rust.service", active=False, sub_state="dead")
|
||||
_LIVE_VM = LiveVm(
|
||||
name="job1", vmx="/x/job1.vmx", ip="10.0.0.5", guest_os="ubuntu-64",
|
||||
uptime_sec=120, is_orphan=False,
|
||||
)
|
||||
_ORPHAN_VM = LiveVm(
|
||||
name="job2", vmx="/x/job2.vmx", ip=None, guest_os="ubuntu-64",
|
||||
uptime_sec=99999, is_orphan=True,
|
||||
)
|
||||
_CACHE = CacheStatus(
|
||||
root=Path("/cache"), total_bytes=1024, entry_count=1, oldest_age_days=1, retention_days=30
|
||||
)
|
||||
|
||||
|
||||
def test_health_summary_backend_unavailable_is_vm_unknown() -> None:
|
||||
health = app_module._health_summary(
|
||||
[], backend_available=False, cache=None, disk=None, runners=[]
|
||||
)
|
||||
assert health.vm_class == "tile-unknown"
|
||||
assert health.vm_label == "unknown"
|
||||
|
||||
|
||||
def test_health_summary_orphan_present_is_vm_warn() -> None:
|
||||
health = app_module._health_summary(
|
||||
[_LIVE_VM, _ORPHAN_VM], backend_available=True, cache=None, disk=None, runners=[]
|
||||
)
|
||||
assert health.vm_count == 2
|
||||
assert health.orphan_count == 1
|
||||
assert health.vm_class == "tile-warn"
|
||||
assert health.vm_label == "orphaned"
|
||||
|
||||
|
||||
def test_health_summary_no_orphans_is_vm_ok() -> None:
|
||||
health = app_module._health_summary(
|
||||
[_LIVE_VM], backend_available=True, cache=None, disk=None, runners=[]
|
||||
)
|
||||
assert health.vm_class == "tile-ok"
|
||||
assert health.vm_label == "ok"
|
||||
|
||||
|
||||
def test_health_summary_no_runners_is_unknown() -> None:
|
||||
health = app_module._health_summary(
|
||||
[], backend_available=True, cache=None, disk=None, runners=[]
|
||||
)
|
||||
assert health.runners_total == 0
|
||||
assert health.runner_class == "tile-unknown"
|
||||
|
||||
|
||||
def test_health_summary_all_runners_active_is_ok() -> None:
|
||||
health = app_module._health_summary(
|
||||
[], backend_available=True, cache=None, disk=None, runners=[_ACTIVE_RUNNER]
|
||||
)
|
||||
assert health.runners_active == 1
|
||||
assert health.runners_total == 1
|
||||
assert health.runner_class == "tile-ok"
|
||||
|
||||
|
||||
def test_health_summary_a_down_runner_is_bad() -> None:
|
||||
health = app_module._health_summary(
|
||||
[], backend_available=True, cache=None, disk=None,
|
||||
runners=[_ACTIVE_RUNNER, _DEAD_RUNNER],
|
||||
)
|
||||
assert health.runners_active == 1
|
||||
assert health.runners_total == 2
|
||||
assert health.runner_class == "tile-bad"
|
||||
assert health.runner_label == "down"
|
||||
|
||||
|
||||
def test_health_summary_disk_none_is_unknown() -> None:
|
||||
health = app_module._health_summary(
|
||||
[], backend_available=True, cache=None, disk=None, runners=[]
|
||||
)
|
||||
assert health.disk_used_pct is None
|
||||
assert health.disk_class == "tile-unknown"
|
||||
|
||||
|
||||
def test_health_summary_disk_above_critical_is_bad() -> None:
|
||||
disk = DiskStatus(path=Path("/"), total_gb=100.0, free_gb=5.0, used_pct=90.0)
|
||||
health = app_module._health_summary(
|
||||
[], backend_available=True, cache=None, disk=disk, runners=[]
|
||||
)
|
||||
assert health.disk_class == "tile-bad"
|
||||
assert health.disk_label == "critical"
|
||||
|
||||
|
||||
def test_health_summary_disk_above_elevated_is_warn() -> None:
|
||||
disk = DiskStatus(path=Path("/"), total_gb=100.0, free_gb=25.0, used_pct=75.0)
|
||||
health = app_module._health_summary(
|
||||
[], backend_available=True, cache=None, disk=disk, runners=[]
|
||||
)
|
||||
assert health.disk_class == "tile-warn"
|
||||
assert health.disk_label == "elevated"
|
||||
|
||||
|
||||
def test_health_summary_disk_healthy_is_ok() -> None:
|
||||
disk = DiskStatus(path=Path("/"), total_gb=100.0, free_gb=60.0, used_pct=40.0)
|
||||
health = app_module._health_summary(
|
||||
[], backend_available=True, cache=None, disk=disk, runners=[]
|
||||
)
|
||||
assert health.disk_class == "tile-ok"
|
||||
assert health.disk_label == "ok"
|
||||
|
||||
|
||||
def test_health_summary_cache_none_is_unknown() -> None:
|
||||
health = app_module._health_summary(
|
||||
[], backend_available=True, cache=None, disk=None, runners=[]
|
||||
)
|
||||
assert health.cache_bytes is None
|
||||
assert health.cache_class == "tile-unknown"
|
||||
|
||||
|
||||
def test_health_summary_cache_present_is_ok() -> None:
|
||||
health = app_module._health_summary(
|
||||
[], backend_available=True, cache=_CACHE, disk=None, runners=[]
|
||||
)
|
||||
assert health.cache_bytes == 1024
|
||||
assert health.cache_class == "tile-ok"
|
||||
|
||||
|
||||
def test_dashboard_includes_status_header_tiles(
|
||||
def test_dashboard_renders_the_info_cards_without_a_status_header(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
"""Stage F: the tiles are gone; every readout they carried is in a card.
|
||||
|
||||
Threshold levels themselves are covered in ``test_web_cards.py``
|
||||
(``_resource_levels``) -- this only pins the page composition.
|
||||
"""
|
||||
_quiet_host(monkeypatch)
|
||||
app = create_app(_make_config(tmp_path))
|
||||
app.dependency_overrides[get_backend] = lambda: _FakeBackend([])
|
||||
@@ -407,4 +293,23 @@ def test_dashboard_includes_status_header_tiles(
|
||||
resp = client.get("/")
|
||||
|
||||
assert resp.status_code == 200
|
||||
assert 'id="status-header"' in resp.text
|
||||
assert 'id="status-header"' not in resp.text
|
||||
assert 'id="host-card"' in resp.text
|
||||
|
||||
|
||||
def test_dashboard_lists_a_running_vm_under_its_run(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
"""The VM count left with the tiles -- the Executions card is where VMs show."""
|
||||
_quiet_host(monkeypatch)
|
||||
vmx = tmp_path / "build-vms" / "job1" / "job1.vmx"
|
||||
app = create_app(_make_config(tmp_path))
|
||||
app.dependency_overrides[get_backend] = lambda: _FakeBackend(
|
||||
[VmHandle(identifier=str(vmx), name=_LIVE_VM.name)]
|
||||
)
|
||||
client = TestClient(app)
|
||||
|
||||
resp = client.get("/")
|
||||
|
||||
assert resp.status_code == 200
|
||||
assert "job1" in resp.text
|
||||
|
||||
@@ -0,0 +1,331 @@
|
||||
"""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 <dl>; VMware and Runners used to be
|
||||
a bordered <table> 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 <table>."""
|
||||
client = _client(tmp_path, monkeypatch)
|
||||
|
||||
for route in ("/host", "/vmware", "/runners", "/cache"):
|
||||
resp = client.get(route)
|
||||
assert resp.status_code == 200
|
||||
assert "<dl>" in resp.text
|
||||
assert "<table>" 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 "<dt>System</dt>" in resp.text
|
||||
assert "ci-host" in resp.text
|
||||
assert "7.0.0-28-generic" in resp.text
|
||||
assert "<dt>CPU</dt>" in resp.text
|
||||
assert "16 cores" in resp.text
|
||||
for retired in ("<dt>Hostname</dt>", "<dt>Kernel</dt>", "<dt>Cores</dt>"):
|
||||
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("<dt>") == 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 ``<dl>`` it has no ``<dt>``."""
|
||||
resp = _client(tmp_path, monkeypatch).get("/host")
|
||||
|
||||
assert "head-meta" in resp.text
|
||||
assert "uptime 26h 03m" in resp.text # 93784s
|
||||
assert "<dt>Uptime</dt>" 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 "<dt>Load avg</dt>" 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
|
||||
Reference in New Issue
Block a user