Files
Claude 3adebb731f Rename FunPack to FunUI (5.0.3 Classy Chopper): /fun endpoint, wink logo, load-time migration of old names
Old project/workflow names and browser settings migrate on load; /funpack redirects to /fun.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016acRss69eH1235eRdFTZHY
2026-10-25 13:11:03 +00:00

185 lines
7.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 FunUIModifierSettings
return FunUIModifierSettings.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_skipped_and_said(registry):
"""Saved settings outlive settings: the run goes on, the log names the dead key (v4 did it silently)."""
from core import log as _log
_log._reset()
values, _status = _run('{"alg": {"stength": 0.5}}')
assert "stength" not in values["alg"]
assert any("stength" in r["message"] and "ignored" in r["message"] for r in _log.history())
def test_a_module_that_is_not_installed_is_skipped_and_said(registry):
"""A module that was removed, renamed or failed to load must not stop the projects that saved settings for it."""
from core import log as _log
_log._reset()
values, _status = _run('{"ghost": {"x": 1}, "alg": {"enabled": true}}')
assert "ghost" not in values and values["alg"]["enabled"] is True
assert any("'ghost'" in r["message"] and "ignored" in r["message"] for r in _log.history())
@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'])})")
# --- refused at queue time, not at execution time --------------------------
def test_a_bad_payload_is_refused_before_anything_runs(registry):
"""ComfyUI calls validate_inputs inside validate_prompt, before any node
executes: a wrong VALUE is found before the checkpoint loads, not after
(on a rented GPU that is the cost the guard exists to avoid)."""
from modules.sampling.modifiers.nodes import FunUIModifierSettings
said = FunUIModifierSettings.validate_inputs('{"alg": {"enabled": "yes please"}}')
assert said is not True
assert FunUIModifierSettings.validate_inputs('{"ghost": {"x": 1}}') is True # a name nobody has is skipped, not refused
def test_a_good_payload_passes_validation(registry):
from modules.sampling.modifiers.nodes import FunUIModifierSettings
assert FunUIModifierSettings.validate_inputs('{"alg": {"enabled": true}}') is True
def test_an_empty_payload_passes_validation(registry):
from modules.sampling.modifiers.nodes import FunUIModifierSettings
assert FunUIModifierSettings.validate_inputs("") is True
@pytest.mark.parametrize("payload,expected", [
("{not json", "not valid JSON"),
("[1,2]", "keyed by module id"),
('{"alg": {"strength": 9.0}}', "above its maximum"),
('{"alg": {"strength": NaN}}', "finite"),
])
def test_validation_catches_the_same_things_execution_would(registry, payload, expected):
"""The two doors must agree. A payload the queue accepts and execution
rejects is worse than either alone."""
from modules.sampling.modifiers.nodes import FunUIModifierSettings
said = FunUIModifierSettings.validate_inputs(payload)
assert said is not True and expected in said
with pytest.raises(RuntimeError):
_run(payload)
def test_the_projects_off_list_passes_through_and_is_not_mistaken_for_a_module(registry):
out, _status = _run('{"alg": {"enabled": true}, "_off": {"modules": ["sharpen"]}}')
assert out["_off"] == {"modules": ["sharpen"]} and out["alg"]["enabled"] is True
def test_a_malformed_off_list_is_refused_with_a_reason(registry):
with pytest.raises(RuntimeError, match="modules"):
_run('{"_off": ["sharpen"]}')
def test_a_switched_off_modules_bad_value_does_not_refuse_the_run(registry):
import dataclasses
registry.specs["alg"] = dataclasses.replace(registry.specs["alg"], provides={"modifier": lambda *a, **k: None})
out, _ = _run('{"alg": {"strength": 99}, "_off": {"modules": ["alg"]}}')
assert "alg" not in out
with pytest.raises(RuntimeError, match="strength"):
_run('{"alg": {"strength": 99}}')