from __future__ import annotations from typing import Any import pytest from yuxi.channel.plugins.builders import _BaseChannelPlugin from yuxi.channel.plugins.protocol import ( ChannelHealthStatus, ChannelMeta, ChannelPlugin, DeliveryCapabilities, InboundMedia, InboundMessage, InboundRequest, OutboundMessage, QRLoginSession, SessionConversationRef, Transport, ) from yuxi.channel.ports import ( AuthPort, BindingPort, ConfigPort, InboundPort, LifecyclePort, MetaPort, OutboundPort, SecurityPort, SessionPort, StatusPort, ThreadingPort, TransportPort, ) from yuxi.channel.security.models import DmPolicy, GroupPolicy, RateLimitPolicy class TestPortsAreRuntimeCheckable: def test_meta_port_is_runtime_checkable(self): assert getattr(MetaPort, "_is_runtime_checkable", False) def test_config_port_is_runtime_checkable(self): assert getattr(ConfigPort, "_is_runtime_checkable", False) def test_inbound_port_is_runtime_checkable(self): assert getattr(InboundPort, "_is_runtime_checkable", False) def test_outbound_port_is_runtime_checkable(self): assert getattr(OutboundPort, "_is_runtime_checkable", False) def test_transport_port_is_runtime_checkable(self): assert getattr(TransportPort, "_is_runtime_checkable", False) def test_security_port_is_runtime_checkable(self): assert getattr(SecurityPort, "_is_runtime_checkable", False) def test_status_port_is_runtime_checkable(self): assert getattr(StatusPort, "_is_runtime_checkable", False) def test_lifecycle_port_is_runtime_checkable(self): assert getattr(LifecyclePort, "_is_runtime_checkable", False) def test_session_port_is_runtime_checkable(self): assert getattr(SessionPort, "_is_runtime_checkable", False) def test_auth_port_is_runtime_checkable(self): assert getattr(AuthPort, "_is_runtime_checkable", False) def test_binding_port_is_runtime_checkable(self): assert getattr(BindingPort, "_is_runtime_checkable", False) def test_threading_port_is_runtime_checkable(self): assert getattr(ThreadingPort, "_is_runtime_checkable", False) class TestMinimalPortImplementations: def test_minimal_meta_port_implementation(self): class MinimalMeta: def get_meta(self) -> ChannelMeta: return ChannelMeta(channel_type="test", display_name="Test") assert isinstance(MinimalMeta(), MetaPort) def test_minimal_config_port_implementation(self): class MinimalConfig: def validate_config(self, config: dict) -> tuple[bool, list[str]]: return True, [] def list_account_ids(self, config: dict) -> list[str]: return [] def resolve_account(self, config: dict, account_id: str | None = None) -> dict: return config assert isinstance(MinimalConfig(), ConfigPort) def test_minimal_inbound_port_implementation(self): class MinimalInbound: async def validate_webhook(self, request: InboundRequest) -> bool: return True async def preprocess_event( self, request: InboundRequest, config: dict, account_id: str ) -> tuple[bool, dict[str, Any] | None, dict | None]: return True, None, {} async def normalize_inbound(self, request: InboundRequest) -> InboundMessage: return InboundMessage(channel_type="test", account_id="acc") async def build_webhook_response( self, request: InboundRequest, inbound: InboundMessage ) -> dict[str, Any] | None: return None async def resolve_account_id_from_webhook(self, request: InboundRequest) -> str | None: return None assert isinstance(MinimalInbound(), InboundPort) def test_minimal_outbound_port_implementation(self): class MinimalOutbound: async def format_outbound(self, message: OutboundMessage, config: dict | None = None) -> dict: return {} async def send_message( self, channel_session_id: str, payload: dict, config: dict | None = None ) -> str | None: return None def chunk_text(self, text: str, limit: int) -> list[str]: return [] def get_delivery_capabilities(self) -> DeliveryCapabilities: return DeliveryCapabilities() async def enrich_outbound( self, payload: dict, message: OutboundMessage, config: dict | None = None ) -> dict: return payload async def upload_media(self, media: InboundMedia | dict, config: dict | None = None) -> dict | None: return None def supports_batch_send(self, config: dict | None = None) -> bool: return False async def send_batch( self, channel_session_id: str, payloads: list[dict], config: dict | None = None ) -> list[str | None]: return [] async def send_typing_indicator(self, channel_session_id: str, config: dict | None = None) -> None: return None def supports_content_type(self, content_type: str, config: dict | None = None) -> bool: return False assert isinstance(MinimalOutbound(), OutboundPort) def test_minimal_transport_port_implementation(self): class MinimalTransport: async def create_transport( self, config: dict, account_id: str, credentials: dict | None = None ) -> Transport | None: return None async def on_transport_message(self, raw: bytes, config: dict, account_id: str) -> InboundMessage | None: return None assert isinstance(MinimalTransport(), TransportPort) def test_minimal_security_port_implementation(self): class MinimalSecurity: def resolve_dm_policy(self, config: dict, account_id: str | None = None) -> DmPolicy | None: return None def resolve_group_policy(self, config: dict, account_id: str | None = None) -> GroupPolicy | None: return None def resolve_rate_limit_policy(self, config: dict, account_id: str | None = None) -> RateLimitPolicy | None: return None assert isinstance(MinimalSecurity(), SecurityPort) def test_minimal_status_port_implementation(self): class MinimalStatus: async def health_check(self, config: dict, account_id: str) -> ChannelHealthStatus: return ChannelHealthStatus(healthy=True, state="ok", enabled=True) assert isinstance(MinimalStatus(), StatusPort) def test_minimal_lifecycle_port_implementation(self): class MinimalLifecycle: async def on_config_changed(self, config: dict, account_id: str) -> None: return None async def on_account_removed(self, account_id: str) -> None: return None async def on_channel_enabled(self, config: dict, account_id: str) -> None: return None async def on_channel_disabled(self, config: dict, account_id: str) -> None: return None async def run_startup_maintenance(self, config: dict, account_id: str) -> None: return None assert isinstance(MinimalLifecycle(), LifecyclePort) def test_minimal_session_port_implementation(self): class MinimalSession: def resolve_session_conversation(self, chat_type: str, raw_id: str) -> SessionConversationRef | None: return None def parse_session_key(self, inbound: InboundMessage) -> str: return "key" assert isinstance(MinimalSession(), SessionPort) def test_minimal_auth_port_implementation(self): class MinimalAuth: def supports_qr_login(self, config: dict | None = None) -> bool: return False async def create_qr_login_session(self, config: dict, account_id: str) -> QRLoginSession: return QRLoginSession(session_id="s") async def poll_qr_login_status( self, session: QRLoginSession, config: dict, account_id: str ) -> QRLoginSession: return session async def load_qr_credentials(self, config: dict, account_id: str) -> QRLoginSession | None: return None async def logout_qr_login(self, config: dict, account_id: str) -> bool: return False assert isinstance(MinimalAuth(), AuthPort) def test_minimal_binding_port_implementation(self): class MinimalBinding: def compile_binding(self, config: dict, inbound: InboundMessage) -> Any | None: return None def match_binding(self, config: dict, binding: Any, inbound: InboundMessage) -> bool: return False assert isinstance(MinimalBinding(), BindingPort) def test_minimal_threading_port_implementation(self): class MinimalThreading: def resolve_reply_to_mode(self, config: dict, inbound: InboundMessage) -> str | None: return None def resolve_auto_thread_id(self, config: dict, inbound: InboundMessage) -> str | None: return None assert isinstance(MinimalThreading(), ThreadingPort) class TestBaseChannelPluginPortCompliance: @pytest.fixture def plugin(self): meta = ChannelMeta(channel_type="test", display_name="Test") return _BaseChannelPlugin(meta=meta, adapters={}) def test_is_channel_plugin(self, plugin): assert isinstance(plugin, ChannelPlugin) def test_implements_meta_port(self, plugin): assert isinstance(plugin, MetaPort) def test_implements_config_port(self, plugin): assert isinstance(plugin, ConfigPort) def test_implements_inbound_port(self, plugin): assert isinstance(plugin, InboundPort) def test_implements_outbound_port(self, plugin): assert isinstance(plugin, OutboundPort) def test_implements_transport_port(self, plugin): assert isinstance(plugin, TransportPort) def test_implements_security_port(self, plugin): assert isinstance(plugin, SecurityPort) def test_implements_status_port(self, plugin): assert isinstance(plugin, StatusPort) def test_implements_lifecycle_port(self, plugin): assert isinstance(plugin, LifecyclePort) def test_implements_session_port(self, plugin): assert isinstance(plugin, SessionPort) def test_implements_auth_port(self, plugin): assert isinstance(plugin, AuthPort) def test_implements_binding_port(self, plugin): assert isinstance(plugin, BindingPort) def test_implements_threading_port(self, plugin): assert isinstance(plugin, ThreadingPort) class TestMetaMixin: def test_init_stores_meta(self): from yuxi.channel.ports import MetaMixin meta = ChannelMeta(channel_type="test", display_name="Test") mixin = MetaMixin(meta) assert mixin.get_meta() is meta class TestOutboundMixin: @pytest.fixture def mixin(self): from yuxi.channel.ports import OutboundMixin return OutboundMixin() async def test_format_outbound_returns_content(self, mixin): msg = OutboundMessage(content="hello") result = await mixin.format_outbound(msg) assert result == {"content": "hello"} def test_get_delivery_capabilities_defaults(self, mixin): caps = mixin.get_delivery_capabilities() assert isinstance(caps, DeliveryCapabilities) assert caps.max_text_length == 4000 assert caps.supports_markdown is False assert caps.supports_interactive is False assert caps.supports_media is False def test_chunk_text_short(self, mixin): assert mixin.chunk_text("hello", 10) == ["hello"] def test_chunk_text_splits(self, mixin): assert mixin.chunk_text("abcdef", 2) == ["ab", "cd", "ef"] def test_supports_content_type_text(self, mixin): assert mixin.supports_content_type("text") is True def test_supports_content_type_defaults(self, mixin): assert mixin.supports_content_type("markdown") is False assert mixin.supports_content_type("image") is False assert mixin.supports_content_type("interactive") is False class TestSecurityMixin: @pytest.fixture def mixin(self): from yuxi.channel.ports import SecurityMixin return SecurityMixin() def test_resolve_dm_policy(self, mixin): policy = mixin.resolve_dm_policy( {"dm_policy": {"mode": "allow_from", "allow_list": ["u1"], "deny_list": ["u2"]}} ) assert policy.mode == "allow_from" assert policy.allow_list == ["u1"] assert policy.deny_list == ["u2"] def test_resolve_dm_policy_defaults(self, mixin): policy = mixin.resolve_dm_policy({}) assert policy.mode == "open" assert policy.allow_list == [] assert policy.deny_list == [] def test_resolve_group_policy(self, mixin): policy = mixin.resolve_group_policy( {"group_policy": {"require_mention": True, "allow_groups": ["g1"], "deny_groups": ["g2"]}} ) assert policy.require_mention is True assert policy.allow_groups == ["g1"] assert policy.deny_groups == ["g2"] def test_resolve_rate_limit_policy(self, mixin): policy = mixin.resolve_rate_limit_policy( {"rate_limit": {"max_requests_per_minute": 120, "max_concurrent_agent_runs": 10}} ) assert policy.max_requests_per_minute == 120 assert policy.max_concurrent_agent_runs == 10 def test_resolve_rate_limit_policy_returns_none_when_missing(self, mixin): assert mixin.resolve_rate_limit_policy({}) is None