feat: add unified WebSocket connection manager and routing
- Implemented UnifiedWebSocketManager for managing WebSocket connections, including subscription handling and message sending. - Created unified WebSocket router to handle client messages, including authentication, subscription, and chat session management. - Added support for logging and plugin progress subscriptions. - Enhanced error handling and response structure for WebSocket operations.
This commit is contained in:
136
pytests/test_runtime_business_hooks.py
Normal file
136
pytests/test_runtime_business_hooks.py
Normal file
@@ -0,0 +1,136 @@
|
||||
"""业务命名 Hook 集成测试。"""
|
||||
|
||||
from types import SimpleNamespace
|
||||
from typing import Any
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
# 确保项目根目录在 sys.path 中
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
||||
# SDK 包路径
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "packages", "maibot-plugin-sdk"))
|
||||
|
||||
|
||||
class _FakeHookManager:
|
||||
"""用于业务 Hook 测试的最小运行时管理器。"""
|
||||
|
||||
def __init__(self, responses: dict[str, SimpleNamespace]) -> None:
|
||||
"""初始化测试管理器。
|
||||
|
||||
Args:
|
||||
responses: 按 Hook 名称预设的返回结果映射。
|
||||
"""
|
||||
|
||||
self._responses = responses
|
||||
self.calls: list[tuple[str, dict[str, Any]]] = []
|
||||
|
||||
async def invoke_hook(self, hook_name: str, **kwargs: Any) -> SimpleNamespace:
|
||||
"""模拟调用运行时命名 Hook。
|
||||
|
||||
Args:
|
||||
hook_name: 目标 Hook 名称。
|
||||
**kwargs: 传入 Hook 的参数。
|
||||
|
||||
Returns:
|
||||
SimpleNamespace: 预设的 Hook 返回结果。
|
||||
"""
|
||||
|
||||
self.calls.append((hook_name, dict(kwargs)))
|
||||
return self._responses.get(hook_name, SimpleNamespace(kwargs=dict(kwargs), aborted=False))
|
||||
|
||||
|
||||
def test_builtin_hook_catalog_includes_new_business_hooks(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""内置 Hook 目录应包含三个业务系统新增的 Hook。"""
|
||||
|
||||
monkeypatch.setattr(sys, "exit", lambda code=0: None)
|
||||
from src.plugin_runtime.hook_catalog import register_builtin_hook_specs
|
||||
from src.plugin_runtime.host.hook_spec_registry import HookSpecRegistry
|
||||
|
||||
registry = HookSpecRegistry()
|
||||
hook_names = {spec.name for spec in register_builtin_hook_specs(registry)}
|
||||
|
||||
assert "emoji.maisaka.before_select" in hook_names
|
||||
assert "emoji.register.after_build_emotion" in hook_names
|
||||
assert "jargon.extract.before_persist" in hook_names
|
||||
assert "jargon.query.after_search" in hook_names
|
||||
assert "expression.select.before_select" in hook_names
|
||||
assert "expression.learn.before_upsert" in hook_names
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_send_emoji_for_maisaka_can_be_aborted_by_hook(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""表情包系统应允许在选择前被 Hook 中止。"""
|
||||
|
||||
from src.chat.emoji_system import maisaka_tool
|
||||
|
||||
fake_manager = _FakeHookManager(
|
||||
{
|
||||
"emoji.maisaka.before_select": SimpleNamespace(
|
||||
kwargs={"abort_message": "插件阻止了表情发送。"},
|
||||
aborted=True,
|
||||
)
|
||||
}
|
||||
)
|
||||
monkeypatch.setattr(maisaka_tool, "_get_runtime_manager", lambda: fake_manager)
|
||||
|
||||
result = await maisaka_tool.send_emoji_for_maisaka(stream_id="stream-1", requested_emotion="开心")
|
||||
|
||||
assert result.success is False
|
||||
assert result.message == "插件阻止了表情发送。"
|
||||
assert fake_manager.calls[0][0] == "emoji.maisaka.before_select"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_jargon_extract_can_be_aborted_before_persist(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""黑话提取结果应允许在写库前被 Hook 中止。"""
|
||||
|
||||
from src.learners.jargon_miner import JargonMiner
|
||||
|
||||
fake_manager = _FakeHookManager(
|
||||
{
|
||||
"jargon.extract.before_persist": SimpleNamespace(
|
||||
kwargs={"entries": []},
|
||||
aborted=True,
|
||||
)
|
||||
}
|
||||
)
|
||||
monkeypatch.setattr(JargonMiner, "_get_runtime_manager", staticmethod(lambda: fake_manager))
|
||||
|
||||
miner = JargonMiner(session_id="session-1", session_name="测试会话")
|
||||
await miner.process_extracted_entries(
|
||||
[{"content": "yyds", "raw_content": {"[1] yyds 太强了"}}],
|
||||
)
|
||||
|
||||
assert fake_manager.calls[0][0] == "jargon.extract.before_persist"
|
||||
assert fake_manager.calls[0][1]["session_id"] == "session-1"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_expression_selection_can_be_aborted_by_hook(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""表达方式选择流程应允许在开始前被 Hook 中止。"""
|
||||
|
||||
from src.learners.expression_selector import ExpressionSelector
|
||||
|
||||
fake_manager = _FakeHookManager(
|
||||
{
|
||||
"expression.select.before_select": SimpleNamespace(
|
||||
kwargs={},
|
||||
aborted=True,
|
||||
)
|
||||
}
|
||||
)
|
||||
monkeypatch.setattr(ExpressionSelector, "_get_runtime_manager", staticmethod(lambda: fake_manager))
|
||||
monkeypatch.setattr(ExpressionSelector, "can_use_expression_for_chat", lambda self, chat_id: True)
|
||||
|
||||
selector = ExpressionSelector()
|
||||
selected_expressions, selected_ids = await selector.select_suitable_expressions(
|
||||
chat_id="session-1",
|
||||
chat_info="用户刚刚发来一条消息。",
|
||||
)
|
||||
|
||||
assert selected_expressions == []
|
||||
assert selected_ids == []
|
||||
assert fake_manager.calls[0][0] == "expression.select.before_select"
|
||||
Reference in New Issue
Block a user