- Typed serverless pools validate the unpruned workflow, so a preview on any new slot failed the whole run; both serverless compilers now accept every Collector output slot (COLLECTOR_OUTPUT_COUNT). - Serverless pools are config workers: their serverless:<pool>:<job> participants now share the pool's worker_N slot instead of overflowing. - A worker returning only empty frames no longer raises in the preview split (the combined batch already skipped them). - Worker previews are views into the combined batch, not a second copy, and a frame-count guard skips them rather than misattribute frames. - Frontend: one shared config fetch per load; unnamed slots drop their saved label instead of storing a redundant one. - Tooltip notes that the per-GPU outputs are unavailable downstream of Ultimate SD Upscale Distributed. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
63 lines
2.8 KiB
JavaScript
63 lines
2.8 KiB
JavaScript
import { readFileSync } from "node:fs";
|
|
import vm from "node:vm";
|
|
import { describe, expect, it } from "vitest";
|
|
|
|
const source = readFileSync(new URL("../collectorOutputs.js", import.meta.url), "utf8")
|
|
.replace(/^import .*;\r?\n/gm, "");
|
|
|
|
function setup(workers, onFetch = () => {}) {
|
|
let extension;
|
|
const listeners = new Map();
|
|
vm.runInNewContext(source, {
|
|
app: { registerExtension: (ext) => { extension = ext; } },
|
|
ENDPOINTS: { CONFIG: "/distributed/config" },
|
|
NODE_CLASSES: { DISTRIBUTED_COLLECTOR: "DistributedCollector" },
|
|
fetch: async () => { onFetch(); return { ok: true, json: async () => ({ workers }) }; },
|
|
Promise,
|
|
window: { addEventListener: (name, fn) => listeners.set(name, fn) },
|
|
parseInt,
|
|
Array,
|
|
console,
|
|
});
|
|
const node = {
|
|
comfyClass: "DistributedCollector",
|
|
outputs: ["images", "audio", "master", "worker_1", "worker_2", "worker_3"].map((name) => ({ name })),
|
|
};
|
|
return { extension, listeners, node };
|
|
}
|
|
|
|
describe("DistributedCollector per-GPU outputs", () => {
|
|
it("labels worker_N with the Nth configured worker and keeps slot names", async () => {
|
|
const { extension, node } = setup([{ id: "a", name: "GPU A", enabled: false }, { id: "b", name: "GPU B" }]);
|
|
await extension.nodeCreated(node);
|
|
expect(node.outputs.map((o) => o.name)).toEqual(
|
|
["images", "audio", "master", "worker_1", "worker_2", "worker_3"]);
|
|
expect(node.outputs.slice(3).map((o) => o.label)).toEqual(
|
|
["worker_1 (GPU A)", "worker_2 (GPU B)", undefined]);
|
|
expect(node.outputs[2].label).toBeUndefined();
|
|
});
|
|
|
|
it("shares one config fetch across collectors created together", async () => {
|
|
let fetches = 0;
|
|
const { extension, node } = setup([{ id: "a", name: "GPU A" }], () => { fetches += 1; });
|
|
const second = { ...node, outputs: node.outputs.map((o) => ({ ...o })) };
|
|
await Promise.all([extension.nodeCreated(node), extension.nodeCreated(second)]);
|
|
expect(fetches).toBe(1);
|
|
expect(second.outputs[3].label).toBe("worker_1 (GPU A)");
|
|
});
|
|
|
|
it("clears a stale saved label when the slot has no configured worker", async () => {
|
|
const { extension, node } = setup([]);
|
|
node.outputs[3].label = "worker_1 (Old PC)";
|
|
await extension.nodeCreated(node);
|
|
expect("label" in node.outputs[3]).toBe(false);
|
|
});
|
|
|
|
it("relabels when the worker list changes", async () => {
|
|
const { extension, listeners, node } = setup([{ id: "a", name: "GPU A" }]);
|
|
await extension.nodeCreated(node);
|
|
listeners.get("distributed:workers-changed")({ detail: { workers: [{ id: "c", name: "Renamed" }] } });
|
|
expect(node.outputs[3].label).toBe("worker_1 (Renamed)");
|
|
});
|
|
});
|