"""Tests for the custom node startup error tracking introduced for Comfy-Org/ComfyUI-Launcher#303. Covers: - load_custom_node populates NODE_STARTUP_ERRORS with the correct source for each module_parent (custom_nodes / comfy_extras / comfy_api_nodes). - Composite keying prevents collisions between modules with the same name in different sources. - record_node_startup_error stores the expected fields. """ import textwrap import pytest import nodes @pytest.fixture(autouse=True) def _clear_startup_errors(): nodes.NODE_STARTUP_ERRORS.clear() yield nodes.NODE_STARTUP_ERRORS.clear() def _write_broken_module(tmp_path, name: str) -> str: path = tmp_path / f"{name}.py" path.write_text(textwrap.dedent("""\ # Deliberately broken module to exercise startup-error tracking. raise RuntimeError("boom from " + __name__) """)) return str(path) def test_record_node_startup_error_fields(tmp_path): err = ValueError("kaboom") nodes.record_node_startup_error( module_path=str(tmp_path / "my_pack"), source="custom_node", phase="import", error=err, tb="traceback-text", ) assert "custom_node:my_pack" in nodes.NODE_STARTUP_ERRORS entry = nodes.NODE_STARTUP_ERRORS["custom_node:my_pack"] assert entry["source"] == "custom_node" assert entry["module_name"] == "my_pack" assert entry["phase"] == "import" assert entry["error"] == "kaboom" assert entry["traceback"] == "traceback-text" assert entry["module_path"].endswith("my_pack") @pytest.mark.asyncio @pytest.mark.parametrize( "module_parent,expected_source", [ ("custom_nodes", "custom_node"), ("comfy_extras", "comfy_extra"), ("comfy_api_nodes", "api_node"), ], ) async def test_load_custom_node_records_source(tmp_path, module_parent, expected_source): module_path = _write_broken_module(tmp_path, "broken_pack") success = await nodes.load_custom_node(module_path, module_parent=module_parent) assert success is False key = f"{expected_source}:broken_pack" assert key in nodes.NODE_STARTUP_ERRORS, nodes.NODE_STARTUP_ERRORS entry = nodes.NODE_STARTUP_ERRORS[key] assert entry["source"] == expected_source assert entry["module_name"] == "broken_pack" assert entry["phase"] == "import" assert "boom from" in entry["error"] assert "RuntimeError" in entry["traceback"] @pytest.mark.asyncio async def test_load_custom_node_collision_across_sources(tmp_path): # Same module name registered as both a custom_node and a comfy_extra; # composite keying should keep both entries. cn_dir = tmp_path / "cn" extras_dir = tmp_path / "extras" cn_dir.mkdir() extras_dir.mkdir() cn_path = _write_broken_module(cn_dir, "nodes_audio") extras_path = _write_broken_module(extras_dir, "nodes_audio") assert await nodes.load_custom_node(cn_path, module_parent="custom_nodes") is False assert await nodes.load_custom_node(extras_path, module_parent="comfy_extras") is False assert "custom_node:nodes_audio" in nodes.NODE_STARTUP_ERRORS assert "comfy_extra:nodes_audio" in nodes.NODE_STARTUP_ERRORS assert ( nodes.NODE_STARTUP_ERRORS["custom_node:nodes_audio"]["module_path"] != nodes.NODE_STARTUP_ERRORS["comfy_extra:nodes_audio"]["module_path"] ) def test_unknown_module_parent_defaults_to_custom_node(): assert nodes._node_source_from_parent("custom_nodes") == "custom_node" assert nodes._node_source_from_parent("comfy_extras") == "comfy_extra" assert nodes._node_source_from_parent("comfy_api_nodes") == "api_node" assert nodes._node_source_from_parent("something_else") == "custom_node"