"""Unit tests for Feishu/Lark channel adapter. Tests the normalizer, formatter, cards, session helpers, and verify modules. """ from yuxi.channels.adapters.feishu.adapter import FeishuAdapter from yuxi.channels.adapters.feishu.cards import build_feishu_card, build_feishu_text_content, TEXT_CHUNK_LIMIT from yuxi.channels.adapters.feishu.formatter import format_outbound from yuxi.channels.adapters.feishu.normalizer import ( extract_attachments, normalize_inbound, ) from yuxi.channels.adapters.feishu.session import generate_feishu_chat_id, resolve_feishu_chat_type from yuxi.channels.adapters.feishu.verify import verify_feishu_signature from yuxi.channels.models import ChannelStatus, ChannelType, ChatType, EventType, MessageType CHANNEL_ID = "feishu" CHANNEL_TYPE = ChannelType.FEISHU class TestFeishuNormalizer: def test_dm_text_message(self): raw = { "event": { "sender": {"sender_id": {"open_id": "ou_abc123"}}, "message": { "message_id": "om_msg001", "chat_id": "oc_dm001", "chat_type": "p2p", "msg_type": "text", "content": '{"text": "Hello"}', }, }, "event_type": "im.message.receive_v1", } msg = normalize_inbound(CHANNEL_ID, CHANNEL_TYPE, raw) assert msg.chat_type == ChatType.DIRECT assert msg.event_type == EventType.MESSAGE_RECEIVED assert msg.message_type == MessageType.TEXT assert msg.identity.channel_chat_id == "feishu:dm:ou_abc123" assert msg.identity.channel_user_id == "ou_abc123" assert msg.identity.channel_message_id == "om_msg001" def test_group_message_with_mentions(self): raw = { "event": { "sender": {"sender_id": {"open_id": "ou_user2"}}, "message": { "message_id": "om_msg002", "chat_id": "oc_group001", "chat_type": "group", "msg_type": "text", "content": '{"text": "@bot hello"}', "mentions": [{"key": "ou_bot", "name": "AI助手"}], }, }, "event_type": "im.message.receive_v1", } msg = normalize_inbound(CHANNEL_ID, CHANNEL_TYPE, raw) assert msg.chat_type == ChatType.GROUP assert msg.identity.channel_chat_id == "feishu:group:oc_group001" assert msg.mentions is not None assert "ou_bot" in msg.mentions.mentioned_user_ids def test_card_action_event(self): raw = { "event": { "sender": {"sender_id": {"open_id": "ou_user3"}}, "message": { "message_id": "om_msg003", "chat_id": "oc_dm001", "chat_type": "p2p", "msg_type": "interactive", }, }, "event_type": "card.action.trigger", } msg = normalize_inbound(CHANNEL_ID, CHANNEL_TYPE, raw) assert msg.event_type == EventType.CARD_ACTION assert msg.message_type == MessageType.CARD def test_bot_added_event(self): raw = { "event": { "sender": {"sender_id": {"open_id": "ou_admin"}}, "message": {"chat_id": "oc_group002", "chat_type": "group"}, }, "event_type": "im.chat.member.bot.added_v1", } msg = normalize_inbound(CHANNEL_ID, CHANNEL_TYPE, raw) assert msg.event_type == EventType.BOT_ADDED def test_bot_removed_event(self): raw = { "event": { "sender": {"sender_id": {"open_id": "ou_admin"}}, "message": {"chat_id": "oc_group002", "chat_type": "group"}, }, "event_type": "im.chat.member.bot.deleted_v1", } msg = normalize_inbound(CHANNEL_ID, CHANNEL_TYPE, raw) assert msg.event_type == EventType.BOT_REMOVED def test_thread_message(self): raw = { "event": { "sender": {"sender_id": {"open_id": "ou_user4"}}, "message": { "message_id": "om_msg006", "chat_id": "oc_group001", "chat_type": "group", "msg_type": "text", "content": '{"text": "reply"}', "root_id": "om_root001", "parent_id": "om_parent001", }, }, "event_type": "im.message.receive_v1", } msg = normalize_inbound(CHANNEL_ID, CHANNEL_TYPE, raw) assert msg.chat_type == ChatType.THREAD assert msg.metadata.get("root_id") == "om_root001" assert msg.metadata.get("parent_id") == "om_parent001" def test_post_message_type(self): raw = { "event": { "sender": {"sender_id": {"open_id": "ou_user5"}}, "message": { "message_id": "om_msg007", "chat_id": "oc_dm002", "chat_type": "p2p", "msg_type": "post", "content": { "zh_cn": { "title": "post title", "content": [[{"tag": "text", "text": "rich text"}]], } }, }, }, "event_type": "im.message.receive_v1", } msg = normalize_inbound(CHANNEL_ID, CHANNEL_TYPE, raw) assert msg.message_type == MessageType.TEXT assert "rich text" in msg.content def test_image_message_type(self): raw = { "event": { "sender": {"sender_id": {"open_id": "ou_user6"}}, "message": { "message_id": "om_msg008", "chat_id": "oc_dm001", "chat_type": "p2p", "msg_type": "image", }, }, "event_type": "im.message.receive_v1", } msg = normalize_inbound(CHANNEL_ID, CHANNEL_TYPE, raw) assert msg.message_type == MessageType.IMAGE def test_audio_message_type(self): raw = { "event": { "sender": {"sender_id": {"open_id": "ou_user7"}}, "message": { "message_id": "om_msg009", "chat_id": "oc_dm001", "chat_type": "p2p", "msg_type": "audio", }, }, "event_type": "im.message.receive_v1", } msg = normalize_inbound(CHANNEL_ID, CHANNEL_TYPE, raw) assert msg.message_type == MessageType.AUDIO def test_file_message_type(self): raw = { "event": { "sender": {"sender_id": {"open_id": "ou_user8"}}, "message": { "message_id": "om_msg010", "chat_id": "oc_dm001", "chat_type": "p2p", "msg_type": "file", }, }, "event_type": "im.message.receive_v1", } msg = normalize_inbound(CHANNEL_ID, CHANNEL_TYPE, raw) assert msg.message_type == MessageType.FILE def test_image_attachment_extraction(self): raw = { "event": { "sender": {"sender_id": {"open_id": "ou_user1"}}, "message": { "message_id": "om_img001", "chat_id": "oc_dm001", "chat_type": "p2p", "msg_type": "image", "content": '{"image_key": "img_abc123"}', }, }, "event_type": "im.message.receive_v1", } msg = normalize_inbound(CHANNEL_ID, CHANNEL_TYPE, raw) assert msg.content == "[图片]" assert len(msg.attachments) == 1 assert msg.attachments[0].type == "image" import json parsed = json.loads(msg.attachments[0].file_id) assert parsed["file_key"] == "img_abc123" assert parsed["file_type"] == "image" def test_file_attachment_extraction(self): raw = { "event": { "sender": {"sender_id": {"open_id": "ou_user2"}}, "message": { "message_id": "om_file001", "chat_id": "oc_dm001", "chat_type": "p2p", "msg_type": "file", "content": '{"file_key": "file_xyz789", "file_name": "report.pdf", "file_size": 1024}', }, }, "event_type": "im.message.receive_v1", } msg = normalize_inbound(CHANNEL_ID, CHANNEL_TYPE, raw) assert msg.content == "[文件: report.pdf]" assert len(msg.attachments) == 1 assert msg.attachments[0].type == "file" assert msg.attachments[0].filename == "report.pdf" assert msg.attachments[0].size_bytes == 1024 def test_audio_attachment_extraction(self): raw = { "event": { "sender": {"sender_id": {"open_id": "ou_user3"}}, "message": { "message_id": "om_audio001", "chat_id": "oc_dm001", "chat_type": "p2p", "msg_type": "audio", "content": '{"file_key": "audio_key_001"}', }, }, "event_type": "im.message.receive_v1", } msg = normalize_inbound(CHANNEL_ID, CHANNEL_TYPE, raw) assert msg.content == "[语音]" assert len(msg.attachments) == 1 assert msg.attachments[0].type == "audio" def test_video_attachment_extraction(self): raw = { "event": { "sender": {"sender_id": {"open_id": "ou_user4"}}, "message": { "message_id": "om_video001", "chat_id": "oc_dm001", "chat_type": "p2p", "msg_type": "video", "content": '{"image_key": "vid_cover_001", "file_key": "vid_file_001"}', }, }, "event_type": "im.message.receive_v1", } msg = normalize_inbound(CHANNEL_ID, CHANNEL_TYPE, raw) assert msg.content == "[视频]" assert len(msg.attachments) == 2 types = {a.type for a in msg.attachments} assert types == {"image", "video"} cover = [a for a in msg.attachments if a.type == "image"][0] assert cover.metadata.get("role") == "cover" def test_sticker_attachment_extraction(self): raw = { "event": { "sender": {"sender_id": {"open_id": "ou_user5"}}, "message": { "message_id": "om_sticker001", "chat_id": "oc_dm001", "chat_type": "p2p", "msg_type": "sticker", "content": '{"file_key": "sticker_key_001"}', }, }, "event_type": "im.message.receive_v1", } msg = normalize_inbound(CHANNEL_ID, CHANNEL_TYPE, raw) assert msg.content == "[贴纸]" assert len(msg.attachments) == 1 assert msg.attachments[0].type == "sticker" def test_text_message_no_attachments(self): raw = { "event": { "sender": {"sender_id": {"open_id": "ou_user6"}}, "message": { "message_id": "om_text001", "chat_id": "oc_dm001", "chat_type": "p2p", "msg_type": "text", "content": '{"text": "hello"}', }, }, "event_type": "im.message.receive_v1", } msg = normalize_inbound(CHANNEL_ID, CHANNEL_TYPE, raw) assert msg.attachments == [] def test_extract_attachments_image(self): event = { "message": { "message_id": "om_123", "msg_type": "image", "content": '{"image_key": "img_abc"}', } } attachments = extract_attachments(event) assert len(attachments) == 1 assert attachments[0].type == "image" assert attachments[0].file_id def test_extract_attachments_file(self): event = { "message": { "message_id": "om_456", "msg_type": "file", "content": '{"file_key": "file_xyz", "file_name": "doc.pdf", "file_size": 2048}', } } attachments = extract_attachments(event) assert len(attachments) == 1 assert attachments[0].type == "file" assert attachments[0].filename == "doc.pdf" assert attachments[0].size_bytes == 2048 def test_extract_attachments_empty_for_text(self): event = { "message": { "message_id": "om_789", "msg_type": "text", "content": '{"text": "hello"}', } } assert extract_attachments(event) == [] def test_extract_attachments_media_combined(self): event = { "message": { "message_id": "om_media001", "msg_type": "media", "content": '{"items": [' '{"msg_type": "image", "content": "{\\"image_key\\": \\"img_1\\"}"},' '{"msg_type": "image", "content": "{\\"image_key\\": \\"img_2\\"}"}' "]}", } } attachments = extract_attachments(event) assert len(attachments) == 2 assert all(a.type == "image" for a in attachments) def test_unknown_event_type_defaults_to_message_received(self): raw = { "event": { "sender": {"sender_id": {"open_id": "ou_user9"}}, "message": { "message_id": "om_msg011", "chat_id": "oc_dm001", "chat_type": "p2p", "msg_type": "text", "content": '{"text": "test"}', }, }, "event_type": "unknown.event.type", } msg = normalize_inbound(CHANNEL_ID, CHANNEL_TYPE, raw) assert msg.event_type == EventType.MESSAGE_RECEIVED def test_no_mentions(self): raw = { "event": { "sender": {"sender_id": {"open_id": "ou_user10"}}, "message": { "message_id": "om_msg012", "chat_id": "oc_dm001", "chat_type": "p2p", "msg_type": "text", "content": '{"text": "plain text"}', }, }, "event_type": "im.message.receive_v1", } msg = normalize_inbound(CHANNEL_ID, CHANNEL_TYPE, raw) assert msg.mentions is not None assert msg.mentions.mentioned_user_ids == [] def test_metadata(self): raw = { "event": { "sender": {"sender_id": {"open_id": "ou_user11"}}, "message": { "message_id": "om_msg013", "chat_id": "oc_dm001", "chat_type": "p2p", "msg_type": "text", "content": '{"text": "test"}', }, }, "event_type": "im.message.receive_v1", } msg = normalize_inbound(CHANNEL_ID, CHANNEL_TYPE, raw) assert msg.metadata.get("msg_type") == "text" assert msg.metadata.get("chat_type_raw") == "p2p" def test_bot_mentioned(self): raw = { "event": { "sender": {"sender_id": {"open_id": "ou_user12"}}, "message": { "message_id": "om_msg014", "chat_id": "oc_group001", "chat_type": "group", "msg_type": "text", "content": '{"text": "@bot help"}', "mentions": [{"key": "ou_bot123", "name": "AI助手"}], }, }, "event_type": "im.message.receive_v1", } msg = normalize_inbound(CHANNEL_ID, CHANNEL_TYPE, raw, bot_open_id="ou_bot123") assert msg.mentions is not None assert msg.mentions.is_bot_mentioned is True def test_all_users_mentioned(self): raw = { "event": { "sender": {"sender_id": {"open_id": "ou_user13"}}, "message": { "message_id": "om_msg015", "chat_id": "oc_group001", "chat_type": "group", "msg_type": "text", "content": '{"text": "@所有人 hello"}', "mentions": [{"key": "@all_users", "name": "所有人"}], }, }, "event_type": "im.message.receive_v1", } msg = normalize_inbound(CHANNEL_ID, CHANNEL_TYPE, raw) assert msg.mentions is not None assert msg.mentions.is_bot_mentioned is True def test_bot_not_mentioned(self): raw = { "event": { "sender": {"sender_id": {"open_id": "ou_user14"}}, "message": { "message_id": "om_msg016", "chat_id": "oc_group001", "chat_type": "group", "msg_type": "text", "content": '{"text": "@someone hello"}', "mentions": [{"key": "ou_other", "name": "Someone"}], }, }, "event_type": "im.message.receive_v1", } msg = normalize_inbound(CHANNEL_ID, CHANNEL_TYPE, raw, bot_open_id="ou_bot123") assert msg.mentions is not None assert msg.mentions.is_bot_mentioned is False class TestFeishuFormatter: def test_basic_format(self): fmt = format_outbound("Hello", chat_type="private") assert fmt["content"] == "Hello" assert fmt["chat_type"] == "private" def test_format_with_buttons(self): fmt = format_outbound( "Click me", chat_type="private", metadata={"buttons": [{"text": "OK", "action": "confirm"}]}, ) assert fmt["buttons"] == [{"text": "OK", "action": "confirm"}] def test_format_with_thread_id(self): fmt = format_outbound("Reply", chat_type="group", metadata={"thread_id": "om_thread_123"}) assert fmt["thread_id"] == "om_thread_123" class TestFeishuCards: def test_text_content(self): result = build_feishu_text_content("Hello") assert result == {"text": "Hello"} def test_basic_card(self): card = build_feishu_card("Hello World", title="Test") assert card["header"]["template"] == "blue" assert card["header"]["title"]["content"] == "Test" assert len(card["elements"]) >= 1 assert "Hello World" in card["elements"][0]["content"] def test_card_with_buttons(self): card = build_feishu_card("Click me", buttons=[{"text": "OK", "action": "ok"}]) has_action = any(elem.get("tag") == "action" for elem in card["elements"]) assert has_action def test_card_with_streaming(self): card = build_feishu_card("Loading", streaming=True) assert "回复中" in card["header"]["title"]["content"] def test_text_chunk_limit(self): assert TEXT_CHUNK_LIMIT == 30000 def test_card_with_images(self): card = build_feishu_card("Look at this", images=["img_key_001", "img_key_002"]) img_elements = [e for e in card["elements"] if e.get("tag") == "img"] assert len(img_elements) == 2 assert img_elements[0]["img_key"] == "img_key_001" assert img_elements[1]["img_key"] == "img_key_002" def test_card_with_files(self): card = build_feishu_card( "Files attached", files=[{"name": "report.pdf", "url": "https://example.com/report.pdf"}], ) markdown_elements = [e for e in card["elements"] if e.get("tag") == "markdown"] file_md = [e for e in markdown_elements if "report.pdf" in e.get("content", "")] assert len(file_md) == 1 def test_card_with_files_no_url(self): card = build_feishu_card( "Files attached", files=[{"name": "data.csv"}], ) markdown_elements = [e for e in card["elements"] if e.get("tag") == "markdown"] file_md = [e for e in markdown_elements if "data.csv" in e.get("content", "")] assert len(file_md) == 1 def test_card_with_note(self): card = build_feishu_card("Hello", note="仅供参考") note_elements = [e for e in card["elements"] if e.get("tag") == "note"] assert len(note_elements) == 1 assert note_elements[0]["elements"][0]["content"] == "仅供参考" def test_card_with_all_features(self): card = build_feishu_card( "Main content", title="综合卡片", images=["img_1"], files=[{"name": "data.csv", "url": "https://example.com/data.csv"}], buttons=[{"text": "确认", "action": "ok"}], url_unfurl=["https://example.com"], note="此消息由AI生成", streaming=False, ) tags = {e.get("tag") for e in card["elements"]} assert "img" in tags assert "markdown" in tags assert "action" in tags assert "note" in tags assert card["header"]["title"]["content"] == "综合卡片" class TestFeishuSession: def test_dm_chat_id(self): assert generate_feishu_chat_id(raw_chat_id="ou_123", chat_type="direct") == "feishu:dm:ou_123" def test_group_chat_id(self): assert generate_feishu_chat_id(raw_chat_id="oc_456", chat_type="group") == "feishu:group:oc_456" def test_dm_chat_type(self): assert resolve_feishu_chat_type("ou_123") == "direct" def test_group_chat_type(self): assert resolve_feishu_chat_type("oc_456") == "group" def test_thread_chat_type(self): assert resolve_feishu_chat_type("oc_thread_123") == "group" def test_thread_chat_type_by_parent_id(self): assert resolve_feishu_chat_type("oc_456") == "group" def test_default_chat_type(self): assert resolve_feishu_chat_type("bt_unknown") == "group" class TestFeishuVerify: def test_empty_headers_fail_closed(self): assert verify_feishu_signature({}, b"") is False def test_missing_signature_headers(self): headers = {"X-Lark-Request-Timestamp": "123"} assert verify_feishu_signature(headers, b"") is False def test_valid_signature_with_encrypt_key_and_body(self): encrypt_key = "test_encrypt_key_32_bytes_long_" timestamp = "1234567890" nonce = "abcdefgh" body = b'{"challenge":"test_challenge_value","token":"v_token","type":"url_verification"}' import hashlib raw = f"{timestamp}{nonce}{encrypt_key}".encode("utf-8") + body expected_signature = hashlib.sha256(raw).hexdigest() headers = { "X-Lark-Request-Timestamp": timestamp, "X-Lark-Request-Nonce": nonce, "X-Lark-Signature": expected_signature, } assert verify_feishu_signature(headers, body, encrypt_key) is True def test_invalid_signature_with_wrong_encrypt_key(self): encrypt_key = "test_encrypt_key_32_bytes_long_" wrong_key = "wrong_encrypt_key_32_bytes_xxxx_" timestamp = "1234567890" nonce = "abcdefgh" body = b'{"challenge":"test_challenge_value"}' import hashlib raw = f"{timestamp}{nonce}{wrong_key}".encode("utf-8") + body fake_signature = hashlib.sha256(raw).hexdigest() headers = { "X-Lark-Request-Timestamp": timestamp, "X-Lark-Request-Nonce": nonce, "X-Lark-Signature": fake_signature, } assert verify_feishu_signature(headers, body, encrypt_key) is False def test_signature_verification_fails_with_wrong_body(self): encrypt_key = "test_encrypt_key_32_bytes_long_" timestamp = "1234567890" nonce = "abcdefgh" original_body = b'{"challenge":"original"}' tampered_body = b'{"challenge":"tampered"}' import hashlib raw = f"{timestamp}{nonce}{encrypt_key}".encode("utf-8") + original_body signature = hashlib.sha256(raw).hexdigest() headers = { "X-Lark-Request-Timestamp": timestamp, "X-Lark-Request-Nonce": nonce, "X-Lark-Signature": signature, } assert verify_feishu_signature(headers, tampered_body, encrypt_key) is False def test_signature_case_insensitive_headers(self): encrypt_key = "test_encrypt_key_32_bytes_long_" timestamp = "1234567890" nonce = "abcdefgh" body = b'{"challenge":"test"}' import hashlib raw = f"{timestamp}{nonce}{encrypt_key}".encode("utf-8") + body signature = hashlib.sha256(raw).hexdigest() headers = { "x-lark-request-timestamp": timestamp, "x-lark-request-nonce": nonce, "x-lark-signature": signature, } # Headers are accessed with case-sensitive keys by the implementation # This tests the actual behavior - headers dict keys should match exactly assert verify_feishu_signature(headers, body, encrypt_key) is False class TestFeishuSessionDefensive: def test_generate_chat_id_with_missing_sender(self): chat_id = generate_feishu_chat_id(raw_chat_id="oc_group123", chat_type="group") assert chat_id == "feishu:group:oc_group123" def test_generate_chat_id_with_none_sender(self): chat_id = generate_feishu_chat_id(raw_chat_id="oc_group123", chat_type="group", sender_id=None) assert chat_id == "feishu:group:oc_group123" def test_generate_chat_id_with_empty_sender_dict(self): chat_id = generate_feishu_chat_id(raw_chat_id="oc_group123", chat_type="group") assert chat_id == "feishu:group:oc_group123" def test_generate_chat_id_full_event(self): chat_id = generate_feishu_chat_id( raw_chat_id="oc_chat456", chat_type="group", root_id="om_root123", sender_id="ou_sender456", ) assert chat_id == "feishu:group:om_root123:oc_chat456" class TestFeishuNormalizerValidation: CHANNEL_ID = "feishu" CHANNEL_TYPE = ChannelType.FEISHU def test_invalid_raw_payload_not_dict(self): import pytest with pytest.raises(ValueError, match="Invalid raw_payload"): normalize_inbound(self.CHANNEL_ID, self.CHANNEL_TYPE, "not_a_dict") def test_invalid_raw_payload_missing_event(self): import pytest with pytest.raises(ValueError, match="Invalid raw_payload"): normalize_inbound(self.CHANNEL_ID, self.CHANNEL_TYPE, {"not_event": "value"}) def test_invalid_raw_payload_none(self): import pytest with pytest.raises(ValueError, match="Invalid raw_payload"): normalize_inbound(self.CHANNEL_ID, self.CHANNEL_TYPE, None) class TestFeishuAnalysisModule: def test_orchestrator_import(self): from yuxi.channels.adapters.feishu.analysis import FeishuAnalyzer analyzer = FeishuAnalyzer() assert analyzer is not None assert len(analyzer._agents) == 3 def test_web_research_agent(self): from yuxi.channels.adapters.feishu.analysis.sub_agents import SubAgentWebResearch agent = SubAgentWebResearch() assert agent is not None def test_functional_research_agent(self): from yuxi.channels.adapters.feishu.analysis.sub_agents import SubAgentFunctionalResearch agent = SubAgentFunctionalResearch() assert agent is not None def test_issue_research_agent(self): from yuxi.channels.adapters.feishu.analysis.sub_agents import SubAgentIssueResearch agent = SubAgentIssueResearch() assert agent is not None def test_report_generator_import(self): from yuxi.channels.adapters.feishu.analysis.report_generator import ( GapReport, generate_gap_report, ) assert GapReport is not None assert generate_gap_report is not None class TestReplyMessage: async def test_reply_message_no_sdk(self, monkeypatch): import yuxi.channels.adapters.feishu.send as send_mod monkeypatch.setattr(send_mod, "HAS_LARK_SDK", False) result = await send_mod.reply_message(None, "msg_123", "hello") assert result.success is False assert "not available" in result.error async def test_reply_message_text(self): from unittest.mock import AsyncMock, MagicMock mock_client = MagicMock() mock_resp = MagicMock() mock_resp.success.return_value = True mock_resp.data = {"message_id": "reply_msg_456"} mock_client.im.v1.message.reply = AsyncMock(return_value=mock_resp) from yuxi.channels.adapters.feishu.send import reply_message result = await reply_message(mock_client, "msg_123", "hello") assert result.success is True assert result.message_id == "reply_msg_456" async def test_reply_message_with_buttons(self): from unittest.mock import AsyncMock, MagicMock mock_client = MagicMock() mock_resp = MagicMock() mock_resp.success.return_value = True mock_resp.data = {"message_id": "reply_card_msg"} mock_client.im.v1.message.reply = AsyncMock(return_value=mock_resp) from yuxi.channels.adapters.feishu.send import reply_message buttons = [{"text": "OK", "value": "confirm"}] result = await reply_message(mock_client, "msg_123", "Choose an option", buttons=buttons) assert result.success is True assert result.message_id == "reply_card_msg" async def test_reply_message_api_error(self): from unittest.mock import AsyncMock, MagicMock mock_client = MagicMock() mock_resp = MagicMock() mock_resp.success.return_value = False mock_resp.msg = "permission denied" mock_client.im.v1.message.reply = AsyncMock(return_value=mock_resp) from yuxi.channels.adapters.feishu.send import reply_message result = await reply_message(mock_client, "msg_123", "hello") assert result.success is False assert "permission denied" in result.error class TestTokenRefresh: def test_token_fields_initialized(self): adapter = FeishuAdapter(config={"app_id": "test", "app_secret": "test"}) assert adapter._token_expire_at == 0 assert adapter._token_refresh_task is None def test_disconnect_cancels_token_refresh(self): import asyncio adapter = FeishuAdapter(config={"app_id": "test", "app_secret": "test"}) adapter._status = ChannelStatus.CONNECTED adapter._ws_task = asyncio.Future() adapter._token_refresh_task = asyncio.Future() loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) try: loop.run_until_complete(adapter.disconnect()) finally: loop.close() assert adapter._token_refresh_task is None assert adapter._status == ChannelStatus.DISCONNECTED def test_receive_method_removed(self): adapter = FeishuAdapter(config={"app_id": "test", "app_secret": "test"}) assert "receive" not in type(adapter).__dict__ class TestFeishuAdapterLongPoll: def test_start_long_poll_no_sdk(self, monkeypatch): import pytest import yuxi.channels.adapters.feishu.adapter as adapter_mod monkeypatch.setattr(adapter_mod, "HAS_LARK_SDK", False) adapter = FeishuAdapter(config={"app_id": "test", "app_secret": "test"}) with pytest.raises(Exception) as exc_info: import asyncio loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) try: loop.run_until_complete(adapter._start_long_poll()) finally: loop.close() assert "not installed" in str(exc_info.value)