Files
ausboss-ComfyUI-AusBoss/tests/lora_stack.test.mjs
T
ausboss c15c6c9317 Implement the pack audit batch: on-node pad editors, feature gaps, removals
The audit compared every node against ComfyUI core and the major packs;
this lands its outcomes in one branch.

On-node visual editing (the old Codex-pack interaction, rebuilt):
- New Load Image + Pad: image loader with whole-edge pad dragging on the
  node canvas, align-to-multiple rounding, and a megapixel target that
  rescales the source before padding so the mask seam stays binary.
- Pad Image gains the same handle canvas, fed by an execution-time
  snapshot of its input; wireframe with diagnostic empty states before
  the first run. Widgets, outputs, and saved workflows unchanged.
- Image Crop + Rotate + Pad's compact panel is now a live stage - crop,
  pad, and rotate handles on the node, one shared implementation with
  the full-screen editor so the two surfaces cannot drift.

Feature gaps closed against the ecosystem:
- Crop For Inpaint: target_megapixels, selectable rescale_algorithm
  (recorded in the stitcher for the paste-back), and extend_* inputs
  that grow the frame itself for outpainting.
- Save Video: format choice - mp4 h264, mp4 h265, webm vp9 with Opus
  (resampled to 48 kHz); CRF drives all three.
- Load Video: every_nth thinning (fps output divides to match) and a
  max_frames cap that ends the decode early.
- LoRA Loader: saved stack templates behind a bar button, and a console
  warning when a LoRA's base-model family disagrees with the checkpoint.
- LM Studio Chat: history in/out for multi-turn chains and a json_schema
  widget for structured replies.
- Drop Shadow: multiply blend and a shadow_mask output. Align Image:
  offset outputs for un-aligning after sampling. Color Match:
  first_frame reference mode as a one-node video flicker fix.

Removed (backed up in git history): Select Frame Range and the Video
Bundle family - core ImageFromBatch and the core VIDEO wire now cover
both jobs. Dead NODE_ID module variables cleaned out of every node file.

455 Python tests, 216 JS tests, validate_nodes.py green on 19 node files.
2026-08-17 10:49:55 -05:00

230 lines
9.4 KiB
JavaScript

import test from "node:test";
import assert from "node:assert/strict";
import {
DEFAULT_STEP,
FINE_STEP,
SCRUB_DEAD_ZONE,
SCRUB_PIXELS_PER_STEP,
clampHighlight,
clampStrength,
commonFolderPrefix,
filterLoras,
groupByFolder,
highlightedName,
isScrubbing,
moveHighlight,
moveRow,
newRow,
normalizeRows,
parseRows,
roundStrength,
scrubValue,
serializeRows,
setStrength,
strengthOutOfRange,
summarizeRows,
toggleAllRows,
toggleAllState,
toggleTrigger,
} from "../js/shared/lora_stack.mjs";
test("strengths clamp to the UI range and round to two decimals", () => {
assert.equal(clampStrength(50), 10);
assert.equal(clampStrength(-50), -10);
assert.equal(clampStrength("nonsense"), 0);
assert.equal(roundStrength(0.12345), 0.12);
});
test("normalizeRows heals partial rows and round-trips through serialize/parse", () => {
const rows = normalizeRows([
{ name: "styles/ink.safetensors", strength: 0.8 },
{ bogus: true },
"not an object",
]);
assert.equal(rows.length, 3);
assert.equal(rows[0].strength_clip, 0.8, "clip strength defaults to model strength");
assert.equal(rows[0].enabled, true);
assert.equal(rows[1].name, "");
const back = parseRows(serializeRows(rows));
assert.deepEqual(back, rows);
assert.deepEqual(parseRows("not json"), []);
});
test("row ids survive normalization so external references stay stable", () => {
const row = newRow();
const [kept] = normalizeRows([{ ...row, name: "a.safetensors" }]);
assert.equal(kept.id, row.id);
});
test("moveRow reorders within bounds and ignores impossible moves", () => {
const rows = normalizeRows([{ name: "a" }, { name: "b" }, { name: "c" }]);
const moved = moveRow(rows, 2, -1);
assert.deepEqual(moved.map((r) => r.name), ["a", "c", "b"]);
assert.equal(moveRow(rows, 0, -1), rows);
assert.equal(moveRow(rows, 2, 1), rows);
});
test("setStrength mirrors to clip only while linked", () => {
const rows = normalizeRows([{ name: "a", strength: 1 }]);
const linked = setStrength(rows, 0, 0.4, true);
assert.equal(linked[0].strength_clip, 0.4);
const split = setStrength(rows, 0, 0.4, false);
assert.equal(split[0].strength_clip, 1);
});
test("scrubbing is inert inside the dead zone, then steps with distance", () => {
assert.equal(scrubValue(1, SCRUB_DEAD_ZONE), 1);
assert.equal(scrubValue(1, -SCRUB_DEAD_ZONE), 1);
const oneStep = scrubValue(1, SCRUB_DEAD_ZONE + SCRUB_PIXELS_PER_STEP);
assert.equal(oneStep, roundStrength(1 + DEFAULT_STEP));
const fine = scrubValue(1, SCRUB_DEAD_ZONE + SCRUB_PIXELS_PER_STEP, true);
assert.equal(fine, roundStrength(1 + FINE_STEP));
assert.equal(scrubValue(9.99, 10_000), 10, "scrub clamps at the range edge");
assert.equal(scrubValue(-9.99, -10_000), -10);
});
test("scrubbing honors a custom coarse step but Shift stays fine", () => {
const travel = SCRUB_DEAD_ZONE + SCRUB_PIXELS_PER_STEP;
assert.equal(scrubValue(1, travel, false, 0.25), roundStrength(1.25));
assert.equal(scrubValue(1, travel, true, 0.25), roundStrength(1 + FINE_STEP));
assert.equal(scrubValue(1, travel, false, 0), roundStrength(1 + DEFAULT_STEP),
"junk steps fall back to the default");
});
test("a mostly-vertical drag is not a scrub", () => {
assert.equal(isScrubbing(10, 4), true);
assert.equal(isScrubbing(4, 10), false);
assert.equal(isScrubbing(SCRUB_DEAD_ZONE, 0), false);
});
test("picker filter is a case-insensitive substring over the full list", () => {
const names = ["styles/InkWash.safetensors", "faces/portrait.safetensors"];
assert.deepEqual(filterLoras(names, "ink"), ["styles/InkWash.safetensors"]);
assert.deepEqual(filterLoras(names, " "), names);
assert.deepEqual(filterLoras(names, "zzz"), []);
});
test("highlight wraps with arrows, clamps when the list shrinks, resolves names", () => {
assert.equal(moveHighlight(-1, 1, 3), 0, "first ArrowDown lands on the first item");
assert.equal(moveHighlight(-1, -1, 3), 2, "first ArrowUp lands on the last item");
assert.equal(moveHighlight(2, 1, 3), 0, "wraps past the end");
assert.equal(moveHighlight(0, -1, 3), 2, "wraps past the start");
assert.equal(moveHighlight(0, 1, 0), -1, "empty list has no highlight");
assert.equal(clampHighlight(5, 3), 2);
assert.equal(clampHighlight(-1, 3), 0);
assert.equal(highlightedName(["a", "b"], 1), "b");
assert.equal(highlightedName(["a", "b"], -1), null);
});
test("trigger chips toggle in and out preserving order", () => {
let triggers = toggleTrigger("", "ink wash");
triggers = toggleTrigger(triggers, "brush");
assert.equal(triggers, "ink wash, brush");
triggers = toggleTrigger(triggers, "Ink Wash");
assert.equal(triggers, "brush", "toggle-off matches case-insensitively");
});
test("row summary counts only named rows", () => {
assert.equal(summarizeRows([]), "no LoRAs");
const rows = normalizeRows([{ name: "a" }, { name: "b", enabled: false }, { name: "" }]);
assert.equal(summarizeRows(rows), "1 / 2 on");
});
test("master toggle reports on/off/mixed and blank rows do not vote", () => {
const mixed = normalizeRows([{ name: "a" }, { name: "b", enabled: false }, { name: "" }]);
assert.equal(toggleAllState(mixed), "mixed");
assert.equal(toggleAllState(normalizeRows([{ name: "a" }, { name: "" }])), "on");
assert.equal(toggleAllState(normalizeRows([{ name: "a", enabled: false }])), "off");
assert.equal(toggleAllState(normalizeRows([{ name: "" }])), "off");
});
test("master toggle click: mixed and off turn all on, on turns all off", () => {
const mixed = normalizeRows([{ name: "a" }, { name: "b", enabled: false }]);
const allOn = toggleAllRows(mixed);
assert.equal(toggleAllState(allOn), "on");
const allOff = toggleAllRows(allOn);
assert.equal(toggleAllState(allOff), "off");
assert.equal(toggleAllState(toggleAllRows(allOff)), "on");
});
test("common folder prefix strips whole segments only, never a lone file's dir", () => {
assert.equal(
commonFolderPrefix(["styles/wan/a.safetensors", "styles/wan/b.safetensors"]),
"styles/wan/"
);
assert.equal(commonFolderPrefix(["styles/a.safetensors", "faces/b.safetensors"]), "");
assert.equal(commonFolderPrefix(["only/one.safetensors"]), "");
// The shared segment is also one entry's full remaining path: stop above it.
assert.equal(commonFolderPrefix(["styles/a.safetensors", "styles/wan/b.safetensors"]), "styles/");
});
test("folder grouping buckets by top folder in first-appearance order", () => {
const groups = groupByFolder(["root.safetensors", "styles/a", "faces/b", "styles/c"]);
assert.deepEqual(
groups.map((group) => [group.folder, group.names.length]),
[["", 1], ["styles", 2], ["faces", 1]]
);
});
test("strength range flags only real finite bounds", () => {
assert.equal(strengthOutOfRange(1.5, { min: 0, max: 1 }), true);
assert.equal(strengthOutOfRange(-0.5, { min: 0, max: 1 }), true);
assert.equal(strengthOutOfRange(0.5, { min: 0, max: 1 }), false);
assert.equal(strengthOutOfRange(5, { min: null, max: null }), false);
assert.equal(strengthOutOfRange(5, null), false);
assert.equal(strengthOutOfRange(0.5, { max: 1 }), false);
});
test("templateFromRows snapshots rows without ids and trims the name", async () => {
const { templateFromRows } = await import("../js/shared/lora_stack.mjs");
const rows = [
{ id: "r1", name: "styles/a.safetensors", strength: 0.8, strength_clip: 0.6, enabled: true, triggers: "foo" },
];
const template = templateFromRows(" My Set ", rows);
assert.equal(template.name, "My Set");
assert.equal(template.rows.length, 1);
assert.equal(template.rows[0].id, undefined);
assert.equal(template.rows[0].strength, 0.8);
assert.equal(templateFromRows(" ", rows), null);
});
test("applyTemplate mints fresh row ids and keeps values", async () => {
const { applyTemplate, templateFromRows } = await import("../js/shared/lora_stack.mjs");
const template = templateFromRows("set", [
{ name: "a", strength: 0.5, strength_clip: 0.4, enabled: false, triggers: "x, y" },
]);
const rows = applyTemplate(template);
assert.equal(rows.length, 1);
assert.ok(rows[0].id);
assert.equal(rows[0].strength, 0.5);
assert.equal(rows[0].enabled, false);
assert.equal(rows[0].triggers, "x, y");
assert.deepEqual(applyTemplate(null), []);
});
test("upsertTemplate replaces same names case-insensitively and sorts", async () => {
const { templateFromRows, upsertTemplate } = await import("../js/shared/lora_stack.mjs");
const a = templateFromRows("Beta", []);
const b = templateFromRows("alpha", []);
let list = upsertTemplate(upsertTemplate([], a), b);
assert.deepEqual(list.map((t) => t.name), ["alpha", "Beta"]);
const replacement = templateFromRows("BETA", [{ name: "x" }]);
list = upsertTemplate(list, replacement);
assert.deepEqual(list.map((t) => t.name), ["alpha", "BETA"]);
assert.equal(list[1].rows.length, 1);
});
test("removeTemplate and parseTemplates round-trip storage", async () => {
const { parseTemplates, removeTemplate, templateFromRows, upsertTemplate } = await import(
"../js/shared/lora_stack.mjs"
);
const list = upsertTemplate([], templateFromRows("keep", [{ name: "a" }]));
const stored = JSON.stringify(upsertTemplate(list, templateFromRows("drop", [])));
const loaded = parseTemplates(stored);
assert.deepEqual(loaded.map((t) => t.name), ["drop", "keep"]);
assert.deepEqual(removeTemplate(loaded, "DROP").map((t) => t.name), ["keep"]);
assert.deepEqual(parseTemplates("not json"), []);
assert.deepEqual(parseTemplates('{"name":"x"}'), []);
});