* Add queue-driven loop status node * Document ComfyUI server API testing * Harden loop queue controller * Guard loop controller HTTP requests
187 lines
6.2 KiB
Python
187 lines
6.2 KiB
Python
"""Regression tests for scripts/validate_workflow.py.
|
|
|
|
Anchored by PR #170 review: the validator must report problems rather than
|
|
raise on malformed-but-valid JSON. These tests pin that behavior plus a
|
|
sampling of the existing happy-path and consistency checks so future edits
|
|
cannot silently regress the safety guarantees.
|
|
"""
|
|
from __future__ import annotations
|
|
import importlib.util
|
|
import json
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
# Load the script directly — it lives under scripts/, not under a package.
|
|
_SCRIPT = Path(__file__).resolve().parents[2] / "scripts" / "validate_workflow.py"
|
|
_spec = importlib.util.spec_from_file_location("validate_workflow", _SCRIPT)
|
|
assert _spec is not None and _spec.loader is not None
|
|
_module = importlib.util.module_from_spec(_spec)
|
|
_spec.loader.exec_module(_module)
|
|
validate = _module.validate
|
|
main = _module.main
|
|
|
|
|
|
def _minimal_clean() -> dict:
|
|
"""Smallest workflow that should validate cleanly: two nodes, one link."""
|
|
return {
|
|
"last_node_id": 2,
|
|
"last_link_id": 1,
|
|
"nodes": [
|
|
{
|
|
"id": 1,
|
|
"type": "Src",
|
|
"inputs": [],
|
|
"outputs": [
|
|
{"name": "out", "type": "FOO", "links": [1]},
|
|
],
|
|
},
|
|
{
|
|
"id": 2,
|
|
"type": "Dst",
|
|
"inputs": [
|
|
{"name": "in", "type": "FOO", "link": 1},
|
|
],
|
|
"outputs": [],
|
|
},
|
|
],
|
|
"links": [[1, 1, 0, 2, 0, "FOO"]],
|
|
}
|
|
|
|
|
|
# --- Happy path -------------------------------------------------------------
|
|
|
|
def test_minimal_clean_workflow_has_no_problems() -> None:
|
|
assert validate(_minimal_clean()) == []
|
|
|
|
|
|
# --- PR #170 review repros: malformed-but-valid JSON must not crash --------
|
|
|
|
@pytest.mark.parametrize("data", [None, 42, 3.14, "string", [1, 2], (1, 2)])
|
|
def test_top_level_not_object_reports_does_not_crash(data) -> None:
|
|
problems = validate(data)
|
|
assert problems, "expected at least one problem"
|
|
assert "top-level JSON must be an object" in problems[0]
|
|
|
|
|
|
def test_node_entry_not_dict_reports_does_not_crash() -> None:
|
|
# Reviewer's repro 1: nodes contains an int.
|
|
data = {"nodes": [1], "links": [], "last_node_id": 0, "last_link_id": 0}
|
|
problems = validate(data)
|
|
assert any("nodes[0]" in p and "object" in p for p in problems), problems
|
|
|
|
|
|
def test_nodes_field_not_list_reports() -> None:
|
|
data = {"nodes": {"oops": "dict not list"}, "links": []}
|
|
problems = validate(data)
|
|
assert any("'nodes' must be a list" in p for p in problems), problems
|
|
|
|
|
|
def test_links_field_not_list_reports() -> None:
|
|
data = {"nodes": [], "links": "not-a-list"}
|
|
problems = validate(data)
|
|
assert any("'links' must be a list" in p for p in problems), problems
|
|
|
|
|
|
def test_inputs_field_not_list_reports() -> None:
|
|
data = _minimal_clean()
|
|
data["nodes"][1]["inputs"] = {"oops": "dict not list"}
|
|
problems = validate(data)
|
|
assert any("'inputs' must be a list" in p for p in problems), problems
|
|
|
|
|
|
def test_input_entry_not_dict_reports() -> None:
|
|
data = _minimal_clean()
|
|
data["nodes"][1]["inputs"] = ["string-not-dict"]
|
|
problems = validate(data)
|
|
assert any("inputs[0]" in p and "object" in p for p in problems), problems
|
|
|
|
|
|
def test_output_entry_not_dict_reports() -> None:
|
|
data = _minimal_clean()
|
|
data["nodes"][0]["outputs"] = [99]
|
|
problems = validate(data)
|
|
assert any("outputs[0]" in p and "object" in p for p in problems), problems
|
|
|
|
|
|
def test_malformed_link_not_list_reports() -> None:
|
|
data = _minimal_clean()
|
|
data["links"].append("not-a-list")
|
|
problems = validate(data)
|
|
assert any("links[1]" in p for p in problems), problems
|
|
|
|
|
|
def test_malformed_link_wrong_length_reports() -> None:
|
|
data = _minimal_clean()
|
|
data["links"].append([2, 1, 0]) # too short
|
|
problems = validate(data)
|
|
assert any("links[1]" in p and "6 elements" in p for p in problems), problems
|
|
|
|
|
|
# --- Existing consistency checks -------------------------------------------
|
|
|
|
def test_link_to_missing_src_node_reports() -> None:
|
|
data = _minimal_clean()
|
|
data["links"] = [[1, 99, 0, 2, 0, "FOO"]]
|
|
data["nodes"][0]["outputs"][0]["links"] = [] # not referenced from anywhere now
|
|
problems = validate(data)
|
|
assert any("src node 99 missing" in p for p in problems), problems
|
|
|
|
|
|
def test_type_mismatch_on_slot_reports() -> None:
|
|
data = _minimal_clean()
|
|
data["links"] = [[1, 1, 0, 2, 0, "BAR"]] # link declares BAR, slots are FOO
|
|
problems = validate(data)
|
|
assert any("type mismatch" in p for p in problems), problems
|
|
|
|
|
|
def test_wildcard_destination_accepts_concrete_link_type() -> None:
|
|
data = _minimal_clean()
|
|
data["nodes"][1]["inputs"][0]["type"] = "*"
|
|
assert validate(data) == []
|
|
|
|
|
|
def test_duplicate_node_ids_report() -> None:
|
|
data = _minimal_clean()
|
|
data["nodes"][1]["id"] = 1 # collision with first node
|
|
problems = validate(data)
|
|
assert any("Duplicate node IDs" in p for p in problems), problems
|
|
|
|
|
|
def test_endpoint_ref_desync_reports() -> None:
|
|
data = _minimal_clean()
|
|
data["nodes"][0]["outputs"][0]["links"] = [] # source side forgot link 1
|
|
problems = validate(data)
|
|
assert any("missing from node 1 output[0]" in p for p in problems), problems
|
|
|
|
|
|
def test_last_node_id_too_low_reports() -> None:
|
|
data = _minimal_clean()
|
|
data["last_node_id"] = 0 # but max is 2
|
|
problems = validate(data)
|
|
assert any("last_node_id" in p for p in problems), problems
|
|
|
|
|
|
# --- CLI smoke (covers main() exit codes) ----------------------------------
|
|
|
|
def test_cli_returns_zero_on_clean_workflow(tmp_path: Path) -> None:
|
|
p = tmp_path / "clean.json"
|
|
p.write_text(json.dumps(_minimal_clean()), encoding="utf-8")
|
|
assert main([str(p), "--quiet"]) == 0
|
|
|
|
|
|
def test_cli_returns_one_on_problems(tmp_path: Path) -> None:
|
|
p = tmp_path / "broken.json"
|
|
p.write_text(json.dumps({"nodes": [1], "links": []}), encoding="utf-8")
|
|
assert main([str(p), "--quiet"]) == 1
|
|
|
|
|
|
def test_cli_returns_two_on_missing_file(tmp_path: Path) -> None:
|
|
assert main([str(tmp_path / "does-not-exist.json"), "--quiet"]) == 2
|
|
|
|
|
|
def test_cli_returns_two_on_invalid_json(tmp_path: Path) -> None:
|
|
p = tmp_path / "bad.json"
|
|
p.write_text("{not valid", encoding="utf-8")
|
|
assert main([str(p), "--quiet"]) == 2
|