"""Comprehensive unit tests for DingDing channel adapter. Covers: dedup, policy, config_schema, directory, reactions, stream, normalizer (extended events), session, cards, media, adapter snapshot. Target: ~35+ test scenarios. """ from __future__ import annotations import asyncio import time from unittest.mock import AsyncMock, MagicMock import pytest from yuxi.channels.adapters.dingding.cards import ( TEXT_CHUNK_LIMIT, build_card_template, build_standard_card, generate_card_biz_id, parse_card_callback_params, parse_card_private_data, ) from yuxi.channels.adapters.dingding.config_schema import refine_dingding_config, validate_dingding_config from yuxi.channels.adapters.dingding.config_ui_hints import get_dingding_ui_hints, resolve_field_order from yuxi.channels.adapters.dingding.dedup import DingDingDedupGuard from yuxi.channels.adapters.dingding.formatter import format_outbound from yuxi.channels.adapters.dingding.normalizer import ( extract_mentions, map_event_type, normalize_inbound, ) from yuxi.channels.adapters.dingding.policy import DingDingPolicyMatcher, PolicyAccessTracker from yuxi.channels.adapters.dingding.reactions import ( DINGDING_EMOJI_MAP, EMOTION_TYPE_TO_EMOJI, clear_all_bot_reactions, list_reactions, resolve_emoji, ) from yuxi.channels.adapters.dingding.session import ( SessionMode, SessionScope, generate_dingding_chat_id, generate_thread_key, normalize_chat_id, parse_chat_id, resolve_session_scope, ) from yuxi.channels.adapters.dingding.sign import compute_dingtalk_sign, verify_webhook_signature from yuxi.channels.adapters.dingding.stream import ( MAX_CONSECUTIVE_FAILURES, STREAM_NATURAL_BOUNDARIES, DingDingStreamingSession, ) from yuxi.channels.adapters.dingding.token import DingDingTokenManager from yuxi.channels.models import ( ChannelIdentity, ChannelMessage, ChannelStatus, ChannelType, ChatType, DeliveryResult, EventType, MessageType, ) CHANNEL_ID = "dingding" CHANNEL_TYPE = ChannelType.DINGDING def _make_adapter(config=None): from yuxi.channels.adapters.dingding.adapter import DingDingChannelAdapter return DingDingChannelAdapter(config=config or {"name": "test", "accounts": {"default": {}}}) def _make_connected_adapter(): from yuxi.channels.adapters.dingding.adapter import DingDingChannelAdapter adapter = DingDingChannelAdapter( config={ "name": "test", "accounts": { "default": { "app_key": "fake_key", "app_secret": "fake_secret", "robot_code": "fake_robot", } }, "mode": "webhook", } ) tm = DingDingTokenManager("fake_key", "fake_secret") tm._access_token = "fake_token" tm._token_expires_at = 9999999999 adapter._token_manager = tm mock_http = MagicMock() adapter._get_http_client = AsyncMock(return_value=mock_http) adapter._connected_at = time.time() return adapter # ============================================================ # DedupGuard tests # ============================================================ class TestDedupGuard: def test_has_processed_returns_false_initially(self): guard = DingDingDedupGuard() assert guard.has_processed("msg_001") is False def test_record_and_check(self): guard = DingDingDedupGuard() guard.record_as_processed("msg_001") assert guard.has_processed("msg_001") is True def test_different_ids_are_independent(self): guard = DingDingDedupGuard() guard.record_as_processed("msg_001") assert guard.has_processed("msg_002") is False def test_empty_message_id_safe(self): guard = DingDingDedupGuard() assert guard.has_processed("") is False guard.record_as_processed("") def test_claim_first_time_returns_true(self): guard = DingDingDedupGuard() assert guard.claim("msg_new") is True def test_claim_second_time_returns_false(self): guard = DingDingDedupGuard() guard.claim("msg_new") guard.commit("msg_new") assert guard.claim("msg_new") is False def test_claim_pending_returns_false(self): guard = DingDingDedupGuard() guard.claim("msg_pending") assert guard.claim("msg_pending") is False def test_release_allows_reclaim(self): guard = DingDingDedupGuard() guard.claim("msg_rel") guard.release("msg_rel") assert guard.claim("msg_rel") is True def test_finalize_processing(self): guard = DingDingDedupGuard() guard.claim("msg_fin") guard.finalize_processing("msg_fin") assert guard.has_processed("msg_fin") is True def test_check_and_mark(self): guard = DingDingDedupGuard() assert guard.check_and_mark("msg_cm") is False assert guard.check_and_mark("msg_cm") is True def test_stats(self): guard = DingDingDedupGuard() guard.record_as_processed("msg_1") guard.record_as_processed("msg_2") guard.claim("msg_3") stats = guard.stats() assert stats["committed"] == 2 assert stats["pending"] == 1 def test_clear(self): guard = DingDingDedupGuard() guard.record_as_processed("msg_1") guard.clear() assert guard.has_processed("msg_1") is False # ============================================================ # Policy tests # ============================================================ 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]["allowed"] is True assert history[1]["allowed"] is False def test_count_recent(self): tracker = PolicyAccessTracker() tracker.record_access("open", "chat_1", True) tracker.record_access("open", "chat_2", False) allowed, denied = tracker.count_recent("open", window_s=3600) assert allowed == 1 assert denied == 1 def test_clear_key(self): tracker = PolicyAccessTracker() tracker.record_access("open", "chat_1", True) tracker.clear("open") assert tracker.get_history("open") == [] class TestDingDingPolicyMatcher: def _make_config(self, **overrides): config = { "groupPolicy": "allowlist", "dmPolicy": "pairing", "allowFrom": ["group_abc", "user_*"], } config.update(overrides) return config def test_dm_pairing_allows_any(self): matcher = DingDingPolicyMatcher(self._make_config()) allowed, msg = matcher.check_chat_access("dm_user123", "direct") assert allowed is True def test_group_open_allows_any(self): matcher = DingDingPolicyMatcher(self._make_config(groupPolicy="open")) allowed, msg = matcher.check_chat_access("group_xyz", "group") assert allowed is True def test_group_disabled_denies_all(self): matcher = DingDingPolicyMatcher(self._make_config(groupPolicy="disabled")) allowed, msg = matcher.check_chat_access("group_xyz", "group") assert allowed is False assert "没有权限" in msg def test_allowlist_exact_match(self): matcher = DingDingPolicyMatcher(self._make_config()) allowed, msg = matcher.check_chat_access("group_abc", "group") assert allowed is True def test_allowlist_wildcard_match(self): matcher = DingDingPolicyMatcher(self._make_config()) allowed, msg = matcher.check_chat_access("user_bot_001", "direct") assert allowed is True def test_allowlist_no_match(self): matcher = DingDingPolicyMatcher(self._make_config(allowFrom=["group_specific"])) allowed, msg = matcher.check_chat_access("group_other", "group") assert allowed is False def test_blocklist_overrides_allowlist(self): matcher = DingDingPolicyMatcher( self._make_config(allowFrom=["group_abc"], blockFrom=["group_abc"]) ) allowed, msg = matcher.check_chat_access("group_abc", "group") assert allowed is False def test_resolve_require_mention_global(self): matcher = DingDingPolicyMatcher(self._make_config(requireMention=True)) assert matcher.resolve_require_mention("group_abc") is True def test_resolve_require_mention_per_group(self): matcher = DingDingPolicyMatcher( self._make_config( requireMention=True, groups={"group_abc": {"requireMention": False}}, ) ) assert matcher.resolve_require_mention("group_abc") is False assert matcher.resolve_require_mention("group_other") is True def test_resolve_system_prompt(self): matcher = DingDingPolicyMatcher( self._make_config( systemPrompt="全局提示词", groups={"group_abc": {"systemPrompt": "群专属提示词"}}, ) ) assert matcher.resolve_system_prompt("group_abc", "group") == "群专属提示词" assert matcher.resolve_system_prompt("group_other", "group") == "全局提示词" def test_resolve_dm_config(self): matcher = DingDingPolicyMatcher( self._make_config(dms={"dm_user123": {"systemPrompt": "私聊专属"}}) ) assert matcher.resolve_system_prompt("dm_user123", "direct") == "私聊专属" # ============================================================ # Config Schema tests # ============================================================ class TestConfigSchema: def test_validate_valid_config(self): config = { "accounts": {"default": {"app_key": "key123", "app_secret": "sec456"}}, "mode": "stream", "groupPolicy": "allowlist", "dmPolicy": "pairing", } errors = validate_dingding_config(config) assert errors == [] def test_validate_missing_accounts(self): errors = validate_dingding_config({}) assert any("accounts" in e for e in errors) def test_validate_missing_app_key(self): errors = validate_dingding_config({"accounts": {"default": {}}}) assert any("app_key" in e for e in errors) def test_validate_invalid_mode(self): errors = validate_dingding_config( {"accounts": {"default": {"app_key": "k", "app_secret": "s"}}, "mode": "invalid_mode"} ) assert any("mode" in e for e in errors) def test_validate_invalid_group_policy(self): errors = validate_dingding_config( { "accounts": {"default": {"app_key": "k", "app_secret": "s"}}, "groupPolicy": "bad_policy", } ) assert any("groupPolicy" in e for e in errors) def test_validate_invalid_text_limit(self): errors = validate_dingding_config( { "accounts": {"default": {"app_key": "k", "app_secret": "s"}}, "text_chunk_limit": 10, } ) assert any("text_chunk_limit" in e for e in errors) def test_validate_blank_allowlist_entry(self): errors = validate_dingding_config( { "accounts": {"default": {"app_key": "k", "app_secret": "s"}}, "allowlist": [" "], } ) assert any("allowlist" in e for e in errors) def test_refine_falls_back_to_stream_on_invalid_mode(self): refined = refine_dingding_config({"mode": "bogus"}) assert refined["mode"] == "stream" def test_refine_falls_back_on_invalid_policies(self): refined = refine_dingding_config({"groupPolicy": "bad", "dmPolicy": "bad"}) assert refined["groupPolicy"] == "allowlist" assert refined["dmPolicy"] == "pairing" def test_refine_warns_on_invalid_streaming_mode(self): refined = refine_dingding_config({"streaming": {"mode": "bad"}}) assert refined["streaming"]["mode"] == "block" def test_refine_preserves_valid_values(self): refined = refine_dingding_config( { "mode": "webhook", "groupPolicy": "open", "dmPolicy": "open", "accounts": {"default": {"robot_code": "rc"}}, } ) assert refined["mode"] == "webhook" assert refined["groupPolicy"] == "open" assert refined["dmPolicy"] == "open" # ============================================================ # Directory tests # ============================================================ class TestDirectoryClient: def test_directory_client_init(self): from yuxi.channels.adapters.dingding.directory import DingDingDirectoryClient tm = DingDingTokenManager("fake", "fake_secret") client = DingDingDirectoryClient(tm) assert client._token_manager is tm def test_get_user_info_structure(self): from yuxi.channels.adapters.dingding.directory import DingDingDirectoryClient tm = DingDingTokenManager("fake", "fake_secret") client = DingDingDirectoryClient(tm) assert client._token_manager is tm @pytest.mark.asyncio async def test_get_channel_info_no_token(self): from yuxi.channels.adapters.dingding.directory import DingDingDirectoryClient tm = DingDingTokenManager("fake", "fake_secret") client = DingDingDirectoryClient(tm) result = await client.get_channel_info("group_cid123") assert result["chat_id"] == "group_cid123" assert "error" in result # ============================================================ # Reactions tests # ============================================================ class TestReactionsExtended: def test_emoji_map_has_all_entries(self): assert len(DINGDING_EMOJI_MAP) == 14 assert DINGDING_EMOJI_MAP["👍"] == ("101", "like") assert DINGDING_EMOJI_MAP["❤️"] == ("103", "heart") def test_resolve_emoji_by_emoji(self): result = resolve_emoji("👍") assert result == ("101", "like") def test_resolve_emoji_by_name(self): result = resolve_emoji("like") assert result == ("101", "like") def test_resolve_emoji_unknown(self): assert resolve_emoji("invalid_emoji") is None def test_emotion_type_to_emoji_reverse(self): assert EMOTION_TYPE_TO_EMOJI["101"] == "👍" assert EMOTION_TYPE_TO_EMOJI.get("999") is None def test_remove_reaction_not_connected(self): adapter = _make_adapter() result = asyncio.run(adapter.remove_reaction("dm_user1", "msg_001", "👍")) assert result.success is False def test_list_reactions_not_connected(self): adapter = _make_adapter() result = asyncio.run(adapter.list_reactions("dm_user1", "msg_001")) assert result["total"] == 0 # ============================================================ # Session tests # ============================================================ class TestSessionExtended: def test_session_scope_values(self): assert SessionScope.DIRECT == "dm" assert SessionScope.GROUP == "group" assert SessionScope.GROUP_SENDER == "group_sender" assert SessionScope.TOPIC == "topic" def test_session_mode_values(self): assert SessionMode.RAW == "raw" assert SessionMode.CHAT_RAW == "chat_raw" assert SessionMode.CHAT_RESOLVE == "chat_resolve" def test_generate_chat_id_group_sender(self): raw = {"senderId": "user001", "isGroupChat": True, "conversationId": "cid_grp"} chat_id = generate_dingding_chat_id(raw, "group", sender_id="user001", scope="group_sender") assert chat_id.startswith("dingding:group_sender:") assert "cid_grp" in chat_id assert "user001" in chat_id def test_normalize_chat_id_dm(self): assert normalize_chat_id("dm_user123") == "dm_user123" def test_normalize_chat_id_group(self): assert normalize_chat_id("group_cid123") == "group_cid123" def test_normalize_chat_id_raw(self): result = normalize_chat_id("cid_raw") assert result.startswith("dingding:group:") def test_parse_chat_id_dm(self): info = parse_chat_id("dm_user123") assert info["type"] == "direct" assert info["open_id"] == "user123" def test_parse_chat_id_group(self): info = parse_chat_id("group_cid123") assert info["type"] == "group" assert info["open_conversation_id"] == "cid123" def test_resolve_session_scope(self): assert resolve_session_scope("dm_user123") == SessionScope.DIRECT assert resolve_session_scope("group_cid123") == SessionScope.GROUP def test_generate_thread_key(self): key = generate_thread_key("dingding", "group_cid123") assert key == "dingding:group:group_cid123" # ============================================================ # Streaming session tests # ============================================================ class TestDingDingStreamingSession: def _make_session(self, **overrides): config = {"streaming": {"mode": "partial"}} config.update(overrides) tm = DingDingTokenManager("fake", "fake_secret") return DingDingStreamingSession( tm, "cid_open", "robot_code", "group_cid_open", config=config, ) def test_session_init_state(self): session = self._make_session() assert session.message_id is None assert session.accumulated_text == "" assert session.is_closed is False assert session.consecutive_failures == 0 def test_session_should_not_degrade_early(self): session = self._make_session() assert session.should_degrade() is False def test_backoff_delay_zero_when_no_failures(self): session = self._make_session() assert session.backoff_delay() == 0.0 @pytest.mark.parametrize( "boundary", ["\n", "。", "!", "?", ";", ":", ",", ". ", "! ", "? "], ) def test_natural_boundaries(self, boundary): assert boundary in STREAM_NATURAL_BOUNDARIES def test_max_consecutive_failures_constant(self): assert MAX_CONSECUTIVE_FAILURES == 5 # ============================================================ # Cards tests # ============================================================ class TestCardsExtended: def test_generate_card_biz_id_unique(self): id1 = generate_card_biz_id() id2 = generate_card_biz_id() assert id1 != id2 assert id1.startswith("fp_") def test_generate_card_biz_id_custom_prefix(self): bid = generate_card_biz_id("custom") assert bid.startswith("custom_") def test_build_standard_card_basic(self): card = build_standard_card(title="Test", markdown_content="**Hello**") assert "cardData" in card assert card["cardData"]["header"]["title"]["text"] == "Test" body = card["cardData"]["body"] assert body[0]["text"] == "**Hello**" def test_build_standard_card_with_buttons(self): card = build_standard_card( title="Test", markdown_content="content", buttons=[ {"title": "OK", "action_type": "CALLBACK", "callback_key": "key1"}, {"title": "URL", "action_type": "URL", "url": "https://example.com"}, ], ) body = card["cardData"]["body"] action_group = body[-1] assert action_group["componentType"] == "actionGroup" assert len(action_group["actions"]) == 2 def test_build_standard_card_private_data(self): card = build_standard_card(title="T", private_data={"key": "value"}) assert card["privateData"] == {"key": "value"} def test_build_card_template(self): card = build_card_template("tmpl_001", variables={"name": "test"}) assert card["cardData"]["templateId"] == "tmpl_001" assert card["cardData"]["cardTemplateVariableParams"]["name"] == "test" def test_parse_card_private_data(self): callback = {"cardPrivateData": {"action": "approve"}} assert parse_card_private_data(callback) == {"action": "approve"} def test_parse_card_callback_params(self): callback = {"actionParameter": {"key": "val"}} assert parse_card_callback_params(callback) == {"key": "val"} def test_text_chunk_limit_value(self): assert TEXT_CHUNK_LIMIT == 4096 # ============================================================ # Normalizer extended event tests # ============================================================ class TestNormalizerExtendedEvents: @pytest.mark.parametrize( "event_type_str,expected", [ ("message_updated", EventType.MESSAGE_UPDATED), ("message_deleted", EventType.MESSAGE_DELETED), ("bot_removed", EventType.BOT_REMOVED), ("member_joined", EventType.MEMBER_JOINED), ("member_left", EventType.MEMBER_LEFT), ("member_added", EventType.MEMBER_ADDED), ("member_removed", EventType.MEMBER_REMOVED), ("reaction_added", EventType.REACTION_ADDED), ("reaction_removed", EventType.REACTION_REMOVED), ("channel_updated", EventType.SYSTEM_EVENT), ], ) def test_map_event_type(self, event_type_str, expected): raw = {"event_type": event_type_str} assert map_event_type(raw) == expected def test_map_event_type_default(self): assert map_event_type({"event_type": "unknown"}) == EventType.MESSAGE_RECEIVED def test_bot_added_via_robot_code(self): raw = {"robotCode": "rb_001"} assert map_event_type(raw) == EventType.BOT_ADDED def test_mentions_not_triggered_for_dm(self): raw = { "senderId": "user1", "isGroupChat": False, "conversationId": "cid1", "msgId": "msg1", "msgtype": "text", "text": {"content": "hello"}, "atUsers": [{"dingtalkId": "bot"}], } mentions = extract_mentions(raw) assert mentions is None def test_mentions_at_all(self): raw = { "senderId": "user1", "isGroupChat": True, "conversationId": "cid1", "msgId": "msg1", "msgtype": "text", "text": {"content": "@all"}, "isAtAll": True, } mentions = extract_mentions(raw) assert mentions is not None assert "@all" in mentions.mentioned_user_ids # ============================================================ # Config UI Hints tests # ============================================================ class TestConfigUIHints: def test_get_ui_hints_returns_dict(self): hints = get_dingding_ui_hints() assert isinstance(hints, dict) assert "mode" in hints assert "accounts.default.app_key" in hints def test_resolve_field_order(self): fields = resolve_field_order() assert isinstance(fields, list) assert len(fields) > 5 orders = [f["order"] for f in fields] assert orders == sorted(orders) # ============================================================ # Sign extended tests # ============================================================ class TestSignExtended: def test_sign_url_encoded(self): sign = compute_dingtalk_sign("1700000000000", "test") assert "%" in sign or sign.replace("-", "").replace("_", "").isalnum() def test_tampered_sign_fails(self): now_ms = str(int(time.time() * 1000)) headers = {"timestamp": now_ms, "sign": compute_dingtalk_sign(now_ms, "secret")} tampered = dict(headers) tampered["sign"] = "bad_signature" assert verify_webhook_signature(tampered, "secret") is False def test_missing_timestamp_passes(self): headers = {"sign": "anything"} assert verify_webhook_signature(headers, "secret") is True # ============================================================ # Token manager extended tests # ============================================================ class TestTokenManagerExtended: def test_is_valid_when_no_token(self): tm = DingDingTokenManager("k", "s") assert tm._is_valid() is False def test_is_valid_when_expired(self): tm = DingDingTokenManager("k", "s") tm._access_token = "tok" tm._token_expires_at = 0 assert tm._is_valid() is False def test_is_valid_when_active(self): tm = DingDingTokenManager("k", "s") tm._access_token = "tok" tm._token_expires_at = 9999999999 assert tm._is_valid() is True # ============================================================ # Formatter extended tests # ============================================================ class TestFormatterExtended: def test_metadata_title_preserved(self): result = format_outbound("content", metadata={"use_markdown": True, "title": "MyTitle"}) import json params = json.loads(result["msgParam"]) assert params["title"] == "MyTitle" def test_open_conversation_id_passed(self): result = format_outbound("text", metadata={"openConversationId": "oc_123"}) assert result["openConversationId"] == "oc_123" # ============================================================ # Adapter build_snapshot tests # ============================================================ class TestAdapterSnapshot: def test_build_snapshot_without_connection(self): adapter = _make_adapter() snapshot = adapter.build_snapshot() assert snapshot.running is False assert snapshot.connected is False assert snapshot.health_state in ("stopped", "disconnected") def test_build_snapshot_with_connection(self): adapter = _make_connected_adapter() adapter._status = ChannelStatus.CONNECTED snapshot = adapter.build_snapshot() assert snapshot.running is True assert snapshot.connected is True assert snapshot.account_id != "" def test_snapshot_includes_bot_info(self): adapter = _make_connected_adapter() snapshot = adapter.build_snapshot() assert "bot" in snapshot.model_dump() bot = snapshot.bot assert bot["channel"] == "dingding" assert "capabilities" in bot def test_snapshot_includes_audit(self): adapter = _make_connected_adapter() snapshot = adapter.build_snapshot() audit = snapshot.audit assert "dedup_committed" in audit assert "rate_limit" in audit # ============================================================ # Capabilities tests # ============================================================ class TestCapabilities: def test_chat_types_includes_group(self): from yuxi.channels.adapters.dingding.adapter import DingDingChannelAdapter caps = DingDingChannelAdapter.capabilities assert "group" in caps.chat_types assert "direct" in caps.chat_types def test_streaming_modes_include_all(self): from yuxi.channels.adapters.dingding.adapter import DingDingChannelAdapter assert "off" in DingDingChannelAdapter.streaming_modes assert "block" in DingDingChannelAdapter.streaming_modes assert "partial" in DingDingChannelAdapter.streaming_modes def test_capabilities_declare_all_features(self): from yuxi.channels.adapters.dingding.adapter import DingDingChannelAdapter caps = DingDingChannelAdapter.capabilities assert caps.reactions is True assert caps.edit is True assert caps.unsend is True assert caps.reply is True assert caps.media is True assert caps.threads is True assert caps.typing is False def test_text_chunk_limit_consistent(self): from yuxi.channels.adapters.dingding.adapter import DingDingChannelAdapter from yuxi.channels.adapters.dingding.cards import TEXT_CHUNK_LIMIT as CARD_LIMIT assert DingDingChannelAdapter.text_chunk_limit == 4096 assert DingDingChannelAdapter.capabilities.text_chunk_limit == 4096 assert CARD_LIMIT == 4096 # ============================================================ # ChannelMeta tests # ============================================================ class TestChannelMeta: def test_meta_has_aliases(self): from yuxi.channels.adapters.dingding.adapter import DingDingChannelAdapter meta = DingDingChannelAdapter.meta assert "dingtalk" in meta.aliases def test_meta_has_docs(self): from yuxi.channels.adapters.dingding.adapter import DingDingChannelAdapter meta = DingDingChannelAdapter.meta assert meta.docs_path == "/channels/dingding" assert meta.docs_label == "钉钉渠道配置" def test_meta_has_blurb(self): from yuxi.channels.adapters.dingding.adapter import DingDingChannelAdapter meta = DingDingChannelAdapter.meta assert len(meta.blurb) > 10 # ============================================================ # PolicyAccessTracker scenario tests # ============================================================ class TestPolicyScenario: def test_allowlist_empty_with_policy_denies(self): matcher = DingDingPolicyMatcher( {"groupPolicy": "allowlist", "allowFrom": []} ) allowed, msg = matcher.check_chat_access("group_any", "group") assert allowed is False def test_access_history_aggregation(self): matcher = DingDingPolicyMatcher( { "dmPolicy": "allowlist", "allowFrom": ["dm_user_ok"], "groupPolicy": "allowlist", } ) matcher.check_chat_access("dm_user_ok", "direct") matcher.check_chat_access("dm_user_bad", "direct") history = matcher.get_access_history() assert len(history) == 2 def test_reset_tracker(self): matcher = DingDingPolicyMatcher({"dmPolicy": "open"}) matcher.check_chat_access("dm_user1", "direct") matcher.reset_tracker() history = matcher.get_access_history() assert history == [] print(f"\nDingDing comprehensive tests loaded. TEXT_CHUNK_LIMIT={TEXT_CHUNK_LIMIT}")