Files
olivv-cs-ComfyUI-FunPack/core/tests/test_nodes.py
T
digital-garbageandClaude Opus 5 eaae196c1f core: four holes the attacker found in the node layer
The naming of a node happened outside the guard: getattr's default is
evaluated eagerly, so a class that raises in __repr__ threw before the try
and cost every other module its nodes -- which ComfyUI reports as the whole
pack failing.

`get("nodes") or []` repaired a typo into "ships no nodes". Absent is none;
0 is a mistake, and this file refuses rather than repairs. Same for the id
lists.

A probe by bare class name matches Attention or Block from any architecture,
so a dotted name now requires the defining module too.

And the fixture that claimed a duplicate id sorted BEFORE the good one, so
every test asserting "the good node was collected" was satisfied by the
lookalike. The tests now assert identity, and the duplicate is made to lose.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 03:13:32 +03:00

173 lines
5.6 KiB
Python

"""Announcing ComfyUI nodes.
Core registers whatever modules declared and nothing it names itself. These tests
use stub node classes rather than real comfy_api ones on purpose: `collect()` only
ever asks a node for its schema, so it stays provable without ComfyUI installed --
the same property the rest of the suite has.
"""
from pathlib import Path
import pytest
from core import nodes as nodes_mod
from core import registry as registry_mod
from core.schema import SchemaError, validate
FIXTURES = Path(__file__).parent / "fixtures" / "fixture_modules"
PACKAGE = "core.tests.fixtures.fixture_modules"
@pytest.fixture(scope="module")
def collected():
registry = registry_mod.scan(FIXTURES, package=PACKAGE)
return registry, nodes_mod.collect(registry)
def _ids(nodes):
return [n.GET_SCHEMA().node_id for n in nodes]
def _good():
"""The real GoodNode class, by identity."""
from core.tests.fixtures.fixture_modules.nodes.good import GoodNode
return GoodNode
def test_an_announced_node_is_collected(collected):
_, (nodes, _) = collected
# By identity. Asserting on the id alone let a DIFFERENT fixture module that
# claims the same id satisfy this, so the test passed without the module
# under test contributing anything.
assert _good() in nodes
def test_a_node_outside_the_funpack_namespace_is_refused(collected):
_, (nodes, rejected) = collected
# ComfyUI would let this silently replace core's own VAELoader.
assert "VAELoader" not in _ids(nodes)
assert any("VAELoader" in why for _, why in rejected)
def test_two_modules_cannot_claim_one_node_id(collected):
_, (nodes, rejected) = collected
assert _ids(nodes).count("FunPackGood") == 1
# And the one that survived is the first module to claim it, not whichever
# happened to be reached last.
assert _good() in nodes
assert any("duplicate node_id" in why and "nodes_zz_clashes" in where
for where, why in rejected)
def test_something_that_is_not_a_comfy_node_is_refused(collected):
_, (nodes, rejected) = collected
assert any("not a comfy_api" in why for _, why in rejected)
# And it did not stop the good one being collected.
assert _good() in nodes
def test_a_node_whose_schema_raises_does_not_take_the_others_down():
class Exploding:
@classmethod
def define_schema(cls):
raise RuntimeError("boom")
@classmethod
def execute(cls):
return None
@classmethod
def GET_SCHEMA(cls):
raise RuntimeError("boom")
registry = registry_mod.scan(FIXTURES, package=PACKAGE)
spec = registry.specs["nodes_good"]
registry.specs["nodes_good"] = type(spec)(
**{**spec.__dict__, "nodes": [Exploding, *spec.nodes]}
)
nodes, rejected = nodes_mod.collect(registry)
assert _good() in nodes, "a raising sibling took the good node with it"
assert any("boom" in why for _, why in rejected)
def test_a_node_only_module_needs_no_mount(collected):
registry, _ = collected
assert registry.specs["nodes_good"].mount == ""
def test_a_module_with_settings_still_needs_a_mount():
with pytest.raises(SchemaError, match="mount"):
validate({
"id": "m", "title": "M",
"settings": {"x": {"type": "bool", "default": True, "label": "X"}},
})
@pytest.mark.parametrize("bad", [
"not a list",
["not a class"],
[object], # a class, but with no define_schema/execute
])
def test_a_malformed_nodes_declaration_refuses_the_module(bad):
with pytest.raises(SchemaError):
validate({"id": "m", "title": "M", "nodes": bad})
def test_a_node_that_cannot_be_named_does_not_take_the_others_down():
"""`getattr(node, "__name__", repr(node))` evaluates repr() eagerly, so a
class whose metaclass raises there threw outside the guard and killed the
whole collection -- and ComfyUI reports that as the entire pack failing."""
class Meta(type):
def __repr__(cls):
raise RuntimeError("unreprable")
def __getattribute__(cls, item):
if item == "__name__":
raise RuntimeError("unnameable")
return type.__getattribute__(cls, item)
class Hostile(metaclass=Meta):
@classmethod
def define_schema(cls):
return None
@classmethod
def execute(cls):
return None
@classmethod
def GET_SCHEMA(cls):
return None
registry = registry_mod.scan(FIXTURES, package=PACKAGE)
spec = registry.specs["nodes_good"]
registry.specs["nodes_good"] = type(spec)(
**{**spec.__dict__, "nodes": [Hostile, *spec.nodes]}
)
nodes, rejected = nodes_mod.collect(registry)
assert _good() in nodes
assert any("unnameable" in why or "unreprable" in why for _, why in rejected)
@pytest.mark.parametrize("falsy", [0, False, ""])
def test_a_falsy_but_wrong_nodes_declaration_is_refused_not_repaired(falsy):
"""`announcement.get("nodes") or []` quietly turned a typo into "this module
ships no nodes". This file refuses rather than repairs, and 0 is not []."""
with pytest.raises(SchemaError, match="must be a list"):
validate({"id": "m", "title": "M", "nodes": falsy})
@pytest.mark.parametrize("field", ["requires", "after", "before"])
def test_a_falsy_but_wrong_id_list_is_refused_too(field):
with pytest.raises(SchemaError, match="must be a list"):
validate({"id": "m", "title": "M", field: 0})
def test_an_absent_declaration_is_still_fine():
# Absent means "none", which is different from a typo.
spec = validate({"id": "m", "title": "M"})
assert spec.nodes == [] and spec.requires == []