test(game-tests): 收敛热重载长方法 warning

- 重构 YamlConfigLoaderTests 的热重载夹具与回调接线,消化 4 个 MA0051 长方法警告
- 更新 analyzer-warning-reduction active todo 与 trace,记录 652 条根构建 warning 的新基线
- 保持 GFramework.Game.Tests 的 Release build 为 0 Warning 和 0 Error
This commit is contained in:
gewuyou 2026-04-25 15:13:22 +08:00
parent 6a704f3aa7
commit be26640b58
3 changed files with 312 additions and 268 deletions

View File

@ -1,4 +1,5 @@
using System.IO;
using GFramework.Core.Abstractions.Events;
using GFramework.Game.Abstractions.Config;
using GFramework.Game.Config;
@ -2789,83 +2790,15 @@ public class YamlConfigLoaderTests
[Test]
public async Task EnableHotReload_Should_Keep_Previous_State_When_Contains_Reference_Dependency_Breaks()
{
CreateConfigFile(
"item/potion.yaml",
"""
id: potion
name: Potion
""");
CreateConfigFile(
"monster/slime.yaml",
"""
id: 1
name: Slime
dropItemIds:
- potion
""");
CreateSchemaFile(
"schemas/item.schema.json",
"""
{
"type": "object",
"required": ["id", "name"],
"properties": {
"id": { "type": "string" },
"name": { "type": "string" }
}
}
""");
CreateSchemaFile(
"schemas/monster.schema.json",
"""
{
"type": "object",
"required": ["id", "name", "dropItemIds"],
"properties": {
"id": { "type": "integer" },
"name": { "type": "string" },
"dropItemIds": {
"type": "array",
"minContains": 1,
"contains": {
"type": "string",
"x-gframework-ref-table": "item"
},
"items": {
"type": "string"
}
}
}
}
""");
var loader = new YamlConfigLoader(_rootPath)
.RegisterTable<string, ItemConfigStub>("item", "item", "schemas/item.schema.json",
static config => config.Id)
.RegisterTable<int, MonsterDropArrayConfigStub>("monster", "monster", "schemas/monster.schema.json",
static config => config.Id);
var registry = new ConfigRegistry();
await loader.LoadAsync(registry);
var reloadFailureTaskSource =
new TaskCompletionSource<(string TableName, Exception Exception)>(TaskCreationOptions
.RunContinuationsAsynchronously);
var hotReload = loader.EnableHotReload(
registry,
onTableReloadFailed: (tableName, exception) =>
reloadFailureTaskSource.TrySetResult((tableName, exception)),
debounceDelay: TimeSpan.FromMilliseconds(150));
var (loader, registry) = await CreateLoadedContainsReferenceHotReloadScenarioAsync().ConfigureAwait(false);
var (reloadFailureTaskSource, hotReload) = EnableHotReloadWithFailureCapture(loader, registry);
try
{
CreateConfigFile(
"item/potion.yaml",
"""
id: elixir
name: Elixir
""");
CreateConfigFile("item/potion.yaml", UpdatedItemConfigContent);
var failure = await WaitForTaskWithinAsync(reloadFailureTaskSource.Task, TimeSpan.FromSeconds(5));
var failure = await WaitForTaskWithinAsync(reloadFailureTaskSource.Task, TimeSpan.FromSeconds(5))
.ConfigureAwait(false);
var diagnosticException = failure.Exception as ConfigLoadException;
Assert.Multiple(() =>
@ -2958,65 +2891,17 @@ public class YamlConfigLoaderTests
[Test]
public async Task EnableHotReload_Should_Support_Options_Object()
{
CreateConfigFile(
"monster/slime.yaml",
"""
id: 1
name: Slime
hp: 10
""");
CreateSchemaFile(
"schemas/monster.schema.json",
"""
{
"type": "object",
"required": ["id", "name"],
"properties": {
"id": { "type": "integer" },
"name": { "type": "string" },
"hp": { "type": "integer" }
}
}
""");
var loader = new YamlConfigLoader(_rootPath)
.RegisterTable(
new YamlConfigTableRegistrationOptions<int, MonsterConfigStub>(
"monster",
"monster",
static config => config.Id)
{
SchemaRelativePath = "schemas/monster.schema.json"
});
var registry = new ConfigRegistry();
await loader.LoadAsync(registry);
var reloadTaskSource = new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously);
var hotReload = loader.EnableHotReload(
registry,
new YamlConfigHotReloadOptions
{
OnTableReloaded = tableName => reloadTaskSource.TrySetResult(tableName),
DebounceDelay = TimeSpan.FromMilliseconds(150)
});
var (loader, registry) = await CreateLoadedMonsterHotReloadScenarioAsync(useOptionsObject: true)
.ConfigureAwait(false);
var (reloadTaskSource, hotReload) = EnableHotReloadWithReloadCapture(loader, registry, useOptionsObject: true);
try
{
CreateConfigFile(
"monster/slime.yaml",
"""
id: 1
name: Slime
hp: 25
""");
CreateConfigFile("monster/slime.yaml", UpdatedMonsterConfigContent);
var tableName = await WaitForTaskWithinAsync(reloadTaskSource.Task, TimeSpan.FromSeconds(5));
Assert.Multiple(() =>
{
Assert.That(tableName, Is.EqualTo("monster"));
Assert.That(registry.GetTable<int, MonsterConfigStub>("monster").Get(1).Hp, Is.EqualTo(25));
});
var tableName = await WaitForTaskWithinAsync(reloadTaskSource.Task, TimeSpan.FromSeconds(5))
.ConfigureAwait(false);
AssertMonsterHotReloadUpdated(tableName, registry);
}
finally
{
@ -3050,60 +2935,15 @@ public class YamlConfigLoaderTests
[Test]
public async Task EnableHotReload_Should_Keep_Previous_Table_When_Schema_Change_Makes_Reload_Fail()
{
CreateConfigFile(
"monster/slime.yaml",
"""
id: 1
name: Slime
hp: 10
""");
CreateSchemaFile(
"schemas/monster.schema.json",
"""
{
"type": "object",
"required": ["id", "name"],
"properties": {
"id": { "type": "integer" },
"name": { "type": "string" },
"hp": { "type": "integer" }
}
}
""");
var loader = new YamlConfigLoader(_rootPath)
.RegisterTable<int, MonsterConfigStub>("monster", "monster", "schemas/monster.schema.json",
static config => config.Id);
var registry = new ConfigRegistry();
await loader.LoadAsync(registry);
var reloadFailureTaskSource =
new TaskCompletionSource<(string TableName, Exception Exception)>(TaskCreationOptions
.RunContinuationsAsynchronously);
var hotReload = loader.EnableHotReload(
registry,
onTableReloadFailed: (tableName, exception) =>
reloadFailureTaskSource.TrySetResult((tableName, exception)),
debounceDelay: TimeSpan.FromMilliseconds(150));
var (loader, registry) = await CreateLoadedMonsterHotReloadScenarioAsync().ConfigureAwait(false);
var (reloadFailureTaskSource, hotReload) = EnableHotReloadWithFailureCapture(loader, registry);
try
{
CreateSchemaFile(
"schemas/monster.schema.json",
"""
{
"type": "object",
"required": ["id", "name", "rarity"],
"properties": {
"id": { "type": "integer" },
"name": { "type": "string" },
"hp": { "type": "integer" },
"rarity": { "type": "string" }
}
}
""");
CreateSchemaFile("schemas/monster.schema.json", MonsterSchemaWithRarityContent);
var failure = await WaitForTaskWithinAsync(reloadFailureTaskSource.Task, TimeSpan.FromSeconds(5));
var failure = await WaitForTaskWithinAsync(reloadFailureTaskSource.Task, TimeSpan.FromSeconds(5))
.ConfigureAwait(false);
var diagnosticException = failure.Exception as ConfigLoadException;
Assert.Multiple(() =>
@ -3130,75 +2970,15 @@ public class YamlConfigLoaderTests
[Test]
public async Task EnableHotReload_Should_Keep_Previous_State_When_Dependency_Table_Breaks_Cross_Table_Reference()
{
CreateConfigFile(
"item/potion.yaml",
"""
id: potion
name: Potion
""");
CreateConfigFile(
"monster/slime.yaml",
"""
id: 1
name: Slime
dropItemId: potion
""");
CreateSchemaFile(
"schemas/item.schema.json",
"""
{
"type": "object",
"required": ["id", "name"],
"properties": {
"id": { "type": "string" },
"name": { "type": "string" }
}
}
""");
CreateSchemaFile(
"schemas/monster.schema.json",
"""
{
"type": "object",
"required": ["id", "name", "dropItemId"],
"properties": {
"id": { "type": "integer" },
"name": { "type": "string" },
"dropItemId": {
"type": "string",
"x-gframework-ref-table": "item"
}
}
}
""");
var loader = new YamlConfigLoader(_rootPath)
.RegisterTable<string, ItemConfigStub>("item", "item", "schemas/item.schema.json",
static config => config.Id)
.RegisterTable<int, MonsterDropConfigStub>("monster", "monster", "schemas/monster.schema.json",
static config => config.Id);
var registry = new ConfigRegistry();
await loader.LoadAsync(registry);
var reloadFailureTaskSource =
new TaskCompletionSource<(string TableName, Exception Exception)>(TaskCreationOptions
.RunContinuationsAsynchronously);
var hotReload = loader.EnableHotReload(
registry,
onTableReloadFailed: (tableName, exception) =>
reloadFailureTaskSource.TrySetResult((tableName, exception)),
debounceDelay: TimeSpan.FromMilliseconds(150));
var (loader, registry) = await CreateLoadedCrossTableReferenceHotReloadScenarioAsync().ConfigureAwait(false);
var (reloadFailureTaskSource, hotReload) = EnableHotReloadWithFailureCapture(loader, registry);
try
{
CreateConfigFile(
"item/potion.yaml",
"""
id: elixir
name: Elixir
""");
CreateConfigFile("item/potion.yaml", UpdatedItemConfigContent);
var failure = await WaitForTaskWithinAsync(reloadFailureTaskSource.Task, TimeSpan.FromSeconds(5));
var failure = await WaitForTaskWithinAsync(reloadFailureTaskSource.Task, TimeSpan.FromSeconds(5))
.ConfigureAwait(false);
var diagnosticException = failure.Exception as ConfigLoadException;
Assert.Multiple(() =>
@ -3223,6 +3003,243 @@ public class YamlConfigLoaderTests
}
}
private const string ItemSchemaContent =
"""
{
"type": "object",
"required": ["id", "name"],
"properties": {
"id": { "type": "string" },
"name": { "type": "string" }
}
}
""";
private const string InitialMonsterConfigContent =
"""
id: 1
name: Slime
hp: 10
""";
private const string UpdatedMonsterConfigContent =
"""
id: 1
name: Slime
hp: 25
""";
private const string MonsterSchemaContent =
"""
{
"type": "object",
"required": ["id", "name"],
"properties": {
"id": { "type": "integer" },
"name": { "type": "string" },
"hp": { "type": "integer" }
}
}
""";
private const string MonsterSchemaWithRarityContent =
"""
{
"type": "object",
"required": ["id", "name", "rarity"],
"properties": {
"id": { "type": "integer" },
"name": { "type": "string" },
"hp": { "type": "integer" },
"rarity": { "type": "string" }
}
}
""";
private const string UpdatedItemConfigContent =
"""
id: elixir
name: Elixir
""";
private const string MonsterDropArrayConfigContent =
"""
id: 1
name: Slime
dropItemIds:
- potion
""";
private const string MonsterDropArraySchemaContent =
"""
{
"type": "object",
"required": ["id", "name", "dropItemIds"],
"properties": {
"id": { "type": "integer" },
"name": { "type": "string" },
"dropItemIds": {
"type": "array",
"minContains": 1,
"contains": {
"type": "string",
"x-gframework-ref-table": "item"
},
"items": {
"type": "string"
}
}
}
}
""";
private const string MonsterDropConfigContent =
"""
id: 1
name: Slime
dropItemId: potion
""";
private const string MonsterDropSchemaContent =
"""
{
"type": "object",
"required": ["id", "name", "dropItemId"],
"properties": {
"id": { "type": "integer" },
"name": { "type": "string" },
"dropItemId": {
"type": "string",
"x-gframework-ref-table": "item"
}
}
}
""";
/// <summary>
/// 创建并加载标准 monster 热重载夹具,供重载成功与 schema 失败场景复用。
/// </summary>
/// <param name="useOptionsObject">是否通过选项对象注册表。</param>
/// <returns>已完成首次加载的加载器与注册表。</returns>
private async Task<(YamlConfigLoader Loader, ConfigRegistry Registry)> CreateLoadedMonsterHotReloadScenarioAsync(
bool useOptionsObject = false)
{
CreateConfigFile("monster/slime.yaml", InitialMonsterConfigContent);
CreateSchemaFile("schemas/monster.schema.json", MonsterSchemaContent);
var loader = useOptionsObject
? new YamlConfigLoader(_rootPath).RegisterTable(
new YamlConfigTableRegistrationOptions<int, MonsterConfigStub>(
"monster",
"monster",
static config => config.Id)
{
SchemaRelativePath = "schemas/monster.schema.json"
})
: new YamlConfigLoader(_rootPath)
.RegisterTable<int, MonsterConfigStub>("monster", "monster", "schemas/monster.schema.json",
static config => config.Id);
var registry = new ConfigRegistry();
await loader.LoadAsync(registry).ConfigureAwait(false);
return (loader, registry);
}
/// <summary>
/// 创建并加载 contains 子 schema 引用场景,供热重载依赖回滚测试复用。
/// </summary>
/// <returns>已完成首次加载的加载器与注册表。</returns>
private async Task<(YamlConfigLoader Loader, ConfigRegistry Registry)>
CreateLoadedContainsReferenceHotReloadScenarioAsync()
{
var loader = CreateItemBackedMonsterLoader<MonsterDropArrayConfigStub>(
MonsterDropArrayConfigContent,
MonsterDropArraySchemaContent,
static config => config.Id,
("item/potion.yaml", "potion", "Potion"));
var registry = new ConfigRegistry();
await loader.LoadAsync(registry).ConfigureAwait(false);
return (loader, registry);
}
/// <summary>
/// 创建并加载跨表单值引用场景,供热重载依赖回滚测试复用。
/// </summary>
/// <returns>已完成首次加载的加载器与注册表。</returns>
private async Task<(YamlConfigLoader Loader, ConfigRegistry Registry)>
CreateLoadedCrossTableReferenceHotReloadScenarioAsync()
{
var loader = CreateItemBackedMonsterLoader<MonsterDropConfigStub>(
MonsterDropConfigContent,
MonsterDropSchemaContent,
static config => config.Id,
("item/potion.yaml", "potion", "Potion"));
var registry = new ConfigRegistry();
await loader.LoadAsync(registry).ConfigureAwait(false);
return (loader, registry);
}
/// <summary>
/// 以统一的失败回调配置启用热重载,避免每个测试重复接线相同的通知逻辑。
/// </summary>
/// <param name="loader">已完成首次加载的加载器。</param>
/// <param name="registry">要复用的配置注册表。</param>
/// <returns>失败通知任务源与取消注册句柄。</returns>
private static (TaskCompletionSource<(string TableName, Exception Exception)> TaskSource, IUnRegister Registration)
EnableHotReloadWithFailureCapture(YamlConfigLoader loader, ConfigRegistry registry)
{
var reloadFailureTaskSource =
new TaskCompletionSource<(string TableName, Exception Exception)>(TaskCreationOptions
.RunContinuationsAsynchronously);
var hotReload = loader.EnableHotReload(
registry,
onTableReloadFailed: (tableName, exception) =>
reloadFailureTaskSource.TrySetResult((tableName, exception)),
debounceDelay: TimeSpan.FromMilliseconds(150));
return (reloadFailureTaskSource, hotReload);
}
/// <summary>
/// 以统一的成功回调配置启用热重载,避免相同的防抖与回调装配在测试中重复出现。
/// </summary>
/// <param name="loader">已完成首次加载的加载器。</param>
/// <param name="registry">要复用的配置注册表。</param>
/// <param name="useOptionsObject">是否通过选项对象启用热重载。</param>
/// <returns>成功通知任务源与取消注册句柄。</returns>
private static (TaskCompletionSource<string> TaskSource, IUnRegister Registration) EnableHotReloadWithReloadCapture(
YamlConfigLoader loader,
ConfigRegistry registry,
bool useOptionsObject = false)
{
var reloadTaskSource = new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously);
var hotReload = useOptionsObject
? loader.EnableHotReload(
registry,
new YamlConfigHotReloadOptions
{
OnTableReloaded = tableName => reloadTaskSource.TrySetResult(tableName),
DebounceDelay = TimeSpan.FromMilliseconds(150)
})
: loader.EnableHotReload(
registry,
onTableReloaded: tableName => reloadTaskSource.TrySetResult(tableName),
debounceDelay: TimeSpan.FromMilliseconds(150));
return (reloadTaskSource, hotReload);
}
/// <summary>
/// 断言标准 monster 热重载成功后,通知表名与刷新后的生命值都符合预期。
/// </summary>
/// <param name="tableName">热重载回调返回的表名。</param>
/// <param name="registry">承载刷新结果的注册表。</param>
private static void AssertMonsterHotReloadUpdated(string tableName, ConfigRegistry registry)
{
Assert.Multiple(() =>
{
Assert.That(tableName, Is.EqualTo("monster"));
Assert.That(registry.GetTable<int, MonsterConfigStub>("monster").Get(1).Hp, Is.EqualTo(25));
});
}
/// <summary>
/// 为对象数组 <c>contains</c> 子集匹配场景创建加载器,避免测试方法体被大段固定 schema 稀释。
/// </summary>

View File

@ -6,37 +6,39 @@
## 当前恢复点
- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-065`
- 当前阶段:`Phase 65`
- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-066`
- 当前阶段:`Phase 66`
- 当前焦点:
- `2026-04-25` 已确认此前一批 `dotnet clean` / `dotnet build` / `dotnet test` 异常主要来自 agent 沙箱环境,而不是仓库或 WSL 默认 shell 本身
- 已用提权后的直接命令重新建立仓库根基线:`dotnet clean` 成功,`dotnet build` 结果为 `656 Warning(s)``0 Error(s)`
- 当前 `HEAD``origin/main` 对齐;基于 `$gframework-batch-boot 50` 的 committed branch diff 现为 `0 files / 0 lines`
- 当前活跃写集是 4 个测试噪音清理文件,属于新的低风险 warning-reduction 批次;提交前需由主线程在沙箱外重新验证
- 主线程已完成该批次的沙箱外复核,当前可安全并入本轮提交
- 已决定把“沙箱内 .NET 验证失败时必须申请沙箱外重跑并以该结果为准”写入 `AGENTS.md`,避免后续继续扩散伪环境阻塞
- `2026-04-25` 已先提交 `6a704f3` `fix(analyzer): 固化沙箱外验证并清理测试噪音`,把 AGENTS / active ai-plan 真值修正与 4 文件测试噪音批次一起收口
- 当前单文件批次聚焦 `GFramework.Game.Tests/Config/YamlConfigLoaderTests.cs`,已收敛 4 个根构建直接确认的 `MA0051`
- 提权后的直接仓库根基线已从 `656 Warning(s)` 降至 `652 Warning(s)`,说明本轮单文件批次有效
- `GFramework.Game.Tests` 的直接受影响 Release build 当前为 `0 Warning(s)``0 Error(s)`
- 本批次落地后branch diff 相对 `origin/main` 将从 `7 files` 推进到 `8 files`,仍显著低于 `$gframework-batch-boot 50` 阈值
## 当前活跃事实
- 当前 `origin/main` 基线提交为 `4ad880c``2026-04-25T14:35:38+08:00`)。
- `fix/analyzer-warning-reduction-batch` 当前 `HEAD` 等于 `origin/main`;因此 shorthand 阈值 `$gframework-batch-boot 50` 的当前 committed 规模为:
- `git diff --name-only origin/main...HEAD | wc -l``0`
- `git diff --numstat origin/main...HEAD``0 added / 0 deleted`
- 提权后的直接仓库根验证已经确认
- `6a704f3` 落地后,当前 committed branch diff 相对 `origin/main` 为:
- `git diff --name-only origin/main...HEAD | wc -l``7`
- `git diff --numstat origin/main...HEAD`累计 `104` added、`123` deleted
- 提权后的直接仓库根验证当前确认为
- `dotnet clean`
- 结果:成功;此前沙箱内 “Build FAILED but 0 errors” 的 clean 结果不是仓库真值
- `dotnet build`
- 结果:成功;`656 Warning(s)``0 Error(s)`,当前 warning reduction 应以此为总基线
- 当前待集成的低风险批次文件:
- 最新结果:成功;`652 Warning(s)``0 Error(s)`
- 已提交的低风险批次文件:
- `AGENTS.md`
- `GFramework.Ecs.Arch.Tests/Ecs/EcsAdvancedTests.cs`
- `GFramework.Game.Tests/Config/GameConfigBootstrapTests.cs`
- `GFramework.Game.Tests/Config/GeneratedConfigConsumerIntegrationTests.cs`
- `GFramework.Game.Tests/Config/YamlConfigTextValidatorTests.cs`
- `GFramework.Ecs.Arch.Tests/Ecs/EcsAdvancedTests.cs`
- 上述批次的 worker 侧验证结果:
- `ai-plan/public/analyzer-warning-reduction/todos/analyzer-warning-reduction-tracking.md`
- `ai-plan/public/analyzer-warning-reduction/traces/analyzer-warning-reduction-trace.md`
- 当前待提交批次文件:
- `GFramework.Game.Tests/Config/YamlConfigLoaderTests.cs`
- 当前批次验证结果:
- `dotnet build GFramework.Game.Tests/GFramework.Game.Tests.csproj -c Release`
- 最新主线程结果:成功;`0 Warning(s)``0 Error(s)`
- `dotnet build GFramework.Ecs.Arch.Tests/GFramework.Ecs.Arch.Tests.csproj -c Release`
- 最新主线程结果:成功;`0 Warning(s)``0 Error(s)`
## 当前风险
@ -64,18 +66,16 @@
- `dotnet clean`
- 当前结果:成功;在提权后的直接 shell 中可正常完成仓库根 clean
- `dotnet build`
- 当前结果:成功;`656 Warning(s)`、`0 Error(s)`
- 当前结果:成功;`652 Warning(s)`、`0 Error(s)`
- `dotnet build GFramework.Game.Tests/GFramework.Game.Tests.csproj -c Release`
- 当前结果:成功;`0 Warning(s)``0 Error(s)`
- `dotnet build GFramework.Ecs.Arch.Tests/GFramework.Ecs.Arch.Tests.csproj -c Release`
- 当前结果:成功;`0 Warning(s)``0 Error(s)`
- `git diff --name-only origin/main...HEAD | wc -l`
- 当前结果:`0`
- 当前结果:`7`
- `git diff --numstat origin/main...HEAD`
- 当前结果:`0 added / 0 deleted`
- 当前结果:累计 `104` added、`123` deleted
## 下一步建议
1. 集成并复核当前 4 文件测试噪音批次后,用沙箱外 `dotnet build GFramework.Game.Tests/GFramework.Game.Tests.csproj -c Release``dotnet build GFramework.Ecs.Arch.Tests/GFramework.Ecs.Arch.Tests.csproj -c Release` 重新确认结果
2. 下一轮 warning reduction 继续优先处理 `GFramework.Game.Tests/Config/YamlConfigLoaderTests.cs` 的长方法切片;它仍是已确认的单文件低风险热点,适合继续用 worker 独立推进
1. 提交当前 `YamlConfigLoaderTests.cs` 单文件批次后,继续按 `$gframework-batch-boot 50` 规则重算 branch diff并挑选下一个 1-3 文件的低风险热点
2. 下一轮优先从 `GFramework.Cqrs.Tests``GFramework.Game` 中继续选择单文件 `MA0051` / 测试噪音切片,避免过早推高 review 范围
3. 后续凡是沙箱内 `.NET` 验证再次出现无诊断失败、pipe/socket 权限问题或与普通 shell 不一致的结果,直接申请沙箱外重跑同一命令,不再扩散 workaround 型命令噪音。

View File

@ -1,5 +1,32 @@
# Analyzer Warning Reduction 追踪
## 2026-04-25 — RP-066
### 阶段:主线程回收停滞的单文件批次,并继续压低根构建 warning 基线
- 触发背景:
- `RP-065` 收尾后,`fix/analyzer-warning-reduction-batch` 已通过 `6a704f3` 把 AGENTS / active ai-plan 真值修正和 4 文件测试噪音批次提交到分支
- 原先负责 `YamlConfigLoaderTests.cs` 的 worker 长时间无结果,主线程收回该单文件批次以避免继续阻塞
- 主线程实施:
- 关闭停滞 worker直接重构 `GFramework.Game.Tests/Config/YamlConfigLoaderTests.cs`
- 通过提取固定夹具内容、热重载接线 helper 与共享断言,收敛以下 4 个长方法 warning
- `EnableHotReload_Should_Keep_Previous_State_When_Contains_Reference_Dependency_Breaks`
- `EnableHotReload_Should_Support_Options_Object`
- `EnableHotReload_Should_Keep_Previous_Table_When_Schema_Change_Makes_Reload_Fail`
- `EnableHotReload_Should_Keep_Previous_State_When_Dependency_Table_Breaks_Cross_Table_Reference`
- 在第一次仓库根重建中命中了两个 `CS0411` 泛型推断错误,主线程随即补上显式类型参数并重新建立 clean/build 基线
- 验证里程碑:
- `dotnet clean`
- 结果:成功
- `dotnet build`
- 结果:成功;`652 Warning(s)``0 Error(s)`,相较 `RP-065``656` 再下降 `4`
- `dotnet build GFramework.Game.Tests/GFramework.Game.Tests.csproj -c Release`
- 结果:成功;`0 Warning(s)``0 Error(s)`
- 当前结论:
- `YamlConfigLoaderTests.cs` 这 4 个根构建直接确认的 `MA0051` 已被消化
- 当前分支在 `6a704f3` 之后的下一提交只会新增 1 个唯一文件,因此 branch diff 仍明显低于 `$gframework-batch-boot 50` 阈值
- 下一轮可继续选择新的单文件或小写集热点,而不必暂停当前 batch loop
## 2026-04-25 — RP-065
### 阶段:确认 .NET 验证噪音来自沙箱,并把无沙箱直跑写成仓库规则