import pytest from app.services.integrations.feishu_integration_service.mixin_receive_prepare import ( FeishuReceivePrepareMixin, ) from app.services.integrations.feishu_integration_service.service import FeishuIntegrationService @pytest.mark.asyncio async def test_handle_message_receive_ignores_bot_sender() -> None: service = FeishuIntegrationService(db=object()) result = await service._handle_message_receive( { "message": {"message_id": "m-bot"}, "sender": {"sender_type": "bot"}, "chat": {"chat_id": "c1"}, } ) assert result["code"] == 0 assert result["msg"] == "Ignore bot/app message" @pytest.mark.asyncio async def test_handle_message_receive_routes_empty_text_branch(monkeypatch) -> None: service = FeishuIntegrationService(db=object()) called: dict[str, object] = {} def _fake_build_receive_context(*, event, state_trace_id): # noqa: ARG001 return { "ingress_source": "feishu", "message": {"message_id": "m-empty", "message_type": "post"}, "sender": {"sender_name": "测试用户"}, "message_id": "m-empty", "sender_type": "user", "raw_message_content": "", "message_content": "", "chat_id": "chat-empty", "mentioned_bot_names": ["PMFlow"], "sender_open_id": "ou-empty", "perf_t0": None, } async def _fake_record_ingress_trace(**kwargs): called["trace"] = kwargs async def _fake_reply_for_empty_text_ingress(**kwargs): called["empty_reply"] = kwargs return True monkeypatch.setattr(service, "_build_receive_context", _fake_build_receive_context) monkeypatch.setattr(service, "_record_ingress_trace", _fake_record_ingress_trace) monkeypatch.setattr(service, "_reply_for_empty_text_ingress", _fake_reply_for_empty_text_ingress) result = await service._handle_message_receive({"type": "im.message.receive_v1"}) assert result["code"] == 0 assert result["msg"] == "No text content" assert called["trace"]["message_id"] == "m-empty" assert called["empty_reply"]["chat_id"] == "chat-empty" def test_classify_message_scene_does_not_infer_execute_from_raw_text_only() -> None: scene = FeishuIntegrationService._classify_message_scene( capture_result={ "status": "answered", "classification": "question", "intent": "question", }, message_content="帮我创建项目和信号源", ) assert scene == "qa" @pytest.mark.asyncio async def test_handle_message_receive_orchestrates_capture_and_finalize(monkeypatch) -> None: service = FeishuIntegrationService(db=object()) called: dict[str, object] = {} def _fake_build_receive_context(*, event, state_trace_id): # noqa: ARG001 return { "ingress_source": "long_connection", "message": {"message_id": "m-1", "message_type": "text"}, "sender": {"sender_name": "原始用户名"}, "sender_id": {"open_id": "ou-1"}, "message_id": "m-1", "sender_type": "user", "raw_message_content": "hello", "message_content": "hello", "chat_id": "chat-1", "chat_type": "group", "captured_at": None, "mentioned_bot_names": [], "sender_open_id": "ou-1", "perf_t0": 1.2, "message_attachments": [], "occurred_at_iso": None, } async def _fake_record_ingress_trace(**kwargs): called["trace"] = kwargs async def _fake_resolve_sender_display_name(*, sender, sender_id): # noqa: ARG001 return "解析后用户名" def _fake_normalize_message_attachments_from_context(*, context): # noqa: ARG001 return [] async def _fake_capture_and_persist_from_ingress(**kwargs): called["capture"] = kwargs return { "status": "captured", "classification": "signal", "linked_project_ids": [11], "auto_tasks": [], "ai_trace_id": "trace-1", } async def _fake_finalize_receive_result(**kwargs): called["finalize"] = kwargs return {"code": 0, "msg": "success", "data": {"scene": "execute"}} monkeypatch.setattr(service, "_build_receive_context", _fake_build_receive_context) monkeypatch.setattr(service, "_record_ingress_trace", _fake_record_ingress_trace) monkeypatch.setattr(service, "_resolve_sender_display_name", _fake_resolve_sender_display_name) monkeypatch.setattr(service, "_normalize_message_attachments_from_context", _fake_normalize_message_attachments_from_context) monkeypatch.setattr(service, "_capture_and_persist_from_ingress", _fake_capture_and_persist_from_ingress) monkeypatch.setattr(service, "_finalize_receive_result", _fake_finalize_receive_result) result = await service._handle_message_receive({"type": "im.message.receive_v1"}) assert result["code"] == 0 assert result["data"]["scene"] == "execute" assert called["capture"]["chat_id"] == "chat-1" assert called["capture"]["sender_name"] == "解析后用户名" assert called["finalize"]["chat_type"] == "group" def test_extract_thread_context_reads_top_level_reply_fields() -> None: event = { "thread_id": "th-1", "root_id": "root-1", "parent_id": "reply-1", "message": { "message_id": "msg-1", "content": "{\"text\":\"hello\"}", }, } context = FeishuReceivePrepareMixin._extract_thread_context( event=event, message=event["message"], message_payload={"text": "hello", "attachments": [], "message_type": "text"}, ) assert context == { "thread_id": "th-1", "root_message_id": "root-1", "reply_to_message_id": "reply-1", } def test_extract_thread_context_reads_nested_reply_fields() -> None: event = { "message": { "message_id": "msg-1", "meta": { "conversation": { "thread_id": "th-nested", "root_message_id": "root-nested", "reply_to_message_id": "reply-nested", } }, "content": "{\"text\":\"hello\"}", } } context = FeishuReceivePrepareMixin._extract_thread_context( event=event, message=event["message"], message_payload={"text": "hello", "attachments": [], "message_type": "text"}, ) assert context == { "thread_id": "th-nested", "root_message_id": "root-nested", "reply_to_message_id": "reply-nested", }