| """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, |
| ) |
|
|
|
|
| |
| |
| |
|
|
| 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"] |
| assert isinstance(body["citations"], list) |
| assert "latency_ms" in body |
|
|
|
|
| |
| |
| |
|
|
| 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 |
|
|
|
|
| |
| |
| |
|
|
| 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" |
|
|
|
|
| |
| |
| |
|
|
| 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" |
|
|
|
|
| |
| |
| |
|
|
| 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 |
|
|
| 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" |
| 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 |
|
|