Files
DemonGatanjieu-Anomalous_Mo…/tests/graph_splice.mjs
T
DemonGatanjieuandClaude Opus 5.5 7b09909e5b test: publish the plugin's tests, one command to run them
tests/ (Python backend tests, .mjs frontend rule tests) is no longer
ignored. tools/run_tests.mjs runs the structure check, every
tests/*.mjs and the Python tests with $PYTHON, the portable
python_embeded or python. docs/guides/testing.md says how to run and
write them; README links it. Developer notes no longer point at 18 test
files that were removed earlier (AGENTS.md table, backend, frontend and
update-guide notes).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 17:47:35 +08:00

136 lines
6.6 KiB
JavaScript

// Where a LoRA goes and how it is wired (web/modules/graph_splice.js), on a small stand-in graph.
import assert from 'node:assert/strict';
import { chainEnd, loraInsertionCapabilities, mainModelLoaders, planLoraInsertion, spliceLora } from '../web/modules/graph_splice.js';
const SLOTS = {
CheckpointLoaderSimple: [[], ['MODEL', 'CLIP', 'VAE']],
UNETLoader: [[], ['MODEL']],
CLIPLoader: [[], ['CLIP']],
CLIPTextEncode: [['CLIP'], ['CONDITIONING']],
KSampler: [['MODEL', 'CONDITIONING', 'CONDITIONING', 'LATENT'], ['LATENT']],
LoraLoader: [['MODEL', 'CLIP'], ['MODEL', 'CLIP']],
LoraLoaderModelOnly: [['MODEL'], ['MODEL']],
};
class Graph {
constructor() { this._nodes = []; this.links = {}; this.nextLink = 1; this.nextNode = 1; }
getNodeById(id) { return this._nodes.find(node => node.id === id) || null; }
getLink(id) { return this.links[id] || null; }
add(node) { node.id ??= this.nextNode++; node.graph = this; this._nodes.push(node); }
remove(node) {
node.inputs.forEach((input, slot) => input.link != null && this.unlink(input.link));
node.outputs.forEach(output => [...output.links].forEach(id => this.unlink(id)));
this._nodes = this._nodes.filter(item => item !== node);
}
unlink(id) {
const link = this.links[id];
if (!link) return;
const origin = this.getNodeById(link.origin_id);
origin.outputs[link.origin_slot].links = origin.outputs[link.origin_slot].links.filter(item => item !== id);
this.getNodeById(link.target_id).inputs[link.target_slot].link = null;
delete this.links[id];
}
}
function node(graph, type, failConnect = false) {
const [inputs, outputs] = SLOTS[type];
const item = {
type, mode: 0,
inputs: inputs.map(t => ({ type: t, link: null })),
outputs: outputs.map(t => ({ type: t, links: [] })),
connect(slot, target, targetSlot) {
if (failConnect) return null;
const g = this.graph;
if (target.inputs[targetSlot].link != null) g.unlink(target.inputs[targetSlot].link);
const id = g.nextLink++;
g.links[id] = { id, origin_id: this.id, origin_slot: slot, target_id: target.id, target_slot: targetSlot };
this.outputs[slot].links.push(id);
target.inputs[targetSlot].link = id;
return g.links[id];
},
};
if (graph) graph.add(item);
return item;
}
const source = (graph, target, slot) => {
const link = graph.getLink(target.inputs[slot].link);
return link ? `${graph.getNodeById(link.origin_id).type}#${link.origin_id}` : null;
};
// A checkpoint whose CLIP feeds both prompts.
function checkpointWorkflow() {
const g = new Graph();
const ckpt = node(g, 'CheckpointLoaderSimple');
const pos = node(g, 'CLIPTextEncode');
const neg = node(g, 'CLIPTextEncode');
const sampler = node(g, 'KSampler');
ckpt.connect(1, pos, 0); ckpt.connect(1, neg, 0); ckpt.connect(0, sampler, 0);
pos.connect(0, sampler, 1); neg.connect(0, sampler, 2);
return { g, ckpt, pos, neg, sampler };
}
{
const { g, ckpt, pos, neg, sampler } = checkpointWorkflow();
const plan = planLoraInsertion(g, ckpt, 'after', { textEncoder: true });
assert.equal(plan.clip, true);
const lora = spliceLora({ graph: g, plan, anchorNode: ckpt, insertedNode: node(null, 'LoraLoader') });
assert.equal(source(g, lora, 0), `CheckpointLoaderSimple#${ckpt.id}`);
assert.equal(source(g, lora, 1), `CheckpointLoaderSimple#${ckpt.id}`);
for (const prompt of [pos, neg]) assert.equal(source(g, prompt, 0), `LoraLoader#${lora.id}`, 'both prompts move behind the LoRA');
assert.equal(source(g, sampler, 0), `LoraLoader#${lora.id}`);
// The next one goes after the LoRA already there; a model-only LoRA leaves CLIP alone.
assert.equal(chainEnd(g, mainModelLoaders(g)[0], 'MODEL'), lora);
const plan2 = planLoraInsertion(g, lora, 'after', { textEncoder: false });
const lora2 = spliceLora({ graph: g, plan: plan2, anchorNode: lora, insertedNode: node(null, 'LoraLoaderModelOnly') });
assert.equal(source(g, sampler, 0), `LoraLoaderModelOnly#${lora2.id}`);
assert.equal(source(g, pos, 0), `LoraLoader#${lora.id}`);
}
{
// A UNet with its own CLIP loader: the CLIP line comes from the CLIP loader.
const g = new Graph();
const unet = node(g, 'UNETLoader');
const clip = node(g, 'CLIPLoader');
const pos = node(g, 'CLIPTextEncode');
const neg = node(g, 'CLIPTextEncode');
const sampler = node(g, 'KSampler');
clip.connect(0, pos, 0); clip.connect(0, neg, 0); unet.connect(0, sampler, 0);
pos.connect(0, sampler, 1); neg.connect(0, sampler, 2);
const plan = planLoraInsertion(g, unet, 'after', { textEncoder: true });
const lora = spliceLora({ graph: g, plan, anchorNode: unet, insertedNode: node(null, 'LoraLoader') });
assert.equal(source(g, lora, 0), `UNETLoader#${unet.id}`);
assert.equal(source(g, lora, 1), `CLIPLoader#${clip.id}`);
assert.equal(source(g, neg, 0), `LoraLoader#${lora.id}`);
assert.equal(planLoraInsertion(g, unet, 'after', { textEncoder: false }).clip, false);
}
{
// Before a sampler: only its own model input moves; CLIP through the prompts wired into it.
const { g, ckpt, pos, neg, sampler } = checkpointWorkflow();
const caps = loraInsertionCapabilities(g, sampler);
assert.equal(caps.before.supported, true);
assert.equal(caps.after.supported, false); // a sampler has no MODEL output
const plan = planLoraInsertion(g, sampler, 'before', { textEncoder: true });
assert.equal(plan.clip, true);
const lora = spliceLora({ graph: g, plan, anchorNode: sampler, insertedNode: node(null, 'LoraLoader') });
assert.equal(source(g, sampler, 0), `LoraLoader#${lora.id}`);
assert.equal(source(g, pos, 0), `LoraLoader#${lora.id}`);
assert.equal(source(g, neg, 0), `LoraLoader#${lora.id}`);
assert.equal(source(g, lora, 1), `CheckpointLoaderSimple#${ckpt.id}`);
}
{
// A connection that fails leaves the graph as it was.
const { g, ckpt, pos, sampler } = checkpointWorkflow();
const plan = planLoraInsertion(g, ckpt, 'after', { textEncoder: true });
const broken = node(null, 'LoraLoader', true);
assert.throws(() => spliceLora({ graph: g, plan, anchorNode: ckpt, insertedNode: broken }), /Failed to connect/);
assert.equal(g._nodes.includes(broken), false);
assert.equal(source(g, pos, 0), `CheckpointLoaderSimple#${ckpt.id}`);
assert.equal(source(g, sampler, 0), `CheckpointLoaderSimple#${ckpt.id}`);
assert.equal(planLoraInsertion(g, pos, 'after').supported, false); // a prompt has no MODEL output
}
console.log('graph_splice: ok');