PMFlow/backend/tests/test_ai_pmo_qa_http_mainlin...

205 lines
7.2 KiB
Python

from types import SimpleNamespace
import pytest
from fastapi import FastAPI
from httpx import ASGITransport, AsyncClient
from app.api.router import api_router
from app.api.ai_pmo_api import routes_core
from app.api.rag_api import routes_query
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() -> FastAPI:
app = FastAPI()
app.include_router(routes_query.router)
app.include_router(routes_core.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,
name="测试用户",
username="tester",
)
return app
@pytest.mark.asyncio
async def test_http_rag_ask_readonly_mainline_contract(monkeypatch) -> None:
captured: dict[str, object] = {}
class FakeKernel:
def __init__(self, db, tenant_id: int):
_ = db
captured["tenant_id"] = tenant_id
async def process_ingress(self, **kwargs):
captured["ingress_kwargs"] = kwargs
return {
"handled": True,
"status": "answered",
"intent": "query.read_only",
"direct_reply_required": True,
"direct_reply_text": "当前主链状态正常。",
"rag_result": {"answer": "当前主链状态正常。", "processing_stage": "qa"},
}
monkeypatch.setattr(routes_query, "AIPMOKernelService", FakeKernel)
app = _build_test_app()
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://test") as client:
response = await client.post(
"/rag/ask",
json={
"question": "当前主链状态?",
"asked_via": "pmflow_manual",
"action_mode": "read",
},
)
assert response.status_code == 200
payload = response.json()
assert payload["code"] == 0
assert "当前主链状态正常" in str((payload.get("data") or {}).get("answer") or "")
ingress_kwargs = captured["ingress_kwargs"]
assert isinstance(ingress_kwargs, dict)
assert ingress_kwargs["source_system"] == "pmflow_manual"
rag_request_context = ingress_kwargs["ingress_options"]["rag_request_context"]
assert rag_request_context["asked_via"] == "pmflow_manual"
assert rag_request_context["action_mode"] == "read"
assert rag_request_context["message_intent"] is None
@pytest.mark.asyncio
async def test_http_rag_ask_mixed_mainline_contract(monkeypatch) -> None:
captured: dict[str, object] = {}
class FakeKernel:
def __init__(self, db, tenant_id: int):
_ = db
captured["tenant_id"] = tenant_id
async def process_ingress(self, **kwargs):
captured["ingress_kwargs"] = kwargs
return {
"handled": True,
"status": "answered",
"intent": "query.read_only",
"direct_reply_required": True,
"direct_reply_text": "[回答] 进展稳定\n[已收到需求] 新增任务确认流",
"rag_result": {"answer": "[回答] 进展稳定\n[已收到需求] 新增任务确认流"},
}
monkeypatch.setattr(routes_query, "AIPMOKernelService", FakeKernel)
app = _build_test_app()
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://test") as client:
response = await client.post(
"/rag/ask",
json={
"question": "顺便问进展并新增任务确认流",
"asked_via": "pmflow_manual",
"action_mode": "read",
"message_intent": "mixed",
"mixed_signal_part": "新增任务确认流",
},
)
assert response.status_code == 200
payload = response.json()
assert payload["code"] == 0
assert "[回答]" in str((payload.get("data") or {}).get("answer") or "")
ingress_kwargs = captured["ingress_kwargs"]
assert isinstance(ingress_kwargs, dict)
rag_request_context = ingress_kwargs["ingress_options"]["rag_request_context"]
assert rag_request_context["message_intent"] == "mixed"
assert rag_request_context["mixed_signal_part"] == "新增任务确认流"
@pytest.mark.asyncio
async def test_http_ai_pmo_process_text_draft_pending_confirm_mainline(monkeypatch) -> None:
captured: dict[str, object] = {}
class FakeKernel:
def __init__(self, db, tenant_id: int):
_ = db
captured["tenant_id"] = tenant_id
async def process_ingress(self, **kwargs):
captured["ingress_kwargs"] = kwargs
return {
"handled": True,
"status": "draft_pending_confirm",
"classification": "command",
"direct_reply_required": True,
"direct_reply_text": "草稿已生成,请确认。",
"draft_version": 2,
}
monkeypatch.setattr(routes_core, "AIPMOKernelService", FakeKernel)
app = _build_test_app()
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://test") as client:
response = await client.post(
"/ai-pmo/process-text",
json={
"message_text": "请创建任务并先给草稿",
"sender": "产品经理A",
"source_message_id": "manual-msg-100",
"project_id": 12,
},
)
assert response.status_code == 200
payload = response.json()
assert payload["code"] == 0
data = payload["data"]
assert data["status"] == "draft_pending_confirm"
kwargs = captured["ingress_kwargs"]
assert isinstance(kwargs, dict)
assert kwargs["source_system"] == "pmflow_manual"
assert kwargs["message_text"] == "请创建任务并先给草稿"
assert kwargs["project_hint_id"] == 12
@pytest.mark.asyncio
async def test_http_ai_pmo_work_items_list_smoke(monkeypatch) -> None:
class FakeResult:
def all(self):
return []
class FakeDB:
async def execute(self, *_args, **_kwargs):
return FakeResult()
async def _fake_get_db():
yield FakeDB()
app = _build_test_app()
app.dependency_overrides[get_db] = _fake_get_db
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://test") as client:
response = await client.get("/ai-pmo/work-items", params={"limit": 18})
assert response.status_code == 200
payload = response.json()
assert payload["code"] == 0
assert payload["data"]["items"] == []
def test_ai_pmo_api_router_uses_single_prefix() -> None:
route_paths = {str(getattr(route, "path", "")).strip() for route in api_router.routes}
assert "/ai-pmo/work-items" in route_paths
assert "/ai-pmo/process-text" in route_paths
assert "/ai-pmo/ai-pmo/work-items" not in route_paths
assert "/ai-pmo/ai-pmo/process-text" not in route_paths