Files
olivv-cs-ComfyUI-FunPack/modules/sampling/tests/test_modifier_reaches_sampling.py
T
digital-garbageandClaude Opus 5 def940d9d5 foundation: a module cannot end a run, unless you asked for that
Catching a modifier that fails to INSTALL was never the hard part. The one
that costs a rental is the hook that works at step 0 and raises at step 3,
after the GPU time is spent -- which the Detail modifier does on any video
latent, because core's sharpen kernel cannot take a 5-D tensor.

A module is now handed a patcher that guards every hook it installs, so it
does not opt in to being safe: a module that could forget to is a module
that can break the foundation. A hook that raises is dropped for the rest of
that run, reported once with its traceback, and sampling continues. Thirty
steps on the latent that used to end the run now complete untouched.

Recorded per RUN, not per process, so a long session reports every
generation that went inert instead of only the first after a restart.

Full control turns all of it off. A one-step schedule, a width the model
cannot use, a modifier that throws -- someone exploring the edges wants the
real behaviour and the real traceback, not a careful refusal. One switch
rather than one per guard, because a half-disabled safety net is harder to
reason about than either extreme. It is asked for by capability, so nothing
names the module that answers, and a build without it installed has nobody
saying yes -- which reads as guarded.

Honest about its own limit: a hook shape with no known neutral result is
installed unguarded and named in the status, rather than counted as safe.

Two test faults fixed while writing this. The assertions compared conds_out
against the list passed in -- the same object the hook mutates -- so they
were true whatever the hook did. And the registry fakes were hand-rolled and
missing methods the real one has, which passed until the code called one.
They use the real Registry now.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 13:52:18 +03:00

96 lines
3.4 KiB
Python

"""A modifier installed on the model is reached by ComfyUI's own sampling path.
This is the architectural claim made checkable: modifiers attach to the MODEL, so
no sampler has to cooperate and therefore no sampler can own them. If this test
holds, wiring the modified model into ComfyUI's own SamplerCustomAdvanced runs
every per-step modifier.
It uses comfy's real functions -- prepare_model_patcher, get_all_wrappers,
WrapperExecutor -- rather than asserting on our own dict. Checking that we wrote
a key we chose would prove nothing about whether anything reads it.
"""
import pytest
@pytest.fixture(autouse=True)
def _needs_comfy(comfyui):
"""Imports comfy."""
@pytest.fixture
def registry(monkeypatch):
from core import registry as registry_mod
from core.contract import ModuleSpec
fake = registry_mod.Registry()
monkeypatch.setattr(registry_mod, "current", lambda rescan=False: fake)
return fake, ModuleSpec
def test_the_wrapper_a_modifier_installs_is_executed_by_comfys_sampler_path(registry):
import torch
import comfy.patcher_extension as ext
import comfy.sampler_helpers
from comfy.model_patcher import ModelPatcher
from comfy.patcher_extension import WrappersMP
from modules.sampling.modifiers.nodes import FunPackLoadModifiers
fake, ModuleSpec = registry
ran = []
def install(target, values, key):
def wrapper(executor, *args, **kwargs):
ran.append(key)
return executor(*args, **kwargs)
target.add_wrapper_with_key(WrappersMP.SAMPLER_SAMPLE, key, wrapper)
return "on"
fake.specs["m"] = ModuleSpec(id="m", title="m", mount="",
provides={"modifier": install})
class Stub(torch.nn.Module):
pass
patcher = ModelPatcher(Stub(), load_device=torch.device("cpu"),
offload_device=torch.device("cpu"))
patched, _status = FunPackLoadModifiers.execute(patcher).result
# Exactly what CFGGuider.inner_sample does: the patcher's wrappers are merged
# into model_options, then the sampler's own sample() is wrapped in them.
model_options = {"transformer_options": {}}
comfy.sampler_helpers.prepare_model_patcher(patched, {}, model_options)
def sample(*args, **kwargs):
return "sampled"
executor = ext.WrapperExecutor.new_class_executor(
sample, None,
ext.get_all_wrappers(WrappersMP.SAMPLER_SAMPLE, model_options, is_model_options=True))
assert executor.execute() == "sampled"
assert ran == ["funpack.m"], (
"the modifier was installed somewhere ComfyUI's sampling path does not read")
def test_an_unmodified_model_carries_none_of_our_wrappers(registry):
"""The control. Without it the test above could pass on a machine where
something else installs a wrapper under any key at all."""
import torch
import comfy.sampler_helpers
from comfy.model_patcher import ModelPatcher
from comfy.patcher_extension import WrappersMP
class Stub(torch.nn.Module):
pass
patcher = ModelPatcher(Stub(), load_device=torch.device("cpu"),
offload_device=torch.device("cpu"))
model_options = {"transformer_options": {}}
comfy.sampler_helpers.prepare_model_patcher(patcher, {}, model_options)
installed = model_options["transformer_options"].get("wrappers", {}).get(
WrappersMP.SAMPLER_SAMPLE, {})
assert not [k for k in installed if str(k).startswith("funpack.")]