why-agent / client /backend /tests /test_api_smoke.py
MapoTofu9's picture
deploy: HF Spaces
5d30bdc
Raw
History Blame Contribute Delete
6.5 kB
"""Smoke tests for the FastAPI backend. All tests use a fake graph — no LLM, no network."""
from __future__ import annotations
import json
import os
from collections.abc import Iterator
from unittest.mock import patch
import pytest
from starlette.testclient import TestClient
os.environ.setdefault("MODEL_BACKEND", "replay")
from client.backend.main import app # noqa: E402
class _FakeGraph:
"""Minimal graph stub that yields predictable state chunks then a final report."""
def stream(self, init_state, stream_mode="values") -> Iterator[dict]:
from agent.state import EvidenceEntry, Phase
yield {
"phase": Phase.PLAN,
"evidence": [],
"hypotheses": [],
"retry_count": 0,
"final_report": None,
}
yield {
"phase": Phase.DECOMPOSE,
"evidence": [
EvidenceEntry(
phase=Phase.DECOMPOSE,
tool_name="run_sql",
args={"query": "SELECT 1", "max_rows": 10},
output={
"rows": [{"1": 1}],
"row_count": 1,
"truncated": False,
"execution_ms": 5.0,
},
)
],
"hypotheses": [],
"retry_count": 0,
"final_report": None,
}
yield {
"phase": Phase.REPORT,
"evidence": [
EvidenceEntry(
phase=Phase.DECOMPOSE,
tool_name="run_sql",
args={"query": "SELECT 1", "max_rows": 10},
output={
"rows": [{"1": 1}],
"row_count": 1,
"truncated": False,
"execution_ms": 5.0,
},
)
],
"hypotheses": [],
"retry_count": 0,
"final_report": {
"user_question": "test question",
"text": "Test report text.",
"hypotheses": [],
"evidence_count": 1,
"critique_passed": True,
"error": None,
},
}
def _parse_sse(lines: Iterator[str]) -> list[dict]:
"""Parse raw SSE text lines into a list of {event, data} dicts."""
events: list[dict] = []
current: dict = {}
for line in lines:
if line.startswith("event:"):
current["event"] = line[6:].strip()
elif line.startswith("data:"):
raw = line[5:].strip()
try:
current["data"] = json.loads(raw)
except json.JSONDecodeError:
current["data"] = raw
elif line == "" and current:
events.append(current)
current = {}
if current:
events.append(current)
return events
@pytest.fixture(autouse=True)
def _clear_graph_cache():
from client.backend.deps import get_graph
get_graph.cache_clear()
yield
get_graph.cache_clear()
@pytest.fixture()
def client():
with patch("client.backend.deps.build_graph", return_value=_FakeGraph()):
with TestClient(app, raise_server_exceptions=True) as c:
yield c
def test_health(client):
r = client.get("/api/health")
assert r.status_code == 200
assert r.json() == {"ok": True}
def test_demo_questions(client):
r = client.get("/api/demo-questions")
assert r.status_code == 200
data = r.json()
assert "questions" in data
assert len(data["questions"]) == 4
assert all(isinstance(q, str) for q in data["questions"])
def test_investigate_empty_question_rejected(client):
r = client.post("/api/investigate", json={"question": ""})
assert r.status_code == 422
def test_investigate_missing_question_rejected(client):
r = client.post("/api/investigate", json={})
assert r.status_code == 422
def test_investigate_streams_sse(client):
"""Full integration: POST /api/investigate yields SSE events in expected order."""
with client.stream(
"POST", "/api/investigate", json={"question": "Why did metric X drop?"}
) as response:
assert response.status_code == 200
assert "text/event-stream" in response.headers["content-type"]
events = _parse_sse(response.iter_lines())
types = [e.get("data", {}).get("type") for e in events]
assert "run_started" in types
assert "phase" in types
assert "evidence" in types
assert "report" in types
assert "done" in types
assert types[-1] == "done"
def test_investigate_report_has_required_fields(client):
with client.stream("POST", "/api/investigate", json={"question": "test"}) as response:
events = _parse_sse(response.iter_lines())
report_events = [e["data"] for e in events if e.get("data", {}).get("type") == "report"]
assert len(report_events) == 1
report = report_events[0]
assert "user_question" in report
assert "text" in report
assert "evidence_count" in report
assert "critique_passed" in report
def test_investigate_evidence_has_required_fields(client):
with client.stream("POST", "/api/investigate", json={"question": "test"}) as response:
events = _parse_sse(response.iter_lines())
evidence_events = [e["data"] for e in events if e.get("data", {}).get("type") == "evidence"]
assert len(evidence_events) >= 1
ev = evidence_events[0]
assert "index" in ev
assert "tool_name" in ev
assert "args" in ev
assert "output" in ev
def test_investigate_run_started_has_run_id(client):
with client.stream("POST", "/api/investigate", json={"question": "test"}) as response:
events = _parse_sse(response.iter_lines())
started = [e["data"] for e in events if e.get("data", {}).get("type") == "run_started"]
assert len(started) == 1
assert "run_id" in started[0]
assert started[0]["user_question"] == "test"
def test_phase_events_include_known_phases(client):
with client.stream("POST", "/api/investigate", json={"question": "test"}) as response:
events = _parse_sse(response.iter_lines())
phase_events = [e["data"] for e in events if e.get("data", {}).get("type") == "phase"]
assert len(phase_events) >= 1
known = {"plan", "decompose", "drill", "cross_check", "critique", "report"}
for pe in phase_events:
assert pe["phase"] in known