| """FastAPI endpoints via TestClient, with the orchestrator mocked.""" |
|
|
| import json |
| from datetime import datetime, timedelta |
|
|
| from fastapi.testclient import TestClient |
|
|
| import main |
| from app.providers import llm |
|
|
| client = TestClient(main.app) |
|
|
| |
| |
| ARRIVE_BY = (datetime.now() + timedelta(hours=20)).strftime("%Y-%m-%dT%H:%M") |
|
|
|
|
| def test_index_renders(): |
| response = client.get("/") |
| assert response.status_code == 200 |
| assert "Saarthi" in response.text |
| assert "plan-form" in response.text |
|
|
|
|
| def test_health(): |
| response = client.get("/api/health") |
| assert response.status_code == 200 |
| assert response.json()["status"] == "ok" |
|
|
|
|
| def test_plan_stream_emits_sse_events(monkeypatch): |
| def fake_run_plan(from_text, to_text, arrive_by, mode, **kwargs): |
| yield {"type": "status", "message": "working"} |
| yield {"type": "verdict", "data": {"risk": {"score": 10, "level": "LOW"}}} |
|
|
| monkeypatch.setattr(main.orchestrator, "run_plan", fake_run_plan) |
|
|
| response = client.get( |
| "/api/plan/stream", |
| params={"from": "Gomti Nagar", "to": "Charbagh", "arrive_by": ARRIVE_BY}, |
| ) |
| assert response.status_code == 200 |
| assert response.headers["content-type"].startswith("text/event-stream") |
|
|
| payloads = [ |
| json.loads(line[6:]) |
| for line in response.text.splitlines() |
| if line.startswith("data: ") |
| ] |
| types = [payload["type"] for payload in payloads] |
| assert types == ["status", "verdict", "done"] |
|
|
|
|
| def test_plan_stream_converts_crashes_to_error_events(monkeypatch): |
| def exploding_run_plan(*args, **kwargs): |
| raise RuntimeError("unexpected crash") |
| yield |
|
|
| monkeypatch.setattr(main.orchestrator, "run_plan", exploding_run_plan) |
|
|
| response = client.get( |
| "/api/plan/stream", |
| params={"from": "Aliganj", "to": "Chowk", "arrive_by": ARRIVE_BY}, |
| ) |
| assert response.status_code == 200 |
| assert '"type": "error"' in response.text or '"error"' in response.text |
|
|
|
|
| def test_plan_stream_validates_mode(): |
| response = client.get( |
| "/api/plan/stream", |
| params={"from": "Aliganj", "to": "Chowk", "arrive_by": ARRIVE_BY, "mode": "rocket"}, |
| ) |
| assert response.status_code == 422 |
|
|
|
|
| def test_plan_stream_requires_params(): |
| response = client.get("/api/plan/stream", params={"from": "A"}) |
| assert response.status_code == 422 |
|
|
|
|
| def test_ask_returns_503_without_llm(monkeypatch): |
| monkeypatch.setattr(llm, "available", lambda: False) |
| monkeypatch.setattr(main.llm, "available", lambda: False) |
| response = client.post("/api/ask", json={"question": "test"}) |
| assert response.status_code == 503 |
|
|
|
|
| def test_autocomplete_returns_suggestions(monkeypatch): |
| monkeypatch.setattr( |
| main.geocode, "autocomplete_place", |
| lambda q: [{"name": "Hazratganj", "address": "MG Marg, Lucknow", "lat": 26.84, "lon": 80.94}], |
| ) |
| response = client.get("/api/autocomplete", params={"q": "hazra"}) |
| assert response.status_code == 200 |
| suggestions = response.json()["suggestions"] |
| assert suggestions[0]["name"] == "Hazratganj" |
|
|
|
|
| def test_autocomplete_requires_min_length(): |
| response = client.get("/api/autocomplete", params={"q": "h"}) |
| assert response.status_code == 422 |
|
|
|
|
| def test_plan_stream_passes_coords_to_orchestrator(monkeypatch): |
| captured = {} |
|
|
| def fake_run_plan(from_text, to_text, arrive_by, mode, origin_coords=None, dest_coords=None): |
| captured["origin_coords"] = origin_coords |
| captured["dest_coords"] = dest_coords |
| yield {"type": "verdict", "data": {}} |
|
|
| monkeypatch.setattr(main.orchestrator, "run_plan", fake_run_plan) |
|
|
| response = client.get( |
| "/api/plan/stream", |
| params={ |
| "from": "IIIT Lucknow", "to": "Hazratganj", "arrive_by": ARRIVE_BY, |
| "from_lat": 26.8003, "from_lon": 81.0242, |
| "to_lat": 26.8488, "to_lon": 80.9436, |
| }, |
| ) |
| assert response.status_code == 200 |
| assert captured["origin_coords"] == (26.8003, 81.0242) |
| assert captured["dest_coords"] == (26.8488, 80.9436) |
|
|
|
|
| def test_ask_stream_emits_sse_events(monkeypatch): |
| monkeypatch.setattr(main.llm, "available", lambda: True) |
|
|
| def fake_stream(question, history=None): |
| yield {"type": "tool", "name": "get_festivals", "args": {}} |
| yield {"type": "answer", "text": "Bada Mangal tomorrow.", "provider": "gemini", "steps": []} |
|
|
| monkeypatch.setattr(main.orchestrator, "agent_ask_stream", fake_stream) |
|
|
| response = client.get( |
| "/api/ask/stream", |
| params={"question": "Any festival tomorrow?", "history": '[{"role":"user","content":"hi"}]'}, |
| ) |
| assert response.status_code == 200 |
| payloads = [ |
| json.loads(line[6:]) |
| for line in response.text.splitlines() |
| if line.startswith("data: ") |
| ] |
| types = [payload["type"] for payload in payloads] |
| assert types == ["tool", "answer", "done"] |
|
|
|
|
| def test_ask_stream_handles_bad_history_json(monkeypatch): |
| monkeypatch.setattr(main.llm, "available", lambda: True) |
| captured = {} |
|
|
| def fake_stream(question, history=None): |
| captured["history"] = history |
| yield {"type": "answer", "text": "ok", "provider": "gemini", "steps": []} |
|
|
| monkeypatch.setattr(main.orchestrator, "agent_ask_stream", fake_stream) |
| response = client.get( |
| "/api/ask/stream", |
| params={"question": "test question", "history": "{not json"}, |
| ) |
| assert response.status_code == 200 |
| assert captured["history"] == [] |
|
|
|
|
| def test_ask_stream_503_without_llm(monkeypatch): |
| monkeypatch.setattr(main.llm, "available", lambda: False) |
| response = client.get("/api/ask/stream", params={"question": "test question"}) |
| assert response.status_code == 503 |
|
|
|
|
| def test_ask_calls_agent(monkeypatch): |
| monkeypatch.setattr(main.llm, "available", lambda: True) |
| monkeypatch.setattr( |
| main.orchestrator, "agent_ask", |
| lambda question: {"answer": f"echo: {question}", "steps": [], "provider": "gemini"}, |
| ) |
| response = client.post("/api/ask", json={"question": "When should I leave?"}) |
| assert response.status_code == 200 |
| assert response.json()["answer"] == "echo: When should I leave?" |
|
|
|
|