diff --git a/gateway/run.py b/gateway/run.py index 5a438d25b9..c1106b1350 100644 --- a/gateway/run.py +++ b/gateway/run.py @@ -17691,6 +17691,10 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew # _app_lock_identity, so including it lets the multiplex guard # refuse cloned profiles competing for the same Feishu app. "_app_id", + # Same class: Teams (client_id/client_secret) and WeCom + # (bot_id/secret) authenticate with an app-style id pair too. + "_client_id", + "_bot_id", ): val = getattr(adapter, attr, None) if isinstance(val, str) and val.strip(): diff --git a/tests/gateway/test_multiplex_adapter_registry.py b/tests/gateway/test_multiplex_adapter_registry.py index 859d56cdb2..aa2ee8a1a1 100644 --- a/tests/gateway/test_multiplex_adapter_registry.py +++ b/tests/gateway/test_multiplex_adapter_registry.py @@ -1,6 +1,7 @@ """Phase 3: secondary-profile adapter registry + same-token conflict detection.""" import logging import asyncio +import types from contextlib import contextmanager from pathlib import Path from unittest.mock import AsyncMock, MagicMock @@ -72,6 +73,18 @@ class TestCredentialFingerprint: assert fp_a is not None and fp_b is not None assert fp_a != fp_b + @pytest.mark.parametrize("attr", ["_client_id", "_bot_id"]) + def test_reads_app_style_ids_teams_wecom(self, attr): + """Teams (_client_id) and WeCom (_bot_id) are the same class as Feishu: + id/secret pairs, no token — cloned profiles must collide.""" + a = types.SimpleNamespace(**{attr: "app-1"}) + b = types.SimpleNamespace(**{attr: "app-1"}) + c = types.SimpleNamespace(**{attr: "app-2"}) + fp = GatewayRunner._adapter_credential_fingerprint + assert fp(a) is not None and fp(a) == fp(b) + assert fp(a) != fp(c) + assert "app-1" not in fp(a) + def test_reads_config_token(self): """Adapters like Discord store token on `config`, not on self.