Capture the nearest existing source model evaluation without inserting sampling steps, retain its actual noise coordinate, and recover residuals with stable native scaling. Observe source and receiver states at ComfyUI's shared boundaries, discover compatible named/custom source nodes, and report graph node identities and selected samplers in failures. Admit the complete sampler catalog through CD and tiled routes with regression coverage for future additions. Verify the isolated CD-only tree: 4322 Python tests passed, 48 skipped; 115 frontend tests passed; format, lint, strict typing, architecture, and test governance passed. Contextual Attention remains outside this commit.
156 lines
5.9 KiB
Python
156 lines
5.9 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
|
|
|
|
"""Enforce automatic pack-wide sampler and scheduler menu availability."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any, Protocol, cast
|
|
|
|
import comfy.samplers
|
|
import pytest
|
|
|
|
from simple_syrup import nodes_v3
|
|
from simple_syrup.runtime import sampling_samplers, sampling_schedulers
|
|
|
|
|
|
class SamplingNode(Protocol):
|
|
"""Describe the host-facing schema boundary inspected by this contract."""
|
|
|
|
@classmethod
|
|
def define_schema(cls) -> Any:
|
|
"""Declare sampling controls without executing a workflow."""
|
|
|
|
|
|
def sampling_nodes() -> tuple[type[SamplingNode], ...]:
|
|
"""Discover every exported KSampler and detailer without a manual inventory."""
|
|
nodes = tuple(
|
|
cast(type[SamplingNode], node)
|
|
for node in nodes_v3.get_nodes()
|
|
if node.__name__.startswith(("KSampler", "Detail"))
|
|
)
|
|
assert nodes, "The pack must export sampling nodes."
|
|
return nodes
|
|
|
|
|
|
@pytest.mark.parametrize("prompt_control_available", [False, True])
|
|
def test_every_exported_sampling_node_uses_complete_catalogs(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
prompt_control_available: bool,
|
|
) -> None:
|
|
"""Keep all registered sampling menus aligned in both integration states."""
|
|
monkeypatch.setattr(
|
|
nodes_v3, "prompt_control_is_available", lambda: prompt_control_available
|
|
)
|
|
for node in sampling_nodes():
|
|
schema = node.define_schema()
|
|
inputs = {item.id: item for item in schema.inputs}
|
|
assert tuple(inputs["sampler_name"].options) == (
|
|
sampling_samplers.available_samplers()
|
|
), schema.node_id
|
|
assert tuple(inputs["scheduler"].options) == (
|
|
sampling_schedulers.available_schedulers()
|
|
), schema.node_id
|
|
|
|
|
|
@pytest.mark.parametrize("source", ["pack", "res4lyf", "host"])
|
|
def test_catalog_additions_propagate_without_node_edits(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
source: str,
|
|
) -> None:
|
|
"""Rebuild every menu from changed catalogs, preserving all other contracts."""
|
|
monkeypatch.setattr(nodes_v3, "prompt_control_is_available", lambda: False)
|
|
nodes = sampling_nodes()
|
|
original = {node: node.define_schema() for node in nodes}
|
|
sampler_name = "catalog_test_sampler"
|
|
scheduler_name = "catalog_test_scheduler"
|
|
|
|
if source == "host":
|
|
monkeypatch.setattr(
|
|
comfy.samplers.KSampler,
|
|
"SAMPLERS",
|
|
(*comfy.samplers.KSampler.SAMPLERS, sampler_name, sampler_name),
|
|
)
|
|
monkeypatch.setattr(
|
|
comfy.samplers.KSampler,
|
|
"SCHEDULERS",
|
|
(*comfy.samplers.KSampler.SCHEDULERS, scheduler_name, scheduler_name),
|
|
)
|
|
else:
|
|
catalog = "RES4LYF_SAMPLER_NAMES" if source == "res4lyf" else "EXTRA_SAMPLERS"
|
|
monkeypatch.setattr(
|
|
sampling_samplers,
|
|
catalog,
|
|
(*getattr(sampling_samplers, catalog), sampler_name, sampler_name),
|
|
)
|
|
monkeypatch.setattr(
|
|
sampling_schedulers,
|
|
"EXTRA_SCHEDULERS",
|
|
(*sampling_schedulers.EXTRA_SCHEDULERS, scheduler_name, scheduler_name),
|
|
)
|
|
|
|
for node in nodes:
|
|
schema = node.define_schema()
|
|
previous = original[node]
|
|
assert schema.node_id == previous.node_id
|
|
assert [item.as_dict() for item in schema.outputs] == [
|
|
item.as_dict() for item in previous.outputs
|
|
]
|
|
assert [item.id for item in schema.inputs] == [
|
|
item.id for item in previous.inputs
|
|
]
|
|
inputs = {item.id: item for item in schema.inputs}
|
|
assert tuple(inputs["sampler_name"].options) == (
|
|
sampling_samplers.available_samplers()
|
|
), schema.node_id
|
|
assert tuple(inputs["scheduler"].options) == (
|
|
sampling_schedulers.available_schedulers()
|
|
), schema.node_id
|
|
assert inputs["sampler_name"].options.count(sampler_name) == 1
|
|
assert inputs["scheduler"].options.count(scheduler_name) == 1
|
|
for current, prior in zip(schema.inputs, previous.inputs, strict=True):
|
|
assert current.optional == prior.optional
|
|
assert getattr(current, "default", None) == getattr(prior, "default", None)
|
|
|
|
|
|
@pytest.mark.parametrize("prompt_control_available", [False, True])
|
|
def test_every_exported_latent_sampler_admits_source_capture(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
prompt_control_available: bool,
|
|
) -> None:
|
|
"""Enforce graph-capture support when registration adds a new sampling node."""
|
|
from simple_syrup.runtime.noise_reuse_graph import rewrite_noise_reuse_graph
|
|
from simple_syrup.runtime.noise_reuse_source import resolve_source_sampling_graph
|
|
|
|
monkeypatch.setattr(
|
|
nodes_v3, "prompt_control_is_available", lambda: prompt_control_available
|
|
)
|
|
discovered: list[str] = []
|
|
for node in sampling_nodes():
|
|
schema = node.define_schema()
|
|
if schema.outputs[0].io_type != "LATENT":
|
|
continue
|
|
inputs: dict[str, object] = {item.id: 0 for item in schema.inputs}
|
|
assert {"model", "latent_image", "sampler_name"} <= inputs.keys(), (
|
|
schema.node_id
|
|
)
|
|
inputs.update(
|
|
model=["loader", 0], latent_image=["empty", 0], sampler_name="er_sde"
|
|
)
|
|
prompt = {
|
|
"source": {"class_type": schema.node_id, "inputs": inputs},
|
|
"upscale": {
|
|
"class_type": "SimpleSyrup.SimpleUpscaleLatent",
|
|
"inputs": {"latent": ["source", 0], "method": "Auto"},
|
|
},
|
|
}
|
|
assert resolve_source_sampling_graph(prompt, "source") is not None, (
|
|
schema.node_id
|
|
)
|
|
rewrite_noise_reuse_graph(prompt, {})
|
|
assert inputs["model"] == ["source.trajectory_capture", 0], schema.node_id
|
|
assert inputs["sampler_name"] == "er_sde", schema.node_id
|
|
discovered.append(schema.node_id)
|
|
assert len(discovered) >= 9
|