legco_ai_assistant/backend/app/test/test_phase1_query.py

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 "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 = 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["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