75 lines
5.6 KiB
JavaScript
75 lines
5.6 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
import {readFileSync} from 'node:fs';
|
|
import {reconcileConfiguredNodeOutputs,reconcileLoraRegistryOutputOrder} from '../ui/src/regional/nodeOutputCompatibility.ts';
|
|
|
|
function fixture(map=true){
|
|
const graph={_nodes:[],links:map?new Map():{},getNodeById(id){return this._nodes.find(n=>String(n.id)===String(id))}};
|
|
const node={id:1,type:'BV LoRA Registry',graph,widgets_values:['unchanged'],outputs:[['resource_provider','BV_RUNTIME_RESOURCE_PROVIDER'],['lora_count','INT'],['registry_summary','STRING']].map(([name,type],i)=>({name,type,slot_index:i,links:[10+i]}))};
|
|
graph._nodes.push(node);
|
|
for(let i=0;i<3;i++){
|
|
const target={id:2+i,graph,inputs:[{link:10+i}]};graph._nodes.push(target);
|
|
const link={id:10+i,origin_id:1,origin_slot:i,target_id:target.id,target_slot:0,type:node.outputs[i].type};
|
|
if(map)graph.links.set(link.id,link);else graph.links[link.id]=link;
|
|
}
|
|
return {node,graph,links:()=>map?[...graph.links.values()]:Object.values(graph.links)};
|
|
}
|
|
for(const map of [true,false])test(`saved 1.2.3 outputs preserve identities and execution meaning (${map?'Map':'object'})`,()=>{
|
|
const f=fixture(map), original=[...f.node.outputs],targets=f.graph._nodes.slice(1).map(n=>JSON.stringify(n.inputs));
|
|
const before=f.links().map(({origin_slot,...rest})=>rest);
|
|
assert.equal(reconcileConfiguredNodeOutputs(f.graph),1);
|
|
assert.deepEqual(f.node.outputs,[original[1],original[2],original[0]]);
|
|
assert.deepEqual(f.links().map(l=>l.origin_slot),[2,0,1]);
|
|
assert.deepEqual(f.links().map(({origin_slot,...rest})=>rest),before);
|
|
assert.deepEqual(f.graph._nodes.slice(1).map(n=>JSON.stringify(n.inputs)),targets);
|
|
assert.deepEqual(f.node.outputs.map(s=>s.slot_index),[0,1,2]);
|
|
// API consumers use indices; each old consumer must receive its original value.
|
|
const backend=['count','summary','provider'];
|
|
assert.deepEqual(f.links().map(l=>backend[l.origin_slot]),['provider','count','summary']);
|
|
assert.deepEqual(f.node.widgets_values,['unchanged']);
|
|
assert.equal(reconcileConfiguredNodeOutputs(f.graph),0);
|
|
});
|
|
test('root and nested concrete subgraphs with overlapping node IDs are independent',()=>{
|
|
const a=fixture(),b=fixture(false);a.graph._nodes.push({id:7,subgraph:b.graph});
|
|
assert.equal(reconcileConfiguredNodeOutputs(a.graph),2);
|
|
assert.deepEqual(a.links().map(l=>l.origin_slot),[2,0,1]);assert.deepEqual(b.links().map(l=>l.origin_slot),[2,0,1]);
|
|
});
|
|
test('missing graph links defer without touching outputs; ready graph can migrate later',()=>{
|
|
const f=fixture(),outputs=f.node.outputs,link=f.graph.links.get(12);f.graph.links.delete(12);
|
|
assert.equal(reconcileLoraRegistryOutputOrder(f.node),false);assert.equal(f.node.outputs,outputs);assert.equal(f.graph.links.get(10).origin_slot,0);
|
|
f.graph.links.set(12,link);assert.equal(reconcileLoraRegistryOutputOrder(f.node),true);
|
|
});
|
|
test('native Subgraph output boundary retains slot identity and external consumers',()=>{
|
|
const f=fixture(), boundary={id:-20,slots:f.node.outputs.map((slot,i)=>({id:`boundary-${i}`,type:slot.type,linkIds:[10+i]}))};
|
|
f.graph._nodes.splice(1);f.graph.outputNode=boundary;
|
|
f.links().forEach((link,i)=>{link.target_id=boundary.id;link.target_slot=i});
|
|
const external={id:40,inputs:[{link:50}]},instance={id:30,subgraph:f.graph,outputs:[{name:'provider',type:'BV_RUNTIME_RESOURCE_PROVIDER',links:[50]}]};
|
|
const root={_nodes:[instance,external],links:new Map([[50,{id:50,origin_id:30,origin_slot:0,target_id:40,target_slot:0,type:'BV_RUNTIME_RESOURCE_PROVIDER'}]])};
|
|
const before=JSON.stringify([boundary,instance.outputs,external.inputs,[...root.links.values()]]);
|
|
assert.equal(reconcileConfiguredNodeOutputs(root),1);
|
|
assert.deepEqual(f.links().map(link=>link.origin_slot),[2,0,1]);
|
|
assert.deepEqual(f.links().map(link=>link.target_slot),[0,1,2]);
|
|
assert.equal(JSON.stringify([boundary,instance.outputs,external.inputs,[...root.links.values()]]),before);
|
|
assert.equal(reconcileConfiguredNodeOutputs(root),0);
|
|
});
|
|
test('inconsistent native boundary references leave every output and link unchanged',()=>{
|
|
const f=fixture();f.graph.outputNode={id:-20,slots:[{type:'BV_RUNTIME_RESOURCE_PROVIDER',linkIds:[999]}]};
|
|
f.graph.links.get(10).target_id=-20;
|
|
const before=JSON.stringify([f.node.outputs,f.links()]);
|
|
assert.equal(reconcileConfiguredNodeOutputs(f.graph),0);
|
|
assert.equal(JSON.stringify([f.node.outputs,f.links()]),before);
|
|
});
|
|
for(const mutate of [f=>f.node.outputs[1].name='resource_provider',f=>f.node.type='Other',f=>f.graph.links.get(11).origin_id=99,f=>f.graph._nodes[2].inputs[0].link=999,f=>f.graph.links.set(99,{origin_id:1,origin_slot:0})])test('invalid or unrelated graph is unchanged',()=>{
|
|
const f=fixture();mutate(f);const outputs=f.node.outputs,snapshot=JSON.stringify([outputs,f.links()]);
|
|
assert.equal(reconcileLoraRegistryOutputOrder(f.node),false);assert.equal(f.node.outputs,outputs);assert.equal(JSON.stringify([outputs,f.links()]),snapshot);
|
|
});
|
|
test('aliased and separate link stores migrate consistently',()=>{
|
|
for(const alias of [true,false]){const f=fixture();f.graph._links=alias?f.graph.links:Object.fromEntries(f.links().map(l=>[l.id,{...l}]));
|
|
assert.equal(reconcileLoraRegistryOutputOrder(f.node),true);assert.deepEqual(f.links().map(l=>l.origin_slot),[2,0,1]);
|
|
if(!alias)assert.deepEqual(Object.values(f.graph._links).map(l=>l.origin_slot),[2,0,1]);}
|
|
});
|
|
test('migration is wired to the actual afterConfigureGraph hook before consumer reconciliation',()=>{
|
|
const source=readFileSync(new URL('../ui/src/index.tsx',import.meta.url),'utf8');
|
|
assert.match(source,/afterConfigureGraph\(\)\s*\{\s*reconcileConfiguredNodeOutputs\(\(comfyApp as any\)\.graph\);\s*scheduleRegionalConsumersRefresh/);
|
|
});
|