ForcePilot/backend/test/unit/channel/plugins/test_ports.py
Kris bab30f2715
Some checks failed
Deploy VitePress site to Pages / build (push) Has been cancelled
Ruff Format Check / Ruff Format & Lint (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
feat:0715
2026-07-15 12:30:58 +08:00

390 lines
14 KiB
Python

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