from dataclasses import dataclass, field from typing import Any from unittest.mock import AsyncMock, MagicMock from yuxi.channel.capabilities import ChannelCapabilities @dataclass class SentMessage: text: str target_id: str = "" msg_id: str = "" kwargs: dict = field(default_factory=dict) def create_mock_channel_plugin( channel_id: str = "mock_channel", capabilities: ChannelCapabilities | None = None, *, with_gateway: bool = False, with_status: bool = False, account_ids: list[str] | None = None, ) -> Any: class _MockPlugin: id = channel_id name = f"Mock {channel_id}" order = 10 def __init__(self): self._sent: list[SentMessage] = [] plugin = _MockPlugin() _caps = capabilities or ChannelCapabilities(chat_types=["direct", "group"], message_types=["text"]) setattr(plugin.__class__, "capabilities", property(lambda self: _caps)) if account_ids is None: account_ids = ["mock_account"] def list_account_ids(config: dict | None = None) -> list[str]: if config is None: config = {} return list(account_ids) async def resolve_account(account_id: str) -> dict: return {"id": account_id, "enabled": True} def is_configured(account: dict) -> bool: return True plugin.list_account_ids = list_account_ids plugin.resolve_account = resolve_account plugin.is_configured = is_configured async def send_text(target_id: str, content: str, *, reply_to_id=None, thread_id=None, account_id=None) -> str: msg_id = f"mock_msg_{len(plugin._sent) + 1}" plugin._sent.append(SentMessage( text=content, target_id=target_id, msg_id=msg_id, kwargs={"reply_to_id": reply_to_id, "thread_id": thread_id, "account_id": account_id}, )) return msg_id async def send_media( target_id: str, media_url: str, media_type: str, *, reply_to_id=None, thread_id=None, account_id=None ) -> str: msg_id = f"mock_media_{len(plugin._sent) + 1}" plugin._sent.append(SentMessage( text=f"[media: {media_url}]", target_id=target_id, msg_id=msg_id, kwargs={"media_url": media_url, "media_type": media_type, "reply_to_id": reply_to_id}, )) return msg_id async def edit_message(target_id: str, message_id: str, content: str, *, thread_id=None, account_id=None) -> str: return message_id plugin.send_text = send_text plugin.send_media = send_media plugin.edit_message = edit_message async def check_allowlist(peer_id: str, channel_type: str) -> bool: return True plugin.check_allowlist = check_allowlist if with_gateway: async def start(ctx): return {"ok": True} async def stop(ctx): pass plugin.start = start plugin.stop = stop if with_status: async def probe(account: dict | None = None) -> bool: return True def build_summary(snapshot: object) -> dict: return {"ok": True} plugin.probe = probe plugin.build_summary = build_summary return plugin def create_mock_gateway_client(): client = MagicMock() client.call = AsyncMock(return_value={"success": True}) client.connect = AsyncMock() client.disconnect = AsyncMock() return client def create_mock_binding( agent_config_id: int = 1, channel: str = "mock_channel", account_id: str = "mock_account", dm_scope: str = "per-channel-peer", priority: int = 0, ): from yuxi.channel.routing.models import BindingMatch, PeerConstraint, PeerKind, RouteBinding peer = PeerConstraint(kind=PeerKind.DIRECT, peer_id=None) match = BindingMatch(channel=channel, account_id=account_id, peer=peer) return RouteBinding( agent_config_id=agent_config_id, match=match, dm_scope=dm_scope, priority=priority, )