252 lines
9.0 KiB
Python
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"] == "新增任务确认流"
|