Files
olivv-cs-ComfyUI-FunPack/modules/sampling/tests/test_modifier_settings.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

118 lines
4.4 KiB
Python

"""The settings payload, checked rather than trusted.
v4 carried these as JSON in a string widget that nothing validated. The cost was
visible in the code: a hand-mirrored table of every default, "regenerated from
engine_settings.js by a test", plus a list of keys that no longer meant anything
and had to be filtered out so they could not reach the render. Every test here
is a thing that silently did nothing in v4.
"""
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()
fake.specs = {
"alg": ModuleSpec(id="alg", title="ALG", mount="", settings={
"enabled": {"type": "bool", "default": False, "label": "On"},
"strength": {"type": "float", "default": 0.5, "min": 0.0, "max": 1.0, "label": "S"},
"steps": {"type": "int", "default": 2, "min": 0, "max": 8, "label": "N"},
"mode": {"type": "enum", "default": "beat", "label": "M", "options": [
{"value": "beat", "label": "Beat"}, {"value": "flat", "label": "Flat"}]},
}),
}
monkeypatch.setattr(registry_mod, "current", lambda rescan=False: fake)
return fake
def _run(text):
from modules.sampling.modifiers.nodes import FunPackModifierSettings
return FunPackModifierSettings.execute(text).result
def test_a_valid_payload_comes_back_merged_over_the_defaults(registry):
values, _status = _run('{"alg": {"enabled": true}}')
assert values["alg"] == {"enabled": True, "strength": 0.5, "steps": 2, "mode": "beat"}
def test_an_empty_payload_is_fine(registry):
values, _status = _run("")
assert values == {}
def test_malformed_json_says_so_instead_of_meaning_nothing(registry):
with pytest.raises(RuntimeError, match="not valid JSON"):
_run("{not json")
def test_a_setting_no_module_declares_is_reported(registry):
"""In v4 this was simply ignored, which is how dead keys accumulated."""
with pytest.raises(RuntimeError, match="no setting named 'stength'"):
_run('{"alg": {"stength": 0.5}}')
def test_a_module_that_is_not_installed_is_reported(registry):
with pytest.raises(RuntimeError, match="no module named 'ghost'"):
_run('{"ghost": {"x": 1}}')
@pytest.mark.parametrize("payload,expected", [
('{"alg": {"strength": "loud"}}', "float"),
('{"alg": {"strength": 2.0}}', "above its maximum"),
('{"alg": {"strength": -1.0}}', "below its minimum"),
('{"alg": {"steps": 1.5}}', "int"),
('{"alg": {"enabled": "yes"}}', "bool"),
('{"alg": {"mode": "swing"}}', "must be one of"),
])
def test_a_wrong_value_is_refused_not_coerced(registry, payload, expected):
with pytest.raises(RuntimeError, match=expected):
_run(payload)
def test_a_bool_is_not_accepted_as_a_number(registry):
"""True == 1 in Python, so an unguarded numeric check lets a checkbox through
as a strength."""
with pytest.raises(RuntimeError, match="float"):
_run('{"alg": {"strength": true}}')
def test_a_payload_that_is_not_an_object_is_refused(registry):
with pytest.raises(RuntimeError, match="keyed by module id"):
_run("[1, 2, 3]")
def test_every_problem_is_reported_at_once(registry):
"""One error per run means finding them one at a time."""
with pytest.raises(RuntimeError) as raised:
_run('{"alg": {"strength": 9.0, "steps": 99}}')
message = str(raised.value)
assert "above its maximum 1.0" in message and "above its maximum 8" in message
@pytest.mark.parametrize("literal", ["NaN", "Infinity", "-Infinity"])
def test_a_non_finite_number_is_refused(registry, literal):
"""json.loads accepts all three by default, and NaN compares False against
BOTH bounds -- so a range check alone lets it through and it reaches sampling
as a strength that poisons everything it touches."""
with pytest.raises(RuntimeError, match="finite|maximum|minimum"):
_run('{"alg": {"strength": %s}}' % literal)
def test_nan_specifically_is_not_silently_accepted(registry):
import math
try:
values, _ = _run('{"alg": {"strength": NaN}}')
except RuntimeError:
return # refused, which is the point
pytest.fail(f"NaN was accepted as {values['alg']['strength']!r} "
f"(isnan={math.isnan(values['alg']['strength'])})")