Files
ausboss-ComfyUI-AusBoss/tests/input_preview.test.mjs
T
ausboss 5b8cad57d7 Loaders: take files ComfyUI made, as core Load Image does
Image Crop + Rotate + Pad, Load Image + Pad and Load Video refused a
picture named the way ComfyUI names a generated file
("photo.png [output]", "[temp]") with "The selected source is outside
ComfyUI's input folder." Core Load Image takes those names.

resolve_input_path now reads the name the same way the video nodes'
Outputs list already does (resolve_server_file): the tag picks the
input, output or temp folder, and the rest is a name below it. A whole
path, "~", "." and "..", and a network path are refused from the text
alone; a link that leads out of the folder is refused once resolved.
Both functions share that code. A whole path inside the input folder,
accepted before, is now refused too, as the security rule asks.

Load Video's player and the shared upstream preview stripped the tag
and asked /view for the input folder; they now keep the folder.

Tests cover input, [input], [output] and [temp] names through the
resolver and through ComfyUI's own validation next to core Load Image,
and the refused escapes. Help pages, SECURITY.md and CHANGELOG say so.
2026-10-21 14:21:55 -05:00

191 lines
7.9 KiB
JavaScript

import assert from "node:assert/strict";
import test from "node:test";
import {
describeNodePreview,
describeOwnResult,
describeSourcePreview,
linkById,
outputLocatorId,
outputRecordCount,
outputRecordQuery,
placeholderText,
staleText,
sourceFileWidget,
upstreamNode,
viewQueryForFile,
} from "../js/shared/input_preview.mjs";
test("links resolve through Map and legacy object stores", () => {
const link = { origin_id: 7 };
assert.equal(linkById({ links: new Map([[3, link]]) }, 3), link);
assert.equal(linkById({ links: { 3: link } }, 3), link);
assert.equal(linkById({ links: new Map() }, 3), null);
assert.equal(linkById({}, 3), null);
assert.equal(linkById({ links: new Map([[3, link]]) }, null), null);
});
test("the upstream node is found by input name", () => {
const source = { id: 7 };
const graph = {
links: new Map([[3, { origin_id: 7 }]]),
getNodeById: (id) => (id === 7 ? source : null),
};
const node = {
graph,
inputs: [{ name: "mask", link: null }, { name: "image", link: 3 }],
};
assert.equal(upstreamNode(node, "image"), source);
assert.equal(upstreamNode(node, "mask"), null);
assert.equal(upstreamNode(node, "model"), null);
assert.equal(upstreamNode({ inputs: [] }, "image"), null);
});
test("cached execution previews win over the file widget", () => {
const source = {
imgs: [{ src: "blob:thumbnail" }],
widgets: [{ name: "image", value: "photo.png" }],
};
assert.deepEqual(describeSourcePreview(source), {
kind: "url",
url: "blob:thumbnail",
});
});
test("file widgets resolve to a deterministic input /view query", () => {
assert.equal(
viewQueryForFile("clips\\photo.png [input]"),
new URLSearchParams({
filename: "photo.png",
subfolder: "clips",
type: "input",
}).toString(),
);
const imageSource = { widgets: [{ name: "image", value: "photo.png" }] };
assert.deepEqual(describeSourcePreview(imageSource), {
kind: "view",
isVideo: false,
query: viewQueryForFile("photo.png"),
});
const videoSource = { widgets: [{ name: "video", value: "loop.mp4" }] };
assert.equal(describeSourcePreview(videoSource).isVideo, true);
});
test("a picture ComfyUI made is previewed from its own folder", () => {
for (const [name, subfolder, type] of [
["moss_image_00001.png [output]", "", "output"],
["video\\take.mp4 [output]", "video", "output"],
["preview.png [temp]", "", "temp"],
]) {
const filename = name.replace(/^.*[\\/]/, "").replace(/ \[\w+\]$/, "");
assert.equal(viewQueryForFile(name), new URLSearchParams({ filename, subfolder, type }).toString());
}
});
test("the watchable file widget is found regardless of its value", () => {
const widget = { name: "video" };
assert.equal(sourceFileWidget({ widgets: [{ name: "seed" }, widget] }), widget);
assert.equal(sourceFileWidget({ widgets: [{ name: "seed" }] }), null);
assert.equal(sourceFileWidget(null), null);
});
test("unpreviewable sources fall back to the quiet placeholder", () => {
assert.equal(describeSourcePreview(null), null);
assert.equal(describeSourcePreview({ widgets: [{ name: "seed", value: 3 }] }), null);
assert.equal(describeSourcePreview({ widgets: [{ name: "image", value: "" }] }), null);
assert.equal(placeholderText(false, "an image"), "Connect an image to see it here");
assert.equal(placeholderText(false, "a mask"), "Connect a mask to see it here");
// Connected but nothing to show yet: the node has to run to make a picture,
// which is the whole instruction the panel can usefully give.
assert.equal(placeholderText(true, "a mask"), "Run the workflow to see it here");
// It ran with the preview off: the picture it kept is an older run's.
assert.equal(staleText(), "Run the workflow again to see it here");
});
test("the node's own result is the newest image it has", () => {
// Progress frames append during a run, so the last entry is the current one.
assert.deepEqual(
describeOwnResult({ imgs: [{ src: "frame1" }, { src: "frame9" }] }),
{ kind: "url", url: "frame9" },
);
assert.equal(describeOwnResult({ imgs: [] }), null);
assert.equal(describeOwnResult({ imgs: [{ src: "" }] }), null);
assert.equal(describeOwnResult({}), null);
assert.equal(describeOwnResult(null), null);
});
test("a node's own result outranks the input feeding it", () => {
const source = { imgs: [{ src: "upstream" }] };
const graph = {
links: new Map([[1, { origin_id: 5 }]]),
getNodeById: () => source,
};
const node = { graph, inputs: [{ name: "image", link: 1 }] };
// Before the run there is only the input to look at...
assert.deepEqual(describeNodePreview(node, "image"), { kind: "url", url: "upstream" });
// ...and once this node has produced something, that is what matters.
node.imgs = [{ src: "own-result" }];
assert.deepEqual(describeNodePreview(node, "image"), { kind: "url", url: "own-result" });
// Nothing anywhere stays null so the caller shows its placeholder.
assert.equal(describeNodePreview({ inputs: [] }, "image"), null);
});
test("a node's stored outputs are found under the frontend's key for it", () => {
const root = { id: "root-graph" };
root.rootGraph = root;
const subgraph = { id: "0f3c9a52-sub", rootGraph: root };
assert.equal(outputLocatorId({ id: 16, graph: root }), "16");
assert.equal(outputLocatorId({ id: 5, graph: subgraph }), "0f3c9a52-sub:5");
// A graph without a rootGraph link is its own root.
assert.equal(outputLocatorId({ id: 7, graph: {} }), "7");
assert.equal(outputLocatorId({ id: 7 }), null);
assert.equal(outputLocatorId(null), null);
});
test("the stored output resolves to the newest image the node wrote", () => {
const record = {
images: [
{ filename: "shot_00001.png", subfolder: "", type: "output" },
{ filename: "shot_00002.png", subfolder: "sets", type: "output" },
],
};
assert.equal(
outputRecordQuery(record),
new URLSearchParams({ filename: "shot_00002.png", subfolder: "sets", type: "output" }).toString(),
);
// A preview node's temp file keeps its own folder type.
const temp = { images: [{ filename: "ausboss_select_frame_ab_00001_.png", subfolder: "", type: "temp" }] };
assert.match(outputRecordQuery(temp), /type=temp/);
assert.equal(outputRecordQuery({ images: [] }), null);
// An index picks one of several pictures; a bad one falls back to the newest.
const three = { images: ["1", "2", "3"].map((n) => ({ filename: `s${n}.png`, subfolder: "", type: "output" })) };
assert.equal(outputRecordCount(three), 3);
assert.equal(outputRecordCount({}), 0);
assert.match(outputRecordQuery(three, 0), /filename=s1\.png/);
assert.match(outputRecordQuery(three, 1), /filename=s2\.png/);
assert.match(outputRecordQuery(three), /filename=s3\.png/);
assert.match(outputRecordQuery(three, 7), /filename=s3\.png/);
assert.match(outputRecordQuery(three, -1), /filename=s3\.png/);
assert.equal(outputRecordQuery({ images: [{ subfolder: "" }] }), null);
assert.equal(outputRecordQuery({ text: ["not an image"] }), null);
assert.equal(outputRecordQuery(undefined), null);
});
test("without node.imgs the stored output is the result, ahead of the input", () => {
// Nodes 2.0 never fills node.imgs once its own copy of the preview is
// stood down, so the stored output has to stand in for it.
const source = { imgs: [{ src: "upstream" }] };
const graph = {
links: new Map([[1, { origin_id: 5 }]]),
getNodeById: () => source,
};
const node = { graph, inputs: [{ name: "images", link: 1 }] };
assert.deepEqual(describeNodePreview(node, "images", "/view?stored"), { kind: "url", url: "/view?stored" });
// No stored output yet: the input is still what the panel shows.
assert.deepEqual(describeNodePreview(node, "images", null), { kind: "url", url: "upstream" });
// Classic fills node.imgs (streamed progress frames too); that still wins.
node.imgs = [{ src: "own-result" }];
assert.deepEqual(describeNodePreview(node, "images", "/view?stored"), { kind: "url", url: "own-result" });
});