ComfyUI/tests-unit/execution_test/validate_node_executable_test.py
Wei Hai 82c954bd2a Validate that a node is executable before running the prompt
A node whose FUNCTION points at a method that does not exist (e.g. a typo in
a custom node), or a V3 node missing its execute override, was only detected
once that node ran -- after every upstream node had already executed. In a
multi-node workflow the user waited for the whole graph to run up to the
broken node before seeing the error.

validate_prompt already walks every node before execution; add an
executability check there so the error is reported up front and attributed
to the offending node (returned in node_errors), and nothing runs.

The check resolves the V1 FUNCTION method on the class (the common case) and
falls back to an instance, since the runtime invokes it on an instance and a
node may define FUNCTION or its method in __init__. V3 nodes are checked via
their existing VALIDATE_CLASS.

Add tests for V1 typo, V3 typo, good nodes, and a node whose method is
defined in __init__ (must not be falsely rejected).
2026-06-26 15:53:34 -07:00

135 lines
3.6 KiB
Python

"""Tests for pre-execution validation that a node is actually executable.
validate_prompt rejects a node whose declared entry point does not resolve to a
real method (a V1 FUNCTION typo, or a V3 node missing its execute override) before
any node runs, attributing the error to the offending node.
"""
import asyncio
import nodes
from comfy_api.latest import io
from execution import node_not_executable_reason, validate_prompt
class _GoodV1Node:
@classmethod
def INPUT_TYPES(cls):
return {"required": {}}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "run"
OUTPUT_NODE = True
CATEGORY = "Test"
def run(self):
return (None,)
class _TypoV1Node:
@classmethod
def INPUT_TYPES(cls):
return {"required": {}}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "invert" # method below is misspelled
OUTPUT_NODE = True
CATEGORY = "Test"
def invvert(self):
return (None,)
class _InstanceMethodV1Node:
"""Defines its FUNCTION method on the instance, as the runtime resolves it."""
@classmethod
def INPUT_TYPES(cls):
return {"required": {}}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "run"
OUTPUT_NODE = True
CATEGORY = "Test"
def __init__(self):
self.run = lambda: (None,)
def _v3_schema(node_id):
return io.Schema(
node_id=node_id,
display_name=node_id,
category="Test",
inputs=[],
outputs=[io.Image.Output()],
is_output_node=True,
)
class _GoodV3Node(io.ComfyNode):
@classmethod
def define_schema(cls):
return _v3_schema("GoodV3Node")
@classmethod
def execute(cls):
return io.NodeOutput(None)
class _TypoV3Node(io.ComfyNode):
@classmethod
def define_schema(cls):
return _v3_schema("TypoV3Node")
@classmethod
def exicute(cls): # typo: should be "execute"
return io.NodeOutput(None)
def _register(class_type, class_def):
nodes.NODE_CLASS_MAPPINGS[class_type] = class_def
def _validate(class_type):
prompt = {"1": {"class_type": class_type, "inputs": {}}}
return asyncio.run(validate_prompt("pid", prompt, None))
def test_good_node_passes():
_register("GoodV1Node", _GoodV1Node)
assert node_not_executable_reason(_GoodV1Node, "GoodV1Node") is None
valid, _, _, _ = _validate("GoodV1Node")
assert valid is True
def test_typo_node_rejected_with_node_error():
_register("TypoV1Node", _TypoV1Node)
valid, error, _, node_errors = _validate("TypoV1Node")
assert valid is False
assert error["type"] == "invalid_node_definition"
assert node_errors["1"]["class_type"] == "TypoV1Node"
assert node_errors["1"]["errors"][0]["type"] == "invalid_node_definition"
assert "invert" in node_errors["1"]["errors"][0]["details"]
def test_instance_defined_method_not_false_positived():
"""A node whose method is defined in __init__ runs fine and must not be blocked."""
_register("InstanceMethodV1Node", _InstanceMethodV1Node)
assert node_not_executable_reason(_InstanceMethodV1Node, "InstanceMethodV1Node") is None
valid, _, _, _ = _validate("InstanceMethodV1Node")
assert valid is True
def test_good_v3_node_passes():
_register("GoodV3Node", _GoodV3Node)
assert node_not_executable_reason(_GoodV3Node, "GoodV3Node") is None
valid, _, _, _ = _validate("GoodV3Node")
assert valid is True
def test_typo_v3_node_rejected_with_node_error():
_register("TypoV3Node", _TypoV3Node)
valid, error, _, node_errors = _validate("TypoV3Node")
assert valid is False
assert error["type"] == "invalid_node_definition"
assert node_errors["1"]["errors"][0]["type"] == "invalid_node_definition"