Skip to content

Commit ce0005c

Browse files
test: pin the comfyui-promptchain slot-replacement break to the read path
Replays the two functions the one confirmed-broken pack actually runs (mobcat40/ComfyUI-PromptChain order-chain.js:123 and main.js:507) against the ECS branch. The break is a read divergence, not a rejected write: replaceNodeInputs never rejects on any promptchain sequence. Adds a forced-rejection arm that pins the slotLinks.ts:192 early return, so the claim that a rejected batch abandons the node layout is asserted rather than read off the source.
1 parent 9f6ab9a commit ce0005c

1 file changed

Lines changed: 121 additions & 1 deletion

File tree

src/lib/litegraph/src/node/legacySlotLinkMutations.test.ts

Lines changed: 121 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
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 type {
66
INodeInputSlot,
77
INodeOutputSlot
88
} from '@/lib/litegraph/src/interfaces'
99
import { LGraph, LGraphNode } from '@/lib/litegraph/src/litegraph'
10+
import { useLinkStore } from '@/stores/linkStore'
1011

1112
function connectedPair() {
1213
const graph = new LGraph()
@@ -159,3 +160,122 @@ describe('legacy slot link creation and plain-object slots (uncovered)', () => {
159160
expect(target.isInputConnected(0)).toBe(false)
160161
})
161162
})
163+
164+
function autogrowChain(inputCount: number, connectedSlots: number[]) {
165+
const graph = new LGraph()
166+
const source = new LGraphNode('Source')
167+
source.addOutput('out', 'STRING')
168+
graph.add(source)
169+
170+
const target = new LGraphNode('PromptChain')
171+
for (let i = 0; i < inputCount; i++) {
172+
target.addInput(`inputs.in_${i}`, 'STRING')
173+
}
174+
graph.add(target)
175+
for (const slot of connectedSlots) source.connect(0, target, slot)
176+
177+
return { graph, source, target }
178+
}
179+
180+
/** mobcat40/ComfyUI-PromptChain js/lib/order-chain.js:123 `updateInputLabels`. */
181+
function replaceSlotsWithLabelledCopies(node: LGraphNode) {
182+
for (const [index, slot] of node.inputs.entries()) {
183+
if (!slot.name.includes('in_')) continue
184+
slot.label = slot.link == null ? 'in' : 'PromptChain'
185+
node.inputs[index] = { ...slot }
186+
}
187+
}
188+
189+
/** mobcat40/ComfyUI-PromptChain js/main.js:507 `trimEmptyAutogrowSlots`. */
190+
function trimEmptyAutogrowSlots(node: LGraphNode) {
191+
const { inputs } = node
192+
const autogrow = inputs.filter((input) => input.name.startsWith('inputs.in_'))
193+
if (autogrow.length <= 1) return
194+
195+
const lastConnected = autogrow.findLastIndex((input) => input.link != null)
196+
const keepCount = Math.max(1, lastConnected + 2)
197+
if (keepCount >= autogrow.length) return
198+
199+
for (const input of autogrow.slice(keepCount)) {
200+
const index = inputs.indexOf(input)
201+
if (index !== -1) inputs.splice(index, 1)
202+
}
203+
}
204+
205+
describe('comfyui-promptchain indexed slot replacement', () => {
206+
beforeEach(() => {
207+
setActivePinia(createTestingPinia({ stubActions: false }))
208+
})
209+
210+
it('keeps the link store correct when the pack replaces every slot', () => {
211+
const { graph, target } = autogrowChain(4, [0, 1, 2])
212+
213+
replaceSlotsWithLabelledCopies(target)
214+
target.removeInput(0)
215+
216+
expect(target.inputs).toHaveLength(3)
217+
expect(graph.links.size).toBe(2)
218+
expect(target.isInputConnected(0)).toBe(true)
219+
expect(target.isInputConnected(1)).toBe(true)
220+
expect(target.isInputConnected(2)).toBe(false)
221+
})
222+
223+
it('never rejects the endpoint batch, though a rejection is detectable', () => {
224+
const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {})
225+
const rejected = () =>
226+
consoleError.mock.calls.filter(
227+
([message]) => message === 'Failed to replace node inputs'
228+
).length
229+
230+
const live = autogrowChain(4, [0, 1, 2])
231+
replaceSlotsWithLabelledCopies(live.target)
232+
trimEmptyAutogrowSlots(live.target)
233+
replaceSlotsWithLabelledCopies(live.target)
234+
live.target.removeInput(0)
235+
expect(rejected()).toBe(0)
236+
237+
const forced = autogrowChain(4, [0, 1, 2])
238+
const layoutBefore = forced.target.inputs.map((input) => input.name)
239+
vi.spyOn(useLinkStore(), 'updateEndpoints').mockReturnValue({
240+
ok: false,
241+
error: { code: 'occupied-target', message: 'forced' }
242+
})
243+
forced.target.removeInput(0)
244+
245+
expect(rejected()).toBe(1)
246+
expect(forced.target.inputs.map((input) => input.name)).toEqual(
247+
layoutBefore
248+
)
249+
250+
consoleError.mockRestore()
251+
})
252+
253+
it.fails('reads the live link id back through a spread copy', () => {
254+
const { target } = autogrowChain(2, [0])
255+
const linkId = target.getInputLink(0)!.id
256+
257+
const copy: INodeInputSlot = { ...target.inputs[0] }
258+
target.inputs[0] = copy
259+
260+
expect(target.inputs[0].link).toBe(linkId)
261+
})
262+
263+
it.fails('keeps connected inputs when the pack re-reads slot.link', () => {
264+
const { graph, target } = autogrowChain(4, [0, 1, 2])
265+
266+
replaceSlotsWithLabelledCopies(target)
267+
trimEmptyAutogrowSlots(target)
268+
269+
expect(target.inputs).toHaveLength(4)
270+
expect(graph.links.size).toBe(3)
271+
})
272+
273+
it('keeps every input when the same trim reads live slots', () => {
274+
const { graph, target } = autogrowChain(4, [0, 1, 2])
275+
276+
trimEmptyAutogrowSlots(target)
277+
278+
expect(target.inputs).toHaveLength(4)
279+
expect(graph.links.size).toBe(3)
280+
})
281+
})

0 commit comments

Comments
 (0)