test(cqrs): 补齐 stream invoker gate 回归

- 补充 stream invoker descriptor 与 descriptor entry 缺失时整体跳过 provider 元数据的生成器回归

- 优化测试辅助重命名逻辑,精确模拟 metadata name 缺失而不破坏其余合同编译

- 更新 cqrs-rewrite 跟踪与追踪,记录 PR #307 follow-up 的恢复点和验证结果
This commit is contained in:
gewuyou 2026-04-30 17:50:30 +08:00
parent 83528742bb
commit 9296def108
3 changed files with 140 additions and 1 deletions

View File

@ -3156,6 +3156,58 @@ public class CqrsHandlerRegistryGeneratorTests
});
}
/// <summary>
/// 验证当 runtime 缺少 <c>CqrsStreamInvokerDescriptor</c> 时,
/// 生成器不会继续发射依赖描述符类型的 stream provider 元数据。
/// </summary>
[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"));
});
}
/// <summary>
/// 验证当 runtime 缺少 <c>CqrsStreamInvokerDescriptorEntry</c> 时,
/// 生成器不会继续保留 stream provider 的枚举接口或静态 invoker 元数据。
/// </summary>
[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"));
});
}
/// <summary>
/// 验证当 stream handler 仍需走 precise reflected 注册时,
/// 生成器即使检测到 stream invoker provider runtime 合同,也不会错误发射无法稳定表达隐藏请求/响应类型的 provider 元数据。
@ -3258,6 +3310,66 @@ public class CqrsHandlerRegistryGeneratorTests
return source.Remove(startIndex, endIndex - startIndex);
}
/// <summary>
/// 仅按完整类型标识符重命名测试输入中的合同类型,避免误伤共享前缀的其他类型名。
/// </summary>
/// <param name="source">原始测试源码。</param>
/// <param name="originalTypeName">原始合同类型名。</param>
/// <param name="replacementTypeName">替换后的占位类型名。</param>
/// <returns>完成精确类型重命名后的源码。</returns>
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();
}
/// <summary>
/// 判断给定位置是否位于 C# 标识符边界,用于避免把共享前缀的其他类型名一并改写。
/// </summary>
/// <param name="source">待检查的完整源码。</param>
/// <param name="index">边界位置;允许落在字符串两端之外。</param>
/// <returns>若当前位置不在标识符内部,则返回 <see langword="true" />。</returns>
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 != '_';
}
/// <summary>
/// 统计生成源码中某个固定片段的出现次数,用于锁定程序集级 fallback 特性的发射个数。
/// </summary>

View File

@ -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 两条错误消息语义一致

View File

@ -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` 仍然成立的本地问题