Files
robertvoy-ComfyUI-Distributed/web/tests/collectorOutputs.test.js
T
speedyrulzandClaude Fable 5.1 339719ece7 fix: harden per-GPU preview outputs after review
- 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>
2026-10-31 16:42:40 -04:00

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