Files
hermes-agent/tests/gateway/feishu_helpers.py
Roy-oss1 b94cb8e2c4 feat(feishu): operator-configurable bot admission and mention policy
Add two operator-facing toggles for inbound Feishu admission, enabling
bot-to-bot scenarios such as A2A orchestration and inter-bot
notifications:

  FEISHU_ALLOW_BOTS=none|mentions|all   (default: none)
    Accept messages from other bots. `mentions` requires the peer
    bot to @-mention Hermes; `all` admits every peer-bot message.

  FEISHU_REQUIRE_MENTION=true|false     (default: true)
    Whether group messages must @-mention the bot. Override per-chat
    via `group_rules.<chat_id>.require_mention` in config.yaml.

Defaults preserve prior behavior. Self-echo protection is always on:
when the bot's identity is unresolved (auto-detection failed and
FEISHU_BOT_OPEN_ID unset), peer-bot messages are rejected fail-closed
to avoid feedback loops.

Admitted peer bots bypass the human-user allowlist
(FEISHU_ALLOWED_USERS) to match existing Discord behavior; humans
still need an explicit allowlist entry. yaml feishu.allow_bots is
bridged to the env var so the adapter and gateway auth layer share
one source of truth.

Resolving peer-bot display names requires the
application:bot.basic_info:read scope; without it, peers still route
but appear as their open_id.

Test: tests/gateway/test_feishu_bot_admission.py covers the admission
pipeline, group-policy bot-bypass, hydration, and event-dispatch
plumbing as a parametrized matrix.

Change-Id: I363cccb578c2a5c8b8bf0f0a890c01c89909e256
2026-04-30 20:30:31 -07:00

66 lines
2.0 KiB
Python

"""Shared fixtures for Feishu adapter tests (admission, group policy, dispatch)."""
from __future__ import annotations
import threading
from types import SimpleNamespace
from typing import Any, Optional
def make_sender(sender_type: str = "user", open_id: str = "ou_human",
user_id: Optional[str] = None, union_id: Optional[str] = None) -> Any:
return SimpleNamespace(
sender_type=sender_type,
sender_id=SimpleNamespace(open_id=open_id, user_id=user_id, union_id=union_id),
)
def make_message(message_id: str = "om_xxx", chat_type: str = "p2p",
chat_id: str = "oc_1", mentions: Optional[list] = None) -> Any:
return SimpleNamespace(
message_id=message_id,
chat_type=chat_type,
chat_id=chat_id,
mentions=mentions,
content="",
message_type="text",
)
def make_adapter_skeleton(
*,
bot_open_id: str = "ou_me",
bot_user_id: str = "",
allow_bots: str = "none",
require_mention: bool = True,
group_policy: str = "allowlist",
) -> Any:
from gateway.platforms.feishu import FeishuAdapter
adapter = object.__new__(FeishuAdapter)
adapter._bot_open_id = bot_open_id
adapter._bot_user_id = bot_user_id
adapter._bot_name = ""
adapter._app_id = ""
adapter._admins = set()
adapter._group_rules = {}
adapter._group_policy = group_policy
adapter._default_group_policy = group_policy
adapter._allowed_group_users = frozenset()
adapter._allow_bots = allow_bots
adapter._require_mention = require_mention
return adapter
def install_dedup_state(adapter: Any, seen: Optional[dict] = None) -> None:
adapter._seen_message_ids = dict(seen) if seen else {}
adapter._seen_message_order = list((seen or {}).keys())
adapter._dedup_cache_size = 100
adapter._dedup_lock = threading.Lock()
adapter._dedup_state_path = None
adapter._persist_seen_message_ids = lambda: None
def stub_mention(adapter: Any, mentions_self: bool) -> None:
adapter._mentions_self = lambda _message: mentions_self