PMFlow/backend/tests/test_ai_pmo_ingress_mainlin...

252 lines
9.0 KiB
Python

from types import SimpleNamespace
import pytest
from fastapi import HTTPException
from app.api.ai_pmo_api import routes_core
from app.api.ai_pmo_api.schemas import PMOProcessTextRequest
from app.services.ai_pmo.ai_pmo_kernel_ingress_flow import mixin_entry
from app.services.ai_pmo.ai_pmo_typed_executor_execute import mixin_execute_query_project
@pytest.mark.asyncio
async def test_process_text_route_uses_pmflow_manual_ingress(monkeypatch) -> None:
captured: dict[str, object] = {}
class FakeKernel:
def __init__(self, db, tenant_id: int):
captured["init_db"] = db
captured["init_tenant_id"] = tenant_id
async def process_ingress(self, **kwargs):
captured["ingress_kwargs"] = kwargs
return {
"handled": True,
"status": "answered",
"classification": "question",
"direct_reply_required": True,
"direct_reply_text": "当前主链正常。",
}
monkeypatch.setattr(routes_core, "AIPMOKernelService", FakeKernel)
request = PMOProcessTextRequest(
message_text="现在进展怎么样?",
sender="产品经理A",
source_message_id="manual-1",
project_id=11,
)
current_user = SimpleNamespace(name="ignored", username="ignored")
response = await routes_core.process_text(
request=request,
current_user=current_user,
db=object(),
tenant_id=5,
)
assert response["code"] == 0
data = response["data"]
assert data["status"] == "answered"
kwargs = captured["ingress_kwargs"]
assert isinstance(kwargs, dict)
assert kwargs["source_system"] == "pmflow_manual"
assert kwargs["source_message_id"] == "manual-1"
assert kwargs["message_text"] == "现在进展怎么样?"
assert kwargs["sender"] == "产品经理A"
assert kwargs["channel_ref"] == "pmflow_manual"
assert kwargs["chat_type"] == "manual"
assert kwargs["project_hint_id"] == 11
@pytest.mark.asyncio
async def test_process_text_route_rejects_unhandled_result(monkeypatch) -> None:
class FakeKernel:
def __init__(self, db, tenant_id: int):
_ = (db, tenant_id)
async def process_ingress(self, **kwargs):
_ = kwargs
return {"handled": False, "reason": "kernel_not_handled"}
monkeypatch.setattr(routes_core, "AIPMOKernelService", FakeKernel)
request = PMOProcessTextRequest(message_text="hello")
current_user = SimpleNamespace(name="u", username="u")
with pytest.raises(HTTPException) as exc_info:
await routes_core.process_text(
request=request,
current_user=current_user,
db=object(),
tenant_id=1,
)
assert exc_info.value.status_code == 400
assert str(exc_info.value.detail) == "kernel_not_handled"
@pytest.mark.asyncio
async def test_prepare_mixed_sidecar_context_passes_mixed_contract(monkeypatch) -> None:
captured: dict[str, object] = {}
class FakeAdaptiveRAGService:
def __init__(self, db, tenant_id: int):
captured["tenant_id"] = tenant_id
_ = db
async def classify_intent(self, text: str):
captured["classified_text"] = text
return {
"intent": "mixed",
"question_part": "现在进展如何",
"statement_part": "新增任务确认流",
}
async def answer_question(self, **kwargs):
captured["answer_kwargs"] = kwargs
return {"answer": "[回答] 进展稳定\n[已收到需求] 新增任务确认流"}
class FakeKernel(mixin_entry.KernelIngressFlowEntryMixin):
def __init__(self) -> None:
self.db = object()
self.tenant_id = 9
monkeypatch.setattr(mixin_entry, "AdaptiveRAGService", FakeAdaptiveRAGService)
kernel = FakeKernel()
clean_text, mixed_meta = await kernel._prepare_mixed_sidecar_context(
source_system="pmflow_manual",
source_message_id="manual-msg-2",
channel_ref="pmflow-chat",
message_text="顺便问下现在进展如何,并且新增任务确认流",
project_hint_id=22,
)
assert clean_text == "新增任务确认流"
assert mixed_meta["mixed_intent"] is True
assert mixed_meta["mixed_question_part"] == "现在进展如何"
assert mixed_meta["mixed_signal_part"] == "新增任务确认流"
assert "[回答] 进展稳定" in str(mixed_meta.get("mixed_sidecar_reply") or "")
answer_kwargs = captured["answer_kwargs"]
assert isinstance(answer_kwargs, dict)
assert answer_kwargs["asked_via"] == "pmflow_manual"
assert answer_kwargs["message_intent"] == "mixed"
assert answer_kwargs["mixed_signal_part"] == "新增任务确认流"
assert answer_kwargs["conversation_key"] == "manual-msg-2"
@pytest.mark.asyncio
async def test_process_ingress_prioritizes_draft_pending_confirm(monkeypatch) -> None:
class FakeKernel(mixin_entry.KernelIngressFlowEntryMixin):
def __init__(self) -> None:
self.db = object()
self.tenant_id = 1
self.draft_called = False
async def _prepare_ingress_envelope(self, **kwargs):
_ = kwargs
return {
"early_result": None,
"clean_text": "确认",
"synthetic_message_id": "s-msg-1",
"closure_payload": {},
"ingest_event": SimpleNamespace(id=100),
"operator": SimpleNamespace(id=8),
"work_item": SimpleNamespace(id=55),
"session": SimpleNamespace(last_intent=mixin_entry.STATE_DRAFT_PENDING_CONFIRM),
}
async def _maybe_handle_draft_confirmation_ingress(self, **kwargs):
self.draft_called = True
_ = kwargs
return {
"handled": True,
"status": "answered",
"classification": "command",
"direct_reply_required": True,
"direct_reply_text": "已确认草稿并继续执行。",
}
def _is_short_confirmation_message(self, **kwargs):
_ = kwargs
return False
async def _prepare_mixed_sidecar_context(self, **kwargs):
_ = kwargs
raise AssertionError("draft_pending_confirm flow should bypass mixed sidecar precheck")
kernel = FakeKernel()
result = await kernel.process_ingress(
source_system="feishu",
source_message_id="feishu-1",
message_text="确认",
sender="tester",
channel_ref="chat-1",
source_sender_open_id="ou_xxx",
)
assert kernel.draft_called is True
assert result["status"] == "answered"
assert "草稿" in result["direct_reply_text"]
@pytest.mark.asyncio
async def test_execute_query_intent_honors_rag_request_context(monkeypatch) -> None:
captured: dict[str, object] = {}
class FakeAdaptiveRAGService:
def __init__(self, db, tenant_id: int):
_ = db
captured["tenant_id"] = tenant_id
async def answer_question(self, **kwargs):
captured["answer_kwargs"] = kwargs
return {"answer": "来自统一 ingress 的回答", "session_id": 7}
class FakeExecutor(mixin_execute_query_project.ExecuteQueryProjectMixin):
def __init__(self) -> None:
self.db = object()
self.tenant_id = 3
monkeypatch.setattr(mixin_execute_query_project, "AdaptiveRAGService", FakeAdaptiveRAGService)
executor = FakeExecutor()
intent = SimpleNamespace(
project_id=12,
metadata={
"rag_request_context": {
"asked_by": 101,
"asked_via": "pmflow_manual",
"project_id": 19,
"source_chat_id": "chat-abc",
"session_id": 55,
"force_new_session": True,
"thinking_mode": "deep",
"action_mode": "read_write",
"sync_requirement": True,
"message_intent": "mixed",
"mixed_signal_part": "新增任务确认流",
}
},
)
result = await executor._execute_query_intent(
intent=intent,
message_text="请同步更新状态",
operator_user_id=999,
source_system="feishu",
source_message_id="msg-1",
)
assert result["status"] == "succeeded"
assert "统一 ingress" in result["answer"]
answer_kwargs = captured["answer_kwargs"]
assert isinstance(answer_kwargs, dict)
assert answer_kwargs["asked_by"] == 101
assert answer_kwargs["asked_via"] == "pmflow_manual"
assert answer_kwargs["project_id"] == 19
assert answer_kwargs["source_chat_id"] == "chat-abc"
assert answer_kwargs["session_id"] == 55
assert answer_kwargs["force_new_session"] is True
assert answer_kwargs["thinking_mode"] == "deep"
assert answer_kwargs["action_mode"] == "read_write"
assert answer_kwargs["sync_requirement"] is True
assert answer_kwargs["message_intent"] == "mixed"
assert answer_kwargs["mixed_signal_part"] == "新增任务确认流"