diff --git a/tools/gframework-config-tool/src/configValidation.js b/tools/gframework-config-tool/src/configValidation.js index 8dcb5de6..59c1f3cb 100644 --- a/tools/gframework-config-tool/src/configValidation.js +++ b/tools/gframework-config-tool/src/configValidation.js @@ -897,6 +897,11 @@ function parseSchemaNode(rawNode, displayPath) { const containsNode = value.contains && typeof value.contains === "object" ? parseSchemaNode(value.contains, joinArrayTemplatePath(displayPath)) : undefined; + if (!containsNode && + (typeof metadata.minContains === "number" || typeof metadata.maxContains === "number")) { + throw new Error(`Schema property '${displayPath}' declares 'minContains' or 'maxContains' without 'contains'.`); + } + if (containsNode && containsNode.type === "array") { throw new Error(`Schema property '${displayPath}' uses unsupported nested array 'contains' schemas.`); } diff --git a/tools/gframework-config-tool/test/configValidation.test.js b/tools/gframework-config-tool/test/configValidation.test.js index 3d3ba6e4..881da545 100644 --- a/tools/gframework-config-tool/test/configValidation.test.js +++ b/tools/gframework-config-tool/test/configValidation.test.js @@ -863,6 +863,64 @@ dropRates: 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 accept large decimal multiples without floating-point drift", () => { const schema = parseSchemaContent(` { @@ -1180,6 +1238,42 @@ test("parseSchemaContent should reject nested-array contains schemas", () => { /unsupported nested array 'contains' schemas/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(`