SaarthiAI / tests /test_resilience.py
parthmax24's picture
working proto 5
8b96826
Raw
History Blame Contribute Delete
6.07 kB
"""The app must degrade gracefully, never crash: input validation,
fail-open cache, broad LLM fallback, global exception handler."""
import json
import pytest
import requests as requests_lib
from fastapi.testclient import TestClient
import main
from app import cache
from app.agents import synthesizer
from app.providers import gemini, groq, llm
from app.providers.gemini import ProviderError
from tests.test_synthesizer import make_context
client = TestClient(main.app)
crash_client = TestClient(main.app, raise_server_exceptions=False)
def stream_payloads(response):
return [
json.loads(line[6:])
for line in response.text.splitlines()
if line.startswith("data: ")
]
# ---- plan stream input validation ------------------------------------------
def test_invalid_arrive_by_returns_friendly_sse_error():
response = client.get(
"/api/plan/stream",
params={"from": "Aliganj", "to": "Chowk", "arrive_by": "not-a-date"},
)
assert response.status_code == 200 # SSE, not a 500
payloads = stream_payloads(response)
assert payloads[0]["type"] == "error"
assert "invalid" in payloads[0]["message"].lower()
assert payloads[-1]["type"] == "done"
def test_past_arrive_by_returns_friendly_sse_error():
response = client.get(
"/api/plan/stream",
params={"from": "Aliganj", "to": "Chowk", "arrive_by": "2020-01-01T09:30"},
)
payloads = stream_payloads(response)
assert payloads[0]["type"] == "error"
assert "past" in payloads[0]["message"].lower()
def test_far_future_arrive_by_returns_friendly_sse_error():
response = client.get(
"/api/plan/stream",
params={"from": "Aliganj", "to": "Chowk", "arrive_by": "2099-01-01T09:30"},
)
payloads = stream_payloads(response)
assert payloads[0]["type"] == "error"
assert "week" in payloads[0]["message"].lower()
# ---- endpoint crash safety ----------------------------------------------------
def test_autocomplete_crash_returns_empty_not_500(monkeypatch):
def boom(q):
raise RuntimeError("unexpected bug key=secret123")
monkeypatch.setattr(main.geocode, "autocomplete_place", boom)
response = client.get("/api/autocomplete", params={"q": "hazra"})
assert response.status_code == 200
assert response.json() == {"suggestions": []}
def test_ask_crash_returns_clean_500_without_secrets(monkeypatch):
monkeypatch.setattr(main.llm, "available", lambda: True)
def boom(question):
raise RuntimeError("boom at url?key=topsecret99")
monkeypatch.setattr(main.orchestrator, "agent_ask", boom)
response = client.post("/api/ask", json={"question": "hello"})
assert response.status_code == 500
assert "topsecret99" not in response.text
assert "detail" in response.json()
def test_ask_empty_question_rejected(monkeypatch):
monkeypatch.setattr(main.llm, "available", lambda: True)
response = client.post("/api/ask", json={"question": " "})
assert response.status_code == 422
def test_global_handler_catches_unhandled_route_errors(monkeypatch):
def boom():
raise ValueError("internal bug key=hidden42")
monkeypatch.setattr(main.llm, "available", boom)
response = crash_client.get("/api/health")
assert response.status_code == 500
assert "hidden42" not in response.text
assert "detail" in response.json()
# ---- fail-open cache ----------------------------------------------------------
def test_cache_read_failure_is_a_miss(monkeypatch):
def broken_conn():
raise RuntimeError("disk locked")
monkeypatch.setattr(cache, "_get_conn", broken_conn)
assert cache.get("anything") is None # no exception
def test_cache_write_failure_is_silent(monkeypatch):
def broken_conn():
raise RuntimeError("disk full")
monkeypatch.setattr(cache, "_get_conn", broken_conn)
cache.set("key", "value", ttl_seconds=60) # must not raise
def test_cached_function_still_works_when_cache_broken(monkeypatch):
def broken_conn():
raise RuntimeError("corrupted db")
monkeypatch.setattr(cache, "_get_conn", broken_conn)
@cache.cached(ttl_seconds=60)
def compute(x):
return x * 2
assert compute(21) == 42 # function result delivered despite broken cache
# ---- LLM layer degradation ----------------------------------------------------
def test_synthesizer_survives_unexpected_llm_crash(monkeypatch, fake_keys):
def boom(*args, **kwargs):
raise ValueError("adapter bug, not a ProviderError")
monkeypatch.setattr(synthesizer.llm, "chat", boom)
verdict, provider = synthesizer.synthesize(make_context())
assert provider == "deterministic"
assert verdict["recommended_departure"] == "08:15"
def test_llm_falls_back_when_gemini_adapter_crashes(monkeypatch, fake_keys):
def gemini_bug(*args, **kwargs):
raise KeyError("unexpected response shape")
monkeypatch.setattr(gemini, "chat", gemini_bug)
monkeypatch.setattr(
groq, "chat", lambda *a, **k: {"text": "ok", "tool_calls": [], "provider": "groq"}
)
result = llm.chat([{"role": "user", "content": "hi"}])
assert result["provider"] == "groq"
def test_gemini_non_json_response_is_provider_error(monkeypatch, fake_keys):
class BadResponse:
status_code = 200
text = "<html>gateway error</html>"
def json(self):
raise ValueError("not json")
monkeypatch.setattr(gemini.requests, "post", lambda *a, **k: BadResponse())
with pytest.raises(ProviderError, match="non-JSON"):
gemini.chat([{"role": "user", "content": "hi"}])
def test_groq_non_json_response_is_provider_error(monkeypatch, fake_keys):
class BadResponse:
status_code = 200
text = "<html>cloudflare</html>"
def json(self):
raise ValueError("not json")
monkeypatch.setattr(groq.requests, "post", lambda *a, **k: BadResponse())
with pytest.raises(ProviderError, match="non-JSON"):
groq.chat([{"role": "user", "content": "hi"}])