from types import SimpleNamespace import pytest from fastapi import FastAPI from httpx import ASGITransport, AsyncClient from app.api.rag_api import routes_route_governance from app.api.router import api_router from app.core.database import get_db from app.dependencies.auth import get_current_user from app.dependencies.tenant import get_tenant_id def _build_test_app(*, role: str = "ADMIN") -> FastAPI: app = FastAPI() app.include_router(routes_route_governance.router) async def _fake_get_db(): yield object() app.dependency_overrides[get_db] = _fake_get_db app.dependency_overrides[get_tenant_id] = lambda: 9 app.dependency_overrides[get_current_user] = lambda: SimpleNamespace( id=101, tenant_id=9, username="tester", role=role, ) return app def test_rag_api_router_exposes_query_route_governance_routes() -> None: route_paths = {str(getattr(route, "path", "")).strip() for route in api_router.routes} assert "/rag/query-route-governance/summary" in route_paths assert "/rag/query-route-governance/items" in route_paths @pytest.mark.asyncio async def test_query_route_governance_summary_requires_manager_role() -> None: app = _build_test_app(role="MEMBER") transport = ASGITransport(app=app) async with AsyncClient(transport=transport, base_url="http://test") as client: response = await client.get("/rag/query-route-governance/summary") assert response.status_code == 403 assert response.json()["detail"] == "Manager permission required" @pytest.mark.asyncio async def test_query_route_governance_summary_returns_manager_payload(monkeypatch) -> None: captured: dict[str, object] = {} class FakeService: def __init__(self, db, tenant_id: int): _ = db captured["tenant_id"] = tenant_id async def query_route_governance_summary(self, **kwargs): captured["summary_kwargs"] = kwargs return { "window_days": 30, "project_id": 17, "kpis": { "total_records": 4, "mismatch_count": 1, "clarify_count": 2, "negative_feedback_count": 1, "internal_count": 1, "external_count": 2, "uncertain_count": 1, }, "route_modes": [{"key": "external", "label": "外部实时查询", "count": 2}], "decision_origins": [{"key": "reply_anchor", "label": "reply_anchor", "count": 2}], } monkeypatch.setattr(routes_route_governance, "AdaptiveRAGService", FakeService) app = _build_test_app(role="ADMIN") transport = ASGITransport(app=app) async with AsyncClient(transport=transport, base_url="http://test") as client: response = await client.get("/rag/query-route-governance/summary", params={"project_id": 17, "window_days": 30}) assert response.status_code == 200 payload = response.json() assert payload["code"] == 0 assert payload["data"]["kpis"]["mismatch_count"] == 1 assert captured["tenant_id"] == 9 assert captured["summary_kwargs"] == {"project_id": 17, "window_days": 30} @pytest.mark.asyncio async def test_query_route_governance_items_and_detail_return_contract(monkeypatch) -> None: class FakeService: def __init__(self, db, tenant_id: int): _ = db, tenant_id async def list_query_route_governance_records(self, **kwargs): assert kwargs["mismatch_only"] is True return { "items": [ { "id": 7, "qa_record_id": 70, "project_id": 17, "session_id": 12, "session_title": "Alpha 会话", "question_text": "这个项目怎么样?", "route_mode_predicted": "internal", "route_mode_effective": "external", "decision_origin": "reply_anchor", "reply_mode": "clarify", "feedback_rating": "negative", "feedback_helpfulness": "could_be_better", "is_route_mismatch": True, "is_clarify": True, "anchor_summary": "source_chat:pmflow-project-17", "answer_mode": "clarify", "grounding_status": "insufficient_evidence", "asked_via": "pmflow_manual", "created_at": "2026-04-17T12:00:00+08:00", "updated_at": "2026-04-17T12:00:00+08:00", } ], "total": 1, "limit": 20, "offset": 0, } async def get_query_route_governance_detail(self, **kwargs): assert kwargs == {"record_id": 7, "project_id": 17} return { "record": { "id": 7, "qa_record_id": 70, "project_id": 17, "session_id": 12, "session_title": "Alpha 会话", "question_text": "这个项目怎么样?", "route_mode_predicted": "internal", "route_mode_effective": "external", "decision_origin": "reply_anchor", "reply_mode": "clarify", "feedback_rating": "negative", "feedback_helpfulness": "could_be_better", "is_route_mismatch": True, "is_clarify": True, "anchor_summary": "source_chat:pmflow-project-17", "answer_mode": "clarify", "grounding_status": "insufficient_evidence", "asked_via": "pmflow_manual", "created_at": "2026-04-17T12:00:00+08:00", "updated_at": "2026-04-17T12:00:00+08:00", }, "qa_record": { "id": 70, "question": "这个项目怎么样?", "answer": "外部实时查询:证据不足。", }, "route_memory": { "reply_contract": {"mode": "clarify"}, "probe_snapshot": {"contract": {"source_policy": "uncertain"}}, "query_profile": {"subject_focus": "project"}, "feedback_outcome": {"rating": "negative"}, "decision_origin": "reply_anchor", }, } monkeypatch.setattr(routes_route_governance, "AdaptiveRAGService", FakeService) app = _build_test_app(role="ADMIN") transport = ASGITransport(app=app) async with AsyncClient(transport=transport, base_url="http://test") as client: list_response = await client.get( "/rag/query-route-governance/items", params={"project_id": 17, "mismatch_only": True, "limit": 20, "offset": 0}, ) detail_response = await client.get( "/rag/query-route-governance/items/7", params={"project_id": 17}, ) assert list_response.status_code == 200 list_payload = list_response.json() assert list_payload["code"] == 0 assert list_payload["data"]["total"] == 1 assert list_payload["data"]["items"][0]["is_route_mismatch"] is True assert detail_response.status_code == 200 detail_payload = detail_response.json() assert detail_payload["code"] == 0 assert detail_payload["data"]["route_memory"]["feedback_outcome"]["rating"] == "negative"