"""Tests for the read-only (W4) web dashboard app. In-process only: routes call ``operations/`` + a backend injected via FastAPI dependency override -- no real ``vmrun`` subprocess is ever spawned. ``shutil.disk_usage``/``shutil.which`` are monkeypatched so no real host state (disk, systemd) leaks into the assertions either. """ 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.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 _DiskUsage = namedtuple("_DiskUsage", ["total", "used", "free"]) def _fake_disk_usage(_path: Any) -> _DiskUsage: gb = 1024**3 return _DiskUsage(total=100 * gb, used=40 * gb, free=60 * gb) 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: """Stub ``VmBackend`` -- proves no real ``vmrun`` is ever invoked.""" def __init__(self, handles: list[VmHandle], ip: str | None = "192.168.79.201") -> None: self._handles = handles self._ip = ip self.list_running_calls = 0 self.get_ip_calls = 0 def list_running(self) -> list[VmHandle]: self.list_running_calls += 1 return self._handles def get_ip(self, handle: VmHandle, timeout: float = 0.0) -> str | None: self.get_ip_calls += 1 return self._ip class _BoomBackend: """Backend whose ``list_running`` raises something other than ``BackendError``.""" def list_running(self) -> list[VmHandle]: raise RuntimeError("boom") def get_ip(self, handle: VmHandle, timeout: float = 0.0) -> str | None: return None def _quiet_host(monkeypatch: pytest.MonkeyPatch) -> None: """No real disk/systemctl calls from ``operations.host`` during a test.""" monkeypatch.setattr(shutil, "disk_usage", _fake_disk_usage) monkeypatch.setattr(shutil, "which", lambda _name: None) # ── /healthz ────────────────────────────────────────────────────────────── def test_healthz_returns_ok(tmp_path: Path) -> None: app = create_app(_make_config(tmp_path)) client = TestClient(app) resp = client.get("/healthz") assert resp.status_code == 200 assert resp.json() == {"status": "ok"} # ── / (dashboard) ──────────────────────────────────────────────────────── def test_dashboard_renders_all_four_cards( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: _quiet_host(monkeypatch) app = create_app(_make_config(tmp_path)) app.dependency_overrides[get_backend] = lambda: _FakeBackend([]) client = TestClient(app) resp = client.get("/") assert resp.status_code == 200 for needle in ("Live VMs", "Cargo cache", "Disk", "Runners"): assert needle in resp.text def test_dashboard_shows_backend_unavailable_notice_when_backend_is_none( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: _quiet_host(monkeypatch) app = create_app(_make_config(tmp_path)) app.dependency_overrides[get_backend] = lambda: None client = TestClient(app) resp = client.get("/") assert resp.status_code == 200 assert "unavailable" in resp.text.lower() # ── /vms ────────────────────────────────────────────────────────────────── def test_vms_route_renders_injected_vm_and_never_touches_real_vmrun(tmp_path: Path) -> None: vmx = tmp_path / "build-vms" / "job1" / "job1.vmx" fake = _FakeBackend([VmHandle(identifier=str(vmx), name="job1")]) app = create_app(_make_config(tmp_path)) app.dependency_overrides[get_backend] = lambda: fake client = TestClient(app) resp = client.get("/vms") assert resp.status_code == 200 assert "job1" in resp.text assert fake.list_running_calls == 1 def test_vms_route_degrades_when_backend_is_none(tmp_path: Path) -> None: app = create_app(_make_config(tmp_path)) app.dependency_overrides[get_backend] = lambda: None client = TestClient(app) resp = client.get("/vms") assert resp.status_code == 200 assert "unavailable" in resp.text.lower() def test_vms_route_degrades_on_unexpected_backend_error(tmp_path: Path) -> None: app = create_app(_make_config(tmp_path)) app.dependency_overrides[get_backend] = lambda: _BoomBackend() client = TestClient(app) resp = client.get("/vms") assert resp.status_code == 200 def test_get_backend_degrades_to_none_when_vmrun_missing( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: def _raise(_config: Config) -> Any: raise BackendNotAvailable("vmrun not found") monkeypatch.setattr(app_module, "load_backend", _raise) app = create_app(_make_config(tmp_path)) client = TestClient(app) resp = client.get("/vms") assert resp.status_code == 200 assert "unavailable" in resp.text.lower() # ── /cache ──────────────────────────────────────────────────────────────── def test_cache_route_renders_status(tmp_path: Path) -> None: app = create_app(_make_config(tmp_path)) client = TestClient(app) resp = client.get("/cache") assert resp.status_code == 200 assert "Cargo cache" in resp.text def test_cache_route_degrades_when_operations_raises( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: def _boom(_config: Config) -> Any: raise RuntimeError("boom") monkeypatch.setattr(app_module, "cargo_cache_status", _boom) app = create_app(_make_config(tmp_path)) client = TestClient(app) resp = client.get("/cache") assert resp.status_code == 200 # ── /host ───────────────────────────────────────────────────────────────── def test_runners_route_renders_runner_status( 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) resp = client.get("/runners") assert resp.status_code == 200 assert "Runners" in resp.text assert "act-runner.service" in resp.text assert "act-runner-rust.service" in resp.text def test_runners_route_degrades_when_disk_status_raises( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: def _boom(_path: Path) -> Any: raise RuntimeError("boom") monkeypatch.setattr(app_module, "disk_status", _boom) monkeypatch.setattr(shutil, "which", lambda _name: None) app = create_app(_make_config(tmp_path)) client = TestClient(app) resp = client.get("/runners") assert resp.status_code == 200 def test_runners_route_degrades_when_runner_status_raises( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: def _boom(_names: list[str]) -> Any: raise RuntimeError("boom") _quiet_host(monkeypatch) monkeypatch.setattr(app_module, "runner_status", _boom) app = create_app(_make_config(tmp_path)) client = TestClient(app) resp = client.get("/runners") assert resp.status_code == 200 # ── static mount ───────────────────────────────────────────────────────── def test_static_mount_serves_app_css(tmp_path: Path) -> None: app = create_app(_make_config(tmp_path)) client = TestClient(app) resp = client.get("/static/app.css") assert resp.status_code == 200 # ── _health_summary (A3) ───────────────────────────────────────────────── _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( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: _quiet_host(monkeypatch) app = create_app(_make_config(tmp_path)) app.dependency_overrides[get_backend] = lambda: _FakeBackend([]) client = TestClient(app) resp = client.get("/") assert resp.status_code == 200 assert 'id="status-header"' in resp.text