mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-04 13:12:10 +00:00
fix: stabilize nested subgraph promoted widget resolution (#9282)
## Summary Fix multiple issues with promoted widget resolution in nested subgraphs, ensuring correct value propagation, slot matching, and rendering for deeply nested promoted widgets. ## Changes - **What**: Stabilize nested subgraph promoted widget resolution chain - Use deep source keys for promoted widget values in Vue rendering mode - Resolve effective widget options from the source widget instead of the promoted view - Stabilize slot resolution for nested promoted widgets - Preserve combo value rendering for promoted subgraph widgets - Prevent subgraph definition deletion while other nodes still reference the same type - Clean up unused exported resolution types ## Review Focus - `resolveConcretePromotedWidget.ts` — new recursive resolution logic for deeply nested promoted widgets - `useGraphNodeManager.ts` — option extraction now uses `effectiveWidget` for promoted widgets - `SubgraphNode.ts` — unpack no longer force-deletes definitions referenced by other nodes ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-9282-fix-stabilize-nested-subgraph-promoted-widget-resolution-3146d73d365081208a4fe931bb7569cf) by [Unito](https://www.unito.io) --------- Co-authored-by: Amp <amp@ampcode.com> Co-authored-by: GitHub Action <action@github.com>
This commit is contained in:
147
src/core/graph/subgraph/resolveSubgraphInputLink.test.ts
Normal file
147
src/core/graph/subgraph/resolveSubgraphInputLink.test.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
import { createTestingPinia } from '@pinia/testing'
|
||||
import { setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, test, vi } from 'vitest'
|
||||
|
||||
import { resolveSubgraphInputLink } from '@/core/graph/subgraph/resolveSubgraphInputLink'
|
||||
import { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import {
|
||||
createTestSubgraph,
|
||||
createTestSubgraphNode
|
||||
} from '@/lib/litegraph/src/subgraph/__fixtures__/subgraphHelpers'
|
||||
import type { Subgraph } from '@/lib/litegraph/src/subgraph/Subgraph'
|
||||
import type { SubgraphNode } from '@/lib/litegraph/src/subgraph/SubgraphNode'
|
||||
|
||||
vi.mock('@/renderer/core/canvas/canvasStore', () => ({
|
||||
useCanvasStore: () => ({})
|
||||
}))
|
||||
vi.mock('@/stores/domWidgetStore', () => ({
|
||||
useDomWidgetStore: () => ({ widgetStates: new Map() })
|
||||
}))
|
||||
vi.mock('@/services/litegraphService', () => ({
|
||||
useLitegraphService: () => ({ updatePreviews: () => ({}) })
|
||||
}))
|
||||
|
||||
function createSubgraphSetup(inputName: string): {
|
||||
subgraph: Subgraph
|
||||
subgraphNode: SubgraphNode
|
||||
} {
|
||||
const subgraph = createTestSubgraph({
|
||||
inputs: [{ name: inputName, type: '*' }]
|
||||
})
|
||||
const subgraphNode = createTestSubgraphNode(subgraph, { id: 1 })
|
||||
return { subgraph, subgraphNode }
|
||||
}
|
||||
|
||||
function addLinkedInteriorInput(
|
||||
subgraph: Subgraph,
|
||||
inputName: string,
|
||||
linkedInputName: string,
|
||||
widgetName: string
|
||||
): {
|
||||
node: LGraphNode
|
||||
linkId: number
|
||||
} {
|
||||
const inputSlot = subgraph.inputNode.slots.find(
|
||||
(slot) => slot.name === inputName
|
||||
)
|
||||
if (!inputSlot) throw new Error(`Missing subgraph input slot: ${inputName}`)
|
||||
|
||||
const node = new LGraphNode(`Interior-${linkedInputName}`)
|
||||
const input = node.addInput(linkedInputName, '*')
|
||||
node.addWidget('text', widgetName, '', () => undefined)
|
||||
input.widget = { name: widgetName }
|
||||
subgraph.add(node)
|
||||
inputSlot.connect(input, node)
|
||||
|
||||
if (input.link == null)
|
||||
throw new Error(`Expected link to be created for input ${linkedInputName}`)
|
||||
|
||||
return { node, linkId: input.link }
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
setActivePinia(createTestingPinia({ stubActions: false }))
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('resolveSubgraphInputLink', () => {
|
||||
test('returns undefined for non-subgraph nodes', () => {
|
||||
const node = new LGraphNode('plain-node')
|
||||
|
||||
const result = resolveSubgraphInputLink(node, 'missing', () => 'resolved')
|
||||
|
||||
expect(result).toBeUndefined()
|
||||
})
|
||||
|
||||
test('returns undefined when input slot is missing', () => {
|
||||
const { subgraphNode } = createSubgraphSetup('existing')
|
||||
|
||||
const result = resolveSubgraphInputLink(
|
||||
subgraphNode,
|
||||
'missing',
|
||||
() => 'resolved'
|
||||
)
|
||||
|
||||
expect(result).toBeUndefined()
|
||||
})
|
||||
|
||||
test('skips stale links where inputNode.inputs is unavailable', () => {
|
||||
const { subgraph, subgraphNode } = createSubgraphSetup('prompt')
|
||||
addLinkedInteriorInput(subgraph, 'prompt', 'seed_input', 'seed')
|
||||
const stale = addLinkedInteriorInput(
|
||||
subgraph,
|
||||
'prompt',
|
||||
'stale_input',
|
||||
'stale'
|
||||
)
|
||||
|
||||
const originalGetLink = subgraph.getLink.bind(subgraph)
|
||||
vi.spyOn(subgraph, 'getLink').mockImplementation((linkId) => {
|
||||
if (typeof linkId !== 'number') return originalGetLink(linkId)
|
||||
if (linkId === stale.linkId) {
|
||||
return {
|
||||
resolve: () => ({
|
||||
inputNode: {
|
||||
inputs: undefined,
|
||||
getWidgetFromSlot: () => ({ name: 'ignored' })
|
||||
}
|
||||
})
|
||||
} as unknown as ReturnType<typeof subgraph.getLink>
|
||||
}
|
||||
|
||||
return originalGetLink(linkId)
|
||||
})
|
||||
|
||||
const result = resolveSubgraphInputLink(
|
||||
subgraphNode,
|
||||
'prompt',
|
||||
({ targetInput }) => targetInput.name
|
||||
)
|
||||
|
||||
expect(result).toBe('seed_input')
|
||||
})
|
||||
|
||||
test('caches getTargetWidget result within the same callback evaluation', () => {
|
||||
const { subgraph, subgraphNode } = createSubgraphSetup('model')
|
||||
const linked = addLinkedInteriorInput(
|
||||
subgraph,
|
||||
'model',
|
||||
'model_input',
|
||||
'modelWidget'
|
||||
)
|
||||
const getWidgetFromSlot = vi.spyOn(linked.node, 'getWidgetFromSlot')
|
||||
|
||||
const result = resolveSubgraphInputLink(
|
||||
subgraphNode,
|
||||
'model',
|
||||
({ getTargetWidget }) => {
|
||||
expect(getTargetWidget()?.name).toBe('modelWidget')
|
||||
expect(getTargetWidget()?.name).toBe('modelWidget')
|
||||
return 'ok'
|
||||
}
|
||||
)
|
||||
|
||||
expect(result).toBe('ok')
|
||||
expect(getWidgetFromSlot).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user