Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 21 additions & 5 deletions src/utils/executableGroupNodeChildDTO.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Expand Down
199 changes: 199 additions & 0 deletions tests-ui/tests/utils/executableGroupNodeChildDTO.test.ts
Original file line number Diff line number Diff line change
@@ -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<ExecutionId, ExecutableLGraphNode>
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.'
)
})
})
})