Files
Artificial-Sweetener-Simple…/tests/test_registration.py
T

231 lines
8.0 KiB
Python

# SimpleSyrup - workflow-focused ComfyUI extensions for image generation
# Copyright (C) 2026 Artificial Sweetener and contributors
# SPDX-License-Identifier: AGPL-3.0-or-later
"""Tests for SimpleSyrup Comfy v3-only node registration."""
from __future__ import annotations
import asyncio
import importlib
import subprocess
import sys
from pathlib import Path
from types import ModuleType
from typing import Any, Protocol, cast
import pytest
BASE_NODE_IDS = [
"SimpleSyrup.AllPromptAttentionSEGS",
"SimpleSyrup.AttentionCaptureModel",
"SimpleSyrup.AttentionMaskedConditioning",
"SimpleSyrup.AttentionRegionMask",
"SimpleSyrup.BatchRegionConditioning",
"SimpleSyrup.BatchSEGS",
"SimpleSyrup.ConditioningBatchAppend",
"SimpleSyrup.ConditioningBatchStart",
"SimpleSyrup.ComposeRegionalConditioning",
"SimpleSyrup.DetailSEGSAsRegions",
"SimpleSyrup.DetailSEGSByScaleFactorTiledDiffusion",
"SimpleSyrup.DetailSEGSByScaleFactor",
"SimpleSyrup.DetectSEGSWithUltralytics",
"SimpleSyrup.EncodePromptBatch",
"SimpleSyrup.ExternalLLMPrompt",
"SimpleSyrup.GroundedSAMModelInfo",
"SimpleSyrup.GroundingDINOModelLoader",
"SimpleSyrup.KSamplerExtras",
"SimpleSyrup.KSamplerAttentionCoupling",
"SimpleSyrup.KSamplerAttentionCouplingTiled",
"SimpleSyrup.KSamplerAttentionCouplingContextual",
"SimpleSyrup.KSamplerPromptByRegion",
"SimpleSyrup.KSamplerPromptByTiledRegion",
"SimpleSyrup.KSamplerContextualDiffusion",
"SimpleSyrup.KSamplerTiledDiffusion",
"SimpleSyrup.LatentDiagnostics",
"SimpleSyrup.LayerStyleSAMModelsAdapter",
"SimpleSyrup.LoadUltralyticsModel",
"SimpleSyrup.LoadImageList",
"SimpleSyrup.LoadMaskBatch",
"SimpleSyrup.MaskToSEGS",
"SimpleSyrup.PromptEncodeStyleAndNormalization",
"SimpleSyrup.PromptEncodeStyle",
"SimpleSyrup.PromptSEGSWithSAM",
"SimpleSyrup.ResizeImageToTarget",
"SimpleSyrup.SAMModelLoader",
"SimpleSyrup.SEGSFromSAMOutput",
"SimpleSyrup.ScaleFactor",
"SimpleSyrup.ConceptAttentionSEGS",
"SimpleSyrup.Seed",
"SimpleSyrup.SeedVariation",
"SimpleSyrup.SimpleLoadAnima",
"SimpleSyrup.SimplePreviewSEGS",
"SimpleSyrup.SimpleLoadCheckpoint",
"SimpleSyrup.SimpleLoadFlux",
"SimpleSyrup.SimpleLoadFlux2",
"SimpleSyrup.SimpleVAEEncode",
"SimpleSyrup.TagSEGSWithExternalLLM",
"SimpleSyrup.TagSEGSWithWD14",
"SimpleSyrup.TileAndTagSEGS",
"SimpleSyrup.UpscaleLatentFromImage",
"SimpleSyrup.VAEDecodeOptions",
"SimpleSyrup.VAEEncodeOptions",
"SimpleSyrup.ViTMatteModelLoader",
"SimpleSyrup.WD14TaggerLoader",
]
PROMPT_CONTROL_NODE_IDS = [
"SimpleSyrup.AttachRegionalGlobalConditioning",
"SimpleSyrup.EncodePromptBatchWithPromptControl",
"SimpleSyrup.LabelRegionalLoraHooks",
"SimpleSyrup.PrepareRegionalLoraHooks",
"SimpleSyrup.ScheduleAndEncodePromptsWithPromptControl",
]
class _V3Node(Protocol):
"""Protocol for Comfy v3 node schema declarations."""
@classmethod
def define_schema(cls) -> Any:
"""Return a Comfy v3 schema object."""
def test_package_exports_v3_entrypoint_only() -> None:
"""Root package exposes Comfy v3 registration and no legacy mappings."""
package = importlib.import_module("SimpleSyrup")
assert hasattr(package, "comfy_entrypoint")
assert package.WEB_DIRECTORY == "./web/dist"
assert not hasattr(package, "NODE_CLASS_MAPPINGS")
assert not hasattr(package, "NODE_DISPLAY_NAME_MAPPINGS")
assert package.__all__ == ["WEB_DIRECTORY", "comfy_entrypoint"]
def test_nodes_package_is_not_a_legacy_registry() -> None:
"""The implementation package no longer owns ComfyUI registration."""
nodes_package = importlib.import_module("SimpleSyrup.simple_syrup.nodes")
assert not hasattr(nodes_package, "NODE_CLASS_MAPPINGS")
assert not hasattr(nodes_package, "NODE_DISPLAY_NAME_MAPPINGS")
def test_package_imports_from_custom_nodes_parent_path() -> None:
"""ComfyUI-style import works without the repository root on sys.path."""
project_root = Path(__file__).resolve().parents[1]
custom_nodes_root = project_root.parent
script = (
"import importlib, pathlib, sys; "
f"project = pathlib.Path({str(project_root)!r}).resolve(); "
"sys.path = [p for p in sys.path "
"if pathlib.Path(p or '.').resolve() != project]; "
f"sys.path.insert(0, {str(custom_nodes_root)!r}); "
"package = importlib.import_module('SimpleSyrup'); "
"assert hasattr(package, 'comfy_entrypoint'); "
"assert not hasattr(package, 'NODE_CLASS_MAPPINGS'); "
"assert not hasattr(package, 'NODE_DISPLAY_NAME_MAPPINGS'); "
"assert 'server' not in sys.modules"
)
result = subprocess.run(
[sys.executable, "-c", script],
cwd=custom_nodes_root,
check=False,
capture_output=True,
text=True,
)
assert result.returncode == 0, result.stderr
def test_comfy_import_exposes_stable_internal_package_alias() -> None:
"""ComfyUI-style import exposes `simple_syrup` to nested vendored packages."""
project_root = Path(__file__).resolve().parents[1]
custom_nodes_root = project_root.parent
script = (
"import importlib, pathlib, sys; "
f"project = pathlib.Path({str(project_root)!r}).resolve(); "
"sys.path = [p for p in sys.path "
"if pathlib.Path(p or '.').resolve() != project]; "
f"sys.path.insert(0, {str(custom_nodes_root)!r}); "
"importlib.import_module('SimpleSyrup'); "
"runtime = importlib.import_module("
"'simple_syrup.third_party.groundingdino_runtime.util'"
"); "
"assert runtime.__name__.endswith('groundingdino_runtime.util')"
)
result = subprocess.run(
[sys.executable, "-c", script],
cwd=custom_nodes_root,
check=False,
capture_output=True,
text=True,
)
assert result.returncode == 0, result.stderr
def test_registration_import_does_not_require_torchlanc() -> None:
"""Importing registration does not eagerly import TorchLanc."""
sys.modules.pop("torchlanc", None)
importlib.import_module("SimpleSyrup")
imported_module: ModuleType | None = sys.modules.get("torchlanc")
assert imported_module is None
def test_v3_entrypoint_exports_all_base_nodes_without_prompt_control(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Comfy v3 entrypoint exports every maintained non-conditional node."""
sys.modules.pop("prompt_control.nodes_lazy", None)
package = importlib.import_module("SimpleSyrup")
nodes_v3 = importlib.import_module("SimpleSyrup.simple_syrup.nodes_v3")
monkeypatch.setattr(nodes_v3, "prompt_control_is_available", lambda: False)
extension = asyncio.run(package.comfy_entrypoint())
nodes = asyncio.run(extension.get_node_list())
assert _node_ids(cast(list[type[_V3Node]], nodes)) == BASE_NODE_IDS
assert "prompt_control.nodes_lazy" not in sys.modules
def test_v3_entrypoint_adds_only_prompt_control_nodes_when_available(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Prompt Control availability adds conditional nodes without removing others."""
sys.modules.pop("prompt_control.nodes_lazy", None)
package = importlib.import_module("SimpleSyrup")
nodes_v3 = importlib.import_module("SimpleSyrup.simple_syrup.nodes_v3")
monkeypatch.setattr(nodes_v3, "prompt_control_is_available", lambda: True)
extension = asyncio.run(package.comfy_entrypoint())
nodes = asyncio.run(extension.get_node_list())
assert _node_ids(cast(list[type[_V3Node]], nodes)) == [
*BASE_NODE_IDS,
*PROMPT_CONTROL_NODE_IDS,
]
assert "prompt_control.nodes_lazy" not in sys.modules
def _node_ids(nodes: list[type[_V3Node]]) -> list[str]:
"""Return node ids from v3 schemas."""
ids: list[str] = []
for node in nodes:
schema = node.define_schema()
node_id: Any = schema.node_id
if not isinstance(node_id, str):
raise AssertionError(f"{node.__name__} has invalid node_id.")
ids.append(node_id)
return ids