diff --git a/agent-team/tests/conftest.py b/agent-team/tests/conftest.py index 900c650..236b80e 100644 --- a/agent-team/tests/conftest.py +++ b/agent-team/tests/conftest.py @@ -11,3 +11,9 @@ from pathlib import Path _PROJECT_ROOT = Path(__file__).resolve().parents[1] if str(_PROJECT_ROOT) not in sys.path: sys.path.insert(0, str(_PROJECT_ROOT)) + +# Also expose the tests/ dir so a test module can import shared helpers from a +# sibling test module by bare name (e.g. ``from test_status_page import ...``). +_TESTS_DIR = Path(__file__).resolve().parent +if str(_TESTS_DIR) not in sys.path: + sys.path.insert(0, str(_TESTS_DIR)) diff --git a/agent-team/tests/test_dashboard.py b/agent-team/tests/test_dashboard.py new file mode 100644 index 0000000..14a936a --- /dev/null +++ b/agent-team/tests/test_dashboard.py @@ -0,0 +1,118 @@ +"""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 diff --git a/agent-team/tests/test_status_page.py b/agent-team/tests/test_status_page.py index 700bdaf..0b03067 100644 --- a/agent-team/tests/test_status_page.py +++ b/agent-team/tests/test_status_page.py @@ -1,10 +1,12 @@ -"""Tests for the LAN-only read-only status dashboard (``agent_team.status_page``). +"""Tests for the status dashboard DATA LAYER (``agent_team.status_page``). -No live socket is bound: :func:`render_html` is exercised against fabricated -:class:`Snapshot` objects, and the read-only reader (:func:`build_snapshot`) is -exercised against a temp SQLite ledger seeded with a couple of rows (a real -``SqliteSaver`` checkpoint plus ``pending_questions`` / ``budget_ledger`` rows). -``conftest.py`` already puts ``agent-team/`` on ``sys.path``. +After the WebUI makeover, ``status_page`` no longer renders HTML — the dashboard +is a React/Vite SPA served by ``agent_team.dashboard`` (see ``test_dashboard.py`` +for the FastAPI endpoints). What remains here is the read-only reader +(:func:`build_snapshot`) and the ``/api/state`` payload builder +(:func:`snapshot_to_dict`), exercised against fabricated :class:`Snapshot` +objects and a temp SQLite ledger seeded with real rows. ``conftest.py`` puts +``agent-team/`` on ``sys.path``. """ from __future__ import annotations @@ -21,12 +23,9 @@ from agent_team.status_page import ( Snapshot, TaskView, build_snapshot, - render_html, snapshot_to_dict, ) -# --- pure render_html tests (no DB, no socket). ------------------------------ - def _sample_snapshot() -> Snapshot: return Snapshot( @@ -74,85 +73,16 @@ def _sample_snapshot() -> Snapshot: ) -def test_render_html_returns_complete_document() -> None: - html_out = render_html(_sample_snapshot()) - assert html_out.startswith("") - assert html_out.rstrip().endswith("") - # Auto-refresh + offline (no external CDN / http references). - assert '' in html_out - assert "http://" not in html_out and "https://" not in html_out - assert "