"""Tests for the read-only dashboard FastAPI app (``agent_team.dashboard``). Exercises the three JSON endpoints against a seeded temp ledger via FastAPI's TestClient: /api/state (contract preserved + per-node live state), /api/topology, and /api/task/{id} (timeline assembly, thread_id validation, partial fallback, cost join). Guarded so the suite still runs where FastAPI is absent. """ from __future__ import annotations import importlib.util from pathlib import Path import pytest from agent_team.db import TransitionRecorder, connect, init_db from agent_team.dashboard import task_detail # Reuse the seeded-ledger helper from the status-page tests (sibling module, # importable by bare name under pytest's default prepend import mode). from test_status_page import seed_ledger _HAS_FASTAPI = importlib.util.find_spec("fastapi") is not None pytestmark = pytest.mark.skipif(_HAS_FASTAPI is False, reason="fastapi not installed") def _client(db: Path): from fastapi.testclient import TestClient from agent_team.dashboard import make_dashboard_app return TestClient(make_dashboard_app(db_path=db)) def _seed_budget(db: Path, thread_id: str, stage: str, usd: float) -> None: conn = connect(db) try: conn.execute( "INSERT INTO budget_ledger (thread_id, stage, model, billing_mode, " "usd_cost, recorded_at, day_bucket) VALUES (?,?,?,?,?,?,?)", ( thread_id, stage, "claude", "subscription", usd, "2026-06-23T00:00:00+00:00", "2026-06-23", ), ) finally: conn.close() def test_api_state_contract_and_nodes(tmp_path: Path) -> None: db = tmp_path / "agent_team.sqlite" seed_ledger(db) r = _client(db).get("/api/state") assert r.status_code == 200 payload = r.json() # Existing contract preserved. assert payload["ok"] is True assert {"stages", "tasks", "summary", "budget"} <= set(payload) # New per-node live state, keyed by topology node id. assert "nodes" in payload # thread-waiting is at clarify and awaiting human; thread-active at plan. assert payload["nodes"]["clarify"]["state"] == "awaiting_human" assert payload["nodes"]["plan"]["state"] == "active" def test_api_topology(tmp_path: Path) -> None: db = tmp_path / "agent_team.sqlite" init_db(db) payload = _client(db).get("/api/topology").json() ids = {n["id"] for n in payload["nodes"]} assert {"intake", "clarify", "plan", "review"} <= ids assert payload["edges"] def test_api_task_timeline_and_cost(tmp_path: Path) -> None: db = tmp_path / "agent_team.sqlite" seed_ledger(db) rec = TransitionRecorder(db) rec.record_entry(thread_id="thread-active", to_phase="intake", status="active") rec.record_entry(thread_id="thread-active", to_phase="plan", status="active") _seed_budget(db, "thread-active", "plan", 0.5) payload = _client(db).get("/api/task/thread-active").json() assert payload["ok"] is True assert payload["partial"] is False steps = {s["to_phase"]: s for s in payload["timeline"]} assert "intake" in steps and "plan" in steps assert steps["plan"]["cost_usd"] == pytest.approx(0.5) assert payload["total_usd"] == pytest.approx(0.5) def test_api_task_partial_when_no_transitions(tmp_path: Path) -> None: db = tmp_path / "agent_team.sqlite" seed_ledger(db) # checkpoints exist, but no task_transitions rows payload = _client(db).get("/api/task/thread-active").json() assert payload["ok"] is True assert payload["partial"] is True # Best-effort single step reconstructed from the checkpoint phase. assert payload["timeline"] assert payload["timeline"][0]["note"] def test_api_task_rejects_bad_thread_id(tmp_path: Path) -> None: db = tmp_path / "agent_team.sqlite" init_db(db) cli = _client(db) assert cli.get("/api/task/has space").status_code == 400 assert cli.get("/api/task/" + "x" * 65).status_code == 400 def test_task_detail_missing_ledger_is_failsafe(tmp_path: Path) -> None: out = task_detail(tmp_path / "nope.sqlite", "abc") assert out["ok"] is False