Files
kunail0804-ComfyUI-Scene-Co…/tests/validator/test_scene_validator.py
T
3f0358bfa9 feat(validator): implement the Scene Validator (#11) (#55)
Add the Scene Validator per MASTER_SPEC §14 and a central message-code registry.

- compiler/common/message_codes.py: single source of truth for message codes
  (Appendix B SC0001-SC0014 plus documented extensions SC0015 Unexpected Field
  Removed, SC0016 Empty Concept Removed, SC0017 Interaction Dropped) with a
  message() factory that pins each code's severity and title.
- compiler/validator/scene_validator.py: validate_scene(scene_json, config,
  logger=None) -> CompilerResult.
  - Strict schema validation classifies failures: missing required -> SC0009,
    wrong type/malformed -> SC0002 (Errors that stop the compilation).
  - Recoverable normalization: unexpected fields -> SC0015 warning + removed
    (suppressed when config.validator.allow_unknown_fields is true); empty/
    whitespace concepts -> SC0016 warning + removed after trimming; interactions
    whose participants reference a non-existent character -> SC0017 warning +
    dropped.
  - Never creates concepts, generates tags, or does KB lookup. Output is a
    Scene model wrapped in a CompilerResult; order is deterministic.

New warning codes (SC0015-SC0017) are extensions to be folded into Appendix B.

Tests: valid input, hard errors (SC0009/SC0002) that stop, recoverable warnings
(SC0015/SC0016/SC0017), allow_unknown_fields suppression, determinism, and the
code registry.

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

175 lines
5.4 KiB
Python

"""Tests for the Scene Validator (issue #11, MASTER_SPEC §14)."""
from __future__ import annotations
from compiler.common.config import Config
from compiler.common.result import Severity
from compiler.validator.scene_validator import validate_scene
from schemas.models import Scene
def valid_scene() -> dict:
return {
"characters": [
{
"id": 0,
"identity": ["female"],
"appearance": [],
"clothing": ["dress"],
"accessories": [],
"pose": ["standing"],
"expression": ["smile"],
"actions": [],
}
],
"interactions": [],
"objects": [],
"environment": ["classroom"],
"camera": [],
"lighting": ["sunset"],
"metadata": {"schema_version": "1.0"},
}
def default_config() -> Config:
return Config()
def codes(messages) -> list[str]:
return [m.code for m in messages]
# --- valid input -----------------------------------------------------------
def test_valid_scene_succeeds() -> None:
result = validate_scene(valid_scene(), default_config())
assert result.success
assert isinstance(result.data, Scene)
assert result.warnings == ()
assert result.data.characters[0].identity[0].name == "female"
# --- hard errors stop compilation ------------------------------------------
def test_missing_required_field_is_sc0009_and_stops() -> None:
scene = valid_scene()
del scene["metadata"]
result = validate_scene(scene, default_config())
assert not result.success
assert result.data is None
assert "SC0009" in codes(result.errors)
def test_wrong_type_is_sc0002_and_stops() -> None:
scene = valid_scene()
scene["characters"] = "not a list"
result = validate_scene(scene, default_config())
assert not result.success
assert result.data is None
assert "SC0002" in codes(result.errors)
# --- recoverable warnings continue -----------------------------------------
def test_unexpected_top_level_field_warns_and_continues() -> None:
scene = valid_scene()
scene["bogus"] = 1
result = validate_scene(scene, default_config())
assert result.success
assert "SC0015" in codes(result.warnings)
assert isinstance(result.data, Scene) # field removed, scene still built
def test_unexpected_character_field_warns() -> None:
scene = valid_scene()
scene["characters"][0]["nickname"] = "Ann"
result = validate_scene(scene, default_config())
assert result.success
assert "SC0015" in codes(result.warnings)
def test_allow_unknown_fields_suppresses_warning() -> None:
scene = valid_scene()
scene["bogus"] = 1
config = Config.from_json({"validator": {"allow_unknown_fields": True}})
result = validate_scene(scene, config)
assert result.success
assert "SC0015" not in codes(result.warnings)
def test_empty_concept_is_removed_with_warning() -> None:
scene = valid_scene()
scene["characters"][0]["identity"] = ["", "female"]
result = validate_scene(scene, default_config())
assert result.success
assert "SC0016" in codes(result.warnings)
assert [c.name for c in result.data.characters[0].identity] == ["female"]
def test_whitespace_concept_is_trimmed_and_empty_dropped() -> None:
scene = valid_scene()
scene["characters"][0]["identity"] = [" female ", " "]
result = validate_scene(scene, default_config())
assert result.success
assert [c.name for c in result.data.characters[0].identity] == ["female"]
assert "SC0016" in codes(result.warnings)
def test_dangling_interaction_is_dropped_with_warning() -> None:
scene = valid_scene()
scene["interactions"] = [{"participants": [0, 5], "concept": "hug"}]
result = validate_scene(scene, default_config())
assert result.success
assert "SC0017" in codes(result.warnings)
assert result.data.interactions == ()
def test_valid_interaction_is_kept() -> None:
scene = valid_scene()
scene["characters"].append(
{
"id": 1,
"identity": ["male"],
"appearance": [],
"clothing": [],
"accessories": [],
"pose": [],
"expression": [],
"actions": [],
}
)
scene["interactions"] = [{"participants": [0, 1], "concept": "hug"}]
result = validate_scene(scene, default_config())
assert result.success
assert len(result.data.interactions) == 1
assert "SC0017" not in codes(result.warnings)
# --- guarantees ------------------------------------------------------------
def test_does_not_generate_tags_or_lookup_kb() -> None:
# Concept names must be preserved verbatim (no tag substitution / KB lookup).
scene = valid_scene()
result = validate_scene(scene, default_config())
assert result.data.characters[0].clothing[0].name == "dress" # not "dress" -> tag
def test_output_is_deterministic() -> None:
scene = valid_scene()
scene["bogus"] = 1
scene["characters"][0]["identity"] = ["", "female"]
a = validate_scene(scene, default_config())
b = validate_scene(scene, default_config())
assert codes(a.warnings) == codes(b.warnings)
def test_warnings_have_warning_severity() -> None:
scene = valid_scene()
scene["bogus"] = 1
result = validate_scene(scene, default_config())
assert all(m.severity is Severity.WARNING for m in result.warnings)