diff --git a/GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarFallbackFailureTests.cs b/GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarFallbackFailureTests.cs
new file mode 100644
index 00000000..213f3870
--- /dev/null
+++ b/GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarFallbackFailureTests.cs
@@ -0,0 +1,254 @@
+using GFramework.Core.Abstractions.Logging;
+using GFramework.Core.Ioc;
+using GFramework.Core.Logging;
+using GFramework.Cqrs.Abstractions.Cqrs;
+using GFramework.Cqrs.Tests.Logging;
+
+namespace GFramework.Cqrs.Tests.Cqrs;
+
+///
+/// 验证 CQRS handler registrar 在 reflection fallback 元数据失效时的可观察告警行为。
+///
+[TestFixture]
+internal sealed class CqrsHandlerRegistrarFallbackFailureTests
+{
+ private ILoggerFactoryProvider? _originalLoggerFactoryProvider;
+ private CapturingLoggerFactoryProvider? _capturingLoggerFactoryProvider;
+
+ ///
+ /// 切换为捕获型日志工厂,并清空 registrar 进程级缓存,避免跨用例共享状态污染断言。
+ ///
+ [SetUp]
+ public void SetUp()
+ {
+ _originalLoggerFactoryProvider = LoggerFactoryResolver.Provider;
+ _capturingLoggerFactoryProvider = new CapturingLoggerFactoryProvider(LogLevel.Warning);
+ LoggerFactoryResolver.Provider = _capturingLoggerFactoryProvider;
+ ClearRegistrarCaches();
+ }
+
+ ///
+ /// 恢复测试前的日志工厂,并清理 registrar 缓存。
+ ///
+ [TearDown]
+ public void TearDown()
+ {
+ LoggerFactoryResolver.Provider = _originalLoggerFactoryProvider!;
+ _capturingLoggerFactoryProvider = null;
+ _originalLoggerFactoryProvider = null;
+ ClearRegistrarCaches();
+ }
+
+ ///
+ /// 验证当 fallback 类型名无法解析时,registrar 会跳过该条目并记录告警。
+ ///
+ [Test]
+ public void RegisterHandlers_Should_Skip_Unresolvable_Named_Fallback_And_Log_Warning()
+ {
+ const string missingTypeName =
+ "GFramework.Cqrs.Tests.Cqrs.MissingGeneratedRegistryNotificationHandler";
+ var generatedAssembly = CreateGeneratedFallbackAssembly(
+ "GFramework.Cqrs.Tests.Cqrs.NamedFallbackMissingAssembly, Version=1.0.0.0",
+ new CqrsReflectionFallbackAttribute(missingTypeName));
+ generatedAssembly
+ .Setup(static assembly => assembly.GetType(missingTypeName, false, false))
+ .Returns((Type?)null);
+
+ var container = new MicrosoftDiContainer();
+ CqrsTestRuntime.RegisterHandlers(container, generatedAssembly.Object);
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(
+ GetGeneratedRegistryNotificationHandlerTypes(container),
+ Is.EqualTo([typeof(GeneratedRegistryNotificationHandler)]));
+ Assert.That(
+ GetWarningLogs().Any(log =>
+ log.Message.Contains(
+ $"Generated CQRS reflection fallback type {missingTypeName} could not be resolved",
+ StringComparison.Ordinal)),
+ Is.True);
+ });
+ }
+
+ ///
+ /// 验证当 fallback 类型名解析抛出异常时,registrar 会记录该加载失败告警并继续跳过条目。
+ ///
+ [Test]
+ public void RegisterHandlers_Should_Log_Warning_When_Named_Fallback_Resolution_Throws()
+ {
+ const string failingTypeName =
+ "GFramework.Cqrs.Tests.Cqrs.ThrowingGeneratedRegistryNotificationHandler";
+ const string exceptionMessage = "Fallback resolution exploded.";
+ var generatedAssembly = CreateGeneratedFallbackAssembly(
+ "GFramework.Cqrs.Tests.Cqrs.NamedFallbackThrowingAssembly, Version=1.0.0.0",
+ new CqrsReflectionFallbackAttribute(failingTypeName));
+ generatedAssembly
+ .Setup(static assembly => assembly.GetType(failingTypeName, false, false))
+ .Throws(new TypeLoadException(exceptionMessage));
+
+ var container = new MicrosoftDiContainer();
+ CqrsTestRuntime.RegisterHandlers(container, generatedAssembly.Object);
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(
+ GetGeneratedRegistryNotificationHandlerTypes(container),
+ Is.EqualTo([typeof(GeneratedRegistryNotificationHandler)]));
+ Assert.That(
+ GetWarningLogs().Any(log =>
+ log.Message.Contains(
+ $"Generated CQRS reflection fallback type {failingTypeName} failed to load",
+ StringComparison.Ordinal) &&
+ log.Message.Contains(exceptionMessage, StringComparison.Ordinal)),
+ Is.True);
+ });
+ }
+
+ ///
+ /// 验证当 direct fallback 类型属于其他程序集时,registrar 会跳过该条目并记录跨程序集告警。
+ ///
+ [Test]
+ public void RegisterHandlers_Should_Skip_Cross_Assembly_Direct_Fallback_Type_And_Log_Warning()
+ {
+ var crossAssemblyFallbackType = ReflectionFallbackNotificationContainer.ReflectionOnlyHandlerType;
+ var generatedAssembly = CreateGeneratedFallbackAssembly(
+ "GFramework.Cqrs.Tests.Cqrs.DirectFallbackMismatchAssembly, Version=1.0.0.0",
+ new CqrsReflectionFallbackAttribute(crossAssemblyFallbackType));
+
+ var container = new MicrosoftDiContainer();
+ CqrsTestRuntime.RegisterHandlers(container, generatedAssembly.Object);
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(
+ GetGeneratedRegistryNotificationHandlerTypes(container),
+ Is.EqualTo([typeof(GeneratedRegistryNotificationHandler)]));
+ Assert.That(
+ GetWarningLogs().Any(log =>
+ log.Message.Contains(
+ $"Generated CQRS reflection fallback type {crossAssemblyFallbackType.FullName} was declared on assembly",
+ StringComparison.Ordinal) &&
+ log.Message.Contains("Skipping mismatched fallback entry.", StringComparison.Ordinal)),
+ Is.True);
+ });
+ }
+
+ ///
+ /// 创建一个仅通过 generated registry 注册主 handler、并附带指定 fallback 元数据的程序集替身。
+ ///
+ /// 用于日志与缓存键的程序集名。
+ /// 要暴露给 registrar 的 fallback attribute。
+ /// 已完成基础接线的程序集 mock。
+ private static Mock CreateGeneratedFallbackAssembly(
+ string assemblyName,
+ CqrsReflectionFallbackAttribute fallbackAttribute)
+ {
+ var generatedAssembly = new Mock();
+ generatedAssembly
+ .SetupGet(static assembly => assembly.FullName)
+ .Returns(assemblyName);
+ generatedAssembly
+ .Setup(static assembly => assembly.GetCustomAttributes(typeof(CqrsHandlerRegistryAttribute), false))
+ .Returns([new CqrsHandlerRegistryAttribute(typeof(PartialGeneratedNotificationHandlerRegistry))]);
+ generatedAssembly
+ .Setup(static assembly => assembly.GetCustomAttributes(typeof(CqrsReflectionFallbackAttribute), false))
+ .Returns([fallbackAttribute]);
+ return generatedAssembly;
+ }
+
+ ///
+ /// 提取容器中针对 generated notification 注册的处理器实现类型。
+ ///
+ /// 已执行注册的测试容器。
+ /// 按注册顺序返回的处理器类型数组。
+ private static Type[] GetGeneratedRegistryNotificationHandlerTypes(MicrosoftDiContainer container)
+ {
+ return container.GetServicesUnsafe
+ .Where(static descriptor =>
+ descriptor.ServiceType == typeof(INotificationHandler) &&
+ descriptor.ImplementationType is not null)
+ .Select(static descriptor => descriptor.ImplementationType!)
+ .ToArray();
+ }
+
+ ///
+ /// 清空本测试依赖的 registrar 静态缓存,确保每个用例都会重新执行 fallback 元数据解析。
+ ///
+ private static void ClearRegistrarCaches()
+ {
+ ClearCache(GetRegistrarCacheField("AssemblyMetadataCache"));
+ ClearCache(GetRegistrarCacheField("RegistryActivationMetadataCache"));
+ ClearCache(GetRegistrarCacheField("LoadableTypesCache"));
+ ClearCache(GetRegistrarCacheField("SupportedHandlerInterfacesCache"));
+ }
+
+ ///
+ /// 通过反射读取 registrar 的静态缓存字段。
+ ///
+ /// 缓存字段名。
+ /// 缓存实例。
+ private static object GetRegistrarCacheField(string fieldName)
+ {
+ var field = GetRegistrarType().GetField(
+ fieldName,
+ BindingFlags.NonPublic | BindingFlags.Static);
+
+ Assert.That(field, Is.Not.Null, $"Missing registrar cache field {fieldName}.");
+
+ return field!.GetValue(null)
+ ?? throw new InvalidOperationException(
+ $"Registrar cache field {fieldName} returned null.");
+ }
+
+ ///
+ /// 清空缓存对象中的已保存条目。
+ ///
+ /// 目标缓存实例。
+ private static void ClearCache(object cache)
+ {
+ _ = InvokeInstanceMethod(cache, "Clear");
+ }
+
+ ///
+ /// 调用缓存对象上的实例方法。
+ ///
+ /// 目标对象。
+ /// 方法名。
+ /// 方法参数。
+ /// 方法返回值。
+ private static object? InvokeInstanceMethod(object target, string methodName, params object[] arguments)
+ {
+ var method = target.GetType().GetMethod(
+ methodName,
+ BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
+
+ Assert.That(method, Is.Not.Null, $"Missing cache method {target.GetType().FullName}.{methodName}.");
+
+ return method!.Invoke(target, arguments);
+ }
+
+ ///
+ /// 获取 CQRS handler registrar 的运行时类型。
+ ///
+ /// registrar 实现类型。
+ private static Type GetRegistrarType()
+ {
+ return typeof(CqrsReflectionFallbackAttribute).Assembly
+ .GetType("GFramework.Cqrs.Internal.CqrsHandlerRegistrar", throwOnError: true)!;
+ }
+
+ ///
+ /// 汇总当前测试期间捕获到的 warning 日志。
+ ///
+ /// 所有 warning 级别日志条目。
+ private IReadOnlyList GetWarningLogs()
+ {
+ Assert.That(_capturingLoggerFactoryProvider, Is.Not.Null);
+
+ return _capturingLoggerFactoryProvider!.Loggers
+ .SelectMany(static logger => logger.Logs)
+ .Where(static log => log.Level == LogLevel.Warning)
+ .ToArray();
+ }
+}