mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-20 14:30:41 +00:00
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:
@@ -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 }))
|
||||
|
||||
@@ -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 ??= []
|
||||
|
||||
|
||||
Reference in New Issue
Block a user