Six fixes found while testing every node against real workflows. LoRA Loader: the "wrong base model" warning never fired on a modern stack. It matched comfy's model class name against a table covering 8 of the 81 classes comfy ships, so Krea 2, Qwen, WAN, LTXV, Z-Image and friends read as "unknown" and skipped the check entirely - an SD 1.5 LoRA on Krea 2 patched nothing, changed nothing, and said nothing. It now measures the applied-patch count across model and CLIP before and after each row and warns when a row patched nothing, which needs no table, no LoRA metadata, and works on every model family. base_model_family also stopped mining ss_sd_model_name: that is the trainer's source filename, and reading substrings out of it labelled any ..._v1.safetensors as SD1.5 and anything containing xl as SDXL. LM Studio Chat: reasoning returned in the server's own reasoning_content field now reaches the thinking output. Only inline <think> tags were read, so with gemma/qwen-style hybrids the whole reply landed in a field the node never looked at and text came back blank. An empty answer now explains itself, naming the max_tokens budget when the model ran out of room mid-thought. Compare: the stage follows the node's height, not only its width. Its panel declared a computeSize, and the widget layout gives any widget that defines one a fixed height and leaves it out of the leftover-space split, so dragging the node taller only added dead space under the image. Pad Image / Load Image + Pad: a stitcher output, so an outpaint can put the source back exactly. Padding now builds the same stitcher shape Crop For Inpaint emits - the crop is simply the whole canvas - so the existing Stitch Inpaint node serves both and no second stitch node has to exist. Appended last, so saved workflows keep their links. Settings menus: editing one row no longer reverts the others. Each save handed back a value set rebuilt from stored defaults, silently replacing every other row on a node whose workflow differed from those defaults. LoRA templates popover: Save was clipped off the panel edge and each saved row's delete x was stretched across the whole row - one blanket .ausboss-lora-menu button rule outranked the specialized buttons inside it.
124 lines
4.3 KiB
JavaScript
124 lines
4.3 KiB
JavaScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
|
|
import {
|
|
coerceSetting,
|
|
isOverrideActive,
|
|
isOverrideEntry,
|
|
mergeSettings,
|
|
overrideEnableValue,
|
|
overrideIsCustom,
|
|
schemaDefaults,
|
|
} from "../js/shared/node_settings.mjs";
|
|
|
|
const SCHEMA = [
|
|
{ section: "Behavior" },
|
|
{ key: "linked", label: "Linked strengths", type: "toggle", default: true },
|
|
{ key: "step", label: "Strength step", type: "number", default: 0.05, min: 0.01, max: 1 },
|
|
{ key: "separator", label: "Separator", type: "text", default: ", " },
|
|
{ key: "memory", label: "Memory", type: "choice", default: "standard", options: ["standard", "fast", "lowest"] },
|
|
];
|
|
|
|
test("schemaDefaults skips section headers", () => {
|
|
assert.deepEqual(schemaDefaults(SCHEMA), {
|
|
linked: true,
|
|
step: 0.05,
|
|
separator: ", ",
|
|
memory: "standard",
|
|
});
|
|
});
|
|
|
|
test("mergeSettings validates every stored value", () => {
|
|
const merged = mergeSettings(SCHEMA, {
|
|
linked: "yes", // wrong type -> default
|
|
step: "0.2", // numeric string -> number
|
|
separator: 7, // wrong type -> default
|
|
memory: "turbo", // not an option -> default
|
|
stray: true, // unknown key -> dropped
|
|
});
|
|
assert.deepEqual(merged, {
|
|
linked: true,
|
|
step: 0.2,
|
|
separator: ", ",
|
|
memory: "standard",
|
|
});
|
|
assert.equal("stray" in merged, false);
|
|
});
|
|
|
|
test("numbers clamp to the schema range", () => {
|
|
const entry = SCHEMA.find((item) => item.key === "step");
|
|
assert.equal(coerceSetting(entry, 99), 1);
|
|
assert.equal(coerceSetting(entry, 0), 0.01);
|
|
assert.equal(coerceSetting(entry, Number.NaN), 0.05);
|
|
assert.equal(coerceSetting(entry, ""), 0.05);
|
|
});
|
|
|
|
test("mergeSettings tolerates junk storage", () => {
|
|
assert.deepEqual(mergeSettings(SCHEMA, null), schemaDefaults(SCHEMA));
|
|
assert.deepEqual(mergeSettings(SCHEMA, "corrupt"), schemaDefaults(SCHEMA));
|
|
});
|
|
|
|
test("choice keeps valid stored picks", () => {
|
|
const merged = mergeSettings(SCHEMA, { memory: "fast", linked: false });
|
|
assert.equal(merged.memory, "fast");
|
|
assert.equal(merged.linked, false);
|
|
});
|
|
|
|
// ---------- override entries ----------
|
|
|
|
const TOP_P = {
|
|
key: "top_p", label: "Top-p", type: "number",
|
|
default: 1, neutral: 1, active: 0.95, min: 0, max: 1,
|
|
};
|
|
const PRESENCE = {
|
|
key: "presence_penalty", label: "Presence penalty", type: "number",
|
|
default: 0, neutral: 0, active: 0.5, min: -2, max: 2,
|
|
};
|
|
const PLAIN = { key: "steps", label: "Steps", type: "number", default: 8, min: 1, max: 50 };
|
|
|
|
test("only entries declaring a neutral value are overrides", () => {
|
|
assert.equal(isOverrideEntry(TOP_P), true);
|
|
assert.equal(isOverrideEntry(PLAIN), false);
|
|
assert.equal(isOverrideEntry({ section: "Sampling" }), false);
|
|
assert.equal(isOverrideEntry(undefined), false);
|
|
});
|
|
|
|
test("an override sitting on its neutral value is off", () => {
|
|
assert.equal(isOverrideActive(TOP_P, 1), false);
|
|
assert.equal(isOverrideActive(TOP_P, 0.9), true);
|
|
assert.equal(isOverrideActive(PRESENCE, 0), false);
|
|
assert.equal(isOverrideActive(PRESENCE, -0.5), true);
|
|
});
|
|
|
|
test("a hand-typed neutral value reads as off", () => {
|
|
// The checkbox has to follow the number box, not just the other way round.
|
|
assert.equal(isOverrideActive(TOP_P, "1"), false);
|
|
});
|
|
|
|
test("plain entries are always active", () => {
|
|
assert.equal(isOverrideActive(PLAIN, 8), true);
|
|
});
|
|
|
|
test("ticking on prefers the user's previous value", () => {
|
|
assert.equal(overrideEnableValue(TOP_P, 0.8), 0.8);
|
|
});
|
|
|
|
test("ticking on falls back to the suggested active value", () => {
|
|
assert.equal(overrideEnableValue(TOP_P, undefined), 0.95);
|
|
assert.equal(overrideEnableValue(PRESENCE, undefined), 0.5);
|
|
});
|
|
|
|
test("ticking on never lands back on neutral", () => {
|
|
// A remembered value of exactly neutral would leave a ticked box sending
|
|
// nothing, which reads as a broken control.
|
|
assert.notEqual(overrideEnableValue(TOP_P, 1), 1);
|
|
const stuck = { key: "x", type: "number", default: 0, neutral: 0, min: 0, max: 1 };
|
|
assert.notEqual(overrideEnableValue(stuck, 0), 0);
|
|
});
|
|
|
|
test("the reset affordance shows only for an on, customized row", () => {
|
|
assert.equal(overrideIsCustom(TOP_P, 1), false); // off
|
|
assert.equal(overrideIsCustom(TOP_P, 0.95), false); // on, at the suggestion
|
|
assert.equal(overrideIsCustom(TOP_P, 0.3), true); // on, customized
|
|
});
|