From 9296def108b9ea8581e9ac5eba3397728a15bed4 Mon Sep 17 00:00:00 2001
From: gewuyou <95328647+GeWuYou@users.noreply.github.com>
Date: Thu, 30 Apr 2026 17:50:30 +0800
Subject: [PATCH] =?UTF-8?q?test(cqrs):=20=E8=A1=A5=E9=BD=90=20stream=20inv?=
=?UTF-8?q?oker=20gate=20=E5=9B=9E=E5=BD=92?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 补充 stream invoker descriptor 与 descriptor entry 缺失时整体跳过 provider 元数据的生成器回归
- 优化测试辅助重命名逻辑,精确模拟 metadata name 缺失而不破坏其余合同编译
- 更新 cqrs-rewrite 跟踪与追踪,记录 PR #307 follow-up 的恢复点和验证结果
---
.../Cqrs/CqrsHandlerRegistryGeneratorTests.cs | 112 ++++++++++++++++++
.../todos/cqrs-rewrite-migration-tracking.md | 5 +-
.../traces/cqrs-rewrite-migration-trace.md | 24 ++++
3 files changed, 140 insertions(+), 1 deletion(-)
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` 仍然成立的本地问题