Files
5e146d5a55 feat!: v1.1 — Resolver emits a flat prompt, drop categories (#85)
* 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>
2026-07-11 00:00:48 +02:00

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") != []