fix: prune orphaned SubgraphNode inputs after configure (#10020)

## Summary

Prune orphaned inputs in `_internalConfigureAfterSlots()` to fix
duplicate SubgraphNode inputs that accumulate on serialize-load cycles.

## Changes

- **What**: After `_rebindInputSubgraphSlots()`, filter out inputs with
no matching `_subgraphSlot`. This prevents `LGraphNode.configure()`
`cloneObject` expansion from persisting stale duplicates.
- Added 3 regression tests covering: corrupted serialized data,
reconfigure round-trips, and serialization output.

## Review Focus

The fix is a single `filter()` call. The existing `console.warn` guard
at line ~976 (for inputs without `_subgraphSlot`) becomes dead code
after this fix but is retained as defense-in-depth.

Fixes #9977

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10020-fix-prune-orphaned-SubgraphNode-inputs-after-configure-3256d73d3650812e8cecf4a3c86f2c33)
by [Unito](https://www.unito.io)
This commit is contained in:
Christian Byrne
2026-03-17 00:54:35 -07:00
committed by GitHub
parent 14274fb8fa
commit 918095f197
2 changed files with 86 additions and 2 deletions

View File

@@ -9,9 +9,9 @@ import { describe, expect, it, vi } from 'vitest'
import { createTestingPinia } from '@pinia/testing'
import { setActivePinia } from 'pinia'
import type { SubgraphNode } from '@/lib/litegraph/src/litegraph'
import { LGraph, Subgraph } from '@/lib/litegraph/src/litegraph'
import { LGraph, Subgraph, SubgraphNode } from '@/lib/litegraph/src/litegraph'
import type { SubgraphInput } from '@/lib/litegraph/src/subgraph/SubgraphInput'
import type { ExportedSubgraphInstance } from '@/lib/litegraph/src/types/serialisation'
import { subgraphTest } from './__fixtures__/subgraphFixtures'
import {
@@ -618,6 +618,86 @@ describe.skip('SubgraphNode Cleanup', () => {
})
})
describe('SubgraphNode duplicate input pruning (#9977)', () => {
it('should prune inputs that have no matching subgraph slot after configure', () => {
setActivePinia(createTestingPinia({ stubActions: false }))
const subgraph = createTestSubgraph({
inputs: [
{ name: 'a', type: 'STRING' },
{ name: 'b', type: 'NUMBER' }
]
})
const parentGraph = new LGraph()
const instanceData = {
id: 1 as const,
type: subgraph.id,
pos: [0, 0] as [number, number],
size: [200, 100] as [number, number],
inputs: [
{ name: 'a', type: 'STRING', link: null },
{ name: 'b', type: 'NUMBER', link: null },
{ name: 'a', type: 'STRING', link: null },
{ name: 'b', type: 'NUMBER', link: null }
],
outputs: [],
properties: {},
flags: {},
mode: 0,
order: 0
}
const node = new SubgraphNode(
parentGraph,
subgraph,
instanceData as ExportedSubgraphInstance
)
expect(node.inputs).toHaveLength(2)
expect(node.inputs.every((i) => i._subgraphSlot)).toBe(true)
})
it('should not accumulate duplicate inputs on reconfigure', () => {
setActivePinia(createTestingPinia({ stubActions: false }))
const subgraph = createTestSubgraph({
inputs: [
{ name: 'a', type: 'STRING' },
{ name: 'b', type: 'NUMBER' }
]
})
const node = createTestSubgraphNode(subgraph)
expect(node.inputs).toHaveLength(2)
const serialized = node.serialize()
node.configure(serialized)
expect(node.inputs).toHaveLength(2)
const serialized2 = node.serialize()
node.configure(serialized2)
expect(node.inputs).toHaveLength(2)
})
it('should serialize with exactly the subgraph-defined inputs', () => {
setActivePinia(createTestingPinia({ stubActions: false }))
const subgraph = createTestSubgraph({
inputs: [
{ name: 'x', type: 'IMAGE' },
{ name: 'y', type: 'VAE' }
]
})
const node = createTestSubgraphNode(subgraph)
const serialized = node.serialize()
expect(serialized.inputs).toHaveLength(2)
expect(serialized.inputs?.map((i) => i.name)).toEqual(['x', 'y'])
})
})
describe('SubgraphNode promotion view keys', () => {
it('distinguishes tuples that differ only by colon placement', () => {
setActivePinia(createTestingPinia({ stubActions: false }))

View File

@@ -926,6 +926,10 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
override _internalConfigureAfterSlots() {
this._rebindInputSubgraphSlots()
// Prune inputs that don't map to any subgraph slot definition.
// This prevents stale/duplicate serialized inputs from persisting (#9977).
this.inputs = this.inputs.filter((input) => input._subgraphSlot)
// Ensure proxyWidgets is initialized so it serializes
this.properties.proxyWidgets ??= []