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>
250 lines
7.0 KiB
Python
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") != []
|