Files
EnragedAntelopeandClaude Opus 4.8 77f268c4ec Initial commit: LoRA Slider node for ComfyUI (integrates with Lora Manager)
Self-contained ComfyUI custom node that adds per-LoRA bipolar strength
sliders with editable min/max, semantic end-labels, per-slider / bulk /
each-run randomization (locks respected), batch-add via a path-aware picker,
a paste/type text field, and Lora Pool folder import. Best-effort min/max
auto-fill parsed from Lora Manager's stored CivitAI descriptions. Outputs
LORA_STACK (+ trigger_words). Integrates with Lora Manager purely via its
public HTTP routes and shared ComfyUI types -- no LM modules imported.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 15:23:21 -04:00

141 lines
4.9 KiB
Python

"""Unit tests for the LoraSliderLM node's build() -> LORA_STACK logic."""
import random
from nodes.lora_slider_node import LoraSliderLM
def build(sliders, pool_config=None, **kwargs):
return LoraSliderLM().build(sliders, pool_config=pool_config, **kwargs)
def test_basic_active_slider_emits_tuple():
stack, words = build([
{"name": "folder/lora_a", "value": 1.5, "min": -3, "max": 3, "active": True},
])
assert stack == [("folder/lora_a", 1.5, 1.5)]
assert words == ""
def test_inactive_slider_excluded():
stack, _ = build([
{"name": "a", "value": 1.0, "active": False},
{"name": "b", "value": 1.0, "active": True},
])
assert stack == [("b", 1.0, 1.0)]
def test_value_clamped_to_min_max():
stack, _ = build([
{"name": "hi", "value": 99, "min": -3, "max": 2.5, "active": True},
{"name": "lo", "value": -99, "min": -3, "max": 2.5, "active": True},
])
assert stack == [("hi", 2.5, 2.5), ("lo", -3.0, -3.0)]
def test_min_max_supplied_reversed_is_tolerated():
stack, _ = build([
{"name": "x", "value": 5, "min": 3, "max": -3, "active": True},
])
# Range normalized to [-3, 3]; value 5 clamps to 3.
assert stack == [("x", 3.0, 3.0)]
def test_zero_value_is_skipped():
stack, _ = build([
{"name": "zero", "value": 0, "min": -3, "max": 3, "active": True},
{"name": "nonzero", "value": -1.2, "min": -3, "max": 3, "active": True},
])
assert stack == [("nonzero", -1.2, -1.2)]
def test_separate_clip_strength_respected():
stack, _ = build([
{"name": "x", "value": 1.0, "clipStrength": 0.4, "min": -3, "max": 3, "active": True},
])
assert stack == [("x", 1.0, 0.4)]
def test_trigger_words_aggregated_and_deduped():
stack, words = build([
{"name": "a", "value": 1, "active": True, "triggerWords": "slender, thin"},
{"name": "b", "value": 1, "active": True, "triggerWords": ["Thin", "tall"]},
])
assert len(stack) == 2
# "thin"/"Thin" deduped case-insensitively; first-seen casing kept.
assert words == "slender, thin, tall"
def test_wrapped_value_format_supported():
stack, _ = build({"__value__": [{"name": "a", "value": 1, "active": True}]})
assert stack == [("a", 1.0, 1.0)]
def test_garbage_entries_ignored():
stack, _ = build([None, 42, {"value": 1, "active": True}, {"name": "ok", "value": 1, "active": True}])
# entry with no name skipped; only "ok" survives
assert stack == [("ok", 1.0, 1.0)]
def test_empty_and_non_list_inputs():
assert build([]) == ([], "")
assert build(None) == ([], "")
assert build("nonsense") == ([], "")
def test_input_types_shape():
it = LoraSliderLM.INPUT_TYPES()
assert "sliders" in it["required"]
assert it["required"]["sliders"][0] == "LORA_SLIDERS"
assert it["required"]["randomize_active_each_run"][0] == "BOOLEAN"
assert it["required"]["randomize_strength_each_run"][0] == "BOOLEAN"
assert it["optional"]["pool_config"][0] == "POOL_CONFIG"
assert LoraSliderLM.RETURN_TYPES == ("LORA_STACK", "STRING")
# --- each-run randomization ------------------------------------------------
def test_randomize_strength_each_run_stays_in_range_and_respects_lock():
random.seed(1)
rows = [
{"name": "free", "value": 1.0, "min": -3, "max": 3, "step": 0.05, "active": True},
{"name": "locked", "value": 1.0, "min": -3, "max": 3, "active": True, "locked": True},
]
for _ in range(50):
stack, _w = build(rows, randomize_strength_each_run=True)
by_name = {n: (m, c) for (n, m, c) in stack}
# locked slider always emits its fixed value
assert by_name["locked"] == (1.0, 1.0)
# free slider varies but stays within [-3, 3]
assert -3.0 <= by_name["free"][0] <= 3.0
def test_randomize_active_each_run_never_disables_locked():
random.seed(2)
rows = [
{"name": "free", "value": 1.0, "min": -3, "max": 3, "active": True},
{"name": "locked_on", "value": 1.0, "min": -3, "max": 3, "active": True, "locked": True},
]
seen_free_absent = False
for _ in range(50):
stack, _w = build(rows, randomize_active_each_run=True)
names = {n for (n, _m, _c) in stack}
# locked-on slider is always present
assert "locked_on" in names
if "free" not in names:
seen_free_absent = True
# over many rolls the unlocked slider should sometimes be disabled
assert seen_free_absent
def test_is_changed_forces_rerun_only_when_each_run_enabled():
rows = [{"name": "a", "value": 1, "active": True}]
# stable when no each-run mode
a = LoraSliderLM.IS_CHANGED(rows, False, False)
b = LoraSliderLM.IS_CHANGED(rows, False, False)
assert a == b
# changes (almost surely) when an each-run mode is on
c = LoraSliderLM.IS_CHANGED(rows, False, True)
d = LoraSliderLM.IS_CHANGED(rows, False, True)
assert c != d