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