mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-24 14:45:36 +00:00
Compare commits
2 Commits
fix/cmd-a-
...
drjkl/slop
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fc79c90a2e | ||
|
|
943d90aae8 |
491
src/core/graph/subgraph/migrateLegacyProxyWidgets.test.ts
Normal file
491
src/core/graph/subgraph/migrateLegacyProxyWidgets.test.ts
Normal file
@@ -0,0 +1,491 @@
|
||||
import { createTestingPinia } from '@pinia/testing'
|
||||
import { setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it } from 'vitest'
|
||||
|
||||
import { migrateLegacyProxyWidgets } from '@/core/graph/subgraph/migrateLegacyProxyWidgets'
|
||||
import type {
|
||||
LGraph,
|
||||
Subgraph,
|
||||
SubgraphNode
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
import { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import {
|
||||
createTestRootGraph,
|
||||
createTestSubgraph,
|
||||
createTestSubgraphNode,
|
||||
resetSubgraphFixtureState
|
||||
} from '@/lib/litegraph/src/subgraph/__fixtures__/subgraphHelpers'
|
||||
|
||||
interface Setup {
|
||||
rootGraph: LGraph
|
||||
subgraph: Subgraph
|
||||
host: SubgraphNode
|
||||
innerNode: LGraphNode
|
||||
innerWidgetName: string
|
||||
}
|
||||
|
||||
function setup(widgetName = 'seed', slotType = 'INT'): Setup {
|
||||
const rootGraph = createTestRootGraph()
|
||||
const subgraph = createTestSubgraph({ rootGraph })
|
||||
|
||||
const innerNode = new LGraphNode('Inner')
|
||||
const input = innerNode.addInput(widgetName, slotType)
|
||||
innerNode.addWidget('number', widgetName, 0, () => {})
|
||||
input.widget = { name: widgetName }
|
||||
subgraph.add(innerNode)
|
||||
|
||||
const host = createTestSubgraphNode(subgraph, { parentGraph: rootGraph })
|
||||
|
||||
return { rootGraph, subgraph, host, innerNode, innerWidgetName: widgetName }
|
||||
}
|
||||
|
||||
describe('migrateLegacyProxyWidgets', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createTestingPinia({ stubActions: false }))
|
||||
resetSubgraphFixtureState()
|
||||
})
|
||||
|
||||
it('migrates a two-tuple resolvable entry into a SubgraphInput link', () => {
|
||||
const { subgraph, host, innerNode, innerWidgetName } = setup()
|
||||
host.properties.proxyWidgets = [[String(innerNode.id), innerWidgetName]]
|
||||
|
||||
const inputsBefore = subgraph.inputs.length
|
||||
|
||||
migrateLegacyProxyWidgets(host)
|
||||
|
||||
expect(subgraph.inputs.length).toBe(inputsBefore + 1)
|
||||
const newInput = subgraph.inputs.at(-1)!
|
||||
expect(newInput.linkIds.length).toBeGreaterThan(0)
|
||||
expect(innerNode.inputs[0].link).not.toBeNull()
|
||||
expect(innerNode.inputs[0].link).toBeDefined()
|
||||
expect(host.properties.proxyWidgets).toBeUndefined()
|
||||
})
|
||||
|
||||
it('ignores the third tuple element (disambiguator) and migrates identically', () => {
|
||||
const { subgraph, host, innerNode, innerWidgetName } = setup()
|
||||
host.properties.proxyWidgets = [
|
||||
[String(innerNode.id), innerWidgetName, 'disambiguator-ignored']
|
||||
]
|
||||
|
||||
migrateLegacyProxyWidgets(host)
|
||||
|
||||
expect(subgraph.inputs.length).toBe(1)
|
||||
expect(subgraph.inputs[0].linkIds.length).toBeGreaterThan(0)
|
||||
expect(innerNode.inputs[0].link).not.toBeNull()
|
||||
expect(host.properties.proxyWidgets).toBeUndefined()
|
||||
})
|
||||
|
||||
it('drops entries whose source node does not exist', () => {
|
||||
const { subgraph, host } = setup()
|
||||
host.properties.proxyWidgets = [['99999', 'whatever']]
|
||||
|
||||
migrateLegacyProxyWidgets(host)
|
||||
|
||||
expect(subgraph.inputs.length).toBe(0)
|
||||
expect(host.properties.proxyWidgets).toBeUndefined()
|
||||
})
|
||||
|
||||
it('drops entries whose source widget name is not found on the node', () => {
|
||||
const { subgraph, host, innerNode } = setup()
|
||||
host.properties.proxyWidgets = [[String(innerNode.id), 'nonexistent']]
|
||||
|
||||
migrateLegacyProxyWidgets(host)
|
||||
|
||||
expect(subgraph.inputs.length).toBe(0)
|
||||
expect(host.properties.proxyWidgets).toBeUndefined()
|
||||
})
|
||||
|
||||
it('drops entries for widgets without a backing input slot (preview-style pseudo-widget)', () => {
|
||||
const rootGraph = createTestRootGraph()
|
||||
const subgraph = createTestSubgraph({ rootGraph })
|
||||
|
||||
const innerNode = new LGraphNode('Preview')
|
||||
innerNode.addWidget('text', 'preview', '', () => {})
|
||||
subgraph.add(innerNode)
|
||||
|
||||
const host = createTestSubgraphNode(subgraph, { parentGraph: rootGraph })
|
||||
host.properties.proxyWidgets = [[String(innerNode.id), 'preview']]
|
||||
|
||||
migrateLegacyProxyWidgets(host)
|
||||
|
||||
expect(subgraph.inputs.length).toBe(0)
|
||||
expect(host.properties.proxyWidgets).toBeUndefined()
|
||||
})
|
||||
|
||||
it('migrates only the resolvable entries when mixed with unresolvable ones', () => {
|
||||
const { subgraph, host, innerNode, innerWidgetName } = setup()
|
||||
host.properties.proxyWidgets = [
|
||||
[String(innerNode.id), innerWidgetName],
|
||||
['99999', 'missing-node'],
|
||||
[String(innerNode.id), 'missing-widget']
|
||||
]
|
||||
|
||||
migrateLegacyProxyWidgets(host)
|
||||
|
||||
expect(subgraph.inputs.length).toBe(1)
|
||||
expect(subgraph.inputs[0].linkIds.length).toBeGreaterThan(0)
|
||||
expect(host.properties.proxyWidgets).toBeUndefined()
|
||||
})
|
||||
|
||||
it('is idempotent when invoked multiple times', () => {
|
||||
const { subgraph, host, innerNode, innerWidgetName } = setup()
|
||||
host.properties.proxyWidgets = [[String(innerNode.id), innerWidgetName]]
|
||||
|
||||
migrateLegacyProxyWidgets(host)
|
||||
const inputsAfterFirst = subgraph.inputs.length
|
||||
|
||||
expect(() => migrateLegacyProxyWidgets(host)).not.toThrow()
|
||||
|
||||
expect(subgraph.inputs.length).toBe(inputsAfterFirst)
|
||||
expect(host.properties.proxyWidgets).toBeUndefined()
|
||||
})
|
||||
|
||||
it("drops legacy '-1' source-node-id entries", () => {
|
||||
const { subgraph, host } = setup()
|
||||
host.properties.proxyWidgets = [['-1', 'someName']]
|
||||
|
||||
migrateLegacyProxyWidgets(host)
|
||||
|
||||
expect(subgraph.inputs.length).toBe(0)
|
||||
expect(host.properties.proxyWidgets).toBeUndefined()
|
||||
})
|
||||
|
||||
it('handles an empty proxyWidgets array as a no-op and deletes the property', () => {
|
||||
const { subgraph, host } = setup()
|
||||
host.properties.proxyWidgets = []
|
||||
|
||||
expect(() => migrateLegacyProxyWidgets(host)).not.toThrow()
|
||||
|
||||
expect(subgraph.inputs.length).toBe(0)
|
||||
expect(host.properties.proxyWidgets).toBeUndefined()
|
||||
})
|
||||
|
||||
it('handles invalid (non-JSON string) proxyWidgets without throwing', () => {
|
||||
const { subgraph, host } = setup()
|
||||
host.properties.proxyWidgets = 'not-json'
|
||||
|
||||
expect(() => migrateLegacyProxyWidgets(host)).not.toThrow()
|
||||
|
||||
expect(subgraph.inputs.length).toBe(0)
|
||||
expect(host.properties.proxyWidgets).toBeUndefined()
|
||||
})
|
||||
|
||||
it('handles undefined proxyWidgets as a no-op', () => {
|
||||
const { subgraph, host } = setup()
|
||||
host.properties.proxyWidgets = undefined
|
||||
|
||||
expect(() => migrateLegacyProxyWidgets(host)).not.toThrow()
|
||||
|
||||
expect(subgraph.inputs.length).toBe(0)
|
||||
})
|
||||
|
||||
describe('nested promotions', () => {
|
||||
interface NestedSetup {
|
||||
rootGraph: LGraph
|
||||
hostSubgraph: Subgraph
|
||||
innerSubgraph: Subgraph
|
||||
innerLeaf: LGraphNode
|
||||
nestedNode: SubgraphNode
|
||||
host: SubgraphNode
|
||||
promotedWidgetName: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds: rootGraph -> host(SubgraphNode for hostSubgraph)
|
||||
* └── hostSubgraph contains nestedNode(SubgraphNode for innerSubgraph)
|
||||
* └── innerSubgraph contains innerLeaf with `seed` widget
|
||||
* connected through innerSubgraph's input.
|
||||
* The nestedNode surfaces a promoted widget named after the inner subgraph
|
||||
* input (`seed` here).
|
||||
*/
|
||||
function nestedSetup(promotedName = 'seed'): NestedSetup {
|
||||
const rootGraph = createTestRootGraph()
|
||||
|
||||
const innerSubgraph = createTestSubgraph({
|
||||
rootGraph,
|
||||
inputs: [{ name: promotedName, type: 'INT' }]
|
||||
})
|
||||
|
||||
const innerLeaf = new LGraphNode('Leaf')
|
||||
const leafInput = innerLeaf.addInput(promotedName, 'INT')
|
||||
innerLeaf.addWidget('number', promotedName, 0, () => {})
|
||||
leafInput.widget = { name: promotedName }
|
||||
innerSubgraph.add(innerLeaf)
|
||||
innerSubgraph.inputNode.slots[0].connect(innerLeaf.inputs[0], innerLeaf)
|
||||
|
||||
const hostSubgraph = createTestSubgraph({ rootGraph })
|
||||
|
||||
const nestedNode = createTestSubgraphNode(innerSubgraph, {
|
||||
parentGraph: hostSubgraph
|
||||
})
|
||||
hostSubgraph.add(nestedNode)
|
||||
|
||||
const host = createTestSubgraphNode(hostSubgraph, {
|
||||
parentGraph: rootGraph
|
||||
})
|
||||
|
||||
return {
|
||||
rootGraph,
|
||||
hostSubgraph,
|
||||
innerSubgraph,
|
||||
innerLeaf,
|
||||
nestedNode,
|
||||
host,
|
||||
promotedWidgetName: promotedName
|
||||
}
|
||||
}
|
||||
|
||||
it('migrates an entry whose source widget is on a nested SubgraphNode', () => {
|
||||
const {
|
||||
host,
|
||||
hostSubgraph,
|
||||
innerSubgraph,
|
||||
nestedNode,
|
||||
promotedWidgetName
|
||||
} = nestedSetup()
|
||||
|
||||
const innerInputsBefore = innerSubgraph.inputs.length
|
||||
host.properties.proxyWidgets = [
|
||||
[String(nestedNode.id), promotedWidgetName]
|
||||
]
|
||||
|
||||
migrateLegacyProxyWidgets(host)
|
||||
|
||||
expect(hostSubgraph.inputs.length).toBe(1)
|
||||
const newInput = hostSubgraph.inputs[0]
|
||||
expect(newInput.name).toBe(promotedWidgetName)
|
||||
expect(newInput.linkIds.length).toBeGreaterThan(0)
|
||||
|
||||
const nestedSlot = nestedNode.inputs.find(
|
||||
(slot) => slot._subgraphSlot?.name === promotedWidgetName
|
||||
)
|
||||
expect(nestedSlot).toBeDefined()
|
||||
expect(nestedSlot?.link).not.toBeNull()
|
||||
expect(nestedSlot?.link).toBeDefined()
|
||||
|
||||
// The nested subgraph's own promotion structure is unaffected.
|
||||
expect(innerSubgraph.inputs.length).toBe(innerInputsBefore)
|
||||
|
||||
expect(host.properties.proxyWidgets).toBeUndefined()
|
||||
})
|
||||
|
||||
it('skips entries whose source slot already has a non-null link', () => {
|
||||
const { subgraph, host, innerNode, innerWidgetName } = setup()
|
||||
|
||||
// Pre-wire the inner node's slot through a SubgraphInput so its
|
||||
// `link` is non-null before migration runs.
|
||||
const preExistingInput = subgraph.addInput(innerWidgetName, 'INT')
|
||||
const preLink = preExistingInput.connect(innerNode.inputs[0], innerNode)
|
||||
expect(preLink).not.toBeNull()
|
||||
const inputsBefore = subgraph.inputs.length
|
||||
const preExistingLinkId = innerNode.inputs[0].link
|
||||
expect(preExistingLinkId).not.toBeNull()
|
||||
|
||||
host.properties.proxyWidgets = [[String(innerNode.id), innerWidgetName]]
|
||||
|
||||
migrateLegacyProxyWidgets(host)
|
||||
|
||||
// No new SubgraphInput; pre-existing link untouched.
|
||||
expect(subgraph.inputs.length).toBe(inputsBefore)
|
||||
expect(innerNode.inputs[0].link).toBe(preExistingLinkId)
|
||||
expect(host.properties.proxyWidgets).toBeUndefined()
|
||||
})
|
||||
|
||||
it('migrates multiple entries pointing at widgets on the same nested SubgraphNode', () => {
|
||||
const rootGraph = createTestRootGraph()
|
||||
|
||||
const innerSubgraph = createTestSubgraph({
|
||||
rootGraph,
|
||||
inputs: [
|
||||
{ name: 'seed', type: 'INT' },
|
||||
{ name: 'steps', type: 'INT' }
|
||||
]
|
||||
})
|
||||
|
||||
const innerLeaf = new LGraphNode('Leaf')
|
||||
const seedInput = innerLeaf.addInput('seed', 'INT')
|
||||
const stepsInput = innerLeaf.addInput('steps', 'INT')
|
||||
innerLeaf.addWidget('number', 'seed', 0, () => {})
|
||||
innerLeaf.addWidget('number', 'steps', 20, () => {})
|
||||
seedInput.widget = { name: 'seed' }
|
||||
stepsInput.widget = { name: 'steps' }
|
||||
innerSubgraph.add(innerLeaf)
|
||||
innerSubgraph.inputNode.slots[0].connect(innerLeaf.inputs[0], innerLeaf)
|
||||
innerSubgraph.inputNode.slots[1].connect(innerLeaf.inputs[1], innerLeaf)
|
||||
|
||||
const hostSubgraph = createTestSubgraph({ rootGraph })
|
||||
const nestedNode = createTestSubgraphNode(innerSubgraph, {
|
||||
parentGraph: hostSubgraph
|
||||
})
|
||||
hostSubgraph.add(nestedNode)
|
||||
|
||||
const host = createTestSubgraphNode(hostSubgraph, {
|
||||
parentGraph: rootGraph
|
||||
})
|
||||
host.properties.proxyWidgets = [
|
||||
[String(nestedNode.id), 'seed'],
|
||||
[String(nestedNode.id), 'steps']
|
||||
]
|
||||
|
||||
migrateLegacyProxyWidgets(host)
|
||||
|
||||
expect(hostSubgraph.inputs.length).toBe(2)
|
||||
const names = hostSubgraph.inputs.map((i) => i.name)
|
||||
expect(new Set(names).size).toBe(names.length)
|
||||
expect(names).toEqual(expect.arrayContaining(['seed', 'steps']))
|
||||
for (const input of hostSubgraph.inputs) {
|
||||
expect(input.linkIds.length).toBeGreaterThan(0)
|
||||
}
|
||||
|
||||
const seedSlot = nestedNode.inputs.find(
|
||||
(slot) => slot._subgraphSlot?.name === 'seed'
|
||||
)
|
||||
const stepsSlot = nestedNode.inputs.find(
|
||||
(slot) => slot._subgraphSlot?.name === 'steps'
|
||||
)
|
||||
expect(seedSlot?.link).not.toBeNull()
|
||||
expect(stepsSlot?.link).not.toBeNull()
|
||||
expect(host.properties.proxyWidgets).toBeUndefined()
|
||||
})
|
||||
|
||||
it("does not touch the inner SubgraphNode's own legacy proxyWidgets", () => {
|
||||
const { host, hostSubgraph, nestedNode, promotedWidgetName } =
|
||||
nestedSetup()
|
||||
|
||||
const innerLegacy: [string, string][] = [['12345', 'untouched']]
|
||||
nestedNode.properties.proxyWidgets = innerLegacy.map(
|
||||
(entry) => [...entry] as [string, string]
|
||||
)
|
||||
|
||||
host.properties.proxyWidgets = [
|
||||
[String(nestedNode.id), promotedWidgetName]
|
||||
]
|
||||
|
||||
migrateLegacyProxyWidgets(host)
|
||||
|
||||
expect(host.properties.proxyWidgets).toBeUndefined()
|
||||
expect(hostSubgraph.inputs.length).toBe(1)
|
||||
expect(nestedNode.properties.proxyWidgets).toStrictEqual(innerLegacy)
|
||||
})
|
||||
|
||||
it('drops two-level nested entries whose source widget is itself a chained PromotedWidgetView', () => {
|
||||
// Pins the current contract: when the source widget on the immediate
|
||||
// child SubgraphNode is itself a chained promotion (PromotedWidgetView),
|
||||
// SubgraphNode.getSlotFromWidget returns undefined due to a view-cache
|
||||
// identity mismatch and the migration drops the entry silently. This is
|
||||
// consistent with case 5 (no backing input slot → drop) and with the
|
||||
// plan's "unresolvable entries are dropped silently" contract. Lifting
|
||||
// this restriction requires fixing the disambiguator-aware viewKey
|
||||
// identity in SubgraphNode and is tracked as a follow-up.
|
||||
const rootGraph = createTestRootGraph()
|
||||
|
||||
// Innermost (B): contains the leaf widget.
|
||||
const subgraphB = createTestSubgraph({
|
||||
rootGraph,
|
||||
inputs: [{ name: 'seed', type: 'INT' }]
|
||||
})
|
||||
const leafB = new LGraphNode('LeafB')
|
||||
const leafInputB = leafB.addInput('seed', 'INT')
|
||||
leafB.addWidget('number', 'seed', 7, () => {})
|
||||
leafInputB.widget = { name: 'seed' }
|
||||
subgraphB.add(leafB)
|
||||
subgraphB.inputNode.slots[0].connect(leafB.inputs[0], leafB)
|
||||
|
||||
// Middle (A): contains a SubgraphNode for B; surfaces seed via its own input.
|
||||
const subgraphA = createTestSubgraph({
|
||||
rootGraph,
|
||||
inputs: [{ name: 'seed', type: 'INT' }]
|
||||
})
|
||||
const nodeForB = createTestSubgraphNode(subgraphB, {
|
||||
parentGraph: subgraphA
|
||||
})
|
||||
subgraphA.add(nodeForB)
|
||||
const nodeForBSeedSlot = nodeForB.inputs.find(
|
||||
(slot) => slot._subgraphSlot?.name === 'seed'
|
||||
)
|
||||
expect(nodeForBSeedSlot).toBeDefined()
|
||||
subgraphA.inputNode.slots[0].connect(nodeForBSeedSlot!, nodeForB)
|
||||
|
||||
// Host: contains a SubgraphNode for A.
|
||||
const hostSubgraph = createTestSubgraph({ rootGraph })
|
||||
const nodeForA = createTestSubgraphNode(subgraphA, {
|
||||
parentGraph: hostSubgraph
|
||||
})
|
||||
hostSubgraph.add(nodeForA)
|
||||
|
||||
const host = createTestSubgraphNode(hostSubgraph, {
|
||||
parentGraph: rootGraph
|
||||
})
|
||||
host.properties.proxyWidgets = [[String(nodeForA.id), 'seed']]
|
||||
|
||||
migrateLegacyProxyWidgets(host)
|
||||
|
||||
expect(hostSubgraph.inputs.length).toBe(0)
|
||||
expect(host.properties.proxyWidgets).toBeUndefined()
|
||||
})
|
||||
|
||||
it.todo(
|
||||
'should resolve two-level nested chained promotions once the disambiguator-aware viewKey identity in SubgraphNode is fixed'
|
||||
)
|
||||
|
||||
it('uses nextUniqueName when the desired name collides with an existing input', () => {
|
||||
const { subgraph, host, innerNode } = setup('width', 'INT')
|
||||
// Pre-existing input with the same name (and type irrelevant for the test).
|
||||
const existingInput = subgraph.addInput('width', 'INT')
|
||||
const otherNode = new LGraphNode('Other')
|
||||
otherNode.addOutput('width', 'INT')
|
||||
subgraph.add(otherNode)
|
||||
const preExistingLink = existingInput.connect(
|
||||
otherNode.inputs[0] ?? otherNode.outputs[0],
|
||||
otherNode
|
||||
)
|
||||
// The pre-existing input may or may not connect depending on slot
|
||||
// direction; we only care that it remains in the inputs list with its
|
||||
// identity preserved.
|
||||
void preExistingLink
|
||||
|
||||
const inputsBefore = subgraph.inputs.length
|
||||
host.properties.proxyWidgets = [[String(innerNode.id), 'width']]
|
||||
|
||||
migrateLegacyProxyWidgets(host)
|
||||
|
||||
expect(subgraph.inputs.length).toBe(inputsBefore + 1)
|
||||
expect(subgraph.inputs[0]).toBe(existingInput)
|
||||
expect(subgraph.inputs[0].name).toBe('width')
|
||||
|
||||
const newInput = subgraph.inputs.at(-1)!
|
||||
expect(newInput).not.toBe(existingInput)
|
||||
expect(newInput.name).not.toBe('width')
|
||||
expect(newInput.name.startsWith('width')).toBe(true)
|
||||
expect(newInput.linkIds.length).toBeGreaterThan(0)
|
||||
expect(host.properties.proxyWidgets).toBeUndefined()
|
||||
})
|
||||
|
||||
it('drops entries when the nested SubgraphNode has not materialized the widget', () => {
|
||||
const rootGraph = createTestRootGraph()
|
||||
|
||||
// Inner subgraph with no inputs and no leaf widgets — a SubgraphNode
|
||||
// built from this surfaces zero promoted widgets.
|
||||
const innerSubgraph = createTestSubgraph({ rootGraph })
|
||||
|
||||
const hostSubgraph = createTestSubgraph({ rootGraph })
|
||||
const nestedNode = createTestSubgraphNode(innerSubgraph, {
|
||||
parentGraph: hostSubgraph
|
||||
})
|
||||
hostSubgraph.add(nestedNode)
|
||||
|
||||
// Sanity: the nested SubgraphNode has no synthetic widgets to promote.
|
||||
expect(nestedNode.widgets.length).toBe(0)
|
||||
|
||||
const host = createTestSubgraphNode(hostSubgraph, {
|
||||
parentGraph: rootGraph
|
||||
})
|
||||
host.properties.proxyWidgets = [[String(nestedNode.id), 'never_promoted']]
|
||||
|
||||
expect(() => migrateLegacyProxyWidgets(host)).not.toThrow()
|
||||
|
||||
expect(hostSubgraph.inputs.length).toBe(0)
|
||||
expect(host.properties.proxyWidgets).toBeUndefined()
|
||||
})
|
||||
})
|
||||
})
|
||||
44
src/core/graph/subgraph/migrateLegacyProxyWidgets.ts
Normal file
44
src/core/graph/subgraph/migrateLegacyProxyWidgets.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { parseProxyWidgets } from '@/core/schemas/promotionSchema'
|
||||
import { nextUniqueName } from '@/lib/litegraph/src/strings'
|
||||
import type { SubgraphNode } from '@/lib/litegraph/src/subgraph/SubgraphNode'
|
||||
|
||||
/**
|
||||
* Forward-ratchet legacy `properties.proxyWidgets` into real
|
||||
* SubgraphInput links. Unresolvable entries are dropped silently.
|
||||
* Idempotent: removes the legacy property after running.
|
||||
*/
|
||||
export function migrateLegacyProxyWidgets(hostNode: SubgraphNode): void {
|
||||
const tuples = parseProxyWidgets(hostNode.properties.proxyWidgets)
|
||||
if (tuples.length === 0) {
|
||||
delete hostNode.properties.proxyWidgets
|
||||
return
|
||||
}
|
||||
|
||||
const { subgraph } = hostNode
|
||||
|
||||
for (const [sourceNodeId, sourceWidgetName] of tuples) {
|
||||
const sourceNode = subgraph.getNodeById(sourceNodeId)
|
||||
if (!sourceNode) continue
|
||||
|
||||
const sourceWidget = sourceNode.widgets?.find(
|
||||
(w) => w.name === sourceWidgetName
|
||||
)
|
||||
if (!sourceWidget) continue
|
||||
|
||||
const sourceSlot = sourceNode.getSlotFromWidget(sourceWidget)
|
||||
if (!sourceSlot) continue
|
||||
|
||||
if (sourceSlot.link != null) continue
|
||||
|
||||
const desiredName = nextUniqueName(
|
||||
sourceWidgetName,
|
||||
subgraph.inputs.map((i) => i.name)
|
||||
)
|
||||
const slotType = String(sourceSlot.type ?? sourceWidget.type ?? '*')
|
||||
const subgraphInput = subgraph.addInput(desiredName, slotType)
|
||||
const link = subgraphInput.connect(sourceSlot, sourceNode)
|
||||
if (!link) subgraph.removeInput(subgraphInput)
|
||||
}
|
||||
|
||||
delete hostNode.properties.proxyWidgets
|
||||
}
|
||||
@@ -1128,67 +1128,6 @@ describe('SubgraphNode.widgets getter', () => {
|
||||
])
|
||||
})
|
||||
|
||||
test('configure prunes stale disconnected host aliases that resolve to the active linked concrete widget', () => {
|
||||
const nestedSubgraph = createTestSubgraph({
|
||||
inputs: [{ name: 'string_a', type: '*' }]
|
||||
})
|
||||
|
||||
const concreteNode = new LGraphNode('ConcreteNode')
|
||||
const concreteInput = concreteNode.addInput('string_a', '*')
|
||||
concreteNode.addWidget('text', 'string_a', 'value', () => {})
|
||||
concreteInput.widget = { name: 'string_a' }
|
||||
nestedSubgraph.add(concreteNode)
|
||||
nestedSubgraph.inputNode.slots[0].connect(concreteInput, concreteNode)
|
||||
|
||||
const hostSubgraph = createTestSubgraph({
|
||||
inputs: [{ name: 'string_a', type: '*' }]
|
||||
})
|
||||
|
||||
const activeAliasNode = createTestSubgraphNode(nestedSubgraph, { id: 118 })
|
||||
const staleAliasNode = createTestSubgraphNode(nestedSubgraph, { id: 119 })
|
||||
hostSubgraph.add(activeAliasNode)
|
||||
hostSubgraph.add(staleAliasNode)
|
||||
|
||||
activeAliasNode._internalConfigureAfterSlots()
|
||||
staleAliasNode._internalConfigureAfterSlots()
|
||||
hostSubgraph.inputNode.slots[0].connect(
|
||||
activeAliasNode.inputs[0],
|
||||
activeAliasNode
|
||||
)
|
||||
|
||||
const hostSubgraphNode = createTestSubgraphNode(hostSubgraph, { id: 120 })
|
||||
hostSubgraphNode.graph?.add(hostSubgraphNode)
|
||||
|
||||
setPromotions(hostSubgraphNode, [
|
||||
[String(activeAliasNode.id), 'string_a'],
|
||||
[String(staleAliasNode.id), 'string_a']
|
||||
])
|
||||
|
||||
const serialized = hostSubgraphNode.serialize()
|
||||
const restoredNode = createTestSubgraphNode(hostSubgraph, { id: 121 })
|
||||
restoredNode.configure({
|
||||
...serialized,
|
||||
id: restoredNode.id,
|
||||
type: hostSubgraph.id,
|
||||
inputs: []
|
||||
})
|
||||
|
||||
const restoredPromotions = usePromotionStore().getPromotions(
|
||||
restoredNode.rootGraph.id,
|
||||
restoredNode.id
|
||||
)
|
||||
expect(restoredPromotions).toStrictEqual([
|
||||
{
|
||||
sourceNodeId: String(activeAliasNode.id),
|
||||
sourceWidgetName: 'string_a'
|
||||
}
|
||||
])
|
||||
|
||||
const restoredWidgets = promotedWidgets(restoredNode)
|
||||
expect(restoredWidgets).toHaveLength(1)
|
||||
expect(restoredWidgets[0].sourceNodeId).toBe(String(activeAliasNode.id))
|
||||
})
|
||||
|
||||
test('serialize syncs duplicate-name linked inputs by subgraph slot identity', () => {
|
||||
const subgraph = createTestSubgraph({
|
||||
inputs: [
|
||||
@@ -1337,193 +1276,6 @@ describe('SubgraphNode.widgets getter', () => {
|
||||
expect(subgraphNode.widgets).toHaveLength(1)
|
||||
})
|
||||
|
||||
test('migrates legacy -1 entries via _resolveLegacyEntry', () => {
|
||||
const [subgraphNode, innerNodes] = setupSubgraph(1)
|
||||
innerNodes[0].addWidget('text', 'stringWidget', 'value', () => {})
|
||||
|
||||
// Simulate a slot-connected widget so legacy resolution works
|
||||
const subgraph = subgraphNode.subgraph
|
||||
subgraph.addInput('stringWidget', '*')
|
||||
subgraphNode._internalConfigureAfterSlots()
|
||||
|
||||
// The _internalConfigureAfterSlots would have set up the slot-connected
|
||||
// widget via _setWidget if there's a link. For unit testing legacy
|
||||
// migration, we need to set up the input._widget manually.
|
||||
const input = subgraphNode.inputs.find((i) => i.name === 'stringWidget')
|
||||
if (input) {
|
||||
input._widget = createPromotedWidgetView(
|
||||
subgraphNode,
|
||||
String(innerNodes[0].id),
|
||||
'stringWidget'
|
||||
)
|
||||
}
|
||||
|
||||
// Set legacy -1 format via properties and re-run hydration
|
||||
subgraphNode.properties.proxyWidgets = [['-1', 'stringWidget']]
|
||||
subgraphNode._internalConfigureAfterSlots()
|
||||
|
||||
// Migration should have rewritten the store with resolved IDs
|
||||
const entries = usePromotionStore().getPromotions(
|
||||
subgraphNode.rootGraph.id,
|
||||
subgraphNode.id
|
||||
)
|
||||
expect(entries).toStrictEqual([
|
||||
{
|
||||
sourceNodeId: String(innerNodes[0].id),
|
||||
sourceWidgetName: 'stringWidget'
|
||||
}
|
||||
])
|
||||
})
|
||||
|
||||
test('hydrate promotions from serialize/configure round-trip', () => {
|
||||
const [subgraphNode, innerNodes] = setupSubgraph(1)
|
||||
const innerNode = firstInnerNode(innerNodes)
|
||||
innerNode.addWidget('text', 'widgetA', 'a', () => {})
|
||||
|
||||
setPromotions(subgraphNode, [[String(innerNode.id), 'widgetA']])
|
||||
const serialized = subgraphNode.serialize()
|
||||
|
||||
const restoredNode = createTestSubgraphNode(subgraphNode.subgraph, {
|
||||
id: 99
|
||||
})
|
||||
restoredNode.configure({
|
||||
...serialized,
|
||||
id: restoredNode.id,
|
||||
type: subgraphNode.subgraph.id
|
||||
})
|
||||
|
||||
const restoredEntries = usePromotionStore().getPromotions(
|
||||
restoredNode.rootGraph.id,
|
||||
restoredNode.id
|
||||
)
|
||||
expect(restoredEntries).toStrictEqual([
|
||||
{
|
||||
sourceNodeId: String(innerNode.id),
|
||||
sourceWidgetName: 'widgetA'
|
||||
}
|
||||
])
|
||||
})
|
||||
|
||||
test('configure with empty serialized inputs keeps linked filtering active', () => {
|
||||
const subgraph = createTestSubgraph({
|
||||
inputs: [{ name: 'string_a', type: '*' }]
|
||||
})
|
||||
const subgraphNode = createTestSubgraphNode(subgraph, { id: 97 })
|
||||
subgraphNode.graph?.add(subgraphNode)
|
||||
|
||||
const linkedNodeA = new LGraphNode('LinkedNodeA')
|
||||
const linkedInputA = linkedNodeA.addInput('string_a', '*')
|
||||
linkedNodeA.addWidget('text', 'string_a', 'a', () => {})
|
||||
linkedInputA.widget = { name: 'string_a' }
|
||||
subgraph.add(linkedNodeA)
|
||||
|
||||
const linkedNodeB = new LGraphNode('LinkedNodeB')
|
||||
const linkedInputB = linkedNodeB.addInput('string_a', '*')
|
||||
linkedNodeB.addWidget('text', 'string_a', 'b', () => {})
|
||||
linkedInputB.widget = { name: 'string_a' }
|
||||
subgraph.add(linkedNodeB)
|
||||
|
||||
const storeOnlyNode = new LGraphNode('StoreOnlyNode')
|
||||
storeOnlyNode.addWidget('text', 'string_a', 'independent', () => {})
|
||||
subgraph.add(storeOnlyNode)
|
||||
|
||||
subgraph.inputNode.slots[0].connect(linkedInputA, linkedNodeA)
|
||||
subgraph.inputNode.slots[0].connect(linkedInputB, linkedNodeB)
|
||||
|
||||
setPromotions(subgraphNode, [
|
||||
[String(linkedNodeA.id), 'string_a'],
|
||||
[String(linkedNodeB.id), 'string_a'],
|
||||
[String(storeOnlyNode.id), 'string_a']
|
||||
])
|
||||
|
||||
const serialized = subgraphNode.serialize()
|
||||
const restoredNode = createTestSubgraphNode(subgraph, { id: 98 })
|
||||
restoredNode.configure({
|
||||
...serialized,
|
||||
id: restoredNode.id,
|
||||
type: subgraph.id,
|
||||
inputs: []
|
||||
})
|
||||
|
||||
const restoredWidgets = promotedWidgets(restoredNode)
|
||||
expect(restoredWidgets).toHaveLength(2)
|
||||
|
||||
const linkedViewCount = restoredWidgets.filter((widget) =>
|
||||
[String(linkedNodeA.id), String(linkedNodeB.id)].includes(
|
||||
widget.sourceNodeId
|
||||
)
|
||||
).length
|
||||
expect(linkedViewCount).toBe(1)
|
||||
expect(
|
||||
restoredWidgets.some(
|
||||
(widget) => widget.sourceNodeId === String(storeOnlyNode.id)
|
||||
)
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
test('configure with serialized inputs rebinds subgraph slots for linked filtering', () => {
|
||||
const subgraph = createTestSubgraph({
|
||||
inputs: [{ name: 'string_a', type: '*' }]
|
||||
})
|
||||
const subgraphNode = createTestSubgraphNode(subgraph, { id: 107 })
|
||||
subgraphNode.graph?.add(subgraphNode)
|
||||
|
||||
const linkedNodeA = new LGraphNode('LinkedNodeA')
|
||||
const linkedInputA = linkedNodeA.addInput('string_a', '*')
|
||||
linkedNodeA.addWidget('text', 'string_a', 'a', () => {})
|
||||
linkedInputA.widget = { name: 'string_a' }
|
||||
subgraph.add(linkedNodeA)
|
||||
|
||||
const linkedNodeB = new LGraphNode('LinkedNodeB')
|
||||
const linkedInputB = linkedNodeB.addInput('string_a', '*')
|
||||
linkedNodeB.addWidget('text', 'string_a', 'b', () => {})
|
||||
linkedInputB.widget = { name: 'string_a' }
|
||||
subgraph.add(linkedNodeB)
|
||||
|
||||
const storeOnlyNode = new LGraphNode('StoreOnlyNode')
|
||||
storeOnlyNode.addWidget('text', 'string_a', 'independent', () => {})
|
||||
subgraph.add(storeOnlyNode)
|
||||
|
||||
subgraph.inputNode.slots[0].connect(linkedInputA, linkedNodeA)
|
||||
subgraph.inputNode.slots[0].connect(linkedInputB, linkedNodeB)
|
||||
|
||||
setPromotions(subgraphNode, [
|
||||
[String(linkedNodeA.id), 'string_a'],
|
||||
[String(linkedNodeB.id), 'string_a'],
|
||||
[String(storeOnlyNode.id), 'string_a']
|
||||
])
|
||||
|
||||
const serialized = subgraphNode.serialize()
|
||||
const restoredNode = createTestSubgraphNode(subgraph, { id: 108 })
|
||||
restoredNode.configure({
|
||||
...serialized,
|
||||
id: restoredNode.id,
|
||||
type: subgraph.id,
|
||||
inputs: [
|
||||
{
|
||||
name: 'string_a',
|
||||
type: '*',
|
||||
link: null
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
const restoredWidgets = promotedWidgets(restoredNode)
|
||||
expect(restoredWidgets).toHaveLength(2)
|
||||
|
||||
const linkedViewCount = restoredWidgets.filter((widget) =>
|
||||
[String(linkedNodeA.id), String(linkedNodeB.id)].includes(
|
||||
widget.sourceNodeId
|
||||
)
|
||||
).length
|
||||
expect(linkedViewCount).toBe(1)
|
||||
expect(
|
||||
restoredWidgets.some(
|
||||
(widget) => widget.sourceNodeId === String(storeOnlyNode.id)
|
||||
)
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
test('fixture keeps earliest linked representative and independent promotion only', () => {
|
||||
const { graph, hostNode } = setupComplexPromotionFixture()
|
||||
|
||||
@@ -1558,34 +1310,8 @@ describe('SubgraphNode.widgets getter', () => {
|
||||
expect(getValue('19')).toBe('independent-value')
|
||||
})
|
||||
|
||||
test('fixture refreshes duplicate fallback after linked representative recovers', () => {
|
||||
const { subgraph, hostNode } = setupComplexPromotionFixture()
|
||||
|
||||
const earliestLinkedNode = subgraph.getNodeById(20)
|
||||
if (!earliestLinkedNode?.widgets)
|
||||
throw new Error('Expected fixture to contain node 20 with widgets')
|
||||
|
||||
const originalWidgets = earliestLinkedNode.widgets
|
||||
earliestLinkedNode.widgets = originalWidgets.filter(
|
||||
(widget) => widget.name !== 'string_a'
|
||||
)
|
||||
|
||||
const unresolvedWidgets = promotedWidgets(hostNode)
|
||||
expect(
|
||||
unresolvedWidgets.map((widget) => widget.sourceNodeId)
|
||||
).toStrictEqual(['18', '20', '19'])
|
||||
|
||||
earliestLinkedNode.widgets = originalWidgets
|
||||
|
||||
const restoredWidgets = promotedWidgets(hostNode)
|
||||
expect(restoredWidgets.map((widget) => widget.sourceNodeId)).toStrictEqual([
|
||||
'20',
|
||||
'19'
|
||||
])
|
||||
})
|
||||
|
||||
test('fixture converges external widgets and keeps rendered value isolation after transient linked fallback churn', () => {
|
||||
const { subgraph, hostNode } = setupComplexPromotionFixture()
|
||||
test('fixture keeps rendered value isolation between linked representative and second promotion', () => {
|
||||
const { hostNode } = setupComplexPromotionFixture()
|
||||
|
||||
const initialWidgets = promotedWidgets(hostNode)
|
||||
expect(initialWidgets.map((widget) => widget.sourceNodeId)).toStrictEqual([
|
||||
@@ -1593,33 +1319,10 @@ describe('SubgraphNode.widgets getter', () => {
|
||||
'19'
|
||||
])
|
||||
|
||||
const earliestLinkedNode = subgraph.getNodeById(20)
|
||||
if (!earliestLinkedNode?.widgets)
|
||||
throw new Error('Expected fixture to contain node 20 with widgets')
|
||||
|
||||
const originalWidgets = earliestLinkedNode.widgets
|
||||
earliestLinkedNode.widgets = originalWidgets.filter(
|
||||
(widget) => widget.name !== 'string_a'
|
||||
)
|
||||
|
||||
const transientWidgets = promotedWidgets(hostNode)
|
||||
expect(transientWidgets.map((widget) => widget.sourceNodeId)).toStrictEqual(
|
||||
['18', '20', '19']
|
||||
)
|
||||
|
||||
earliestLinkedNode.widgets = originalWidgets
|
||||
|
||||
const finalWidgets = promotedWidgets(hostNode)
|
||||
expect(finalWidgets).toHaveLength(2)
|
||||
expect(finalWidgets.map((widget) => widget.sourceNodeId)).toStrictEqual([
|
||||
'20',
|
||||
'19'
|
||||
])
|
||||
|
||||
const finalLinkedView = finalWidgets.find(
|
||||
const finalLinkedView = initialWidgets.find(
|
||||
(widget) => widget.sourceNodeId === '20'
|
||||
)
|
||||
const finalIndependentView = finalWidgets.find(
|
||||
const finalIndependentView = initialWidgets.find(
|
||||
(widget) => widget.sourceNodeId === '19'
|
||||
)
|
||||
if (!finalLinkedView || !finalIndependentView)
|
||||
@@ -1633,50 +1336,6 @@ describe('SubgraphNode.widgets getter', () => {
|
||||
expect(finalLinkedView.value).toBe('linked-final')
|
||||
expect(finalIndependentView.value).toBe('independent-final')
|
||||
})
|
||||
|
||||
test('clone output preserves proxyWidgets for promotion hydration', () => {
|
||||
const [subgraphNode, innerNodes] = setupSubgraph(1)
|
||||
const innerNode = firstInnerNode(innerNodes)
|
||||
innerNode.addWidget('text', 'widgetA', 'a', () => {})
|
||||
|
||||
setPromotions(subgraphNode, [[String(innerNode.id), 'widgetA']])
|
||||
|
||||
const createNodeSpy = vi
|
||||
.spyOn(LiteGraph, 'createNode')
|
||||
.mockImplementation(() =>
|
||||
createTestSubgraphNode(subgraphNode.subgraph, { id: 999 })
|
||||
)
|
||||
|
||||
const clonedNode = subgraphNode.clone()
|
||||
expect(clonedNode).toBeTruthy()
|
||||
createNodeSpy.mockRestore()
|
||||
if (!clonedNode) throw new Error('Expected clone to return a node')
|
||||
|
||||
const clonedSerialized = clonedNode.serialize()
|
||||
expect(clonedSerialized.properties?.proxyWidgets).toStrictEqual([
|
||||
[String(innerNode.id), 'widgetA']
|
||||
])
|
||||
|
||||
const hydratedClone = createTestSubgraphNode(subgraphNode.subgraph, {
|
||||
id: 100
|
||||
})
|
||||
hydratedClone.configure({
|
||||
...clonedSerialized,
|
||||
id: hydratedClone.id,
|
||||
type: subgraphNode.subgraph.id
|
||||
})
|
||||
|
||||
const hydratedEntries = usePromotionStore().getPromotions(
|
||||
hydratedClone.rootGraph.id,
|
||||
hydratedClone.id
|
||||
)
|
||||
expect(hydratedEntries).toStrictEqual([
|
||||
{
|
||||
sourceNodeId: String(innerNode.id),
|
||||
sourceWidgetName: 'widgetA'
|
||||
}
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('widgets getter caching', () => {
|
||||
|
||||
@@ -33,17 +33,15 @@ import {
|
||||
createPromotedWidgetView,
|
||||
isPromotedWidgetView
|
||||
} from '@/core/graph/subgraph/promotedWidgetView'
|
||||
import { normalizeLegacyProxyWidgetEntry } from '@/core/graph/subgraph/legacyProxyWidgetNormalization'
|
||||
import { migrateLegacyProxyWidgets } from '@/core/graph/subgraph/migrateLegacyProxyWidgets'
|
||||
import type { PromotedWidgetView } from '@/core/graph/subgraph/promotedWidgetView'
|
||||
import type { PromotedWidgetSource } from '@/core/graph/subgraph/promotedWidgetTypes'
|
||||
import { resolveConcretePromotedWidget } from '@/core/graph/subgraph/resolveConcretePromotedWidget'
|
||||
import { resolveSubgraphInputTarget } from '@/core/graph/subgraph/resolveSubgraphInputTarget'
|
||||
import { hasWidgetNode } from '@/core/graph/subgraph/widgetNodeTypeGuard'
|
||||
import {
|
||||
CANVAS_IMAGE_PREVIEW_WIDGET,
|
||||
supportsVirtualCanvasImagePreview
|
||||
} from '@/composables/node/canvasImagePreviewTypes'
|
||||
import { parseProxyWidgets } from '@/core/schemas/promotionSchema'
|
||||
import { useDomWidgetStore } from '@/stores/domWidgetStore'
|
||||
import {
|
||||
makePromotionEntryKey,
|
||||
@@ -655,31 +653,6 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
|
||||
)
|
||||
}
|
||||
|
||||
private _resolveLegacyEntry(
|
||||
widgetName: string
|
||||
): [string, string] | undefined {
|
||||
// Legacy -1 entries use the slot name as the widget name.
|
||||
// Find the input with that name, then trace to the connected interior widget.
|
||||
const input = this.inputs.find((i) => i.name === widgetName)
|
||||
if (!input?._widget) {
|
||||
// Fallback: find via subgraph input slot connection
|
||||
const resolvedTarget = resolveSubgraphInputTarget(this, widgetName)
|
||||
if (!resolvedTarget) return undefined
|
||||
return [resolvedTarget.nodeId, resolvedTarget.widgetName]
|
||||
}
|
||||
|
||||
const widget = input._widget
|
||||
if (isPromotedWidgetView(widget)) {
|
||||
return [widget.sourceNodeId, widget.sourceWidgetName]
|
||||
}
|
||||
|
||||
// Fallback: find via subgraph input slot connection
|
||||
const resolvedTarget = resolveSubgraphInputTarget(this, widgetName)
|
||||
if (!resolvedTarget) return undefined
|
||||
|
||||
return [resolvedTarget.nodeId, resolvedTarget.widgetName]
|
||||
}
|
||||
|
||||
/** Manages lifecycle of all subgraph event listeners */
|
||||
private _eventAbortController = new AbortController()
|
||||
|
||||
@@ -1052,51 +1025,15 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
|
||||
// This prevents stale/duplicate serialized inputs from persisting (#9977).
|
||||
this.inputs = this.inputs.filter((input) => input._subgraphSlot)
|
||||
|
||||
// Ensure proxyWidgets is initialized so it serializes
|
||||
this.properties.proxyWidgets ??= []
|
||||
|
||||
// Clear view cache — forces re-creation on next getter access.
|
||||
// Do NOT clear properties.proxyWidgets — it was already populated
|
||||
// from serialized data by super.configure(info) before this runs.
|
||||
this._promotedViewManager.clear()
|
||||
this._invalidatePromotedViewsCache()
|
||||
|
||||
// Hydrate the store from serialized properties.proxyWidgets
|
||||
const raw = parseProxyWidgets(this.properties.proxyWidgets)
|
||||
migrateLegacyProxyWidgets(this)
|
||||
this._rebindInputSubgraphSlots()
|
||||
|
||||
const store = usePromotionStore()
|
||||
|
||||
const entries = raw
|
||||
.map(([nodeId, widgetName, sourceNodeId]) => {
|
||||
if (nodeId === '-1') {
|
||||
const resolved = this._resolveLegacyEntry(widgetName)
|
||||
if (resolved)
|
||||
return { sourceNodeId: resolved[0], sourceWidgetName: resolved[1] }
|
||||
if (import.meta.env.DEV) {
|
||||
console.warn(
|
||||
`[SubgraphNode] Failed to resolve legacy -1 entry for widget "${widgetName}"`
|
||||
)
|
||||
}
|
||||
return null
|
||||
}
|
||||
if (!this.subgraph.getNodeById(nodeId)) return null
|
||||
|
||||
return normalizeLegacyProxyWidgetEntry(
|
||||
this,
|
||||
nodeId,
|
||||
widgetName,
|
||||
sourceNodeId
|
||||
)
|
||||
})
|
||||
.filter((e): e is NonNullable<typeof e> => e !== null)
|
||||
|
||||
store.setPromotions(this.rootGraph.id, this.id, entries)
|
||||
|
||||
// Write back resolved entries so legacy or stale entries don't persist
|
||||
const serialized = this._serializeEntries(entries)
|
||||
if (JSON.stringify(serialized) !== JSON.stringify(raw)) {
|
||||
this.properties.proxyWidgets = serialized
|
||||
}
|
||||
|
||||
// Check all inputs for connected widgets
|
||||
for (const input of this.inputs) {
|
||||
const subgraphInput = input._subgraphSlot
|
||||
|
||||
@@ -12,7 +12,6 @@ import { isPromotedWidgetView } from '@/core/graph/subgraph/promotedWidgetTypes'
|
||||
|
||||
import {
|
||||
createEventCapture,
|
||||
createTestRootGraph,
|
||||
createTestSubgraph,
|
||||
createTestSubgraphNode,
|
||||
resetSubgraphFixtureState
|
||||
@@ -291,7 +290,7 @@ describe('SubgraphWidgetPromotion', () => {
|
||||
|
||||
hostNode.configure(serializedHostNode)
|
||||
|
||||
expect(hostNode.properties.proxyWidgets).toStrictEqual([
|
||||
expect(hostNode.serialize().properties?.proxyWidgets).toStrictEqual([
|
||||
[String(interiorNode.id), 'batch_size']
|
||||
])
|
||||
expect(hostNode.widgets).toHaveLength(1)
|
||||
@@ -356,63 +355,6 @@ describe('SubgraphWidgetPromotion', () => {
|
||||
expect(widgetSourceIds).toContain(keptSamplerNodeId)
|
||||
})
|
||||
|
||||
it('should normalize legacy prefixed proxyWidgets on configure', () => {
|
||||
const rootGraph = createTestRootGraph()
|
||||
|
||||
const innerSubgraph = createTestSubgraph({
|
||||
rootGraph,
|
||||
inputs: [{ name: 'seed', type: 'number' }]
|
||||
})
|
||||
|
||||
const samplerNode = new LGraphNode('Sampler')
|
||||
const samplerInput = samplerNode.addInput('seed', 'number')
|
||||
samplerNode.addWidget('number', 'noise_seed', 123, () => {})
|
||||
samplerInput.widget = { name: 'noise_seed' }
|
||||
innerSubgraph.add(samplerNode)
|
||||
innerSubgraph.inputNode.slots[0].connect(
|
||||
samplerNode.inputs[0],
|
||||
samplerNode
|
||||
)
|
||||
|
||||
const outerSubgraph = createTestSubgraph({ rootGraph })
|
||||
const nestedNode = createTestSubgraphNode(innerSubgraph, {
|
||||
parentGraph: outerSubgraph
|
||||
})
|
||||
outerSubgraph.add(nestedNode)
|
||||
|
||||
const hostNode = createTestSubgraphNode(outerSubgraph, {
|
||||
parentGraph: rootGraph
|
||||
})
|
||||
|
||||
const serializedHostNode = hostNode.serialize()
|
||||
serializedHostNode.properties = {
|
||||
...serializedHostNode.properties,
|
||||
proxyWidgets: [
|
||||
[
|
||||
String(nestedNode.id),
|
||||
`${nestedNode.id}: ${samplerNode.id}: noise_seed`
|
||||
]
|
||||
]
|
||||
}
|
||||
|
||||
hostNode.configure(serializedHostNode)
|
||||
|
||||
const promotedWidgets = hostNode.widgets
|
||||
.filter(isPromotedWidgetView)
|
||||
.filter((widget) => !widget.name.startsWith('$$'))
|
||||
|
||||
expect(promotedWidgets).toHaveLength(1)
|
||||
expect(promotedWidgets[0].type).toBe('number')
|
||||
expect(promotedWidgets[0].value).toBe(123)
|
||||
expect(promotedWidgets[0].sourceWidgetName).toBe('noise_seed')
|
||||
expect(promotedWidgets[0].disambiguatingSourceNodeId).toBe(
|
||||
String(samplerNode.id)
|
||||
)
|
||||
expect(hostNode.properties.proxyWidgets).toStrictEqual([
|
||||
[String(nestedNode.id), 'noise_seed', String(samplerNode.id)]
|
||||
])
|
||||
})
|
||||
|
||||
it('should preserve promoted widget entries after cloning', () => {
|
||||
const subgraph = createTestSubgraph({
|
||||
inputs: [{ name: 'text', type: 'STRING' }]
|
||||
@@ -441,7 +383,8 @@ describe('SubgraphWidgetPromotion', () => {
|
||||
// Simulate clone: create a second SubgraphNode configured from serialized data
|
||||
const cloneNode = createTestSubgraphNode(subgraph)
|
||||
cloneNode.configure(serialized)
|
||||
const cloneProxyWidgets = cloneNode.properties.proxyWidgets as string[][]
|
||||
const cloneProxyWidgets = cloneNode.serialize().properties!
|
||||
.proxyWidgets as string[][]
|
||||
|
||||
expect(cloneProxyWidgets.length).toBeGreaterThan(0)
|
||||
expect(
|
||||
|
||||
Reference in New Issue
Block a user