New nodes: - Isulion Censored Part: blur/pixelate/black bar/noise over a mask and/or a percent rectangle; accepts 2D/3D/4D masks and fails closed (blacks the item out) instead of passing an uncensored image through - Prompt Vault Save/Load: named prompt library stored in ComfyUI's user directory (outside the checkout, so updates never conflict), seeded from prompt_vault.example.json; Load re-runs when the vault changes Refactor: - SimpleSelectorNode base: ~20 selector nodes use an instance RNG instead of reseeding the global random module (same seed => same output) - Theme handlers declare DISPLAY_NAME; theme package auto-discovers modules - __init__ imports node classes directly Fixes: - net_utils: re-validate every redirect hop, unwrap IPv4-mapped IPv6, block NAT64 and all non-global addresses (SSRF) - Multiple Prompt Generator: no duplicate prompts for themes listed in two categories; 6 themes missing from every category are now reachable - Shutdown node: parameter renamed to match its "any" input (TypeError) - ConfigManager: deterministic (sorted) merge order, warns on key collisions; drop stale space_colony copy from tropical_paradise_config - Mega Prompt V3: seed honored again with randomize=disable Breaking: Civitai api_key widget removed (use CIVITAI_API_TOKEN), absolute paths refused by Load Images, Negative Prompt pools de-duplicated (shorter "strict" output), Dynamic Random theme order is now alphabetical. Tooling: pytest suite (51 tests), GitHub CI (compileall, pytest, ruff), ruff config, uv.lock. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
148 lines
4.9 KiB
Python
148 lines
4.9 KiB
Python
"""Seeded-selection determinism checks for representative selector nodes.
|
|
|
|
Covered: Isulion_AnimalRandom, IsulionTechGenerator, Isulion_EmotionGenerator,
|
|
IsulionAlienWorldGenerator. All nodes return their (possibly new) seed as the
|
|
LAST element of the output tuple.
|
|
"""
|
|
|
|
import random
|
|
|
|
from isulion_testkit import import_package
|
|
|
|
|
|
def _callers():
|
|
"""Per-node callables taking (randomize, seed) -> output tuple."""
|
|
pkg = import_package()
|
|
mappings = pkg.NODE_CLASS_MAPPINGS
|
|
animal = mappings["IsulionAnimalRandom"]()
|
|
tech = mappings["IsulionTechGenerator"]()
|
|
emotion = mappings["IsulionEmotionGenerator"]()
|
|
alien = mappings["IsulionAlienWorldGenerator"]()
|
|
|
|
def call_animal(randomize, seed):
|
|
return animal.random_animal(randomize=randomize, seed=seed, animal="Cat")
|
|
|
|
def call_tech(randomize, seed):
|
|
return tech.generate(randomize=randomize, tech="quantum blade",
|
|
seed=seed, tech_type="any")
|
|
|
|
def call_emotion(randomize, seed):
|
|
return emotion.generate_emotion(randomize=randomize, seed=seed,
|
|
emotion="Happy")
|
|
|
|
def call_alien(randomize, seed):
|
|
return alien.generate(randomize=randomize, atmosphere="toxic",
|
|
terrain="floating islands", color="purple",
|
|
feature="binary suns", seed=seed)
|
|
|
|
return {
|
|
"Isulion_AnimalRandom": call_animal,
|
|
"IsulionTechGenerator": call_tech,
|
|
"Isulion_EmotionGenerator": call_emotion,
|
|
"IsulionAlienWorldGenerator": call_alien,
|
|
}
|
|
|
|
|
|
def _assert_seeded_path_deterministic_and_pollution_free(call):
|
|
# Deterministic across calls with the same seed...
|
|
random.seed(1111)
|
|
first = call("enable", 42)
|
|
random.seed(2222)
|
|
second = call("enable", 42)
|
|
assert first == second
|
|
assert first[-1] == 42
|
|
# ...and unaffected by external global-random pollution.
|
|
random.seed(987654321)
|
|
third = call("enable", 42)
|
|
assert third == first
|
|
|
|
|
|
def _assert_disable_path_passthrough(call, extra_check):
|
|
result = call("disable", 77)
|
|
assert result[-1] == 77, "disable path must return the original seed"
|
|
extra_check(result)
|
|
|
|
|
|
def _assert_fresh_seed_zero_path(call):
|
|
out = call("enable", 0)
|
|
fresh_seed = out[-1]
|
|
assert isinstance(fresh_seed, int) and fresh_seed > 0, (
|
|
f"enable + seed=0 must report a fresh positive seed, got {fresh_seed!r}"
|
|
)
|
|
replay = call("enable", fresh_seed)
|
|
assert replay[:-1] == out[:-1], (
|
|
"replaying the freshly reported seed must reproduce the same choices"
|
|
)
|
|
|
|
|
|
def test_animal_random_seeded_deterministic_and_pollution_free():
|
|
_assert_seeded_path_deterministic_and_pollution_free(
|
|
_callers()["Isulion_AnimalRandom"])
|
|
|
|
|
|
def test_animal_random_disable_returns_dropdown_value_and_original_seed():
|
|
|
|
def check(result):
|
|
assert result[0] == "Cat", result
|
|
|
|
_assert_disable_path_passthrough(_callers()["Isulion_AnimalRandom"], check)
|
|
|
|
|
|
def test_animal_random_fresh_seed_zero_is_positive_and_self_consistent():
|
|
_assert_fresh_seed_zero_path(_callers()["Isulion_AnimalRandom"])
|
|
|
|
|
|
def test_tech_generator_seeded_deterministic_and_pollution_free():
|
|
_assert_seeded_path_deterministic_and_pollution_free(
|
|
_callers()["IsulionTechGenerator"])
|
|
|
|
|
|
def test_tech_generator_disable_returns_dropdown_value_and_original_seed():
|
|
|
|
def check(result):
|
|
assert result[0] == "quantum blade", result
|
|
|
|
_assert_disable_path_passthrough(_callers()["IsulionTechGenerator"], check)
|
|
|
|
|
|
def test_tech_generator_fresh_seed_zero_is_positive_and_self_consistent():
|
|
_assert_fresh_seed_zero_path(_callers()["IsulionTechGenerator"])
|
|
|
|
|
|
def test_emotion_generator_seeded_deterministic_and_pollution_free():
|
|
_assert_seeded_path_deterministic_and_pollution_free(
|
|
_callers()["Isulion_EmotionGenerator"])
|
|
|
|
|
|
def test_emotion_generator_disable_keeps_emotion_and_original_seed():
|
|
|
|
def check(result):
|
|
prompt, emotion = result[0], result[1]
|
|
assert emotion == "Happy", result
|
|
assert "happy" in prompt, result
|
|
|
|
_assert_disable_path_passthrough(_callers()["Isulion_EmotionGenerator"], check)
|
|
|
|
|
|
def test_emotion_generator_fresh_seed_zero_is_positive_and_self_consistent():
|
|
_assert_fresh_seed_zero_path(_callers()["Isulion_EmotionGenerator"])
|
|
|
|
|
|
def test_alien_world_seeded_deterministic_and_pollution_free():
|
|
_assert_seeded_path_deterministic_and_pollution_free(
|
|
_callers()["IsulionAlienWorldGenerator"])
|
|
|
|
|
|
def test_alien_world_disable_composes_dropdown_values_with_original_seed():
|
|
|
|
def check(result):
|
|
desc = result[0]
|
|
for fragment in ("toxic", "floating islands", "purple", "binary suns"):
|
|
assert fragment in desc, (fragment, desc)
|
|
|
|
_assert_disable_path_passthrough(_callers()["IsulionAlienWorldGenerator"], check)
|
|
|
|
|
|
def test_alien_world_fresh_seed_zero_is_positive_and_self_consistent():
|
|
_assert_fresh_seed_zero_path(_callers()["IsulionAlienWorldGenerator"])
|