"""yuxi.channels.infrastructure.factory 单元测试。 覆盖以下纯函数 / 装配辅助函数: - ``_register_default_match_tiers``:注册 8 个默认层级 + 6 个内置 matcher - ``_construct_health_dependencies``:构造健康检查子依赖五元组 - ``_register_builtin_event_subscribers``:注册内置事件订阅者 - ``_register_di_singletons``:注册全量 DI 单例到容器 ``create_host_bootstrap`` / ``create_host_shutdown`` / ``load_channel_plugins`` 依赖 Redis / ARQ / DB 等外部资源,不属于单元测试范畴,本文件不覆盖。 不依赖运行中的 Docker 服务,纯单元测试。 """ from __future__ import annotations from unittest.mock import MagicMock import pytest from yuxi.channels.application.circuit_breaker.channel_circuit_breaker import ( ChannelCircuitBreaker, ) from yuxi.channels.application.extension.event_bus import EventBus from yuxi.channels.application.health.channel_probe import ChannelProbe from yuxi.channels.application.health.diagnostics_exporter import ( DiagnosticsExporter, ) from yuxi.channels.application.health.health_aggregator import HealthAggregator from yuxi.channels.application.lifecycle.plugin_lifecycle_manager import ( PluginLifecycleManager, ) from yuxi.channels.application.lifecycle.plugin_loader import PluginLoader from yuxi.channels.application.pipeline.stage_slot_injector import StageSlotInjector from yuxi.channels.application.transport import TransportManager from yuxi.channels.adapters.content_review_repository_adapter import ( ContentReviewRepositoryAdapter, ) from yuxi.channels.adapters.default_content_moderation_adapter import ( DefaultContentModerationAdapter, ) from yuxi.channels.adapters.inmemory_tracer_adapter import ( InMemoryTracerAdapter, ) from yuxi.channels.adapters.redis_config_adapter import RedisConfigAdapter from yuxi.channels.application.extension.channel_event_broadcaster import ( ChannelEventBroadcaster, ) from yuxi.channels.application.extension.config_source_registry import ( ConfigSourceRegistry, ) from yuxi.channels.application.extension.event_subscription_registry import ( EventSubscriptionRegistry, ) from yuxi.channels.application.extension.stage_slot_registry import StageSlotRegistry from yuxi.channels.application.lifecycle.config_scope_registry import ( ConfigScopeRegistry, ) from yuxi.channels.application.lifecycle.sensitive_field_registry import ( SensitiveFieldRegistry, ) from yuxi.channels.application.lifecycle.plugin_dependency_resolver import ( PluginDependencyResolver, ) from yuxi.channels.contract.dtos.outbox import OutboxConfig from yuxi.channels.contract.ports.driven.cache_port import CachePort from yuxi.channels.contract.ports.driven.config_port import ConfigPort from yuxi.channels.contract.ports.driven.event_publisher_port import ( EventPublisherPort, ) from yuxi.channels.contract.ports.driven.logger_port import LoggerPort from yuxi.channels.contract.ports.driven.persistence_port import PersistencePort from yuxi.channels.contract.ports.driven.queue_port import QueuePort from yuxi.channels.contract.ports.driven.content_review_repository_port import ( ContentReviewRepositoryPort, ) from yuxi.channels.contract.ports.driven.agent_run_execution_port import ( AgentRunExecutionPort, ) from yuxi.channels.core.registry.capability_registry import CapabilityRegistry from yuxi.channels.core.registry.command_registry import CommandRegistryTable from yuxi.channels.core.registry.identity_resolver_registry import ( IdentityResolverRegistry, ) from yuxi.channels.core.registry.plugin_registry import PluginRegistry from yuxi.channels.core.registry.route_match_registry import RouteMatchRegistry from yuxi.channels.core.registry.whitelist_registry import WhitelistRegistry from yuxi.channels.core.service.degradation_manager import DegradationManager from yuxi.channels.infrastructure.dependency_injection import ( DependencyInjectionContainer, ) from yuxi.channels.infrastructure.factory import ( _PluginLoadingCore, _construct_health_dependencies, _register_builtin_event_subscribers, _register_default_match_tiers, _register_di_singletons, ) pytestmark = pytest.mark.unit # --------------------------------------------------------------------------- # _register_default_match_tiers # --------------------------------------------------------------------------- @pytest.mark.unit class TestRegisterDefaultMatchTiers: """``_register_default_match_tiers`` 注册 8 个默认层级 + 6 个内置 matcher。""" def test_registers_eight_default_tiers(self, fake_logger): # Arrange registry = RouteMatchRegistry() # Act _register_default_match_tiers(registry) # Assert: 8 个默认层级 tiers = registry.listTiers() tier_names = [t.name for t in tiers] expected_names = [ "session_key", "identity_id", "peer_id", "chat_type", "channel_session", "channel_type", "account", "default", ] assert sorted(tier_names) == sorted(expected_names) assert len(tiers) == 8 def test_default_tier_priorities_descending_for_route_resolver(self, fake_logger): """优先级数值由高到低:session_key(800) → default(100)。""" # Arrange registry = RouteMatchRegistry() # Act _register_default_match_tiers(registry) # Assert tiers_by_name = {t.name: t.priority for t in registry.listTiers()} assert tiers_by_name["session_key"] == 800 assert tiers_by_name["identity_id"] == 700 assert tiers_by_name["peer_id"] == 600 assert tiers_by_name["chat_type"] == 500 assert tiers_by_name["channel_session"] == 400 assert tiers_by_name["channel_type"] == 300 assert tiers_by_name["account"] == 200 assert tiers_by_name["default"] == 100 def test_registers_six_builtin_matchers(self, fake_logger): """注册 6 个内置 matcher(session_key/identity_id/peer_id/chat_type/account/default)。 channel_session / channel_type 两层无内置 matcher,由插件可注入。 """ # Arrange registry = RouteMatchRegistry() # Act _register_default_match_tiers(registry) # Assert: 通过尝试注册同名的 matcher 不抛错来间接验证已注册 # (registerMatcher 内部允许覆盖)。这里仅断言 6 个 matcher 名称 # 可以重新注册(覆盖),不会因为缺少对应层级而失败。 builtin_matcher_names = [ "session_key", "identity_id", "peer_id", "chat_type", "account", "default", ] for name in builtin_matcher_names: # registerMatcher 内部按名查找 tier,未注册时会抛 ValidationError # 这里重新注册同名 matcher 不抛错即证明对应 tier 与原 matcher 已存在 registry.registerMatcher(name, lambda ctx: None) # --------------------------------------------------------------------------- # _construct_health_dependencies # --------------------------------------------------------------------------- @pytest.mark.unit class TestConstructHealthDependencies: """``_construct_health_dependencies`` 构造健康检查子依赖五元组。""" def test_returns_five_tuple_with_expected_types(self, fake_logger): # Arrange plugin_registry = MagicMock(spec=PluginRegistry) persistence_port = MagicMock(spec=PersistencePort) cache_port = MagicMock(spec=CachePort) queue_port = MagicMock(spec=QueuePort) capability_registry = MagicMock(spec=CapabilityRegistry) event_publisher = MagicMock(spec=EventBus) redis_client = MagicMock() # Act ( config_port, channel_circuit_breaker, channel_probe, tracer_port, diagnostics_exporter, ) = _construct_health_dependencies( plugin_registry=plugin_registry, persistence_port=persistence_port, cache_port=cache_port, queue_port=queue_port, capability_registry=capability_registry, event_publisher=event_publisher, redis_client=redis_client, logger=fake_logger, ) # Assert assert isinstance(config_port, RedisConfigAdapter) assert isinstance(channel_circuit_breaker, ChannelCircuitBreaker) assert isinstance(channel_probe, ChannelProbe) assert isinstance(tracer_port, InMemoryTracerAdapter) assert isinstance(diagnostics_exporter, DiagnosticsExporter) def test_key_to_scope_map_propagated_to_config_port(self, fake_logger): """key_to_scope_map 透传到 RedisConfigAdapter(F-02)。""" # Arrange from yuxi.channels.contract.dtos.config import ConfigScope key_to_scope_map = {"channel.test_key": ConfigScope.CHANNEL} # Act ( config_port, *_rest, ) = _construct_health_dependencies( plugin_registry=MagicMock(spec=PluginRegistry), persistence_port=MagicMock(spec=PersistencePort), cache_port=MagicMock(spec=CachePort), queue_port=MagicMock(spec=QueuePort), capability_registry=MagicMock(spec=CapabilityRegistry), event_publisher=MagicMock(spec=EventBus), redis_client=MagicMock(), logger=fake_logger, key_to_scope_map=key_to_scope_map, ) # Assert: RedisConfigAdapter 持有 key_to_scope_map 视图 assert config_port._key_to_scope_map is key_to_scope_map def test_key_to_scope_map_none_does_not_raise(self, fake_logger): """key_to_scope_map=None 时正常构造(向后兼容)。""" # Act result = _construct_health_dependencies( plugin_registry=MagicMock(spec=PluginRegistry), persistence_port=MagicMock(spec=PersistencePort), cache_port=MagicMock(spec=CachePort), queue_port=MagicMock(spec=QueuePort), capability_registry=MagicMock(spec=CapabilityRegistry), event_publisher=MagicMock(spec=EventBus), redis_client=MagicMock(), logger=fake_logger, key_to_scope_map=None, ) # Assert assert len(result) == 5 assert isinstance(result[0], RedisConfigAdapter) # --------------------------------------------------------------------------- # _register_builtin_event_subscribers # --------------------------------------------------------------------------- @pytest.mark.unit class TestRegisterBuiltinEventSubscribers: """``_register_builtin_event_subscribers`` 注册内置事件订阅。""" def _make_event_bus(self, fake_logger) -> EventBus: """构造真实 EventBus 实例(避免 mock 丢失内部状态)。""" degradation_manager = MagicMock(spec=DegradationManager) return EventBus(logger=fake_logger, degradation_manager=degradation_manager) def test_registers_outbox_state_changed_subscriber(self, fake_logger): # Arrange event_bus = self._make_event_bus(fake_logger) # Act _register_builtin_event_subscribers( event_bus=event_bus, persistence_port=MagicMock(spec=PersistencePort), queue_port=MagicMock(spec=QueuePort), whitelist_registry=MagicMock(spec=WhitelistRegistry), cache_port=MagicMock(spec=CachePort), plugin_registry=MagicMock(spec=PluginRegistry), logger=fake_logger, ) # Assert assert "OutboxStateChanged" in event_bus._subscriptions assert len(event_bus._subscriptions["OutboxStateChanged"]) == 1 def test_registers_eight_plugin_lifecycle_event_subscribers(self, fake_logger): """8 类插件生命周期事件共用同一 PluginLifecycleAuditHandler 实例。""" # Arrange event_bus = self._make_event_bus(fake_logger) plugin_event_types = ( "PluginDiscovered", "PluginLoaded", "PluginStarted", "PluginPaused", "PluginResumed", "PluginStopped", "PluginUnloaded", "PluginFailed", ) # Act _register_builtin_event_subscribers( event_bus=event_bus, persistence_port=MagicMock(spec=PersistencePort), queue_port=MagicMock(spec=QueuePort), whitelist_registry=MagicMock(spec=WhitelistRegistry), cache_port=MagicMock(spec=CachePort), plugin_registry=MagicMock(spec=PluginRegistry), logger=fake_logger, ) # Assert for event_type in plugin_event_types: assert event_type in event_bus._subscriptions assert len(event_bus._subscriptions[event_type]) == 1 def test_registers_config_event_pair_for_whitelist_and_route_match(self, fake_logger): """ConfigChanged/ConfigRollback 各注册 WhitelistConfigHandler + RouteMatchCacheHandler 共 2 个订阅。""" # Arrange event_bus = self._make_event_bus(fake_logger) # Act _register_builtin_event_subscribers( event_bus=event_bus, persistence_port=MagicMock(spec=PersistencePort), queue_port=MagicMock(spec=QueuePort), whitelist_registry=MagicMock(spec=WhitelistRegistry), cache_port=MagicMock(spec=CachePort), plugin_registry=MagicMock(spec=PluginRegistry), logger=fake_logger, ) # Assert for config_event in ("ConfigChanged", "ConfigRollback"): assert config_event in event_bus._subscriptions assert len(event_bus._subscriptions[config_event]) == 2 def test_registers_pairing_approved_subscriber(self, fake_logger): # Arrange event_bus = self._make_event_bus(fake_logger) # Act _register_builtin_event_subscribers( event_bus=event_bus, persistence_port=MagicMock(spec=PersistencePort), queue_port=MagicMock(spec=QueuePort), whitelist_registry=MagicMock(spec=WhitelistRegistry), cache_port=MagicMock(spec=CachePort), plugin_registry=MagicMock(spec=PluginRegistry), logger=fake_logger, ) # Assert assert "PairingApproved" in event_bus._subscriptions assert len(event_bus._subscriptions["PairingApproved"]) == 1 def test_registers_channel_degraded_and_recovered_subscribers(self, fake_logger): # Arrange event_bus = self._make_event_bus(fake_logger) # Act _register_builtin_event_subscribers( event_bus=event_bus, persistence_port=MagicMock(spec=PersistencePort), queue_port=MagicMock(spec=QueuePort), whitelist_registry=MagicMock(spec=WhitelistRegistry), cache_port=MagicMock(spec=CachePort), plugin_registry=MagicMock(spec=PluginRegistry), logger=fake_logger, ) # Assert assert len(event_bus._subscriptions["ChannelDegraded"]) == 1 assert len(event_bus._subscriptions["ChannelRecovered"]) == 1 def test_registers_three_channel_event_broadcaster_subscribers(self, fake_logger): """ChannelSessionUpdated/ChannelMessageReceived/ChannelMessageSent → 广播器。""" # Arrange event_bus = self._make_event_bus(fake_logger) # Act _register_builtin_event_subscribers( event_bus=event_bus, persistence_port=MagicMock(spec=PersistencePort), queue_port=MagicMock(spec=QueuePort), whitelist_registry=MagicMock(spec=WhitelistRegistry), cache_port=MagicMock(spec=CachePort), plugin_registry=MagicMock(spec=PluginRegistry), logger=fake_logger, ) # Assert for channel_event in ( "ChannelSessionUpdated", "ChannelMessageReceived", "ChannelMessageSent", ): assert channel_event in event_bus._subscriptions assert len(event_bus._subscriptions[channel_event]) == 1 def test_di_container_register_broadcaster_when_provided(self, fake_logger): """di_container 提供时注册 ChannelEventBroadcaster 单例。""" # Arrange event_bus = self._make_event_bus(fake_logger) di_container = DependencyInjectionContainer() # Act _register_builtin_event_subscribers( event_bus=event_bus, persistence_port=MagicMock(spec=PersistencePort), queue_port=MagicMock(spec=QueuePort), whitelist_registry=MagicMock(spec=WhitelistRegistry), cache_port=MagicMock(spec=CachePort), plugin_registry=MagicMock(spec=PluginRegistry), logger=fake_logger, di_container=di_container, ) # Assert: ChannelEventBroadcaster 单例可解析 broadcaster = di_container.resolve(ChannelEventBroadcaster) assert isinstance(broadcaster, ChannelEventBroadcaster) def test_di_container_none_does_not_raise(self, fake_logger): """di_container=None 时不抛错(仅不注册 broadcaster 单例)。""" # Arrange event_bus = self._make_event_bus(fake_logger) # Act / Assert: 不抛异常 _register_builtin_event_subscribers( event_bus=event_bus, persistence_port=MagicMock(spec=PersistencePort), queue_port=MagicMock(spec=QueuePort), whitelist_registry=MagicMock(spec=WhitelistRegistry), cache_port=MagicMock(spec=CachePort), plugin_registry=MagicMock(spec=PluginRegistry), logger=fake_logger, di_container=None, ) # --------------------------------------------------------------------------- # _register_di_singletons # --------------------------------------------------------------------------- def _make_plugin_loading_core() -> _PluginLoadingCore: """构造 _PluginLoadingCore 桩,所有字段使用 MagicMock。""" return _PluginLoadingCore( plugin_registry=MagicMock(spec=PluginRegistry), stage_slot_registry=MagicMock(spec=StageSlotRegistry), event_subscription_registry=MagicMock(spec=EventSubscriptionRegistry), config_source_registry=MagicMock(spec=ConfigSourceRegistry), route_match_registry=MagicMock(spec=RouteMatchRegistry), whitelist_registry=MagicMock(spec=WhitelistRegistry), capability_registry=MagicMock(spec=CapabilityRegistry), event_bus=MagicMock(spec=EventBus), cache_port=MagicMock(spec=CachePort), degradation_manager=MagicMock(spec=DegradationManager), plugin_lifecycle_manager=MagicMock(spec=PluginLifecycleManager), plugin_dependency_resolver=MagicMock(spec=PluginDependencyResolver), plugin_loader=MagicMock(spec=PluginLoader), outbox_config=OutboxConfig( ttl_seconds=3600, retry_backoff_schedule=(60,), max_retry=3, ), redis_client=MagicMock(), arq_pool=MagicMock(), execution_port_impl=MagicMock(), sensitive_registry=MagicMock(spec=SensitiveFieldRegistry), config_scope_registry=MagicMock(spec=ConfigScopeRegistry), ) @pytest.mark.unit class TestRegisterDiSingletons: """``_register_di_singletons`` 注册全量 DI 单例到容器。""" def test_registers_core_registries_and_shared_dependencies(self, fake_logger): # Arrange di = DependencyInjectionContainer() core = _make_plugin_loading_core() # Act _register_di_singletons( di_container=di, core=core, persistence_port=MagicMock(spec=PersistencePort), session_factory=MagicMock(), queue_port=MagicMock(spec=QueuePort), config_port=MagicMock(spec=ConfigPort), stage_slot_injector=MagicMock(spec=StageSlotInjector), health_aggregator=MagicMock(spec=HealthAggregator), channel_circuit_breaker=MagicMock(spec=ChannelCircuitBreaker), transport_manager=MagicMock(spec=TransportManager), logger=fake_logger, ) # Assert: 注册中心与共享依赖 assert di.resolve(PluginRegistry) is core.plugin_registry assert di.resolve(CapabilityRegistry) is core.capability_registry assert di.resolve(RouteMatchRegistry) is core.route_match_registry assert di.resolve(WhitelistRegistry) is core.whitelist_registry assert di.resolve(EventBus) is core.event_bus assert di.resolve(EventPublisherPort) is core.event_bus assert di.resolve(StageSlotRegistry) is core.stage_slot_registry assert di.resolve(EventSubscriptionRegistry) is core.event_subscription_registry assert di.resolve(ConfigSourceRegistry) is core.config_source_registry assert di.resolve(PluginLifecycleManager) is core.plugin_lifecycle_manager assert di.resolve(PluginDependencyResolver) is core.plugin_dependency_resolver assert di.resolve(PluginLoader) is core.plugin_loader assert di.resolve(SensitiveFieldRegistry) is core.sensitive_registry assert di.resolve(ConfigScopeRegistry) is core.config_scope_registry def test_registers_command_and_identity_resolver_registries(self, fake_logger): """命令注册表与身份解析器注册表由本函数构造并注册。""" # Arrange di = DependencyInjectionContainer() core = _make_plugin_loading_core() # Act _register_di_singletons( di_container=di, core=core, persistence_port=MagicMock(spec=PersistencePort), session_factory=MagicMock(), queue_port=MagicMock(spec=QueuePort), config_port=MagicMock(spec=ConfigPort), stage_slot_injector=MagicMock(spec=StageSlotInjector), health_aggregator=MagicMock(spec=HealthAggregator), channel_circuit_breaker=MagicMock(spec=ChannelCircuitBreaker), transport_manager=MagicMock(spec=TransportManager), logger=fake_logger, ) # Assert cmd_registry = di.resolve(CommandRegistryTable) assert isinstance(cmd_registry, CommandRegistryTable) identity_registry = di.resolve(IdentityResolverRegistry) assert isinstance(identity_registry, IdentityResolverRegistry) def test_registers_app_level_ports_and_orchestration_components(self, fake_logger): # Arrange di = DependencyInjectionContainer() core = _make_plugin_loading_core() persistence_port = MagicMock(spec=PersistencePort) queue_port = MagicMock(spec=QueuePort) config_port = MagicMock(spec=ConfigPort) stage_slot_injector = MagicMock(spec=StageSlotInjector) health_aggregator = MagicMock(spec=HealthAggregator) channel_circuit_breaker = MagicMock(spec=ChannelCircuitBreaker) transport_manager = MagicMock(spec=TransportManager) # Act _register_di_singletons( di_container=di, core=core, persistence_port=persistence_port, session_factory=MagicMock(), queue_port=queue_port, config_port=config_port, stage_slot_injector=stage_slot_injector, health_aggregator=health_aggregator, channel_circuit_breaker=channel_circuit_breaker, transport_manager=transport_manager, logger=fake_logger, ) # Assert assert di.resolve(StageSlotInjector) is stage_slot_injector assert di.resolve(HealthAggregator) is health_aggregator assert di.resolve(ChannelCircuitBreaker) is channel_circuit_breaker assert di.resolve(LoggerPort) is fake_logger assert di.resolve(CachePort) is core.cache_port assert di.resolve(PersistencePort) is persistence_port assert di.resolve(QueuePort) is queue_port assert di.resolve(ConfigPort) is config_port assert di.resolve(TransportManager) is transport_manager def test_registers_content_review_domain_singletons(self, fake_logger): """内容审核域 3 个单例(CR-01~CR-03)。""" # Arrange di = DependencyInjectionContainer() core = _make_plugin_loading_core() session_factory = MagicMock() # Act _register_di_singletons( di_container=di, core=core, persistence_port=MagicMock(spec=PersistencePort), session_factory=session_factory, queue_port=MagicMock(spec=QueuePort), config_port=MagicMock(spec=ConfigPort), stage_slot_injector=MagicMock(spec=StageSlotInjector), health_aggregator=MagicMock(spec=HealthAggregator), channel_circuit_breaker=MagicMock(spec=ChannelCircuitBreaker), transport_manager=MagicMock(spec=TransportManager), logger=fake_logger, ) # Assert moderation = di.resolve(DefaultContentModerationAdapter) assert isinstance(moderation, DefaultContentModerationAdapter) review_adapter = di.resolve(ContentReviewRepositoryAdapter) assert isinstance(review_adapter, ContentReviewRepositoryAdapter) review_port = di.resolve(ContentReviewRepositoryPort) # ContentReviewRepositoryPort 与 ContentReviewRepositoryAdapter 绑定到同一实例 assert review_port is review_adapter def test_registers_redis_arq_and_execution_port_singletons(self, fake_logger): # Arrange di = DependencyInjectionContainer() core = _make_plugin_loading_core() # Act _register_di_singletons( di_container=di, core=core, persistence_port=MagicMock(spec=PersistencePort), session_factory=MagicMock(), queue_port=MagicMock(spec=QueuePort), config_port=MagicMock(spec=ConfigPort), stage_slot_injector=MagicMock(spec=StageSlotInjector), health_aggregator=MagicMock(spec=HealthAggregator), channel_circuit_breaker=MagicMock(spec=ChannelCircuitBreaker), transport_manager=MagicMock(spec=TransportManager), logger=fake_logger, ) # Assert: 共享 Redis / ARQ / AgentRunExecutionPort 单例 # (由 core 直接注册) assert di.resolve(AgentRunExecutionPort) is core.execution_port_impl def test_async_session_not_registered_as_singleton(self, fake_logger): """AsyncSession 是请求级资源,不应注册为单例;session_factory 注册为单例供按需创建。""" # Arrange di = DependencyInjectionContainer() core = _make_plugin_loading_core() from sqlalchemy.ext.asyncio import AsyncSession # Act _register_di_singletons( di_container=di, core=core, persistence_port=MagicMock(spec=PersistencePort), session_factory=MagicMock(), queue_port=MagicMock(spec=QueuePort), config_port=MagicMock(spec=ConfigPort), stage_slot_injector=MagicMock(spec=StageSlotInjector), health_aggregator=MagicMock(spec=HealthAggregator), channel_circuit_breaker=MagicMock(spec=ChannelCircuitBreaker), transport_manager=MagicMock(spec=TransportManager), logger=fake_logger, ) # Assert: AsyncSession 未注册为单例(避免并发污染) with pytest.raises(Exception): di.resolve(AsyncSession)