diff --git a/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs b/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs index 1254e273..277b0ee7 100644 --- a/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs +++ b/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs @@ -3156,6 +3156,58 @@ public class CqrsHandlerRegistryGeneratorTests }); } + /// + /// 验证当 runtime 缺少 CqrsStreamInvokerDescriptor 时, + /// 生成器不会继续发射依赖描述符类型的 stream provider 元数据。 + /// + [Test] + public void Does_Not_Emit_Stream_Invoker_Provider_Metadata_When_Runtime_Lacks_Stream_Descriptor_Type() + { + var source = RenameTypeIdentifier( + StreamInvokerProviderSource, + "CqrsStreamInvokerDescriptor", + "MissingCqrsStreamInvokerDescriptor"); + var generatedSource = RunGenerator(source); + + Assert.Multiple(() => + { + Assert.That( + generatedSource, + Does.Contain( + "internal sealed class __GFrameworkGeneratedCqrsHandlerRegistry : global::GFramework.Cqrs.ICqrsHandlerRegistry")); + Assert.That(generatedSource, Does.Not.Contain("ICqrsStreamInvokerProvider")); + Assert.That(generatedSource, Does.Not.Contain("IEnumeratesCqrsStreamInvokerDescriptors")); + Assert.That(generatedSource, Does.Not.Contain("CqrsStreamInvokerDescriptorEntry(")); + Assert.That(generatedSource, Does.Not.Contain("InvokeStreamHandler0")); + }); + } + + /// + /// 验证当 runtime 缺少 CqrsStreamInvokerDescriptorEntry 时, + /// 生成器不会继续保留 stream provider 的枚举接口或静态 invoker 元数据。 + /// + [Test] + public void Does_Not_Emit_Stream_Invoker_Provider_Metadata_When_Runtime_Lacks_Stream_Descriptor_Entry_Type() + { + var source = RenameTypeIdentifier( + StreamInvokerProviderSource, + "CqrsStreamInvokerDescriptorEntry", + "MissingCqrsStreamInvokerDescriptorEntry"); + var generatedSource = RunGenerator(source); + + Assert.Multiple(() => + { + Assert.That( + generatedSource, + Does.Contain( + "internal sealed class __GFrameworkGeneratedCqrsHandlerRegistry : global::GFramework.Cqrs.ICqrsHandlerRegistry")); + Assert.That(generatedSource, Does.Not.Contain("ICqrsStreamInvokerProvider")); + Assert.That(generatedSource, Does.Not.Contain("IEnumeratesCqrsStreamInvokerDescriptors")); + Assert.That(generatedSource, Does.Not.Contain("CqrsStreamInvokerDescriptorEntry(")); + Assert.That(generatedSource, Does.Not.Contain("InvokeStreamHandler0")); + }); + } + /// /// 验证当 stream handler 仍需走 precise reflected 注册时, /// 生成器即使检测到 stream invoker provider runtime 合同,也不会错误发射无法稳定表达隐藏请求/响应类型的 provider 元数据。 @@ -3258,6 +3310,66 @@ public class CqrsHandlerRegistryGeneratorTests return source.Remove(startIndex, endIndex - startIndex); } + /// + /// 仅按完整类型标识符重命名测试输入中的合同类型,避免误伤共享前缀的其他类型名。 + /// + /// 原始测试源码。 + /// 原始合同类型名。 + /// 替换后的占位类型名。 + /// 完成精确类型重命名后的源码。 + private static string RenameTypeIdentifier(string source, string originalTypeName, string replacementTypeName) + { + ArgumentNullException.ThrowIfNull(source); + ArgumentNullException.ThrowIfNull(originalTypeName); + ArgumentNullException.ThrowIfNull(replacementTypeName); + + var result = new System.Text.StringBuilder(source.Length); + var currentIndex = 0; + + while (currentIndex < source.Length) + { + var matchIndex = source.IndexOf(originalTypeName, currentIndex, StringComparison.Ordinal); + if (matchIndex < 0) + { + result.Append(source, currentIndex, source.Length - currentIndex); + break; + } + + result.Append(source, currentIndex, matchIndex - currentIndex); + + if (IsIdentifierBoundary(source, matchIndex - 1) && + IsIdentifierBoundary(source, matchIndex + originalTypeName.Length)) + { + result.Append(replacementTypeName); + } + else + { + result.Append(originalTypeName); + } + + currentIndex = matchIndex + originalTypeName.Length; + } + + return result.ToString(); + } + + /// + /// 判断给定位置是否位于 C# 标识符边界,用于避免把共享前缀的其他类型名一并改写。 + /// + /// 待检查的完整源码。 + /// 边界位置;允许落在字符串两端之外。 + /// 若当前位置不在标识符内部,则返回 + private static bool IsIdentifierBoundary(string source, int index) + { + if (index < 0 || index >= source.Length) + { + return true; + } + + var character = source[index]; + return !char.IsLetterOrDigit(character) && character != '_'; + } + /// /// 统计生成源码中某个固定片段的出现次数,用于锁定程序集级 fallback 特性的发射个数。 /// 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 ee254359..06cc5b02 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-075` +- 恢复点编号:`CQRS-REWRITE-RP-076` - 当前阶段:`Phase 8` - 当前焦点: - 已完成一轮 `CQRS vs Mediator` 只读评估归档,结论已沉淀到 `archive/todos/cqrs-vs-mediator-assessment-rp063.md` @@ -74,6 +74,9 @@ CQRS 迁移与收敛。 - 已完成一轮 invoker provider gate 合同回归: - `GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs` 现新增四条回归,分别锁定 request / stream 在缺少 `ICqrsRequestInvokerProvider`、`IEnumeratesCqrsRequestInvokerDescriptors`、`ICqrsStreamInvokerProvider` 或 `IEnumeratesCqrsStreamInvokerDescriptors` 时,generator 都会整体跳过对应 provider 元数据发射 - 本轮最初采用固定源码片段替换来裁剪测试输入,但因三引号字符串缩进差异导致 helper 过脆;当前已收敛为按稳定起止标记移除源码块的 `RemoveBlock(...)` helper,避免 gate 回归依赖精确空格对齐 + - 已完成一轮 stream invoker descriptor gate 合同补强: + - `GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs` 现额外新增两条 stream gate 回归,分别锁定 runtime 缺少 `CqrsStreamInvokerDescriptor` 或 `CqrsStreamInvokerDescriptorEntry` 时,generator 同样会整体跳过 stream provider 元数据发射 + - 本轮补强直接对应 `CqrsHandlerRegistryGenerator` 中 `supportsStreamInvokerProvider` 的四项合同探测,避免此前只覆盖 provider / enumerator 缺失而漏掉 descriptor 两条分支 - 已完成一轮 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 两条错误消息语义一致 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 8a3d6cde..95bac3aa 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,30 @@ ## 2026-04-30 +### 阶段:PR #307 stream invoker gate 回归补强(CQRS-REWRITE-RP-076) + +- 继续沿用 `$gframework-pr-review` 对 `PR #307` 的 latest-head review triage,只处理本地仍成立且写集可控的 generator regression gap +- 主线程复核 `GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs:88-92` 后确认:`supportsStreamInvokerProvider` 依赖四项合同,但现有测试只覆盖 `ICqrsStreamInvokerProvider` 与 `IEnumeratesCqrsStreamInvokerDescriptors` 缺失分支,确实遗漏 `CqrsStreamInvokerDescriptor` / `CqrsStreamInvokerDescriptorEntry` +- 本轮实现收敛: + - `GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs` 新增两条 `RemoveBlock(...)` 回归,分别移除 `CqrsStreamInvokerDescriptor` 与 `CqrsStreamInvokerDescriptorEntry` 合同定义 + - 新回归继续锁定统一结果:当 stream invoker runtime 合同四者缺一时,generated registry 不会残留 provider 接口、descriptor entry 枚举或静态 invoker 桥接 + - active tracking 已把恢复点推进到 `RP-076`,避免 PR review 结论只体现在测试代码里 + +### 验证(RP-076) + +- `dotnet build GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release` + - 结果:通过,`0 warning / 0 error` + - 备注:首轮并发跑 build/test 时出现过 `MSB3248` / `MSB3026` 输出文件占用噪音;按仓库规则改为串行复核后,本轮 authoritative build 结果为干净通过 +- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "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.Does_Not_Emit_Stream_Invoker_Provider_Metadata_When_Runtime_Lacks_Stream_Descriptor_Type|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Does_Not_Emit_Stream_Invoker_Provider_Metadata_When_Runtime_Lacks_Stream_Descriptor_Entry_Type|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Emits_Stream_Invoker_Provider_Metadata_When_Runtime_Contract_Is_Available"` + - 结果:通过,`5/5` passed + - 备注:新增两条 descriptor gate 回归与既有 stream happy-path 一并通过,确认 `supportsStreamInvokerProvider` 的四项合同缺一不可 + +### 当前下一步(RP-076) + +1. 提交本轮 `PR #307` stream gate 合同补强与 `ai-plan` 恢复点更新 +2. 后续若继续处理 review,优先清点 request 侧是否也存在同构遗漏,再决定是否追加同批对称测试 +3. 保持忽略工作区里无关的 `.gitignore` 本地改动,不把它混入本轮提交 + ### 阶段:PR #307 review follow-up 收敛(CQRS-REWRITE-RP-075) - 在 `RP-074` 后继续沿用 `gframework-batch-boot 50` 的低风险切片策略,本轮只处理 `$gframework-pr-review` 对当前 `PR #307` 仍然成立的本地问题