From a0470f51165f2fe8b9c633d4eef95ec8fc79269c Mon Sep 17 00:00:00 2001 From: DrJKL Date: Fri, 1 May 2026 23:47:29 -0700 Subject: [PATCH] test(subgraph): cover multi-instance promoted-widget selection round-trip Two SubgraphNode instances of the same definition share the same promoted-widget storeName but distinct host ids. Pin the contract that loadSelections preserves both as distinct entries through pruneLinearData + normalizeSelectedInput. Amp-Thread-ID: https://ampcode.com/threads/T-019de73b-91ac-76c8-8b10-99552857d285 Co-authored-by: Amp --- src/stores/appModeStore.test.ts | 72 +++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/src/stores/appModeStore.test.ts b/src/stores/appModeStore.test.ts index 01ce66a35d..5a773862ac 100644 --- a/src/stores/appModeStore.test.ts +++ b/src/stores/appModeStore.test.ts @@ -342,6 +342,78 @@ describe('appModeStore', () => { } }) + it('preserves selected promoted-widget identity per instance across save/reload', async () => { + // Build one subgraph definition with one promoted widget, + // then create two SubgraphNode instances of that definition. + const subgraph = createTestSubgraph({ + inputs: [{ name: 'value', type: '*' }] + }) + const inner = new LiteGraphNode('Inner') + const input = inner.addInput('value', '*') + inner.addWidget('text', 'value', 'a', () => {}) + input.widget = { name: 'value' } + subgraph.add(inner) + subgraph.inputNode.slots[0].connect(input, inner) + + const hostA = createTestSubgraphNode(subgraph, { id: 701 }) + hostA._internalConfigureAfterSlots() + hostA.graph!.add(hostA) + + const hostB = createTestSubgraphNode(subgraph, { id: 702 }) + hostB._internalConfigureAfterSlots() + hostB.graph!.add(hostB) + + const promotionStore = usePromotionStore() + promotionStore.setPromotions(hostA.rootGraph.id, hostA.id, [ + { sourceNodeId: String(inner.id), sourceWidgetName: 'value' } + ]) + promotionStore.setPromotions(hostB.rootGraph.id, hostB.id, [ + { sourceNodeId: String(inner.id), sourceWidgetName: 'value' } + ]) + + const promotedA = hostA.widgets.find(isPromotedWidgetView) + const promotedB = hostB.widgets.find(isPromotedWidgetView) + if (!promotedA || !promotedB) throw new Error('Expected promoted views') + + // Precondition: storeNames are equal (interior identity matches), + // host ids differ. + expect(promotedA.storeName).toBe(promotedB.storeName) + expect(hostA.id).not.toBe(hostB.id) + + const { resolveNode: actualResolveNode } = (await vi.importActual( + '@/utils/litegraphUtil' + )) as { + resolveNode: (nodeId: NodeId, graph: LGraph) => LGraphNode | undefined + } + const graph = hostA.graph! + const originalRootGraph = app.rootGraph + mockResolveNode.mockImplementation((id) => actualResolveNode(id, graph)) + Object.defineProperty(app, 'rootGraph', { value: graph, writable: true }) + + try { + store.loadSelections({ + inputs: [ + [hostA.id, promotedA.storeName], + [hostB.id, promotedB.storeName] + ], + outputs: [] + }) + + expect(store.selectedInputs).toHaveLength(2) + expect(store.selectedInputs).toEqual( + expect.arrayContaining([ + [hostA.id, promotedA.storeName], + [hostB.id, promotedB.storeName] + ]) + ) + } finally { + Object.defineProperty(app, 'rootGraph', { + value: originalRootGraph, + writable: true + }) + } + }) + it('keeps inputs for existing nodes even if widget is missing', async () => { const node1 = mockNode(1) mockResolveNode.mockImplementation((id) =>