Compare commits

...

1 Commits

Author SHA1 Message Date
dante01yoon
d480373658 fix: resolve "No inner node DTO found" error when muting subgraphs
When a subgraph or group node is muted (mode=NEVER), resolveOutput()
still tried to look up inner node DTOs that were never registered,
causing execution errors. Add early returns for NEVER mode in both
ExecutableNodeDTO and ExecutableGroupNodeDTO.

Fixes #8986

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 21:38:16 +09:00
4 changed files with 147 additions and 0 deletions

View File

@@ -3,6 +3,7 @@ import { describe, expect, it, vi } from 'vitest'
import {
LGraph,
LGraphEventMode,
LGraphNode,
ExecutableNodeDTO
} from '@/lib/litegraph/src/litegraph'
@@ -477,3 +478,120 @@ describe.skip('ExecutableNodeDTO Scale Testing', () => {
}
})
})
describe('Muted node output resolution', () => {
it('returns undefined when node mode is NEVER', () => {
const graph = new LGraph()
const node = new LGraphNode('Muted Node')
node.addOutput('out', 'number')
node.mode = LGraphEventMode.NEVER
graph.add(node)
const dto = new ExecutableNodeDTO(node, [], new Map(), undefined)
const result = dto.resolveOutput(0, 'number', new Set())
expect(result).toBeUndefined()
})
it('does not throw when inner nodes are not registered for muted subgraph', () => {
const graph = new LGraph()
const node = new LGraphNode('Muted Subgraph')
node.addOutput('out', 'number')
graph.add(node)
node.mode = LGraphEventMode.NEVER
// Simulate a subgraph node that would throw if _resolveSubgraphOutput ran
vi.spyOn(node, 'isSubgraphNode').mockReturnValue(true)
const dto = new ExecutableNodeDTO(node, [], new Map(), undefined)
expect(() => dto.resolveOutput(0, 'number', new Set())).not.toThrow()
expect(dto.resolveOutput(0, 'number', new Set())).toBeUndefined()
})
it('resolveInput to a muted upstream node resolves undefined', () => {
const graph = new LGraph()
const upstream = new LGraphNode('Upstream')
upstream.addOutput('out', 'number')
upstream.mode = LGraphEventMode.NEVER
graph.add(upstream)
const downstream = new LGraphNode('Downstream')
downstream.addInput('in', 'number')
graph.add(downstream)
downstream.connect(0, upstream, 0)
const nodesByExecutionId = new Map()
const upstreamDto = new ExecutableNodeDTO(
upstream,
[],
nodesByExecutionId,
undefined
)
const downstreamDto = new ExecutableNodeDTO(
downstream,
[],
nodesByExecutionId,
undefined
)
nodesByExecutionId.set(upstreamDto.id, upstreamDto)
nodesByExecutionId.set(downstreamDto.id, downstreamDto)
const result = downstreamDto.resolveInput(0)
expect(result).toBeUndefined()
})
})
describe('Bypass node output resolution', () => {
it('bypasses through matching input when mode is BYPASS', () => {
const graph = new LGraph()
const source = new LGraphNode('Source')
source.addOutput('out', 'number')
graph.add(source)
const bypass = new LGraphNode('Bypass')
bypass.addInput('in', 'number')
bypass.addOutput('out', 'number')
bypass.mode = LGraphEventMode.BYPASS
graph.add(bypass)
source.connect(0, bypass, 0)
const nodesByExecutionId = new Map()
const sourceDto = new ExecutableNodeDTO(
source,
[],
nodesByExecutionId,
undefined
)
const bypassDto = new ExecutableNodeDTO(
bypass,
[],
nodesByExecutionId,
undefined
)
nodesByExecutionId.set(sourceDto.id, sourceDto)
nodesByExecutionId.set(bypassDto.id, bypassDto)
const result = bypassDto.resolveOutput(0, 'number', new Set())
expect(result).toBeDefined()
expect(result?.node).toBe(sourceDto)
})
})
describe('ALWAYS mode node output resolution', () => {
it('resolves to itself when mode is ALWAYS', () => {
const graph = new LGraph()
const node = new LGraphNode('Normal Node')
node.addOutput('out', 'number')
node.mode = LGraphEventMode.ALWAYS
graph.add(node)
const dto = new ExecutableNodeDTO(node, [], new Map(), undefined)
const result = dto.resolveOutput(0, 'number', new Set())
expect(result).toBeDefined()
expect(result?.node).toBe(dto)
expect(result?.origin_id).toBe(dto.id)
expect(result?.origin_slot).toBe(0)
})
})

View File

@@ -266,6 +266,9 @@ export class ExecutableNodeDTO implements ExecutableLGraphNode {
}
visited.add(uniqueId)
// Muted nodes produce no output
if (this.mode === LGraphEventMode.NEVER) return
// Upstreamed: Bypass nodes are bypassed using the first input with matching type
if (this.mode === LGraphEventMode.BYPASS) {
// Bypass nodes by finding first input with matching type

View File

@@ -0,0 +1,23 @@
import { describe, expect, it } from 'vitest'
import {
LGraph,
LGraphEventMode,
LGraphNode
} from '@/lib/litegraph/src/litegraph'
import { ExecutableGroupNodeDTO } from './executableGroupNodeDto'
describe('ExecutableGroupNodeDTO muted output resolution', () => {
it('returns undefined when node mode is NEVER', () => {
const graph = new LGraph()
const node = new LGraphNode('Muted Group')
node.addOutput('out', 'number')
node.mode = LGraphEventMode.NEVER
graph.add(node)
const dto = new ExecutableGroupNodeDTO(node, [], new Map(), undefined)
const result = dto.resolveOutput(0, 'number', new Set())
expect(result).toBeUndefined()
})
})

View File

@@ -24,6 +24,9 @@ export class ExecutableGroupNodeDTO extends ExecutableNodeDTO {
}
override resolveOutput(slot: number, type: ISlotType, visited: Set<string>) {
// Muted nodes produce no output
if (this.mode === LGraphEventMode.NEVER) return
// Temporary duplication: Bypass nodes are bypassed using the first input with matching type
if (this.mode === LGraphEventMode.BYPASS) {
const { inputs } = this