"""全面的飞书模块单元测试 - 覆盖所有未充分测试的核心功能、边界条件和异常处理场景""" from __future__ import annotations import asyncio import hashlib import json import os import tempfile import time from unittest.mock import AsyncMock, MagicMock, patch import pytest from yuxi.channels.adapters.feishu.config_schema import ( _camel_to_snake, super_refine_feishu_config, validate_feishu_config, ) from yuxi.channels.adapters.feishu.policy import FeishuPolicyMatcher, PolicyAccessTracker from yuxi.channels.adapters.feishu.subagent import FeishuSubAgentManager, SubAgentLifecycle from yuxi.channels.adapters.feishu.reasoning import build_reasoning_preview_card from yuxi.channels.adapters.feishu.commands import build_synthetic_message, parse_bot_menu_event from yuxi.channels.adapters.feishu.reply_dispatcher import dispatch_render, extract_urls, should_use_card from yuxi.channels.adapters.feishu.cards import ( CARD_TEMPLATE_COLORS, PRESENTATION_TONE_MAP, build_feishu_card, build_feishu_post_content, build_feishu_text_content, is_post_format_requested, make_checkbox_group, make_date_picker, make_datetime_picker, make_image_element, make_input_field, make_overflow_menu, make_select_person, make_select_static, make_time_picker, resolve_button_type, resolve_selector_tag, resolve_template_color, ) from yuxi.channels.adapters.feishu.dedup import FeishuDedupStore from yuxi.channels.adapters.feishu.formatter import ( format_outbound, format_outbound_card, format_outbound_diagnostic_card, format_outbound_message, make_error_card, ) from yuxi.channels.adapters.feishu.media import ( MediaSizeError, recover_utf8_filename_from_latin1_header, resolve_feishu_outbound_media_kind, sanitize_filename_for_upload, validate_file_size, validate_image_size, validate_media_size, ) from yuxi.channels.adapters.feishu.normalizer import ( _escape_post_md, _extract_interactive_text, _get_i18n_text, _resolve_template_vars, extract_content, extract_mentioned_user_ids, extract_mentions, map_event_type, map_msg_type, normalize_inbound, strip_at_mentions, ) from yuxi.channels.adapters.feishu.reactions import emoji_type_to_emoji from yuxi.channels.adapters.feishu.secret_resolver import ( invalidate_secret_cache, resolve_secret, resolve_secret_with_rotation, ) from yuxi.channels.adapters.feishu.sequential import FeishuSequentialQueue from yuxi.channels.adapters.feishu.session import ( SessionMode, SessionPersistence, generate_feishu_chat_id, looks_like_feishu_id, match_feishu_acp_conversation, normalize_feishu_acp_conversation_id, normalize_feishu_target, resolve_feishu_chat_type, resolve_feishu_command_conversation, ) from yuxi.channels.adapters.feishu.stream import CharacterStreamingSession, merge_streaming_text from yuxi.channels.adapters.feishu.verify import ( check_webhook_rate_limit, decrypt_feishu_body, verify_and_decrypt_webhook, verify_feishu_signature, ) from yuxi.channels.models import ChannelType, ChatType, EventType, MessageType CHANNEL_ID = "feishu" CHANNEL_TYPE = ChannelType.FEISHU # ============================================================ # config_schema.py # ============================================================ class TestConfigSchema: def test_camel_to_snake_simple(self): assert _camel_to_snake("appId") == "app_id" assert _camel_to_snake("appSecret") == "app_secret" assert _camel_to_snake("verifyToken") == "verify_token" assert _camel_to_snake("encryptKey") == "encrypt_key" def test_camel_to_snake_single_word(self): assert _camel_to_snake("domain") == "domain" assert _camel_to_snake("platform") == "platform" def test_camel_to_snake_empty(self): assert _camel_to_snake("") == "" def test_validate_config_valid_minimal(self): errors = validate_feishu_config({"appId": "cli_123", "appSecret": "secret_456"}) assert errors == [] def test_validate_config_snake_case(self): errors = validate_feishu_config({"app_id": "cli_123", "app_secret": "secret_456"}) assert errors == [] def test_validate_config_missing_app_id(self): errors = validate_feishu_config({"appSecret": "secret_456"}) assert any("app_id" in e or "appId" in e for e in errors) def test_validate_config_missing_app_secret(self): errors = validate_feishu_config({"appId": "cli_123"}) assert any("app_secret" in e or "appSecret" in e for e in errors) def test_validate_config_empty_all(self): errors = validate_feishu_config({}) assert len(errors) >= 2 def test_validate_config_invalid_group_policy(self): errors = validate_feishu_config({"appId": "x", "appSecret": "x", "groupPolicy": "invalid"}) assert any("groupPolicy" in e for e in errors) def test_validate_config_valid_group_policies(self): for policy in ("open", "allowlist", "disabled", "allowall"): errors = validate_feishu_config({"appId": "x", "appSecret": "x", "groupPolicy": policy}) assert not any("groupPolicy" in e for e in errors) def test_validate_config_invalid_dm_policy(self): errors = validate_feishu_config({"appId": "x", "appSecret": "x", "dmPolicy": "nonexistent"}) assert any("dmPolicy" in e for e in errors) def test_validate_config_valid_dm_policies(self): for policy in ("open", "pairing", "allowlist"): errors = validate_feishu_config({"appId": "x", "appSecret": "x", "dmPolicy": policy}) assert not any("dmPolicy" in e for e in errors) def test_validate_config_accounts_missing_fields(self): errors = validate_feishu_config( {"appId": "x", "appSecret": "x", "accounts": {"acct1": {"appId": "", "appSecret": ""}}} ) assert any("acct1" in e for e in errors) def test_validate_config_accounts_valid(self): errors = validate_feishu_config( { "appId": "x", "appSecret": "x", "accounts": {"acct1": {"appId": "id1", "appSecret": "secret1"}}, } ) assert len(errors) == 0 def test_validate_config_invalid_max_media_zero(self): errors = validate_feishu_config({"appId": "x", "appSecret": "x", "mediaMaxMb": 0}) assert any("mediaMaxMb" in e for e in errors) def test_validate_config_invalid_max_media_too_high(self): errors = validate_feishu_config({"appId": "x", "appSecret": "x", "mediaMaxMb": 300}) assert any("mediaMaxMb" in e for e in errors) def test_validate_config_max_media_valid(self): errors = validate_feishu_config({"appId": "x", "appSecret": "x", "mediaMaxMb": 50}) assert not any("mediaMaxMb" in e for e in errors) def test_super_refine_allowall_conversion(self): config = {"appId": "x", "appSecret": "x", "group_policy": "allowall"} refined = super_refine_feishu_config(config) assert refined["groupPolicy"] == "open" def test_super_refine_default_account_validation(self): config = {"appId": "x", "appSecret": "x", "defaultAccount": "nonexistent", "accounts": {}} refined = super_refine_feishu_config(config) assert "defaultAccount" not in refined def test_super_refine_default_account_valid(self): config = { "appId": "x", "appSecret": "x", "defaultAccount": "acct1", "accounts": {"acct1": {"appId": "id1"}}, } refined = super_refine_feishu_config(config) assert refined.get("defaultAccount") == "acct1" def test_super_refine_webhook_warning(self): config = {"appId": "x", "appSecret": "x", "webhookPath": "/webhook", "verifyToken": "", "encryptKey": ""} refined = super_refine_feishu_config(config) assert refined["webhookPath"] == "/webhook" def test_super_refine_webhook_with_token(self): config = { "appId": "x", "appSecret": "x", "webhookPath": "/webhook", "verifyToken": "token123", } refined = super_refine_feishu_config(config) assert refined["webhookPath"] == "/webhook" def test_super_refine_account_field_inheritance(self): config = { "app_id": "top_app_id", "app_secret": "top_secret", "accounts": {"acct1": {}}, } refined = super_refine_feishu_config(config) assert refined["accounts"]["acct1"]["appId"] == "top_app_id" assert refined["accounts"]["acct1"]["appSecret"] == "top_secret" def test_super_refine_account_field_no_override(self): config = { "appId": "top_app_id", "appSecret": "top_secret", "accounts": {"acct1": {"appId": "override_id", "appSecret": "override_secret"}}, } refined = super_refine_feishu_config(config) assert refined["accounts"]["acct1"]["appId"] == "override_id" def test_super_refine_dm_policy_open_wildcard(self): config = {"appId": "x", "appSecret": "x", "dmPolicy": "open", "allowlist": ["*"]} refined = super_refine_feishu_config(config) assert refined["dmPolicy"] == "open" def test_super_refine_no_accounts(self): config = {"appId": "x", "appSecret": "x"} refined = super_refine_feishu_config(config) assert refined["appId"] == "x" # ============================================================ # policy.py # ============================================================ class TestPolicyAccessTracker: def test_record_and_get_history(self): tracker = PolicyAccessTracker() tracker.record_access("allowlist", "chat_1", True) tracker.record_access("allowlist", "chat_2", False) history = tracker.get_history("allowlist") assert len(history) == 2 assert history[0]["resource_id"] == "chat_1" assert history[0]["allowed"] is True assert history[1]["resource_id"] == "chat_2" assert history[1]["allowed"] is False def test_count_recent(self): tracker = PolicyAccessTracker() tracker.record_access("allowlist", "chat_1", True) tracker.record_access("allowlist", "chat_2", False) tracker.record_access("allowlist", "chat_3", True) allowed, denied = tracker.count_recent("allowlist", window_s=120) assert allowed == 2 assert denied == 1 def test_history_max_entries(self): tracker = PolicyAccessTracker() for i in range(150): tracker.record_access("allowlist", f"chat_{i}", True) history = tracker.get_history("allowlist") assert len(history) <= PolicyAccessTracker.MAX_HISTORY_PER_KEY def test_clear_specific_key(self): tracker = PolicyAccessTracker() tracker.record_access("allowlist", "chat_1", True) tracker.record_access("open", "chat_2", True) tracker.clear("allowlist") assert len(tracker.get_history("allowlist")) == 0 assert len(tracker.get_history("open")) == 1 def test_clear_all(self): tracker = PolicyAccessTracker() tracker.record_access("allowlist", "chat_1", True) tracker.clear() assert len(tracker.get_history("allowlist")) == 0 def test_empty_history(self): tracker = PolicyAccessTracker() assert tracker.get_history("nonexistent") == [] def test_count_recent_empty(self): tracker = PolicyAccessTracker() allowed, denied = tracker.count_recent("empty") assert allowed == 0 assert denied == 0 class TestFeishuPolicyMatcher: def test_open_policy_allows_all(self): matcher = FeishuPolicyMatcher({"groupPolicy": "open"}) allowed, _ = matcher.check_chat_access("any_chat", "group") assert allowed is True def test_allowlist_matched(self): matcher = FeishuPolicyMatcher({"groupPolicy": "allowlist", "allowFrom": ["oc_123", "oc_456"]}) allowed, _ = matcher.check_chat_access("oc_123", "group") assert allowed is True def test_allowlist_not_matched(self): matcher = FeishuPolicyMatcher({"groupPolicy": "allowlist", "allowFrom": ["oc_123"]}) allowed, msg = matcher.check_chat_access("oc_999", "group") assert allowed is False assert len(msg) > 0 def test_allowlist_empty(self): matcher = FeishuPolicyMatcher({"groupPolicy": "allowlist", "allowFrom": []}) allowed, _ = matcher.check_chat_access("oc_123", "group") assert allowed is False def test_allowlist_wildcard(self): matcher = FeishuPolicyMatcher({"groupPolicy": "allowlist", "allowFrom": ["oc_*"]}) allowed, _ = matcher.check_chat_access("oc_anything", "group") assert allowed is True def test_allowlist_question_mark_wildcard(self): matcher = FeishuPolicyMatcher({"groupPolicy": "allowlist", "allowFrom": ["oc_???"]}) allowed, _ = matcher.check_chat_access("oc_abc", "group") assert allowed is True def test_disabled_policy(self): matcher = FeishuPolicyMatcher({"groupPolicy": "disabled"}) allowed, _ = matcher.check_chat_access("oc_123", "group") assert allowed is False def test_blocklist_takes_priority(self): matcher = FeishuPolicyMatcher( {"groupPolicy": "open", "blockFrom": ["oc_blocked"], "allowFrom": ["oc_blocked"]} ) allowed, _ = matcher.check_chat_access("oc_blocked", "group") assert allowed is False def test_dm_policy_for_direct_chat(self): matcher = FeishuPolicyMatcher({"dmPolicy": "open", "groupPolicy": "disabled"}) allowed, _ = matcher.check_chat_access("ou_123", "direct") assert allowed is True def test_dm_policy_pairing(self): matcher = FeishuPolicyMatcher({"dmPolicy": "pairing"}) allowed, _ = matcher.check_chat_access("ou_123", "direct") assert allowed is False def test_custom_deny_message(self): matcher = FeishuPolicyMatcher( {"groupPolicy": "disabled", "denyMessage": "自定义拒绝消息"} ) _, msg = matcher.check_chat_access("oc_123", "group") assert msg == "自定义拒绝消息" def test_resolve_require_mention_global(self): matcher = FeishuPolicyMatcher({"requireMention": True}) assert matcher.resolve_require_mention("oc_123") is True def test_resolve_require_mention_group_override(self): matcher = FeishuPolicyMatcher( {"requireMention": True, "groups": {"oc_123": {"requireMention": False}}} ) assert matcher.resolve_require_mention("oc_123") is False def test_resolve_group_config_empty(self): matcher = FeishuPolicyMatcher({}) assert matcher.resolve_group_config("unknown") == {} def test_resolve_group_config_found(self): matcher = FeishuPolicyMatcher({"groups": {"oc_123": {"threadOnly": True}}}) cfg = matcher.resolve_group_config("oc_123") assert cfg["threadOnly"] is True def test_get_access_history(self): matcher = FeishuPolicyMatcher({"groupPolicy": "open"}) matcher.check_chat_access("chat_1", "group") history = matcher.get_access_history() assert len(history) >= 1 def test_reset_tracker(self): matcher = FeishuPolicyMatcher({"groupPolicy": "open"}) matcher.check_chat_access("chat_1", "group") matcher.reset_tracker() assert matcher.get_access_history() == [] def test_match_pattern_exact(self): assert FeishuPolicyMatcher._match_pattern("exact", "exact") is True assert FeishuPolicyMatcher._match_pattern("exact", "different") is False def test_match_pattern_no_wildcard_no_match(self): assert FeishuPolicyMatcher._match_pattern("abc123", "xyz789") is False def test_unknown_policy_defaults_deny(self): matcher = FeishuPolicyMatcher({"groupPolicy": "invalid_policy"}) allowed, _ = matcher.check_chat_access("oc_123", "group") assert allowed is False # ============================================================ # sequential.py # ============================================================ class TestFeishuSequentialQueue: @pytest.mark.asyncio async def test_run_sequential_order(self): queue = FeishuSequentialQueue(timeout_s=5) results = [] async def task(val): results.append(val) await queue.run_sequential("key1", task(1)) await queue.run_sequential("key1", task(2)) assert results == [1, 2] @pytest.mark.asyncio async def test_different_keys_concurrent(self): queue = FeishuSequentialQueue(timeout_s=5) results = [] async def task(val): await asyncio.sleep(0.01) results.append(val) await asyncio.gather( queue.run_sequential("key1", task(1)), queue.run_sequential("key2", task(2)), ) assert len(results) == 2 @pytest.mark.asyncio async def test_release_unlocks(self): queue = FeishuSequentialQueue(timeout_s=5) await queue.acquire("key_release") queue.release("key_release") executed = False async def task(): nonlocal executed executed = True await asyncio.wait_for(queue.run_sequential("key_release", task()), timeout=1) assert executed is True @pytest.mark.asyncio async def test_remove_releases_lock(self): queue = FeishuSequentialQueue(timeout_s=5) await queue.acquire("key_remove") queue.remove("key_remove") executed = False async def task(): nonlocal executed executed = True await asyncio.wait_for(queue.run_sequential("key_remove", task()), timeout=1) assert executed is True @pytest.mark.asyncio async def test_clear_releases_all(self): queue = FeishuSequentialQueue(timeout_s=5) await queue.acquire("key1") await queue.acquire("key2") queue.clear() executed = 0 async def task(): nonlocal executed executed += 1 await asyncio.wait_for(queue.run_sequential("key1", task()), timeout=1) await asyncio.wait_for(queue.run_sequential("key2", task()), timeout=1) assert executed == 2 # ============================================================ # subagent.py # ============================================================ class TestFeishuSubAgentManager: def test_start_sub_agent(self): mgr = FeishuSubAgentManager() lifecycle = mgr.start_sub_agent("agent_1", "chat_1") assert lifecycle.agent_id == "agent_1" assert lifecycle.parent_chat_id == "chat_1" assert lifecycle.status == "active" assert lifecycle.created_at > 0 def test_complete_sub_agent(self): mgr = FeishuSubAgentManager() mgr.start_sub_agent("agent_1", "chat_1") lifecycle = mgr.complete_sub_agent("agent_1", result={"output": "done"}) assert lifecycle is not None assert lifecycle.status == "completed" assert lifecycle.result.get("output") == "done" def test_complete_nonexistent(self): mgr = FeishuSubAgentManager() assert mgr.complete_sub_agent("nonexistent") is None def test_fail_sub_agent(self): mgr = FeishuSubAgentManager() mgr.start_sub_agent("agent_1", "chat_1") lifecycle = mgr.fail_sub_agent("agent_1", "timeout") assert lifecycle is not None assert lifecycle.status == "failed" assert lifecycle.error == "timeout" def test_end_sub_agent(self): mgr = FeishuSubAgentManager() mgr.start_sub_agent("agent_1", "chat_1") lifecycle = mgr.end_sub_agent("agent_1") assert lifecycle is not None assert lifecycle.status == "ended" assert mgr.get_sub_agent("agent_1") is None def test_get_sub_agent(self): mgr = FeishuSubAgentManager() mgr.start_sub_agent("agent_1", "chat_1") found = mgr.get_sub_agent("agent_1") assert found is not None assert found.agent_id == "agent_1" def test_list_active(self): mgr = FeishuSubAgentManager() mgr.start_sub_agent("agent_1", "chat_1") mgr.start_sub_agent("agent_2", "chat_1") mgr.complete_sub_agent("agent_2") active = mgr.list_active() assert len(active) == 1 assert active[0].agent_id == "agent_1" def test_list_by_parent(self): mgr = FeishuSubAgentManager() mgr.start_sub_agent("agent_1", "chat_A") mgr.start_sub_agent("agent_2", "chat_B") mgr.start_sub_agent("agent_3", "chat_A") by_parent = mgr.list_by_parent("chat_A") assert len(by_parent) == 2 assert {s.agent_id for s in by_parent} == {"agent_1", "agent_3"} def test_max_concurrent_eviction(self): mgr = FeishuSubAgentManager(max_concurrent=3) mgr.start_sub_agent("agent_1", "chat_1") mgr.start_sub_agent("agent_2", "chat_1") mgr.start_sub_agent("agent_3", "chat_1") mgr.start_sub_agent("agent_4", "chat_1") assert mgr.get_sub_agent("agent_1") is None assert mgr.get_sub_agent("agent_4") is not None def test_clean_completed(self): mgr = FeishuSubAgentManager() mgr.start_sub_agent("agent_1", "chat_1") mgr.complete_sub_agent("agent_1") lifecycle = mgr.get_sub_agent("agent_1") assert lifecycle is not None lifecycle.completed_at = time.monotonic() - 3600 removed = mgr.clean_completed(max_age_s=0) assert removed == 1 assert mgr.get_sub_agent("agent_1") is None def test_clean_not_completed(self): mgr = FeishuSubAgentManager() mgr.start_sub_agent("agent_1", "chat_1") removed = mgr.clean_completed(max_age_s=3600) assert removed == 0 assert mgr.get_sub_agent("agent_1") is not None def test_clear(self): mgr = FeishuSubAgentManager() mgr.start_sub_agent("agent_1", "chat_1") mgr.start_sub_agent("agent_2", "chat_1") mgr.clear() assert mgr.list_active() == [] def test_sub_agent_lifecycle_defaults(self): lifecycle = SubAgentLifecycle(agent_id="test", parent_chat_id="chat") assert lifecycle.status == "pending" assert lifecycle.created_at == 0.0 assert lifecycle.result == {} assert lifecycle.error == "" # ============================================================ # reasoning.py # ============================================================ class TestReasoning: def test_build_card_with_reasoning(self): card = build_reasoning_preview_card("答案内容", reasoning_text="推理过程") markdown_elements = [e for e in card["elements"] if e.get("tag") == "markdown"] assert len(markdown_elements) >= 1 content = markdown_elements[0]["content"] assert "推理中" in content assert "推理过程" in content assert "推理完成" in content assert "答案内容" in content def test_build_card_without_reasoning(self): card = build_reasoning_preview_card("答案内容", reasoning_text="") markdown_elements = [e for e in card["elements"] if e.get("tag") == "markdown"] assert len(markdown_elements) >= 1 content = markdown_elements[0]["content"] assert "推理中" not in content def test_build_card_default_title(self): card = build_reasoning_preview_card("内容") assert card["header"]["title"]["content"] == "AI 助手" # ============================================================ # commands.py # ============================================================ class TestCommands: def test_parse_bot_menu_event_valid(self): event = { "event": { "event_key": "/help", "timestamp": "1234567890", "operator": {"open_id": "ou_123"}, } } result = parse_bot_menu_event(event) assert result is not None assert result["command"] == "/help" assert result["open_id"] == "ou_123" def test_parse_bot_menu_event_no_event_key(self): event = {"event": {"operator": {"open_id": "ou_123"}}} result = parse_bot_menu_event(event) assert result is None def test_parse_bot_menu_event_empty_operator(self): event = {"event": {"event_key": "/help", "operator": {}}} result = parse_bot_menu_event(event) assert result is not None assert result["open_id"] == "" def test_build_synthetic_message(self): from yuxi.channels.models import ChannelType msg = build_synthetic_message("feishu", ChannelType.FEISHU, "ou_123", "chat_456", "help") assert msg.event_type == EventType.BOT_MENU assert msg.content == "/help" assert msg.identity.channel_user_id == "ou_123" assert msg.identity.channel_chat_id == "chat_456" assert msg.metadata["source"] == "bot_menu" # ============================================================ # reply_dispatcher.py # ============================================================ class TestReplyDispatcher: def test_should_use_card_with_buttons(self): assert should_use_card("short", buttons=[{"text": "OK"}]) is True def test_should_use_card_long_content(self): long_text = "x" * 2000 assert should_use_card(long_text) is True def test_should_use_card_code_block(self): assert should_use_card("```python\nprint('hello')\n```") is True def test_should_use_card_table(self): assert should_use_card("| col1 | col2 |\n|------|------|") is True def test_should_use_card_short_plain(self): assert should_use_card("Hello world") is False def test_dispatch_render_card_mode(self): assert dispatch_render("hello", render_mode="card") == "card" def test_dispatch_render_raw_mode(self): assert dispatch_render("hello", render_mode="raw") == "text" def test_dispatch_render_auto_long(self): assert dispatch_render("x" * 2000, render_mode="auto") == "card" def test_dispatch_render_auto_short(self): assert dispatch_render("Hello", render_mode="auto") == "text" def test_extract_urls_multiple(self): urls = extract_urls("a https://a.com b https://b.com c https://c.com d https://d.com e https://e.com f https://f.com") assert len(urls) == 5 def test_extract_urls_no_duplicates(self): urls = extract_urls("https://a.com and https://a.com") assert urls == ["https://a.com"] # ============================================================ # cards.py - 额外覆盖 # ============================================================ class TestCardsExtended: def test_resolve_template_color_known(self): assert resolve_template_color("green") == "green" assert resolve_template_color("red") == "red" def test_resolve_template_color_tone(self): assert resolve_template_color(tone="danger") == "red" assert resolve_template_color(tone="warning") == "orange" assert resolve_template_color(tone="success") == "green" def test_resolve_template_color_default(self): assert resolve_template_color() == "blue" assert resolve_template_color("unknown") == "blue" if "unknown" not in CARD_TEMPLATE_COLORS else "unknown" def test_resolve_selector_tag_valid(self): assert resolve_selector_tag("select_static") == "select_static" assert resolve_selector_tag("date_picker") == "date_picker" assert resolve_selector_tag("overflow") == "overflow" def test_resolve_selector_tag_invalid(self): assert resolve_selector_tag("invalid_tag") == "select_static" def test_make_select_static_full(self): sel = make_select_static( placeholder="请选择", options=[{"text": "选项A", "value": "a"}, {"text": "选项B", "value": "b"}], value="a", initial_option="选项A", ) assert sel["tag"] == "select_static" assert sel["placeholder"]["content"] == "请选择" assert len(sel["options"]) == 2 assert sel["value"] == "a" assert sel["initial_option"] == "选项A" def test_make_select_static_minimal(self): sel = make_select_static() assert sel["tag"] == "select_static" def test_make_select_person_full(self): sel = make_select_person(placeholder="选择成员", value="ou_123") assert sel["tag"] == "select_person" assert sel["placeholder"]["content"] == "选择成员" assert sel["value"] == "ou_123" def test_make_date_picker_minimal(self): sel = make_date_picker() assert sel["tag"] == "date_picker" def test_make_time_picker_full(self): sel = make_time_picker(placeholder="选择时间", value="12:00", initial_time="12:00") assert sel["tag"] == "time_picker" assert sel["value"] == "12:00" assert sel["initial_time"] == "12:00" def test_make_datetime_picker_full(self): sel = make_datetime_picker( placeholder="选择日期时间", value="2026-05-10 12:00", initial_datetime="2026-05-10 12:00" ) assert sel["tag"] == "datetime_picker" assert sel["value"] == "2026-05-10 12:00" def test_make_overflow_menu_full(self): sel = make_overflow_menu( options=[{"text": "编辑", "value": "edit"}, {"text": "删除", "value": "delete"}], value="edit", ) assert sel["tag"] == "overflow" assert len(sel["options"]) == 2 assert sel["value"] == "edit" def test_make_input_field_minimal(self): field = make_input_field() assert field["tag"] == "input" def test_make_input_field_multiline(self): field = make_input_field(placeholder="输入内容", value="test", multiline=True, max_length=500) assert field["tag"] == "input" assert field["placeholder"]["content"] == "输入内容" assert field["value"] == "test" assert field["multiline"] is True assert field["max_length"] == 500 def test_make_checkbox_group(self): group = make_checkbox_group( options=[{"text": "选项1", "value": "1"}, {"text": "选项2", "value": "2"}], values=["1"], ) assert group["tag"] == "checkbox" assert len(group["options"]) == 2 assert group["value"] == ["1"] def test_make_image_element(self): img = make_image_element("img_key_123", alt_text="测试图片") assert img["tag"] == "img" assert img["img_key"] == "img_key_123" assert img["alt"]["content"] == "测试图片" def test_is_post_format_requested_true(self): assert is_post_format_requested({"use_post_format": True}) is True assert is_post_format_requested({"usePostFormat": True}) is True def test_is_post_format_requested_false(self): assert is_post_format_requested(None) is False assert is_post_format_requested({}) is False assert is_post_format_requested({"other": True}) is False def test_build_feishu_post_content(self): result = build_feishu_post_content("Hello\n\nWorld") assert "zh_cn" in result assert "content" in result["zh_cn"] paragraphs = result["zh_cn"]["content"] assert len(paragraphs) > 0 def test_card_with_visible_to_operator(self): card = build_feishu_card("secret", visible_to_operator=True) assert card.get("config", {}).get("update_multi") is False def test_card_with_context_text(self): card = build_feishu_card("content", context_text="上下文信息") note_elements = [e for e in card["elements"] if e.get("tag") == "note"] assert len(note_elements) >= 1 def test_card_with_dividers(self): card = build_feishu_card("content", dividers=3) hr_elements = [e for e in card["elements"] if e.get("tag") == "hr"] assert len(hr_elements) == 3 def test_card_with_selectors(self): card = build_feishu_card( "content", selectors=[ {"tag": "date_picker", "placeholder": "选择日期", "value": "2026-01-01"} ], ) selector_elements = [e for e in card["elements"] if e.get("tag") == "date_picker"] assert len(selector_elements) == 1 # ============================================================ # session.py - 额外覆盖 # ============================================================ class TestSessionExtended: def test_session_mode_enum(self): assert SessionMode.RAW == "raw" assert SessionMode.THREAD == "thread" assert SessionMode.CHAT_RAW == "chat_raw" assert SessionMode.CHAT_RESOLVE == "chat_resolve" def test_generate_chat_id_raw_mode(self): chat_id = generate_feishu_chat_id("ou_123", "direct", mode=SessionMode.RAW) assert chat_id == "ou_123" def test_generate_chat_id_chat_raw_mode(self): chat_id = generate_feishu_chat_id("oc_456", "group", mode=SessionMode.CHAT_RAW) assert chat_id == "feishu:group:oc_456" def test_generate_chat_id_thread_mode(self): chat_id = generate_feishu_chat_id("oc_456", "group", mode=SessionMode.THREAD, root_id="om_root") assert chat_id == "feishu:thread:om_root" def test_generate_chat_id_group_topic_sender_scope(self): chat_id = generate_feishu_chat_id( "oc_456", "group", root_id="om_root", sender_id="ou_sender", scope="group_topic_sender" ) assert chat_id == "feishu:group_topic_sender:om_root:oc_456:ou_sender" def test_generate_chat_id_group_topic_scope(self): chat_id = generate_feishu_chat_id("oc_456", "group", root_id="om_root", scope="group_topic") assert chat_id == "feishu:group_topic:om_root:oc_456" def test_generate_chat_id_group_sender_scope(self): chat_id = generate_feishu_chat_id("oc_456", "group", sender_id="ou_sender", scope="group_sender") assert chat_id == "feishu:group_sender:oc_456:ou_sender" def test_generate_chat_id_direct_sender_scope(self): chat_id = generate_feishu_chat_id("ou_123", "direct", sender_id="ou_sender", scope="group_sender") assert chat_id == "feishu:dm:ou_123:ou_sender" def test_generate_chat_id_unknown_type(self): chat_id = generate_feishu_chat_id("bt_unknown", "unknown") assert chat_id.startswith("feishu:dm:") def test_looks_like_feishu_id_valid(self): assert looks_like_feishu_id("ou_123") is True assert looks_like_feishu_id("oc_456") is True assert looks_like_feishu_id("on_789") is True assert looks_like_feishu_id("om_000") is True assert looks_like_feishu_id("od_111") is True assert looks_like_feishu_id("og_222") is True assert looks_like_feishu_id("tg_333") is True def test_looks_like_feishu_id_invalid(self): assert looks_like_feishu_id("") is False assert looks_like_feishu_id("not_a_feishu_id") is False assert looks_like_feishu_id(123) is False def test_normalize_feishu_target_dm(self): assert normalize_feishu_target("ou_123") == "feishu:dm:ou_123" assert normalize_feishu_target("on_456") == "feishu:dm:on_456" def test_normalize_feishu_target_group(self): assert normalize_feishu_target("oc_789") == "feishu:group:oc_789" assert normalize_feishu_target("om_000") == "feishu:group:om_000" def test_normalize_feishu_target_already_normalized(self): assert normalize_feishu_target("feishu:dm:ou_123") == "feishu:dm:ou_123" def test_normalize_feishu_target_lark_prefix(self): assert normalize_feishu_target("lark:dm:ou_123") == "feishu:dm:ou_123" def test_normalize_feishu_target_feishu_protocol(self): assert normalize_feishu_target("feishu://dm/ou_123") == "feishu://dm/ou_123" def test_normalize_feishu_target_empty(self): assert normalize_feishu_target("") == "" def test_normalize_feishu_target_unknown(self): assert normalize_feishu_target("unknown_id") == "unknown_id" def test_normalize_acp_conv_id_feishu_prefix(self): assert normalize_feishu_acp_conversation_id("feishu:dm:ou_123") == "feishu:dm:ou_123" def test_normalize_acp_conv_id_dm(self): assert normalize_feishu_acp_conversation_id("ou_123") == "feishu:dm:ou_123" def test_normalize_acp_conv_id_group(self): assert normalize_feishu_acp_conversation_id("oc_456") == "feishu:group:oc_456" def test_match_feishu_acp_conversation(self): assert match_feishu_acp_conversation("ou_123", "feishu:dm:ou_*") is True assert match_feishu_acp_conversation("ou_123", "feishu:group:*") is False def test_resolve_feishu_command_conversation_dm(self): result = resolve_feishu_command_conversation("oc_chat", "ou_user", scope="group") assert result == "feishu:dm:ou_user" def test_resolve_feishu_command_conversation_group(self): result = resolve_feishu_command_conversation("oc_chat", "oc_group") assert result == "feishu:group:oc_chat" class TestSessionPersistence: def test_get_set_delete(self): with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f: path = f.name try: p = SessionPersistence(path) p.set("session_1", "key_a", "value_a") assert p.get("session_1", "key_a") == "value_a" p.delete("session_1", "key_a") assert p.get("session_1", "key_a") is None finally: try: os.unlink(path) except OSError: pass def test_delete_session(self): with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f: path = f.name try: p = SessionPersistence(path) p.set("session_1", "key_a", "value_a") p.delete("session_1") assert p.get("session_1", "key_a") is None finally: try: os.unlink(path) except OSError: pass def test_no_persist_path(self): p = SessionPersistence() p.set("session_1", "key_a", "value_a") assert p.get("session_1", "key_a") == "value_a" def test_load_invalid_json(self): with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f: f.write("not valid json") path = f.name try: p = SessionPersistence(path) assert p._data == {} finally: try: os.unlink(path) except OSError: pass def test_cleanup_expired(self): with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f: path = f.name try: p = SessionPersistence(path) p._data["old_session"] = {"_last_active": 0} p._data["recent_session"] = {"_last_active": time.time()} p.cleanup_expired(max_age_s=3600) assert "old_session" not in p._data assert "recent_session" in p._data finally: try: os.unlink(path) except OSError: pass # ============================================================ # verify.py - 额外覆盖 # ============================================================ class TestVerifyExtended: def test_decrypt_body_no_key(self): result = decrypt_feishu_body(b"encrypted", "") assert result is None def test_verify_and_decrypt_plain(self): body = b'{"challenge":"test"}' ts = "1234567890" nonce = "abc" key = "" raw = f"{ts}{nonce}{key}".encode() + body sig = hashlib.sha256(raw).hexdigest() headers = { "X-Lark-Request-Timestamp": ts, "X-Lark-Request-Nonce": nonce, "X-Lark-Signature": sig, } ok, result_body, status = verify_and_decrypt_webhook(headers, body, key, "test_ip") assert ok is True assert status == "ok" assert result_body == body def test_verify_and_decrypt_rate_limited(self): for _ in range(1000): check_webhook_rate_limit("rate_limit_test_ip") body = b"test" ts = "123" nonce = "abc" key = "secret" raw = f"{ts}{nonce}{key}".encode() + body sig = hashlib.sha256(raw).hexdigest() headers = { "X-Lark-Request-Timestamp": ts, "X-Lark-Request-Nonce": nonce, "X-Lark-Signature": sig, } ok, result_body, status = verify_and_decrypt_webhook(headers, body, key, "rate_limit_test_ip") assert ok is False assert status == "rate_limited" def test_verify_and_decrypt_signature_mismatch(self): headers = { "X-Lark-Request-Timestamp": "123", "X-Lark-Request-Nonce": "abc", "X-Lark-Signature": "wrong_sig", } ok, _, status = verify_and_decrypt_webhook(headers, b"test", "secret", "test_ip2") assert ok is False assert status == "signature_mismatch" # ============================================================ # formatter.py - 额外覆盖 # ============================================================ class TestFormatterExtended: def test_format_outbound_message_text(self): result = format_outbound_message("Hello World") assert "text" in result assert result["text"] == "Hello World" def test_format_outbound_message_post(self): result = format_outbound_message("Hello", metadata={"use_post_format": True}) assert "zh_cn" in result def test_format_outbound_card_full(self): card = format_outbound_card( "Content", title="标题", buttons=[{"text": "OK", "action": "confirm"}], streaming=True, url_unfurl=["https://example.com"], images=["img_key_1"], files=[{"name": "file.pdf", "url": "https://example.com/file.pdf"}], note="备注", template="green", tone="success", context_text="上下文", dividers=1, selectors=[{"tag": "date_picker"}], ) assert "header" in card assert "elements" in card def test_format_outbound_diagnostic_card(self): card = format_outbound_diagnostic_card("诊断标题", {"key1": "value1", "key2": "value2"}) markdown_elements = [e for e in card["elements"] if e.get("tag") == "markdown"] assert len(markdown_elements) >= 1 content = markdown_elements[0]["content"] assert "诊断标题" in content assert "key1" in content assert "key2" in content def test_make_error_card(self): card = make_error_card("错误标题", "错误详情") assert card["header"]["template"] == "red" markdown_elements = [e for e in card["elements"] if e.get("tag") == "markdown"] assert len(markdown_elements) >= 1 def test_format_outbound_with_metadata_buttons(self): result = format_outbound("Hello", metadata={"buttons": [{"text": "OK"}]}) assert result["buttons"] == [{"text": "OK"}] def test_format_outbound_with_metadata_thread_id(self): result = format_outbound("Hello", metadata={"thread_id": "om_thread_123"}) assert result["thread_id"] == "om_thread_123" def test_format_outbound_basic(self): result = format_outbound("Hello") assert result["content"] == "Hello" def test_truncate_long_text(self): long_text = "x" * 40000 result = format_outbound_message(long_text) text = result.get("text", "") assert len(text) <= 30000 # ============================================================ # normalizer.py - 额外覆盖 # ============================================================ class TestNormalizerExtended: def test_map_event_type_all(self): assert map_event_type("im.message.receive_v1") == EventType.MESSAGE_RECEIVED assert map_event_type("im.message.updated_v1") == EventType.MESSAGE_UPDATED assert map_event_type("im.message.deleted_v1") == EventType.MESSAGE_DELETED assert map_event_type("im.message.message_read_v1") == EventType.READ_RECEIPT assert map_event_type("im.message.reaction.created_v1") == EventType.REACTION_ADDED assert map_event_type("im.message.reaction.deleted_v1") == EventType.REACTION_REMOVED assert map_event_type("im.chat.member.bot.added_v1") == EventType.BOT_ADDED assert map_event_type("im.chat.member.bot.deleted_v1") == EventType.BOT_REMOVED assert map_event_type("card.action.trigger") == EventType.CARD_ACTION assert map_event_type("card.action.trigger_v1") == EventType.CARD_ACTION assert map_event_type("application.bot.menu_v6") == EventType.BOT_MENU def test_map_msg_type_all(self): assert map_msg_type("text") == MessageType.TEXT assert map_msg_type("image") == MessageType.IMAGE assert map_msg_type("file") == MessageType.FILE assert map_msg_type("audio") == MessageType.AUDIO assert map_msg_type("video") == MessageType.VIDEO assert map_msg_type("post") == MessageType.TEXT assert map_msg_type("interactive") == MessageType.CARD assert map_msg_type("unknown") == MessageType.TEXT def test_strip_at_mentions(self): text = '@某人 你好' result = strip_at_mentions(text) assert "@某人" not in result assert "你好" in result def test_strip_at_mentions_replace_with(self): text = '@某人 你好' result = strip_at_mentions(text, replace_with="@某人 ") assert result.startswith("@某人 ") def test_strip_at_mentions_empty(self): assert strip_at_mentions("") == "" assert strip_at_mentions(None) is None def test_extract_mentioned_user_ids(self): text = '@A @B' ids = extract_mentioned_user_ids(text) assert ids == ["ou_123", "ou_456"] def test_extract_mentioned_user_ids_empty(self): assert extract_mentioned_user_ids("") == [] assert extract_mentioned_user_ids(None) == [] def test_get_i18n_text_preferred(self): content = {"i18n": {"zh_cn": "中文", "en_us": "English"}} assert _get_i18n_text(content, prefer_lang="zh_cn") == "中文" def test_get_i18n_text_fallback(self): content = {"i18n": {"en_us": "English"}} assert _get_i18n_text(content, prefer_lang="zh_cn") == "English" def test_get_i18n_text_no_i18n(self): content = {"content": "直接内容"} assert _get_i18n_text(content) == "直接内容" def test_get_i18n_text_first_fallback(self): content = {"i18n": {"ja_jp": "日本語"}} assert _get_i18n_text(content, prefer_lang="zh_cn") == "日本語" def test_resolve_template_vars(self): text = "Hello ${name}, {{age}} years old" event = {"name": "Alice", "age": 25} result = _resolve_template_vars(text, event) assert "Alice" in result assert "25" in result def test_resolve_template_vars_from_message(self): text = "${msg_type}" event = {"message": {"msg_type": "text"}} result = _resolve_template_vars(text, event) assert result == "text" def test_resolve_template_vars_none_value(self): text = "${name}" event = {"name": None} result = _resolve_template_vars(text, event) assert result == "" def test_resolve_template_vars_unknown(self): text = "${unknown_var}" event = {} result = _resolve_template_vars(text, event) assert result == "${unknown_var}" def test_escape_post_md(self): assert _escape_post_md("hello*world*") == "hello\\*world\\*" assert _escape_post_md("test[link](url)") != "test[link](url)" def test_extract_interactive_text_title(self): content = {"title": "卡片标题", "elements": []} result = _extract_interactive_text(content) assert "卡片标题" in result def test_extract_interactive_text_header(self): content = {"header": {"title": {"content": "头部标题"}}, "elements": []} result = _extract_interactive_text(content) assert "头部标题" in result def test_extract_interactive_text_markdown(self): content = {"elements": [{"tag": "markdown", "content": "**加粗**"}]} result = _extract_interactive_text(content) assert "**加粗**" in result def test_extract_interactive_text_plain_text(self): content = {"elements": [{"tag": "plain_text", "content": "普通文本"}]} result = _extract_interactive_text(content) assert "普通文本" in result def test_extract_interactive_text_action(self): content = { "elements": [ { "tag": "action", "actions": [{"tag": "button", "value": {"text": {"content": "点击"}}}], } ] } result = _extract_interactive_text(content) assert "[点击]" in result def test_extract_content_forwarded(self): event = { "message": { "msg_type": "forwarded", "content": json.dumps({"msg_type": "text", "content": {"text": "转发内容"}}), "sender": {"sender_name": "张三"}, } } content = extract_content(event, EventType.MESSAGE_RECEIVED) assert "转发" in content assert "转发内容" in content def test_extract_content_media(self): event = { "message": { "msg_type": "media", "content": json.dumps({"items": [{"msg_type": "image"}, {"msg_type": "file"}]}), } } content = extract_content(event, EventType.MESSAGE_RECEIVED) assert "媒体消息" in content def test_extract_content_sticker(self): event = {"message": {"msg_type": "sticker", "content": '{"file_key": "sticker_1"}'}} content = extract_content(event, EventType.MESSAGE_RECEIVED) assert content == "[贴纸]" def test_mentions_with_no_mentions_list(self): event = {"message": {"content": '{"text": "hello"}'}} mentions = extract_mentions(event) assert mentions.mentioned_user_ids == [] assert mentions.is_bot_mentioned is False # ============================================================ # dedup.py - 额外覆盖 # ============================================================ class TestDedupExtended: def test_make_key_no_event_message_ids(self): event = {"data": "test", "type": "unknown"} key = FeishuDedupStore._make_key(event) assert len(key) == 32 def test_record_processing_state(self): store = FeishuDedupStore() event = {"event": {"event_id": "evt_001"}} assert not store.has_processed(event) store.record_processed(event) assert store.has_processed(event) def test_finalize_removes_from_processing(self): store = FeishuDedupStore() event = {"event": {"event_id": "evt_001"}} store.record_processed(event) store.finalize_processing(event) assert store.has_processed(event) def test_eviction_by_ttl(self): store = FeishuDedupStore(ttl_s=0, max_entries=10) event = {"event": {"event_id": "evt_ttl_test"}} store.record_processed(event) store.finalize_processing(event) assert not store.has_processed(event) def test_trim_to_max(self): store = FeishuDedupStore(max_entries=3) for i in range(5): event = {"event": {"event_id": f"evt_{i}"}} store.record_processed(event) store.finalize_processing(event) assert len(store) <= 3 def test_clear(self): store = FeishuDedupStore() for i in range(5): event = {"event": {"event_id": f"evt_{i}"}} store.record_processed(event) store.finalize_processing(event) count_before = len(store) store.clear() assert len(store) == 0 assert count_before > 0 # ============================================================ # sent_cache.py # ============================================================ class TestFeishuSentCache: @pytest.mark.asyncio async def test_cache_and_get(self): cache = None from yuxi.channels.adapters.feishu.sent_cache import FeishuSentCache cache = FeishuSentCache() await cache.cache_sent("msg_1", "chat_1") entry = await cache.get_sent("msg_1", "chat_1") assert entry is not None assert entry["message_id"] == "msg_1" @pytest.mark.asyncio async def test_get_nonexistent(self): from yuxi.channels.adapters.feishu.sent_cache import FeishuSentCache cache = FeishuSentCache() entry = await cache.get_sent("nonexistent", "chat_1") assert entry is None @pytest.mark.asyncio async def test_invalidate(self): from yuxi.channels.adapters.feishu.sent_cache import FeishuSentCache cache = FeishuSentCache() await cache.cache_sent("msg_1", "chat_1") await cache.invalidate("msg_1", "chat_1") entry = await cache.get_sent("msg_1", "chat_1") assert entry is None @pytest.mark.asyncio async def test_clear(self): from yuxi.channels.adapters.feishu.sent_cache import FeishuSentCache cache = FeishuSentCache() await cache.cache_sent("msg_1", "chat_1") await cache.cache_sent("msg_2", "chat_1") await cache.clear() assert await cache.get_sent("msg_1", "chat_1") is None assert await cache.get_sent("msg_2", "chat_1") is None @pytest.mark.asyncio async def test_expired_ttl(self): from yuxi.channels.adapters.feishu.sent_cache import FeishuSentCache cache = FeishuSentCache(ttl_s=0) await cache.cache_sent("msg_1", "chat_1") entry = await cache.get_sent("msg_1", "chat_1") assert entry is None @pytest.mark.asyncio async def test_max_entries_eviction(self): from yuxi.channels.adapters.feishu.sent_cache import FeishuSentCache cache = FeishuSentCache(max_entries=3) await cache.cache_sent("msg_1", "chat_1") await cache.cache_sent("msg_2", "chat_1") await cache.cache_sent("msg_3", "chat_1") await cache.cache_sent("msg_4", "chat_1") assert await cache.get_sent("msg_1", "chat_1") is None @pytest.mark.asyncio async def test_make_key_format(self): from yuxi.channels.adapters.feishu.sent_cache import FeishuSentCache key = FeishuSentCache._make_key("msg_123", "chat_456") assert key == "chat_456:msg_123" # ============================================================ # reactions.py # ============================================================ class TestReactions: def test_emoji_type_to_emoji_known(self): assert emoji_type_to_emoji("THUMBSUP") == "👍" assert emoji_type_to_emoji("HEART") == "❤️" assert emoji_type_to_emoji("FIRE") == "🔥" assert emoji_type_to_emoji("CHECK") == "✅" def test_emoji_type_to_emoji_unknown(self): assert emoji_type_to_emoji("CUSTOM_TYPE") == "CUSTOM_TYPE" # ============================================================ # media.py - 额外覆盖 # ============================================================ class TestMediaExtended: def test_resolve_media_kind_by_ext(self): assert resolve_feishu_outbound_media_kind(filename="test.png") == "img" assert resolve_feishu_outbound_media_kind(filename="test.mp3") == "stream" assert resolve_feishu_outbound_media_kind(filename="test.mp4") == "mp4" assert resolve_feishu_outbound_media_kind(filename="test.opus") == "opus" def test_resolve_media_kind_by_mime(self): assert resolve_feishu_outbound_media_kind(mime_type="image/jpeg") == "img" assert resolve_feishu_outbound_media_kind(mime_type="audio/mpeg") == "stream" def test_resolve_media_kind_default(self): assert resolve_feishu_outbound_media_kind(filename="test.xyz") == "stream" assert resolve_feishu_outbound_media_kind() == "stream" def test_resolve_media_kind_document_ext(self): assert resolve_feishu_outbound_media_kind(filename="test.pdf") == "stream" assert resolve_feishu_outbound_media_kind(filename="test.csv") == "stream" def test_sanitize_filename_normal(self): assert sanitize_filename_for_upload("hello.txt") == "hello.txt" def test_sanitize_filename_control_chars(self): result = sanitize_filename_for_upload("test\x00file.txt") assert "\x00" not in result def test_sanitize_filename_empty_name(self): result = sanitize_filename_for_upload(".txt") assert "txt" in result.lower() def test_validate_media_size_ok(self): validate_media_size(b"x" * 100, max_mb=1) def test_validate_media_size_exceeded(self): with pytest.raises(MediaSizeError): validate_media_size(b"x" * (2 * 1024 * 1024), max_mb=1) def test_validate_image_size(self): validate_image_size(b"x" * 512) def test_validate_file_size(self): validate_file_size(b"x" * 512) def test_recover_utf8_no_need(self): result = recover_utf8_filename_from_latin1_header("hello.txt") assert result == "hello.txt" def test_recover_utf8_non_latin1(self): result = recover_utf8_filename_from_latin1_header("测试文件.txt") assert result == "测试文件.txt" # ============================================================ # stream.py - 额外覆盖 # ============================================================ class TestStreamExtended: def test_merge_empty_existing(self): assert merge_streaming_text("", "Hello") == "Hello" def test_merge_empty_incoming(self): assert merge_streaming_text("Hello", "") == "Hello" def test_merge_incoming_starts_with_existing(self): assert merge_streaming_text("Hello", "Hello World") == "Hello World" def test_merge_existing_starts_with_incoming(self): assert merge_streaming_text("Hello World", "Hello") == "Hello World" def test_merge_overlap_suffix(self): assert merge_streaming_text("today is", "is lovely") == "today is lovely" def test_merge_overlap_prefix(self): assert merge_streaming_text("is lovely", "today is") == "today is lovely" def test_merge_no_overlap(self): assert merge_streaming_text("Hello", "World") == "HelloWorld" def test_merge_same_text(self): assert merge_streaming_text("Hello", "Hello") == "Hello" def test_merge_partial_overlap(self): assert merge_streaming_text("Hello Wo", "o World!") == "Hello World!" # ============================================================ # secret_resolver.py - 额外覆盖 # ============================================================ class TestSecretResolverExtended: def test_resolve_secret_with_rotation_cache(self): from yuxi.channels.adapters.feishu.secret_resolver import invalidate_secret_cache invalidate_secret_cache() result = resolve_secret_with_rotation({"test_key": "cached_value"}, "test_key", rotation_window_s=3600) assert result == "cached_value" def test_resolve_secret_with_rotation_hit(self): invalidate_secret_cache() result1 = resolve_secret_with_rotation({"test_key": "cached_value"}, "test_key", rotation_window_s=3600) result2 = resolve_secret_with_rotation({"test_key": "cached_value"}, "test_key", rotation_window_s=3600) assert result1 == "cached_value" assert result2 == "cached_value" def test_invalidate_specific_cache(self): invalidate_secret_cache() resolve_secret_with_rotation({"test_key": "value1"}, "test_key", rotation_window_s=3600) invalidate_secret_cache("test_key") result = resolve_secret_with_rotation({"test_key": "value2"}, "test_key", rotation_window_s=3600) assert result == "value2" def test_invalidate_all_cache(self): invalidate_secret_cache() resolve_secret_with_rotation({"test_key": "value1"}, "test_key", rotation_window_s=3600) invalidate_secret_cache() result = resolve_secret_with_rotation({"test_key": "value2"}, "test_key", rotation_window_s=3600) assert result == "value2" def test_resolve_secret_env_source(self, monkeypatch): monkeypatch.setenv("MY_SECRET_ENV", "env_secret_value") result = resolve_secret({"test_key": {"source": "env", "env": "MY_SECRET_ENV"}}, "test_key") assert result == "env_secret_value" def test_resolve_secret_command_failure(self): result = resolve_secret({"test_key": {"source": "exec", "command": "exit 1"}}, "test_key") assert result == "" # ============================================================ # send.py - 额外覆盖 # ============================================================ class TestSendExtended: def test_is_local_image_path_valid(self): import yuxi.channels.adapters.feishu.send as send_mod result = send_mod.is_local_image_path("C:\\Windows\\win.ini") assert result is None def test_is_local_image_path_not_image(self): import yuxi.channels.adapters.feishu.send as send_mod result = send_mod.is_local_image_path("/some/path/file.txt") assert result is None def test_is_local_image_path_not_absolute(self): import yuxi.channels.adapters.feishu.send as send_mod result = send_mod.is_local_image_path("relative/path/image.png") assert result is None def test_resolve_receive_id_type(self): from yuxi.channels.adapters.feishu.send import _resolve_receive_id_type assert _resolve_receive_id_type("direct") == "open_id" assert _resolve_receive_id_type("private") == "open_id" assert _resolve_receive_id_type("p2p") == "open_id" assert _resolve_receive_id_type("group") == "chat_id" assert _resolve_receive_id_type("thread") == "chat_id" assert _resolve_receive_id_type("unknown") == "open_id" @pytest.mark.asyncio 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 @pytest.mark.asyncio async def test_send_text_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.send_text(None, "chat_123", "hello") assert result.success is False assert "not available" in result.error @pytest.mark.asyncio async def test_send_card_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.send_card(None, "chat_123", "hello") assert result.success is False @pytest.mark.asyncio async def test_send_reaction_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.send_reaction(None, "msg_123", "THUMBSUP") assert result.success is False @pytest.mark.asyncio async def test_read_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.read_message(None, "msg_123") assert result == {} @pytest.mark.asyncio async def test_list_messages_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.list_messages(None, "chat_123") assert result == {"messages": [], "has_more": False, "page_token": ""} @pytest.mark.asyncio async def test_update_card_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.update_card_message(None, "msg_123", "updated") assert result.success is False @pytest.mark.asyncio async def test_forward_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.forward_message(None, "msg_123", "target_chat") assert result.success is False @pytest.mark.asyncio async def test_get_message_read_status_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.get_message_read_status(None, "msg_123") assert result == {"users": [], "has_more": False, "page_token": ""} # ============================================================ # send.py - mock SDK tests # ============================================================ class TestSendWithMockSDK: @pytest.mark.asyncio async def test_reply_message_success(self, monkeypatch): mock_client = MagicMock() mock_resp = MagicMock() mock_resp.success.return_value = True mock_resp.data = {"message_id": "reply_123"} 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_123" @pytest.mark.asyncio async def test_reply_message_with_post_format(self, monkeypatch): mock_client = MagicMock() mock_resp = MagicMock() mock_resp.success.return_value = True mock_resp.data = {"message_id": "reply_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", use_post_format=True) assert result.success is True @pytest.mark.asyncio async def test_reply_message_api_error(self, monkeypatch): 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 @pytest.mark.asyncio async def test_reply_message_exception(self, monkeypatch): mock_client = MagicMock() mock_client.im.v1.message.reply = AsyncMock(side_effect=Exception("network error")) from yuxi.channels.adapters.feishu.send import reply_message result = await reply_message(mock_client, "msg_123", "hello") assert result.success is False assert "network error" in result.error @pytest.mark.asyncio async def test_send_text_success(self, monkeypatch): import yuxi.channels.adapters.feishu.send as send_mod monkeypatch.setattr(send_mod, "_make_create_msg_request", MagicMock(return_value="mock_req")) mock_client = MagicMock() mock_resp = MagicMock() mock_resp.success.return_value = True mock_resp.data = {"message_id": "send_123"} mock_client.im.message.create = AsyncMock(return_value=mock_resp) result = await send_mod.send_text(mock_client, "chat_123", "hello") assert result.success is True assert result.message_id == "send_123" @pytest.mark.asyncio async def test_send_card_success(self, monkeypatch): import yuxi.channels.adapters.feishu.send as send_mod monkeypatch.setattr(send_mod, "_make_create_msg_request", MagicMock(return_value="mock_req")) mock_client = MagicMock() mock_resp = MagicMock() mock_resp.success.return_value = True mock_resp.data = {"message_id": "card_123"} mock_client.im.message.create = AsyncMock(return_value=mock_resp) result = await send_mod.send_card(mock_client, "chat_123", "hello") assert result.success is True assert result.message_id == "card_123" @pytest.mark.asyncio async def test_send_card_raw(self, monkeypatch): import yuxi.channels.adapters.feishu.send as send_mod monkeypatch.setattr(send_mod, "_make_create_msg_request", MagicMock(return_value="mock_req")) mock_client = MagicMock() mock_resp = MagicMock() mock_resp.success.return_value = True mock_resp.data = {"message_id": "card_raw"} mock_client.im.message.create = AsyncMock(return_value=mock_resp) result = await send_mod.send_card(mock_client, "chat_123", '{"key": "value"}', raw_card=True) assert result.success is True @pytest.mark.asyncio async def test_send_reaction_sdk_limitation(self, monkeypatch): import yuxi.channels.adapters.feishu.send as send_mod mock_client = MagicMock() result = await send_mod.send_reaction(mock_client, "msg_123", "THUMBSUP") assert result.success is False assert "SDK version incompatible" in result.error @pytest.mark.asyncio async def test_update_card_success(self, monkeypatch): mock_client = MagicMock() mock_resp = MagicMock() mock_resp.success.return_value = True mock_client.im.v1.message.patch = AsyncMock(return_value=mock_resp) from yuxi.channels.adapters.feishu.send import update_card_message result = await update_card_message(mock_client, "msg_123", "updated content") assert result.success is True @pytest.mark.asyncio async def test_forward_message_success(self, monkeypatch): mock_client = MagicMock() mock_resp = MagicMock() mock_resp.success.return_value = True mock_resp.data = {"message_id": "fwd_123"} mock_client.im.v1.message.forward = AsyncMock(return_value=mock_resp) from yuxi.channels.adapters.feishu.send import forward_message result = await forward_message(mock_client, "msg_123", "target_chat") assert result.success is True # ============================================================ # reactions.py - mock SDK tests # ============================================================ class TestReactionsWithMockSDK: @pytest.mark.asyncio async def test_remove_reaction_no_sdk(self, monkeypatch): import yuxi.channels.adapters.feishu.reactions as reactions_mod monkeypatch.setattr(reactions_mod, "HAS_LARK_SDK", False) result = await reactions_mod.remove_reaction(None, "msg_123", "rid_123") assert result.success is False assert "not available" in result.error @pytest.mark.asyncio async def test_list_reactions_no_sdk(self, monkeypatch): import yuxi.channels.adapters.feishu.reactions as reactions_mod monkeypatch.setattr(reactions_mod, "HAS_LARK_SDK", False) result = await reactions_mod.list_reactions(None, "msg_123") assert result == {"reactions": [], "total": 0} @pytest.mark.asyncio async def test_clear_all_bot_reactions_no_sdk(self, monkeypatch): import yuxi.channels.adapters.feishu.reactions as reactions_mod monkeypatch.setattr(reactions_mod, "HAS_LARK_SDK", False) result = await reactions_mod.clear_all_bot_reactions(None, "msg_123", "bot_123") assert result.success is False