mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-04-02 19:55:57 +08:00
- 实现AI-First配表方案,支持怪物、物品、技能等静态内容管理 - 集成YAML配置源文件与JSON Schema结构描述功能 - 提供一对象一文件的目录组织方式和运行时只读查询能力 - 实现Source Generator生成配置类型和表包装类 - 集成VS Code插件提供配置浏览、raw编辑和递归校验功能 - 开发YamlConfigSchemaValidator实现JSON Schema子集校验 - 支持嵌套对象、对象数组、标量数组与深层enum引用约束校验 - 实现跨表引用检测和热重载时依赖表联动校验机制
304 lines
8.6 KiB
JavaScript
304 lines
8.6 KiB
JavaScript
const test = require("node:test");
|
|
const assert = require("node:assert/strict");
|
|
const {
|
|
applyFormUpdates,
|
|
applyScalarUpdates,
|
|
getEditableSchemaFields,
|
|
parseBatchArrayValue,
|
|
parseSchemaContent,
|
|
parseTopLevelYaml,
|
|
validateParsedConfig
|
|
} = require("../src/configValidation");
|
|
|
|
test("parseSchemaContent should capture nested objects and object-array metadata", () => {
|
|
const schema = parseSchemaContent(`
|
|
{
|
|
"type": "object",
|
|
"required": ["id", "reward", "phases"],
|
|
"properties": {
|
|
"id": {
|
|
"type": "integer",
|
|
"title": "Monster Id",
|
|
"description": "Primary monster key.",
|
|
"default": 1
|
|
},
|
|
"reward": {
|
|
"type": "object",
|
|
"required": ["gold"],
|
|
"properties": {
|
|
"gold": {
|
|
"type": "integer",
|
|
"default": 10
|
|
},
|
|
"currency": {
|
|
"type": "string",
|
|
"enum": ["coin", "gem"]
|
|
}
|
|
}
|
|
},
|
|
"phases": {
|
|
"type": "array",
|
|
"description": "Encounter phases.",
|
|
"items": {
|
|
"type": "object",
|
|
"required": ["wave"],
|
|
"properties": {
|
|
"wave": { "type": "integer" },
|
|
"monsterId": { "type": "string" }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
`);
|
|
|
|
assert.equal(schema.type, "object");
|
|
assert.deepEqual(schema.required, ["id", "reward", "phases"]);
|
|
assert.equal(schema.properties.id.defaultValue, "1");
|
|
assert.equal(schema.properties.reward.type, "object");
|
|
assert.deepEqual(schema.properties.reward.required, ["gold"]);
|
|
assert.equal(schema.properties.reward.properties.currency.enumValues[1], "gem");
|
|
assert.equal(schema.properties.phases.type, "array");
|
|
assert.equal(schema.properties.phases.items.type, "object");
|
|
assert.equal(schema.properties.phases.items.properties.wave.type, "integer");
|
|
});
|
|
|
|
test("parseTopLevelYaml should parse nested mappings and object arrays", () => {
|
|
const yaml = parseTopLevelYaml(`
|
|
id: 1
|
|
reward:
|
|
gold: 10
|
|
currency: coin
|
|
phases:
|
|
-
|
|
wave: 1
|
|
monsterId: slime
|
|
`);
|
|
|
|
assert.equal(yaml.kind, "object");
|
|
assert.equal(yaml.map.get("reward").kind, "object");
|
|
assert.equal(yaml.map.get("reward").map.get("currency").value, "coin");
|
|
assert.equal(yaml.map.get("phases").kind, "array");
|
|
assert.equal(yaml.map.get("phases").items[0].kind, "object");
|
|
assert.equal(yaml.map.get("phases").items[0].map.get("wave").value, "1");
|
|
});
|
|
|
|
test("validateParsedConfig should report missing and unknown nested properties", () => {
|
|
const schema = parseSchemaContent(`
|
|
{
|
|
"type": "object",
|
|
"required": ["reward"],
|
|
"properties": {
|
|
"reward": {
|
|
"type": "object",
|
|
"required": ["gold", "currency"],
|
|
"properties": {
|
|
"gold": { "type": "integer" },
|
|
"currency": { "type": "string" }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
`);
|
|
const yaml = parseTopLevelYaml(`
|
|
reward:
|
|
gold: 10
|
|
rarity: epic
|
|
`);
|
|
|
|
const diagnostics = validateParsedConfig(schema, yaml);
|
|
|
|
assert.equal(diagnostics.length, 2);
|
|
assert.match(diagnostics[0].message, /reward\.currency/u);
|
|
assert.match(diagnostics[1].message, /reward\.rarity/u);
|
|
});
|
|
|
|
test("validateParsedConfig should report object-array item issues", () => {
|
|
const schema = parseSchemaContent(`
|
|
{
|
|
"type": "object",
|
|
"properties": {
|
|
"phases": {
|
|
"type": "array",
|
|
"items": {
|
|
"type": "object",
|
|
"required": ["wave", "monsterId"],
|
|
"properties": {
|
|
"wave": { "type": "integer" },
|
|
"monsterId": { "type": "string" }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
`);
|
|
const yaml = parseTopLevelYaml(`
|
|
phases:
|
|
-
|
|
wave: 1
|
|
hpScale: 1.5
|
|
`);
|
|
|
|
const diagnostics = validateParsedConfig(schema, yaml);
|
|
|
|
assert.equal(diagnostics.length, 2);
|
|
assert.match(diagnostics[0].message, /phases\[0\]\.monsterId/u);
|
|
assert.match(diagnostics[1].message, /phases\[0\]\.hpScale/u);
|
|
});
|
|
|
|
test("validateParsedConfig should report deep enum mismatches", () => {
|
|
const schema = parseSchemaContent(`
|
|
{
|
|
"type": "object",
|
|
"properties": {
|
|
"reward": {
|
|
"type": "object",
|
|
"properties": {
|
|
"currency": {
|
|
"type": "string",
|
|
"enum": ["coin", "gem"]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
`);
|
|
const yaml = parseTopLevelYaml(`
|
|
reward:
|
|
currency: ticket
|
|
`);
|
|
|
|
const diagnostics = validateParsedConfig(schema, yaml);
|
|
|
|
assert.equal(diagnostics.length, 1);
|
|
assert.match(diagnostics[0].message, /coin, gem/u);
|
|
});
|
|
|
|
test("applyFormUpdates should update nested scalar and scalar-array paths", () => {
|
|
const updated = applyFormUpdates(
|
|
[
|
|
"id: 1",
|
|
"reward:",
|
|
" gold: 10",
|
|
"phases:",
|
|
" -",
|
|
" wave: 1"
|
|
].join("\n"),
|
|
{
|
|
scalars: {
|
|
"reward.currency": "coin",
|
|
name: "Slime"
|
|
},
|
|
arrays: {
|
|
dropItems: ["potion", "hi potion"]
|
|
}
|
|
});
|
|
|
|
assert.match(updated, /^name: Slime$/mu);
|
|
assert.match(updated, /^reward:$/mu);
|
|
assert.match(updated, /^ currency: coin$/mu);
|
|
assert.match(updated, /^dropItems:$/mu);
|
|
assert.match(updated, /^ - potion$/mu);
|
|
assert.match(updated, /^ - hi potion$/mu);
|
|
assert.match(updated, /^phases:$/mu);
|
|
});
|
|
|
|
test("applyScalarUpdates should preserve the scalar-only compatibility wrapper", () => {
|
|
const updated = applyScalarUpdates(
|
|
[
|
|
"id: 1",
|
|
"name: Slime"
|
|
].join("\n"),
|
|
{
|
|
name: "Goblin",
|
|
hp: "25"
|
|
});
|
|
|
|
assert.match(updated, /^name: Goblin$/mu);
|
|
assert.match(updated, /^hp: 25$/mu);
|
|
});
|
|
|
|
test("getEditableSchemaFields should keep batch editing limited to top-level scalar and scalar-array properties", () => {
|
|
const schema = parseSchemaContent(`
|
|
{
|
|
"type": "object",
|
|
"required": ["id", "dropItems"],
|
|
"properties": {
|
|
"id": { "type": "integer" },
|
|
"name": {
|
|
"type": "string",
|
|
"title": "Monster Name",
|
|
"description": "Display name."
|
|
},
|
|
"reward": {
|
|
"type": "object",
|
|
"properties": {
|
|
"gold": { "type": "integer" }
|
|
}
|
|
},
|
|
"dropItems": {
|
|
"type": "array",
|
|
"description": "Drop ids.",
|
|
"items": {
|
|
"type": "string",
|
|
"enum": ["potion", "bomb"]
|
|
}
|
|
},
|
|
"waypoints": {
|
|
"type": "array",
|
|
"items": {
|
|
"type": "object",
|
|
"properties": {
|
|
"x": { "type": "integer" }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
`);
|
|
|
|
assert.deepEqual(getEditableSchemaFields(schema), [
|
|
{
|
|
key: "dropItems",
|
|
path: "dropItems",
|
|
type: "array",
|
|
itemType: "string",
|
|
title: undefined,
|
|
description: "Drop ids.",
|
|
defaultValue: undefined,
|
|
itemEnumValues: ["potion", "bomb"],
|
|
refTable: undefined,
|
|
inputKind: "array",
|
|
required: true
|
|
},
|
|
{
|
|
key: "id",
|
|
path: "id",
|
|
type: "integer",
|
|
title: undefined,
|
|
description: undefined,
|
|
defaultValue: undefined,
|
|
enumValues: undefined,
|
|
refTable: undefined,
|
|
inputKind: "scalar",
|
|
required: true
|
|
},
|
|
{
|
|
key: "name",
|
|
path: "name",
|
|
type: "string",
|
|
title: "Monster Name",
|
|
description: "Display name.",
|
|
defaultValue: undefined,
|
|
enumValues: undefined,
|
|
refTable: undefined,
|
|
inputKind: "scalar",
|
|
required: false
|
|
}
|
|
]);
|
|
});
|
|
|
|
test("parseBatchArrayValue should keep comma-separated batch editing behavior", () => {
|
|
assert.deepEqual(parseBatchArrayValue(" potion, bomb , ,elixir "), ["potion", "bomb", "elixir"]);
|
|
});
|