"""GET /api/runs|personas|workflows|budget — D1 read-only route smoke tests.""" from __future__ import annotations from collections.abc import AsyncIterator from pathlib import Path import pytest from httpx import ASGITransport, AsyncClient from my_deepagent.api.app import create_app from my_deepagent.config import load_config from my_deepagent.persistence.db import Database @pytest.fixture async def app_client(tmp_path: Path) -> AsyncIterator[AsyncClient]: """Yield an AsyncClient wired to an isolated config + sqlite test DB. httpx's `ASGITransport` does NOT trigger FastAPI's lifespan, so we run `app.router.lifespan_context(app)` manually around the client. """ db_url = f"sqlite+aiosqlite:///{tmp_path / 'api_read.sqlite3'}" cfg = load_config( workspace_root=tmp_path, data_dir=tmp_path / "data", database_url=db_url, ) # init_schema once so the API lifespan does not have to migrate. db = Database(db_url) await db.init_schema() await db.dispose() app = create_app(cfg) transport = ASGITransport(app=app) async with app.router.lifespan_context(app): async with AsyncClient(transport=transport, base_url="http://test") as client: yield client @pytest.mark.asyncio async def test_list_runs_empty(app_client: AsyncClient) -> None: r = await app_client.get("/api/runs") assert r.status_code == 200 assert r.json() == [] @pytest.mark.asyncio async def test_get_run_404(app_client: AsyncClient) -> None: r = await app_client.get("/api/runs/00000000-0000-0000-0000-000000000000") assert r.status_code == 404 @pytest.mark.asyncio async def test_list_personas_returns_seed(app_client: AsyncClient) -> None: r = await app_client.get("/api/personas") assert r.status_code == 200 rows = r.json() # Seed has 12 personas (10 OpenRouter Claude/DeepSeek + 1 default-interactive + # 1 DeepSeek-code-reviewer-without-subagents added in Step 15). assert len(rows) == 12 names = {p["name"] for p in rows} assert "openrouter-deepseek-spec-writer" in names # response model must include all keys (PersonaSummary contract) sample = rows[0] assert {"name", "version", "model", "capabilities", "max_risk_level"} <= set(sample) @pytest.mark.asyncio async def test_list_workflows_returns_seed(app_client: AsyncClient) -> None: r = await app_client.get("/api/workflows") assert r.status_code == 200 rows = r.json() # Seed has 3 workflows (spec-and-review, bug-fix-with-reproduction, code-investigation). assert len(rows) >= 3 names = {w["name"] for w in rows} assert "spec-and-review" in names # response shape sample = rows[0] assert {"path", "name", "version", "roles", "phases"} <= set(sample) @pytest.mark.asyncio async def test_get_budget_summary_empty(app_client: AsyncClient) -> None: r = await app_client.get("/api/budget") assert r.status_code == 200 payload = r.json() # No ledger rows yet → empty buckets. assert payload["day"] is None assert payload["runs"] == [] assert payload["personas"] == []