Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,6 @@ ai-plan/public/*
!ai-plan/public/**/*.md
ai-plan/private/
ai-libs/
.codex
# tool
.venv/
9 changes: 9 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,13 @@ All AI agents and contributors must follow these rules when writing, reviewing,
baseline from a non-incremental repository-root build by running `dotnet clean` and then `dotnet build`.
- Contributors MUST NOT treat a repeated incremental `dotnet build` result as authoritative for warning inspection when
a clean baseline has not been captured in the same round.
- If a direct `dotnet clean`, `dotnet build`, or `dotnet test` command fails inside the agent sandbox with missing
diagnostics, `Permission denied`, MSBuild pipe/socket errors, or other environment-only noise that does not match a
normal shell invocation, contributors MUST request permission and rerun the same direct command outside the sandbox
before concluding that the repository or toolchain is broken.
- For repository truth, contributors MUST prefer the result of the original direct command executed outside the sandbox
over sandbox-only failures, workaround-heavy variants, or speculative environment flags unless the user explicitly
asks for a non-default command shape.
- If the task changes multiple projects or shared abstractions, prefer a solution-level or affected-project
`dotnet build ... -c Release`; otherwise use the smallest build command that still proves the result compiles.
- When a task adds a feature or modifies code, contributors MUST run a Release build for every directly affected
Expand Down Expand Up @@ -235,6 +242,8 @@ All generated or modified code MUST include clear and meaningful comments where
### Validation Commands

Use the smallest command set that proves the change, then expand if the change is cross-cutting.
If a sandboxed agent run reports environment-specific .NET failures, rerun the same direct command outside the sandbox
and treat that unsandboxed result as authoritative for validation and warning baselines.

```bash
# Check warnings from the default repository build entrypoint
Expand Down
6 changes: 3 additions & 3 deletions GFramework.Core.Tests/Logging/LogContextTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ public async Task Push_InAsyncContext_ShouldIsolateAcrossThreads()
using (LogContext.Push("TaskId", "Task1"))
{
task1Values.Add(LogContext.Current["TaskId"]);
await Task.Delay(50);
await Task.Delay(50).ConfigureAwait(false);
task1Values.Add(LogContext.Current["TaskId"]);
}
});
Expand All @@ -167,12 +167,12 @@ public async Task Push_InAsyncContext_ShouldIsolateAcrossThreads()
using (LogContext.Push("TaskId", "Task2"))
{
task2Values.Add(LogContext.Current["TaskId"]);
await Task.Delay(50);
await Task.Delay(50).ConfigureAwait(false);
task2Values.Add(LogContext.Current["TaskId"]);
}
});

await Task.WhenAll(task1, task2);
await Task.WhenAll(task1, task2).ConfigureAwait(false);

Assert.That(task1Values, Has.All.EqualTo("Task1"));
Assert.That(task2Values, Has.All.EqualTo("Task2"));
Expand Down
10 changes: 6 additions & 4 deletions GFramework.Core.Tests/Logging/LoggerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,8 @@ public void Logger_WithFatalMinLevel_ShouldDisableAllButFatal()
/// </summary>
public sealed class TestLogger : AbstractLogger
{
private readonly List<LogEntry> _logs = new();

/// <summary>
/// 初始化TestLogger的新实例
/// </summary>
Expand All @@ -435,9 +437,9 @@ public TestLogger(string? name = null, LogLevel minLevel = LogLevel.Info) : base
}

/// <summary>
/// 获取记录的日志条目列表
/// 获取按写入顺序保存的日志条目只读视图
/// </summary>
public List<LogEntry> Logs { get; } = new();
public IReadOnlyList<LogEntry> Logs => _logs;

/// <summary>
/// 将日志信息写入内部存储
Expand All @@ -447,7 +449,7 @@ public TestLogger(string? name = null, LogLevel minLevel = LogLevel.Info) : base
/// <param name="exception">相关异常(可选)</param>
protected override void Write(LogLevel level, string message, Exception? exception)
{
Logs.Add(new LogEntry(level, message, exception));
_logs.Add(new LogEntry(level, message, exception));
}

/// <summary>
Expand All @@ -457,4 +459,4 @@ protected override void Write(LogLevel level, string message, Exception? excepti
/// <param name="Message">日志消息</param>
/// <param name="Exception">相关异常(可选)</param>
public sealed record LogEntry(LogLevel Level, string Message, Exception? Exception);
}
}
148 changes: 90 additions & 58 deletions GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -327,27 +327,7 @@ public void RegisterHandlers_Should_Use_Direct_Fallback_Types_Without_GetType_Or
[Test]
public void RegisterHandlers_Should_Cache_Assembly_Metadata_Across_Containers()
{
var generatedAssembly = new Mock<Assembly>();
generatedAssembly
.SetupGet(static assembly => assembly.FullName)
.Returns("GFramework.Core.Tests.Cqrs.CachedMetadataAssembly, Version=1.0.0.0");
generatedAssembly
.Setup(static assembly => assembly.GetCustomAttributes(typeof(CqrsHandlerRegistryAttribute), false))
.Returns([new CqrsHandlerRegistryAttribute(typeof(PartialGeneratedNotificationHandlerRegistry))]);
generatedAssembly
.Setup(static assembly => assembly.GetCustomAttributes(typeof(CqrsReflectionFallbackAttribute), false))
.Returns(
[
new CqrsReflectionFallbackAttribute(
ReflectionFallbackNotificationContainer.ReflectionOnlyHandlerType.FullName!)
]);
generatedAssembly
.Setup(static assembly => assembly.GetType(
ReflectionFallbackNotificationContainer.ReflectionOnlyHandlerType.FullName!,
false,
false))
.Returns(ReflectionFallbackNotificationContainer.ReflectionOnlyHandlerType);

var generatedAssembly = CreateCachedMetadataAssembly();
var firstContainer = new MicrosoftDiContainer();
var secondContainer = new MicrosoftDiContainer();

Expand All @@ -356,43 +336,8 @@ public void RegisterHandlers_Should_Cache_Assembly_Metadata_Across_Containers()
firstContainer.Freeze();
secondContainer.Freeze();

var firstRegistrations = firstContainer.GetAll<INotificationHandler<GeneratedRegistryNotification>>()
.Select(static handler => handler.GetType())
.ToArray();
var secondRegistrations = secondContainer.GetAll<INotificationHandler<GeneratedRegistryNotification>>()
.Select(static handler => handler.GetType())
.ToArray();

Assert.Multiple(() =>
{
Assert.That(
firstRegistrations,
Is.EqualTo(
[
typeof(GeneratedRegistryNotificationHandler),
ReflectionFallbackNotificationContainer.ReflectionOnlyHandlerType
]));
Assert.That(
secondRegistrations,
Is.EqualTo(
[
typeof(GeneratedRegistryNotificationHandler),
ReflectionFallbackNotificationContainer.ReflectionOnlyHandlerType
]));
});

generatedAssembly.Verify(
static assembly => assembly.GetCustomAttributes(typeof(CqrsHandlerRegistryAttribute), false),
Times.Once);
generatedAssembly.Verify(
static assembly => assembly.GetCustomAttributes(typeof(CqrsReflectionFallbackAttribute), false),
Times.Once);
generatedAssembly.Verify(
static assembly => assembly.GetType(
ReflectionFallbackNotificationContainer.ReflectionOnlyHandlerType.FullName!,
false,
false),
Times.Once);
AssertGeneratedRegistryNotificationHandlers(firstContainer, secondContainer);
VerifyCachedMetadataAssemblyLookups(generatedAssembly);
}

/// <summary>
Expand Down Expand Up @@ -526,6 +471,93 @@ private static void ClearRegistrarCaches()
ClearCache(GetRegistrarCacheField("SupportedHandlerInterfacesCache"));
}

/// <summary>
/// 创建一个携带 generated registry 与 reflection fallback 元数据的程序集替身,
/// 用于验证 registrar 是否会跨容器复用程序集级元数据。
/// </summary>
private static Mock<Assembly> CreateCachedMetadataAssembly()
{
var generatedAssembly = new Mock<Assembly>();
generatedAssembly
.SetupGet(static assembly => assembly.FullName)
.Returns("GFramework.Core.Tests.Cqrs.CachedMetadataAssembly, Version=1.0.0.0");
generatedAssembly
.Setup(static assembly => assembly.GetCustomAttributes(typeof(CqrsHandlerRegistryAttribute), false))
.Returns([new CqrsHandlerRegistryAttribute(typeof(PartialGeneratedNotificationHandlerRegistry))]);
generatedAssembly
.Setup(static assembly => assembly.GetCustomAttributes(typeof(CqrsReflectionFallbackAttribute), false))
.Returns(
[
new CqrsReflectionFallbackAttribute(
ReflectionFallbackNotificationContainer.ReflectionOnlyHandlerType.FullName!)
]);
generatedAssembly
.Setup(static assembly => assembly.GetType(
ReflectionFallbackNotificationContainer.ReflectionOnlyHandlerType.FullName!,
false,
false))
.Returns(ReflectionFallbackNotificationContainer.ReflectionOnlyHandlerType);
return generatedAssembly;
}

/// <summary>
/// 断言两个容器都获得了相同的 generated-registry 与 reflection-fallback 处理器集合。
/// </summary>
private static void AssertGeneratedRegistryNotificationHandlers(
MicrosoftDiContainer firstContainer,
MicrosoftDiContainer secondContainer)
{
var firstRegistrations = GetGeneratedRegistryNotificationHandlerTypes(firstContainer);
var secondRegistrations = GetGeneratedRegistryNotificationHandlerTypes(secondContainer);

Assert.Multiple(() =>
{
Assert.That(firstRegistrations, Is.EqualTo(GetExpectedGeneratedRegistryNotificationHandlerTypes()));
Assert.That(secondRegistrations, Is.EqualTo(GetExpectedGeneratedRegistryNotificationHandlerTypes()));
});
}

/// <summary>
/// 读取容器中针对 generated notification 的 handler 运行时类型列表。
/// </summary>
private static Type[] GetGeneratedRegistryNotificationHandlerTypes(MicrosoftDiContainer container)
{
return container.GetAll<INotificationHandler<GeneratedRegistryNotification>>()
.Select(static handler => handler.GetType())
.ToArray();
}

/// <summary>
/// 获取 generated registry 与 reflection fallback 共同组成的预期 handler 顺序。
/// </summary>
private static Type[] GetExpectedGeneratedRegistryNotificationHandlerTypes()
{
return
[
typeof(GeneratedRegistryNotificationHandler),
ReflectionFallbackNotificationContainer.ReflectionOnlyHandlerType
];
}

/// <summary>
/// 断言程序集级 generated registry / fallback 元数据只会被读取一次。
/// </summary>
private static void VerifyCachedMetadataAssemblyLookups(Mock<Assembly> generatedAssembly)
{
generatedAssembly.Verify(
static assembly => assembly.GetCustomAttributes(typeof(CqrsHandlerRegistryAttribute), false),
Times.Once);
generatedAssembly.Verify(
static assembly => assembly.GetCustomAttributes(typeof(CqrsReflectionFallbackAttribute), false),
Times.Once);
generatedAssembly.Verify(
static assembly => assembly.GetType(
ReflectionFallbackNotificationContainer.ReflectionOnlyHandlerType.FullName!,
false,
false),
Times.Once);
}

/// <summary>
/// 通过反射读取 registrar 的静态缓存对象。
/// </summary>
Expand Down
6 changes: 4 additions & 2 deletions GFramework.Cqrs.Tests/Logging/TestLogger.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ namespace GFramework.Cqrs.Tests.Logging;
/// </summary>
public sealed class TestLogger : AbstractLogger
{
private readonly List<LogEntry> _logs = [];

/// <summary>
/// 初始化测试日志记录器。
/// </summary>
Expand All @@ -33,7 +35,7 @@ public TestLogger(string? name = null, LogLevel minLevel = LogLevel.Info) : base
/// <summary>
/// 获取当前测试期间捕获到的日志条目。
/// </summary>
public List<LogEntry> Logs { get; } = [];
public IReadOnlyList<LogEntry> Logs => _logs;

/// <summary>
/// 将日志写入内存,供断言使用。
Expand All @@ -43,7 +45,7 @@ public TestLogger(string? name = null, LogLevel minLevel = LogLevel.Info) : base
/// <param name="exception">关联异常。</param>
protected override void Write(LogLevel level, string message, Exception? exception)
{
Logs.Add(new LogEntry(level, message, exception));
_logs.Add(new LogEntry(level, message, exception));
}

/// <summary>
Expand Down
26 changes: 18 additions & 8 deletions GFramework.Cqrs.Tests/Mediator/MediatorAdvancedFeaturesTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -366,11 +366,6 @@ public ValueTask<string> Handle(TestBehaviorRequest request, CancellationToken c
}
}

public static class TestLoggingBehavior
{
public static List<string> LoggedMessages { get; set; } = new();
}

public sealed record TestValidatedRequest : IRequest<string>
{
public int Value { get; init; }
Expand Down Expand Up @@ -467,8 +462,19 @@ public sealed record TestCircuitBreakerRequest : IRequest<string>
// 复杂场景相关类
public class SagaData
{
public List<int> CompletedSteps { get; } = new();
public List<int> CompensatedSteps { get; } = new();
/// <summary>
/// 获取 Saga 已成功执行的步骤集合。
/// </summary>
public IList<int> CompletedSteps { get; } = new List<int>();

/// <summary>
/// 获取 Saga 失败后已执行补偿的步骤集合。
/// </summary>
public IList<int> CompensatedSteps { get; } = new List<int>();

/// <summary>
/// 获取或设置 Saga 是否已经完整结束。
/// </summary>
public bool IsCompleted { get; set; }
}

Expand All @@ -489,7 +495,11 @@ public sealed record TestExternalServiceRequest : IRequest<string>
public sealed record TestDatabaseRequest : IRequest<string>
{
public string Data { get; init; } = string.Empty;
public List<string> Storage { get; init; } = new();

/// <summary>
/// 获取或初始化用于模拟数据库写入的可变存储集合,同时避免泄漏具体集合实现。
/// </summary>
public IList<string> Storage { get; init; } = new List<string>();
}

#endregion
11 changes: 4 additions & 7 deletions GFramework.Ecs.Arch.Tests/Ecs/EcsAdvancedTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,8 @@ public void World_Destroy_Should_Be_Safe()
InitializeEcsModule();
_world!.Create(new Position(0, 0));

Assert.DoesNotThrow(() =>
{
World.Destroy(_world);
_world = null;
});
World.Destroy(_world);
_world = null;
}

[Test]
Expand Down Expand Up @@ -124,7 +121,7 @@ public void ArchEcsModule_WithNoSystems_Should_NotThrow()

_world = _container!.Get<World>();

Assert.DoesNotThrow(() => _ecsModule.Update(1.0f));
_ecsModule.Update(1.0f);
}

[Test]
Expand Down Expand Up @@ -211,4 +208,4 @@ public void MultipleEntities_WithDifferentComponents_Should_CoExist()
Assert.That(_world.Has<Position>(entity3), Is.False);
Assert.That(_world.Has<Velocity>(entity3), Is.True);
}
}
}
2 changes: 1 addition & 1 deletion GFramework.Game.Tests/Config/GameConfigBootstrapTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ public void InitializeAsync_Should_Reject_Concurrent_Caller_While_Initialization

continueInitialization.Set();

Assert.DoesNotThrowAsync(() => firstInitializeTask);
Assert.That(async () => await firstInitializeTask.ConfigureAwait(false), Throws.Nothing);

Assert.Multiple(() =>
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -198,11 +198,10 @@ public async Task GeneratedBindings_Should_Expose_Serializer_And_Validator_Helpe
Assert.That(yaml.EndsWith("\n", StringComparison.Ordinal), Is.True);
});

Assert.DoesNotThrow(() =>
MonsterConfigBindings.ValidateYaml(_rootPath, "monster/generated.yaml", yaml));

Assert.DoesNotThrowAsync(async () =>
await MonsterConfigBindings.ValidateYamlAsync(_rootPath, "monster/generated.yaml", yaml).ConfigureAwait(false));
MonsterConfigBindings.ValidateYaml(_rootPath, "monster/generated.yaml", yaml);
Assert.That(
async () => await MonsterConfigBindings.ValidateYamlAsync(_rootPath, "monster/generated.yaml", yaml).ConfigureAwait(false),
Throws.Nothing);

var invalidYaml = """
id: 3
Expand Down
Loading
Loading