"""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) @pytest.mark.skip(reason="Deprecated: endpoint now returns SSE stream, not JSON") 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" @pytest.mark.skip(reason="Deprecated: endpoint now returns SSE stream, not JSON") 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