ForcePilot/backend/test/unit/channel/middlewares/test_registry.py
Kris bab30f2715
Some checks failed
Deploy VitePress site to Pages / build (push) Has been cancelled
Ruff Format Check / Ruff Format & Lint (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
feat:0715
2026-07-15 12:30:58 +08:00

360 lines
12 KiB
Python

"""入站/出站中间件注册表单元测试。"""
from __future__ import annotations
from unittest.mock import MagicMock
import pytest
from yuxi.channel.exceptions import ChannelConfigurationError
from yuxi.channel.middlewares.protocols import InboundContext, InboundResult, OutboundContext, OutboundResult
from yuxi.channel.middlewares.registry import InboundMiddlewareRegistry, OutboundMiddlewareRegistry
class _DummyInbound:
name = "dummy"
default_order = 100
async def process(self, ctx: InboundContext, next_mw) -> InboundResult:
return await next_mw()
class _AlphaInbound:
name = "alpha"
default_order = 200
async def process(self, ctx: InboundContext, next_mw) -> InboundResult:
return await next_mw()
class _BetaInbound:
name = "beta"
default_order = 100
async def process(self, ctx: InboundContext, next_mw) -> InboundResult:
return await next_mw()
class _DedupeInbound:
name = "dedupe"
default_order = 100
async def process(self, ctx: InboundContext, next_mw) -> InboundResult:
return await next_mw()
class _TransactionInbound:
name = "transaction"
default_order = 300
async def process(self, ctx: InboundContext, next_mw) -> InboundResult:
return await next_mw()
class _SessionInbound:
name = "session"
default_order = 400
async def process(self, ctx: InboundContext, next_mw) -> InboundResult:
return await next_mw()
class _DummyOutbound:
name = "dummy"
default_order = 100
async def process(self, ctx: OutboundContext, next_mw) -> OutboundResult:
return await next_mw()
class _AlphaOutbound:
name = "alpha"
default_order = 200
async def process(self, ctx: OutboundContext, next_mw) -> OutboundResult:
return await next_mw()
class _BetaOutbound:
name = "beta"
default_order = 100
async def process(self, ctx: OutboundContext, next_mw) -> OutboundResult:
return await next_mw()
class _SendOutbound:
name = "send"
default_order = 700
async def process(self, ctx: OutboundContext, next_mw) -> OutboundResult:
return await next_mw()
class _StatusUpdateOutbound:
name = "status_update"
default_order = 800
async def process(self, ctx: OutboundContext, next_mw) -> OutboundResult:
return await next_mw()
@pytest.fixture
def inbound_registry() -> InboundMiddlewareRegistry:
return InboundMiddlewareRegistry()
@pytest.fixture
def outbound_registry() -> OutboundMiddlewareRegistry:
return OutboundMiddlewareRegistry()
@pytest.mark.unit
class TestInboundMiddlewareRegistry:
def test_register_and_unregister(self, inbound_registry: InboundMiddlewareRegistry) -> None:
mw = _DummyInbound()
inbound_registry.register(mw)
assert inbound_registry.get("dummy") is mw
removed = inbound_registry.unregister("dummy")
assert removed is mw
assert inbound_registry.get("dummy") is None
def test_register_requires_name(self, inbound_registry: InboundMiddlewareRegistry) -> None:
class NoName:
name = ""
default_order = 0
async def process(self, ctx: InboundContext, next_mw) -> InboundResult:
return await next_mw()
with pytest.raises(ValueError):
inbound_registry.register(NoName())
def test_default_chain_sorted_by_order(self, inbound_registry: InboundMiddlewareRegistry) -> None:
alpha = _AlphaInbound()
beta = _BetaInbound()
inbound_registry.register(alpha)
inbound_registry.register(beta)
chain = inbound_registry.resolve_chain({})
assert [m.name for m in chain] == ["beta", "alpha"]
def test_configured_chain_enabled_and_sorted(self, inbound_registry: InboundMiddlewareRegistry) -> None:
alpha = _AlphaInbound()
beta = _BetaInbound()
inbound_registry.register(alpha)
inbound_registry.register(beta)
config = {
"channel_type": "feishu",
"account_id": "a1",
"inbound_middlewares": [
{"name": "alpha", "enabled": True, "order": 50},
{"name": "beta", "enabled": True, "order": 150},
],
}
chain = inbound_registry.resolve_chain(config)
assert [m.name for m in chain] == ["alpha", "beta"]
def test_configured_chain_uses_default_order_when_missing(
self, inbound_registry: InboundMiddlewareRegistry
) -> None:
alpha = _AlphaInbound()
inbound_registry.register(alpha)
config = {"inbound_middlewares": [{"name": "alpha", "enabled": True}]}
chain = inbound_registry.resolve_chain(config)
assert [m.name for m in chain] == ["alpha"]
def test_configured_chain_disabled_ignored(self, inbound_registry: InboundMiddlewareRegistry) -> None:
alpha = _AlphaInbound()
beta = _BetaInbound()
inbound_registry.register(alpha)
inbound_registry.register(beta)
config = {
"inbound_middlewares": [
{"name": "alpha", "enabled": False, "order": 1},
{"name": "beta", "enabled": True, "order": 2},
],
}
chain = inbound_registry.resolve_chain(config)
assert [m.name for m in chain] == ["beta"]
def test_configured_chain_unknown_name_warns_and_ignored(
self, inbound_registry: InboundMiddlewareRegistry, monkeypatch: pytest.MonkeyPatch
) -> None:
inbound_registry.register(_AlphaInbound())
warn_mock = MagicMock()
monkeypatch.setattr("yuxi.channel.middlewares.registry.logger", type("L", (), {"warning": warn_mock})())
config = {
"inbound_middlewares": [
{"name": "unknown", "enabled": True},
{"name": "alpha", "enabled": True},
],
}
chain = inbound_registry.resolve_chain(config)
assert [m.name for m in chain] == ["alpha"]
warn_mock.assert_called_once()
def test_resolve_chain_caches_by_config_hash(self, inbound_registry: InboundMiddlewareRegistry) -> None:
mw = _AlphaInbound()
inbound_registry.register(mw)
config = {
"channel_type": "feishu",
"account_id": "a1",
"inbound_middlewares": [{"name": "alpha", "enabled": True, "order": 50}],
}
chain1 = inbound_registry.resolve_chain(config)
chain2 = inbound_registry.resolve_chain(config)
assert chain1 is chain2
def test_invalidate_removes_cache_for_account(self, inbound_registry: InboundMiddlewareRegistry) -> None:
mw = _AlphaInbound()
inbound_registry.register(mw)
config1 = {
"channel_type": "feishu",
"account_id": "a1",
"inbound_middlewares": [{"name": "alpha", "enabled": True}],
}
config2 = {
"channel_type": "feishu",
"account_id": "a2",
"inbound_middlewares": [{"name": "alpha", "enabled": True}],
}
chain1 = inbound_registry.resolve_chain(config1)
chain2 = inbound_registry.resolve_chain(config2)
inbound_registry.invalidate("feishu", "a1")
assert inbound_registry.resolve_chain(config1) is not chain1
assert inbound_registry.resolve_chain(config2) is chain2
def test_dedupe_must_be_first(self, inbound_registry: InboundMiddlewareRegistry) -> None:
inbound_registry.register(_DedupeInbound())
inbound_registry.register(_AlphaInbound())
config = {
"inbound_middlewares": [
{"name": "alpha", "enabled": True, "order": 50},
{"name": "dedupe", "enabled": True, "order": 100},
],
}
with pytest.raises(ChannelConfigurationError, match="dedupe 必须位于链首"):
inbound_registry.resolve_chain(config)
def test_transaction_must_be_before_session_route_create_run(
self, inbound_registry: InboundMiddlewareRegistry
) -> None:
inbound_registry.register(_TransactionInbound())
inbound_registry.register(_SessionInbound())
config = {
"inbound_middlewares": [
{"name": "session", "enabled": True, "order": 100},
{"name": "transaction", "enabled": True, "order": 200},
],
}
with pytest.raises(ChannelConfigurationError, match="transaction 必须位于 session"):
inbound_registry.resolve_chain(config)
async def test_start_all_and_stop_all(self, inbound_registry: InboundMiddlewareRegistry) -> None:
start = MagicMock()
stop = MagicMock()
class _LifecycleInbound:
name = "lifecycle"
default_order = 0
async def process(self, ctx: InboundContext, next_mw) -> InboundResult:
return await next_mw()
async def start(self) -> None:
start()
async def stop(self) -> None:
stop()
inbound_registry.register(_LifecycleInbound())
await inbound_registry.start_all()
await inbound_registry.stop_all()
start.assert_called_once()
stop.assert_called_once()
@pytest.mark.unit
class TestOutboundMiddlewareRegistry:
def test_register_and_unregister(self, outbound_registry: OutboundMiddlewareRegistry) -> None:
mw = _DummyOutbound()
outbound_registry.register(mw)
assert outbound_registry.get("dummy") is mw
removed = outbound_registry.unregister("dummy")
assert removed is mw
assert outbound_registry.get("dummy") is None
def test_default_chain_sorted_by_order(self, outbound_registry: OutboundMiddlewareRegistry) -> None:
alpha = _AlphaOutbound()
beta = _BetaOutbound()
outbound_registry.register(alpha)
outbound_registry.register(beta)
chain = outbound_registry.resolve_chain({})
assert [m.name for m in chain] == ["beta", "alpha"]
def test_configured_chain_enabled_and_sorted(self, outbound_registry: OutboundMiddlewareRegistry) -> None:
alpha = _AlphaOutbound()
beta = _BetaOutbound()
outbound_registry.register(alpha)
outbound_registry.register(beta)
config = {
"channel_type": "feishu",
"account_id": "a1",
"outbound_middlewares": [
{"name": "alpha", "enabled": True, "order": 50},
{"name": "beta", "enabled": True, "order": 150},
],
}
chain = outbound_registry.resolve_chain(config)
assert [m.name for m in chain] == ["alpha", "beta"]
def test_send_must_be_before_status_update(self, outbound_registry: OutboundMiddlewareRegistry) -> None:
outbound_registry.register(_StatusUpdateOutbound())
outbound_registry.register(_SendOutbound())
config = {
"outbound_middlewares": [
{"name": "status_update", "enabled": True, "order": 100},
{"name": "send", "enabled": True, "order": 200},
],
}
with pytest.raises(ChannelConfigurationError, match="send 必须位于 status_update 之前"):
outbound_registry.resolve_chain(config)
async def test_start_all_and_stop_all(self, outbound_registry: OutboundMiddlewareRegistry) -> None:
start = MagicMock()
stop = MagicMock()
class _LifecycleOutbound:
name = "lifecycle"
default_order = 0
async def process(self, ctx: OutboundContext, next_mw) -> OutboundResult:
return await next_mw()
async def start(self) -> None:
start()
async def stop(self) -> None:
stop()
outbound_registry.register(_LifecycleOutbound())
await outbound_registry.start_all()
await outbound_registry.stop_all()
start.assert_called_once()
stop.assert_called_once()