fix(test-helpers): 收敛PR300评审问题

- 修复测试架构上下文、生命周期钩子与注册表初始化钩子的评审问题,避免静默成功或错误共享状态

- 补充 TestResourceLoader、TestLogger、CapturingLoggerFactoryProvider 与 CQRS 测试辅助类型的契约文档和并发语义

- 新增测试覆盖并更新 analyzer-warning-reduction 活跃跟踪,记录 PR #300 跟进验证与现存 Cqrs warning blocker
This commit is contained in:
gewuyou 2026-04-28 09:26:20 +08:00
parent ba4ace8d40
commit 5693ab7e6f
14 changed files with 403 additions and 88 deletions

View File

@ -1,3 +1,7 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using GFramework.Core.Abstractions.Architectures; using GFramework.Core.Abstractions.Architectures;
using GFramework.Core.Abstractions.Command; using GFramework.Core.Abstractions.Command;
using GFramework.Core.Abstractions.Environment; using GFramework.Core.Abstractions.Environment;
@ -27,6 +31,7 @@ namespace GFramework.Core.Tests.Architectures;
public class TestArchitectureContext : IArchitectureContext public class TestArchitectureContext : IArchitectureContext
{ {
private readonly MicrosoftDiContainer _container = new(); private readonly MicrosoftDiContainer _container = new();
private readonly EventBus _eventBus = new();
/// <summary> /// <summary>
/// 获取用于解析测试服务的依赖注入容器。 /// 获取用于解析测试服务的依赖注入容器。
@ -36,7 +41,11 @@ public class TestArchitectureContext : IArchitectureContext
/// <summary> /// <summary>
/// 获取测试事件总线实例。 /// 获取测试事件总线实例。
/// </summary> /// </summary>
public IEventBus EventBus => new EventBus(); /// <remarks>
/// 返回同一个缓存事件总线,以便 <see cref="RegisterEvent{TEvent}" />、<see cref="SendEvent{TEvent}()" /> 与
/// <see cref="UnRegisterEvent{TEvent}" /> 在同一份订阅状态上协作。
/// </remarks>
public IEventBus EventBus => _eventBus;
/// <summary> /// <summary>
/// 获取测试命令执行器实例。 /// 获取测试命令执行器实例。
@ -183,6 +192,7 @@ public class TestArchitectureContext : IArchitectureContext
/// <typeparam name="TEvent">事件类型。</typeparam> /// <typeparam name="TEvent">事件类型。</typeparam>
public void SendEvent<TEvent>() where TEvent : new() public void SendEvent<TEvent>() where TEvent : new()
{ {
_eventBus.Send<TEvent>();
} }
/// <summary> /// <summary>
@ -192,6 +202,8 @@ public class TestArchitectureContext : IArchitectureContext
/// <param name="e">事件实例。</param> /// <param name="e">事件实例。</param>
public void SendEvent<TEvent>(TEvent e) where TEvent : class public void SendEvent<TEvent>(TEvent e) where TEvent : class
{ {
ArgumentNullException.ThrowIfNull(e);
_eventBus.Send(e);
} }
/// <summary> /// <summary>
@ -199,10 +211,11 @@ public class TestArchitectureContext : IArchitectureContext
/// </summary> /// </summary>
/// <typeparam name="TEvent">事件类型。</typeparam> /// <typeparam name="TEvent">事件类型。</typeparam>
/// <param name="handler">事件处理委托。</param> /// <param name="handler">事件处理委托。</param>
/// <returns>用于测试的注销句柄。</returns> /// <returns>用于测试的事件注销句柄。</returns>
public IUnRegister RegisterEvent<TEvent>(Action<TEvent> handler) public IUnRegister RegisterEvent<TEvent>(Action<TEvent> handler)
{ {
return new DefaultUnRegister(() => { }); ArgumentNullException.ThrowIfNull(handler);
return _eventBus.Register(handler);
} }
/// <summary> /// <summary>
@ -212,6 +225,8 @@ public class TestArchitectureContext : IArchitectureContext
/// <param name="onEvent">事件处理委托。</param> /// <param name="onEvent">事件处理委托。</param>
public void UnRegisterEvent<TEvent>(Action<TEvent> onEvent) public void UnRegisterEvent<TEvent>(Action<TEvent> onEvent)
{ {
ArgumentNullException.ThrowIfNull(onEvent);
_eventBus.UnRegister(onEvent);
} }
/// <summary> /// <summary>
@ -358,8 +373,10 @@ public class TestArchitectureContext : IArchitectureContext
/// 发送旧版命令。 /// 发送旧版命令。
/// </summary> /// </summary>
/// <param name="command">命令对象。</param> /// <param name="command">命令对象。</param>
/// <exception cref="NotSupportedException">该测试桩不支持旧版命令执行入口。</exception>
public void SendCommand(ICommand command) public void SendCommand(ICommand command)
{ {
throw new NotSupportedException();
} }
/// <summary> /// <summary>
@ -367,20 +384,21 @@ public class TestArchitectureContext : IArchitectureContext
/// </summary> /// </summary>
/// <typeparam name="TResult">返回值类型。</typeparam> /// <typeparam name="TResult">返回值类型。</typeparam>
/// <param name="command">命令对象。</param> /// <param name="command">命令对象。</param>
/// <returns>测试桩默认返回值。</returns> /// <returns>此方法始终抛出异常,不返回结果。</returns>
/// <exception cref="NotSupportedException">该测试桩不支持旧版命令执行入口。</exception>
public TResult SendCommand<TResult>(ICommand<TResult> command) public TResult SendCommand<TResult>(ICommand<TResult> command)
{ {
return default!; throw new NotSupportedException();
} }
/// <summary> /// <summary>
/// 异步发送旧版命令。 /// 异步发送旧版命令。
/// </summary> /// </summary>
/// <param name="command">命令对象。</param> /// <param name="command">命令对象。</param>
/// <returns>已完成任务。</returns> /// <returns>已失败的任务。</returns>
public Task SendCommandAsync(IAsyncCommand command) public Task SendCommandAsync(IAsyncCommand command)
{ {
return Task.CompletedTask; return Task.FromException(new NotSupportedException());
} }
/// <summary> /// <summary>
@ -388,10 +406,10 @@ public class TestArchitectureContext : IArchitectureContext
/// </summary> /// </summary>
/// <typeparam name="TResult">返回值类型。</typeparam> /// <typeparam name="TResult">返回值类型。</typeparam>
/// <param name="command">命令对象。</param> /// <param name="command">命令对象。</param>
/// <returns>包含测试桩默认返回值的任务。</returns> /// <returns>已失败的任务。</returns>
public Task<TResult> SendCommandAsync<TResult>(IAsyncCommand<TResult> command) public Task<TResult> SendCommandAsync<TResult>(IAsyncCommand<TResult> command)
{ {
return Task.FromResult(default(TResult)!); return Task.FromException<TResult>(new NotSupportedException());
} }
/// <summary> /// <summary>
@ -399,10 +417,11 @@ public class TestArchitectureContext : IArchitectureContext
/// </summary> /// </summary>
/// <typeparam name="TResult">查询结果类型。</typeparam> /// <typeparam name="TResult">查询结果类型。</typeparam>
/// <param name="query">查询对象。</param> /// <param name="query">查询对象。</param>
/// <returns>测试桩默认返回值。</returns> /// <returns>此方法始终抛出异常,不返回结果。</returns>
/// <exception cref="NotSupportedException">该测试桩不支持旧版查询执行入口。</exception>
public TResult SendQuery<TResult>(IQuery<TResult> query) public TResult SendQuery<TResult>(IQuery<TResult> query)
{ {
return default!; throw new NotSupportedException();
} }
/// <summary> /// <summary>
@ -410,10 +429,10 @@ public class TestArchitectureContext : IArchitectureContext
/// </summary> /// </summary>
/// <typeparam name="TResult">查询结果类型。</typeparam> /// <typeparam name="TResult">查询结果类型。</typeparam>
/// <param name="query">异步查询对象。</param> /// <param name="query">异步查询对象。</param>
/// <returns>包含测试桩默认返回值的任务。</returns> /// <returns>已失败的任务。</returns>
public Task<TResult> SendQueryAsync<TResult>(IAsyncQuery<TResult> query) public Task<TResult> SendQueryAsync<TResult>(IAsyncQuery<TResult> query)
{ {
return Task.FromResult(default(TResult)!); return Task.FromException<TResult>(new NotSupportedException());
} }
/// <summary> /// <summary>

View File

@ -0,0 +1,156 @@
using System;
using System.Threading.Tasks;
using GFramework.Core.Abstractions.Architectures;
using GFramework.Core.Abstractions.Command;
using GFramework.Core.Abstractions.Enums;
using GFramework.Core.Abstractions.Query;
namespace GFramework.Core.Tests.Architectures;
/// <summary>
/// 覆盖测试架构上下文替身的共享事件与显式失败契约。
/// </summary>
[TestFixture]
public class TestArchitectureContextBehaviorTests
{
/// <summary>
/// 验证测试上下文会把事件注册与发送委托到同一个事件总线实例。
/// </summary>
[Test]
public void RegisterEvent_And_SendEvent_Should_Use_Shared_EventBus()
{
var context = new TestArchitectureContext();
var eventReceived = false;
context.RegisterEvent<TestEventV2>(_ => eventReceived = true);
context.SendEvent<TestEventV2>();
Assert.That(eventReceived, Is.True);
}
/// <summary>
/// 验证测试上下文的旧版命令与查询入口会显式抛出未支持异常。
/// </summary>
[Test]
public async Task Legacy_Entries_Should_Throw_Or_Return_Faulted_Tasks()
{
var context = new TestArchitectureContext();
Assert.That(() => context.SendCommand(new TestCommandV2()), Throws.TypeOf<NotSupportedException>());
Assert.That(
() => context.SendCommand(new TestCommandWithResultV2 { Result = 1 }),
Throws.TypeOf<NotSupportedException>());
Assert.That(() => context.SendQuery(new TestQueryV2 { Result = 1 }), Throws.TypeOf<NotSupportedException>());
Assert.That(
async () => await context.SendCommandAsync(new TestAsyncCommand()).ConfigureAwait(false),
Throws.TypeOf<NotSupportedException>());
Assert.That(
async () => await context.SendCommandAsync(new TestAsyncCommandWithResult()).ConfigureAwait(false),
Throws.TypeOf<NotSupportedException>());
Assert.That(
async () => await context.SendQueryAsync(new TestAsyncQuery()).ConfigureAwait(false),
Throws.TypeOf<NotSupportedException>());
}
/// <summary>
/// 验证用于 ArchitectureServices 的上下文替身也会把旧版入口显式标记为不支持。
/// </summary>
[Test]
public async Task Legacy_Entries_On_TestArchitectureContextV3_Should_Throw_Or_Return_Faulted_Tasks()
{
var context = new TestArchitectureContextV3();
Assert.That(() => context.SendCommand(new TestCommandV2()), Throws.TypeOf<NotSupportedException>());
Assert.That(
() => context.SendCommand(new TestCommandWithResultV2 { Result = 1 }),
Throws.TypeOf<NotSupportedException>());
Assert.That(() => context.SendQuery(new TestQueryV2 { Result = 1 }), Throws.TypeOf<NotSupportedException>());
Assert.That(
async () => await context.SendCommandAsync(new TestAsyncCommand()).ConfigureAwait(false),
Throws.TypeOf<NotSupportedException>());
Assert.That(
async () => await context.SendCommandAsync(new TestAsyncCommandWithResult()).ConfigureAwait(false),
Throws.TypeOf<NotSupportedException>());
Assert.That(
async () => await context.SendQueryAsync(new TestAsyncQuery()).ConfigureAwait(false),
Throws.TypeOf<NotSupportedException>());
}
/// <summary>
/// 验证两类架构测试替身在接口视角下都会以 no-op 方式接受生命周期钩子。
/// </summary>
[Test]
public void RegisterLifecycleHook_Via_Interface_Should_Return_Original_Hook()
{
IArchitecture withRegistry = new TestArchitectureWithRegistry(new TestRegistry());
IArchitecture withoutRegistry = new TestArchitectureWithoutRegistry();
var hook = new NoOpLifecycleHook();
Assert.That(withRegistry.RegisterLifecycleHook(hook), Is.SameAs(hook));
Assert.That(withoutRegistry.RegisterLifecycleHook(hook), Is.SameAs(hook));
}
/// <summary>
/// 为旧版异步命令入口提供最小实现的测试命令。
/// </summary>
private sealed class TestAsyncCommand : IAsyncCommand
{
public Task ExecuteAsync()
{
return Task.CompletedTask;
}
public IArchitectureContext GetContext()
{
throw new NotSupportedException();
}
public void SetContext(IArchitectureContext context)
{
ArgumentNullException.ThrowIfNull(context);
}
}
/// <summary>
/// 为旧版异步命令入口提供最小实现的带结果测试命令。
/// </summary>
private sealed class TestAsyncCommandWithResult : IAsyncCommand<int>
{
public Task<int> ExecuteAsync()
{
return Task.FromResult(1);
}
public IArchitectureContext GetContext()
{
throw new NotSupportedException();
}
public void SetContext(IArchitectureContext context)
{
ArgumentNullException.ThrowIfNull(context);
}
}
/// <summary>
/// 为旧版异步查询入口提供最小实现的测试查询。
/// </summary>
private sealed class TestAsyncQuery : IAsyncQuery<int>
{
public Task<int> DoAsync()
{
return Task.FromResult(1);
}
}
/// <summary>
/// 为生命周期钩子接口提供空实现的测试替身。
/// </summary>
private sealed class NoOpLifecycleHook : IArchitectureLifecycleHook
{
public void OnPhase(ArchitecturePhase phase, IArchitecture architecture)
{
ArgumentNullException.ThrowIfNull(architecture);
}
}
}

View File

@ -343,8 +343,10 @@ public class TestArchitectureContextV3 : IArchitectureContext
/// 发送旧版无返回值命令。 /// 发送旧版无返回值命令。
/// </summary> /// </summary>
/// <param name="command">要发送的命令。</param> /// <param name="command">要发送的命令。</param>
/// <exception cref="NotSupportedException">该测试桩不支持旧版命令执行入口。</exception>
public void SendCommand(ICommand command) public void SendCommand(ICommand command)
{ {
throw new NotSupportedException();
} }
/// <summary> /// <summary>
@ -352,20 +354,21 @@ public class TestArchitectureContextV3 : IArchitectureContext
/// </summary> /// </summary>
/// <typeparam name="TResult">命令响应类型。</typeparam> /// <typeparam name="TResult">命令响应类型。</typeparam>
/// <param name="command">要发送的命令。</param> /// <param name="command">要发送的命令。</param>
/// <returns>默认响应值。</returns> /// <returns>此方法始终抛出异常,不返回结果。</returns>
/// <exception cref="NotSupportedException">该测试桩不支持旧版命令执行入口。</exception>
public TResult SendCommand<TResult>(GFramework.Core.Abstractions.Command.ICommand<TResult> command) public TResult SendCommand<TResult>(GFramework.Core.Abstractions.Command.ICommand<TResult> command)
{ {
return default!; throw new NotSupportedException();
} }
/// <summary> /// <summary>
/// 异步发送旧版无返回值命令。 /// 异步发送旧版无返回值命令。
/// </summary> /// </summary>
/// <param name="command">要发送的命令。</param> /// <param name="command">要发送的命令。</param>
/// <returns>已完成任务。</returns> /// <returns>已失败的任务。</returns>
public Task SendCommandAsync(IAsyncCommand command) public Task SendCommandAsync(IAsyncCommand command)
{ {
return Task.CompletedTask; return Task.FromException(new NotSupportedException());
} }
/// <summary> /// <summary>
@ -373,10 +376,10 @@ public class TestArchitectureContextV3 : IArchitectureContext
/// </summary> /// </summary>
/// <typeparam name="TResult">命令响应类型。</typeparam> /// <typeparam name="TResult">命令响应类型。</typeparam>
/// <param name="command">要发送的命令。</param> /// <param name="command">要发送的命令。</param>
/// <returns>占位任务。</returns> /// <returns>已失败的任务。</returns>
public Task<TResult> SendCommandAsync<TResult>(IAsyncCommand<TResult> command) public Task<TResult> SendCommandAsync<TResult>(IAsyncCommand<TResult> command)
{ {
return (Task<TResult>)Task.CompletedTask; return Task.FromException<TResult>(new NotSupportedException());
} }
/// <summary> /// <summary>
@ -384,10 +387,11 @@ public class TestArchitectureContextV3 : IArchitectureContext
/// </summary> /// </summary>
/// <typeparam name="TResult">查询结果类型。</typeparam> /// <typeparam name="TResult">查询结果类型。</typeparam>
/// <param name="query">要发送的查询。</param> /// <param name="query">要发送的查询。</param>
/// <returns>默认查询结果。</returns> /// <returns>此方法始终抛出异常,不返回结果。</returns>
/// <exception cref="NotSupportedException">该测试桩不支持旧版查询执行入口。</exception>
public TResult SendQuery<TResult>(GFramework.Core.Abstractions.Query.IQuery<TResult> query) public TResult SendQuery<TResult>(GFramework.Core.Abstractions.Query.IQuery<TResult> query)
{ {
return default!; throw new NotSupportedException();
} }
/// <summary> /// <summary>
@ -395,10 +399,10 @@ public class TestArchitectureContextV3 : IArchitectureContext
/// </summary> /// </summary>
/// <typeparam name="TResult">查询结果类型。</typeparam> /// <typeparam name="TResult">查询结果类型。</typeparam>
/// <param name="query">要发送的查询。</param> /// <param name="query">要发送的查询。</param>
/// <returns>占位任务。</returns> /// <returns>已失败的任务。</returns>
public Task<TResult> SendQueryAsync<TResult>(IAsyncQuery<TResult> query) public Task<TResult> SendQueryAsync<TResult>(IAsyncQuery<TResult> query)
{ {
return (Task<TResult>)Task.CompletedTask; return Task.FromException<TResult>(new NotSupportedException());
} }
/// <summary> /// <summary>

View File

@ -95,7 +95,8 @@ public class TestArchitectureWithRegistry : IArchitecture
IArchitectureLifecycleHook IArchitecture.RegisterLifecycleHook(IArchitectureLifecycleHook hook) IArchitectureLifecycleHook IArchitecture.RegisterLifecycleHook(IArchitectureLifecycleHook hook)
{ {
throw new NotSupportedException(); RegisterLifecycleHook(hook);
return hook;
} }
Task IArchitecture.WaitUntilReadyAsync() Task IArchitecture.WaitUntilReadyAsync()

View File

@ -93,7 +93,8 @@ public class TestArchitectureWithoutRegistry : IArchitecture
IArchitectureLifecycleHook IArchitecture.RegisterLifecycleHook(IArchitectureLifecycleHook hook) IArchitectureLifecycleHook IArchitecture.RegisterLifecycleHook(IArchitectureLifecycleHook hook)
{ {
throw new NotSupportedException(); RegisterLifecycleHook(hook);
return hook;
} }
/// <summary> /// <summary>

View File

@ -1,3 +1,4 @@
using System;
using GFramework.Core.Abstractions.Events; using GFramework.Core.Abstractions.Events;
using GFramework.Core.Coroutine.Instructions; using GFramework.Core.Coroutine.Instructions;
using GFramework.Core.Events; using GFramework.Core.Events;
@ -8,25 +9,28 @@ namespace GFramework.Core.Tests.Coroutine
[TestFixture] [TestFixture]
public class WaitForMultipleEventsTests public class WaitForMultipleEventsTests
{ {
private IEventBus? _eventBus;
private IEventBus EventBus => _eventBus ?? throw new InvalidOperationException("EventBus has not been initialized.");
[SetUp] [SetUp]
public void SetUp() public void SetUp()
{ {
eventBus = new EventBus(); _eventBus = new EventBus();
} }
[TearDown] [TearDown]
public void TearDown() public void TearDown()
{ {
(eventBus as IDisposable)?.Dispose(); (EventBus as IDisposable)?.Dispose();
_eventBus = null;
} }
private IEventBus eventBus = null!;
[Test] [Test]
public void Constructor_RegistersBothEventTypes() public void Constructor_RegistersBothEventTypes()
{ {
// Arrange & Act // Arrange & Act
var waitForMultipleEvents = new WaitForMultipleEvents<TestEvent1, TestEvent2>(eventBus); var waitForMultipleEvents = new WaitForMultipleEvents<TestEvent1, TestEvent2>(EventBus);
// Assert // Assert
Assert.That(waitForMultipleEvents.IsDone, Is.False); Assert.That(waitForMultipleEvents.IsDone, Is.False);
@ -37,11 +41,11 @@ namespace GFramework.Core.Tests.Coroutine
public async Task FirstEventWins_WhenBothEventsFired() public async Task FirstEventWins_WhenBothEventsFired()
{ {
// Arrange // Arrange
var waitForMultipleEvents = new WaitForMultipleEvents<TestEvent1, TestEvent2>(eventBus); var waitForMultipleEvents = new WaitForMultipleEvents<TestEvent1, TestEvent2>(EventBus);
// Act // Act
eventBus.Send(new TestEvent1 { Data = "first_event" }); EventBus.Send(new TestEvent1 { Data = "first_event" });
eventBus.Send(new TestEvent2 { Data = "second_event" }); EventBus.Send(new TestEvent2 { Data = "second_event" });
// Assert // Assert
Assert.That(waitForMultipleEvents.IsDone, Is.True); Assert.That(waitForMultipleEvents.IsDone, Is.True);
@ -54,10 +58,10 @@ namespace GFramework.Core.Tests.Coroutine
public async Task SecondEventWins_WhenOnlySecondEventFired() public async Task SecondEventWins_WhenOnlySecondEventFired()
{ {
// Arrange // Arrange
var waitForMultipleEvents = new WaitForMultipleEvents<TestEvent1, TestEvent2>(eventBus); var waitForMultipleEvents = new WaitForMultipleEvents<TestEvent1, TestEvent2>(EventBus);
// Act // Act
eventBus.Send(new TestEvent2 { Data = "second_event" }); EventBus.Send(new TestEvent2 { Data = "second_event" });
// Assert // Assert
Assert.That(waitForMultipleEvents.IsDone, Is.True); Assert.That(waitForMultipleEvents.IsDone, Is.True);
@ -70,11 +74,11 @@ namespace GFramework.Core.Tests.Coroutine
public async Task FirstEventWins_WhenBothEventsFiredInReverseOrder() public async Task FirstEventWins_WhenBothEventsFiredInReverseOrder()
{ {
// Arrange // Arrange
var waitForMultipleEvents = new WaitForMultipleEvents<TestEvent1, TestEvent2>(eventBus); var waitForMultipleEvents = new WaitForMultipleEvents<TestEvent1, TestEvent2>(EventBus);
// Act // Act
eventBus.Send(new TestEvent2 { Data = "second_event" }); EventBus.Send(new TestEvent2 { Data = "second_event" });
eventBus.Send(new TestEvent1 { Data = "first_event" }); EventBus.Send(new TestEvent1 { Data = "first_event" });
// Assert // Assert
Assert.That(waitForMultipleEvents.IsDone, Is.True); Assert.That(waitForMultipleEvents.IsDone, Is.True);
@ -88,10 +92,10 @@ namespace GFramework.Core.Tests.Coroutine
public async Task MultipleEvents_AfterCompletion_DoNotOverrideState() public async Task MultipleEvents_AfterCompletion_DoNotOverrideState()
{ {
// Arrange // Arrange
var waitForMultipleEvents = new WaitForMultipleEvents<TestEvent1, TestEvent2>(eventBus); var waitForMultipleEvents = new WaitForMultipleEvents<TestEvent1, TestEvent2>(EventBus);
// Act - Fire first event // Act - Fire first event
eventBus.Send(new TestEvent1 { Data = "first_event" }); EventBus.Send(new TestEvent1 { Data = "first_event" });
// Verify first event was processed // Verify first event was processed
Assert.That(waitForMultipleEvents.IsDone, Is.True); Assert.That(waitForMultipleEvents.IsDone, Is.True);
@ -99,7 +103,7 @@ namespace GFramework.Core.Tests.Coroutine
Assert.That(waitForMultipleEvents.FirstEventData?.Data, Is.EqualTo("first_event")); Assert.That(waitForMultipleEvents.FirstEventData?.Data, Is.EqualTo("first_event"));
// Fire second event after completion // Fire second event after completion
eventBus.Send(new TestEvent2 { Data = "second_event" }); EventBus.Send(new TestEvent2 { Data = "second_event" });
// Assert - The state should not change // Assert - The state should not change
Assert.That(waitForMultipleEvents.IsDone, Is.True); Assert.That(waitForMultipleEvents.IsDone, Is.True);
@ -113,13 +117,13 @@ namespace GFramework.Core.Tests.Coroutine
public async Task Disposal_PreventsFurtherEventHandling() public async Task Disposal_PreventsFurtherEventHandling()
{ {
// Arrange // Arrange
var waitForMultipleEvents = new WaitForMultipleEvents<TestEvent1, TestEvent2>(eventBus); var waitForMultipleEvents = new WaitForMultipleEvents<TestEvent1, TestEvent2>(EventBus);
// Act - Dispose the instance // Act - Dispose the instance
waitForMultipleEvents.Dispose(); waitForMultipleEvents.Dispose();
// Fire an event after disposal // Fire an event after disposal
eventBus.Send(new TestEvent1 { Data = "after_disposal" }); EventBus.Send(new TestEvent1 { Data = "after_disposal" });
// Assert - Event should not be processed due to disposal // Assert - Event should not be processed due to disposal
// Since we disposed, no event data should be captured // Since we disposed, no event data should be captured

View File

@ -1,3 +1,6 @@
using System;
using System.Collections.Generic;
using System.Threading;
using GFramework.Core.Abstractions.Logging; using GFramework.Core.Abstractions.Logging;
using GFramework.Core.Logging; using GFramework.Core.Logging;
@ -6,9 +9,13 @@ namespace GFramework.Core.Tests.Logging;
/// <summary> /// <summary>
/// 表示供日志相关测试复用的内存日志记录器。 /// 表示供日志相关测试复用的内存日志记录器。
/// </summary> /// </summary>
/// <remarks>
/// 并发写入会通过内部锁串行化;<see cref="Logs" /> 每次返回快照,避免断言观察到正在被修改的可变集合。
/// </remarks>
public sealed class TestLogger : AbstractLogger public sealed class TestLogger : AbstractLogger
{ {
private readonly List<LogEntry> _logs = new(); private readonly List<LogEntry> _logs = new();
private readonly Lock _sync = new();
/// <summary> /// <summary>
/// 初始化 <see cref="TestLogger" /> 的新实例。 /// 初始化 <see cref="TestLogger" /> 的新实例。
@ -20,9 +27,18 @@ public sealed class TestLogger : AbstractLogger
} }
/// <summary> /// <summary>
/// 获取按写入顺序保存的日志条目只读视图 /// 获取按写入顺序保存的日志条目快照
/// </summary> /// </summary>
public IReadOnlyList<LogEntry> Logs => _logs; public IReadOnlyList<LogEntry> Logs
{
get
{
lock (_sync)
{
return _logs.ToArray();
}
}
}
/// <summary> /// <summary>
/// 将日志信息追加到内存列表,供断言读取。 /// 将日志信息追加到内存列表,供断言读取。
@ -32,7 +48,10 @@ public sealed class TestLogger : AbstractLogger
/// <param name="exception">相关异常;没有异常时为 <see langword="null" />。</param> /// <param name="exception">相关异常;没有异常时为 <see langword="null" />。</param>
protected override void Write(LogLevel level, string message, Exception? exception) protected override void Write(LogLevel level, string message, Exception? exception)
{ {
_logs.Add(new LogEntry(level, message, exception)); lock (_sync)
{
_logs.Add(new LogEntry(level, message, exception));
}
} }
/// <summary> /// <summary>

View File

@ -1,18 +1,30 @@
using System;
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Threading.Tasks;
using GFramework.Core.Abstractions.Resource; using GFramework.Core.Abstractions.Resource;
namespace GFramework.Core.Tests.Resource; namespace GFramework.Core.Tests.Resource;
/// <summary> /// <summary>
/// 为 ResourceManager 测试提供可控数据源的资源加载器。 /// 为 ResourceManager 测试提供可控数据源的资源加载器。
/// </summary> /// </summary>
public class TestResourceLoader : IResourceLoader<TestResource> public class TestResourceLoader : IResourceLoader<TestResource>
{ {
private readonly Dictionary<string, string> _resourceData = new(StringComparer.Ordinal); private readonly Dictionary<string, string> _resourceData = new(StringComparer.Ordinal);
/// <inheritdoc /> /// <summary>
/// 同步加载指定路径的测试资源。
/// </summary>
/// <param name="path">资源路径。</param>
/// <returns>加载得到的测试资源。</returns>
/// <exception cref="ArgumentNullException"><paramref name="path" /> 为 <see langword="null" />。</exception>
/// <exception cref="ArgumentException"><paramref name="path" /> 为空字符串。</exception>
/// <exception cref="FileNotFoundException">指定路径的测试资源不存在。</exception>
public TestResource Load(string path) public TestResource Load(string path)
{ {
ArgumentException.ThrowIfNullOrEmpty(path);
if (_resourceData.TryGetValue(path, out var content)) if (_resourceData.TryGetValue(path, out var content))
{ {
return new TestResource { Content = content }; return new TestResource { Content = content };
@ -21,22 +33,40 @@ public class TestResourceLoader : IResourceLoader<TestResource>
throw new FileNotFoundException($"Resource not found: {path}"); throw new FileNotFoundException($"Resource not found: {path}");
} }
/// <inheritdoc /> /// <summary>
public async Task<TestResource> LoadAsync(string path) /// 异步加载指定路径的测试资源。
/// </summary>
/// <param name="path">资源路径。</param>
/// <returns>加载得到的测试资源任务。</returns>
/// <exception cref="ArgumentNullException"><paramref name="path" /> 为 <see langword="null" />。</exception>
/// <exception cref="ArgumentException"><paramref name="path" /> 为空字符串。</exception>
/// <exception cref="FileNotFoundException">指定路径的测试资源不存在。</exception>
public Task<TestResource> LoadAsync(string path)
{ {
await Task.Delay(10).ConfigureAwait(false); // 模拟异步加载 return Task.FromResult(Load(path));
return Load(path);
} }
/// <inheritdoc /> /// <summary>
/// 卸载已加载的测试资源。
/// </summary>
/// <param name="resource">要标记为已释放的资源。</param>
/// <exception cref="ArgumentNullException"><paramref name="resource" /> 为 <see langword="null" />。</exception>
public void Unload(TestResource resource) public void Unload(TestResource resource)
{ {
ArgumentNullException.ThrowIfNull(resource);
resource.IsDisposed = true; resource.IsDisposed = true;
} }
/// <inheritdoc /> /// <summary>
/// 判断当前加载器是否包含指定路径的测试资源。
/// </summary>
/// <param name="path">资源路径。</param>
/// <returns>存在对应测试资源时返回 <see langword="true" />;否则返回 <see langword="false" />。</returns>
/// <exception cref="ArgumentNullException"><paramref name="path" /> 为 <see langword="null" />。</exception>
/// <exception cref="ArgumentException"><paramref name="path" /> 为空字符串。</exception>
public bool CanLoad(string path) public bool CanLoad(string path)
{ {
ArgumentException.ThrowIfNullOrEmpty(path);
return _resourceData.ContainsKey(path); return _resourceData.ContainsKey(path);
} }
@ -45,8 +75,14 @@ public class TestResourceLoader : IResourceLoader<TestResource>
/// </summary> /// </summary>
/// <param name="path">资源路径。</param> /// <param name="path">资源路径。</param>
/// <param name="content">资源内容。</param> /// <param name="content">资源内容。</param>
/// <exception cref="ArgumentNullException">
/// <paramref name="path" /> 或 <paramref name="content" /> 为 <see langword="null" />。
/// </exception>
/// <exception cref="ArgumentException"><paramref name="path" /> 为空字符串。</exception>
public void AddTestData(string path, string content) public void AddTestData(string path, string content)
{ {
ArgumentException.ThrowIfNullOrEmpty(path);
ArgumentNullException.ThrowIfNull(content);
_resourceData[path] = content; _resourceData[path] = content;
} }
} }

View File

@ -1,3 +1,4 @@
using System;
using GFramework.Core.Abstractions.Architectures; using GFramework.Core.Abstractions.Architectures;
using GFramework.Core.Abstractions.Enums; using GFramework.Core.Abstractions.Enums;
using GFramework.Core.Abstractions.Utility; using GFramework.Core.Abstractions.Utility;
@ -33,12 +34,29 @@ public abstract class RegistryInitializationHookBase<TRegistry, TConfig> : IArch
/// </summary> /// </summary>
/// <param name="phase">当前的架构阶段</param> /// <param name="phase">当前的架构阶段</param>
/// <param name="architecture">相关的架构实例</param> /// <param name="architecture">相关的架构实例</param>
/// <remarks>
/// 当目标注册表未被装入当前架构上下文时,该钩子会保持 no-op
/// 以便同一组配置可以安全复用于不包含该注册表的测试或裁剪场景。
/// </remarks>
public void OnPhase(ArchitecturePhase phase, IArchitecture architecture) public void OnPhase(ArchitecturePhase phase, IArchitecture architecture)
{ {
if (phase != _targetPhase) return; ArgumentNullException.ThrowIfNull(architecture);
var registry = architecture.Context.GetUtility<TRegistry>(); if (phase != _targetPhase)
if (registry == null) return; {
return;
}
TRegistry registry;
try
{
registry = architecture.Context.GetUtility<TRegistry>();
}
catch (InvalidOperationException)
{
return;
}
foreach (var config in _configs) foreach (var config in _configs)
{ {
@ -52,4 +70,4 @@ public abstract class RegistryInitializationHookBase<TRegistry, TConfig> : IArch
/// <param name="registry">注册表实例</param> /// <param name="registry">注册表实例</param>
/// <param name="config">配置项</param> /// <param name="config">配置项</param>
protected abstract void RegisterConfig(TRegistry registry, TConfig config); protected abstract void RegisterConfig(TRegistry registry, TConfig config);
} }

View File

@ -1,4 +1,5 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading;
using GFramework.Core.Abstractions.Logging; using GFramework.Core.Abstractions.Logging;
using GFramework.Core.Logging; using GFramework.Core.Logging;
using GFramework.Cqrs.Tests.Logging; using GFramework.Cqrs.Tests.Logging;
@ -11,10 +12,12 @@ namespace GFramework.Cqrs.Tests.Cqrs;
/// <remarks> /// <remarks>
/// 处理器注册入口会分别为测试运行时、容器和注册器创建日志器。 /// 处理器注册入口会分别为测试运行时、容器和注册器创建日志器。
/// 该提供程序统一保留这些测试日志器,以便断言警告是否经由公开入口真正发出。 /// 该提供程序统一保留这些测试日志器,以便断言警告是否经由公开入口真正发出。
/// 并发创建日志器时会通过内部锁串行化,<see cref="Loggers" /> 每次返回快照,避免调用方观察到可变集合。
/// </remarks> /// </remarks>
internal sealed class CapturingLoggerFactoryProvider : ILoggerFactoryProvider internal sealed class CapturingLoggerFactoryProvider : ILoggerFactoryProvider
{ {
private readonly List<TestLogger> _loggers = []; private readonly List<TestLogger> _loggers = [];
private readonly Lock _sync = new();
/// <summary> /// <summary>
/// 使用指定的最小日志级别初始化一个新的捕获型日志工厂提供程序。 /// 使用指定的最小日志级别初始化一个新的捕获型日志工厂提供程序。
@ -26,9 +29,18 @@ internal sealed class CapturingLoggerFactoryProvider : ILoggerFactoryProvider
} }
/// <summary> /// <summary>
/// 获取通过当前提供程序创建的全部测试日志器 /// 获取通过当前提供程序创建的全部测试日志器快照
/// </summary> /// </summary>
public IReadOnlyList<TestLogger> Loggers => _loggers; public IReadOnlyList<TestLogger> Loggers
{
get
{
lock (_sync)
{
return _loggers.ToArray();
}
}
}
/// <summary> /// <summary>
/// 获取或设置新建测试日志器的最小日志级别。 /// 获取或设置新建测试日志器的最小日志级别。
@ -43,7 +55,12 @@ internal sealed class CapturingLoggerFactoryProvider : ILoggerFactoryProvider
public ILogger CreateLogger(string name) public ILogger CreateLogger(string name)
{ {
var logger = new TestLogger(name, MinLevel); var logger = new TestLogger(name, MinLevel);
_loggers.Add(logger);
lock (_sync)
{
_loggers.Add(logger);
}
return logger; return logger;
} }
} }

View File

@ -10,11 +10,18 @@ internal static class DeterministicNotificationHandlerState
/// <summary> /// <summary>
/// 获取当前测试中的通知处理器执行顺序。 /// 获取当前测试中的通知处理器执行顺序。
/// </summary> /// </summary>
/// <remarks>
/// 该集合仅供顺序测试断言使用,不提供并发安全保证。
/// 若多个处理器在并行测试中同时写入,调用方可能观察到竞争条件或未定义顺序。
/// </remarks>
public static List<string> InvocationOrder { get; } = []; public static List<string> InvocationOrder { get; } = [];
/// <summary> /// <summary>
/// 重置共享的执行顺序状态。 /// 重置共享的执行顺序状态。
/// </summary> /// </summary>
/// <remarks>
/// 该方法只支持在单线程测试准备阶段调用;并发调用会与 <see cref="InvocationOrder" /> 的直接写入互相竞争。
/// </remarks>
public static void Reset() public static void Reset()
{ {
InvocationOrder.Clear(); InvocationOrder.Clear();

View File

@ -16,6 +16,9 @@ internal sealed class PartialGeneratedNotificationHandlerRegistry : ICqrsHandler
/// </summary> /// </summary>
/// <param name="services">承载处理器映射的服务集合。</param> /// <param name="services">承载处理器映射的服务集合。</param>
/// <param name="logger">用于记录注册诊断的日志器。</param> /// <param name="logger">用于记录注册诊断的日志器。</param>
/// <exception cref="ArgumentNullException">
/// <paramref name="services" /> 或 <paramref name="logger" /> 为 <see langword="null" />。
/// </exception>
public void Register(IServiceCollection services, ILogger logger) public void Register(IServiceCollection services, ILogger logger)
{ {
ArgumentNullException.ThrowIfNull(services); ArgumentNullException.ThrowIfNull(services);

View File

@ -6,41 +6,38 @@
## 当前恢复点 ## 当前恢复点
- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-087` - 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-088`
- 当前阶段:`Phase 87` - 当前阶段:`Phase 88`
- 当前焦点: - 当前焦点:
- `2026-04-28` 已按 `$gframework-batch-boot 50` 先执行仓库根 `dotnet clean` + `dotnet build`,建立本轮权威基线 `288 Warning(s)` / `214` 个唯一位点 - `2026-04-28` 已执行 `$gframework-pr-review`,确认 `PR #300` 最新 head 上仍有 `8` 条 CodeRabbit open threads、`1` 个 failed test以及 `dotnet-format restore failed` 的 CI 噪音
- 本轮已并行收敛 `GameContextTests.cs``ArchitectureServicesTests.cs``RegistryInitializationHookBaseTests.cs``CqrsDispatcherCacheTests.cs``CqrsHandlerRegistrarTests.cs` - 本轮已核对并收敛仍然成立的 review comments`TestArchitectureContext*` 旧入口显式失败、共享事件总线、`RegisterLifecycleHook` 语义统一、`TestResourceLoader` 契约、`TestLogger` / `CapturingLoggerFactoryProvider` 快照访问、`DeterministicNotificationHandlerState` 并发说明与 `PartialGeneratedNotificationHandlerRegistry` XML 异常文档
- 主线程已补齐 `ResourceManagerTests.cs``TestEvent.cs``LoggerTests.cs``ContextProviderTests.cs``TestArchitectureBase.cs``CommandCoroutineExtensionsTests.cs``Core.Tests` 零散 warning - 已新增 `TestArchitectureContextBehaviorTests.cs`,直接覆盖共享事件总线、旧入口失败契约与接口视角生命周期钩子行为
- 当前 `GFramework.Core.Tests``GFramework.Cqrs.Tests` 的受影响项目 Release 构建都已恢复到 `0 Warning(s)` / `0 Error(s)` - `RegistryInitializationHookBase` 现已在注册表缺失时保持 no-op修复了 PR 上报的失败测试 `OnPhase_Should_Not_Throw_When_Registry_Not_Found`
- 当前仓库根权威基线已从本轮开始时的 `288 Warning(s)` / `214` 个唯一位点下降到 `236 Warning(s)` / `162` 个唯一位点;剩余 warning 只集中在 `Mediator/*``YamlConfigSchemaValidator*`
## 当前活跃事实 ## 当前活跃事实
- 当前 `origin/main` 基线提交为 `6cc87a9``2026-04-27T20:28:50+08:00`)。 - 当前 `origin/main` 基线提交为 `6cc87a9``2026-04-27T20:28:50+08:00`)。
- 当前直接验证结果: - 当前直接验证结果:
- `dotnet clean`
- 最新结果:成功;已刷新本轮 final non-incremental 仓库根基线
- `dotnet build`
- 最新结果:成功;`236 Warning(s)``0 Error(s)`,唯一位点 `162`
- `dotnet build GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release` - `dotnet build GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release`
- 最新结果:成功;`0 Warning(s)``0 Error(s)` - 最新结果:成功;`0 Warning(s)``0 Error(s)`
- `dotnet build GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release` - `dotnet build GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release`
- 最新结果:成功;`125 Warning(s)``0 Error(s)`warning 仍集中在既有 `Mediator/*` 文件,不在本轮 PR review 修复写集内
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release`
- 最新结果:成功;`0 Warning(s)``0 Error(s)` - 最新结果:成功;`0 Warning(s)``0 Error(s)`
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --no-build --filter "FullyQualifiedName~RegistryInitializationHookBaseTests|FullyQualifiedName~WaitForMultipleEventsTests|FullyQualifiedName~ResourceManagerTests|FullyQualifiedName~LoggerTests|FullyQualifiedName~TestArchitectureContextBehaviorTests"`
- 最新结果:成功;`97` 通过、`0` 失败
- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --no-build --filter "FullyQualifiedName~CqrsHandlerRegistrarTests"`
- 最新结果:成功;`11` 通过、`0` 失败
- 当前批次摘要: - 当前批次摘要:
- 本轮接受并集成 `GameContextTests.cs``ArchitectureServicesTests.cs``RegistryInitializationHookBaseTests.cs``CqrsDispatcherCacheTests.cs``CqrsHandlerRegistrarTests.cs` 五个并行 worker 切片 - 当前工作树包含 `11` 个已修改文件和 `1` 个新增测试文件,全部来自 `Core` / `Core.Tests` / `Cqrs.Tests` 的 PR review follow-up
- 主线程补齐 `Core.Tests` 内剩余零散 warning使 `GFramework.Core.Tests` 项目级 Release 构建回到 `0 Warning(s)` / `0 Error(s)` - 本轮没有触碰 `Mediator/*``YamlConfigSchemaValidator*` 的高耦合 warning 波次
- 当前 `origin/main...HEAD` 已提交 branch diff 仍为 `21` 个文件;计入当前待提交工作树后的并集 footprint 为 `45 / 50` 个文件,已接近本轮停止线
- 当前建议保留到下一波次的候选:
- `GFramework.Cqrs.Tests/Mediator/MediatorArchitectureIntegrationTests.cs``MediatorComprehensiveTests.cs``MediatorAdvancedFeaturesTests.cs` 的高密度 `MA0048` / `MA0004`
- `GFramework.Game/Config/YamlConfigSchemaValidator.cs``YamlConfigSchemaValidator.ObjectKeywords.cs` 的高耦合 warning 热点
## 当前风险 ## 当前风险
- `GFramework.Cqrs.Tests/Mediator/*` 仍有 `94` / `88` / `68` 条输出 warning属于高 changed-file 风险的 `MA0048` 大波次 - `GFramework.Cqrs.Tests` 当前项目级 Release 构建仍有 `125` 条既有 warning主要集中在 `MediatorArchitectureIntegrationTests.cs``MediatorAdvancedFeaturesTests.cs``MediatorComprehensiveTests.cs`
- 缓解措施:当前 footprint 已到 `45 / 50`,下一轮应在新提交基础上单独规划 `Mediator*` 波次,而不是继续叠在本轮工作树上 - 缓解措施:本轮仅记录为现存 blocker不在 PR #300 的 review follow-up 里扩展到 `Mediator/*` warning reduction 波次
- `YamlConfigSchemaValidator*` 仍然聚集 `222` 条输出 warning且同时混有 `MA0048``MA0009``MA0051``MA0006` - `GFramework.Game/Config/YamlConfigSchemaValidator*` 仍然是仓库根 warning 热点,但与本轮 review 修复无交集
- 缓解措施:保持为独立高耦合波次,不与测试项目拆分混提 - 缓解措施:继续保持为独立高耦合波次。
## 活跃文档 ## 活跃文档
@ -60,11 +57,12 @@
## 验证说明 ## 验证说明
- 权威验证结果统一维护在“当前活跃事实”。 - 权威验证结果统一维护在“当前活跃事实”。
- `GFramework.Core.Tests``GFramework.Cqrs.Tests` 的当前受影响项目 Release 构建都已在本轮清零,但仓库根 non-incremental 构建仍保留 `Mediator/*``YamlConfigSchemaValidator*` 既有 warning。 - `GFramework.Core``GFramework.Core.Tests` 的当前受影响项目 Release 构建都已清零,并通过对应定向测试回归。
- `GFramework.Cqrs.Tests` 的本轮 helper 改动已由 `CqrsHandlerRegistrarTests` 回归覆盖,但项目级 Release 构建仍暴露 `Mediator/*` 的既有 warning。
- warning reduction 的仓库级真值只以同轮 `dotnet clean` 后的 `dotnet build` 为准。 - warning reduction 的仓库级真值只以同轮 `dotnet clean` 后的 `dotnet build` 为准。
## 下一步建议 ## 下一步建议
1. 提交本轮 `Core.Tests` / `Cqrs.Tests` warning reduction`ai-plan` 同步。 1. 提交本轮 `PR #300` review follow-up`ai-plan` 同步。
2. 下一轮在新提交基础上单独规划 `Mediator/*` 波次,避免在 `45 / 50` footprint 状态继续扩批 2. 若继续处理 `GFramework.Cqrs.Tests` warning下一轮单独切到 `Mediator/*` 波次,并先接受当前 `125` 条 warning 作为显式基线
3. `YamlConfigSchemaValidator*` 保持为独立高耦合波次,必要时先由主线程局部切分再决定是否并行 3. `YamlConfigSchemaValidator*` 继续保持为独立高耦合波次,不与 `Mediator/*` 混提

View File

@ -1,5 +1,37 @@
# Analyzer Warning Reduction 追踪 # Analyzer Warning Reduction 追踪
## 2026-04-28 — RP-088
### 阶段:收敛 PR #300 的 open review threads 与 failed-test follow-up
- 触发背景:
- 用户执行 `$gframework-pr-review`,要求以当前分支 PR 真值为准核对 AI review / failed-test / linter 信号
- `fetch_current_pr_review.py --json-output /tmp/current-pr-review.json` 返回 `PR #300`latest head 上仍有 `8` 条 CodeRabbit open threads、`1` 个失败测试 `RegistryInitializationHookBaseTests.OnPhase_Should_Not_Throw_When_Registry_Not_Found`,以及 `dotnet-format` restore failed 的 CI 噪音
- 主线程实施:
- 核对 `TestArchitectureContext` / `TestArchitectureContextV3` 后,修复共享事件总线与旧版命令/查询入口的静默成功问题,统一改为显式 `NotSupportedException` 或 faulted task
- 校正 `TestArchitectureWithRegistry` / `TestArchitectureWithoutRegistry` 的显式接口 `RegisterLifecycleHook`,使接口视角与公开 no-op 语义一致
- 修复 `RegistryInitializationHookBase` 在注册表缺失场景下的 no-op 行为,并保持有注册表路径继续使用单实例 `GetUtility<TRegistry>()`
- 收敛 `TestResourceLoader``TestLogger``CapturingLoggerFactoryProvider``DeterministicNotificationHandlerState``PartialGeneratedNotificationHandlerRegistry` 的判空、XML 文档、快照访问与并发语义说明
- 新增 `TestArchitectureContextBehaviorTests.cs`,覆盖共享事件总线、旧入口失败契约与 `RegisterLifecycleHook` 接口行为
- 验证里程碑:
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release`
- 结果:成功;`0 Warning(s)``0 Error(s)`
- `dotnet build GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release`
- 结果:成功;`0 Warning(s)``0 Error(s)`
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --no-build --filter "FullyQualifiedName~RegistryInitializationHookBaseTests|FullyQualifiedName~WaitForMultipleEventsTests|FullyQualifiedName~ResourceManagerTests|FullyQualifiedName~LoggerTests|FullyQualifiedName~TestArchitectureContextBehaviorTests"`
- 结果:成功;`97` 通过、`0` 失败
- `dotnet build GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release`
- 结果:成功;`125 Warning(s)``0 Error(s)`warning 全部来自既有 `Mediator/*` 文件,当前 helper 改动未新增 warning
- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --no-build --filter "FullyQualifiedName~CqrsHandlerRegistrarTests"`
- 结果:成功;`11` 通过、`0` 失败
- 当前结论:
- `PR #300` 当前仍然成立的 open review threads 已在本地收敛,且 failed-test 信号已被现有测试回归覆盖
- `Core` / `Core.Tests` 的本轮受影响项目均保持 `0 Warning(s)`,新增测试也覆盖了此前没有直接回归的测试替身行为
- `GFramework.Cqrs.Tests` 仍保留 `125` 条既有 `Mediator/*` warning这属于下一轮 warning reduction 波次,而不是本轮 PR review follow-up 的直接写集
- 下一步:
1. 提交本轮 PR review follow-up 与 `ai-plan` 同步。
2. 若继续处理 `Cqrs.Tests` warning以下一轮单独规划 `Mediator/*` 波次为起点。
## 2026-04-28 — RP-087 ## 2026-04-28 — RP-087
### 阶段:按 `$gframework-batch-boot 50` 并行收敛 `Core.Tests` / `Cqrs.Tests` 低风险切片 ### 阶段:按 `$gframework-batch-boot 50` 并行收敛 `Core.Tests` / `Cqrs.Tests` 低风险切片