Spaces:
Sleeping
Sleeping
| """Q&A endpoint tests.""" | |
| import pytest | |
| def mock_llm(monkeypatch): | |
| """Mock Gemini answer generation.""" | |
| async def fake_answer(*args, **kwargs): | |
| return { | |
| "answer": "Test answer", | |
| "confidence": "High", | |
| "sources": [], | |
| "response_time_ms": 100, | |
| "model": "test", | |
| } | |
| monkeypatch.setattr("app.services.llm_service.llm_service.answer_question", fake_answer) | |
| def mock_vector_search(monkeypatch): | |
| """Mock vector search to avoid Chroma/model dependencies.""" | |
| async def fake_search(*args, **kwargs): | |
| return [ | |
| { | |
| "text": "Pump P-202 has vibration findings.", | |
| "metadata": {"filename": "demo.txt", "chunk_index": 0, "page_number": 3, "section_header": "Maintenance"}, | |
| "distance": 0.1, | |
| "id": "chunk-1", | |
| } | |
| ] | |
| monkeypatch.setattr("app.services.vector_store.vector_store.search", fake_search) | |
| monkeypatch.setattr("app.routers.query.vector_store.search", fake_search) | |
| async def test_ask_empty_question_validation(client) -> None: | |
| """Empty questions are rejected by request validation.""" | |
| response = await client.post("/api/v1/query/ask", json={"question": ""}) | |
| assert response.status_code == 422 | |
| async def test_ask_valid_question(client, mock_llm, mock_vector_search) -> None: | |
| """A valid question returns an answer.""" | |
| response = await client.post("/api/v1/query/ask", json={"question": "What about P-202?"}) | |
| assert response.status_code == 200 | |
| body = response.json() | |
| assert "Trust Summary" in body["answer"] | |
| assert "Test answer" in body["answer"] | |
| assert body["trust_summary"]["engine"] == "Knowledge Decay Engine" | |
| assert body["trust_summary"]["sources"] == 1 | |
| async def test_query_history(client) -> None: | |
| """History endpoint returns query list.""" | |
| response = await client.get("/api/v1/query/history") | |
| assert response.status_code == 200 | |
| assert "queries" in response.json() | |
| async def test_search_chunks(client, mock_vector_search) -> None: | |
| """Raw chunk search returns chunks.""" | |
| response = await client.get("/api/v1/query/search-chunks?query=pump") | |
| assert response.status_code == 200 | |
| assert "chunks" in response.json() | |
| async def test_ask_returns_page_aware_citation(client, mock_llm, mock_vector_search) -> None: | |
| """Retrieved filename, page, and section survive the API citation contract.""" | |
| response = await client.post("/api/v1/query/ask", json={"question": "What about P-202?"}) | |
| assert response.status_code == 200 | |
| source = response.json()["sources"][0] | |
| assert source["filename"] == "demo.txt" | |
| assert source["page_number"] == 3 | |
| assert source["section"] == "Maintenance" | |
| assert source["freshness_score"] is not None | |
| async def test_ask_exposes_low_freshness_trust_gate(client, mock_llm, monkeypatch) -> None: | |
| """Stale review metadata reaches the final trust summary.""" | |
| async def stale_search(*args, **kwargs): | |
| return [ | |
| { | |
| "text": "Pump P-201 maintenance procedure. Last reviewed: 2022-01-01. Verify revision before use.", | |
| "metadata": {"filename": "P-201 Procedure.pdf", "chunk_index": 0, "section_header": "Maintenance"}, | |
| "distance": 0.05, | |
| "id": "stale-chunk", | |
| } | |
| ] | |
| monkeypatch.setattr("app.services.vector_store.vector_store.search", stale_search) | |
| monkeypatch.setattr("app.routers.query.vector_store.search", stale_search) | |
| response = await client.post("/api/v1/query/ask", json={"question": "Can I safely follow this procedure?"}) | |
| assert response.status_code == 200 | |
| summary = response.json()["trust_summary"] | |
| assert summary["risk"] in {"High", "Critical"} | |
| assert summary["knowledge_decay"] >= 60 | |
| assert "Trust Gate" in response.json()["answer"] | |
| async def test_ask_returns_p201_graph_context(client, mock_llm, mock_vector_search) -> None: | |
| """P-201 judge prompt returns connected graph assets for the frontend card.""" | |
| response = await client.post( | |
| "/api/v1/query/ask", | |
| json={"question": "Show all equipment connected to Pump P-201 and cite every source."}, | |
| ) | |
| assert response.status_code == 200 | |
| body = response.json() | |
| mentioned = set(body["equipment_mentioned"]) | |
| assert "P-201" in mentioned | |
| assert {"XV-201", "M-201", "PT-201"} & mentioned | |
| assert body["graph_context"] | |
| assert body["trust_summary"]["graph_assets"] >= 2 | |
| async def test_ask_degrades_when_vector_search_fails(client, mock_llm, monkeypatch) -> None: | |
| """A vector-store outage should not turn Ask into HTTP 500.""" | |
| async def failing_search(*args, **kwargs): | |
| raise RuntimeError("vector store unavailable") | |
| monkeypatch.setattr("app.services.vector_store.vector_store.search", failing_search) | |
| monkeypatch.setattr("app.routers.query.vector_store.search", failing_search) | |
| response = await client.post("/api/v1/query/ask", json={"question": "What about P-201?"}) | |
| assert response.status_code == 200 | |
| body = response.json() | |
| assert "Trust Summary" in body["answer"] | |
| assert body["sources"] == [] | |
| assert body["trust_summary"]["risk"] == "Critical" | |
| async def test_ask_degrades_when_query_log_write_fails(client, mock_llm, mock_vector_search, monkeypatch) -> None: | |
| """A query-history write failure should not block the answer response.""" | |
| from sqlalchemy.ext.asyncio import AsyncSession | |
| async def failing_commit(self): | |
| raise RuntimeError("database write unavailable") | |
| monkeypatch.setattr(AsyncSession, "commit", failing_commit) | |
| response = await client.post("/api/v1/query/ask", json={"question": "What about P-202?"}) | |
| assert response.status_code == 200 | |
| body = response.json() | |
| assert body["query_id"] | |
| assert "Test answer" in body["answer"] |