The template input now renders through RichTextInput, so it has the booru tag
autocomplete, `$var` chips with producer attribution, and the same editing
behaviour as every other WP text surface.
No payload transition. It is the SAME `template` widget: same name, same
position in `widgets_values`, same plain string value — only the renderer
changed. A workflow saved before this loads its template back untouched;
verified by loading a hand-written pre-change workflow JSON.
The seam is `widgetType`. The frontend picks a widget constructor with
`t.widgetType ?? t.type` while the socket keeps `t.type`, so tagging the input
spec routes the lookup to us without touching what the socket accepts. It has
to go through `extra_dict`: V3's `widget_type=` parameter would ALSO change
the socket, because `WidgetInput.get_io_type` returns widget_type when set —
the input would stop being STRING and no upstream STRING output could feed it.
`extra_dict` is merged last in `Input.as_dict` and never consulted by
`get_io_type`. The new schema test asserts both halves together, since either
one alone passes while the feature is broken.
Keeping the link input needs one more thing: ComfyUI only calls
`addInput(name, spec.type, {widget})` when the widget the factory returns has
no `socketless` flag, and `createDomWidgetHost` hardcoded `socketless: true`.
Hence the new `socketed` option — correct for every other WP widget, exactly
wrong for this one. While a link IS connected the editor goes read-only with
a placeholder saying so, matching the stock multiline widget. Derived from the
link rather than the widget's `computedDisabled`, which is recomputed lazily
during a draw and still read `true` a second after the link was removed.
Why a separate file and not `assembler.ts`: the helper widget must stay
socketless (it is an editor, not a value), and the two widgets need opposite
answers to that one question.
The helper's template mutators lose their `inputEl` branches. Those had been
dead for a while — the frontend moved widget values into a store, so the
textarea at `widget.inputEl` is detached and unrendered, and each of those
functions was really working through the `w.value` write sitting next to it.
Insert is the exception: it needs a caret, so it asks the editor through a
WeakMap hook instead of replacing the whole string.
Verified live on frontend 1.48.6 with Vue Nodes on: legacy value restores,
STRING link connects and disconnects, serialize round-trips, clear / remove /
insert all propagate, `$` autocomplete lists upstream vars with attribution,
tag autocomplete and its legend work, and a full Context → assembler chain
previews correctly.
82 lines
3.4 KiB
Python
82 lines
3.4 KiB
Python
"""Unit tests for nodes.assembler_node.WPPromptAssembler."""
|
|
|
|
from wp_nodes.assembler_node import WPPromptAssembler
|
|
from wp_nodes.types import ContextPayload
|
|
|
|
|
|
class TestWPPromptAssemblerSchema:
|
|
def test_schema_basic(self):
|
|
schema = WPPromptAssembler.define_schema()
|
|
assert schema.node_id == "WP_PromptAssembler"
|
|
assert schema.display_name == "WP Prompt Assembler"
|
|
assert schema.category == "wildcard-pipeline"
|
|
|
|
def test_inputs(self):
|
|
schema = WPPromptAssembler.define_schema()
|
|
names = [s.name for s in schema.inputs]
|
|
assert names == ["context", "template"]
|
|
|
|
ctx_in, tmpl_in = schema.inputs
|
|
assert ctx_in.type_name == "PIPELINE_CONTEXT"
|
|
assert tmpl_in.type_name == "STRING"
|
|
assert tmpl_in.multiline is True
|
|
|
|
def test_template_uses_our_editor_without_losing_the_string_socket(self):
|
|
"""The whole point of routing `widgetType` through `extra_dict`.
|
|
|
|
The frontend picks a widget constructor with `widgetType ?? type` and
|
|
adds the socket from `type`, so these two facts have to hold at the
|
|
same time: the widget is ours, the socket is still STRING and can be
|
|
driven by an upstream STRING output.
|
|
|
|
Using V3's `widget_type=` parameter instead would satisfy the first
|
|
and break the second — `WidgetInput.get_io_type` returns widget_type
|
|
when set, so the socket would become `WP_TEMPLATE_EDITOR` and nothing
|
|
could connect to it. Assert both together; either one alone passes
|
|
while the feature is broken.
|
|
"""
|
|
schema = WPPromptAssembler.define_schema()
|
|
tmpl_in = next(s for s in schema.inputs if s.name == "template")
|
|
assert tmpl_in.type_name == "STRING"
|
|
assert (tmpl_in.extra_dict or {}).get("widgetType") == "WP_TEMPLATE_EDITOR"
|
|
|
|
def test_outputs(self):
|
|
schema = WPPromptAssembler.define_schema()
|
|
assert len(schema.outputs) == 1
|
|
assert schema.outputs[0].type_name == "STRING"
|
|
|
|
|
|
class TestWPPromptAssemblerExecute:
|
|
def test_resolves_single_var(self):
|
|
payload = ContextPayload(context={"style": "photoreal"}, debug={})
|
|
out = WPPromptAssembler.execute(context=payload, template="A $style shot")
|
|
assert out.values == ("A photoreal shot",)
|
|
|
|
def test_resolves_multiple_vars(self):
|
|
payload = ContextPayload(
|
|
context={"style": "photo", "subject": "knight", "light": "soft"},
|
|
debug={},
|
|
)
|
|
out = WPPromptAssembler.execute(
|
|
context=payload,
|
|
template="A $style $subject in $light light",
|
|
)
|
|
assert out.values == ("A photo knight in soft light",)
|
|
|
|
def test_missing_var_dropped_with_whitespace_cleanup(self):
|
|
# Missing vars resolve to empty string and surrounding whitespace is
|
|
# collapsed so the prompt stays clean.
|
|
payload = ContextPayload(context={}, debug={})
|
|
out = WPPromptAssembler.execute(context=payload, template="$unknown here")
|
|
assert out.values == ("here",)
|
|
|
|
def test_dollar_escape(self):
|
|
payload = ContextPayload(context={"x": "1"}, debug={})
|
|
out = WPPromptAssembler.execute(context=payload, template="$$ costs $x")
|
|
assert out.values == ("$ costs 1",)
|
|
|
|
def test_empty_template(self):
|
|
payload = ContextPayload(context={"x": "1"}, debug={})
|
|
out = WPPromptAssembler.execute(context=payload, template="")
|
|
assert out.values == ("",)
|