178 lines
7.5 KiB
Python
178 lines
7.5 KiB
Python
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()
|