// Copyright (c) 2025-2026 GeWuYou // SPDX-License-Identifier: Apache-2.0 const test = require("node:test"); const assert = require("node:assert/strict"); const {__test: extensionTest} = require("../src/extension"); const { applyFormUpdates, applyScalarUpdates, createSampleConfigYaml, extractYamlComments, 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("parseSchemaContent should capture const metadata for scalar, object, array, integer, and boolean nodes", () => { const schema = parseSchemaContent(` { "type": "object", "properties": { "rarity": { "type": "string", "const": "common" }, "reward": { "type": "object", "properties": { "gold": { "type": "integer" }, "items": { "type": "array", "items": { "type": "string" } } }, "const": { "gold": 100, "items": [ "potion", "sword" ] } }, "tags": { "type": "array", "const": ["daily", "quest"], "items": { "type": "string" } }, "maxAttempts": { "type": "integer", "const": 3 }, "allowRetry": { "type": "boolean", "const": true } } } `); const reorderedSchema = parseSchemaContent(` { "type": "object", "properties": { "reward": { "type": "object", "properties": { "gold": { "type": "integer" }, "items": { "type": "array", "items": { "type": "string" } } }, "const": { "items": [ "potion", "sword" ], "gold": 100 } } } } `); assert.equal(schema.properties.rarity.constValue, "common"); assert.equal(schema.properties.rarity.constDisplayValue, "\"common\""); assert.ok(schema.properties.rarity.constComparableValue); assert.equal(schema.properties.reward.constValue, "{\"gold\":100,\"items\":[\"potion\",\"sword\"]}"); assert.equal(schema.properties.reward.constDisplayValue, "{\"gold\":100,\"items\":[\"potion\",\"sword\"]}"); assert.equal( schema.properties.reward.constComparableValue, reorderedSchema.properties.reward.constComparableValue); assert.equal(schema.properties.tags.constValue, "[\"daily\",\"quest\"]"); assert.equal(schema.properties.tags.constDisplayValue, "[\"daily\",\"quest\"]"); assert.ok(schema.properties.tags.constComparableValue); assert.equal(schema.properties.maxAttempts.constValue, "3"); assert.equal(schema.properties.maxAttempts.constDisplayValue, "3"); assert.equal(schema.properties.maxAttempts.constComparableValue, "integer:1:3"); assert.equal(schema.properties.allowRetry.constValue, "true"); assert.equal(schema.properties.allowRetry.constDisplayValue, "true"); assert.equal(schema.properties.allowRetry.constComparableValue, "boolean:4:true"); }); test("parseSchemaContent should preserve empty-string const raw and display metadata", () => { const schema = parseSchemaContent(` { "type": "object", "properties": { "name": { "type": "string", "const": "" } } } `); assert.equal(schema.properties.name.constValue, ""); assert.equal(schema.properties.name.constDisplayValue, "\"\""); }); test("parseSchemaContent should reject unsupported oneOf combinators", () => { assert.throws( () => parseSchemaContent(` { "type": "object", "properties": { "reward": { "type": "object", "properties": { "itemCount": { "type": "integer" } }, "oneOf": [ { "type": "object", "required": ["itemCount"], "properties": { "itemCount": { "type": "integer" } } } ] } } } `), /unsupported combinator keyword 'oneOf'/u); }); test("parseSchemaContent should reject unsupported additionalProperties forms", () => { assert.throws( () => parseSchemaContent(` { "type": "object", "properties": { "reward": { "type": "object", "additionalProperties": true, "properties": { "itemCount": { "type": "integer" } } } } } `), /unsupported 'additionalProperties' metadata/u); }); test("parseSchemaContent should allow explicit additionalProperties false", () => { assert.doesNotThrow(() => parseSchemaContent(` { "type": "object", "properties": { "reward": { "type": "object", "additionalProperties": false, "properties": { "itemCount": { "type": "integer" } } } } } `)); }); test("parseSchemaContent should reject unsupported open-object keywords", () => { assert.throws( () => parseSchemaContent(` { "type": "object", "properties": { "reward": { "type": "object", "patternProperties": { "^dynamic-": { "type": "integer" } }, "properties": { "itemCount": { "type": "integer" } } } } } `), /unsupported 'patternProperties' metadata/u); assert.throws( () => parseSchemaContent(` { "type": "object", "properties": { "reward": { "type": "object", "propertyNames": { "pattern": "^[a-z]+$" }, "properties": { "itemCount": { "type": "integer" } } } } } `), /unsupported 'propertyNames' metadata/u); assert.throws( () => parseSchemaContent(` { "type": "object", "properties": { "reward": { "type": "object", "unevaluatedProperties": false, "properties": { "itemCount": { "type": "integer" } } } } } `), /unsupported 'unevaluatedProperties' metadata/u); }); test("parseSchemaContent should reject unsupported explicit schema types", () => { assert.throws( () => parseSchemaContent(` { "type": "object", "properties": { "reward": { "type": "bogus" } } } `), /declares unsupported type 'bogus'/u); }); test("parseSchemaContent should build object const comparable keys with ordinal property ordering", () => { const schema = parseSchemaContent(` { "type": "object", "properties": { "payload": { "type": "object", "properties": { "z": { "type": "integer" }, "ä": { "type": "integer" } }, "const": { "z": 1, "ä": 2 } } } } `); assert.match(schema.properties.payload.constComparableValue, /^1:z=/u); }); 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("parseTopLevelYaml should keep complex mapping keys", () => { const yaml = parseTopLevelYaml(` my-key: slime "complex key": value root: item.id: potion `); assert.equal(yaml.kind, "object"); assert.equal(yaml.map.get("my-key").value, "slime"); assert.equal(yaml.map.get("complex key").value, "value"); assert.equal(yaml.map.get("root").map.get("item.id").value, "potion"); }); 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("validateParsedConfig should report scalar const mismatches", () => { const schema = parseSchemaContent(` { "type": "object", "properties": { "rarity": { "type": "string", "const": "common" } } } `); const yaml = parseTopLevelYaml(` rarity: rare `); const diagnostics = validateParsedConfig(schema, yaml); assert.equal(diagnostics.length, 1); assert.match(diagnostics[0].message, /constant value "common"|固定值 "common"/u); }); test("validateParsedConfig should accept scalar, object, array, integer, and boolean const matches", () => { const schema = parseSchemaContent(` { "type": "object", "properties": { "rarity": { "type": "string", "const": "common" }, "reward": { "type": "object", "properties": { "gold": { "type": "integer" }, "items": { "type": "array", "items": { "type": "string" } } }, "const": { "gold": 100, "items": [ "potion", "sword" ] } }, "tags": { "type": "array", "items": { "type": "string" }, "const": [ "daily", "quest" ] }, "maxAttempts": { "type": "integer", "const": 3 }, "allowRetry": { "type": "boolean", "const": true } } } `); const yaml = parseTopLevelYaml(` rarity: common reward: gold: 100 items: - potion - sword tags: - daily - quest maxAttempts: 3 allowRetry: true `); assert.deepEqual(validateParsedConfig(schema, yaml), []); }); test("validateParsedConfig should normalize object const comparisons but keep array const order", () => { const schema = parseSchemaContent(` { "type": "object", "properties": { "reward": { "type": "object", "properties": { "gold": { "type": "integer" }, "items": { "type": "array", "items": { "type": "string" } } }, "const": { "gold": 100, "items": [ "potion", "sword" ] } }, "tags": { "type": "array", "items": { "type": "string" }, "const": [ "daily", "quest" ] } } } `); const normalizedYaml = parseTopLevelYaml(` reward: items: - potion - sword gold: 100 tags: - daily - quest `); const arrayOrderMismatchYaml = parseTopLevelYaml(` reward: items: - potion - sword gold: 100 tags: - quest - daily `); assert.deepEqual(validateParsedConfig(schema, normalizedYaml), []); const diagnostics = validateParsedConfig(schema, arrayOrderMismatchYaml); assert.equal(diagnostics.length, 1); assert.match(diagnostics[0].message, /tags/u); assert.match(diagnostics[0].message, /constant value \["daily","quest"\]|固定值 \["daily","quest"\]/u); }); test("validateParsedConfig should report object and array const mismatches", () => { const schema = parseSchemaContent(` { "type": "object", "properties": { "reward": { "type": "object", "properties": { "gold": { "type": "integer" }, "currency": { "type": "string" } }, "const": { "gold": 10, "currency": "coin" } }, "dropItemIds": { "type": "array", "const": ["potion", "gem"], "items": { "type": "string" } } } } `); const yaml = parseTopLevelYaml(` reward: gold: 10 currency: gem dropItemIds: - gem - potion `); const diagnostics = validateParsedConfig(schema, yaml); assert.equal(diagnostics.length, 2); assert.match(diagnostics[0].message, /reward/u); assert.match(diagnostics[1].message, /dropItemIds/u); }); test("validateParsedConfig should cover integer and boolean const scalar normalization and mismatches", () => { const schema = parseSchemaContent(` { "type": "object", "properties": { "maxAttempts": { "type": "integer", "const": 3 }, "allowRetry": { "type": "boolean", "const": true } } } `); const normalizedYaml = parseTopLevelYaml(` maxAttempts: "3" allowRetry: "true" `); const integerMismatchYaml = parseTopLevelYaml(` maxAttempts: 3.5 allowRetry: true `); const booleanConstMismatchYaml = parseTopLevelYaml(` maxAttempts: 3 allowRetry: false `); const booleanTypeMismatchYaml = parseTopLevelYaml(` maxAttempts: 3 allowRetry: 0 `); assert.deepEqual(validateParsedConfig(schema, normalizedYaml), []); const integerDiagnostics = validateParsedConfig(schema, integerMismatchYaml); assert.equal(integerDiagnostics.length, 1); assert.match(integerDiagnostics[0].message, /maxAttempts/u); assert.match(integerDiagnostics[0].message, /integer|整数/u); const booleanConstDiagnostics = validateParsedConfig(schema, booleanConstMismatchYaml); assert.equal(booleanConstDiagnostics.length, 1); assert.match(booleanConstDiagnostics[0].message, /allowRetry/u); assert.match(booleanConstDiagnostics[0].message, /constant value true|固定值 true/u); const booleanTypeDiagnostics = validateParsedConfig(schema, booleanTypeMismatchYaml); assert.equal(booleanTypeDiagnostics.length, 1); assert.match(booleanTypeDiagnostics[0].message, /allowRetry/u); assert.match(booleanTypeDiagnostics[0].message, /boolean|布尔/u); }); test("validateParsedConfig should report numeric range and string length mismatches", () => { const schema = parseSchemaContent(` { "type": "object", "properties": { "name": { "type": "string", "minLength": 3, "maxLength": 8 }, "hp": { "type": "integer", "minimum": 1, "maximum": 10 }, "tags": { "type": "array", "items": { "type": "string", "maxLength": 4 } } } } `); const yaml = parseTopLevelYaml(` name: Sl hp: 12 tags: - safe - shield `); const diagnostics = validateParsedConfig(schema, yaml); assert.equal(diagnostics.length, 3); assert.match(diagnostics[0].message, /at least 3 characters|至少为 3 个字符/u); assert.match(diagnostics[1].message, /less than or equal to 10|小于或等于 10/u); assert.match(diagnostics[2].message, /tags\[1\]|shield/u); }); test("validateParsedConfig should report exclusive bounds, pattern, and array item-count mismatches", () => { const schema = parseSchemaContent(` { "type": "object", "properties": { "name": { "type": "string", "pattern": "^[A-Z][a-z]+$" }, "hp": { "type": "integer", "exclusiveMinimum": 10, "exclusiveMaximum": 20 }, "tags": { "type": "array", "minItems": 2, "maxItems": 3, "items": { "type": "string" } } } } `); const yaml = parseTopLevelYaml(` name: slime hp: 10 tags: - onlyOne `); const diagnostics = validateParsedConfig(schema, yaml); assert.equal(diagnostics.length, 3); assert.match(diagnostics[0].message, /pattern|正则模式/u); assert.match(diagnostics[1].message, /greater than 10|大于 10/u); assert.match(diagnostics[2].message, /at least 2 items|至少需要包含 2 个元素/u); }); test("validateParsedConfig should enforce supported string formats", () => { const schema = parseSchemaContent(` { "type": "object", "properties": { "releaseDate": { "type": "string", "format": "date" }, "ancientReleaseDate": { "type": "string", "format": "date" }, "publishedAt": { "type": "string", "format": "date-time" }, "respawnDelay": { "type": "string", "format": "duration" }, "contactEmail": { "type": "string", "format": "email" }, "dailyResetAt": { "type": "string", "format": "time" }, "catalogUri": { "type": "string", "format": "uri" }, "configId": { "type": "string", "format": "uuid" } } } `); const yaml = parseTopLevelYaml(` releaseDate: 2026-02-30 ancientReleaseDate: 0000-01-01 publishedAt: 2026-04-11T08:30:00 respawnDelay: P1Y contactEmail: boss.example.com dailyResetAt: 08:30:00 catalogUri: /loot-table configId: 123e4567e89b12d3a456426614174000 `); const diagnostics = validateParsedConfig(schema, yaml); assert.equal(diagnostics.length, 8); assert.match(diagnostics[0].message, /format 'date'|字符串格式“date”/u); assert.match(diagnostics[1].message, /format 'date'|字符串格式“date”/u); assert.match(diagnostics[2].message, /format 'date-time'|字符串格式“date-time”/u); assert.match(diagnostics[3].message, /format 'duration'|字符串格式“duration”/u); assert.match(diagnostics[4].message, /format 'email'|字符串格式“email”/u); assert.match(diagnostics[5].message, /format 'time'|字符串格式“time”/u); assert.match(diagnostics[6].message, /format 'uri'|字符串格式“uri”/u); assert.match(diagnostics[7].message, /format 'uuid'|字符串格式“uuid”/u); }); test("validateParsedConfig should accept supported string formats", () => { const schema = parseSchemaContent(` { "type": "object", "properties": { "releaseDate": { "type": "string", "format": "date" }, "publishedAt": { "type": "string", "format": "date-time" }, "respawnDelay": { "type": "string", "format": "duration" }, "contactEmail": { "type": "string", "format": "email" }, "dailyResetAt": { "type": "string", "format": "time" }, "catalogUri": { "type": "string", "format": "uri" }, "configId": { "type": "string", "format": "uuid" } } } `); const yaml = parseTopLevelYaml(` releaseDate: 2026-04-11 publishedAt: 2026-04-11T08:30:00Z respawnDelay: P2DT3H4M5.5S contactEmail: boss@example.com dailyResetAt: 08:30:00Z catalogUri: https://example.com/loot-table configId: 123e4567-e89b-12d3-a456-426614174000 `); assert.deepEqual(validateParsedConfig(schema, yaml), []); }); test("validateParsedConfig should report exclusive maximum and maxItems violations", () => { const schema = parseSchemaContent(` { "type": "object", "properties": { "hp": { "type": "integer", "exclusiveMinimum": 10, "exclusiveMaximum": 20 }, "tags": { "type": "array", "minItems": 1, "maxItems": 3, "items": { "type": "string" } } } } `); const yaml = parseTopLevelYaml(` hp: 20 tags: - a - b - c - d `); const diagnostics = validateParsedConfig(schema, yaml); assert.equal(diagnostics.length, 2); assert.match(diagnostics[0].message, /less than 20|小于 20/u); assert.match(diagnostics[1].message, /at most 3 items|最多只能包含 3 个元素/u); }); test("validateParsedConfig should report object property-count mismatches", () => { const schema = parseSchemaContent(` { "type": "object", "minProperties": 2, "maxProperties": 3, "properties": { "reward": { "type": "object", "minProperties": 2, "maxProperties": 2, "properties": { "gold": { "type": "integer" }, "currency": { "type": "string" }, "tier": { "type": "string" } } } } } `); const yaml = parseTopLevelYaml(` reward: gold: 10 currency: coin tier: epic `); const diagnostics = validateParsedConfig(schema, yaml); const messages = diagnostics.map((diagnostic) => diagnostic.message); assert.equal(diagnostics.length, 2); assert.ok(messages.some((message) => /at least 2 properties|至少需要包含 2 个属性/u.test(message))); assert.ok(messages.some((message) => /reward.*at most 2 properties|reward.*最多只能包含 2 个子属性/u.test(message))); }); test("validateParsedConfig should count unique object properties for property-count constraints", () => { const schema = parseSchemaContent(` { "type": "object", "properties": { "reward": { "type": "object", "minProperties": 2, "properties": { "gold": { "type": "integer" }, "currency": { "type": "string" } } } } } `); const yaml = parseTopLevelYaml(` reward: gold: 10 gold: 20 `); const diagnostics = validateParsedConfig(schema, yaml); assert.equal(diagnostics.length, 1); assert.match(diagnostics[0].message, /reward.*at least 2 properties|reward.*至少需要包含 2 个子属性/u); }); test("validateParsedConfig should report multipleOf and uniqueItems violations", () => { const schema = parseSchemaContent(` { "type": "object", "properties": { "hp": { "type": "integer", "multipleOf": 5 }, "phases": { "type": "array", "uniqueItems": true, "items": { "type": "object", "properties": { "wave": { "type": "integer" }, "monsterId": { "type": "string" } } } } } } `); const yaml = parseTopLevelYaml(` hp: 12 phases: - wave: 1 monsterId: slime - monsterId: slime wave: 1 `); const diagnostics = validateParsedConfig(schema, yaml); assert.equal(diagnostics.length, 2); assert.match(diagnostics[0].message, /multiple of 5|5 的整数倍/u); assert.match(diagnostics[1].message, /phases\[1\]|uniqueItems|元素唯一/u); }); test("validateParsedConfig should report contains match-count violations", () => { const schema = parseSchemaContent(` { "type": "object", "properties": { "dropRates": { "type": "array", "minContains": 2, "contains": { "type": "integer", "const": 5 }, "items": { "type": "integer" } } } } `); const yaml = parseTopLevelYaml(` dropRates: - 5 - 7 - 9 `); const diagnostics = validateParsedConfig(schema, yaml); assert.equal(diagnostics.length, 1); assert.match(diagnostics[0].message, /at least 2 items matching the 'contains' schema|至少需要包含 2 个匹配 contains 条件的元素/u); }); test("validateParsedConfig should skip contains match-count when items are structurally invalid", () => { const schema = parseSchemaContent(` { "type": "object", "required": ["dropRates"], "properties": { "dropRates": { "type": "array", "minContains": 2, "contains": { "type": "object", "required": ["type"], "properties": { "type": { "type": "string", "const": "RARE" } } }, "items": { "type": "object", "required": ["type", "value"], "properties": { "type": { "type": "string" }, "value": { "type": "integer" } } } } } } `); const yaml = parseTopLevelYaml(` dropRates: - type: RARE value: "not-a-number" - type: RARE value: 10 `); const diagnostics = validateParsedConfig(schema, yaml); assert.ok(diagnostics.length > 0); assert.match( diagnostics[0].message, /dropRates\[0\]\.value/u); assert.match( diagnostics[0].message, /integer|整数/u); assert.equal( diagnostics.some((diagnostic) => /at least 2 items matching the 'contains' schema|至少需要包含 2 个匹配 contains 条件的元素/u.test(diagnostic.message)), false); assert.equal( diagnostics.some((diagnostic) => /at most \d+ items matching the 'contains' schema|最多只能包含 \d+ 个匹配 contains 条件的元素/u.test(diagnostic.message)), false); }); test("validateParsedConfig should continue contains match-count when items only have value-level violations", () => { const schema = parseSchemaContent(` { "type": "object", "properties": { "dropRates": { "type": "array", "minContains": 1, "contains": { "type": "integer", "const": 7 }, "items": { "type": "integer", "minimum": 10 } } } } `); const yaml = parseTopLevelYaml(` dropRates: - 5 `); const diagnostics = validateParsedConfig(schema, yaml); assert.equal(diagnostics.length, 2); assert.match(diagnostics[0].message, /greater than or equal to 10|大于或等于 10/u); assert.match(diagnostics[1].message, /at least 1 items matching the 'contains' schema|至少需要包含 1 个匹配 contains 条件的元素/u); }); test("validateParsedConfig should report maxContains violations", () => { const schema = parseSchemaContent(` { "type": "object", "properties": { "dropRates": { "type": "array", "maxContains": 1, "contains": { "type": "integer", "const": 5 }, "items": { "type": "integer" } } } } `); const yaml = parseTopLevelYaml(` dropRates: - 5 - 5 - 7 `); const diagnostics = validateParsedConfig(schema, yaml); assert.equal(diagnostics.length, 1); assert.match(diagnostics[0].message, /at most 1 items matching the 'contains' schema|最多只能包含 1 个匹配 contains 条件的元素/u); }); test("validateParsedConfig should accept satisfied contains constraints", () => { const schemaWithRange = parseSchemaContent(` { "type": "object", "properties": { "dropRates": { "type": "array", "minContains": 2, "maxContains": 3, "contains": { "type": "integer", "const": 5 }, "items": { "type": "integer" } } } } `); const yamlWithinRange = parseTopLevelYaml(` dropRates: - 0 - 5 - 5 - 10 `); assert.deepEqual(validateParsedConfig(schemaWithRange, yamlWithinRange), []); const schemaWithDefaultMinContains = parseSchemaContent(` { "type": "object", "properties": { "dropRates": { "type": "array", "contains": { "type": "integer", "const": 5 }, "items": { "type": "integer" } } } } `); const yamlSatisfyingDefaultMinContains = parseTopLevelYaml(` dropRates: - 1 - 2 - 5 - 3 `); assert.deepEqual(validateParsedConfig(schemaWithDefaultMinContains, yamlSatisfyingDefaultMinContains), []); }); test("validateParsedConfig should allow object contains matches with additional declared item fields", () => { const schema = parseSchemaContent(` { "type": "object", "properties": { "entries": { "type": "array", "minContains": 1, "contains": { "type": "object", "required": ["id"], "properties": { "id": { "type": "string", "const": "boss" } } }, "items": { "type": "object", "required": ["id", "weight"], "properties": { "id": { "type": "string" }, "weight": { "type": "integer" } } } } } } `); const yaml = parseTopLevelYaml(` entries: - id: boss weight: 10 - id: slime weight: 3 `); assert.deepEqual(validateParsedConfig(schema, yaml), []); }); test("validateParsedConfig should accept large decimal multiples without floating-point drift", () => { const schema = parseSchemaContent(` { "type": "object", "properties": { "dropRate": { "type": "number", "multipleOf": 0.1 } } } `); const yaml = parseTopLevelYaml(` dropRate: 10000000.2 `); assert.deepEqual(validateParsedConfig(schema, yaml), []); }); test("validateParsedConfig should reject large numbers that are not actually multiples", () => { const schema = parseSchemaContent(` { "type": "object", "properties": { "dropRate": { "type": "number", "multipleOf": 1 } } } `); const yaml = parseTopLevelYaml(` dropRate: 1000000000000.4 `); const diagnostics = validateParsedConfig(schema, yaml); assert.equal(diagnostics.length, 1); assert.match(diagnostics[0].message, /multiple of 1|1 的整数倍/u); }); test("validateParsedConfig should accept scientific-notation numbers", () => { const schema = parseSchemaContent(` { "type": "object", "properties": { "dropRate": { "type": "number" } } } `); const yaml = parseTopLevelYaml(` dropRate: 1.5e10 `); assert.deepEqual(validateParsedConfig(schema, yaml), []); }); test("validateParsedConfig should apply schema patterns with Unicode semantics", () => { const schema = parseSchemaContent(` { "type": "object", "properties": { "name": { "type": "string", "pattern": "^\\\\p{L}+$" } } } `); const yaml = parseTopLevelYaml(` name: 测试 `); assert.deepEqual(validateParsedConfig(schema, yaml), []); }); test("validateParsedConfig should skip uniqueItems checks for invalid array items", () => { const schema = parseSchemaContent(` { "type": "object", "properties": { "values": { "type": "array", "uniqueItems": true, "items": { "type": "integer" } } } } `); const yaml = parseTopLevelYaml(` values: - id: 1 - id: 2 `); const diagnostics = validateParsedConfig(schema, yaml); assert.equal(diagnostics.length, 2); assert.match(diagnostics[0].message, /values\[0\]/u); assert.match(diagnostics[1].message, /values\[1\]/u); assert.ok(diagnostics.every((diagnostic) => !/uniqueItems|元素唯一/u.test(diagnostic.message))); }); test("validateParsedConfig should report every uniqueItems duplicate in one pass", () => { const schema = parseSchemaContent(` { "type": "object", "properties": { "tags": { "type": "array", "uniqueItems": true, "items": { "type": "string" } } } } `); const yaml = parseTopLevelYaml(` tags: - alpha - beta - alpha - beta `); const diagnostics = validateParsedConfig(schema, yaml); assert.equal(diagnostics.length, 2); assert.match(diagnostics[0].message, /tags\[2\]/u); assert.match(diagnostics[1].message, /tags\[3\]/u); }); test("validateParsedConfig should avoid uniqueItems comparable-key collisions for distinct objects", () => { const schema = parseSchemaContent(` { "type": "object", "properties": { "entries": { "type": "array", "uniqueItems": true, "items": { "type": "object", "properties": { "a": { "type": "string" }, "b": { "type": "string" } } } } } } `); const yaml = parseTopLevelYaml(` entries: - a: "x|1:b=string:yz" - a: x b: yz `); assert.deepEqual(validateParsedConfig(schema, yaml), []); }); test("parseSchemaContent should capture scalar range and length metadata", () => { const schema = parseSchemaContent(` { "type": "object", "properties": { "name": { "type": "string", "minLength": 3, "maxLength": 12 }, "hp": { "type": "integer", "minimum": 1, "maximum": 99 }, "tags": { "type": "array", "items": { "type": "string", "minLength": 2, "maxLength": 6 } } } } `); assert.equal(schema.properties.name.minLength, 3); assert.equal(schema.properties.name.maxLength, 12); assert.equal(schema.properties.hp.minimum, 1); assert.equal(schema.properties.hp.maximum, 99); assert.equal(schema.properties.tags.items.minLength, 2); assert.equal(schema.properties.tags.items.maxLength, 6); }); test("parseSchemaContent should capture exclusive bounds, pattern, and array item-count metadata", () => { const schema = parseSchemaContent(` { "type": "object", "properties": { "name": { "type": "string", "pattern": "^[A-Z][a-z]+$" }, "hp": { "type": "integer", "exclusiveMinimum": 1, "exclusiveMaximum": 99 }, "tags": { "type": "array", "minItems": 2, "maxItems": 4, "items": { "type": "string", "pattern": "^[a-z]+$" } } } } `); assert.equal(schema.properties.name.pattern, "^[A-Z][a-z]+$"); assert.equal(schema.properties.hp.exclusiveMinimum, 1); assert.equal(schema.properties.hp.exclusiveMaximum, 99); assert.equal(schema.properties.tags.minItems, 2); assert.equal(schema.properties.tags.maxItems, 4); assert.equal(schema.properties.tags.items.pattern, "^[a-z]+$"); }); test("parseSchemaContent should capture supported string format metadata", () => { const schema = parseSchemaContent(` { "type": "object", "properties": { "contactEmail": { "type": "string", "format": "email" }, "aliases": { "type": "array", "items": { "type": "string", "format": "duration" } } } } `); assert.equal(schema.properties.contactEmail.format, "email"); assert.equal(schema.properties.aliases.items.format, "duration"); }); test("parseSchemaContent should capture multipleOf and uniqueItems metadata", () => { const schema = parseSchemaContent(` { "type": "object", "properties": { "hp": { "type": "integer", "multipleOf": 5 }, "dropRates": { "type": "array", "uniqueItems": true, "items": { "type": "number", "multipleOf": 0.5 } } } } `); assert.equal(schema.properties.hp.multipleOf, 5); assert.equal(schema.properties.dropRates.uniqueItems, true); assert.equal(schema.properties.dropRates.items.multipleOf, 0.5); }); test("parseSchemaContent should capture contains metadata", () => { const schema = parseSchemaContent(` { "type": "object", "properties": { "dropRates": { "type": "array", "minContains": 1, "maxContains": 2, "contains": { "type": "integer", "const": 5 }, "items": { "type": "integer" } } } } `); assert.equal(schema.properties.dropRates.minContains, 1); assert.equal(schema.properties.dropRates.maxContains, 2); assert.equal(schema.properties.dropRates.contains.type, "integer"); assert.equal(schema.properties.dropRates.contains.constDisplayValue, "5"); }); test("parseSchemaContent should reject nested-array contains schemas", () => { assert.throws( () => parseSchemaContent(` { "type": "object", "properties": { "dropRates": { "type": "array", "contains": { "type": "array", "items": { "type": "integer" } }, "items": { "type": "integer" } } } } `), /unsupported nested array 'contains' schemas/u); }); test("parseSchemaContent should reject array items without an explicit typed object schema", () => { assert.throws( () => parseSchemaContent(` { "type": "object", "properties": { "dropRates": { "type": "array", "items": [ { "type": "integer" } ] } } } `), /must declare 'items' as an object-valued schema with an explicit 'type'/u); }); test("parseSchemaContent should reject contains without an explicit typed object schema", () => { assert.throws( () => parseSchemaContent(` { "type": "object", "properties": { "dropRates": { "type": "array", "contains": { "const": 5 }, "items": { "type": "integer" } } } } `), /must declare 'contains' as an object-valued schema with an explicit 'type'/u); }); test("parseSchemaContent should reject minContains and maxContains without contains", () => { assert.throws( () => parseSchemaContent(` { "type": "object", "properties": { "dropRates": { "type": "array", "minContains": 1, "items": { "type": "integer" } } } } `), /'minContains' or 'maxContains' without 'contains'/u); assert.throws( () => parseSchemaContent(` { "type": "object", "properties": { "dropRates": { "type": "array", "maxContains": 1, "items": { "type": "integer" } } } } `), /'minContains' or 'maxContains' without 'contains'/u); }); test("parseSchemaContent should reject contains schemas where default minContains exceeds maxContains", () => { assert.throws( () => parseSchemaContent(` { "type": "object", "properties": { "dropRates": { "type": "array", "maxContains": 0, "contains": { "type": "integer", "const": 5 }, "items": { "type": "integer" } } } } `), /'minContains' greater than 'maxContains'/u); }); test("parseSchemaContent should reject contains schemas where minContains is greater than maxContains", () => { assert.throws( () => parseSchemaContent(` { "type": "object", "properties": { "dropRates": { "type": "array", "minContains": 3, "maxContains": 1, "contains": { "type": "integer", "const": 5 }, "items": { "type": "integer" } } } } `), /'minContains' greater than 'maxContains'/u); }); test("parseSchemaContent should capture object property-count metadata", () => { const schema = parseSchemaContent(` { "type": "object", "minProperties": 2, "maxProperties": 4, "properties": { "reward": { "type": "object", "minProperties": 1, "maxProperties": 2, "properties": { "gold": { "type": "integer" } } } } } `); assert.equal(schema.minProperties, 2); assert.equal(schema.maxProperties, 4); assert.equal(schema.properties.reward.minProperties, 1); assert.equal(schema.properties.reward.maxProperties, 2); }); test("parseSchemaContent should capture dependentRequired metadata", () => { const schema = parseSchemaContent(` { "type": "object", "properties": { "reward": { "type": "object", "properties": { "itemId": { "type": "string" }, "itemCount": { "type": "integer" } }, "dependentRequired": { "itemId": ["itemCount"] } } } } `); assert.deepEqual(schema.properties.reward.dependentRequired, { itemId: ["itemCount"] }); }); test("parseSchemaContent should reject non-object dependentRequired declarations", () => { assert.throws( () => parseSchemaContent(` { "type": "object", "properties": { "reward": { "type": "object", "properties": { "itemId": { "type": "string" }, "itemCount": { "type": "integer" } }, "dependentRequired": ["itemId"] } } } `), /must declare 'dependentRequired' as an object/u ); }); test("parseSchemaContent should reject dependentRequired targets outside the same object schema", () => { assert.throws( () => parseSchemaContent(` { "type": "object", "properties": { "reward": { "type": "object", "properties": { "itemId": { "type": "string" } }, "dependentRequired": { "itemId": ["itemCount"] } } } } `), /dependentRequired' target 'itemCount'/u ); }); test("parseSchemaContent should capture dependentSchemas metadata", () => { const schema = parseSchemaContent(` { "type": "object", "properties": { "reward": { "type": "object", "properties": { "itemId": { "type": "string" }, "itemCount": { "type": "integer" } }, "dependentSchemas": { "itemId": { "type": "object", "required": ["itemCount"], "properties": { "itemId": { "type": "string" }, "itemCount": { "type": "integer" } } } } } } } `); assert.equal(schema.properties.reward.dependentSchemas.itemId.type, "object"); assert.deepEqual(schema.properties.reward.dependentSchemas.itemId.required, ["itemCount"]); }); test("parseSchemaContent should reject non-object dependentSchemas declarations", () => { assert.throws( () => parseSchemaContent(` { "type": "object", "properties": { "reward": { "type": "object", "properties": { "itemId": { "type": "string" }, "itemCount": { "type": "integer" } }, "dependentSchemas": ["itemId"] } } } `), /must declare 'dependentSchemas' as an object/u ); }); test("parseSchemaContent should reject dependentSchemas triggers outside the same object schema", () => { assert.throws( () => parseSchemaContent(` { "type": "object", "properties": { "reward": { "type": "object", "properties": { "itemId": { "type": "string" } }, "dependentSchemas": { "itemCount": { "type": "object", "properties": {} } } } } } `), /dependentSchemas' for undeclared property 'itemCount'/u ); }); test("parseSchemaContent should reject non-object-typed dependentSchemas sub-schemas", () => { assert.throws( () => parseSchemaContent(` { "type": "object", "properties": { "reward": { "type": "object", "properties": { "itemId": { "type": "string" } }, "dependentSchemas": { "itemId": { "type": "string", "const": "potion" } } } } } `), /object-typed 'dependentSchemas' schema/u ); }); test("parseSchemaContent should capture allOf metadata", () => { const schema = parseSchemaContent(` { "type": "object", "properties": { "reward": { "type": "object", "properties": { "itemId": { "type": "string" }, "itemCount": { "type": "integer" } }, "allOf": [ { "type": "object", "required": ["itemCount"], "properties": { "itemCount": { "type": "integer" } } } ] } } } `); assert.equal(schema.properties.reward.allOf[0].type, "object"); assert.deepEqual(schema.properties.reward.allOf[0].required, ["itemCount"]); }); test("parseSchemaContent should reject allOf declarations on non-object schemas", () => { assert.throws( () => parseSchemaContent(` { "type": "object", "properties": { "tag": { "type": "string", "allOf": [ { "type": "object", "properties": {} } ] } } } `), /Only object schemas can declare 'allOf'/u ); }); test("parseSchemaContent should reject non-array allOf declarations", () => { assert.throws( () => parseSchemaContent(` { "type": "object", "properties": { "reward": { "type": "object", "properties": { "itemCount": { "type": "integer" } }, "allOf": { "type": "object", "properties": { "itemCount": { "type": "integer" } } } } } } `), /must declare 'allOf' as an array/u ); }); test("parseSchemaContent should reject non-object-typed allOf sub-schemas", () => { assert.throws( () => parseSchemaContent(` { "type": "object", "properties": { "reward": { "type": "object", "properties": { "itemCount": { "type": "integer" } }, "allOf": [ { "type": "string", "const": "potion" } ] } } } `), /object-typed schemas in 'allOf' entry #1/u ); }); test("parseSchemaContent should reject allOf entries that introduce undeclared parent properties", () => { assert.throws( () => parseSchemaContent(` { "type": "object", "properties": { "reward": { "type": "object", "properties": { "itemCount": { "type": "integer" } }, "allOf": [ { "type": "object", "required": ["bonus"] } ] } } } `), /requires property 'bonus' in 'allOf' entry #1/u ); }); test("parseSchemaContent should use runtime-aligned allOf paths for nested schema errors", () => { assert.throws( () => parseSchemaContent(` { "type": "object", "properties": { "reward": { "type": "object", "properties": { "itemCount": { "type": "integer" } }, "allOf": [ { "type": "object", "properties": { "itemCount": { "type": "integer", "format": "uuid" } } } ] } } } `), /reward\[allOf\[0\]\]\.itemCount/u ); }); test("parseSchemaContent should capture not sub-schema metadata", () => { const schema = parseSchemaContent(` { "type": "object", "properties": { "name": { "type": "string", "not": { "type": "string", "const": "Deprecated" } } } } `); assert.equal(schema.properties.name.not.type, "string"); assert.equal(schema.properties.name.not.constDisplayValue, "\"Deprecated\""); }); test("validateParsedConfig should report missing dependentRequired siblings", () => { const schema = parseSchemaContent(` { "type": "object", "properties": { "reward": { "type": "object", "properties": { "itemId": { "type": "string" }, "itemCount": { "type": "integer" } }, "dependentRequired": { "itemId": ["itemCount"] } } } } `); const yaml = parseTopLevelYaml(` reward: itemId: potion `); const diagnostics = validateParsedConfig(schema, yaml); assert.equal(diagnostics.length, 1); assert.equal(diagnostics[0].severity, "error"); assert.match(diagnostics[0].message, /reward\.itemCount/u); assert.match(diagnostics[0].message, /reward\.itemId/u); }); test("validateParsedConfig should accept missing dependentRequired targets when the trigger is absent", () => { const schema = parseSchemaContent(` { "type": "object", "properties": { "reward": { "type": "object", "properties": { "itemId": { "type": "string" }, "itemCount": { "type": "integer" } }, "dependentRequired": { "itemId": ["itemCount"] } } } } `); const yaml = parseTopLevelYaml(` reward: itemCount: 3 `); assert.deepEqual(validateParsedConfig(schema, yaml), []); }); test("validateParsedConfig should report dependentSchemas violations", () => { const schema = parseSchemaContent(` { "type": "object", "properties": { "reward": { "type": "object", "properties": { "itemId": { "type": "string" }, "itemCount": { "type": "integer" } }, "dependentSchemas": { "itemId": { "type": "object", "required": ["itemCount"], "properties": { "itemId": { "type": "string" }, "itemCount": { "type": "integer" } } } } } } } `); const yaml = parseTopLevelYaml(` reward: itemId: potion `); const diagnostics = validateParsedConfig(schema, yaml); assert.equal(diagnostics.length, 1); assert.equal(diagnostics[0].severity, "error"); assert.match(diagnostics[0].message, /dependent schema/u); assert.match(diagnostics[0].message, /reward\.itemId/u); }); test("validateParsedConfig should accept satisfied dependentSchemas", () => { const schema = parseSchemaContent(` { "type": "object", "properties": { "reward": { "type": "object", "properties": { "itemId": { "type": "string" }, "itemCount": { "type": "integer" }, "bonus": { "type": "integer" } }, "dependentSchemas": { "itemId": { "type": "object", "required": ["itemCount"], "properties": { "itemCount": { "type": "integer" } } } } } } } `); const yaml = parseTopLevelYaml(` reward: itemId: potion itemCount: 3 bonus: 1 `); assert.deepEqual(validateParsedConfig(schema, yaml), []); }); test("validateParsedConfig should report allOf violations", () => { const schema = parseSchemaContent(` { "type": "object", "properties": { "reward": { "type": "object", "properties": { "itemId": { "type": "string" }, "itemCount": { "type": "integer" } }, "allOf": [ { "type": "object", "required": ["itemCount"], "properties": { "itemCount": { "type": "integer" } } } ] } } } `); const yaml = parseTopLevelYaml(` reward: itemId: potion `); const diagnostics = validateParsedConfig(schema, yaml); assert.equal(diagnostics.length, 1); assert.equal(diagnostics[0].severity, "error"); assert.match(diagnostics[0].message, /allOf/u); assert.match(diagnostics[0].message, /#1|第 1 项/u); }); test("validateParsedConfig should accept satisfied allOf constraints", () => { const schema = parseSchemaContent(` { "type": "object", "properties": { "reward": { "type": "object", "properties": { "itemId": { "type": "string" }, "itemCount": { "type": "integer" }, "bonus": { "type": "integer" } }, "allOf": [ { "type": "object", "required": ["itemCount"], "properties": { "itemCount": { "type": "integer" } } }, { "type": "object", "properties": { "itemCount": { "type": "integer", "minimum": 2 } } } ] } } } `); const yaml = parseTopLevelYaml(` reward: itemId: potion itemCount: 3 bonus: 1 `); assert.deepEqual(validateParsedConfig(schema, yaml), []); }); test("parseSchemaContent should reject non-object not declarations", () => { assert.throws( () => parseSchemaContent(` { "type": "object", "properties": { "name": { "type": "string", "not": "deprecated" } } } `), /must declare 'not' as an object-valued schema/u ); }); test("parseSchemaContent should reject invalid pattern declarations instead of dropping them", () => { assert.throws( () => parseSchemaContent(` { "type": "object", "properties": { "name": { "type": "string", "pattern": "[" } } } `), /invalid 'pattern' regular expression/u ); }); test("parseSchemaContent should reject unsupported string format declarations", () => { assert.throws( () => parseSchemaContent(` { "type": "object", "properties": { "name": { "type": "string", "format": "ipv4" } } } `), /unsupported string format 'ipv4'.*'duration'.*'time'/u ); }); test("parseSchemaContent should reject format declarations on non-string schema nodes", () => { assert.throws( () => parseSchemaContent(` { "type": "object", "properties": { "hp": { "type": "integer", "format": "uuid" } } } `), /can only declare 'format' on type 'string'/u ); }); test("parseSchemaContent should reject non-string format metadata values", () => { assert.throws( () => parseSchemaContent(` { "type": "object", "properties": { "name": { "type": "string", "format": 123 } } } `), /must declare 'format' as a string/u ); }); test("parseSchemaContent should ignore mismatched constraint metadata on unsupported scalar types", () => { const schema = parseSchemaContent(` { "type": "object", "properties": { "enabled": { "type": "boolean", "minimum": 1, "minLength": 3 } } } `); const yaml = parseTopLevelYaml(` enabled: true `); assert.equal(schema.properties.enabled.minimum, undefined); assert.equal(schema.properties.enabled.minLength, undefined); assert.deepEqual(validateParsedConfig(schema, yaml), []); }); test("validateParsedConfig should localize diagnostics when Chinese UI is requested", () => { const schema = parseSchemaContent(` { "type": "object", "required": ["name"], "properties": { "name": { "type": "string" } } } `); const yaml = parseTopLevelYaml(` id: 1 `); const diagnostics = validateParsedConfig(schema, yaml, {isChinese: true}); assert.equal(diagnostics.length, 2); assert.match(diagnostics[0].message, /缺少必填属性/u); assert.match(diagnostics[1].message, /未在匹配的 schema 中声明/u); }); test("validateParsedConfig should localize expected object diagnostics when Chinese UI is requested", () => { const schema = parseSchemaContent(` { "type": "object", "properties": { "reward": { "type": "object", "properties": { "gold": { "type": "integer" } } } } } `); const yaml = parseTopLevelYaml(` reward: 1 `); const diagnostics = validateParsedConfig(schema, yaml, {isChinese: true}); assert.equal(diagnostics.length, 1); assert.equal(diagnostics[0].message, "属性“reward”应为对象。"); }); test("validateParsedConfig should reject values that match a forbidden not schema", () => { const schema = parseSchemaContent(` { "type": "object", "properties": { "name": { "type": "string", "not": { "type": "string", "const": "Deprecated" } } } } `); const yaml = parseTopLevelYaml(` name: Deprecated `); const diagnostics = validateParsedConfig(schema, yaml); assert.equal(diagnostics.length, 1); assert.equal( diagnostics[0].message, "Property 'name' must not match the forbidden 'not' schema."); }); test("validateParsedConfig should keep not object matching strict instead of contains-style subset matching", () => { const schema = parseSchemaContent(` { "type": "object", "properties": { "reward": { "type": "object", "not": { "type": "object", "required": ["gold"], "properties": { "gold": { "type": "integer" } } }, "properties": { "gold": { "type": "integer" }, "bonus": { "type": "integer" } } } } } `); const yaml = parseTopLevelYaml(` reward: gold: 10 bonus: 5 `); const diagnostics = validateParsedConfig(schema, yaml); assert.deepEqual(diagnostics, []); }); test("validateParsedConfig should reject objects that fully match a forbidden not schema", () => { const schema = parseSchemaContent(` { "type": "object", "properties": { "reward": { "type": "object", "not": { "type": "object", "required": ["gold"], "properties": { "gold": { "type": "integer" } } }, "properties": { "gold": { "type": "integer" }, "bonus": { "type": "integer" } } } } } `); const yaml = parseTopLevelYaml(` reward: gold: 10 `); const diagnostics = validateParsedConfig(schema, yaml); assert.equal(diagnostics.length, 1); assert.match(diagnostics[0].message, /not/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("applyFormUpdates should rewrite object-array items from structured form payloads", () => { const updated = applyFormUpdates( [ "id: 1", "name: Slime", "phases:", " -", " wave: 1", " monsterId: slime" ].join("\n"), { objectArrays: { phases: [ { wave: "1", monsterId: "slime", tags: ["starter", "melee"], reward: { gold: "10", currency: "coin" } }, { wave: "2", monsterId: "goblin" } ] } }); assert.match(updated, /^id: 1$/mu); assert.match(updated, /^name: Slime$/mu); assert.match(updated, /^phases:$/mu); assert.match(updated, /^ -$/mu); assert.match(updated, /^ wave: 1$/mu); assert.match(updated, /^ monsterId: slime$/mu); assert.match(updated, /^ tags:$/mu); assert.match(updated, /^ - starter$/mu); assert.match(updated, /^ - melee$/mu); assert.match(updated, /^ reward:$/mu); assert.match(updated, /^ gold: 10$/mu); assert.match(updated, /^ currency: coin$/mu); assert.match(updated, /^ monsterId: goblin$/mu); }); test("buildFormModel should expose nested object-array editors inside object-array items", () => { const schema = parseSchemaContent(` { "type": "object", "properties": { "phases": { "type": "array", "items": { "type": "object", "properties": { "wave": { "type": "integer" }, "spawns": { "type": "array", "items": { "type": "object", "properties": { "monsterId": { "type": "string" }, "tags": { "type": "array", "items": { "type": "string" } } } } } } } } } } `); const yaml = parseTopLevelYaml(` phases: - wave: 1 spawns: - monsterId: slime tags: - starter `); const formModel = extensionTest.buildFormModel(schema, yaml, {}); const phasesField = formModel.fields.find((field) => field.path === "phases"); assert.ok(phasesField); assert.equal(phasesField.kind, "objectArray"); assert.equal(phasesField.items.length, 1); assert.deepEqual(formModel.unsupported, []); const nestedSpawnField = phasesField.items[0].fields.find((field) => field.path === "spawns"); assert.ok(nestedSpawnField); assert.equal(nestedSpawnField.kind, "objectArray"); assert.equal(nestedSpawnField.itemMode, true); assert.equal(nestedSpawnField.items.length, 1); const spawnMonsterField = nestedSpawnField.items[0].fields.find((field) => field.path === "monsterId"); assert.ok(spawnMonsterField); assert.equal(spawnMonsterField.kind, "scalar"); const spawnTagsField = nestedSpawnField.items[0].fields.find((field) => field.path === "tags"); assert.ok(spawnTagsField); assert.equal(spawnTagsField.kind, "array"); }); test("applyFormUpdates should rewrite nested object arrays from structured form payloads", () => { const updated = applyFormUpdates( [ "phases:", " -", " wave: 1" ].join("\n"), { objectArrays: { phases: [ { wave: "1", spawns: [ { monsterId: "slime", tags: ["starter", "melee"], reward: { gold: "10" } }, { monsterId: "goblin", conditions: [ { type: "night", value: "true" } ] } ] } ] } }); assert.match(updated, /^phases:$/mu); assert.match(updated, /^ -$/mu); assert.match(updated, /^ wave: 1$/mu); assert.match(updated, /^ spawns:$/mu); assert.match(updated, /^ -$/mu); assert.match(updated, /^ monsterId: slime$/mu); assert.match(updated, /^ tags:$/mu); assert.match(updated, /^ - starter$/mu); assert.match(updated, /^ - melee$/mu); assert.match(updated, /^ reward:$/mu); assert.match(updated, /^ gold: 10$/mu); assert.match(updated, /^ monsterId: goblin$/mu); assert.match(updated, /^ conditions:$/mu); assert.match(updated, /^ -$/mu); assert.match(updated, /^ type: night$/mu); assert.match(updated, /^ value: true$/mu); }); test("applyFormUpdates should not mix nested object-array items into the parent array", () => { const updated = applyFormUpdates( [ "phases:", " -", " wave: 1" ].join("\n"), { objectArrays: { phases: [ { wave: "1", spawns: [ { monsterId: "slime" }, { monsterId: "goblin" } ] }, { wave: "2", spawns: [ { monsterId: "bat" } ] } ] } }); assert.equal((updated.match(/^ -$/gmu) || []).length, 2); assert.equal((updated.match(/^ -$/gmu) || []).length, 3); assert.doesNotMatch(updated, /^ monsterId: slime$/mu); assert.doesNotMatch(updated, /^ monsterId: goblin$/mu); assert.match(updated, /^ -$/mu); assert.match(updated, /^ monsterId: slime$/mu); assert.match(updated, /^ monsterId: goblin$/mu); assert.match(updated, /^ monsterId: bat$/mu); }); test("applyFormUpdates should clear object arrays when the form removes all items", () => { const updated = applyFormUpdates( [ "id: 1", "phases:", " -", " wave: 1", " monsterId: slime" ].join("\n"), { objectArrays: { phases: [] } }); assert.equal(updated, [ "id: 1", "phases: []" ].join("\n")); }); test("extractYamlComments should map nested comments to logical paths", () => { const comments = extractYamlComments(` # Monster display name name: Slime stats: # Current hp value hp: 10 skills: # First skill entry - # Skill id note id: jump `); assert.equal(comments.name, "Monster display name"); assert.equal(comments["stats.hp"], "Current hp value"); assert.equal(comments["skills[0]"], "First skill entry"); assert.equal(comments["skills[0].id"], "Skill id note"); }); test("extractYamlComments should keep comments for complex YAML keys", () => { const comments = extractYamlComments(` # Dashed key comment my-key: Slime # Quoted key comment "complex key": value root: # Dotted key comment item.id: potion `); assert.equal(comments["my-key"], "Dashed key comment"); assert.equal(comments["complex key"], "Quoted key comment"); assert.equal(comments["root.item.id"], "Dotted key comment"); }); test("applyFormUpdates should preserve and update YAML comments", () => { const updated = applyFormUpdates( [ "# Monster display name", "name: Slime", "stats:", " # Current hp value", " hp: 10" ].join("\n"), { scalars: { name: "Slime King" }, comments: { name: "Localized display name", "stats.hp": "Health points after rebalance" } }); assert.match(updated, /^# Localized display name$/mu); assert.match(updated, /^name: Slime King$/mu); assert.match(updated, /^ # Health points after rebalance$/mu); assert.match(updated, /^ hp: 10$/mu); }); test("createSampleConfigYaml should bootstrap comments and placeholder values from schema", () => { const schema = parseSchemaContent(` { "type": "object", "properties": { "name": { "type": "string", "description": "Monster display name." }, "rarity": { "type": "string", "description": "Monster rarity.", "enum": ["common", "rare"] }, "skills": { "type": "array", "description": "Skill entries.", "items": { "type": "object", "properties": { "id": { "type": "string", "description": "Skill id." } } } } } } `); const sample = createSampleConfigYaml(schema); assert.match(sample, /^# Monster display name\.$/mu); assert.match(sample, /^name: example$/mu); assert.match(sample, /^# Monster rarity\.$/mu); assert.match(sample, /^rarity: common$/mu); assert.match(sample, /^# Skill entries\.$/mu); assert.match(sample, /^skills:$/mu); assert.match(sample, /^ -$/mu); assert.match(sample, /^ # Skill id\.$/mu); assert.match(sample, /^ id: example$/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("getEditableSchemaFields should sort keys with ordinal semantics", () => { const schema = parseSchemaContent(` { "type": "object", "properties": { "a": { "type": "string" }, "A": { "type": "string" }, "ä": { "type": "string" }, "z": { "type": "string" } } } `); assert.deepEqual( getEditableSchemaFields(schema).map((field) => field.key), ["A", "a", "z", "ä"]); }); test("createSampleConfigYaml should preserve empty-string scalar const values", () => { const schema = parseSchemaContent(` { "type": "object", "properties": { "name": { "type": "string", "const": "" } } } `); const sample = createSampleConfigYaml(schema); assert.match(sample, /^name: ""$/mu); }); test("createSampleConfigYaml should prefer scalar const values over defaults", () => { const schema = parseSchemaContent(` { "type": "object", "properties": { "rarity": { "type": "string", "const": "common", "default": "rare" } } } `); const sample = createSampleConfigYaml(schema); assert.match(sample, /^rarity: common$/mu); assert.ok(!/^rarity: rare$/mu.test(sample)); }); test("parseSchemaContent should capture object-focused if/then/else metadata", () => { const schema = parseSchemaContent(` { "type": "object", "properties": { "reward": { "type": "object", "properties": { "itemId": { "type": "string" }, "itemCount": { "type": "integer" }, "bonus": { "type": "integer" } }, "if": { "type": "object", "properties": { "itemId": { "type": "string", "const": "potion" } } }, "then": { "type": "object", "required": ["itemCount"], "properties": { "itemCount": { "type": "integer" } } }, "else": { "type": "object", "required": ["bonus"], "properties": { "bonus": { "type": "integer" } } } } } } `); assert.equal(schema.properties.reward.ifSchema.type, "object"); assert.equal(schema.properties.reward.thenSchema.type, "object"); assert.deepEqual(schema.properties.reward.thenSchema.required, ["itemCount"]); assert.deepEqual(schema.properties.reward.elseSchema.required, ["bonus"]); }); test("parseSchemaContent should reject then declarations without if", () => { assert.throws( () => parseSchemaContent(` { "type": "object", "properties": { "reward": { "type": "object", "properties": { "itemCount": { "type": "integer" } }, "then": { "type": "object", "required": ["itemCount"], "properties": { "itemCount": { "type": "integer" } } } } } } `), /must declare 'if' when using 'then' or 'else'/u); }); test("parseSchemaContent should require explicit object type for conditional branches", () => { assert.throws( () => parseSchemaContent(` { "type": "object", "properties": { "reward": { "type": "object", "properties": { "itemId": { "type": "string" } }, "if": { "properties": { "itemId": { "type": "string", "const": "potion" } } }, "then": { "type": "object", "properties": { "itemId": { "type": "string" } } } } } } `), /must declare an object-typed 'if' schema/u); }); test("parseSchemaContent should reject conditional branches with non-object properties metadata", () => { assert.throws( () => parseSchemaContent(` { "type": "object", "properties": { "reward": { "type": "object", "properties": { "itemId": { "type": "string" } }, "if": { "type": "object", "properties": [] }, "then": { "type": "object", "properties": { "itemId": { "type": "string" } } } } } } `), /must declare 'properties' in 'if' as an object-valued map/u); }); test("parseSchemaContent should reject conditional branches with non-array required metadata", () => { assert.throws( () => parseSchemaContent(` { "type": "object", "properties": { "reward": { "type": "object", "properties": { "itemCount": { "type": "integer" } }, "if": { "type": "object", "properties": { "itemCount": { "type": "integer" } } }, "then": { "type": "object", "required": "itemCount", "properties": { "itemCount": { "type": "integer" } } } } } } `), /must declare 'required' in 'then' as an array of property names/u); }); test("parseSchemaContent should reject conditional branches with invalid required entries", () => { assert.throws( () => parseSchemaContent(` { "type": "object", "properties": { "reward": { "type": "object", "properties": { "bonus": { "type": "integer" } }, "if": { "type": "object", "properties": { "bonus": { "type": "integer" } } }, "else": { "type": "object", "required": [" "], "properties": { "bonus": { "type": "integer" } } } } } } `), /cannot declare blank property names in 'required' for 'else'/u); }); test("validateParsedConfig should report then violations", () => { const schema = parseSchemaContent(` { "type": "object", "properties": { "reward": { "type": "object", "properties": { "itemId": { "type": "string" }, "itemCount": { "type": "integer" } }, "if": { "type": "object", "properties": { "itemId": { "type": "string", "const": "potion" } } }, "then": { "type": "object", "required": ["itemCount"], "properties": { "itemCount": { "type": "integer" } } } } } } `); const yaml = parseTopLevelYaml(` reward: itemId: potion `); const diagnostics = validateParsedConfig(schema, yaml); assert.equal(diagnostics.length, 1); assert.match(diagnostics[0].message, /'then' schema|`then` schema/u); }); test("validateParsedConfig should report else violations", () => { const schema = parseSchemaContent(` { "type": "object", "properties": { "reward": { "type": "object", "properties": { "itemId": { "type": "string" }, "bonus": { "type": "integer" } }, "if": { "type": "object", "properties": { "itemId": { "type": "string", "const": "potion" } } }, "else": { "type": "object", "required": ["bonus"], "properties": { "bonus": { "type": "integer" } } } } } } `); const yaml = parseTopLevelYaml(` reward: itemId: sword `); const diagnostics = validateParsedConfig(schema, yaml); assert.equal(diagnostics.length, 1); assert.match(diagnostics[0].message, /'else' schema|`else` schema/u); }); test("validateParsedConfig should accept satisfied if/then/else constraints", () => { const schema = parseSchemaContent(` { "type": "object", "properties": { "reward": { "type": "object", "properties": { "itemId": { "type": "string" }, "itemCount": { "type": "integer" }, "bonus": { "type": "integer" } }, "if": { "type": "object", "properties": { "itemId": { "type": "string", "const": "potion" } } }, "then": { "type": "object", "required": ["itemCount"], "properties": { "itemCount": { "type": "integer" } } }, "else": { "type": "object", "required": ["bonus"], "properties": { "bonus": { "type": "integer" } } } } } } `); const thenYaml = parseTopLevelYaml(` reward: itemId: potion itemCount: 2 `); const elseYaml = parseTopLevelYaml(` reward: itemId: sword bonus: 1 `); assert.deepEqual(validateParsedConfig(schema, thenYaml), []); assert.deepEqual(validateParsedConfig(schema, elseYaml), []); }); test("parseBatchArrayValue should keep comma-separated batch editing behavior", () => { assert.deepEqual(parseBatchArrayValue(" potion, bomb , ,elixir "), ["potion", "bomb", "elixir"]); });