"""Quick validation test for DingDing adapter modules. Uses direct imports to avoid triggering full yuxi.channels init chain. """ # noqa: E402 from yuxi.channels.adapters.dingding.normalizer import normalize_inbound from yuxi.channels.models import ChannelType, EventType, MessageType, ChatType channel_id = "dingding" channel_type = ChannelType.DINGDING # Test 1: DM text message raw_dm = { "senderId": "manager1234", "senderNick": "zhangsan", "isGroupChat": False, "conversationId": "cid123456", "msgId": "msg001", "msgtype": "text", "text": {"content": "你好,AI助手"}, } msg = normalize_inbound(channel_id, channel_type, raw_dm) assert msg.chat_type == ChatType.DIRECT, f"Expected DIRECT, got {msg.chat_type}" assert msg.event_type == EventType.MESSAGE_RECEIVED assert msg.message_type == MessageType.TEXT assert msg.content == "你好,AI助手" assert msg.identity.channel_chat_id == "dm_manager1234" assert msg.identity.channel_user_id == "manager1234" assert msg.identity.channel_message_id == "msg001" print("Test 1 (DM text) PASSED") # Test 2: Group message with @mention raw_group = { "senderId": "user456", "senderNick": "lisi", "isGroupChat": True, "conversationId": "cid6KeBBLovxxxx", "msgId": "msg002", "msgtype": "text", "text": {"content": "@AI助手 今天天气怎么样"}, "atUsers": [{"dingtalkId": "dingbot001"}, {"dingtalkId": "user789"}], } msg2 = normalize_inbound(channel_id, channel_type, raw_group) assert msg2.chat_type == ChatType.GROUP assert msg2.identity.channel_chat_id == "group_cid6KeBBLovxxxx" assert msg2.mentions is not None assert "dingbot001" in msg2.mentions.mentioned_user_ids assert "user789" in msg2.mentions.mentioned_user_ids assert msg2.metadata["isGroupChat"] is True print("Test 2 (Group message with @mention) PASSED") # Test 3: Group message @all raw_at_all = { "senderId": "admin001", "senderNick": "admin", "isGroupChat": True, "conversationId": "cid_grp_admin", "msgId": "msg003", "msgtype": "text", "text": {"content": "@all 重要通知"}, "isAtAll": True, } msg3 = normalize_inbound(channel_id, channel_type, raw_at_all) assert msg3.chat_type == ChatType.GROUP assert msg3.mentions is not None assert "@all" in msg3.mentions.mentioned_user_ids print("Test 3 (Group message @all) PASSED") # Test 4: Markdown message type raw_md = { "senderId": "user111", "isGroupChat": False, "conversationId": "cid_dm_111", "msgId": "msg004", "msgtype": "markdown", "text": {"content": "**bold**"}, } msg4 = normalize_inbound(channel_id, channel_type, raw_md) assert msg4.message_type == MessageType.TEXT, f"Expected TEXT for markdown, got {msg4.message_type}" assert msg4.content == "**bold**" print("Test 4 (Markdown message type) PASSED") # Test 4b: ActionCard message type raw_ac = { "senderId": "user555", "isGroupChat": True, "conversationId": "cid_ac_grp", "msgId": "msg004b", "msgtype": "actionCard", } msg4b = normalize_inbound(channel_id, channel_type, raw_ac) assert msg4b.message_type == MessageType.CARD, f"Expected CARD for actionCard, got {msg4b.message_type}" print("Test 4b (ActionCard message type) PASSED") # Test 5: Image message (now returns media description) raw_img = { "senderId": "user222", "isGroupChat": True, "conversationId": "cid_img_grp", "msgId": "msg005", "msgtype": "image", "text": {}, } msg5 = normalize_inbound(channel_id, channel_type, raw_img) assert msg5.message_type == MessageType.IMAGE assert msg5.content == "[图片]" print("Test 5 (Image message) PASSED") # Test 5b: Image message with downloadCode attachment raw_img_dl = { "senderId": "user555", "isGroupChat": True, "conversationId": "cid_img_dl", "msgId": "msg005b", "msgtype": "image", "downloadCode": "dc12345", } # ruff: noqa: E402 from yuxi.channels.adapters.dingding.normalizer import extract_attachments # noqa: E402 att_img = extract_attachments(raw_img_dl) assert len(att_img) == 1 assert att_img[0].type == "image" assert att_img[0].file_id == "dc12345" print("Test 5b (Image attachment extraction) PASSED") # Test 6: Voice (audio) message raw_voice = { "senderId": "user333", "isGroupChat": False, "conversationId": "cid_dm_333", "msgId": "msg006", "msgtype": "voice", } msg6 = normalize_inbound(channel_id, channel_type, raw_voice) assert msg6.message_type == MessageType.AUDIO assert msg6.content == "[语音]" print("Test 6 (Voice message) PASSED") # Test 6b: Video message raw_video = { "senderId": "user444", "isGroupChat": False, "conversationId": "cid_dm_444", "msgId": "msg006b", "msgtype": "video", } msg6b = normalize_inbound(channel_id, channel_type, raw_video) assert msg6b.message_type == MessageType.VIDEO assert msg6b.content == "[视频]" print("Test 6b (Video message) PASSED") # Test 6c: File message with fileName raw_file = { "senderId": "user999", "isGroupChat": False, "conversationId": "cid_dm_999", "msgId": "msg006c", "msgtype": "file", "fileName": "report.pdf", } msg6c = normalize_inbound(channel_id, channel_type, raw_file) assert msg6c.message_type == MessageType.FILE assert msg6c.content == "[文件: report.pdf]" print("Test 6c (File message) PASSED") # Test 7: Session helpers # ruff: noqa: E402 from yuxi.channels.adapters.dingding.session import ( # noqa: E402 generate_dingding_chat_id, resolve_dingding_chat_type, generate_thread_key, ) dm_raw = {"senderId": "user_dm", "isGroupChat": False, "conversationId": "cid_dm"} assert generate_dingding_chat_id(dm_raw, "direct") == "dm_user_dm" grp_raw = {"senderId": "user_grp", "isGroupChat": True, "conversationId": "cid_grp"} assert generate_dingding_chat_id(grp_raw, "group") == "group_cid_grp" assert resolve_dingding_chat_type(dm_raw) == "direct" assert resolve_dingding_chat_type(grp_raw) == "group" thread_key_dm = generate_thread_key("dingding", "dm_user_dm") assert thread_key_dm == "dingding:direct:dm_user_dm" thread_key_grp = generate_thread_key("dingding", "group_cid_grp") assert thread_key_grp == "dingding:group:group_cid_grp" print("Test 7 (Session helpers) PASSED") # Test 8: Formatter # ruff: noqa: E402 from yuxi.channels.adapters.dingding.formatter import format_outbound # noqa: E402 fmt = format_outbound("Hello", chat_type="direct") assert fmt["msgKey"] == "sampleText" assert "content" in fmt["msgParam"] fmt_md = format_outbound("**bold**", chat_type="direct", metadata={"use_markdown": True}) assert fmt_md["msgKey"] == "sampleMarkdown" print("Test 8 (Formatter) PASSED") # Test 9: Cards # ruff: noqa: E402 from yuxi.channels.adapters.dingding.cards import ( # noqa: E402 build_dingding_text_payload, build_dingding_markdown_payload, build_dingding_action_card, build_dingding_feed_card, TEXT_CHUNK_LIMIT, ) text_payload = build_dingding_text_payload("Hello") assert text_payload["content"] == "Hello" md_pl = build_dingding_markdown_payload("Title", "**Bold**") assert md_pl["title"] == "Title" assert md_pl["text"] == "**Bold**" ac = build_dingding_action_card("Test", "Content", single_title="查看", single_url="https://example.com") assert ac["msg_key"] == "sampleActionCard1" assert ac["msg_param"]["singleTitle"] == "查看" ac2 = build_dingding_action_card("Multi", "Buttons", buttons=[{"title": "OK", "actionURL": "https://ok.com"}]) assert ac2["msg_key"] == "sampleActionCard2" fc = build_dingding_feed_card([{"title": "News", "messageURL": "https://news.com", "picURL": "https://img.com"}]) assert fc["msg_key"] == "sampleFeedCard" print("Test 9 (Cards) PASSED") # Test 10: Sign computation and verification # ruff: noqa: E402 from yuxi.channels.adapters.dingding.sign import compute_dingtalk_sign, verify_webhook_signature # noqa: E402 import time # noqa: E402 sign = compute_dingtalk_sign("1700000000000", "test_secret") assert sign, "Sign should not be empty" assert verify_webhook_signature({}, "") is True now_ms = str(int(time.time() * 1000)) valid_headers = { "timestamp": now_ms, "sign": compute_dingtalk_sign(now_ms, "test_secret"), } assert verify_webhook_signature(valid_headers, "test_secret") is True tampered = dict(valid_headers) tampered["sign"] = "wrong_sign" assert verify_webhook_signature(tampered, "test_secret") is False print("Test 10 (Sign) PASSED") # Test 11: Token manager # ruff: noqa: E402 from yuxi.channels.adapters.dingding.token import DingDingTokenManager # noqa: E402 tm = DingDingTokenManager("fake_appkey", "fake_secret") assert tm._access_token is None assert tm._is_valid() is False tm.invalidate() assert tm._access_token is None print("Test 11 (Token manager) PASSED") # Test 12: Streaming attributes and adapter methods # ruff: noqa: E402 from yuxi.channels.adapters.dingding.adapter import DingDingChannelAdapter # noqa: E402 assert DingDingChannelAdapter.supports_streaming is True assert "off" in DingDingChannelAdapter.streaming_modes assert "block" in DingDingChannelAdapter.streaming_modes adapter = DingDingChannelAdapter(config={"name": "test", "accounts": {"default": {}}}) assert adapter._pending_streams == {} assert adapter._stream_lock is not None print("Test 12 (Streaming attributes) PASSED") # Test 13: Card action event type mapping raw_card = { "senderId": "user777", "isGroupChat": False, "conversationId": "cid_card", "msgId": "msg_card", "event_type": "card_action", "msgtype": "actionCard", } msg_card = normalize_inbound(channel_id, channel_type, raw_card) assert msg_card.event_type == EventType.CARD_ACTION print("Test 13 (Card action event) PASSED") # Test 14: Normalizer helpers (extract_text, extract_attachments) # ruff: noqa: E402 from yuxi.channels.adapters.dingding.normalizer import extract_text # noqa: E402 assert extract_text({"msgtype": "image", "text": {}}) == "[图片]" assert extract_text({"msgtype": "voice"}) == "[语音]" assert extract_text({"msgtype": "text", "text": {"content": "hi"}}) == "hi" print("Test 14 (extract_text helper) PASSED") print(f"\nAll 14 tests PASSED! TEXT_CHUNK_LIMIT={TEXT_CHUNK_LIMIT}")