130 lines
6.0 KiB
Python
130 lines
6.0 KiB
Python
"""The nodes FunUI actually ships, checked against ComfyUI itself.
|
|
|
|
Needs a ComfyUI source tree, because a node's schema is only meaningful in terms
|
|
of comfy_api's own types. Skipped where there is none.
|
|
"""
|
|
|
|
import pytest
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def shipped(comfyui):
|
|
from core import nodes as nodes_mod
|
|
from core import registry as registry_mod
|
|
|
|
registry = registry_mod.scan()
|
|
nodes, rejected = nodes_mod.collect(registry)
|
|
return registry, nodes, rejected
|
|
|
|
|
|
def _schema(node):
|
|
return node.GET_SCHEMA()
|
|
|
|
|
|
def _input_ids(schema):
|
|
return [getattr(i, "id", None) for i in (schema.inputs or [])]
|
|
|
|
|
|
def test_nothing_shipped_is_rejected(shipped):
|
|
_, _, rejected = shipped
|
|
assert rejected == [], f"a shipped node did not register: {rejected}"
|
|
|
|
|
|
def test_every_shipped_node_is_a_real_comfy_node(shipped, comfyui):
|
|
from comfy_api.latest import io
|
|
_, nodes, _ = shipped
|
|
for node in nodes:
|
|
assert issubclass(node, io.ComfyNode), f"{node.__name__} is not an io.ComfyNode"
|
|
assert isinstance(_schema(node), io.Schema)
|
|
|
|
|
|
# Each entry is a promise to everyone's saved workflows: `node_id` is stored as
|
|
# `class_type`, and input ids are stored as key names. Changing either silently
|
|
# breaks graphs that already exist, so this test is meant to FAIL on a rename and
|
|
# make that a decision rather than an accident.
|
|
FROZEN = {
|
|
"FunUICheckpointLoader": ["ckpt_name"],
|
|
"FunUICLIPLoader": [
|
|
"clip_name1", "clip_name2", "clip_name3", "clip_name4", "type", "device",
|
|
],
|
|
"FunUISampler": [
|
|
"model", "positive", "negative", "latent", "seed", "steps", "cfg",
|
|
"sampler_name", "scheduler", "denoise", "settings", "sigmas",
|
|
],
|
|
"FunUILoadModifiers": ["model", "settings"],
|
|
"FunUILatentUpscalerLoader": ["upscaler_name"],
|
|
"FunUICutOpening": ["images", "frames", "fps", "audio"],
|
|
"FunUISaveVideo": ["images", "audio", "fps", "filename_prefix", "encoder", "codec", "quality", "settings"],
|
|
"FunUIPromptMarkup": ["text"],
|
|
"FunUINegativeErase": ["positive", "negative", "strength", "mode", "keep_size"],
|
|
"FunUIShotCamera": ["text", "settings", "length", "frame_rate", "project"],
|
|
"FunUIEnhancePrompt": [
|
|
"clip", "text", "enabled", "instructions", "max_length", "greedy", "temperature", "top_p",
|
|
"top_k", "min_p", "repetition_penalty", "presence_penalty", "seed", "thinking", "encoder_file", "encoder_family", "image", "use_image",
|
|
"shortcuts", "lorebooks", "reference_intro", "chat", "style",
|
|
],
|
|
"FunUIApplyPromptMarkup": ["model", "clip", "positive", "latent", "markup"],
|
|
"FunUILatentResample": ["latent", "upscaler", "operation", "scale", "positive", "negative"],
|
|
"FunUIModifierSettings": ["settings"],
|
|
"FunUIGuideFrames": ["positive", "negative", "vae", "latent", "frames"],
|
|
"FunUIIdentityTransfer": ["model", "vae", "positive", "negative", "latent", "image", "identity_projector",
|
|
"source_id", "phase_scale", "id_strength", "arcface_mode"],
|
|
"FunUIDecode": ["samples", "vae", "model", "audio_vae", "tile_size"],
|
|
"FunUIUpscaleVideo": ["filename", "subfolder", "type", "upscale_model", "rtx_options"],
|
|
"FunUITrainLora": [
|
|
"model", "clip", "vae", "audio_vae", "set_name", "name", "use", "steps", "learning_rate", "rank", "algorithm",
|
|
"optimizer", "loss", "batch_size", "grad_accumulation", "resolution", "aspect_buckets", "frames", "fps", "start",
|
|
"trigger", "seed", "training_dtype", "lora_dtype", "gradient_checkpointing", "offloading", "bypass",
|
|
"model_name", "weight_dtype", "compute_dtype", "postfix", "swap_blocks", "preview_every", "save_every", "resume",
|
|
],
|
|
"FunUIAspectSize": ["image", "width", "height", "multiple_of", "basis"],
|
|
"FunUIEmptyLatent": ["model", "width", "height", "length", "batch_size", "audio_vae", "frame_rate"],
|
|
"FunUIDiffusionModelLoader": [
|
|
"model_name", "weight_dtype", "compute_dtype", "attention", "fp16_accumulation",
|
|
"sla", "sla_sparsity", "sla_block_size", "sla_protect_audio", "sla_min_seq_len",
|
|
"sla_dense_last_steps", "int8_convrot", "sla_engine", "sla_dense_steps", "sla_references",
|
|
"sla_tail", "sla_stabilize_motion", "sla_method", "sla_tau", "sla_sparsity_start", "mxfp8", "veda_predictor", "veda_timing",
|
|
],
|
|
"FunUILoraLoader": [
|
|
"model", "lora_name", "strength_model", "clip", "strength_clip", "apply_to_stage",
|
|
*[f"{k}_{n}" for n in range(2, 9) for k in ("lora_name", "strength_model", "strength_clip", "apply_to_stage")],
|
|
],
|
|
"FunUIVAELoader": ["vae_name", "dtype", "decode_vae_name"],
|
|
"FunUILoadMedia": ["media_id", "blank_width", "blank_height", "blank_frames", "full", "prepare"],
|
|
}
|
|
|
|
|
|
def test_shipped_node_ids_and_inputs_are_frozen(shipped):
|
|
_, nodes, _ = shipped
|
|
actual = {_schema(n).node_id: _input_ids(_schema(n)) for n in nodes}
|
|
assert actual == FROZEN, (
|
|
"a shipped node's id or input names changed. Every saved workflow refers "
|
|
"to these by name; update FROZEN only if you intend to break them."
|
|
)
|
|
|
|
|
|
def test_a_setting_never_becomes_a_node_socket(shipped):
|
|
"""The rule that keeps the sampler from growing 70 widgets, and keeps a new
|
|
setting from rotting saved graphs: settings are a payload, not sockets."""
|
|
registry, _, _ = shipped
|
|
for spec in registry.specs.values():
|
|
keys = set(spec.settings)
|
|
for node in spec.nodes:
|
|
ids = set(_input_ids(_schema(node))) - {None}
|
|
overlap = keys & ids
|
|
assert not overlap, (
|
|
f"{spec.id}'s node {node.__name__} exposes settings as sockets: "
|
|
f"{sorted(overlap)}"
|
|
)
|
|
|
|
|
|
def test_the_extension_hands_comfyui_exactly_what_was_collected(shipped, comfyui):
|
|
import asyncio
|
|
from core import nodes as nodes_mod
|
|
|
|
extension = nodes_mod.extension()
|
|
assert extension is not None
|
|
listed = asyncio.run(extension.get_node_list())
|
|
_, nodes, _ = shipped
|
|
assert {_schema(n).node_id for n in listed} == {_schema(n).node_id for n in nodes}
|