Files
DemonGatanjieu-Anomalous_Mo…/tests/param_compare.mjs
T
DemonGatanjieuandClaude Opus 5.5 5c0448f009 Key parameters and parameter sets in view
Star a value in the comparison (key_params.js: node type + widget name, a hidden ComfyUI
setting) and it stays at the top of every comparison; equal values can be listed to be
starred. Recipe cards show 参数方案 N (sets unlike the recipe, from the new read-only
GET /anomalous/parameters/signatures) and open the Parameters tab; each set there shows
its key values, how many values differ from the recipe, and ⇄ to compare
(ui_param_set_keys.js). The window also compares saved workflows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 19:56:14 +08:00

87 lines
5.3 KiB
JavaScript

// node tests/param_compare.mjs — two sets of workflow parameters side by side.
import assert from 'node:assert/strict';
import { compareParams, readParams, shownValue, textDifference } from '../web/modules/param_compare.js';
const node = (id, type, widgets, extra = {}) => ({
id, type, title: type, mode: 0, inputs: [],
widgets: Object.entries(widgets).map(([name, value]) => ({ name, value })), ...extra,
});
const graph = (nodes, links = {}) => ({ _nodes: nodes, links });
const imageA = graph([
node(3, 'KSampler', { seed: 11, steps: 20, cfg: 7 }, { inputs: [{ name: 'model', link: 1 }] }),
node(4, 'CheckpointLoaderSimple', { ckpt_name: 'a.safetensors' }),
node(6, 'CLIPTextEncode', { text: '1girl, red dress, smile, outdoors' }),
], { 1: [1, 4, 0, 3, 0, 'MODEL'] });
const imageB = graph([
node(3, 'KSampler', { seed: 99, steps: 30, cfg: 7 }, { inputs: [{ name: 'model', link: 1 }] }),
node(4, 'CheckpointLoaderSimple', { ckpt_name: 'a.safetensors' }),
node(6, 'CLIPTextEncode', { text: '1girl, white dress, smile, outdoors' }),
], { 1: [1, 4, 0, 3, 0, 'MODEL'] });
const same = compareParams(readParams(imageA), readParams(imageB));
assert.equal(same.sameStructure, true);
assert.equal(same.sameCount, 2); // cfg and ckpt_name
assert.deepEqual(same.nodes.map(n => [n.id, n.rows.map(r => [r.name, r.aText, r.bText, r.volatile])]), [
['3', [['seed', '11', '99', true], ['steps', '20', '30', false]]],
['6', [['text', '1girl, red dress, smile, outdoors', '1girl, white dress, smile, outdoors', false]]],
]);
assert.equal(same.nodes[0].rows[1].index, 1); // the widget's place on the B side, to write it
// A node bypassed on one side, one only on one side, and an input wired elsewhere.
const imageC = graph([
node(3, 'KSampler', { seed: 11, steps: 20, cfg: 7 }, { mode: 4, inputs: [{ name: 'model', link: 2 }] }),
node(4, 'CheckpointLoaderSimple', { ckpt_name: 'a.safetensors' }),
node(9, 'LoraLoader', { lora_name: 'x.safetensors' }),
], new Map([[2, { origin_id: 9, origin_slot: 0 }]]));
const changed = compareParams(readParams(imageA), readParams(imageC));
assert.equal(changed.sameStructure, false);
assert.deepEqual(changed.onlyA.map(n => n.id), ['6']);
assert.deepEqual(changed.onlyB.map(n => n.id), ['9']);
assert.deepEqual(changed.rewired.map(n => n.id), ['3']);
assert.deepEqual(changed.nodes[0].rows[0], { name: '', mode: true, a: 0, b: 4 });
// A node of another type under the same id is not compared value by value.
const retyped = compareParams(readParams(graph([node(5, 'A', { x: 1 })])), readParams(graph([node(5, 'B', { x: 2 })])));
assert.deepEqual([retyped.onlyA.map(n => n.type), retyped.onlyB.map(n => n.type), retyped.nodes], [['A'], ['B'], []]);
// Starred values stand apart, equal ones too, and count as compared like the rest.
const starred = compareParams(readParams(imageA), readParams(imageB), { isKey: (type, widget) => type === 'KSampler' && ['cfg', 'steps'].includes(widget) });
assert.deepEqual(starred.keys.map(({ node, row }) => [node.id, row.name, row.same]), [['3', 'steps', false], ['3', 'cfg', true]]);
assert.deepEqual(starred.nodes.map(n => [n.id, n.rows.map(r => r.name)]), [['3', ['seed']], ['6', ['text']]]);
assert.deepEqual([starred.sameCount, starred.compared], [2, 5]);
// Equal values listed on request, marked same, counted the same way.
const all = compareParams(readParams(imageA), readParams(imageB), { includeSame: true });
assert.deepEqual(all.nodes.map(n => [n.id, n.rows.map(r => [r.name, r.same])]), [
['3', [['seed', false], ['steps', false], ['cfg', true]]],
['4', [['ckpt_name', true]]],
['6', [['text', false]]],
]);
assert.deepEqual([all.sameCount, all.compared], [2, 5]);
// Two workflows numbered differently: a type found once on each side still pairs, by type.
const other = compareParams(
readParams(graph([node(3, 'KSampler', { steps: 20 }), node(6, 'CLIPTextEncode', { text: 'a' }), node(7, 'CLIPTextEncode', { text: 'b' })])),
readParams(graph([node(70, 'KSampler', { steps: 30 }), node(71, 'CLIPTextEncode', { text: 'c' }), node(72, 'CLIPTextEncode', { text: 'd' })])),
);
assert.equal(other.sameStructure, false);
assert.deepEqual(other.nodes.map(n => [n.aId, n.bId, n.id, n.rows[0].aText, n.rows[0].bText]), [['3', '70', '70', '20', '30']]);
assert.deepEqual([other.onlyA.map(n => n.id), other.onlyB.map(n => n.id), other.compared], [['6', '7'], ['71', '72'], 1]);
assert.equal(compareParams(readParams(graph([node(1, 'A', {})])), readParams(graph([node(2, 'B', {})]))).compared, 0);
// Prompts as tags: the tags only one side has, each once.
assert.deepEqual(textDifference('1girl, red dress, smile, red dress', '1girl, smile, white dress, hat'),
{ tags: true, onlyA: ['red dress'], onlyB: ['white dress', 'hat'] });
// Other text: the changed part with a little around it.
const long = `${'a'.repeat(50)} cat ${'b'.repeat(50)}`;
assert.deepEqual(textDifference(long, long.replace('cat', 'dog')), {
tags: false, before: `…${'a'.repeat(39)} `, aPart: 'cat', bPart: 'dog', after: ` ${'b'.repeat(39)}…`,
});
assert.deepEqual(textDifference('steps 20', 'steps 30'), { tags: false, before: 'steps ', aPart: '2', bPart: '3', after: '0' });
assert.equal(shownValue(0.30000000000000004), '0.3');
assert.equal(shownValue(''), '—');
assert.equal(shownValue(['x', 1]), '["x",1]');
console.log('param_compare ok');