Download ComfyUI/custom_nodes/koolook/tests/scripts/test_validate_workflow.py from vidfom/aimusic: direct link, hf CLI and curl.
- Browser
- Download file 6.4 kB
-
https://huggingface.co/vidfom/aimusic/resolve/main/ComfyUI/custom_nodes/koolook/tests/scripts/test_validate_workflow.py
- Command line
-
hf download hf://vidfom/aimusic/ComfyUI/custom_nodes/koolook/tests/scripts/test_validate_workflow.py
-
curl -L -o test_validate_workflow.py https://huggingface.co/vidfom/aimusic/resolve/main/ComfyUI/custom_nodes/koolook/tests/scripts/test_validate_workflow.py
6.4 kB
| """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 -------- | |
| 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 | |