新增了插件相关的完整领域模型、应用服务、基础设施实现,包括: 1. 插件状态、注册模式、来源等基础枚举和数据结构 2. 插件清单解析、发现、加载工具类 3. 插件注册表领域服务和内存存储实现 4. 插件相关的命令、查询、事件定义 5. 插件REST API接口和DTO映射 6. 集成了原有通道适配器到插件系统 7. 新增内置插件注册和自动发现能力
174 lines
6.0 KiB
Python
174 lines
6.0 KiB
Python
from __future__ import annotations
|
|
|
|
from unittest.mock import AsyncMock
|
|
|
|
import pytest
|
|
|
|
from yuxi.channel.channels.web.adapter import WebAdapter
|
|
from yuxi.channel.channels.web.config import WebConfig
|
|
from yuxi.channel.channels.web.outbound import WEB_CAPABILITIES, WebOutbound
|
|
from yuxi.channel.channels.web.translator import WebTranslator
|
|
from yuxi.channel.domain.model.shared.channel_type import ChannelType
|
|
|
|
|
|
class TestWebTranslator:
|
|
def test_translate_message(self) -> None:
|
|
raw = {
|
|
"id": "msg-001",
|
|
"sender_id": "user-1",
|
|
"sender_name": "Alice",
|
|
"content": "hello world",
|
|
"metadata": {"key": "value"},
|
|
}
|
|
msg = WebTranslator.translate_message(raw)
|
|
assert msg.message_id == "msg-001"
|
|
assert msg.channel_type == ChannelType.WEB
|
|
assert msg.sender.id == "user-1"
|
|
assert msg.sender.name == "Alice"
|
|
assert msg.content == "hello world"
|
|
assert msg.metadata == {"key": "value"}
|
|
|
|
def test_translate_message_defaults(self) -> None:
|
|
raw = {}
|
|
msg = WebTranslator.translate_message(raw)
|
|
assert msg.message_id == ""
|
|
assert msg.content == ""
|
|
assert msg.sender.id == ""
|
|
assert msg.metadata == {}
|
|
|
|
|
|
class TestWebConfig:
|
|
def test_defaults(self) -> None:
|
|
config = WebConfig()
|
|
assert config.max_connections == 1000
|
|
|
|
def test_with_values(self) -> None:
|
|
config = WebConfig(max_connections=500)
|
|
assert config.max_connections == 500
|
|
|
|
|
|
class TestWebOutbound:
|
|
@pytest.mark.asyncio
|
|
async def test_send_text_without_sse_returns_false(self) -> None:
|
|
outbound = WebOutbound()
|
|
result = await outbound.send_text("sess-1", "hello")
|
|
assert result is False
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_send_text_with_sse_push(self) -> None:
|
|
sse = AsyncMock()
|
|
sse.push_event = AsyncMock(return_value=True)
|
|
outbound = WebOutbound(sse_push=sse)
|
|
result = await outbound.send_text("sess-1", "hello", metadata={"channel_type": "web"})
|
|
assert result is True
|
|
sse.push_event.assert_called_once()
|
|
event = sse.push_event.call_args[0][1]
|
|
assert event["type"] == "assistant_message"
|
|
assert event["content"] == "hello"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_send_text_sse_push_failure(self) -> None:
|
|
sse = AsyncMock()
|
|
sse.push_event = AsyncMock(return_value=False)
|
|
outbound = WebOutbound(sse_push=sse)
|
|
result = await outbound.send_text("sess-1", "hello")
|
|
assert result is False
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_send_typing_without_sse(self) -> None:
|
|
outbound = WebOutbound()
|
|
await outbound.send_typing("sess-1")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_send_typing_with_sse(self) -> None:
|
|
sse = AsyncMock()
|
|
sse.push_event = AsyncMock(return_value=True)
|
|
outbound = WebOutbound(sse_push=sse)
|
|
await outbound.send_typing("sess-1")
|
|
sse.push_event.assert_called_once()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_send_media_without_sse_returns_false(self) -> None:
|
|
outbound = WebOutbound()
|
|
result = await outbound.send_media("sess-1", url="http://x", media_type="image")
|
|
assert result is False
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_send_media_with_sse(self) -> None:
|
|
sse = AsyncMock()
|
|
sse.push_event = AsyncMock(return_value=True)
|
|
outbound = WebOutbound(sse_push=sse)
|
|
result = await outbound.send_media("sess-1", url="http://x", media_type="image")
|
|
assert result is True
|
|
|
|
|
|
class TestWebAdapter:
|
|
def test_channel_type(self) -> None:
|
|
adapter = WebAdapter()
|
|
assert adapter.channel_type == ChannelType.WEB.value
|
|
|
|
def test_capabilities(self) -> None:
|
|
assert WEB_CAPABILITIES.media is True
|
|
assert WEB_CAPABILITIES.streaming is True
|
|
assert WEB_CAPABILITIES.max_text_length == 32768
|
|
|
|
def test_get_default_config(self) -> None:
|
|
config = WebAdapter.get_default_config()
|
|
assert "max_connections" in config
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_is_healthy_before_open(self) -> None:
|
|
adapter = WebAdapter()
|
|
assert await adapter.is_healthy() is False
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_is_healthy_after_open(self) -> None:
|
|
adapter = WebAdapter()
|
|
await adapter.open()
|
|
assert await adapter.is_healthy() is True
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_close(self) -> None:
|
|
adapter = WebAdapter()
|
|
await adapter.open()
|
|
await adapter.close()
|
|
assert await adapter.is_healthy() is False
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_receive_message(self) -> None:
|
|
adapter = WebAdapter()
|
|
raw = {"id": "m1", "content": "hi", "sender_id": "u1"}
|
|
msg = await adapter.receive_message(raw)
|
|
assert msg.message_id == "m1"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_send_message_without_sse_returns_failure(self) -> None:
|
|
adapter = WebAdapter()
|
|
result = await adapter.send_message("sess-1", "hello", channel_type="web", metadata={})
|
|
assert result.success is False
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_send_message_with_sse(self) -> None:
|
|
sse = AsyncMock()
|
|
sse.push_event = AsyncMock(return_value=True)
|
|
adapter = WebAdapter(sse_push=sse)
|
|
await adapter.open()
|
|
result = await adapter.send_message("sess-1", "hello", channel_type="web", metadata={})
|
|
assert result.success is True
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_send_typing(self) -> None:
|
|
sse = AsyncMock()
|
|
sse.push_event = AsyncMock(return_value=True)
|
|
adapter = WebAdapter(sse_push=sse)
|
|
await adapter.send_typing("sess-1")
|
|
|
|
def test_ws_connection_is_none(self) -> None:
|
|
adapter = WebAdapter()
|
|
assert adapter.ws_connection is None
|
|
|
|
def test_route_contributor_has_router(self) -> None:
|
|
adapter = WebAdapter()
|
|
contributor = adapter.route_contributor
|
|
assert contributor.router is not None
|