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
4 changes: 4 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ All AI agents and contributors must follow these rules when writing, reviewing,
`git.exe`) instead of the Linux `git` binary.
- If a Git command in WSL fails with a worktree-style “not a git repository” path translation error, rerun it with the
Windows Git executable and treat that as the repository-default Git path for the rest of the task.
- If the shell does not currently resolve `git.exe` to the host Windows Git installation, prepend that installation's
command directory to `PATH` and reset shell command hashing for the current session before continuing.
- After resolving the host Windows Git path, prefer an explicit session-local binding for subsequent commands so the
shell does not fall back to Linux `/usr/bin/git` later in the same WSL session.

## Commenting Rules (MUST)

Expand Down
202 changes: 202 additions & 0 deletions GFramework.Game.Tests/Config/YamlConfigLoaderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,52 @@ public void LoadAsync_Should_Throw_When_Scalar_Value_Is_Not_Declared_In_Schema_E
});
}

/// <summary>
/// 验证标量 <c>const</c> 限制会在运行时被拒绝。
/// </summary>
[Test]
public void LoadAsync_Should_Throw_When_Scalar_Value_Does_Not_Match_Schema_Const()
{
CreateConfigFile(
"monster/slime.yaml",
"""
id: 1
name: Slime
rarity: rare
""");
CreateSchemaFile(
"schemas/monster.schema.json",
"""
{
"type": "object",
"required": ["id", "name", "rarity"],
"properties": {
"id": { "type": "integer" },
"name": { "type": "string" },
"rarity": {
"type": "string",
"const": "common"
}
}
}
""");

var loader = new YamlConfigLoader(_rootPath)
.RegisterTable<int, MonsterConfigStub>("monster", "monster", "schemas/monster.schema.json",
static config => config.Id);
var registry = new ConfigRegistry();

var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));

Assert.Multiple(() =>
{
Assert.That(exception, Is.Not.Null);
Assert.That(exception!.Message, Does.Contain("constant value"));
Assert.That(exception.Message, Does.Contain("\"common\""));
Assert.That(registry.Count, Is.EqualTo(0));
});
}

/// <summary>
/// 验证数值最小值与最大值约束会在运行时被统一拒绝。
/// </summary>
Expand Down Expand Up @@ -1198,6 +1244,58 @@ public void LoadAsync_Should_Throw_When_Array_Item_Is_Not_Declared_In_Schema_Enu
});
}

/// <summary>
/// 验证数组 <c>const</c> 限制会保留元素顺序并按完整序列比较。
/// </summary>
[Test]
public void LoadAsync_Should_Throw_When_Array_Value_Does_Not_Match_Schema_Const()
{
CreateConfigFile(
"monster/slime.yaml",
"""
id: 1
name: Slime
dropItemIds:
- gem
- potion
""");
CreateSchemaFile(
"schemas/monster.schema.json",
"""
{
"type": "object",
"required": ["id", "name"],
"properties": {
"id": { "type": "integer" },
"name": { "type": "string" },
"dropItemIds": {
"type": "array",
"const": ["potion", "gem"],
"items": {
"type": "string"
}
}
}
}
""");

var loader = new YamlConfigLoader(_rootPath)
.RegisterTable<int, MonsterDropArrayConfigStub>("monster", "monster", "schemas/monster.schema.json",
static config => config.Id);
var registry = new ConfigRegistry();

var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));

Assert.Multiple(() =>
{
Assert.That(exception, Is.Not.Null);
Assert.That(exception!.Message, Does.Contain("dropItemIds"));
Assert.That(exception.Message, Does.Contain("potion"));
Assert.That(exception.Message, Does.Contain("gem"));
Assert.That(registry.Count, Is.EqualTo(0));
});
}

/// <summary>
/// 验证嵌套对象中的必填字段同样会按 schema 在运行时生效。
/// </summary>
Expand Down Expand Up @@ -1248,6 +1346,110 @@ public void LoadAsync_Should_Throw_When_Nested_Object_Is_Missing_Required_Proper
});
}

/// <summary>
/// 验证嵌套对象 <c>const</c> 限制会按完整对象内容比较。
/// </summary>
[Test]
public void LoadAsync_Should_Throw_When_Nested_Object_Does_Not_Match_Schema_Const()
{
CreateConfigFile(
"monster/slime.yaml",
"""
id: 1
name: Slime
reward:
gold: 10
currency: gem
""");
CreateSchemaFile(
"schemas/monster.schema.json",
"""
{
"type": "object",
"required": ["id", "name", "reward"],
"properties": {
"id": { "type": "integer" },
"name": { "type": "string" },
"reward": {
"type": "object",
"properties": {
"gold": { "type": "integer" },
"currency": { "type": "string" }
},
"const": {
"gold": 10,
"currency": "coin"
}
}
}
}
""");

var loader = new YamlConfigLoader(_rootPath)
.RegisterTable<int, MonsterNestedConfigStub>("monster", "monster", "schemas/monster.schema.json",
static config => config.Id);
var registry = new ConfigRegistry();

var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));

Assert.Multiple(() =>
{
Assert.That(exception, Is.Not.Null);
Assert.That(exception!.Message, Does.Contain("reward"));
Assert.That(exception.Message, Does.Contain("\"gold\""));
Assert.That(exception.Message, Does.Contain("\"currency\""));
Assert.That(exception.Message, Does.Contain("\"coin\""));
Assert.That(registry.Count, Is.EqualTo(0));
});
}

/// <summary>
/// 验证空对象 <c>const</c> 约束会被视为合法 schema,并与空 YAML 映射正确匹配。
/// </summary>
[Test]
public async Task LoadAsync_Should_Accept_Empty_Object_Schema_Const()
{
CreateConfigFile(
"monster/slime.yaml",
"""
id: 1
name: Slime
reward: {}
""");
CreateSchemaFile(
"schemas/monster.schema.json",
"""
{
"type": "object",
"required": ["id", "name", "reward"],
"properties": {
"id": { "type": "integer" },
"name": { "type": "string" },
"reward": {
"type": "object",
"properties": {},
"const": {}
}
}
}
""");

var loader = new YamlConfigLoader(_rootPath)
.RegisterTable<int, MonsterNestedConfigStub>("monster", "monster", "schemas/monster.schema.json",
static config => config.Id);
var registry = new ConfigRegistry();

await loader.LoadAsync(registry);

var table = registry.GetTable<int, MonsterNestedConfigStub>("monster");

Assert.Multiple(() =>
{
Assert.That(table.Count, Is.EqualTo(1));
Assert.That(table.Get(1).Name, Is.EqualTo("Slime"));
});
}

/// <summary>
/// 验证对象字段不满足 <c>minProperties</c> 时会在运行时被拒绝。
/// </summary>
Expand Down
Loading
Loading