# SimpleSyrup - workflow-focused ComfyUI extensions for image generation # Copyright (C) 2026 Artificial Sweetener and contributors # SPDX-License-Identifier: AGPL-3.0-or-later """Coverage tests for ComfyUI node tooltip metadata.""" from __future__ import annotations import sys from collections.abc import Mapping from types import ModuleType from typing import Any, Protocol, cast import pytest from simple_syrup.nodes import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS from simple_syrup.nodes_v3.encode_prompt_batch_with_prompt_control import ( EncodePromptBatchWithPromptControl, ) from simple_syrup.nodes_v3.scale_factor import ScaleFactorV3 from simple_syrup.nodes_v3.simple_load_checkpoint import SimpleLoadCheckpointV3 from simple_syrup.nodes_v3.tile_and_tag_segs import TileAndTagSEGSV3 from simple_syrup.nodes_v3.vae_decode_options import VAEDecodeOptionsV3 from simple_syrup.nodes_v3.vae_encode_options import VAEEncodeOptionsV3 from simple_syrup.nodes_v3.wd14_tagger_loader import WD14TaggerLoaderV3 class _LegacyNode(Protocol): """Protocol for legacy ComfyUI node declarations.""" DESCRIPTION: str @classmethod def INPUT_TYPES(cls) -> dict[str, dict[str, tuple[Any, ...]]]: """Return ComfyUI legacy input metadata.""" class _V3Node(Protocol): """Protocol for Comfy v3 node schema declarations.""" @classmethod def define_schema(cls) -> Any: """Return a Comfy v3 schema object.""" class _FakeFolderPaths(ModuleType): """Small folder_paths fake for deterministic v3 loader schemas.""" def __init__(self) -> None: """Create deterministic ComfyUI filename lists.""" super().__init__("folder_paths") self.models_dir = "\\models" self.user_directory = "\\user" self._files = { "checkpoints": ["model.safetensors"], "vae": ["manual_vae.safetensors"], "vae_approx": [], } def get_filename_list(self, folder_name: str) -> list[str]: """Return deterministic filenames for a model folder.""" return self._files[folder_name] def test_legacy_nodes_provide_tooltip_metadata() -> None: """All exported legacy nodes expose descriptions and field-level help.""" for node_id, raw_node_class in NODE_CLASS_MAPPINGS.items(): node_class = cast(_LegacyNode, raw_node_class) assert node_id in NODE_DISPLAY_NAME_MAPPINGS description = getattr(node_class, "DESCRIPTION", None) assert isinstance(description, str) and description.strip(), ( f"{node_id} is missing DESCRIPTION." ) input_types = node_class.INPUT_TYPES() assert isinstance(input_types, Mapping), f"{node_id} INPUT_TYPES is invalid." for section_name in ("required", "optional", "hidden"): section = input_types.get(section_name, {}) assert isinstance(section, Mapping), ( f"{node_id} {section_name} inputs must be a mapping." ) for field_name, declaration in section.items(): if section_name == "hidden" and _legacy_hidden_sentinel(declaration): continue assert _tooltip_from_legacy_declaration(declaration), ( f"{node_id} {section_name}.{field_name} is missing tooltip " "metadata." ) def test_legacy_named_outputs_provide_tooltips() -> None: """All named legacy outputs provide matching output tooltip metadata.""" for node_id, node_class in NODE_CLASS_MAPPINGS.items(): return_names = getattr(node_class, "RETURN_NAMES", None) if return_names is None: continue output_tooltips = getattr(node_class, "OUTPUT_TOOLTIPS", None) assert isinstance(output_tooltips, tuple), ( f"{node_id} is missing OUTPUT_TOOLTIPS." ) assert len(output_tooltips) == len(return_names), ( f"{node_id} OUTPUT_TOOLTIPS must match RETURN_NAMES length." ) for output_name, tooltip in zip(return_names, output_tooltips, strict=True): assert isinstance(tooltip, str) and tooltip.strip(), ( f"{node_id} output.{output_name} is missing tooltip metadata." ) @pytest.mark.parametrize( "schema_class", [ SimpleLoadCheckpointV3, ScaleFactorV3, TileAndTagSEGSV3, VAEDecodeOptionsV3, VAEEncodeOptionsV3, WD14TaggerLoaderV3, EncodePromptBatchWithPromptControl, ], ) def test_v3_nodes_provide_tooltip_metadata( monkeypatch: pytest.MonkeyPatch, schema_class: type[_V3Node], ) -> None: """All supported v3 schemas expose descriptions and field-level help.""" monkeypatch.setitem(sys.modules, "folder_paths", _FakeFolderPaths()) schema = schema_class.define_schema() assert isinstance(schema.description, str) and schema.description.strip(), ( f"{schema.node_id} v3 schema is missing description." ) for input_item in schema.inputs: tooltip = getattr(input_item, "tooltip", None) assert isinstance(tooltip, str) and tooltip.strip(), ( f"{schema.node_id} input.{input_item.id} is missing tooltip metadata." ) for output in schema.outputs: tooltip = getattr(output, "tooltip", None) assert isinstance(tooltip, str) and tooltip.strip(), ( f"{schema.node_id} output.{output.id} is missing tooltip metadata." ) def test_high_impact_tooltips_explain_direction_and_units() -> None: """Important numeric controls explain units or practical direction.""" detail_node = cast( _LegacyNode, NODE_CLASS_MAPPINGS["SimpleSyrup.DetailSEGSByScaleFactor"], ) detail_inputs = detail_node.INPUT_TYPES()["required"] denoise = _tooltip_from_legacy_declaration(detail_inputs["denoise"]) feather = _tooltip_from_legacy_declaration(detail_inputs["feather"]) assert "lower" in denoise.lower() and "higher" in denoise.lower() assert "pixels" in feather.lower() tiled_node = cast( _LegacyNode, NODE_CLASS_MAPPINGS["SimpleSyrup.KSamplerTiledDiffusion"], ) tiled_inputs = tiled_node.INPUT_TYPES()["required"] overlap = _tooltip_from_legacy_declaration(tiled_inputs["latent_tile_overlap"]) batch_size = _tooltip_from_legacy_declaration( tiled_inputs["latent_tile_batch_size"] ) assert "overlap" in overlap.lower() and "seams" in overlap.lower() assert "memory" in batch_size.lower() def _tooltip_from_legacy_declaration(declaration: object) -> str: """Return a legacy ComfyUI field tooltip or an empty string.""" if not isinstance(declaration, tuple) or len(declaration) < 2: return "" options = declaration[1] if not isinstance(options, dict): return "" tooltip: Any = options.get("tooltip") if not isinstance(tooltip, str): return "" return tooltip.strip() def _legacy_hidden_sentinel(declaration: object) -> bool: """Return whether a declaration is a Comfy legacy hidden input sentinel.""" return isinstance(declaration, str) and declaration in { "PROMPT", "DYNPROMPT", "EXTRA_PNGINFO", "UNIQUE_ID", "AUTH_TOKEN_COMFY_ORG", "API_KEY_COMFY_ORG", }