Skip to content

Commit e9105f4

Browse files
christian-byrneampagent
authored andcommitted
test: reproduce #15577 and #15581 on the ECS branch
Seven it.fails assertions covering both configure-time link defects, plus controls that keep them honest. #15577: normalizeConfiguredTopology keys duplicates on target_id:target_slot and keeps data.links[first] rather than the link input.link names, so a workflow with two links into one input comes up wired to the wrong upstream node. conflictingOriginLinksRoot is byte-for-byte what main serializes after a pack creates a link through the legacy slot mirrors alone. #15581: a serialized input whose name has no live counterpart keeps its stale target_slot, the resulting occupied-target rejection breaks the loop, and every remaining move for that node is abandoned. Covers the dropped-input, renamed- input and directly-rejected-batch cases.
1 parent 5002fae commit e9105f4

3 files changed

Lines changed: 382 additions & 1 deletion

File tree

src/lib/litegraph/src/LGraph.inputSlotRealign.test.ts

Lines changed: 189 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { createTestingPinia } from '@pinia/testing'
22
import { setActivePinia } from 'pinia'
3-
import { beforeEach, describe, expect, it } from 'vitest'
3+
import { beforeEach, describe, expect, it, vi } from 'vitest'
44

55
import {
66
SUBGRAPH_INPUT_ID,
@@ -379,6 +379,194 @@ describe('LGraph.configure input slot realignment (#3348)', () => {
379379
})
380380
})
381381

382+
const SHRUNK_DEFINITION_ORDER = ['in_a', 'in_b']
383+
384+
/** Drops any serialized input its definition no longer declares. */
385+
class DroppedInputTargetNode extends LGraphNode {
386+
constructor(title?: string) {
387+
super(title ?? 'DroppedInputTarget')
388+
for (const name of SHRUNK_DEFINITION_ORDER) this.addInput(name, 'number')
389+
}
390+
391+
override configure(data: ISerialisedNode): void {
392+
data.inputs = (data.inputs ?? [])
393+
.filter((input) => SHRUNK_DEFINITION_ORDER.includes(input.name))
394+
.sort(
395+
(a, b) =>
396+
SHRUNK_DEFINITION_ORDER.indexOf(a.name) -
397+
SHRUNK_DEFINITION_ORDER.indexOf(b.name)
398+
)
399+
super.configure(data)
400+
}
401+
}
402+
403+
const RENAMED_DEFINITION_ORDER = ['in_a', 'in_b', 'in_c_v2']
404+
405+
/** Renames a live input after configure, as a pack migrating a slot does. */
406+
class RenamedInputTargetNode extends LGraphNode {
407+
constructor(title?: string) {
408+
super(title ?? 'RenamedInputTarget')
409+
for (const name of RENAMED_DEFINITION_ORDER) this.addInput(name, 'number')
410+
}
411+
412+
override configure(data: ISerialisedNode): void {
413+
super.configure(data)
414+
for (const input of this.inputs) {
415+
if (input.name === 'in_c') input.name = 'in_c_v2'
416+
}
417+
this.inputs.sort(
418+
(a, b) =>
419+
RENAMED_DEFINITION_ORDER.indexOf(a.name) -
420+
RENAMED_DEFINITION_ORDER.indexOf(b.name)
421+
)
422+
}
423+
}
424+
425+
/**
426+
* Serialized target carrying one input name the live node will not have.
427+
* `in_c` holds link 3 at slot 0, so the moves `1: 1 -> 0` and `2: 2 -> 1`
428+
* form a batch whose first member collides with link 3's stale placement.
429+
*/
430+
function unmatchedInputNameWorkflow(nodeType: string): SerialisableGraph {
431+
return {
432+
id: 'ab000000-0000-4000-8000-000000000004',
433+
version: 1,
434+
revision: 0,
435+
state: { lastNodeId: 2, lastLinkId: 3, lastGroupId: 0, lastRerouteId: 0 },
436+
nodes: [
437+
{
438+
id: 1,
439+
type: 'test/RealignSource',
440+
pos: [0, 0],
441+
size: [140, 60],
442+
flags: {},
443+
order: 0,
444+
mode: 0,
445+
inputs: [],
446+
outputs: [{ name: 'out', type: 'number', links: [1, 2, 3] }],
447+
properties: {}
448+
},
449+
{
450+
id: 2,
451+
type: nodeType,
452+
pos: [300, 0],
453+
size: [140, 80],
454+
flags: {},
455+
order: 1,
456+
mode: 0,
457+
inputs: [
458+
{ name: 'in_c', type: 'number', link: 3 },
459+
{ name: 'in_a', type: 'number', link: 1 },
460+
{ name: 'in_b', type: 'number', link: 2 }
461+
],
462+
outputs: [],
463+
properties: {}
464+
}
465+
],
466+
links: [
467+
{
468+
id: 3,
469+
origin_id: 1,
470+
origin_slot: 0,
471+
target_id: 2,
472+
target_slot: 0,
473+
type: 'number'
474+
},
475+
{
476+
id: 1,
477+
origin_id: 1,
478+
origin_slot: 0,
479+
target_id: 2,
480+
target_slot: 1,
481+
type: 'number'
482+
},
483+
{
484+
id: 2,
485+
origin_id: 1,
486+
origin_slot: 0,
487+
target_id: 2,
488+
target_slot: 2,
489+
type: 'number'
490+
}
491+
]
492+
}
493+
}
494+
495+
describe('LGraph.configure realignment with an unmatched input name (#15581)', () => {
496+
beforeEach(() => {
497+
setActivePinia(createTestingPinia({ stubActions: false }))
498+
LiteGraph.registerNodeType('test/RealignSource', SourceNode)
499+
LiteGraph.registerNodeType(
500+
'test/DroppedInputTarget',
501+
DroppedInputTargetNode
502+
)
503+
LiteGraph.registerNodeType(
504+
'test/RenamedInputTarget',
505+
RenamedInputTargetNode
506+
)
507+
})
508+
509+
it.fails('realigns siblings when configure drops an input', () => {
510+
const graph = new LGraph()
511+
graph.configure(unmatchedInputNameWorkflow('test/DroppedInputTarget'))
512+
513+
const target = graph.getNodeById(toNodeId(2))!
514+
expect(target.getInputLink(0)?.id).toBe(toLinkId(1))
515+
expect(target.getInputLink(1)?.id).toBe(toLinkId(2))
516+
})
517+
518+
it.fails('realigns siblings when configure renames an input', () => {
519+
const graph = new LGraph()
520+
graph.configure(unmatchedInputNameWorkflow('test/RenamedInputTarget'))
521+
522+
const target = graph.getNodeById(toNodeId(2))!
523+
expect(target.getInputLink(0)?.id).toBe(toLinkId(1))
524+
expect(target.getInputLink(1)?.id).toBe(toLinkId(2))
525+
})
526+
527+
it.fails('reports no error while realigning around an unmatched name', () => {
528+
const error = vi.spyOn(console, 'error').mockImplementation(() => {})
529+
530+
const graph = new LGraph()
531+
graph.configure(unmatchedInputNameWorkflow('test/DroppedInputTarget'))
532+
533+
expect(error).not.toHaveBeenCalled()
534+
})
535+
})
536+
537+
describe('realignInputLinkSlots with a rejected batch (#15581)', () => {
538+
beforeEach(() => {
539+
setActivePinia(createTestingPinia({ stubActions: false }))
540+
})
541+
542+
it.fails('lands the non-conflicting moves when one move is blocked', () => {
543+
const graph = new LGraph()
544+
const source = new LGraphNode('Source')
545+
source.addOutput('out', 'number')
546+
const target = new LGraphNode('Target')
547+
for (const name of ['p', 'q', 'r']) target.addInput(name, 'number')
548+
graph.add(source)
549+
graph.add(target)
550+
551+
const squatter = source.connect(0, target, 0)!
552+
const blocked = source.connect(0, target, 1)!
553+
const free = source.connect(0, target, 2)!
554+
555+
const nodeData = target.serialize()
556+
nodeData.inputs = [
557+
{ name: 'no_such_input', type: 'number', link: squatter.id },
558+
{ name: 'p', type: 'number', link: blocked.id },
559+
{ name: 'q', type: 'number', link: free.id }
560+
]
561+
562+
realignInputLinkSlots(graph, [nodeData])
563+
564+
expect(
565+
useLinkStore().getInputSlotLink(graphScopeOf(graph), target.id, 1)?.id
566+
).toBe(free.id)
567+
})
568+
})
569+
382570
describe('realignInputLinkSlots', () => {
383571
beforeEach(() => {
384572
setActivePinia(createTestingPinia({ stubActions: false }))

src/lib/litegraph/src/__fixtures__/duplicateLinks.ts

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,85 @@ export const duplicateLinksRoot: SerialisableGraph = {
7373
]
7474
}
7575

76+
/**
77+
* Root graph where two links target the same input from *different* origins.
78+
* SourceA (id 1) holds link 1, SourceB (id 2) holds link 2, and Target's
79+
* serialized `inputs[0]` references link **2** — so the first link in array
80+
* order and the link `input.link` names disagree.
81+
*
82+
* Not hypothetical: this is byte-for-byte what `main` serializes after a pack
83+
* creates a link through the legacy slot mirrors alone (`output.links.push`,
84+
* `input.link =`) instead of `node.connect()`, which is rgthree `link_fixer`'s
85+
* repair idiom. Reproduced against `main` at 32d0b6e202.
86+
*/
87+
export const conflictingOriginLinksRoot: SerialisableGraph = {
88+
id: 'dd000000-0000-4000-8000-000000000004',
89+
version: 1,
90+
revision: 0,
91+
state: {
92+
lastNodeId: 3,
93+
lastLinkId: 2,
94+
lastGroupId: 0,
95+
lastRerouteId: 0
96+
},
97+
nodes: [
98+
{
99+
id: 1,
100+
type: 'test/DupTestNode',
101+
pos: [0, 0],
102+
size: [200, 100],
103+
flags: {},
104+
order: 0,
105+
mode: 0,
106+
inputs: [{ name: 'input_0', type: 'number', link: null }],
107+
outputs: [{ name: 'output_0', type: 'number', links: [1] }],
108+
properties: {}
109+
},
110+
{
111+
id: 2,
112+
type: 'test/DupTestNode',
113+
pos: [0, 200],
114+
size: [200, 100],
115+
flags: {},
116+
order: 1,
117+
mode: 0,
118+
inputs: [{ name: 'input_0', type: 'number', link: null }],
119+
outputs: [{ name: 'output_0', type: 'number', links: [2] }],
120+
properties: {}
121+
},
122+
{
123+
id: 3,
124+
type: 'test/DupTestNode',
125+
pos: [300, 0],
126+
size: [200, 100],
127+
flags: {},
128+
order: 2,
129+
mode: 0,
130+
inputs: [{ name: 'input_0', type: 'number', link: 2 }],
131+
outputs: [{ name: 'output_0', type: 'number', links: [] }],
132+
properties: {}
133+
}
134+
],
135+
links: [
136+
{
137+
id: 1,
138+
origin_id: 1,
139+
origin_slot: 0,
140+
target_id: 3,
141+
target_slot: 0,
142+
type: 'number'
143+
},
144+
{
145+
id: 2,
146+
origin_id: 2,
147+
origin_slot: 0,
148+
target_id: 3,
149+
target_slot: 0,
150+
type: 'number'
151+
}
152+
]
153+
}
154+
76155
/**
77156
* Root graph with slot-shifted duplicates. Target node has an extra input
78157
* (simulating widget-to-input conversion) that shifts the connected input

0 commit comments

Comments
 (0)