Files
DemonGatanjieu-Anomalous_Mo…/tests/combo_slots.mjs
T
DemonGatanjieuandClaude Opus 5.5 4ab08a36e4 Combos keep every picked node, not only model and text nodes
Save as combo now keeps all picked nodes but subgraphs, with the links
between them. Model drop-downs and text boxes stay the slots; nodes
without slots are listed in the editor under "Nodes put back as saved"
with their values and go back exactly as saved. Inputs fed by links from
unpicked nodes are stored in structure.open, and the save dialog, the
editor and the card say which ones to wire after putting it down. Cards
of combos without models name their nodes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 02:39:44 +08:00

81 lines
5.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// The rules of combo structures that need no canvas (web/modules/combo_slots.js).
import assert from 'node:assert/strict';
import { chainOrder, findPort, fedByLink, fixedNodes, folderFor, isModelWidget, missingTypes, packOf, structureKey, structureSummary } from '../web/modules/combo_slots.js';
const combo = (value, values) => ({ type: 'combo', value, options: { values } });
// Model drop-downs: by the files they list, not by their name.
assert.equal(isModelWidget(combo('ae.safetensors', ['ae.safetensors', 'other.sft'])), true);
assert.equal(isModelWidget(combo('None', ['None', 'Flux\\a.safetensors'])), true); // a LoRA stack row left empty
assert.equal(isModelWidget(combo('flux', ['sdxl', 'sd3', 'flux'])), false); // CLIP type: not a model
assert.equal(isModelWidget(combo('x.gguf', () => ['x.gguf'])), true); // options given as a function
assert.equal(isModelWidget({ type: 'text', value: 'a.safetensors' }), false);
// A box turned into an input and fed by a link is no slot.
const node = { inputs: [{ name: 'lora_name', widget: { name: 'lora_name' }, link: 7 }, { name: 'model', link: 3 }] };
assert.equal(fedByLink(node, { name: 'lora_name' }), true);
assert.equal(fedByLink(node, { name: 'strength' }), false);
// The models folder: the one whose files hold every option ("None" aside).
const lists = new Map([
['loras', new Set(['Flux/a.safetensors', 'b.safetensors'])],
['clip', new Set(['t5.safetensors', 'clip_l.safetensors'])],
['text_encoders', new Set(['t5.safetensors', 'clip_l.safetensors'])],
['checkpoints', new Set(['sdxl.safetensors'])],
['my_loras', new Set(['Flux/a.safetensors', 'b.safetensors', 'c.safetensors'])],
]);
assert.equal(folderFor(['None', 'Flux\\a.safetensors', 'b.safetensors'], 'None', lists), 'loras'); // known folder before a custom one
assert.equal(folderFor(['t5.safetensors', 'clip_l.safetensors'], 't5.safetensors', lists), 'text_encoders'); // clip is the same folder
assert.equal(folderFor(['c.safetensors'], 'c.safetensors', lists), 'my_loras');
assert.equal(folderFor(['nowhere.safetensors'], 'nowhere.safetensors', lists), '');
assert.equal(folderFor([], 'sdxl.safetensors', lists), 'checkpoints'); // no options: the value decides
assert.equal(folderFor([], 'None', lists), '');
lists.set('vae', new Set(['ae.safetensors']));
assert.equal(folderFor(['ae.safetensors', 'pixel_space', 'taesd'], 'ae.safetensors', lists), 'vae'); // built-in choices are no files
// Link ends: same name and type, else the old place, else the one port of that type.
const outputs = [{ name: 'MODEL', type: 'MODEL' }, { name: 'CLIP', type: 'CLIP' }, { name: 'VAE', type: 'VAE' }];
assert.equal(findPort(outputs, 'CLIP', 1, 'CLIP'), 1);
assert.equal(findPort([{ name: 'VAE', type: 'VAE' }, { name: 'CLIP', type: 'CLIP' }], 'CLIP', 0, 'CLIP'), 1); // moved
assert.equal(findPort([{ name: 'clip_out', type: 'CLIP' }], 'CLIP', 1, 'CLIP'), 0); // renamed, one of its type
assert.equal(findPort([{ name: 'a', type: 'CLIP' }, { name: 'b', type: 'CLIP' }], 'CLIP', 5, 'CLIP'), -1); // two: no guess
assert.equal(findPort(outputs, 'MODEL', 0, 'LATENT'), -1); // wrong type
// Node packs and missing node types (LiteGraph's registry).
globalThis.LiteGraph = { registered_node_types: {
UnetLoaderGGUF: { nodeData: { python_module: 'custom_nodes.ComfyUI-GGUF' } },
CLIPTextEncode: { nodeData: { python_module: 'nodes' } },
} };
assert.equal(packOf('UnetLoaderGGUF'), 'ComfyUI-GGUF');
assert.equal(packOf('CLIPTextEncode'), '');
const structure = { nodes: [{ type: 'UnetLoaderGGUF' }, { type: 'Missing', pack: 'Some-Pack' }, { type: 'Missing', pack: 'Some-Pack' }], links: [], slots: [] };
assert.deepEqual(missingTypes(structure), [{ type: 'Missing', pack: 'Some-Pack' }]);
// Same structure, whatever the values.
const one = { nodes: [{ type: 'A', key: 'n1' }], links: [{ from: 'n1', fromName: 'MODEL', to: 'n2', toName: 'model' }], slots: [{ node: 'n1', widget: 'x' }] };
assert.equal(structureKey(one), structureKey(JSON.parse(JSON.stringify(one))));
assert.notEqual(structureKey(one), structureKey({ ...one, slots: [] }));
// Repeated nodes are counted in the summary.
assert.equal(structureSummary({ nodes: [{ name: 'UNet' }, { name: 'Text' }, { name: 'LoRA' }, { name: 'Text' }, { title: 'Mine', name: 'LoRA' }] }),
'UNet + Text ×2 + LoRA + Mine');
// Flow order: loaders first, a LoRA chain from the model outwards, ties as given, loops last.
assert.deepEqual(chainOrder(['p1', 'p2', 'l1', 'l2', 'l3', 'l4', 'unet', 'clip'],
[['l3', 'l1'], ['l1', 'p1'], ['l1', 'p2'], ['unet', 'l2'], ['clip', 'l2'], ['l4', 'l3'], ['l2', 'l4']]),
['unet', 'clip', 'l2', 'l4', 'l3', 'l1', 'p1', 'p2']);
assert.deepEqual(chainOrder(['a', 'b', 'c'], [['a', 'b'], ['b', 'a'], ['x', 'c']]), ['c', 'a', 'b']);
// Nodes without slots are put back as saved, listed in flow order: an upscale model's loader with a slot,
// the node using it and a saver without.
const upscale = {
nodes: [{ key: 'n1', type: 'SaveImage' }, { key: 'n2', type: 'ImageUpscaleWithModel' }, { key: 'n3', type: 'UpscaleModelLoader' }],
links: [{ from: 'n3', to: 'n2' }, { from: 'n2', to: 'n1' }],
slots: [{ node: 'n3', widget: 'model_name' }],
};
assert.deepEqual(fixedNodes(upscale).map(item => item.type), ['ImageUpscaleWithModel', 'SaveImage']);
assert.deepEqual(fixedNodes({ ...upscale, slots: [] }).map(item => item.key), ['n3', 'n2', 'n1']);
assert.deepEqual(fixedNodes({}), []);
console.log('combo_slots: ok');