ForcePilot/backend/test/unit/yuxi/channel/test_bootstrap.py

168 lines
6.2 KiB
Python
Raw Normal View History

2026-07-15 12:30:58 +08:00
from __future__ import annotations
from unittest.mock import AsyncMock, MagicMock
import pytest
from fastapi import FastAPI
from yuxi.channel import ChannelGatewayBootstrap
@pytest.fixture
def app():
return FastAPI()
@pytest.fixture
def bootstrap():
return ChannelGatewayBootstrap()
@pytest.fixture
def start_mocks(monkeypatch):
"""为 start() 测试准备统一 mock。"""
mocks = {
"load_plugins": MagicMock(),
"registry": MagicMock(),
"config_manager": MagicMock(),
"lifecycle": AsyncMock(),
"dispatcher": AsyncMock(),
"router": MagicMock(),
"session_manager": MagicMock(),
"redis": MagicMock(),
"security_policy": MagicMock(),
"gateway": AsyncMock(),
"dummy_task": AsyncMock(),
}
monkeypatch.setattr("yuxi.channel.bootstrap.load_plugins", mocks["load_plugins"])
monkeypatch.setattr("yuxi.channel.bootstrap.get_registry", lambda: mocks["registry"])
monkeypatch.setattr("yuxi.channel.bootstrap.ChannelConfigManager", lambda: mocks["config_manager"])
monkeypatch.setattr(
"yuxi.channel.bootstrap.ChannelLifecycleManager",
lambda r, c: mocks["lifecycle"],
)
monkeypatch.setattr("yuxi.channel.bootstrap.OutboundDispatcher", lambda **kwargs: mocks["dispatcher"])
monkeypatch.setattr("yuxi.channel.bootstrap.BindingRouter", lambda: mocks["router"])
monkeypatch.setattr("yuxi.channel.bootstrap.SessionManager", lambda r: mocks["session_manager"])
monkeypatch.setattr("yuxi.channel.bootstrap.get_redis_client", AsyncMock(return_value=mocks["redis"]))
monkeypatch.setattr("yuxi.channel.bootstrap.SecurityPolicy", lambda *args, **kwargs: mocks["security_policy"])
monkeypatch.setattr("yuxi.channel.bootstrap.ChannelGateway", lambda **kwargs: mocks["gateway"])
monkeypatch.setattr("yuxi.channel.bootstrap.asyncio.create_task", lambda coro: mocks["dummy_task"])
return mocks
async def test_start_assembles_gateway_and_attaches_state(app, bootstrap, start_mocks):
"""验证启动器会按顺序组装网关组件并挂载到 app.state。"""
await bootstrap.start(app)
start_mocks["load_plugins"].assert_called_once()
start_mocks["lifecycle"].set_message_handler.assert_called_once()
start_mocks["lifecycle"].start_all.assert_awaited_once()
start_mocks["dispatcher"].start.assert_awaited_once()
start_mocks["gateway"].start_config_change_listener.assert_awaited_once()
assert app.state.channel_lifecycle_manager is start_mocks["lifecycle"]
assert app.state.channel_outbound_dispatcher is start_mocks["dispatcher"]
assert app.state.channel_gateway is start_mocks["gateway"]
assert bootstrap._compensate_task is start_mocks["dummy_task"]
assert bootstrap._started is True
async def test_start_registers_message_handler_before_starting_lifecycle(app, bootstrap, start_mocks):
"""验证消息处理器在 lifecycle 启动前注册,避免消息丢失窗口。"""
await bootstrap.start(app)
lifecycle_mock = start_mocks["lifecycle"]
method_names = [call[0] for call in lifecycle_mock.method_calls]
assert method_names[0] == "set_message_handler"
assert method_names.index("set_message_handler") < method_names.index("start_all")
async def test_start_failure_triggers_rollback(app, bootstrap, start_mocks):
"""验证启动中途失败会回滚已启动组件并重新抛出异常。"""
start_mocks["lifecycle"].start_all.side_effect = RuntimeError("boom")
with pytest.raises(RuntimeError, match="boom"):
await bootstrap.start(app)
# 回滚应关闭 gateway含 lifecycle与 outbound dispatcher
start_mocks["gateway"].stop.assert_awaited_once()
start_mocks["dispatcher"].stop.assert_awaited_once()
assert bootstrap._started is False
async def test_start_is_idempotent(app, bootstrap, start_mocks):
"""验证重复调用 start() 不会重复初始化组件。"""
await bootstrap.start(app)
await bootstrap.start(app)
start_mocks["load_plugins"].assert_called_once()
start_mocks["lifecycle"].start_all.assert_awaited_once()
start_mocks["dispatcher"].start.assert_awaited_once()
async def test_stop_closes_components_in_order(bootstrap):
"""验证关闭时按 compensate -> gateway -> outbound 的顺序释放资源。"""
mock_dispatcher = AsyncMock()
mock_gateway = AsyncMock()
mock_lifecycle = AsyncMock()
mock_task = AsyncMock()
bootstrap._outbound_dispatcher = mock_dispatcher
bootstrap._gateway = mock_gateway
bootstrap._lifecycle_manager = mock_lifecycle
bootstrap._compensate_task = mock_task
bootstrap._started = True
await bootstrap.stop()
mock_task.cancel.assert_called_once()
mock_gateway.stop.assert_awaited_once()
# gateway.stop() 内部已负责 lifecycle外部不应重复调用
mock_lifecycle.stop_all.assert_not_awaited()
mock_dispatcher.stop.assert_awaited_once()
assert bootstrap._compensate_task is None
assert bootstrap._started is False
async def test_stop_uses_lifecycle_directly_when_gateway_is_missing(bootstrap):
"""验证 start 失败在未创建 gateway 时仍能正确停止 lifecycle。"""
mock_lifecycle = AsyncMock()
mock_dispatcher = AsyncMock()
bootstrap._lifecycle_manager = mock_lifecycle
bootstrap._outbound_dispatcher = mock_dispatcher
bootstrap._gateway = None
bootstrap._started = False
await bootstrap.stop()
mock_lifecycle.stop_all.assert_awaited_once()
mock_dispatcher.stop.assert_awaited_once()
async def test_stop_is_idempotent(bootstrap):
"""验证重复调用 stop() 不会重复关闭组件。"""
mock_dispatcher = AsyncMock()
mock_gateway = AsyncMock()
mock_task = AsyncMock()
bootstrap._outbound_dispatcher = mock_dispatcher
bootstrap._gateway = mock_gateway
bootstrap._compensate_task = mock_task
bootstrap._started = True
await bootstrap.stop()
await bootstrap.stop()
mock_task.cancel.assert_called_once()
mock_gateway.stop.assert_awaited_once()
mock_dispatcher.stop.assert_awaited_once()
async def test_stop_without_start_is_safe(bootstrap):
"""验证未启动时调用 stop() 不会报错。"""
await bootstrap.stop()
assert bootstrap._started is False