96 lines
3.9 KiB
Python
96 lines
3.9 KiB
Python
"""Phase 1 tests: RAG query endpoint.
|
|
|
|
Covers:
|
|
- POST /api/v1/query question → retrieve → LLM → bullet-point response
|
|
- Strict RAG prompt enforcement (only use retrieved context)
|
|
- Bullet-point response format
|
|
- Source metadata inclusion
|
|
"""
|
|
import pytest
|
|
from fastapi.testclient import TestClient
|
|
from unittest.mock import MagicMock, AsyncMock, patch
|
|
|
|
|
|
class TestQuery:
|
|
|
|
@pytest.fixture
|
|
def client(self):
|
|
from app.main import app
|
|
return TestClient(app)
|
|
|
|
def test_query_returns_bullets(self, client):
|
|
"""Should return bullet-point answer with source metadata."""
|
|
with patch("app.routers.query.QueryDecomposer") as mock_decomposer_class:
|
|
mock_decomposer = MagicMock()
|
|
mock_decomposer.decompose = AsyncMock(return_value=["test", "keywords"])
|
|
mock_decomposer_class.return_value = mock_decomposer
|
|
|
|
with patch("app.routers.query.RAGService") as mock_rag_class:
|
|
mock_rag = MagicMock()
|
|
mock_rag.retrieve.return_value = [
|
|
("chunk one", {"filename": "test.pdf"}, 0.1),
|
|
("chunk two", {"filename": "test.pdf"}, 0.2),
|
|
]
|
|
mock_rag.generate_response = AsyncMock(return_value="- Bullet point answer\n- Another point")
|
|
mock_rag_class.return_value = mock_rag
|
|
|
|
with patch("app.routers.query.RelevanceFilter") as mock_filter_class:
|
|
mock_filter = MagicMock()
|
|
mock_filter.filter = AsyncMock(return_value=[
|
|
("chunk one", {"filename": "test.pdf"}),
|
|
("chunk two", {"filename": "test.pdf"}),
|
|
])
|
|
mock_filter_class.return_value = mock_filter
|
|
|
|
response = client.post(
|
|
"/api/v1/query",
|
|
json={"question": "What is this about?"},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert "extracted_questions" in data
|
|
assert data["extracted_questions"] == ["test", "keywords"]
|
|
assert "answer" in data
|
|
assert "- Bullet point answer" in data["answer"]
|
|
assert "sources" in data
|
|
assert len(data["sources"]) == 2
|
|
assert data["sources"][0]["filename"] == "test.pdf"
|
|
|
|
def test_query_no_relevant_chunks(self, client):
|
|
"""Should handle case when no relevant chunks found."""
|
|
with patch("app.routers.query.QueryDecomposer") as mock_decomposer_class:
|
|
mock_decomposer = MagicMock()
|
|
mock_decomposer.decompose = AsyncMock(return_value=["test"])
|
|
mock_decomposer_class.return_value = mock_decomposer
|
|
|
|
with patch("app.routers.query.RAGService") as mock_rag_class:
|
|
mock_rag = MagicMock()
|
|
mock_rag.retrieve.return_value = [
|
|
("chunk one", {"filename": "test.pdf"}, 0.1),
|
|
]
|
|
mock_rag.generate_response = AsyncMock(return_value="I could not find any relevant information.")
|
|
mock_rag_class.return_value = mock_rag
|
|
|
|
with patch("app.routers.query.RelevanceFilter") as mock_filter_class:
|
|
mock_filter = MagicMock()
|
|
mock_filter.filter = AsyncMock(return_value=[])
|
|
mock_filter_class.return_value = mock_filter
|
|
|
|
response = client.post(
|
|
"/api/v1/query",
|
|
json={"question": "What is this about?"},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["extracted_questions"] == ["test"]
|
|
assert "could not find" in data["answer"].lower()
|
|
assert data["sources"] == []
|
|
|
|
def test_query_no_question(self, client):
|
|
"""Should reject request without question."""
|
|
response = client.post("/api/v1/query", json={})
|
|
|
|
assert response.status_code == 422
|