legco_ai_assistant/backend/app/test/test_phase1_query.py

98 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, patch
class TestQuery:
"""RAG query endpoint tests."""
@pytest.fixture
def client(self):
"""Create test client with mocked dependencies."""
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.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.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.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 "keywords" in data
assert data["keywords"] == ["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.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.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.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["keywords"] == ["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