Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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