diff --git a/scripts/release.py b/scripts/release.py index e3abf3517..b3e1238f0 100755 --- a/scripts/release.py +++ b/scripts/release.py @@ -84,6 +84,7 @@ AUTHOR_MAP = { "abdullahfarukozden@gmail.com": "Farukest", "lovre.pesut@gmail.com": "rovle", "kevinskysunny@gmail.com": "kevinskysunny", + "xiewenxuan462@gmail.com": "yule975", "hakanerten02@hotmail.com": "teyrebaz33", "ruzzgarcn@gmail.com": "Ruzzgar", "alireza78.crypto@gmail.com": "alireza78a", diff --git a/tests/gateway/test_dingtalk.py b/tests/gateway/test_dingtalk.py index 453835ec9..8404281d8 100644 --- a/tests/gateway/test_dingtalk.py +++ b/tests/gateway/test_dingtalk.py @@ -421,3 +421,157 @@ class TestExtractText: msg.rich_text = None assert DingTalkAdapter._extract_text(msg) == "" + +# --------------------------------------------------------------------------- +# Group gating — require_mention + allowed_users (parity with other platforms) +# --------------------------------------------------------------------------- + + +def _make_gating_adapter(monkeypatch, *, extra=None, env=None): + """Build a DingTalkAdapter with only the gating fields populated. + + Clears every DINGTALK_* gating env var before applying the caller's + overrides so individual tests stay isolated. + """ + for key in ( + "DINGTALK_REQUIRE_MENTION", + "DINGTALK_MENTION_PATTERNS", + "DINGTALK_FREE_RESPONSE_CHATS", + "DINGTALK_ALLOWED_USERS", + ): + monkeypatch.delenv(key, raising=False) + for key, value in (env or {}).items(): + monkeypatch.setenv(key, value) + from gateway.platforms.dingtalk import DingTalkAdapter + return DingTalkAdapter(PlatformConfig(enabled=True, extra=extra or {})) + + +class TestAllowedUsersGate: + + def test_empty_allowlist_allows_everyone(self, monkeypatch): + adapter = _make_gating_adapter(monkeypatch) + assert adapter._is_user_allowed("anyone", "any-staff") is True + + def test_wildcard_allowlist_allows_everyone(self, monkeypatch): + adapter = _make_gating_adapter(monkeypatch, extra={"allowed_users": ["*"]}) + assert adapter._is_user_allowed("anyone", "any-staff") is True + + def test_matches_sender_id_case_insensitive(self, monkeypatch): + adapter = _make_gating_adapter( + monkeypatch, extra={"allowed_users": ["SenderABC"]} + ) + assert adapter._is_user_allowed("senderabc", "") is True + + def test_matches_staff_id(self, monkeypatch): + adapter = _make_gating_adapter( + monkeypatch, extra={"allowed_users": ["staff_1234"]} + ) + assert adapter._is_user_allowed("", "staff_1234") is True + + def test_rejects_unknown_user(self, monkeypatch): + adapter = _make_gating_adapter( + monkeypatch, extra={"allowed_users": ["staff_1234"]} + ) + assert adapter._is_user_allowed("other-sender", "other-staff") is False + + def test_env_var_csv_populates_allowlist(self, monkeypatch): + adapter = _make_gating_adapter( + monkeypatch, env={"DINGTALK_ALLOWED_USERS": "alice,bob,carol"} + ) + assert adapter._is_user_allowed("alice", "") is True + assert adapter._is_user_allowed("dave", "") is False + + +class TestMentionPatterns: + + def test_empty_patterns_list(self, monkeypatch): + adapter = _make_gating_adapter(monkeypatch) + assert adapter._mention_patterns == [] + assert adapter._message_matches_mention_patterns("anything") is False + + def test_pattern_matches_text(self, monkeypatch): + adapter = _make_gating_adapter( + monkeypatch, extra={"mention_patterns": ["^hermes"]} + ) + assert adapter._message_matches_mention_patterns("hermes please help") is True + assert adapter._message_matches_mention_patterns("please hermes help") is False + + def test_pattern_is_case_insensitive(self, monkeypatch): + adapter = _make_gating_adapter( + monkeypatch, extra={"mention_patterns": ["^hermes"]} + ) + assert adapter._message_matches_mention_patterns("HERMES help") is True + + def test_invalid_regex_is_skipped_not_raised(self, monkeypatch): + adapter = _make_gating_adapter( + monkeypatch, + extra={"mention_patterns": ["[unclosed", "^valid"]}, + ) + # Invalid pattern dropped, valid one kept + assert len(adapter._mention_patterns) == 1 + assert adapter._message_matches_mention_patterns("valid trigger") is True + + def test_env_var_json_populates_patterns(self, monkeypatch): + adapter = _make_gating_adapter( + monkeypatch, + env={"DINGTALK_MENTION_PATTERNS": '["^bot", "^assistant"]'}, + ) + assert len(adapter._mention_patterns) == 2 + assert adapter._message_matches_mention_patterns("bot ping") is True + + def test_env_var_newline_fallback_when_not_json(self, monkeypatch): + adapter = _make_gating_adapter( + monkeypatch, + env={"DINGTALK_MENTION_PATTERNS": "^bot\n^assistant"}, + ) + assert len(adapter._mention_patterns) == 2 + + +class TestShouldProcessMessage: + + def test_dm_always_accepted(self, monkeypatch): + adapter = _make_gating_adapter( + monkeypatch, extra={"require_mention": True} + ) + msg = MagicMock(is_in_at_list=False) + assert adapter._should_process_message(msg, "hi", is_group=False, chat_id="dm1") is True + + def test_group_rejected_when_require_mention_and_no_trigger(self, monkeypatch): + adapter = _make_gating_adapter( + monkeypatch, extra={"require_mention": True} + ) + msg = MagicMock(is_in_at_list=False) + assert adapter._should_process_message(msg, "hi", is_group=True, chat_id="grp1") is False + + def test_group_accepted_when_require_mention_disabled(self, monkeypatch): + adapter = _make_gating_adapter( + monkeypatch, extra={"require_mention": False} + ) + msg = MagicMock(is_in_at_list=False) + assert adapter._should_process_message(msg, "hi", is_group=True, chat_id="grp1") is True + + def test_group_accepted_when_bot_is_mentioned(self, monkeypatch): + adapter = _make_gating_adapter( + monkeypatch, extra={"require_mention": True} + ) + msg = MagicMock(is_in_at_list=True) + assert adapter._should_process_message(msg, "hi", is_group=True, chat_id="grp1") is True + + def test_group_accepted_when_text_matches_wake_word(self, monkeypatch): + adapter = _make_gating_adapter( + monkeypatch, + extra={"require_mention": True, "mention_patterns": ["^hermes"]}, + ) + msg = MagicMock(is_in_at_list=False) + assert adapter._should_process_message(msg, "hermes help", is_group=True, chat_id="grp1") is True + + def test_group_accepted_when_chat_in_free_response_list(self, monkeypatch): + adapter = _make_gating_adapter( + monkeypatch, + extra={"require_mention": True, "free_response_chats": ["grp1"]}, + ) + msg = MagicMock(is_in_at_list=False) + assert adapter._should_process_message(msg, "hi", is_group=True, chat_id="grp1") is True + # Different group still blocked + assert adapter._should_process_message(msg, "hi", is_group=True, chat_id="grp2") is False +