ForcePilot/backend/test/unit/channels/test_manager.py
Kris 3264900bc9 test: 新增多渠道单元测试用例并配置测试环境变量
新增了Twitch、Telegram、Discord、Slack、Mattermost、WeChat、Zalo等多渠道的单元测试用例,覆盖了令牌处理、速率限制、消息去重、会话解析、格式转换、安全策略等模块
同时在测试配置中添加了测试用的OpenAI API密钥环境变量
2026-05-12 00:56:47 +08:00

347 lines
12 KiB
Python

from __future__ import annotations
import asyncio
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from yuxi.channels.base import BaseChannelAdapter
from yuxi.channels.infra.circuit_breaker import CircuitBreaker
from yuxi.channels.exceptions import ChannelException
from yuxi.channels.manager import ChannelManager
from yuxi.channels.models import (
ChannelIdentity,
ChannelMessage,
ChannelResponse,
ChannelStatus,
ChannelType,
DeliveryResult,
HealthStatus,
)
from yuxi.channels.registry import ChannelRegistry, _BUILTIN_ADAPTERS
def _make_adapter_cls(channel_id: str = "test_chan", channel_type: ChannelType = ChannelType.WEBCHAT):
cid = channel_id
ctype = channel_type
class _TestAdapter(BaseChannelAdapter):
channel_id = cid
channel_type = ctype
text_chunk_limit = 4096
supports_markdown = False
supports_streaming = False
max_media_size_mb = 100
async def connect(self) -> None: ...
async def disconnect(self) -> None: ...
async def send(self, response) -> DeliveryResult: ...
def normalize_inbound(self, raw) -> ChannelMessage: ...
def format_outbound(self, response): ...
async def health_check(self) -> HealthStatus:
return HealthStatus(status="healthy")
return _TestAdapter
def _make_adapter_instance():
adapter = AsyncMock(spec=BaseChannelAdapter)
adapter.channel_id = "test_chan"
adapter.channel_type = ChannelType.WEBCHAT
adapter.text_chunk_limit = 4096
adapter.supports_markdown = False
adapter.supports_streaming = False
adapter.max_media_size_mb = 100
adapter.config = {"enabled": True, "display_name": "Test"}
adapter.pre_connect = AsyncMock(return_value={})
adapter.connect = AsyncMock(return_value=None)
adapter.disconnect = AsyncMock(return_value=None)
adapter.send = AsyncMock(return_value=DeliveryResult(success=True))
adapter.health_check = AsyncMock(return_value=HealthStatus(status="healthy"))
adapter.on_message = MagicMock()
return adapter
class TestChannelManagerInit:
def test_init_with_defaults(self):
manager = ChannelManager()
assert isinstance(manager._registry, ChannelRegistry)
assert manager._router is not None
assert manager._adapters == {}
assert manager._initialized is False
def test_init_with_custom_registry(self):
registry = ChannelRegistry()
manager = ChannelManager(registry=registry)
assert manager._registry is registry
def test_init_with_custom_router(self):
from yuxi.channels.router import MessageRouter
router = MessageRouter()
manager = ChannelManager(router=router)
assert manager._router is router
def test_manager_not_initialized_after_init(self):
manager = ChannelManager()
assert manager._initialized is False
class TestChannelLifecycle:
@pytest.mark.asyncio
async def test_start_channel_success(self):
manager = ChannelManager()
TestCls = _make_adapter_cls("test_chan")
manager._registry.register("test_chan", TestCls)
manager._health_check_loop = AsyncMock()
await manager.start_channel("test_chan", {"enabled": True})
assert "test_chan" in manager._adapters
manager._health_check_loop.assert_called_once()
await manager.shutdown()
@pytest.mark.asyncio
async def test_start_channel_already_running(self):
manager = ChannelManager()
TestCls = _make_adapter_cls("test_chan")
manager._registry.register("test_chan", TestCls)
manager._health_check_loop = AsyncMock()
await manager.start_channel("test_chan", {"enabled": True})
assert "test_chan" in manager._adapters
await manager.start_channel("test_chan", {"enabled": True})
assert "test_chan" in manager._adapters
await manager.shutdown()
@pytest.mark.asyncio
async def test_start_channel_unregistered(self):
manager = ChannelManager()
with pytest.raises(ValueError, match="No adapter registered"):
await manager.start_channel("nonexistent")
@pytest.mark.asyncio
async def test_stop_channel_success(self):
manager = ChannelManager()
TestCls = _make_adapter_cls("test_chan")
manager._registry.register("test_chan", TestCls)
manager._health_check_loop = AsyncMock()
await manager.start_channel("test_chan", {"enabled": True})
assert "test_chan" in manager._adapters
await manager.stop_channel("test_chan")
assert "test_chan" not in manager._adapters
@pytest.mark.asyncio
async def test_stop_channel_not_running(self):
manager = ChannelManager()
await manager.stop_channel("nonexistent")
@pytest.mark.asyncio
async def test_restart_channel(self):
manager = ChannelManager()
TestCls = _make_adapter_cls("test_chan")
manager._registry.register("test_chan", TestCls)
manager._health_check_loop = AsyncMock()
await manager.start_channel("test_chan", {"enabled": True})
await manager.restart_channel("test_chan")
assert "test_chan" in manager._adapters
await manager.shutdown()
class TestHealthCheck:
@pytest.mark.asyncio
async def test_health_check_healthy(self):
manager = ChannelManager()
TestCls = _make_adapter_cls("test_chan")
manager._registry.register("test_chan", TestCls)
manager._health_check_loop = AsyncMock()
await manager.start_channel("test_chan", {"enabled": True})
manager._health_check_loop.assert_called_once()
await manager.shutdown()
@pytest.mark.asyncio
async def test_health_check_loop_runs(self):
manager = ChannelManager()
TestCls = _make_adapter_cls("test_chan")
manager._registry.register("test_chan", TestCls)
loop_count = 0
async def mock_health_loop(channel_id):
nonlocal loop_count
loop_count += 1
manager._health_check_loop = mock_health_loop
await manager.start_channel("test_chan", {"enabled": True})
await asyncio.sleep(0)
assert loop_count == 1
await manager.shutdown()
class TestChannelStatus:
@pytest.mark.asyncio
async def test_get_all_channels_status(self):
manager = ChannelManager()
TestCls = _make_adapter_cls("test_chan")
manager._registry.register("test_chan", TestCls)
status = await manager.get_channel_status()
assert "channels" in status
assert "test_chan" in status["channels"]
@pytest.mark.asyncio
async def test_get_single_channel_status(self):
manager = ChannelManager()
TestCls = _make_adapter_cls("test_chan")
manager._registry.register("test_chan", TestCls)
status = await manager.get_channel_status("test_chan")
assert status["channel_id"] == "test_chan"
assert "channel_type" in status
@pytest.mark.asyncio
async def test_get_not_found_channel(self):
manager = ChannelManager()
status = await manager.get_channel_status("nonexistent")
assert status["status"] == "not_found"
@pytest.mark.asyncio
async def test_status_includes_circuit_state(self):
manager = ChannelManager()
TestCls = _make_adapter_cls("test_chan")
manager._registry.register("test_chan", TestCls)
manager._health_check_loop = AsyncMock()
await manager.start_channel("test_chan", {"enabled": True})
status = await manager.get_channel_status("test_chan")
assert "circuit_state" in status
await manager.shutdown()
class TestRateLimiting:
@pytest.mark.asyncio
async def test_rate_limit_allows(self):
manager = ChannelManager()
result = await manager.check_rate_limit("key1", 5, 60)
assert result is True
@pytest.mark.asyncio
async def test_rate_limit_blocks(self):
manager = ChannelManager()
max_req = 3
for _ in range(max_req):
assert await manager.check_rate_limit("key2", max_req, 60) is True
assert await manager.check_rate_limit("key2", max_req, 60) is False
@pytest.mark.asyncio
async def test_rate_limit_different_keys_independent(self):
manager = ChannelManager()
for _ in range(5):
assert await manager.check_rate_limit("key_a", 5, 60) is True
assert await manager.check_rate_limit("key_b", 5, 60) is True
class TestConfig:
@pytest.mark.asyncio
async def test_update_channel_config(self):
manager = ChannelManager()
TestCls = _make_adapter_cls("test_chan")
manager._registry.register("test_chan", TestCls)
manager._health_check_loop = AsyncMock()
await manager.start_channel("test_chan", {"enabled": True})
result = await manager.update_channel_config("test_chan", {"display_name": "NewName"})
assert result["config_updated"] is True
await manager.shutdown()
@pytest.mark.asyncio
async def test_update_config_channel_not_found(self):
manager = ChannelManager()
with pytest.raises(ChannelException, match="not found"):
await manager.update_channel_config("nonexistent", {})
@pytest.mark.asyncio
async def test_test_channel_healthy(self):
manager = ChannelManager()
TestCls = _make_adapter_cls("test_chan")
manager._registry.register("test_chan", TestCls)
manager._health_check_loop = AsyncMock()
await manager.start_channel("test_chan", {"enabled": True})
result = await manager.test_channel("test_chan")
assert result["test_result"] == "success"
assert "latency_ms" in result
await manager.shutdown()
@pytest.mark.asyncio
async def test_test_channel_not_running(self):
manager = ChannelManager()
result = await manager.test_channel("nonexistent")
assert result["test_result"] == "failure"
assert "not running" in result["error"]
class TestSendOutbound:
@pytest.mark.asyncio
async def test_send_outbound_success(self):
manager = ChannelManager()
TestCls = _make_adapter_cls("test_chan")
manager._registry.register("test_chan", TestCls)
manager._health_check_loop = AsyncMock()
await manager.start_channel("test_chan", {"enabled": True})
adapter = manager._adapters["test_chan"]
adapter.send = AsyncMock(return_value=DeliveryResult(success=True))
identity = ChannelIdentity(
channel_id="test_chan", channel_type=ChannelType.WEBCHAT,
channel_user_id="u1", channel_chat_id="c1",
)
response = ChannelResponse(identity=identity, content="ok")
await manager.send_outbound("test_chan", response)
adapter.send.assert_called_once()
await manager.shutdown()
@pytest.mark.asyncio
async def test_send_outbound_channel_not_found(self):
manager = ChannelManager()
identity = ChannelIdentity(
channel_id="test_chan", channel_type=ChannelType.WEBCHAT,
channel_user_id="u1", channel_chat_id="c1",
)
response = ChannelResponse(identity=identity, content="ok")
with pytest.raises(ChannelException, match="not found"):
await manager.send_outbound("nonexistent", response)
class TestShutdown:
@pytest.mark.asyncio
async def test_shutdown_all_channels(self):
manager = ChannelManager()
TestCls = _make_adapter_cls("test_chan")
manager._registry.register("test_chan", TestCls)
manager._health_check_loop = AsyncMock()
await manager.start_channel("test_chan", {"enabled": True})
assert "test_chan" in manager._adapters
await manager.shutdown()
assert "test_chan" not in manager._adapters
assert manager._initialized is False