205 lines
7.2 KiB
Python
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
|