Closes #114, #115, #116, #117. - Deterministic, offline, dependency-free char-ngram embedding backend + config section (semantic.enabled default false, min_similarity, backend). - Committed embedding index (data/kb_embedding_index.json) mapping vectors to real KB entry ids only; built by scripts/build_embedding_index.py, loaded once/cached. - Resolver falls back to the nearest entry on a deterministic miss when enabled, accepting >= min_similarity and emitting SC0020; NSFW-gated neighbours are skipped. Disabled => byte-identical to before. - Guarantee tests: deterministic lookup always wins (exact hit never queries the index), fallback emits only KB tags (no invention), identical input => identical output. SC0020 registered. Co-authored-by: kunail0804 <kunail0804@gmail.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
91 lines
3.0 KiB
Python
91 lines
3.0 KiB
Python
"""Tests for the Resolver ComfyUI node (issue #21). No Ollama."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import nodes
|
|
import nodes.resolver_node as node_module
|
|
from compiler.common.config import Config
|
|
from compiler.common.knowledge_base import KnowledgeBase, KnowledgeBaseEntry
|
|
from compiler.common.message_codes import message
|
|
from compiler.common.result import CompilerResult
|
|
from nodes.resolver_node import ResolverNode
|
|
from schemas.models import Scene
|
|
|
|
|
|
def kb() -> KnowledgeBase:
|
|
return KnowledgeBase(
|
|
[
|
|
KnowledgeBaseEntry(
|
|
id="female", tags=("1girl",), category="character", aliases=("girl",)
|
|
),
|
|
KnowledgeBaseEntry(id="long_hair", tags=("long hair",), category="hair"),
|
|
]
|
|
)
|
|
|
|
|
|
def scene() -> Scene:
|
|
return Scene.from_json(
|
|
{
|
|
"characters": [
|
|
{
|
|
"id": 0,
|
|
"identity": ["girl"],
|
|
"appearance": ["long hair"],
|
|
"clothing": [],
|
|
"accessories": [],
|
|
"pose": [],
|
|
"expression": [],
|
|
"actions": [],
|
|
}
|
|
],
|
|
"interactions": [],
|
|
"objects": [],
|
|
"environment": [],
|
|
"camera": [],
|
|
"lighting": [],
|
|
"metadata": {"schema_version": "1.0"},
|
|
}
|
|
)
|
|
|
|
|
|
def test_node_metadata() -> None:
|
|
assert ResolverNode.RETURN_TYPES == ("STRING", "STRING", "STRING", "STRING")
|
|
assert ResolverNode.RETURN_NAMES == ("prompt", "warnings", "errors", "json")
|
|
inputs = ResolverNode.INPUT_TYPES()
|
|
assert set(inputs["required"]) == {"scene", "knowledge_base"}
|
|
assert "config" in inputs["optional"]
|
|
|
|
|
|
def test_resolves_to_flat_prompt_with_real_resolver() -> None:
|
|
prompt, warnings, errors, json_out = ResolverNode().run(scene(), kb())
|
|
assert prompt == "1girl,long hair" # resolution order, comma-separated
|
|
assert errors == ""
|
|
assert "1girl" in json_out # traceable resolved-tags JSON
|
|
|
|
|
|
def test_delegates_and_surfaces_messages(monkeypatch) -> None:
|
|
def fake_resolve(scene_arg, knowledge_base, config, embedding_index=None):
|
|
return CompilerResult(data=()).add_warning(message("SC0001", "unknown 'x'"))
|
|
|
|
monkeypatch.setattr(node_module, "resolve_scene", fake_resolve)
|
|
prompt, warnings, errors, *_ = ResolverNode().run(scene(), kb())
|
|
assert prompt == ""
|
|
assert "SC0001: unknown 'x'" in warnings
|
|
|
|
|
|
def test_default_config_used_when_absent(monkeypatch) -> None:
|
|
captured = {}
|
|
|
|
def fake_resolve(scene_arg, knowledge_base, config, embedding_index=None):
|
|
captured["config"] = config
|
|
return CompilerResult(data=())
|
|
|
|
monkeypatch.setattr(node_module, "resolve_scene", fake_resolve)
|
|
ResolverNode().run(scene(), kb())
|
|
assert isinstance(captured["config"], Config)
|
|
|
|
|
|
def test_registered_for_comfyui() -> None:
|
|
assert nodes.NODE_CLASS_MAPPINGS["SceneCompilerResolver"] is ResolverNode
|
|
assert nodes.NODE_DISPLAY_NAME_MAPPINGS["SceneCompilerResolver"] == "Resolver"
|