Files
DumiFlex-ComfyUI-Wildcard-P…/tests/wp_nodes/test_context_node_catalog.py
T
DumiFlex ccbf38af99 feat(nodes): add wp_seedlist + split loop config types + wp_ prefix sweep + tooltips
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.
2026-05-28 12:40:44 +03:00

146 lines
5.0 KiB
Python

"""Tests for catalog injection at WP_Context node execution + the
backwards-compat path for old workflow JSON. Spec §4.4 / §6 / 5.5.3."""
from __future__ import annotations
from wp_nodes.types import deserialize_node_input
def test_deserialize_returns_modules_with_empty_snapshots_for_legacy_input():
"""Old workflow JSON had only `modules` (a list). The deserializer
returns `(modules, snapshots, pickOrder)` where snapshots defaults to
empty dict and pickOrder defaults to empty list — backwards compat,
spec §4.5."""
legacy = {"modules": [
{"type": "fixed_values", "name": "x", "payload": {"values": []}},
]}
modules, snapshots, pick_order = deserialize_node_input(legacy)
assert len(modules) == 1
assert snapshots == {}
assert pick_order == []
def test_deserialize_synthesises_catalog_from_wildcard_modules():
"""Unified-list model: there is no separate `snapshots` field on
the wire any more. The deserializer scans `modules`, picks every
entry with `type == "wildcard"`, and synthesises a SnapshotEntry
for it keyed by `id`. Pick-order is always empty under this model
(the modules list itself preserves order)."""
new_shape = {
"modules": [
{
"id": "ou111111",
"type": "wildcard",
"enabled": True,
"meta": {"name": "outfit"},
"entries": [],
"payload": {"options": [{"value": "x"}]},
"payload_hash": "h" * 64,
},
],
}
modules, catalog, pick_order = deserialize_node_input(new_shape)
assert len(modules) == 1
assert "ou111111" in catalog
assert catalog["ou111111"]["payload"] == {"options": [{"value": "x"}]}
assert catalog["ou111111"]["name"] == "outfit"
assert pick_order == []
def _ctx_capture():
"""Spy that captures the ctx dict passed to PipelineEngine.run()."""
captured: dict = {}
class _FakePipeline:
def run(self, modules, *, ctx, seed):
captured.clear()
captured.update(ctx)
return ctx
return _FakePipeline, captured
def test_execute_injects_catalog_synthesised_from_modules(monkeypatch):
"""Wildcard entries in `modules` become `ctx['__wp_catalog__']`
keyed by id, exactly once at the top of execute. Spec §2.6 plus
the unified-list change — no separate `snapshots` field on the
wire any more."""
from wp_nodes import context_node as cn
fake_cls, captured = _ctx_capture()
monkeypatch.setattr(cn, "PipelineEngine", fake_cls)
cn.WPContext.execute(
seed=42,
wp_modules={
"modules": [
{
"id": "ou111111",
"type": "wildcard",
"enabled": True,
"meta": {"name": "outfit"},
"entries": [],
"payload": {"options": [{"value": "x"}]},
"payload_hash": "h" * 64,
},
],
},
upstream=None,
)
catalog = captured["__wp_catalog__"]
assert "ou111111" in catalog
assert catalog["ou111111"]["payload"] == {"options": [{"value": "x"}]}
assert catalog["ou111111"]["name"] == "outfit"
def test_execute_defaults_to_empty_catalog_for_old_workflow_json(monkeypatch):
"""Old JSON without `snapshots` field — catalog stays {} so resolver
emits "Unknown ref" warning rather than crashing. Spec §4.5."""
from wp_nodes import context_node as cn
fake_cls, captured = _ctx_capture()
monkeypatch.setattr(cn, "PipelineEngine", fake_cls)
cn.WPContext.execute(
seed=0,
wp_modules={"modules": []}, # no snapshots
upstream=None,
)
assert captured["__wp_catalog__"] == {}
def test_pipeline_trace_records_effective_seed_per_module():
"""The pipeline trace's `seed` field captures the seed each module
actually rolled with — `instance.locked_seed` when locked, else
the chain seed. Frontend reads this via the `module_seeds` UI
payload to populate lock-toggle defaults authoritatively (works
even when the seed input is link-driven)."""
from engine.pipeline import PipelineEngine
modules = [
{
"id": "m1",
"type": "wildcard",
"enabled": True,
"meta": {"name": "color"},
"entries": [],
"payload": {
"var_binding": "color",
"options": [{"id": "o1", "value": "red", "weight": 1}],
},
"instance": {"locked_seed": 99},
},
{
"id": "m2",
"type": "wildcard",
"enabled": True,
"meta": {"name": "shape"},
"entries": [],
"payload": {
"var_binding": "shape",
"options": [{"id": "o1", "value": "round", "weight": 1}],
},
},
]
ctx = PipelineEngine().run(modules, seed=12345)
trace = ctx["__wp_trace__"]
assert trace[0]["seed"] == 99 # locked
assert trace[1]["seed"] == 12345 # chain