diff --git a/GFramework.Cqrs.Tests/Cqrs/CqrsGeneratedRequestInvokerProviderTests.cs b/GFramework.Cqrs.Tests/Cqrs/CqrsGeneratedRequestInvokerProviderTests.cs index 2d2725b2..e260b380 100644 --- a/GFramework.Cqrs.Tests/Cqrs/CqrsGeneratedRequestInvokerProviderTests.cs +++ b/GFramework.Cqrs.Tests/Cqrs/CqrsGeneratedRequestInvokerProviderTests.cs @@ -185,6 +185,318 @@ internal sealed class CqrsGeneratedRequestInvokerProviderTests Assert.That(results, Is.EqualTo([300, 301])); } + /// + /// 验证当 generated request invoker provider 返回实例方法时, + /// dispatcher 会显式拒绝该描述符,而不是在后续绑定阶段静默接受非法合同。 + /// + [Test] + public void SendAsync_Should_Throw_When_Generated_Request_Invoker_Is_Not_Static() + { + var generatedAssembly = CreateGeneratedAssembly( + typeof(NonStaticRequestInvokerProviderRegistry), + "GFramework.Cqrs.Tests.Cqrs.NonStaticRequestInvokerAssembly, Version=1.0.0.0"); + var container = new MicrosoftDiContainer(); + + CqrsTestRuntime.RegisterHandlers(container, generatedAssembly.Object); + container.Freeze(); + + var context = new ArchitectureContext(container); + var exception = Assert.ThrowsAsync(async () => + await context.SendRequestAsync(new GeneratedRequestInvokerRequest("payload")).ConfigureAwait(false)); + Assert.That(exception, Is.Not.Null); + Assert.That(exception!.Message, Does.Contain("non-static invoker method")); + } + + /// + /// 验证当 generated request invoker provider 返回与 dispatcher 委托签名不兼容的方法时, + /// dispatcher 会显式抛出契约错误。 + /// + [Test] + public void SendAsync_Should_Throw_When_Generated_Request_Invoker_Is_Incompatible() + { + var generatedAssembly = CreateGeneratedAssembly( + typeof(IncompatibleRequestInvokerProviderRegistry), + "GFramework.Cqrs.Tests.Cqrs.IncompatibleRequestInvokerAssembly, Version=1.0.0.0"); + var container = new MicrosoftDiContainer(); + + CqrsTestRuntime.RegisterHandlers(container, generatedAssembly.Object); + container.Freeze(); + + var context = new ArchitectureContext(container); + var exception = Assert.ThrowsAsync(async () => + await context.SendRequestAsync(new GeneratedRequestInvokerRequest("payload")).ConfigureAwait(false)); + Assert.That(exception, Is.Not.Null); + Assert.That(exception!.Message, Does.Contain("incompatible invoker")); + } + + /// + /// 验证当 generated stream invoker provider 返回实例方法时, + /// dispatcher 会在首次建流时显式拒绝该描述符。 + /// + [Test] + public void CreateStream_Should_Throw_When_Generated_Stream_Invoker_Is_Not_Static() + { + var generatedAssembly = CreateGeneratedAssembly( + typeof(NonStaticStreamInvokerProviderRegistry), + "GFramework.Cqrs.Tests.Cqrs.NonStaticStreamInvokerAssembly, Version=1.0.0.0"); + var container = new MicrosoftDiContainer(); + + CqrsTestRuntime.RegisterHandlers(container, generatedAssembly.Object); + container.Freeze(); + + var context = new ArchitectureContext(container); + var exception = Assert.ThrowsAsync(async () => + await DrainAsync(context.CreateStream(new GeneratedStreamInvokerRequest(3))).ConfigureAwait(false)); + Assert.That(exception, Is.Not.Null); + Assert.That(exception!.Message, Does.Contain("non-static invoker method")); + } + + /// + /// 验证当 generated stream invoker provider 返回与 dispatcher 委托签名不兼容的方法时, + /// dispatcher 会显式抛出契约错误。 + /// + [Test] + public void CreateStream_Should_Throw_When_Generated_Stream_Invoker_Is_Incompatible() + { + var generatedAssembly = CreateGeneratedAssembly( + typeof(IncompatibleStreamInvokerProviderRegistry), + "GFramework.Cqrs.Tests.Cqrs.IncompatibleStreamInvokerAssembly, Version=1.0.0.0"); + var container = new MicrosoftDiContainer(); + + CqrsTestRuntime.RegisterHandlers(container, generatedAssembly.Object); + container.Freeze(); + + var context = new ArchitectureContext(container); + var exception = Assert.ThrowsAsync(async () => + await DrainAsync(context.CreateStream(new GeneratedStreamInvokerRequest(3))).ConfigureAwait(false)); + Assert.That(exception, Is.Not.Null); + Assert.That(exception!.Message, Does.Contain("incompatible invoker")); + } + + /// + /// 模拟返回实例 request invoker 方法的 generated registry。 + /// + private sealed class NonStaticRequestInvokerProviderRegistry : + ICqrsHandlerRegistry, + ICqrsRequestInvokerProvider, + IEnumeratesCqrsRequestInvokerDescriptors + { + private static readonly CqrsRequestInvokerDescriptorEntry DescriptorEntry = new( + typeof(GeneratedRequestInvokerRequest), + typeof(string), + new CqrsRequestInvokerDescriptor( + typeof(IRequestHandler), + typeof(NonStaticRequestInvokerProviderRegistry).GetMethod( + nameof(InvokeGenerated), + BindingFlags.NonPublic | BindingFlags.Instance)!)); + + /// + public void Register(IServiceCollection services, ILogger logger) + { + ArgumentNullException.ThrowIfNull(services); + ArgumentNullException.ThrowIfNull(logger); + + services.AddTransient( + typeof(IRequestHandler), + typeof(GeneratedRequestInvokerRequestHandler)); + } + + /// + public bool TryGetDescriptor( + Type requestType, + Type responseType, + out CqrsRequestInvokerDescriptor? descriptor) + { + if (requestType == typeof(GeneratedRequestInvokerRequest) && responseType == typeof(string)) + { + descriptor = DescriptorEntry.Descriptor; + return true; + } + + descriptor = null; + return false; + } + + /// + public IReadOnlyList GetDescriptors() + { + return [DescriptorEntry]; + } + + private ValueTask InvokeGenerated(object handler, object request, CancellationToken cancellationToken) + { + return ValueTask.FromResult(string.Empty); + } + } + + /// + /// 模拟返回不兼容 request invoker 方法的 generated registry。 + /// + private sealed class IncompatibleRequestInvokerProviderRegistry : + ICqrsHandlerRegistry, + ICqrsRequestInvokerProvider, + IEnumeratesCqrsRequestInvokerDescriptors + { + private static readonly CqrsRequestInvokerDescriptorEntry DescriptorEntry = new( + typeof(GeneratedRequestInvokerRequest), + typeof(string), + new CqrsRequestInvokerDescriptor( + typeof(IRequestHandler), + typeof(IncompatibleRequestInvokerProviderRegistry).GetMethod( + nameof(InvokeGenerated), + BindingFlags.NonPublic | BindingFlags.Static)!)); + + /// + public void Register(IServiceCollection services, ILogger logger) + { + ArgumentNullException.ThrowIfNull(services); + ArgumentNullException.ThrowIfNull(logger); + + services.AddTransient( + typeof(IRequestHandler), + typeof(GeneratedRequestInvokerRequestHandler)); + } + + /// + public bool TryGetDescriptor( + Type requestType, + Type responseType, + out CqrsRequestInvokerDescriptor? descriptor) + { + if (requestType == typeof(GeneratedRequestInvokerRequest) && responseType == typeof(string)) + { + descriptor = DescriptorEntry.Descriptor; + return true; + } + + descriptor = null; + return false; + } + + /// + public IReadOnlyList GetDescriptors() + { + return [DescriptorEntry]; + } + + private static string InvokeGenerated(object handler, object request) + { + return string.Empty; + } + } + + /// + /// 模拟返回实例 stream invoker 方法的 generated registry。 + /// + private sealed class NonStaticStreamInvokerProviderRegistry : + ICqrsHandlerRegistry, + ICqrsStreamInvokerProvider, + IEnumeratesCqrsStreamInvokerDescriptors + { + private static readonly CqrsStreamInvokerDescriptorEntry DescriptorEntry = new( + typeof(GeneratedStreamInvokerRequest), + typeof(int), + new CqrsStreamInvokerDescriptor( + typeof(IStreamRequestHandler), + typeof(NonStaticStreamInvokerProviderRegistry).GetMethod( + nameof(InvokeGenerated), + BindingFlags.NonPublic | BindingFlags.Instance)!)); + + /// + public void Register(IServiceCollection services, ILogger logger) + { + ArgumentNullException.ThrowIfNull(services); + ArgumentNullException.ThrowIfNull(logger); + + services.AddTransient( + typeof(IStreamRequestHandler), + typeof(GeneratedStreamInvokerRequestHandler)); + } + + /// + public bool TryGetDescriptor( + Type requestType, + Type responseType, + out CqrsStreamInvokerDescriptor? descriptor) + { + if (requestType == typeof(GeneratedStreamInvokerRequest) && responseType == typeof(int)) + { + descriptor = DescriptorEntry.Descriptor; + return true; + } + + descriptor = null; + return false; + } + + /// + public IReadOnlyList GetDescriptors() + { + return [DescriptorEntry]; + } + + private object InvokeGenerated(object handler, object request, CancellationToken cancellationToken) + { + return Array.Empty().ToAsyncEnumerable(); + } + } + + /// + /// 模拟返回不兼容 stream invoker 方法的 generated registry。 + /// + private sealed class IncompatibleStreamInvokerProviderRegistry : + ICqrsHandlerRegistry, + ICqrsStreamInvokerProvider, + IEnumeratesCqrsStreamInvokerDescriptors + { + private static readonly CqrsStreamInvokerDescriptorEntry DescriptorEntry = new( + typeof(GeneratedStreamInvokerRequest), + typeof(int), + new CqrsStreamInvokerDescriptor( + typeof(IStreamRequestHandler), + typeof(IncompatibleStreamInvokerProviderRegistry).GetMethod( + nameof(InvokeGenerated), + BindingFlags.NonPublic | BindingFlags.Static)!)); + + /// + public void Register(IServiceCollection services, ILogger logger) + { + ArgumentNullException.ThrowIfNull(services); + ArgumentNullException.ThrowIfNull(logger); + + services.AddTransient( + typeof(IStreamRequestHandler), + typeof(GeneratedStreamInvokerRequestHandler)); + } + + /// + public bool TryGetDescriptor( + Type requestType, + Type responseType, + out CqrsStreamInvokerDescriptor? descriptor) + { + if (requestType == typeof(GeneratedStreamInvokerRequest) && responseType == typeof(int)) + { + descriptor = DescriptorEntry.Descriptor; + return true; + } + + descriptor = null; + return false; + } + + /// + public IReadOnlyList GetDescriptors() + { + return [DescriptorEntry]; + } + + private static object InvokeGenerated(object handler, object request) + { + return Array.Empty().ToAsyncEnumerable(); + } + } + /// /// 创建带有 generated request invoker registry 元数据的程序集替身。 /// @@ -215,6 +527,27 @@ internal sealed class CqrsGeneratedRequestInvokerProviderTests return generatedAssembly; } + /// + /// 创建带有指定 generated registry 元数据的程序集替身。 + /// + /// 测试 registry 类型。 + /// 模拟程序集全名。 + /// 可用于 registrar 注册流程的程序集替身。 + private static Mock CreateGeneratedAssembly(Type registryType, string assemblyFullName) + { + ArgumentNullException.ThrowIfNull(registryType); + ArgumentException.ThrowIfNullOrWhiteSpace(assemblyFullName); + + var generatedAssembly = new Mock(); + generatedAssembly + .SetupGet(static assembly => assembly.FullName) + .Returns(assemblyFullName); + generatedAssembly + .Setup(static assembly => assembly.GetCustomAttributes(typeof(CqrsHandlerRegistryAttribute), false)) + .Returns([new CqrsHandlerRegistryAttribute(registryType)]); + return generatedAssembly; + } + /// /// 创建带有 hidden implementation request invoker registry 元数据的程序集替身。 /// diff --git a/GFramework.Cqrs/Internal/CqrsDispatcher.cs b/GFramework.Cqrs/Internal/CqrsDispatcher.cs index 17cbb67a..0fa91f76 100644 --- a/GFramework.Cqrs/Internal/CqrsDispatcher.cs +++ b/GFramework.Cqrs/Internal/CqrsDispatcher.cs @@ -256,14 +256,23 @@ internal sealed class CqrsDispatcher( $"Generated CQRS request invoker provider returned a non-static invoker method for request type {requestType.FullName} and response type {typeof(TResponse).FullName}."); } - if (Delegate.CreateDelegate(typeof(RequestInvoker), descriptor.InvokerMethod) is not - RequestInvoker invoker) + try + { + if (Delegate.CreateDelegate(typeof(RequestInvoker), descriptor.InvokerMethod) is not + RequestInvoker invoker) + { + throw new InvalidOperationException( + $"Generated CQRS request invoker provider returned an incompatible invoker for request type {requestType.FullName} and response type {typeof(TResponse).FullName}."); + } + + return new RequestInvokerDescriptor(descriptor.HandlerType, invoker); + } + catch (ArgumentException exception) { throw new InvalidOperationException( - $"Generated CQRS request invoker provider returned an incompatible invoker for request type {requestType.FullName} and response type {typeof(TResponse).FullName}."); + $"Generated CQRS request invoker provider returned an incompatible invoker for request type {requestType.FullName} and response type {typeof(TResponse).FullName}.", + exception); } - - return new RequestInvokerDescriptor(descriptor.HandlerType, invoker); } /// @@ -328,13 +337,22 @@ internal sealed class CqrsDispatcher( $"Generated CQRS stream invoker provider returned a non-static invoker method for request type {requestType.FullName} and response type {responseType.FullName}."); } - if (Delegate.CreateDelegate(typeof(StreamInvoker), descriptor.InvokerMethod) is not StreamInvoker invoker) + try + { + if (Delegate.CreateDelegate(typeof(StreamInvoker), descriptor.InvokerMethod) is not StreamInvoker invoker) + { + throw new InvalidOperationException( + $"Generated CQRS stream invoker provider returned an incompatible invoker for request type {requestType.FullName} and response type {responseType.FullName}."); + } + + return new StreamInvokerDescriptor(descriptor.HandlerType, invoker); + } + catch (ArgumentException exception) { throw new InvalidOperationException( - $"Generated CQRS stream invoker provider returned an incompatible invoker for request type {requestType.FullName} and response type {responseType.FullName}."); + $"Generated CQRS stream invoker provider returned an incompatible invoker for request type {requestType.FullName} and response type {responseType.FullName}.", + exception); } - - return new StreamInvokerDescriptor(descriptor.HandlerType, invoker); } /// diff --git a/ai-plan/public/cqrs-rewrite/todos/cqrs-rewrite-migration-tracking.md b/ai-plan/public/cqrs-rewrite/todos/cqrs-rewrite-migration-tracking.md index e0c247ec..271ff6fc 100644 --- a/ai-plan/public/cqrs-rewrite/todos/cqrs-rewrite-migration-tracking.md +++ b/ai-plan/public/cqrs-rewrite/todos/cqrs-rewrite-migration-tracking.md @@ -7,7 +7,7 @@ CQRS 迁移与收敛。 ## 当前恢复点 -- 恢复点编号:`CQRS-REWRITE-RP-072` +- 恢复点编号:`CQRS-REWRITE-RP-073` - 当前阶段:`Phase 8` - 当前焦点: - 已完成一轮 `CQRS vs Mediator` 只读评估归档,结论已沉淀到 `archive/todos/cqrs-vs-mediator-assessment-rp063.md` @@ -74,6 +74,10 @@ CQRS 迁移与收敛。 - 已完成一轮 invoker provider gate 合同回归: - `GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs` 现新增四条回归,分别锁定 request / stream 在缺少 `ICqrsRequestInvokerProvider`、`IEnumeratesCqrsRequestInvokerDescriptors`、`ICqrsStreamInvokerProvider` 或 `IEnumeratesCqrsStreamInvokerDescriptors` 时,generator 都会整体跳过对应 provider 元数据发射 - 本轮最初采用固定源码片段替换来裁剪测试输入,但因三引号字符串缩进差异导致 helper 过脆;当前已收敛为按稳定起止标记移除源码块的 `RemoveBlock(...)` helper,避免 gate 回归依赖精确空格对齐 + - 已完成一轮 generated invoker provider runtime 失败边界修复: + - `GFramework.Cqrs.Tests/Cqrs/CqrsGeneratedRequestInvokerProviderTests.cs` 现新增 request / stream 两组 `non-static invoker` 与 `incompatible invoker` 回归,锁定 dispatcher 在首次绑定阶段会显式拒绝非法 generated descriptor + - `GFramework.Cqrs/Internal/CqrsDispatcher.cs` 现把 `Delegate.CreateDelegate(...)` 抛出的 `ArgumentException` 统一包装为已有 XML 文档承诺的 `InvalidOperationException`,保持 request / stream 两条错误消息语义一致 + - 本轮顺手为新增异步断言补齐 `ConfigureAwait(false)`,消除新测试引入的 `MA0004` warning - 当前相对 `origin/main` 的累计 branch diff 为 `24 files / 1754 changed lines`,仍低于本轮 `$gframework-batch-boot 50` 的主要 stop condition,可继续推进下一批低风险切片 - 已将 mixed fallback 场景进一步收敛:当 runtime 允许同一程序集声明多个 `CqrsReflectionFallbackAttribute` 实例时,generator 现会把可直接引用的 fallback handlers 与仅能按名称恢复的 fallback handlers 拆分发射 - `CqrsReflectionFallbackAttribute` 现允许多实例,以承载 `Type[]` 与字符串 fallback 元数据的组合输出 @@ -297,6 +301,12 @@ CQRS 迁移与收敛。 - `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Does_Not_Emit_Request_Invoker_Provider_Metadata_When_Runtime_Lacks_Request_Provider_Interface|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Does_Not_Emit_Request_Invoker_Provider_Metadata_When_Runtime_Lacks_Request_Descriptor_Enumerator|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Does_Not_Emit_Stream_Invoker_Provider_Metadata_When_Runtime_Lacks_Stream_Provider_Interface|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Does_Not_Emit_Stream_Invoker_Provider_Metadata_When_Runtime_Lacks_Stream_Descriptor_Enumerator|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Emits_Request_Invoker_Provider_Metadata_When_Runtime_Contract_Is_Available|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Emits_Stream_Invoker_Provider_Metadata_When_Runtime_Contract_Is_Available"` - 结果:通过 - 备注:`6/6` passed;锁定 request / stream provider gate 依赖“provider 接口 + descriptor 枚举接口”同时存在,且原有 happy-path 发射仍保持通过 +- `dotnet build GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release` + - 结果:通过 + - 备注:并行执行 build/test 时出现 `MSB3026` 输出文件竞争噪音;当前已确认没有新增 analyzer warning,`GFramework.Cqrs.Tests` 仍能完成 Release 构建 +- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsGeneratedRequestInvokerProviderTests.SendAsync_Should_Throw_When_Generated_Request_Invoker_Is_Not_Static|FullyQualifiedName~CqrsGeneratedRequestInvokerProviderTests.SendAsync_Should_Throw_When_Generated_Request_Invoker_Is_Incompatible|FullyQualifiedName~CqrsGeneratedRequestInvokerProviderTests.CreateStream_Should_Throw_When_Generated_Stream_Invoker_Is_Not_Static|FullyQualifiedName~CqrsGeneratedRequestInvokerProviderTests.CreateStream_Should_Throw_When_Generated_Stream_Invoker_Is_Incompatible|FullyQualifiedName~CqrsGeneratedRequestInvokerProviderTests.SendAsync_Should_Use_Generated_Request_Invoker_When_Provider_Is_Registered|FullyQualifiedName~CqrsGeneratedRequestInvokerProviderTests.CreateStream_Should_Use_Generated_Stream_Invoker_When_Provider_Is_Registered"` + - 结果:通过 + - 备注:`6/6` passed;确认 request / stream 的非法 generated invoker 现统一抛出 `InvalidOperationException`,且原有 happy-path 未回归 - `dotnet build GFramework.Core/GFramework.Core.csproj -c Release` - 结果:通过 - 备注:`0 warning / 0 error`;确认 `CqrsRuntimeModule` 接线变更未引入 `GFramework.Core` 模块构建问题 @@ -330,6 +340,6 @@ CQRS 迁移与收敛。 ## 下一步 -1. 在保持 branch diff 明显低于 `50 files` 的前提下,继续挑选下一批低风险 `dispatch/invoker` 收敛切片,并优先考虑 request / stream provider 的 runtime 失败边界或 generator gate 合同补强 +1. 在保持 branch diff 明显低于 `50 files` 的前提下,继续挑选下一批低风险 `dispatch/invoker` 收敛切片,并优先考虑 request / stream provider 的剩余 runtime 失败边界或 generator gate 合同补强 2. 基于已落地的 notification publisher seam,评估是否需要第二阶段公开配置面、并行 publisher 或 telemetry decorator 3. 单独规划旧 `Command` / `Query` API 的收口顺序;`LegacyICqrsRuntime` compatibility slice 已收口到显式 helper 与专门测试,可暂时移出最高优先级 diff --git a/ai-plan/public/cqrs-rewrite/traces/cqrs-rewrite-migration-trace.md b/ai-plan/public/cqrs-rewrite/traces/cqrs-rewrite-migration-trace.md index 91f372d4..674ca8a1 100644 --- a/ai-plan/public/cqrs-rewrite/traces/cqrs-rewrite-migration-trace.md +++ b/ai-plan/public/cqrs-rewrite/traces/cqrs-rewrite-migration-trace.md @@ -2,6 +2,31 @@ ## 2026-04-30 +### 阶段:generated invoker provider runtime 失败边界修复(CQRS-REWRITE-RP-073) + +- 在 `RP-072` 提交后继续按 `gframework-batch-boot 50` 执行;当前 branch diff 相对 `origin/main` 仍为 `24 files`,文件阈值 headroom 依然充足,因此继续推进下一批 runtime 失败边界回归 +- 本轮原计划只补 `CqrsGeneratedRequestInvokerProviderTests` 的 request / stream 非 happy-path 回归,但定向测试首轮直接暴露出一个真实 runtime 缺口: + - `CqrsDispatcher.CreateRequestInvokerDescriptor(...)` 与 `CreateStreamInvokerDescriptor(...)` 的 XML 文档和消息语义都承诺会抛 `InvalidOperationException` + - 实际实现先调用 `Delegate.CreateDelegate(...)`,当 invoker 签名不兼容时会直接冒出 `ArgumentException`,导致文档承诺与运行时行为不一致 +- 主线程已完成: + - `GFramework.Cqrs.Tests/Cqrs/CqrsGeneratedRequestInvokerProviderTests.cs` 新增 request / stream 两组 `non-static invoker` 与 `incompatible invoker` 回归,并保留 request / stream happy-path 作为同批守护断言 + - `GFramework.Cqrs/Internal/CqrsDispatcher.cs` 现对 request / stream 两条 descriptor 创建路径统一捕获 `ArgumentException`,并转换成带原有错误消息的 `InvalidOperationException` + - 新增异步断言已补齐 `ConfigureAwait(false)`,避免测试批次自身引入 `MA0004` analyzer warning + +### 验证(RP-073) + +- `dotnet build GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release` + - 结果:通过 + - 备注:并行执行 build/test 时曾出现 `MSB3026` 输出文件竞争噪音;无真实编译失败,也未引入新增 analyzer warning +- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsGeneratedRequestInvokerProviderTests.SendAsync_Should_Throw_When_Generated_Request_Invoker_Is_Not_Static|FullyQualifiedName~CqrsGeneratedRequestInvokerProviderTests.SendAsync_Should_Throw_When_Generated_Request_Invoker_Is_Incompatible|FullyQualifiedName~CqrsGeneratedRequestInvokerProviderTests.CreateStream_Should_Throw_When_Generated_Stream_Invoker_Is_Not_Static|FullyQualifiedName~CqrsGeneratedRequestInvokerProviderTests.CreateStream_Should_Throw_When_Generated_Stream_Invoker_Is_Incompatible|FullyQualifiedName~CqrsGeneratedRequestInvokerProviderTests.SendAsync_Should_Use_Generated_Request_Invoker_When_Provider_Is_Registered|FullyQualifiedName~CqrsGeneratedRequestInvokerProviderTests.CreateStream_Should_Use_Generated_Stream_Invoker_When_Provider_Is_Registered"` + - 结果:通过,`6/6` passed + +### 当前下一步(RP-073) + +1. 先提交本轮 runtime 失败边界修复与恢复点更新 +2. 重新复算 branch diff 后,再判断是否继续推进剩余 provider 失败边界或在接近阈值前停下 +3. 若继续下一批,优先保持单文件或双文件写集,避免在本轮后段扩散 review 面积 + ### 阶段:invoker provider gate 合同回归(CQRS-REWRITE-RP-072) - 在 `RP-071` 提交后继续按 `gframework-batch-boot 50` 执行;当前 branch diff 相对 `origin/main` 仍为 `24 files`,未接近主要 stop condition,因此继续追加一轮 test-only generator 合同回归