feat(wp-nodes): inject __wp_catalog__ from embedded snapshots in WP_Context

This commit is contained in:
DumiFlex
2026-04-28 14:17:30 +03:00
parent 40042ac1c1
commit 323dd55e63
4 changed files with 87 additions and 16 deletions
+19 -9
View File
@@ -37,10 +37,20 @@ class PipelineEngine:
ctx.setdefault("__wp_internal_flags__", {})
for index, module in enumerate(modules):
if not getattr(module, "enabled", True):
# Normalise id/type/enabled reads to work for both dicts and objects.
if isinstance(module, dict):
_module_id = module.get("id", "")
_module_type_raw = module.get("type", None) or ""
_module_enabled = module.get("enabled", True)
else:
_module_id = getattr(module, "id", "")
_module_type_raw = getattr(module, "type", None) or ""
_module_enabled = getattr(module, "enabled", True)
if not _module_enabled:
ctx["__wp_trace__"].append({
"id": getattr(module, "id", ""),
"type": getattr(module, "type", ""),
"id": _module_id,
"type": _module_type_raw,
"enabled": False,
"status": "skipped_disabled",
"writes": [],
@@ -48,7 +58,7 @@ class PipelineEngine:
})
continue
module_type = getattr(module, "type", None) or ""
module_type = _module_type_raw
try:
# coerce_legacy_module expects a dict; convert dataclasses or
# duck-typed objects by reading known attrs via getattr.
@@ -75,7 +85,7 @@ class PipelineEngine:
"Failed to coerce module %r at index %s: %s", module, index, e
)
ctx["__wp_trace__"].append({
"id": getattr(module, "id", ""),
"id": _module_id,
"type": module_type,
"enabled": True,
"status": "failed",
@@ -93,7 +103,7 @@ class PipelineEngine:
index,
)
ctx["__wp_trace__"].append({
"id": getattr(module, "id", ""),
"id": _module_id,
"type": module_type,
"enabled": True,
"status": "skipped_unknown_type",
@@ -108,7 +118,7 @@ class PipelineEngine:
ctx["__wp_warnings__"].append({
"type": "handler_error",
"severity": "error",
"module_id": getattr(module, "id", ""),
"module_id": _module_id,
"source_field": "payload",
"position": 0,
"token_index": None,
@@ -122,7 +132,7 @@ class PipelineEngine:
),
})
ctx["__wp_trace__"].append({
"id": getattr(module, "id", ""),
"id": _module_id,
"type": module_type,
"enabled": True,
"status": "failed",
@@ -144,7 +154,7 @@ class PipelineEngine:
})
ctx["__wp_trace__"].append({
"id": getattr(module, "id", ""),
"id": _module_id,
"type": module_type,
"enabled": True,
"status": "ok",
+7 -5
View File
@@ -2,8 +2,6 @@
import json
import pytest
from wp_nodes.context_node import WPContext
from wp_nodes.types import ContextPayload
@@ -88,9 +86,13 @@ class TestWPContextExecute:
assert result.context == {}
assert result.debug["trace"] == []
def test_malformed_modules_raises(self):
with pytest.raises(ValueError):
WPContext.execute(seed=0, modules="{not json", upstream=None)
def test_malformed_modules_runs_empty(self):
# deserialize_node_input is robust: malformed JSON returns ([], {}, [])
# so the pipeline runs with an empty module list rather than crashing.
out = WPContext.execute(seed=0, modules="{not json", upstream=None)
result = out.values[0]
assert result.context == {}
assert result.debug["trace"] == []
def test_downstream_overwrite_flows(self):
first = WPContext.execute(
@@ -41,3 +41,53 @@ def test_deserialize_extracts_snapshots_and_pickorder_from_new_input():
assert len(modules) == 1
assert "ou111111" in snapshots
assert pick_order == ["ou111111"]
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_from_snapshots_field(monkeypatch):
"""Snapshots from the input become ctx['__wp_catalog__'] exactly once
at the top of execute. Spec §2.6."""
from wp_nodes import context_node as cn
fake_cls, captured = _ctx_capture()
monkeypatch.setattr(cn, "PipelineEngine", fake_cls)
snapshots = {
"ou111111": {"snapshot_version": 1, "uuid": "ou111111",
"type": "wildcard", "name": "outfit",
"payload": {"options": []}, "payload_hash": "h" * 64,
"source": {"kind": "user"}},
}
cn.WPContext.execute(
seed=42,
modules={"modules": [], "snapshots": snapshots, "pickOrder": ["ou111111"]},
upstream=None,
)
assert captured["__wp_catalog__"] == snapshots
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,
modules={"modules": []}, # no snapshots
upstream=None,
)
assert captured["__wp_catalog__"] == {}
+11 -2
View File
@@ -7,7 +7,7 @@ from wp_nodes.types import (
ContextModulesInput,
PipelineContext,
build_payload,
deserialize_modules,
deserialize_node_input,
)
@@ -40,8 +40,17 @@ class WPContext(io.ComfyNode):
upstream_ctx: dict = upstream.context if upstream is not None else {}
upstream_debug: dict = upstream.debug if upstream is not None else {}
module_list = deserialize_modules(modules)
# Three orthogonal concerns from one widget value (spec §4.4):
# - module_list: what the pipeline executes (any kind, ordered)
# - snapshots: catalog for @{} ref resolution (wildcards only)
# - pickOrder: explicit user picks (UI-only, not used here)
module_list, snapshots, _pick_order = deserialize_node_input(modules)
ctx: dict = dict(upstream_ctx)
# Inject catalog ONCE at the top of execute, before pipeline run.
# Spec §2.6 — never re-inject mid-run.
ctx["__wp_catalog__"] = snapshots
ctx = PipelineEngine().run(module_list, ctx=ctx, seed=int(seed))
payload = build_payload(ctx, upstream_debug=upstream_debug, seed=int(seed))