Files
olivv-cs-ComfyUI-FunPack/app/shell/tests/pipeline.test.js
T
digital-garbageandClaude Sonnet 5 75e9e9f686 core, app: pipeline wiring, and Restart/auto-stash for the updater
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>
2026-09-07 19:25:06 +03:00

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 });
});