PMFlow/backend/tests/test_query_route_governance...

192 lines
7.6 KiB
Python

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"