Three coordinated changes to the WP_* node family. 1. New WP_SeedList node Emits a list of N derived seeds for a downstream loop (is_output_list). Three independent override toggles (override_seed / override_count / override_strategy) each mirror the wired Loop's base_seed / count / strategy. Connecting loop_config auto-flips override_count + override_strategy ON so the natural intent of wiring in a loop gets the defaults right; override_seed stays explicit opt-in. All emitted seeds are masked to 50 bits via the new MAX_SAFE_SEED constant in engine.seed_derive so values sit inside ComfyUI's frontend randomize range and copy-paste reproduces identically in a standalone sampler. 2. Context Loop type split + downstream Context seed widget lock Split WP_CONTEXT_LOOP_CONFIG (wire payload) from a new WP_CONTEXT_LOOP_WIDGET (DOM-widget-only TYPE). Loop's own widget mounts on the widget type; the wire type has no factory so consumers like WP_SeedList render their loop_config socket as a plain input instead of being hidden by a fallback-undefined widget. New graph walker hasUpstreamLoopOverridingSeed: WP_Context's widget glue greys out the local seed stock widget when an upstream Loop has override_seed ON, so users read "this value is dead while the Loop drives it" without inspecting the Loop config separately. 3. wp_ prefix sweep + tooltips across every WP_* node input Custom DOM widget input names now carry the wp_ prefix so canvas socket labels read as branded WP widgets: modules -> wp_modules, rows -> wp_rows, viewer -> wp_viewer, cleaner -> wp_cleaner, context_loop_config -> wp_context_loop_config. Every stock input across the node family gets a tooltip describing what it does and when to wire it from another node. Tests + comfy_api stub updated. CLAUDE.md notes the wp_ convention for future custom widgets. Bundle size budget bumped 388 -> 395 KB to fit the SeedList Vue chunk (~4.9 KB net gzip) + override lock UI; entry stays at ~4 KB gzipped.
107 lines
3.7 KiB
Python
107 lines
3.7 KiB
Python
"""Pure-Python tests for engine/seed_derive.py.
|
|
|
|
`derive_loop_seeds` and `effective_chain_seed` are the helpers WP_ContextLoop
|
|
+ WP_Context rely on for per-iteration seed shaping. Tests live here so the
|
|
rule stays testable without ComfyUI in scope.
|
|
"""
|
|
|
|
import pytest
|
|
|
|
from engine.seed_derive import MAX_SAFE_SEED, derive_loop_seeds, effective_chain_seed
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"base,count,strategy,expected",
|
|
[
|
|
(10, 1, "sequential", [10]),
|
|
(10, 1, "hash_index", [10]),
|
|
(10, 3, "sequential", [10, 11, 12]),
|
|
(10, 3, "prime_stride", [10, 1000013, 2000016]),
|
|
(42, 1, "prime_stride", [42]),
|
|
],
|
|
)
|
|
def test_derive_loop_seeds(base, count, strategy, expected):
|
|
assert derive_loop_seeds(base, count, strategy) == expected
|
|
|
|
|
|
def test_derive_loop_seeds_hash_index_deterministic():
|
|
a = derive_loop_seeds(10, 3, "hash_index")
|
|
b = derive_loop_seeds(10, 3, "hash_index")
|
|
assert a == b
|
|
assert len(a) == 3
|
|
assert a[0] != a[1]
|
|
assert a[1] != a[2]
|
|
|
|
|
|
def test_derive_loop_seeds_unknown_strategy_raises():
|
|
with pytest.raises(ValueError, match="unknown loop seed strategy"):
|
|
derive_loop_seeds(10, 3, "wat")
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"widget,override,idx,expected",
|
|
[
|
|
# Backwards-compat: no override + loop_index=0 → widget unchanged.
|
|
(42, None, 0, 42),
|
|
# Override beats widget when present.
|
|
(42, 10, 0, 10),
|
|
# 50-bit mask: base larger than 50 bits gets masked.
|
|
(2**65, None, 0, 2**65 & ((1 << 50) - 1)),
|
|
],
|
|
)
|
|
def test_effective_chain_seed_baseline(widget, override, idx, expected):
|
|
got = effective_chain_seed(widget_seed=widget, seed_override=override, loop_index=idx)
|
|
assert got == expected
|
|
|
|
|
|
def test_effective_chain_seed_loop_index_mixes_when_positive():
|
|
base = 42
|
|
a = effective_chain_seed(widget_seed=base, seed_override=None, loop_index=0)
|
|
b = effective_chain_seed(widget_seed=base, seed_override=None, loop_index=1)
|
|
c = effective_chain_seed(widget_seed=base, seed_override=None, loop_index=2)
|
|
assert a == base
|
|
assert b != base
|
|
assert c != base
|
|
assert b != c
|
|
|
|
|
|
def test_effective_chain_seed_override_path_mixes_loop_index():
|
|
a = effective_chain_seed(widget_seed=42, seed_override=10, loop_index=0)
|
|
b = effective_chain_seed(widget_seed=42, seed_override=10, loop_index=1)
|
|
assert a == 10
|
|
assert b != 10
|
|
assert b != 42
|
|
|
|
|
|
def test_effective_chain_seed_deterministic():
|
|
a = effective_chain_seed(widget_seed=42, seed_override=None, loop_index=5)
|
|
b = effective_chain_seed(widget_seed=42, seed_override=None, loop_index=5)
|
|
assert a == b
|
|
|
|
|
|
def test_max_safe_seed_value():
|
|
"""Anchor the 50-bit cap — matches ComfyUI's frontend randomize range."""
|
|
assert MAX_SAFE_SEED == (1 << 50) - 1 == 1_125_899_906_842_623
|
|
|
|
|
|
@pytest.mark.parametrize("base", [0, 42, 2**60])
|
|
@pytest.mark.parametrize("strategy", ["hash_index", "sequential", "prime_stride"])
|
|
def test_derive_loop_seeds_always_fits_50_bits(base, strategy):
|
|
"""Every seed every strategy emits sits inside the 50-bit cap so
|
|
KSampler / canvas seed widgets render the same shape ComfyUI's own
|
|
frontend randomize produces."""
|
|
seeds = derive_loop_seeds(base, 4, strategy)
|
|
assert len(seeds) == 4
|
|
for s in seeds:
|
|
assert 0 <= s <= MAX_SAFE_SEED
|
|
|
|
|
|
def test_effective_chain_seed_always_fits_50_bits():
|
|
"""Same cap applies to the chain-seed helper regardless of which
|
|
branch it takes."""
|
|
for idx in range(0, 5):
|
|
s = effective_chain_seed(widget_seed=2**60, seed_override=None, loop_index=idx)
|
|
assert 0 <= s <= MAX_SAFE_SEED
|
|
s2 = effective_chain_seed(widget_seed=42, seed_override=2**60, loop_index=idx)
|
|
assert 0 <= s2 <= MAX_SAFE_SEED
|