Files
SurrealByDesign 79a8de97e1 Add IdeogramVisualPromptDesigner (Phase 1) and fix ensure_ascii bug across three nodes
IdeogramVisualPromptDesigner is the backend foundation for the planned
Visual Prompt Designer: builds a prompt from scratch or loads an existing
one, reconstructs an editable internal "designer state," restores
designer-only fields (subjects/hierarchy/relationships/notes/per-element
depth-weight-lock-hide-group) from a previously saved state, applies an
optional palette override, and re-validates with the existing
IdeogramJSONValidator. The drag/resize canvas, scene graph, and
relationship UI from the full spec need a custom ComfyUI frontend
extension and are a separate, future phase -- see README's new
"Ideogram Visual Prompt Designer" section for the status table,
architecture, round-trip workflow, and known limitations.

Found and fixed a real bug while building it: IdeogramJSONValidator,
IdeogramPromptAssembler, and IdeogramPaletteToGlobalJSON were all
missing ensure_ascii=False, silently escaping non-ASCII content into
\uXXXX sequences -- inconsistent with every other JSON-producing node
in this repo. IdeogramJSONValidator also gained two new outputs,
errors_json/warnings_json, added after the existing three so saved
workflows' connections by index are unaffected; updated the three
showcase workflows that reference it to match.

This went through a full pre-publish audit pass (category convention,
dead constants, a decorative non-functional id field removed, a no-op
reference_image input removed, validation structure) and was verified
end-to-end against a live ComfyUI server multiple times, including the
full round-trip workflow.

126 tests passing (up from 102).
2026-06-22 22:53:27 -04:00

177 lines
6.8 KiB
Python

"""Tests for IdeogramJSONValidator: parsing, key-order correction, schema-size
limits, hex validation, bbox bounds, structured errors_json/warnings_json
outputs, and graceful failure."""
import json
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from nodes.json_validator import IdeogramJSONValidator
def _node():
return IdeogramJSONValidator()
def _well_ordered(color_palette=None, elements=None):
style = {"aesthetics": "a", "lighting": "l", "color_palette": color_palette if color_palette is not None else ["#FF5733"]}
comp = {"background": "bg", "elements": elements if elements is not None else []}
return {
"high_level_description": "desc",
"style_description": style,
"compositional_deconstruction": comp,
}
def test_valid_well_ordered_passes():
out_json, is_valid, report, *_ = _node().validate(json.dumps(_well_ordered()), True, False)
assert is_valid is True
assert "PASS" in report
parsed = json.loads(out_json)
assert list(parsed.keys()) == ["high_level_description", "style_description", "compositional_deconstruction"]
def test_wrong_key_order_is_corrected():
# Deliberately scrambled key order at both levels.
scrambled = {
"compositional_deconstruction": {"elements": []},
"style_description": {"color_palette": ["#FF5733"], "lighting": "l", "aesthetics": "a"},
"high_level_description": "desc",
}
out_json, is_valid, *_ = _node().validate(json.dumps(scrambled), True, False)
parsed = json.loads(out_json)
assert list(parsed.keys()) == ["high_level_description", "style_description", "compositional_deconstruction"]
assert list(parsed["style_description"].keys()) == ["aesthetics", "lighting", "color_palette"]
assert is_valid is True
def test_key_order_preserved_when_fix_disabled():
scrambled = {"style_description": {"color_palette": []}, "high_level_description": "d"}
out_json, *_ = _node().validate(json.dumps(scrambled), False, False)
# With fixing off, the original string is returned unchanged.
assert json.loads(out_json) == scrambled
def test_invalid_json_does_not_crash():
out_json, is_valid, report, *_ = _node().validate("{not valid json", True, False)
assert is_valid is False
assert out_json == "{not valid json" # original returned untouched
assert "invalid JSON" in report
def test_global_palette_over_16_fails():
palette = [f"#{i:06X}" for i in range(17)] # 17 colors
out_json, is_valid, report, *_ = _node().validate(json.dumps(_well_ordered(color_palette=palette)), True, False)
assert is_valid is False
assert "max 16" in report
def test_element_palette_over_5_fails():
elements = [{"type": "obj", "desc": "x", "color_palette": [f"#{i:06X}" for i in range(6)]}]
out_json, is_valid, report, *_ = _node().validate(json.dumps(_well_ordered(elements=elements)), True, False)
assert is_valid is False
assert "max 5" in report
def test_malformed_hex_is_reported():
out_json, is_valid, report, *_ = _node().validate(
json.dumps(_well_ordered(color_palette=["#FF5733", "not-a-color", "#GGGGGG"])), True, False
)
# Bad hex is a warning, not an error -> still valid in non-strict mode.
assert is_valid is True
assert "malformed hex" in report
def test_malformed_hex_fails_in_strict_mode():
out_json, is_valid, report, *_ = _node().validate(
json.dumps(_well_ordered(color_palette=["bad"])), True, True
)
assert is_valid is False
def test_bbox_out_of_range_fails():
elements = [{"type": "obj", "desc": "TEXT: 'HI' bbox=[250,120,1200,220]"}]
out_json, is_valid, report, *_ = _node().validate(json.dumps(_well_ordered(elements=elements)), True, False)
assert is_valid is False
assert "bbox" in report and "out of range" in report
def test_missing_top_level_key_warns_but_can_pass():
minimal = {"style_description": {"color_palette": ["#FF5733"]}}
out_json, is_valid, report, *_ = _node().validate(json.dumps(minimal), True, False)
assert "missing top-level key" in report
assert is_valid is True # missing keys are warnings, not errors
def test_content_values_unchanged():
original = _well_ordered(color_palette=["#FF5733", "#C70039"])
out_json, *_ = _node().validate(json.dumps(original), True, False)
parsed = json.loads(out_json)
assert parsed["high_level_description"] == "desc"
assert parsed["style_description"]["color_palette"] == ["#FF5733", "#C70039"]
def test_errors_json_and_warnings_json_are_valid_json_arrays_when_clean():
_, is_valid, _, errors_json, warnings_json = _node().validate(json.dumps(_well_ordered()), True, False)
assert is_valid is True
assert json.loads(errors_json) == []
assert json.loads(warnings_json) == []
def test_errors_json_contains_structured_error_messages():
palette = [f"#{i:06X}" for i in range(17)]
_, is_valid, report, errors_json, warnings_json = _node().validate(
json.dumps(_well_ordered(color_palette=palette)), True, False
)
errors = json.loads(errors_json)
assert is_valid is False
assert len(errors) == 1
assert "max 16" in errors[0]
# same content as the prose report, just structured instead of text
assert errors[0] in report
assert json.loads(warnings_json) == []
def test_warnings_json_contains_structured_warning_messages():
_, is_valid, report, errors_json, warnings_json = _node().validate(
json.dumps(_well_ordered(color_palette=["not-a-color"])), True, False
)
warnings = json.loads(warnings_json)
assert is_valid is True
assert json.loads(errors_json) == []
assert len(warnings) == 1
assert "malformed hex" in warnings[0]
assert warnings[0] in report
def test_errors_json_present_even_on_invalid_json_input():
_, is_valid, _, errors_json, warnings_json = _node().validate("{not valid json", True, False)
assert is_valid is False
errors = json.loads(errors_json)
assert len(errors) == 1
assert "invalid JSON" in errors[0]
assert json.loads(warnings_json) == []
def test_errors_json_and_warnings_json_preserve_non_ascii_literally():
# A malformed hex entry containing non-ASCII text ends up quoted (via
# !r) inside the warning message itself -- confirm it survives into
# errors_json/warnings_json as literal UTF-8, not \uXXXX escapes.
_, is_valid, _, errors_json, warnings_json = _node().validate(
json.dumps(_well_ordered(color_palette=["café"])), True, False
)
assert is_valid is True
assert "café" in warnings_json
assert "\\u" not in warnings_json
assert json.loads(errors_json) == []
if __name__ == "__main__":
for name, fn in list(globals().items()):
if name.startswith("test_") and callable(fn):
fn()
print(f"{name}: OK")
print("All json_validator tests passed.")