Add test_transitions.py (recorder idempotency under replay, terminal close, fail-soft, the B4 _instrument signature-preservation guarantee, end-to-end graph drive), test_topology.py (meta coverage, tree grouping, edge classification, phase->node map), and test_dashboard.py (/api/state contract + per-node state, /api/topology, /api/task timeline+cost+partial+thread_id validation, all via TestClient, fastapi-skipif guarded). Rework test_status_page.py to the data layer (drop retired render_html/SVG tests). conftest exposes tests/ for sibling imports. Frontend: Vitest+RTL for layout, TaskList filters/selection, TaskDrawer timeline.
257 lines
8.3 KiB
Python
257 lines
8.3 KiB
Python
"""Tests for the status dashboard DATA LAYER (``agent_team.status_page``).
|
|
|
|
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
|
|
|
|
import json
|
|
import sqlite3
|
|
from contextlib import closing
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from agent_team.status_page import (
|
|
STAGES,
|
|
Snapshot,
|
|
TaskView,
|
|
build_snapshot,
|
|
snapshot_to_dict,
|
|
)
|
|
|
|
|
|
def _sample_snapshot() -> Snapshot:
|
|
return Snapshot(
|
|
generated_at="2026-06-23 12:00:00 UTC",
|
|
db_path="/home/adam/orchestrator/agent-team/state/agent_team.sqlite",
|
|
ok=True,
|
|
tasks=[
|
|
TaskView(
|
|
thread_id="abc123def456",
|
|
short_id="abc123de",
|
|
task="remediate CVE-2026-0001 in payments-svc",
|
|
current_phase="clarify",
|
|
status="waiting_human",
|
|
waiting=True,
|
|
waiting_since="2026-06-23T11:50:00+00:00",
|
|
),
|
|
TaskView(
|
|
thread_id="ffff0000aaaa",
|
|
short_id="ffff0000",
|
|
task="add a smoke-test file",
|
|
current_phase="plan",
|
|
status="active",
|
|
waiting=False,
|
|
),
|
|
TaskView(
|
|
thread_id="dead0000beef",
|
|
short_id="dead0000",
|
|
task="stalled task",
|
|
current_phase="parked",
|
|
status="parked",
|
|
waiting=False,
|
|
),
|
|
],
|
|
question_counts={"open": 1, "answered": 4, "expired": 1},
|
|
recent_spend=[
|
|
{
|
|
"recorded_at": "2026-06-23T11:00:00+00:00",
|
|
"stage": "plan",
|
|
"model": "claude-opus-4",
|
|
"usd_cost": 0.1234,
|
|
}
|
|
],
|
|
spend_total_usd=1.5,
|
|
budget_available=True,
|
|
)
|
|
|
|
|
|
# --- read-only reader tests against a temp seeded SQLite ledger. -------------
|
|
|
|
|
|
def seed_ledger(db_path: Path) -> None:
|
|
"""Create the agent-team schema and seed a couple of rows.
|
|
|
|
Seeds one open pending question and one budget row via the foundation
|
|
schema, and one LangGraph checkpoint per thread via the real ``SqliteSaver``
|
|
so the reader's checkpoint path is exercised faithfully. Imported by
|
|
``test_dashboard.py`` as well.
|
|
"""
|
|
from agent_team.db.schema import init_db
|
|
from langgraph.checkpoint.base import empty_checkpoint
|
|
from langgraph.checkpoint.sqlite import SqliteSaver
|
|
|
|
init_db(db_path)
|
|
|
|
with closing(sqlite3.connect(str(db_path))) as conn:
|
|
conn.execute(
|
|
"INSERT INTO pending_questions "
|
|
"(question_id, thread_id, turn, status, transport, posted_at) "
|
|
"VALUES (?, ?, ?, 'open', 'slack', ?)",
|
|
("q1", "thread-waiting", 0, "2026-06-23T11:50:00+00:00"),
|
|
)
|
|
conn.execute(
|
|
"INSERT INTO budget_ledger "
|
|
"(thread_id, stage, model, billing_mode, usd_cost, recorded_at, day_bucket) "
|
|
"VALUES (?, ?, ?, ?, ?, ?, ?)",
|
|
(
|
|
"thread-waiting",
|
|
"plan",
|
|
"claude-opus-4",
|
|
"subscription",
|
|
0.25,
|
|
"2026-06-23T11:00:00+00:00",
|
|
"2026-06-23",
|
|
),
|
|
)
|
|
conn.commit()
|
|
|
|
with closing(sqlite3.connect(str(db_path), check_same_thread=False)) as conn:
|
|
saver = SqliteSaver(conn)
|
|
for tid, values in (
|
|
(
|
|
"thread-waiting",
|
|
{
|
|
"task": "remediate CVE",
|
|
"current_phase": "clarify",
|
|
"status": "waiting_human",
|
|
},
|
|
),
|
|
(
|
|
"thread-active",
|
|
{
|
|
"task": "add a file",
|
|
"current_phase": "plan",
|
|
"status": "active",
|
|
},
|
|
),
|
|
):
|
|
ck = empty_checkpoint()
|
|
ck["channel_values"] = values
|
|
saver.put(
|
|
{"configurable": {"thread_id": tid, "checkpoint_ns": ""}},
|
|
ck,
|
|
{},
|
|
{},
|
|
)
|
|
|
|
|
|
def test_build_snapshot_reads_seeded_ledger(tmp_path: Path) -> None:
|
|
db = tmp_path / "agent_team.sqlite"
|
|
seed_ledger(db)
|
|
|
|
snap = build_snapshot(db)
|
|
|
|
assert snap.ok is True
|
|
assert snap.error is None
|
|
by_id = {t.thread_id: t for t in snap.tasks}
|
|
assert set(by_id) == {"thread-waiting", "thread-active"}
|
|
|
|
waiting = by_id["thread-waiting"]
|
|
assert waiting.task == "remediate CVE"
|
|
assert waiting.current_phase == "clarify"
|
|
assert waiting.status == "waiting_human"
|
|
assert waiting.waiting is True
|
|
assert waiting.waiting_since == "2026-06-23T11:50:00+00:00"
|
|
|
|
active = by_id["thread-active"]
|
|
assert active.waiting is False
|
|
assert active.status == "active"
|
|
|
|
assert snap.active_count == 2 # active + waiting_human
|
|
assert snap.parked_count == 0
|
|
assert snap.question_counts.get("open") == 1
|
|
|
|
assert snap.budget_available is True
|
|
assert snap.spend_total_usd == pytest.approx(0.25)
|
|
assert snap.recent_spend[0]["model"] == "claude-opus-4"
|
|
|
|
|
|
def test_build_snapshot_missing_db_is_fail_safe(tmp_path: Path) -> None:
|
|
snap = build_snapshot(tmp_path / "does-not-exist.sqlite")
|
|
assert snap.ok is False
|
|
assert snap.error and "not found" in snap.error
|
|
|
|
|
|
def test_build_snapshot_never_writes(tmp_path: Path) -> None:
|
|
"""The reader must open mode=ro; it can never create the DB file."""
|
|
missing = tmp_path / "nope.sqlite"
|
|
build_snapshot(missing)
|
|
assert not missing.exists() # mode=ro did not create it
|
|
|
|
|
|
# --- /api/state JSON payload (snapshot_to_dict) contract ---------------------
|
|
|
|
|
|
def test_snapshot_to_dict_shape_and_grouping() -> None:
|
|
"""The /api/state payload keeps its keys + correct stage grouping."""
|
|
payload = snapshot_to_dict(_sample_snapshot())
|
|
assert payload["ok"] is True
|
|
assert set(payload) >= {
|
|
"ok",
|
|
"generated_at",
|
|
"stages",
|
|
"tasks",
|
|
"waiting",
|
|
"question_counts",
|
|
"summary",
|
|
"budget",
|
|
}
|
|
assert [s["key"] for s in payload["stages"]] == [s.key for s in STAGES]
|
|
|
|
by_key = {s["key"]: s for s in payload["stages"]}
|
|
clarify = by_key["clarify"]
|
|
assert clarify["count"] == 1
|
|
assert clarify["state"] == "awaiting_human"
|
|
assert clarify["tasks"][0]["short_id"] == "abc123de"
|
|
assert by_key["plan"]["state"] == "active"
|
|
assert by_key["plan"]["count"] == 1
|
|
# The parked task maps onto no pipeline node.
|
|
assert all(
|
|
t["thread_id"] != "dead0000beef" for s in payload["stages"] for t in s["tasks"]
|
|
)
|
|
assert by_key["intake"]["state"] == "idle"
|
|
assert by_key["intake"]["count"] == 0
|
|
|
|
assert payload["summary"]["active"] == 2
|
|
assert payload["summary"]["waiting"] == 1
|
|
assert payload["summary"]["open_questions"] == 1
|
|
assert payload["summary"]["total"] == 3
|
|
|
|
|
|
def test_snapshot_to_dict_not_ok_is_serializable() -> None:
|
|
snap = Snapshot(generated_at="now", db_path="/x.sqlite", ok=False, error="boom")
|
|
payload = snapshot_to_dict(snap)
|
|
assert payload["ok"] is False
|
|
assert payload["error"] == "boom"
|
|
assert [s["key"] for s in payload["stages"]] == [s.key for s in STAGES]
|
|
assert all(s["count"] == 0 for s in payload["stages"])
|
|
assert json.loads(json.dumps(payload))["ok"] is False
|
|
|
|
|
|
def test_snapshot_to_dict_carries_raw_description_for_textcontent() -> None:
|
|
"""The JSON sidecar carries the raw description; the SPA renders it safely."""
|
|
snap = Snapshot(
|
|
generated_at="now",
|
|
db_path="/x.sqlite",
|
|
ok=True,
|
|
tasks=[
|
|
TaskView(
|
|
thread_id="x",
|
|
short_id="x",
|
|
task="</script><script>alert(1)</script>",
|
|
current_phase="plan",
|
|
status="active",
|
|
)
|
|
],
|
|
)
|
|
payload = snapshot_to_dict(snap)
|
|
parsed = json.loads(json.dumps(payload))
|
|
assert parsed["tasks"][0]["task"] == "</script><script>alert(1)</script>"
|