Judge / backend /tests /test_api.py
GonzaViss's picture
feat(api): re-resolve corpus_version on demand when empty at startup
365c521
Raw
History Blame Contribute Delete
4.24 kB
"""Smoke tests for POST /api/v1/query."""
from unittest.mock import patch
import pytest
from fastapi.testclient import TestClient
from app.rag.retrieval import Chunk
def _make_chunk() -> Chunk:
return Chunk(
id="chunk-1",
content="Content about the rules of the game.",
section="Game Rules",
parent_section=None,
source_type="rulebook",
similarity=0.92,
)
# ---------------------------------------------------------------------------
# Happy path
# ---------------------------------------------------------------------------
def test_happy_path(client: TestClient):
"""POST with valid question returns 200, non-empty answer, and citations list."""
with patch("app.rag.pipeline.hybrid_search", return_value=[_make_chunk()]):
resp = client.post("/api/v1/query", json={"question": "How does double-tap work?"})
assert resp.status_code == 200
body = resp.json()
assert body["answer"] # non-empty string
assert isinstance(body["citations"], list)
assert "latency_ms" in body
# ---------------------------------------------------------------------------
# Validation error
# ---------------------------------------------------------------------------
def test_empty_question_returns_422(client: TestClient):
"""POST with empty question string returns 422 (Pydantic validation error)."""
resp = client.post("/api/v1/query", json={"question": ""})
assert resp.status_code == 422
# ---------------------------------------------------------------------------
# Timeout → 504
# ---------------------------------------------------------------------------
def test_timeout_returns_504(timeout_client: TestClient):
"""When Gemini raises GenerationTimeout, the endpoint returns 504."""
with patch("app.rag.pipeline.hybrid_search", return_value=[_make_chunk()]):
resp = timeout_client.post(
"/api/v1/query", json={"question": "What happens on timeout?"}
)
assert resp.status_code == 504
assert resp.json()["detail"] == "Generation timeout"
# ---------------------------------------------------------------------------
# Pool exhaustion → 503 (load shedding)
# ---------------------------------------------------------------------------
def test_pool_exhaustion_returns_503_with_retry_after(client: TestClient):
"""When the connection pool is exhausted (PoolError), shed load with 503."""
from psycopg2.pool import PoolError
with patch(
"app.api.v1.query.answer_question",
side_effect=PoolError("connection pool exhausted"),
):
resp = client.post("/api/v1/query", json={"question": "Any question here?"})
assert resp.status_code == 503
assert resp.headers.get("Retry-After") == "2"
# ---------------------------------------------------------------------------
# corpus_version re-resolution (ingest after startup, no restart)
# ---------------------------------------------------------------------------
def test_query_reresolves_corpus_version_when_none(client: TestClient):
"""If startup found an empty corpus, a later request re-resolves and serves."""
from app.rag.schemas import QueryResponse
client.app.state.corpus_version = None # simulate empty corpus at startup
with patch("app.api.v1.query.resolve_corpus_version", return_value="v9") as mock_resolve, \
patch("app.api.v1.query.answer_question") as mock_answer:
mock_answer.return_value = QueryResponse(answer="ok", citations=[], latency_ms=1)
resp = client.post("/api/v1/query", json={"question": "A valid question?"})
assert resp.status_code == 200
mock_resolve.assert_called_once()
assert client.app.state.corpus_version == "v9" # cached back
assert mock_answer.call_args.kwargs["corpus_version"] == "v9"
def test_query_still_503_when_corpus_empty_after_reresolve(client: TestClient):
"""If the corpus is still empty after re-resolving, return 503 (not crash)."""
client.app.state.corpus_version = None
with patch("app.api.v1.query.resolve_corpus_version", return_value=None):
resp = client.post("/api/v1/query", json={"question": "A valid question?"})
assert resp.status_code == 503