Files
kunail0804-ComfyUI-Scene-Co…/tests/resolver/test_illustrious_resolver.py
T
76d440fbaf feat(resolver): implement the deterministic Illustrious Resolver (#12) (#56)
Add compiler/resolver/illustrious_resolver.py: resolve_scene(scene,
knowledge_base, config, logger=None) -> CompilerResult, per MASTER_SPEC §17. No
LLM; no concept or tag is invented.

- normalize_concept (§17.3): NFKC + lowercase, unifying underscores/spaces so
  natural concepts, snake_case ids, and spaced aliases share one lookup key. The
  resolver builds its own normalized index over the Knowledge Base.
- Per-concept pipeline in scene discovery order (§17.8): normalize -> direct
  alias resolution (no chains; skipped when allow_aliases is false) -> canonical
  lookup -> recursive expansion -> tag generation.
- Expansion (§17.5): own tags first, then depth-first in list order per expand
  target, inserted immediately after; capped by max_expansion_depth; skipped when
  expansion_enabled is false; defensive path-based cycle detection -> SC0003.
- Deduplication after expansion keeps the first occurrence (SC0007).
- Unknown concept -> SC0001 warning (continue); deprecated -> SC0006 (continue);
  invalid category -> SC0008 (stop); circular expansion -> SC0003 (stop).
- Every ResolvedTag keeps its entry's category and full traceability
  (source_concept = originating scene concept, knowledge_base_entry = generating
  entry). Verbose logging of each decision.

Tests (no Ollama): normalization, alias resolution + disable, expansion order,
nested/depth-capped/disabled expansion, traceability, dedup, unknown/deprecated,
circular/invalid-category stops, category preservation, and determinism.

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

230 lines
7.9 KiB
Python

"""Tests for the Illustrious Resolver (issue #12, MASTER_SPEC §17). No Ollama."""
from __future__ import annotations
from compiler.common.config import Config
from compiler.common.knowledge_base import KnowledgeBase, KnowledgeBaseEntry
from compiler.resolver.illustrious_resolver import resolve_scene
from schemas.models import Scene
def kb(*entries: KnowledgeBaseEntry) -> KnowledgeBase:
return KnowledgeBase(list(entries))
def entry(id_, tags, category="hair", aliases=(), expand=(), deprecated=False):
return KnowledgeBaseEntry(
id=id_,
tags=tuple(tags),
category=category,
aliases=tuple(aliases),
expand=tuple(expand),
deprecated=deprecated,
)
def scene_with(identity=(), **fields) -> Scene:
character = {
"id": 0,
"identity": list(identity),
"appearance": list(fields.get("appearance", [])),
"clothing": list(fields.get("clothing", [])),
"accessories": [],
"pose": [],
"expression": [],
"actions": [],
}
return Scene.from_json(
{
"characters": [character],
"interactions": [],
"objects": list(fields.get("objects", [])),
"environment": list(fields.get("environment", [])),
"camera": [],
"lighting": [],
"metadata": {"schema_version": "1.0"},
}
)
def default_config() -> Config:
return Config()
def tags(result) -> list[str]:
return [t.tag for t in result.data]
def codes(messages) -> list[str]:
return [m.code for m in messages]
# --- normalization ---------------------------------------------------------
def test_normalization_matches_snake_case_id() -> None:
knowledge = kb(entry("long_hair", ["long hair"]))
for written in ["long hair", "Long Hair", " LONG_HAIR ", "long_hair"]:
result = resolve_scene(scene_with(identity=[written]), knowledge, default_config())
assert tags(result) == ["long hair"], written
# --- alias resolution ------------------------------------------------------
def test_alias_resolves_directly() -> None:
knowledge = kb(entry("female", ["1girl"], category="character", aliases=["girl", "woman"]))
result = resolve_scene(scene_with(identity=["girl"]), knowledge, default_config())
assert tags(result) == ["1girl"]
assert result.data[0].knowledge_base_entry == "female"
assert result.data[0].source_concept == "girl"
def test_alias_ignored_when_disabled() -> None:
knowledge = kb(entry("female", ["1girl"], category="character", aliases=["girl"]))
config = Config.from_json({"resolver": {"allow_aliases": False}})
result = resolve_scene(scene_with(identity=["girl"]), knowledge, config)
assert tags(result) == []
assert "SC0001" in codes(result.warnings)
# --- expansion order -------------------------------------------------------
def test_expansion_order_is_normative() -> None:
knowledge = kb(
entry(
"school_uniform", ["school uniform"], category="clothing", expand=["blazer", "skirt"]
),
entry("blazer", ["blazer"], category="clothing"),
entry("skirt", ["pleated skirt"], category="clothing"),
)
result = resolve_scene(scene_with(clothing=["school uniform"]), knowledge, default_config())
assert tags(result) == ["school uniform", "blazer", "pleated skirt"]
def test_nested_expansion_is_depth_first() -> None:
knowledge = kb(
entry("a", ["ta"], expand=["b"]),
entry("b", ["tb"], expand=["c"]),
entry("c", ["tc"]),
)
result = resolve_scene(scene_with(identity=["a"]), knowledge, default_config())
assert tags(result) == ["ta", "tb", "tc"]
def test_expansion_disabled_emits_only_own_tags() -> None:
knowledge = kb(
entry("a", ["ta"], expand=["b"]),
entry("b", ["tb"]),
)
config = Config.from_json({"resolver": {"expansion_enabled": False}})
result = resolve_scene(scene_with(identity=["a"]), knowledge, config)
assert tags(result) == ["ta"]
def test_max_expansion_depth_caps_recursion() -> None:
# depth 1 allows one expansion level (a -> b) but not the next (b -> c).
knowledge = kb(
entry("a", ["ta"], expand=["b"]),
entry("b", ["tb"], expand=["c"]),
entry("c", ["tc"]),
)
config = Config.from_json({"resolver": {"max_expansion_depth": 1}})
result = resolve_scene(scene_with(identity=["a"]), knowledge, config)
assert tags(result) == ["ta", "tb"]
def test_expanded_tag_traceability() -> None:
knowledge = kb(
entry("school_uniform", ["school uniform"], category="clothing", expand=["blazer"]),
entry("blazer", ["blazer"], category="clothing"),
)
result = resolve_scene(scene_with(clothing=["school uniform"]), knowledge, default_config())
blazer_tag = result.data[1]
assert blazer_tag.tag == "blazer"
assert blazer_tag.knowledge_base_entry == "blazer"
assert blazer_tag.source_concept == "school uniform" # originating scene concept
# --- deduplication ---------------------------------------------------------
def test_duplicate_tags_removed_keeping_first() -> None:
knowledge = kb(
entry("a", ["shared"]),
entry("b", ["shared"]),
)
result = resolve_scene(scene_with(identity=["a", "b"]), knowledge, default_config())
assert tags(result) == ["shared"]
assert "SC0007" in codes(result.warnings)
# --- unknown / deprecated --------------------------------------------------
def test_unknown_concept_warns_and_continues() -> None:
knowledge = kb(entry("female", ["1girl"], category="character"))
result = resolve_scene(scene_with(identity=["dragon", "female"]), knowledge, default_config())
assert tags(result) == ["1girl"]
assert "SC0001" in codes(result.warnings)
def test_deprecated_concept_warns_but_emits_tags() -> None:
knowledge = kb(entry("old_style", ["old style"], deprecated=True))
result = resolve_scene(scene_with(identity=["old_style"]), knowledge, default_config())
assert tags(result) == ["old style"]
assert "SC0006" in codes(result.warnings)
# --- error conditions stop -------------------------------------------------
def test_circular_expansion_stops_with_sc0003() -> None:
knowledge = kb(
entry("a", ["ta"], expand=["b"]),
entry("b", ["tb"], expand=["a"]),
)
result = resolve_scene(scene_with(identity=["a"]), knowledge, default_config())
assert not result.success
assert result.data is None
assert "SC0003" in codes(result.errors)
def test_invalid_category_stops_with_sc0008() -> None:
knowledge = kb(entry("a", ["ta"], category="not_a_category"))
result = resolve_scene(scene_with(identity=["a"]), knowledge, default_config())
assert not result.success
assert "SC0008" in codes(result.errors)
# --- category preservation & determinism -----------------------------------
def test_tag_keeps_entry_category() -> None:
knowledge = kb(entry("blue_eyes", ["blue eyes"], category="eyes"))
result = resolve_scene(scene_with(appearance=["blue_eyes"]), knowledge, default_config())
assert result.data[0].category == "eyes"
def test_discovery_order_preserved_across_scene() -> None:
knowledge = kb(
entry("female", ["1girl"], category="character"),
entry("forest", ["forest"], category="environment"),
)
scene = scene_with(identity=["female"], environment=["forest"])
result = resolve_scene(scene, knowledge, default_config())
assert tags(result) == ["1girl", "forest"]
def test_output_is_deterministic() -> None:
knowledge = kb(
entry("school_uniform", ["school uniform"], category="clothing", expand=["blazer"]),
entry("blazer", ["blazer"], category="clothing"),
)
scene = scene_with(clothing=["school uniform", "school uniform"])
a = resolve_scene(scene, knowledge, default_config())
b = resolve_scene(scene, knowledge, default_config())
assert tags(a) == tags(b)
assert codes(a.warnings) == codes(b.warnings)