feat(wp-nodes): inject __wp_catalog__ from embedded snapshots in WP_Context
This commit is contained in:
+19
-9
@@ -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",
|
||||
|
||||
@@ -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__"] == {}
|
||||
|
||||
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user