Files
kunail0804-ComfyUI-Scene-Co…/tests/validator/test_schema_validation.py
T
03dd578435 feat(schemas): add JSON Schemas and validation helper for inter-stage documents (#2) (#46)
Define the nine JSON Schemas (draft 2020-12) exchanged between compiler stages
per MASTER_SPEC §10–§11, plus a reusable validation helper.

Schemas (schemas/json/, cross-referenced by $id):
- scene, character, concept, interaction, metadata, knowledge_base_entry,
  resolved_tag, category_map, prompt_output
- Scene requires all seven sections (collections may be empty); Concept is a
  bare string or an object with required name; Character has an integer id plus
  seven concept arrays; Interaction has integer participants and a concept.
- Each schema carries a major.minor version; unknown fields rejected by default.

Helper (schemas/validation.py):
- validate_document(doc, schema_name, *, allow_unknown=False) -> list of
  structured ValidationIssue (empty = valid); collects all errors.
- Loads all schemas into a referencing registry so cross-schema $ref resolves.
- allow_unknown relaxes additionalProperties per config without duplicating
  schemas (schema-valued additionalProperties, e.g. Category Map, is preserved).

Tests: valid + several invalid documents per schema, unknown-field toggle,
unknown-schema error (34 assertions). Bumps jsonschema to >=4.18 for referencing.

Co-authored-by: kunail0804 <kunail0804@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 18:52:32 +02:00

250 lines
7.0 KiB
Python

"""Tests for the JSON Schema validation helper (issue #2, MASTER_SPEC §11).
Covers, per schema, at least one valid document and several invalid documents
(wrong types, missing required fields, unknown fields), plus the configurable
unknown-field behaviour.
"""
from __future__ import annotations
import pytest
from schemas.validation import ValidationIssue, list_schemas, validate_document
# --- helpers ---------------------------------------------------------------
def valid_metadata() -> dict:
return {"schema_version": "1.0"}
def valid_scene() -> dict:
return {
"characters": [],
"interactions": [],
"objects": [],
"environment": [],
"camera": [],
"lighting": [],
"metadata": valid_metadata(),
}
def valid_character() -> dict:
return {
"id": 0,
"identity": ["female"],
"appearance": [{"name": "long_hair", "category": "hair"}],
"clothing": [],
"accessories": [],
"pose": [],
"expression": ["smile"],
"actions": [],
}
# --- generic behaviour -----------------------------------------------------
def test_valid_scene_passes() -> None:
assert validate_document(valid_scene(), "scene") == []
def test_returns_structured_issue_objects() -> None:
issues = validate_document({"characters": []}, "scene")
assert issues
assert all(isinstance(i, ValidationIssue) for i in issues)
assert all(i.message for i in issues)
def test_unknown_schema_name_raises() -> None:
with pytest.raises(KeyError):
validate_document({}, "does_not_exist")
def test_all_nine_schemas_are_registered() -> None:
expected = {
"scene",
"character",
"concept",
"interaction",
"metadata",
"knowledge_base_entry",
"resolved_tag",
"category_map",
"prompt_output",
}
assert expected.issubset(set(list_schemas()))
# --- scene -----------------------------------------------------------------
def test_scene_missing_required_field_fails() -> None:
doc = valid_scene()
del doc["metadata"]
issues = validate_document(doc, "scene")
assert any("metadata" in i.message or "metadata" in i.path for i in issues)
def test_scene_unknown_field_rejected_by_default() -> None:
doc = valid_scene()
doc["extra"] = 1
assert validate_document(doc, "scene") != []
def test_scene_unknown_field_allowed_when_configured() -> None:
doc = valid_scene()
doc["extra"] = 1
assert validate_document(doc, "scene", allow_unknown=True) == []
def test_scene_with_populated_nested_documents_passes() -> None:
doc = valid_scene()
doc["characters"] = [valid_character()]
doc["interactions"] = [{"participants": [0], "concept": "holding hands"}]
doc["objects"] = ["chair"]
doc["environment"] = ["classroom"]
assert validate_document(doc, "scene") == []
# --- character -------------------------------------------------------------
def test_valid_character_passes() -> None:
assert validate_document(valid_character(), "character") == []
def test_character_id_must_be_integer() -> None:
doc = valid_character()
doc["id"] = "0"
assert validate_document(doc, "character") != []
def test_character_missing_field_fails() -> None:
doc = valid_character()
del doc["identity"]
assert validate_document(doc, "character") != []
# --- concept ---------------------------------------------------------------
def test_concept_bare_string_valid() -> None:
assert validate_document("blue_eyes", "concept") == []
def test_concept_object_valid() -> None:
assert validate_document({"name": "blue_eyes", "category": "eyes"}, "concept") == []
def test_concept_object_without_name_fails() -> None:
assert validate_document({"category": "eyes"}, "concept") != []
def test_concept_unknown_field_rejected_by_default() -> None:
assert validate_document({"name": "x", "bogus": 1}, "concept") != []
def test_concept_unknown_field_allowed_when_configured() -> None:
assert validate_document({"name": "x", "bogus": 1}, "concept", allow_unknown=True) == []
# --- interaction -----------------------------------------------------------
def test_valid_interaction_passes() -> None:
assert validate_document({"participants": [0, 1], "concept": "hug"}, "interaction") == []
def test_interaction_participants_must_be_integers() -> None:
assert validate_document({"participants": ["a"], "concept": "hug"}, "interaction") != []
def test_interaction_missing_concept_fails() -> None:
assert validate_document({"participants": [0]}, "interaction") != []
# --- metadata --------------------------------------------------------------
def test_valid_metadata_passes() -> None:
assert validate_document({"schema_version": "1.0", "warnings": []}, "metadata") == []
def test_metadata_missing_schema_version_fails() -> None:
assert validate_document({"warnings": []}, "metadata") != []
def test_metadata_warnings_must_be_array() -> None:
assert validate_document({"schema_version": "1.0", "warnings": "oops"}, "metadata") != []
# --- knowledge base entry --------------------------------------------------
def valid_kb_entry() -> dict:
return {"id": "blonde_hair", "tags": ["blonde hair"], "category": "hair"}
def test_valid_kb_entry_passes() -> None:
assert validate_document(valid_kb_entry(), "knowledge_base_entry") == []
def test_kb_entry_id_must_be_snake_case() -> None:
doc = valid_kb_entry()
doc["id"] = "Blonde Hair"
assert validate_document(doc, "knowledge_base_entry") != []
def test_kb_entry_category_must_be_string() -> None:
doc = valid_kb_entry()
doc["category"] = 5
assert validate_document(doc, "knowledge_base_entry") != []
# --- resolved tag ----------------------------------------------------------
def valid_resolved_tag() -> dict:
return {
"tag": "blue eyes",
"category": "eyes",
"source_concept": "blue eyes",
"knowledge_base_entry": "blue_eyes",
}
def test_valid_resolved_tag_passes() -> None:
assert validate_document(valid_resolved_tag(), "resolved_tag") == []
def test_resolved_tag_missing_field_fails() -> None:
doc = valid_resolved_tag()
del doc["knowledge_base_entry"]
assert validate_document(doc, "resolved_tag") != []
# --- category map ----------------------------------------------------------
def test_valid_category_map_passes() -> None:
doc = {"eyes": [valid_resolved_tag()], "hair": []}
assert validate_document(doc, "category_map") == []
def test_category_map_values_must_be_resolved_tag_arrays() -> None:
assert validate_document({"eyes": ["not_a_tag_object"]}, "category_map") != []
# --- prompt output ---------------------------------------------------------
def test_valid_prompt_output_passes() -> None:
doc = {"name": "positive", "value": "blue eyes, smile"}
assert validate_document(doc, "prompt_output") == []
def test_prompt_output_value_must_be_string() -> None:
assert validate_document({"name": "positive", "value": 5}, "prompt_output") != []