PMFlow/backend/tests/test_feishu_routing_receipt...

178 lines
7.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import unittest
import asyncio
from types import SimpleNamespace
from unittest.mock import AsyncMock, patch
from app.services.feishu_integration import FeishuIntegrationService
from app.services.feishu_signal_pipeline import FeishuSignalPipelineService
from app.services.signal_capture import ProactiveSignalCaptureService
from app.services.context_cache import context_cache_service
class FeishuRoutingReceiptsTest(unittest.TestCase):
def test_normalize_incoming_text_strips_bot_mentions_and_quote_noise(self) -> None:
text = FeishuSignalPipelineService._normalize_incoming_text(
"@PMFlow\n回复:\n> 上一条消息\n[附带图片]\n麻烦看下统一通知入口"
)
self.assertEqual(text, "麻烦看下统一通知入口")
def test_pending_receipt_prefers_clarification_message(self) -> None:
reply = FeishuIntegrationService._build_execution_receipt_reply(
message_content="@PMFlow 继续处理",
capture_result={
"status": "captured",
"results": [{"route_status": "queued_for_decision"}],
"clarification_message": "当前群最近同时涉及项目#12、#18,请明确归属项目。",
},
)
self.assertEqual(reply, "当前群最近同时涉及项目#12、#18,请明确归属项目。")
def test_failed_receipt_uses_controlled_executor_message(self) -> None:
reply = FeishuIntegrationService._build_execution_receipt_reply(
message_content="@PMFlow 创建任务",
capture_result={
"status": "captured",
"results": [{"route_status": "failed"}],
},
)
self.assertEqual(reply, "未执行:受控执行器拒绝了这次动作,请补充缺失字段或检查项目上下文。")
def test_followup_confirmation_reply_uses_real_db_ids(self) -> None:
service = FeishuSignalPipelineService(db=None, tenant_id=1) # type: ignore[arg-type]
reply = service._build_followup_confirmation_reply(
target_item=SimpleNamespace(id=23, level="signal"),
confirm_result={"signal_id": "91", "project_id": "12"},
)
self.assertEqual(reply, "已继续处理:信号#91 已确认并关联项目#12。")
def test_provider_error_guard_blocks_aborted_error_text(self) -> None:
self.assertTrue(FeishuIntegrationService._looks_like_provider_error("Request was aborted."))
def test_normalize_outgoing_reply_text_replaces_provider_error(self) -> None:
self.assertEqual(
FeishuIntegrationService._normalize_outgoing_reply_text("Request was aborted."),
"系统处理中出现异常,本次未执行。请稍后重试,或补充更明确的目标、对象和动作。",
)
def test_pipeline_decorate_capture_result_sanitizes_provider_error_reply(self) -> None:
result = FeishuSignalPipelineService._decorate_capture_result(
{
"status": "answered",
"classification": "question",
"direct_reply_text": "Request was aborted.",
},
chat_type="group",
normalized_text="@PMFlow 现在状态如何",
route="group.qa_direct",
db_mutations=[],
)
self.assertEqual(
result["direct_reply_text"],
"系统处理中出现异常,本次未执行。请稍后重试,或补充更明确的目标、对象和动作。",
)
def test_handle_message_receive_skips_reply_for_duplicate_event(self) -> None:
service = FeishuIntegrationService(db=None) # type: ignore[arg-type]
event = {
"message": {
"message_id": "om_duplicate",
"message_type": "text",
"chat_id": "oc_test",
"chat_type": "group",
"content": '{"text":"@PMFlow 帮我继续处理"}',
},
"chat": {
"chat_id": "oc_test",
"chat_type": "group",
},
"sender": {
"sender_type": "user",
"sender_id": {"open_id": "ou_test"},
"sender_name": "林宇航",
},
"type": "im.message.receive_v1",
}
with (
patch.object(service, "_record_ingress_trace", AsyncMock()),
patch.object(service, "_record_message_trace", AsyncMock()),
patch.object(service, "_resolve_sender_display_name", AsyncMock(return_value="林宇航")),
patch.object(service, "_safe_reply_text", AsyncMock()) as reply_mock,
patch(
"app.services.feishu_integration.ProactiveSignalCaptureService.capture_from_feishu",
AsyncMock(
return_value={
"status": "ignored",
"reason": "duplicate_filtered",
"classification": "duplicate",
"direct_reply_required": True,
"direct_reply_text": "Request was aborted.",
}
),
),
):
asyncio.run(service._handle_message_receive(event))
reply_mock.assert_not_awaited()
def test_sync_feishu_recent_cache_backfills_missing_rag_turn(self) -> None:
service = ProactiveSignalCaptureService(db=None) # type: ignore[arg-type]
with (
patch.object(context_cache_service, "get_session_messages", AsyncMock(return_value=[])),
patch.object(context_cache_service, "append_session_messages", AsyncMock()) as append_mock,
):
asyncio.run(
service._sync_feishu_recent_cache(
tenant_id=1,
group_chat_id="oc_test",
sender="林宇航",
sender_open_id="ou_test",
message="目前医药方向值得关注什么?",
pipeline_result={
"rag_result": {"answer": "可关注创新药、器械和公共卫生。"},
"direct_reply_text": "可关注创新药、器械和公共卫生。",
},
)
)
append_mock.assert_awaited_once()
def test_sync_feishu_recent_cache_skips_duplicate_turn(self) -> None:
service = ProactiveSignalCaptureService(db=None) # type: ignore[arg-type]
with (
patch.object(
context_cache_service,
"get_session_messages",
AsyncMock(
return_value=[
{"role": "user", "content": "目前医药方向值得关注什么?"},
{"role": "assistant", "content": "可关注创新药、器械和公共卫生。"},
]
),
),
patch.object(context_cache_service, "append_session_messages", AsyncMock()) as append_mock,
):
asyncio.run(
service._sync_feishu_recent_cache(
tenant_id=1,
group_chat_id="oc_test",
sender="林宇航",
sender_open_id="ou_test",
message="目前医药方向值得关注什么?",
pipeline_result={
"rag_result": {"answer": "可关注创新药、器械和公共卫生。"},
"direct_reply_text": "可关注创新药、器械和公共卫生。",
},
)
)
append_mock.assert_not_awaited()