ForcePilot/backend/test/unit/channel/channels/hooks/test_hooks.py
Kris 9e503becd3
Some checks failed
Deploy VitePress site to Pages / build (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
feat(plugin): 实现完整的插件注册管理系统
新增了插件相关的完整领域模型、应用服务、基础设施实现,包括:
1. 插件状态、注册模式、来源等基础枚举和数据结构
2. 插件清单解析、发现、加载工具类
3. 插件注册表领域服务和内存存储实现
4. 插件相关的命令、查询、事件定义
5. 插件REST API接口和DTO映射
6. 集成了原有通道适配器到插件系统
7. 新增内置插件注册和自动发现能力
2026-05-31 16:44:13 +08:00

198 lines
7.2 KiB
Python

from __future__ import annotations
import pytest
from yuxi.channel.channels.hooks.adapter import HooksAdapter, HOOKS_CAPABILITIES
from yuxi.channel.channels.hooks.config import HookMapping, HooksConfig
from yuxi.channel.channels.hooks.translator import HooksTranslator
from yuxi.channel.domain.model.shared.channel_type import ChannelType
class TestHookMapping:
def test_defaults(self) -> None:
m = HookMapping(match_path="/test")
assert m.match_path == "/test"
assert m.match_source == "*"
assert m.default_agent_id == 1
assert m.allowed_agent_ids == []
assert m.default_session_key == "main"
assert m.allow_request_session_key is False
assert m.allowed_session_key_prefixes == []
assert m.session_key_strategy == "main"
assert m.deliver is True
assert m.max_body_bytes == 256 * 1024
assert m.secret is None
def test_with_values(self) -> None:
m = HookMapping(
match_path="/webhook",
match_source="github",
default_agent_id=5,
secret="s3cret",
max_body_bytes=1024,
)
assert m.match_source == "github"
assert m.default_agent_id == 5
assert m.secret == "s3cret"
assert m.max_body_bytes == 1024
class TestHooksConfig:
def test_defaults(self) -> None:
config = HooksConfig()
assert config.mappings == []
def test_with_mappings(self) -> None:
m = HookMapping(match_path="/h1")
config = HooksConfig(mappings=[m])
assert len(config.mappings) == 1
class TestHooksTranslator:
def test_translate_basic(self) -> None:
mapping = HookMapping(match_path="/test", default_agent_id=3)
raw = {"id": "evt-1", "content": "hello"}
msg = HooksTranslator.translate(raw, mapping)
assert msg.message_id == "evt-1"
assert msg.content == "hello"
assert msg.channel_type == ChannelType.HOOKS
assert msg.agent_config_id == 3
assert msg.metadata["source"] == "hooks"
assert msg.metadata["match_path"] == "/test"
def test_translate_uses_text_fallback(self) -> None:
mapping = HookMapping(match_path="/test")
raw = {"text": "fallback text"}
msg = HooksTranslator.translate(raw, mapping)
assert msg.content == "fallback text"
def test_translate_agent_id_from_raw(self) -> None:
mapping = HookMapping(match_path="/test", default_agent_id=1, allowed_agent_ids=[1, 2, 3])
raw = {"id": "e1", "content": "hi", "agent_id": 2}
msg = HooksTranslator.translate(raw, mapping)
assert msg.agent_config_id == 2
def test_translate_agent_id_not_allowed_falls_back(self) -> None:
mapping = HookMapping(match_path="/test", default_agent_id=1, allowed_agent_ids=[1, 2])
raw = {"id": "e1", "content": "hi", "agent_id": 99}
msg = HooksTranslator.translate(raw, mapping)
assert msg.agent_config_id == 1
def test_translate_session_key_from_request(self) -> None:
mapping = HookMapping(
match_path="/test",
allow_request_session_key=True,
allowed_session_key_prefixes=["sess-"],
)
raw = {"id": "e1", "content": "hi", "session_key": "sess-abc"}
msg = HooksTranslator.translate(raw, mapping)
assert msg.metadata["session_key"] == "sess-abc"
def test_translate_session_key_prefix_not_allowed(self) -> None:
mapping = HookMapping(
match_path="/test",
allow_request_session_key=True,
allowed_session_key_prefixes=["valid-"],
default_session_key="default",
)
raw = {"id": "e1", "content": "hi", "session_key": "invalid-abc"}
msg = HooksTranslator.translate(raw, mapping)
assert msg.metadata["session_key"] == "default"
class TestHooksAdapter:
def test_channel_type(self) -> None:
adapter = HooksAdapter()
assert adapter.channel_type == ChannelType.HOOKS.value
def test_capabilities(self) -> None:
assert HOOKS_CAPABILITIES.media is False
assert HOOKS_CAPABILITIES.dm is True
assert HOOKS_CAPABILITIES.max_text_length == 32768
def test_get_default_config(self) -> None:
config = HooksAdapter.get_default_config()
assert "mappings" in config
def test_match_hook_exact(self) -> None:
adapter = HooksAdapter(
mappings=[
{"match_path": "/webhook", "match_source": "github"},
]
)
result = adapter.match_hook("/webhook", "github")
assert result is not None
assert result.match_path == "/webhook"
def test_match_hook_wildcard(self) -> None:
adapter = HooksAdapter(
mappings=[
{"match_path": "/hook1"},
]
)
result = adapter.match_hook("/hook1", "any-source")
assert result is not None
def test_match_hook_not_found(self) -> None:
adapter = HooksAdapter(
mappings=[
{"match_path": "/hook1"},
]
)
result = adapter.match_hook("/nonexistent")
assert result is None
@pytest.mark.asyncio
async def test_is_healthy_no_mappings(self) -> None:
adapter = HooksAdapter()
await adapter.open()
assert await adapter.is_healthy() is False
@pytest.mark.asyncio
async def test_is_healthy_with_mappings(self) -> None:
adapter = HooksAdapter(mappings=[{"match_path": "/h1"}])
await adapter.open()
assert await adapter.is_healthy() is True
@pytest.mark.asyncio
async def test_close(self) -> None:
adapter = HooksAdapter(mappings=[{"match_path": "/h1"}])
await adapter.open()
await adapter.close()
assert await adapter.is_healthy() is False
@pytest.mark.asyncio
async def test_receive_message(self) -> None:
adapter = HooksAdapter(mappings=[{"match_path": "/test"}])
raw = {"id": "e1", "content": "hi", "match_path": "/test"}
msg = await adapter.receive_message(raw)
assert msg.message_id == "e1"
@pytest.mark.asyncio
async def test_receive_message_no_mapping_raises(self) -> None:
adapter = HooksAdapter(mappings=[{"match_path": "/other"}])
raw = {"id": "e1", "content": "hi", "match_path": "/nonexistent"}
with pytest.raises(ValueError, match="no hook mapping"):
await adapter.receive_message(raw)
@pytest.mark.asyncio
async def test_send_message_always_succeeds(self) -> None:
adapter = HooksAdapter()
result = await adapter.send_message("sess-1", "hello", channel_type="hooks", metadata={})
assert result.success is True
@pytest.mark.asyncio
async def test_send_media_returns_true(self) -> None:
adapter = HooksAdapter()
result = await adapter.send_media("sess-1", url="http://x", media_type="image", metadata={})
assert result is True
def test_ws_connection_is_none(self) -> None:
adapter = HooksAdapter()
assert adapter.ws_connection is None
def test_route_contributor_has_router(self) -> None:
adapter = HooksAdapter()
contributor = adapter.route_contributor
assert contributor.router is not None