Files
ausboss-ComfyUI-AusBoss/tests/run_check.test.mjs

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), []);
});