Files
olivv-cs-ComfyUI-FunPack/modules/sampling/tests/test_modifier_settings.py
T
digital-garbageandClaude Opus 5 b3afba43e7 sampling: tag and strip, because the clone only protected the original
The claim in the last commit was too strong. Cloning stops this node
touching the model everyone else holds; it does nothing about the node's own
output coming back in. add_wrapper_with_key APPENDS under the key and a
clone carries the list, so chaining two of these -- two passes, a flattened
subgraph -- installed the same wrapper twice and ran it twice per step at
double strength, while both nodes reported "1 modifier(s) applied".

That is the accumulation this design is supposed to prevent, arriving
through a different door. Every install is now preceded by stripping our own
namespaced keys and saying how many were replaced. Foreign wrappers are
never in scope, which is what the namespace was for.

NaN passed validation: it compares False against both bounds, so a range
check alone never fires, and json.loads accepts NaN and the infinities by
default. A NaN strength reached a modifier and would have poisoned sampling
with nothing reported. Non-finite numbers are now refused by name.

A settings payload of the wrong shape raised AttributeError rather than the
error reporting everything else here uses; the socket type is only a string
match, so anything can arrive on it.

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

121 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
class 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"}]},
}),
}
failed = []
fake = Fake()
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'])})")