Pipeline wiring: core/graph.py gains wire()/unwire(), and the "Models and
pipeline" editor can now actually connect one node's output to another's
input -- sockets and widgets alike, which is what makes "linked inputs"
(several widgets sharing one Primitive node's value) buildable at all,
without a second mechanism. Three adversarial review rounds found and fixed
real bugs along the way: check()'s request builder silently dropped the
wire-specific fields; the widgets loop had no wiring UI at all despite the
backend supporting it, and a widget already holding a link rendered as an
editable box with the wrong (default) value; wiring an input that already
had an unsaved drafted value let the stale value silently win on Save. Also
fixed, found live rather than by review: the Add-node picker stacked two
disagreeing search boxes (an outer server search and filterList's own
redundant client-side one).
Updates window: "Restart ComfyUI" is now a plain, always-available button,
not just something offered when an update already landed. Rolling back or
switching branch on a dirty tree now auto-stashes instead of refusing --
updating still refuses, unchanged, since pulling new commits onto changes
in progress is not something to do without asking. Three review rounds
here too: stashing before reading the rollback target shifted what HEAD@{1}
meant (a real reflog hazard, not just reasoned about); a stash's disclosure
was lost if the step right after it failed; that fix was too narrow and
missed pull()'s own failures inside checkout(pull_after=True) -- the actual
default path -- and rollback()'s own reset failing; and finally, the
broadened fix still only caught GitUpdateError, missing subprocess-level
failures like a hung fetch timing out, which is the realistic failure mode
on a rented GPU box's network.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
100 lines
4.3 KiB
JavaScript
100 lines
4.3 KiB
JavaScript
// The pipeline client: what actually reaches /api/pipeline on the wire.
|
|
//
|
|
// pipeline_window.test.js injects a FAKE check(), so it can never catch a
|
|
// field this module forgets to forward -- which is exactly what happened:
|
|
// check()'s destructured signature had no `input`/`from_slot`/`from_output`,
|
|
// so a real wire()/unwire() call silently dropped them and the server
|
|
// answered "which input to unwire is named by a string, not a NoneType" to a
|
|
// button click that looked correct in every jsdom test. Caught live, in a
|
|
// browser, not by any suite -- this is the suite that would have caught it.
|
|
|
|
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
|
|
import { check, load, describe, search } from "../pipeline.js";
|
|
|
|
function fetchRecording(response = { slots: [], refused: [], incomplete: [], queueable: true }) {
|
|
const calls = [];
|
|
const fetch = async (url, opts) => {
|
|
calls.push({ url, opts, body: opts && opts.body ? JSON.parse(opts.body) : undefined });
|
|
return { ok: true, json: async () => response };
|
|
};
|
|
return { fetch, calls };
|
|
}
|
|
|
|
test("wire's own fields all reach the request body", async () => {
|
|
const { fetch, calls } = fetchRecording();
|
|
await check({
|
|
fetch, slots: [{ id: "a", node: "X", inputs: {} }],
|
|
action: "wire", slot: "sampler", input: "model", from_slot: "loader", from_output: 0,
|
|
});
|
|
assert.deepEqual(calls[0].body, {
|
|
action: "wire", slots: [{ id: "a", node: "X", inputs: {} }],
|
|
slot: "sampler", input: "model", from_slot: "loader", from_output: 0,
|
|
});
|
|
});
|
|
|
|
test("unwire's fields reach the request body, including a zero from_output on wire", async () => {
|
|
const { fetch, calls } = fetchRecording();
|
|
await check({ fetch, action: "unwire", slot: "sampler", input: "model" });
|
|
assert.deepEqual(calls[0].body, { action: "unwire", slot: "sampler", input: "model" });
|
|
|
|
const second = fetchRecording();
|
|
await check({
|
|
fetch: second.fetch, action: "wire", slot: "a", input: "x",
|
|
from_slot: "b", from_output: 0,
|
|
});
|
|
// 0 is a legal output index and must not be dropped the way `undefined` is.
|
|
assert.equal(second.calls[0].body.from_output, 0);
|
|
});
|
|
|
|
test("fields that were not passed are not sent as null or undefined", async () => {
|
|
const { fetch, calls } = fetchRecording();
|
|
await check({ fetch, action: "check" });
|
|
assert.equal("input" in calls[0].body, false);
|
|
assert.equal("from_slot" in calls[0].body, false);
|
|
assert.equal("from_output" in calls[0].body, false);
|
|
assert.equal("slot" in calls[0].body, false);
|
|
assert.equal("node" in calls[0].body, false);
|
|
});
|
|
|
|
test("a refused check comes back with the server's reasons, not a thrown error", async () => {
|
|
const fetch = async () => ({
|
|
ok: false, status: 400,
|
|
json: async () => ({ problems: ["which input to unwire is named by a string, not a NoneType"] }),
|
|
});
|
|
const result = await check({ fetch, action: "unwire", slot: "a" });
|
|
assert.equal(result.queueable, false);
|
|
assert.deepEqual(result.refused, ["which input to unwire is named by a string, not a NoneType"]);
|
|
});
|
|
|
|
test("load reads the pipeline as it stands", async () => {
|
|
const fetch = async () => ({
|
|
ok: true, json: async () => ({ slots: [{ id: "a" }], incomplete: ["x needs y"], queueable: false }),
|
|
});
|
|
const result = await load({ fetch });
|
|
assert.deepEqual(result, { slots: [{ id: "a" }], incomplete: ["x needs y"], queueable: false });
|
|
});
|
|
|
|
test("load throws with the status when the server refuses to answer at all", async () => {
|
|
const fetch = async () => ({ ok: false, status: 500 });
|
|
await assert.rejects(() => load({ fetch }), /500/);
|
|
});
|
|
|
|
test("describe asks nothing for an empty list, and answers by class name", async () => {
|
|
let asked = false;
|
|
const fetch = async () => { asked = true; return { ok: true, json: async () => ({ nodes: {} }) }; };
|
|
assert.deepEqual(await describe([], { fetch }), {});
|
|
assert.equal(asked, false);
|
|
|
|
const real = async (url) => ({
|
|
ok: true, json: async () => ({ nodes: { A: { node: "A" }, B: null } }),
|
|
});
|
|
assert.deepEqual(await describe(["A", "B"], { fetch: real }), { A: { node: "A" }, B: null });
|
|
});
|
|
|
|
test("search answers with nodes and a total even when the server sends neither", async () => {
|
|
const fetch = async () => ({ ok: true, json: async () => ({}) });
|
|
assert.deepEqual(await search("x", { fetch }), { nodes: [], total: 0 });
|
|
});
|