* feat!: collapse pipeline — Resolver emits a flat prompt, drop categories (v1.1) BREAKING CHANGE: the Category Splitter and Prompt Builder stages and nodes are removed. The Resolver is now the translator from Scene JSON to prompt: it resolves concepts to Knowledge Base tags and joins them into one flat comma-separated prompt string, in resolution order (which naturally leads with character concepts). Resolver node outputs are now (prompt, warnings, errors, json) — the json being the traceable resolved tags so a translation problem is inspectable. Node count drops from 8 to 6. The CategoryMap/PromptOutput models and their schemas are removed; the `category` field remains on entries and resolved tags for traceability only. Debug Viewer simplified to scene/warnings/errors. Goldens, example scenes/workflow, integration/performance/repo-layout/release tests, benchmark script, and the docs (README + wiki) are updated to the flat prompt. Version bumped to 1.1.0. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * build: drop removed splitter/builder packages from pyproject Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: kunail0804 <kunail0804@gmail.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
224 lines
6.2 KiB
Python
224 lines
6.2 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_core_schemas_are_registered() -> None:
|
|
expected = {
|
|
"scene",
|
|
"character",
|
|
"concept",
|
|
"interaction",
|
|
"metadata",
|
|
"knowledge_base_entry",
|
|
"resolved_tag",
|
|
}
|
|
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") != []
|