diff --git a/src/utils/executableGroupNodeChildDTO.ts b/src/utils/executableGroupNodeChildDTO.ts index 8d2d573e6b..5cffd3f348 100644 --- a/src/utils/executableGroupNodeChildDTO.ts +++ b/src/utils/executableGroupNodeChildDTO.ts @@ -27,25 +27,41 @@ export class ExecutableGroupNodeChildDTO extends ExecutableNodeDTO { } override resolveInput(slot: number) { + // Check if this group node is inside a subgraph (unsupported) + if (this.id.split(':').length > 2) { + throw new Error( + 'Group nodes inside subgraphs are not supported. Please convert the group node to a subgraph instead.' + ) + } + const inputNode = this.node.getInputNode(slot) if (!inputNode) return const link = this.node.getInputLink(slot) if (!link) throw new Error('Failed to get input link') - const id = String(inputNode.id).split(':').at(-1) - if (id === undefined) throw new Error('Invalid input node id') + const inputNodeId = String(inputNode.id) + + // Try to find the node using the full ID first (for nodes outside the group) + let inputNodeDto = this.nodesByExecutionId?.get(inputNodeId) + + // If not found, try with just the last part of the ID (for nodes inside the group) + if (!inputNodeDto) { + const id = inputNodeId.split(':').at(-1) + if (id !== undefined) { + inputNodeDto = this.nodesByExecutionId?.get(id) + } + } - const inputNodeDto = this.nodesByExecutionId?.get(id) if (!inputNodeDto) { throw new Error( - `Failed to get input node ${id} for group node child ${this.id} with slot ${slot}` + `Failed to get input node ${inputNodeId} for group node child ${this.id} with slot ${slot}` ) } return { node: inputNodeDto, - origin_id: String(inputNode.id), + origin_id: inputNodeId, origin_slot: link.origin_slot } } diff --git a/tests-ui/tests/utils/executableGroupNodeChildDTO.test.ts b/tests-ui/tests/utils/executableGroupNodeChildDTO.test.ts new file mode 100644 index 0000000000..6b2fb3ffa8 --- /dev/null +++ b/tests-ui/tests/utils/executableGroupNodeChildDTO.test.ts @@ -0,0 +1,199 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +import type { GroupNodeHandler } from '@/extensions/core/groupNode' +import type { + ExecutableLGraphNode, + ExecutionId, + LGraphNode +} from '@/lib/litegraph/src/litegraph' +import { ExecutableGroupNodeChildDTO } from '@/utils/executableGroupNodeChildDTO' + +describe('ExecutableGroupNodeChildDTO', () => { + let mockNode: LGraphNode + let mockInputNode: LGraphNode + let mockNodesByExecutionId: Map + let mockGroupNodeHandler: GroupNodeHandler + + beforeEach(() => { + // Create mock nodes + mockNode = { + id: '3', // Simple node ID for most tests + graph: {}, + getInputNode: vi.fn(), + getInputLink: vi.fn(), + inputs: [] + } as any + + mockInputNode = { + id: '1', + graph: {} + } as any + + // Create the nodesByExecutionId map + mockNodesByExecutionId = new Map() + + mockGroupNodeHandler = {} as GroupNodeHandler + }) + + describe('resolveInput', () => { + it('should resolve input from external node (node outside the group)', () => { + // Setup: Group node child with ID '10:3' + const groupNodeChild = { + id: '10:3', + graph: {}, + getInputNode: vi.fn().mockReturnValue(mockInputNode), + getInputLink: vi.fn().mockReturnValue({ + origin_slot: 0 + }), + inputs: [] + } as any + + // External node with ID '1' + const externalNodeDto = { + id: '1', + type: 'TestNode' + } as ExecutableLGraphNode + + mockNodesByExecutionId.set('1', externalNodeDto) + + const dto = new ExecutableGroupNodeChildDTO( + groupNodeChild, + [], // No subgraph path - group is in root graph + mockNodesByExecutionId, + undefined, + mockGroupNodeHandler + ) + + const result = dto.resolveInput(0) + + expect(result).toEqual({ + node: externalNodeDto, + origin_id: '1', + origin_slot: 0 + }) + }) + + it('should resolve input from internal node (node inside the same group)', () => { + // Setup: Group node child with ID '10:3' + const groupNodeChild = { + id: '10:3', + graph: {}, + getInputNode: vi.fn(), + getInputLink: vi.fn(), + inputs: [] + } as any + + // Internal node with ID '10:2' + const internalInputNode = { + id: '10:2', + graph: {} + } as LGraphNode + + const internalNodeDto = { + id: '2', + type: 'InternalNode' + } as ExecutableLGraphNode + + // Internal nodes are stored with just their index + mockNodesByExecutionId.set('2', internalNodeDto) + + groupNodeChild.getInputNode.mockReturnValue(internalInputNode) + groupNodeChild.getInputLink.mockReturnValue({ + origin_slot: 1 + }) + + const dto = new ExecutableGroupNodeChildDTO( + groupNodeChild, + [], + mockNodesByExecutionId, + undefined, + mockGroupNodeHandler + ) + + const result = dto.resolveInput(0) + + expect(result).toEqual({ + node: internalNodeDto, + origin_id: '10:2', + origin_slot: 1 + }) + }) + + it('should return undefined if no input node exists', () => { + mockNode.getInputNode = vi.fn().mockReturnValue(null) + + const dto = new ExecutableGroupNodeChildDTO( + mockNode, + [], + mockNodesByExecutionId, + undefined, + mockGroupNodeHandler + ) + + const result = dto.resolveInput(0) + + expect(result).toBeUndefined() + }) + + it('should throw error if input link is missing', () => { + mockNode.getInputNode = vi.fn().mockReturnValue(mockInputNode) + mockNode.getInputLink = vi.fn().mockReturnValue(null) + + const dto = new ExecutableGroupNodeChildDTO( + mockNode, + [], + mockNodesByExecutionId, + undefined, + mockGroupNodeHandler + ) + + expect(() => dto.resolveInput(0)).toThrow('Failed to get input link') + }) + + it('should throw error if input node cannot be found in nodesByExecutionId', () => { + // Node exists but is not in the map + mockNode.getInputNode = vi.fn().mockReturnValue(mockInputNode) + mockNode.getInputLink = vi.fn().mockReturnValue({ + origin_slot: 0 + }) + + const dto = new ExecutableGroupNodeChildDTO( + mockNode, + [], + mockNodesByExecutionId, // Empty map + undefined, + mockGroupNodeHandler + ) + + expect(() => dto.resolveInput(0)).toThrow( + 'Failed to get input node 1 for group node child 3 with slot 0' + ) + }) + + it('should throw error for group nodes inside subgraphs (unsupported)', () => { + // Setup: Group node child inside a subgraph (execution ID has more than 2 segments) + const nestedGroupNode = { + id: '1:2:3', // subgraph:groupnode:innernode + graph: {}, + getInputNode: vi.fn().mockReturnValue(mockInputNode), + getInputLink: vi.fn().mockReturnValue({ + origin_slot: 0 + }), + inputs: [] + } as any + + // Create DTO with deeply nested path to simulate group node inside subgraph + const dto = new ExecutableGroupNodeChildDTO( + nestedGroupNode, + ['1', '2'], // Path indicating it's inside a subgraph then group + mockNodesByExecutionId, + undefined, + mockGroupNodeHandler + ) + + expect(() => dto.resolveInput(0)).toThrow( + 'Group nodes inside subgraphs are not supported. Please convert the group node to a subgraph instead.' + ) + }) + }) +})