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.
146 lines
5.0 KiB
Python
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
|