192 lines
7.6 KiB
Python
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"
|
|
|