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_build_capability_reply_returns_stable_text(self) -> None: reply = FeishuIntegrationService._build_capability_reply("别乱创建信号,说明你是谁") self.assertIsNotNone(reply) self.assertIn("PMFlow 的项目协同助手", str(reply)) 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_handle_message_receive_capability_reply_overrides_execute_direct_reply(self) -> None: service = FeishuIntegrationService(db=None) # type: ignore[arg-type] event = { "message": { "message_id": "om_capability_guard", "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": "captured", "classification": "signal", "direct_reply_required": True, "direct_reply_text": "我在等 Interactive Card 的内容。", "results": [{"route_status": "queued_for_decision"}], } ), ), ): asyncio.run(service._handle_message_receive(event)) reply_mock.assert_awaited_once() sent_text = str(reply_mock.await_args.kwargs.get("text") or "") self.assertIn("PMFlow 的项目协同助手", sent_text) self.assertNotIn("Interactive Card", sent_text) 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()