"""事件注册、绑定、调度和错误策略组件的独立测试。""" import sys import threading import types from unittest.mock import Mock from app.runtime.event.binding import ( EventBindingResolver, EventHandlerBinding, ) from app.runtime.event.errors import EventErrorPolicy from app.runtime.events import Event from app.schemas.types import EventType from app.startup.initializers.modules import get_host_event_handler_factories class _UnmanagedHandler: """记录构造次数,用于证明绑定未命中时不会被总线实例化。""" constructed = 0 def __init__(self) -> None: """记录任何非预期的隐式构造。""" type(self).constructed += 1 def handle(self, _event: Event) -> None: """提供可解析的实例方法声明。""" def _free_function_handler(_event: Event) -> None: """模块级自由函数处理器,用于验证直调路径保持不变。""" def test_binding_miss_does_not_construct_handler_owner() -> None: """resolver 未命中时只记录诊断,不能调用 owner_class()。""" resolvers = {} binding = EventBindingResolver( lock=threading.Lock(), resolvers=lambda: resolvers, ) _UnmanagedHandler.constructed = 0 assert binding.resolve(_UnmanagedHandler.handle) is None assert _UnmanagedHandler.constructed == 0 assert binding.unresolved_handlers() == ( f"{__name__}._UnmanagedHandler.handle", ) def test_unloaded_module_class_handler_is_skipped() -> None: """模块缓存被清除后,残留的类方法声明必须跳过而非直调原始函数。""" fake_name = "tests._fake_unloaded_plugin" fake_module = types.ModuleType(fake_name) sys.modules[fake_name] = fake_module try: # 在伪模块命名空间内构造类,使处理器 __module__ 指向该模块 exec( "class _ResidualPlugin:\n" " def reload(self, event):\n" " raise AssertionError('residual handler must not run')\n", fake_module.__dict__, ) residual_handler = fake_module._ResidualPlugin.reload # 模拟插件重载 stop 阶段清除模块缓存后的残留注册 del sys.modules[fake_name] binding = EventBindingResolver( lock=threading.Lock(), resolvers=lambda: {}, ) assert binding.resolve(residual_handler) is None # 模块卸载后 identifier 回退为 unknown_module 前缀,与线上日志一致 assert binding.unresolved_handlers() == ( "unknown_module._ResidualPlugin.reload", ) finally: sys.modules.pop(fake_name, None) def test_unloaded_module_decorator_wrapped_method_is_skipped() -> None: """装饰器包装的类方法限定名含 ,模块卸载后也必须跳过而非直调。""" fake_name = "tests._fake_unloaded_decorated_plugin" fake_module = types.ModuleType(fake_name) sys.modules[fake_name] = fake_module try: exec( "def _deco(f):\n" " def wrapper(self, event):\n" " return f(self, event)\n" " return wrapper\n" "class _DecoratedPlugin:\n" " @_deco\n" " def send_msg(self, event):\n" " raise AssertionError('residual handler must not run')\n", fake_module.__dict__, ) residual_handler = fake_module._DecoratedPlugin.send_msg # 装饰器包装后限定名含 ,不能因此被误判为自由函数 assert "" in residual_handler.__qualname__ del sys.modules[fake_name] binding = EventBindingResolver( lock=threading.Lock(), resolvers=lambda: {}, ) assert binding.resolve(residual_handler) is None assert binding.unresolved_handlers() == ( "unknown_module._deco..wrapper", ) finally: sys.modules.pop(fake_name, None) def test_free_function_handler_still_invoked_directly() -> None: """自由函数处理器不属于类声明,保持直调路径不被新跳过逻辑影响。""" binding = EventBindingResolver( lock=threading.Lock(), resolvers=lambda: {}, ) resolved = binding.resolve(_free_function_handler) assert resolved is not None method, handler_binding, class_name, method_name = resolved assert method is _free_function_handler assert handler_binding.run_sync_in_threadpool is True assert class_name == "" assert method_name == "_free_function_handler" def test_binding_uses_explicit_resolver_instance() -> None: """显式 resolver 应返回当前托管实例上的绑定方法。""" instance = object.__new__(_UnmanagedHandler) resolvers = { "test": lambda owner: EventHandlerBinding( instance=instance, owner_name="托管处理器", ) if owner is _UnmanagedHandler else None } binding = EventBindingResolver( lock=threading.Lock(), resolvers=lambda: resolvers, ) method, resolved, class_name, method_name = binding.resolve( _UnmanagedHandler.handle ) assert method.__self__ is instance assert resolved.owner_name == "托管处理器" assert class_name == "_UnmanagedHandler" assert method_name == "handle" def test_system_error_failure_does_not_rebroadcast() -> None: """SystemError 处理器自身失败时只能通知和日志降级,不能再次发送事件。""" notifier = Mock() emit = Mock() policy = EventErrorPolicy( notifier=lambda: notifier, emit_system_error=emit, ) policy.handle( event=Event(EventType.SystemError, {}), module_name="测试模块", class_name="BrokenHandler", method_name="handle", error=RuntimeError("broken"), ) notifier.assert_called_once() emit.assert_not_called() def test_regular_event_failure_emits_one_system_error() -> None: """普通事件失败应生成一次结构稳定的 SystemError 载荷。""" emit = Mock() policy = EventErrorPolicy( notifier=lambda: None, emit_system_error=emit, ) policy.handle( event=Event(EventType.ConfigChanged, {}), module_name="测试模块", class_name="BrokenHandler", method_name="handle", error=RuntimeError("broken"), ) payload = emit.call_args.args[0] assert payload["type"] == "event" assert payload["event_type"] is EventType.ConfigChanged assert payload["event_handle"] == "BrokenHandler.handle" assert payload["error"] == "broken" def test_all_decorated_host_handler_classes_have_explicit_factories() -> None: """宿主中使用事件装饰器的类必须全部由组合根 resolver 白名单接管。""" factories = get_host_event_handler_factories() assert {owner.__name__ for owner in factories} == { "Command", "DownloadChain", "Scheduler", "ScrapingChain", "SearchChain", "SiteChain", "SubscribeChain", "WorkflowChain", }