202 lines
9.7 KiB
JavaScript
202 lines
9.7 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
|
|
import {
|
|
collapseRepeatedErrors,
|
|
emptySourceMessage,
|
|
findEmptySources,
|
|
nodeLacksSource,
|
|
nodesThatRun,
|
|
} from "../js/shared/run_check.mjs";
|
|
|
|
// Node definitions as the frontend holds them: only output_node and the
|
|
// lazy flag matter to the check.
|
|
const DEFINITIONS = {
|
|
SaveImage: { output_node: true, input: { required: { images: ["IMAGE"] } } },
|
|
PreviewImage: { output_node: true, input: { required: { images: ["IMAGE"] } } },
|
|
AUSBOSS_NODES_SaveVideo: { output_node: true, input: { optional: { frames: ["IMAGE"] } } },
|
|
PickOne: { input: { required: { first: ["IMAGE", { lazy: true }], second: ["IMAGE"] } } },
|
|
};
|
|
const definitionOf = (classType) => DEFINITIONS[classType] ?? null;
|
|
|
|
const node = (class_type, inputs = {}, title = class_type) => ({ class_type, inputs, _meta: { title } });
|
|
|
|
test("a blank Load Image that feeds a save is found, by its own title", () => {
|
|
const prompt = {
|
|
1: node("LoadImage", { image: "" }, "Your image"),
|
|
2: node("SaveImage", { images: ["1", 0] }),
|
|
};
|
|
assert.deepEqual(findEmptySources(prompt, definitionOf), [{ id: "1", title: "Your image", input: "image", kind: "picture" }]);
|
|
assert.equal(emptySourceMessage(findEmptySources(prompt, definitionOf)), "Load a picture first: Your image");
|
|
});
|
|
|
|
test("a picked picture, a wired name or a missing widget value are told apart", () => {
|
|
const run = (image) => findEmptySources({ 1: node("LoadImage", { image }), 2: node("SaveImage", { images: ["1", 0] }) }, definitionOf);
|
|
assert.deepEqual(run("pier.png"), []);
|
|
assert.deepEqual(run(["7", 0]), [], "a name fed by another node is set at run time");
|
|
assert.equal(run(undefined).length, 1);
|
|
assert.equal(run(" ").length, 1);
|
|
});
|
|
|
|
test("a loader no output needs never blocks the run", () => {
|
|
const prompt = {
|
|
1: node("LoadImage", { image: "" }),
|
|
2: node("LoadImage", { image: "pier.png" }),
|
|
3: node("SaveImage", { images: ["2", 0] }),
|
|
};
|
|
assert.deepEqual(findEmptySources(prompt, definitionOf), []);
|
|
});
|
|
|
|
test("bypassed and muted loaders are not in the prompt, so they are never found", () => {
|
|
// The frontend leaves them out of the prompt and reroutes a bypassed one.
|
|
const prompt = { 2: node("LoadImage", { image: "pier.png" }), 3: node("SaveImage", { images: ["2", 0] }) };
|
|
assert.deepEqual(findEmptySources(prompt, definitionOf), []);
|
|
});
|
|
|
|
test("a loader only behind a lazy input may never be read, so it is not found", () => {
|
|
const prompt = {
|
|
1: node("LoadImage", { image: "" }),
|
|
2: node("LoadImage", { image: "pier.png" }),
|
|
3: node("PickOne", { first: ["1", 0], second: ["2", 0] }),
|
|
4: node("SaveImage", { images: ["3", 0] }),
|
|
};
|
|
assert.deepEqual(findEmptySources(prompt, definitionOf), []);
|
|
prompt[3].inputs = { first: ["2", 0], second: ["1", 0] };
|
|
assert.deepEqual(findEmptySources(prompt, definitionOf).map((found) => found.id), ["1"]);
|
|
});
|
|
|
|
test("queueing selected outputs only checks what those outputs read", () => {
|
|
const prompt = {
|
|
1: node("LoadImage", { image: "" }),
|
|
2: node("SaveImage", { images: ["1", 0] }),
|
|
3: node("LoadImage", { image: "pier.png" }),
|
|
4: node("PreviewImage", { images: ["3", 0] }),
|
|
};
|
|
assert.deepEqual(findEmptySources(prompt, definitionOf, ["4"]), []);
|
|
assert.deepEqual(findEmptySources(prompt, definitionOf, ["2"]).map((found) => found.id), ["1"]);
|
|
assert.deepEqual([...nodesThatRun(prompt, definitionOf, [])].sort(), ["1", "2", "3", "4"], "no targets: every output");
|
|
});
|
|
|
|
test("nodes inside subgraphs keep their execution ids", () => {
|
|
const prompt = {
|
|
"12:5": node("LoadImage", { image: "" }, "Inner image"),
|
|
"12:6": node("SaveImage", { images: ["12:5", 0] }),
|
|
};
|
|
assert.deepEqual(findEmptySources(prompt, definitionOf).map((found) => found.id), ["12:5"]);
|
|
});
|
|
|
|
test("Load Image + Pad needs no file while a picture is wired in", () => {
|
|
const run = (inputs) => findEmptySources({ 1: node("AUSBOSS_NODES_LoadImagePad", inputs), 2: node("SaveImage", { images: ["1", 0] }) }, definitionOf);
|
|
assert.equal(run({ image: "" }).length, 1);
|
|
assert.deepEqual(run({ image: "", source_image: ["9", 0] }), []);
|
|
});
|
|
|
|
test("the video nodes check the file or the local path, whichever the mode reads", () => {
|
|
const run = (class_type, inputs) =>
|
|
findEmptySources({ 1: node(class_type, inputs, "Clip"), 2: node("AUSBOSS_NODES_SaveVideo", { frames: ["1", 0] }) }, definitionOf);
|
|
for (const class_type of ["AUSBOSS_NODES_VideoCropRotatePad", "AUSBOSS_NODES_VideoCropRotatePadClip"]) {
|
|
assert.deepEqual(run(class_type, { video: "", source_mode: "input folder", local_path: "" }).map((f) => f.input), ["video"]);
|
|
assert.deepEqual(run(class_type, { video: "pier.mp4", source_mode: "input folder", local_path: "" }), []);
|
|
assert.deepEqual(run(class_type, { video: "", source_mode: "local path", local_path: "" }).map((f) => f.input), ["local_path"]);
|
|
assert.deepEqual(run(class_type, { video: "", source_mode: "local path", local_path: "/comfy/output/a.mp4" }), []);
|
|
assert.deepEqual(run(class_type, { video: "", source_mode: ["4", 0], local_path: "" }), [], "a wired mode is known at run time");
|
|
}
|
|
const found = run("AUSBOSS_NODES_LoadVideo", { video: "" });
|
|
assert.equal(emptySourceMessage(found), "Load a video first: Clip");
|
|
assert.deepEqual(run("LoadVideo", { file: "" }).map((f) => f.input), ["file"]);
|
|
});
|
|
|
|
test("the message names the first loader and counts the rest", () => {
|
|
const found = [
|
|
{ id: "1", title: "Your image", kind: "picture" },
|
|
{ id: "2", title: "Load Video 🆎", kind: "video" },
|
|
{ id: "3", title: "Mask", kind: "picture" },
|
|
];
|
|
assert.equal(emptySourceMessage(found.slice(0, 2)), "Load a picture first: Your image (and 1 more node)");
|
|
assert.equal(emptySourceMessage(found), "Load a picture first: Your image (and 2 more nodes)");
|
|
assert.equal(emptySourceMessage([]), "");
|
|
});
|
|
|
|
test("the widget scan reads the live node the same way", () => {
|
|
const widget = (name, value) => ({ name, value });
|
|
assert.equal(nodeLacksSource({ comfyClass: "LoadImage", widgets: [widget("image", "")] }), true);
|
|
assert.equal(nodeLacksSource({ comfyClass: "LoadImage", widgets: [widget("image", "pier.png")] }), false);
|
|
assert.equal(
|
|
nodeLacksSource({ comfyClass: "LoadImage", widgets: [widget("image", "")], inputs: [{ name: "image", widget: { name: "image" }, link: 4 }] }),
|
|
false,
|
|
"a widget fed by a link is set",
|
|
);
|
|
assert.equal(
|
|
nodeLacksSource({ comfyClass: "AUSBOSS_NODES_LoadImagePad", widgets: [widget("image", "")], inputs: [{ name: "source_image", link: 9 }] }),
|
|
false,
|
|
);
|
|
assert.equal(nodeLacksSource({ type: "KSampler", widgets: [widget("seed", 0)] }), false);
|
|
assert.equal(nodeLacksSource(null), false);
|
|
});
|
|
|
|
// ---------------------------------------------------------------- collapse
|
|
|
|
const failed = (input_name, message) => ({
|
|
type: "custom_validation_failed",
|
|
message: "Custom validation failed for node",
|
|
details: `${input_name} - ${message}`,
|
|
extra_info: { input_name },
|
|
});
|
|
|
|
test("a failed check repeated on every input it read comes back once", () => {
|
|
const message = "Video Crop + Rotate + Pad: Select or upload a source file first.";
|
|
const errors = {
|
|
7: {
|
|
class_type: "AUSBOSS_NODES_VideoCropRotatePadClip",
|
|
errors: [failed("video", message), failed("source_mode", message), failed("local_path", message)],
|
|
dependent_outputs: ["9"],
|
|
},
|
|
};
|
|
collapseRepeatedErrors(errors);
|
|
assert.deepEqual(errors[7].errors.map((error) => error.extra_info.input_name), ["video"]);
|
|
assert.deepEqual(errors[7].dependent_outputs, ["9"]);
|
|
});
|
|
|
|
test("the error stays on the input its message is about", () => {
|
|
const nameMessage = "Save Image: exact_name may not contain '..'.";
|
|
const pathMessage = "Video Crop + Rotate + Pad: Local path mode requires a video path.";
|
|
const errors = {
|
|
1: { class_type: "AUSBOSS_NODES_SaveImage", errors: ["filename_prefix", "exact_name", "output_dir"].map((name) => failed(name, nameMessage)) },
|
|
2: { class_type: "AUSBOSS_NODES_VideoCropRotatePad", errors: ["video", "source_mode", "local_path"].map((name) => failed(name, pathMessage)) },
|
|
};
|
|
collapseRepeatedErrors(errors);
|
|
assert.deepEqual(errors[1].errors.map((error) => error.extra_info.input_name), ["exact_name"]);
|
|
assert.deepEqual(errors[2].errors.map((error) => error.extra_info.input_name), ["local_path"]);
|
|
});
|
|
|
|
test("different problems, other kinds of error and other packs' nodes are left alone", () => {
|
|
const range = { type: "value_smaller_than_min", message: "Value -5 smaller than min of 0", details: "crop_x", extra_info: { input_name: "crop_x" } };
|
|
const errors = {
|
|
1: { class_type: "AUSBOSS_NODES_LoraLoader", errors: [failed("loras", "LoRA Loader: a"), failed("on_missing", "LoRA Loader: a"), failed("loras", "LoRA Loader: b"), range] },
|
|
2: { class_type: "SomePackLoader", errors: [failed("x", "same"), failed("y", "same")] },
|
|
};
|
|
collapseRepeatedErrors(errors);
|
|
assert.deepEqual(errors[1].errors.map((error) => [error.extra_info.input_name, error.details]), [
|
|
["loras", "loras - LoRA Loader: a"],
|
|
["loras", "loras - LoRA Loader: b"],
|
|
["crop_x", "crop_x"],
|
|
]);
|
|
assert.equal(errors[2].errors.length, 2);
|
|
assert.equal(collapseRepeatedErrors(null), null);
|
|
});
|
|
|
|
test("an empty Load Audio asks for a sound file", () => {
|
|
const outputs = (classType) => (classType === "SaveAudio"
|
|
? { output_node: true, input: { required: { audio: ["AUDIO"] } } }
|
|
: definitionOf(classType));
|
|
const prompt = {
|
|
1: node("AUSBOSS_NODES_LoadAudio", { audio: "" }, "Song"),
|
|
2: node("SaveAudio", { audio: ["1", 0] }),
|
|
};
|
|
const found = findEmptySources(prompt, outputs);
|
|
assert.deepEqual(found.map((entry) => entry.input), ["audio"]);
|
|
assert.equal(emptySourceMessage(found), "Load a sound file first: Song");
|
|
prompt[1].inputs.audio = "song.mp3";
|
|
assert.deepEqual(findEmptySources(prompt, outputs), []);
|
|
});
|