Skip to content

Commit c516155

Browse files
viva-jinyigithub-actions
authored andcommitted
[fix] Detect missing nodes in subgraphs (#4547)
Co-authored-by: github-actions <github-actions@github.com>
1 parent 5528ead commit c516155

File tree

3 files changed

+218
-27
lines changed

3 files changed

+218
-27
lines changed

src/composables/nodePack/useMissingNodes.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { app } from '@/scripts/app'
88
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
99
import { useNodeDefStore } from '@/stores/nodeDefStore'
1010
import type { components } from '@/types/comfyRegistryTypes'
11+
import { collectAllNodes } from '@/utils/graphTraversalUtil'
1112

1213
/**
1314
* Composable to find missing NodePacks from workflow
@@ -56,7 +57,7 @@ export const useMissingNodes = () => {
5657
}
5758

5859
const missingCoreNodes = computed<Record<string, LGraphNode[]>>(() => {
59-
const missingNodes = app.graph.nodes.filter(isMissingCoreNode)
60+
const missingNodes = collectAllNodes(app.graph, isMissingCoreNode)
6061
return groupBy(missingNodes, (node) => String(node.properties?.ver || ''))
6162
})
6263

src/composables/nodePack/useWorkflowPacks.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { useNodeDefStore } from '@/stores/nodeDefStore'
99
import { useSystemStatsStore } from '@/stores/systemStatsStore'
1010
import { SelectedVersion, UseNodePacksOptions } from '@/types/comfyManagerTypes'
1111
import type { components } from '@/types/comfyRegistryTypes'
12+
import { collectAllNodes } from '@/utils/graphTraversalUtil'
1213

1314
type WorkflowPack = {
1415
id:
@@ -109,11 +110,13 @@ export const useWorkflowPacks = (options: UseNodePacksOptions = {}) => {
109110
}
110111

111112
/**
112-
* Get the node packs for all nodes in the workflow.
113+
* Get the node packs for all nodes in the workflow (including subgraphs).
113114
*/
114115
const getWorkflowPacks = async () => {
115-
if (!app.graph?.nodes?.length) return []
116-
const packs = await Promise.all(app.graph.nodes.map(workflowNodeToPack))
116+
if (!app.graph) return []
117+
const allNodes = collectAllNodes(app.graph)
118+
if (!allNodes.length) return []
119+
const packs = await Promise.all(allNodes.map(workflowNodeToPack))
117120
workflowPacks.value = packs.filter((pack) => pack !== undefined)
118121
}
119122

tests-ui/tests/composables/useMissingNodes.test.ts

Lines changed: 210 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { useWorkflowPacks } from '@/composables/nodePack/useWorkflowPacks'
77
import { app } from '@/scripts/app'
88
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
99
import { useNodeDefStore } from '@/stores/nodeDefStore'
10+
import { collectAllNodes } from '@/utils/graphTraversalUtil'
1011

1112
// Mock Vue's onMounted to execute immediately for testing
1213
vi.mock('vue', async () => {
@@ -38,9 +39,14 @@ vi.mock('@/scripts/app', () => ({
3839
}
3940
}))
4041

42+
vi.mock('@/utils/graphTraversalUtil', () => ({
43+
collectAllNodes: vi.fn()
44+
}))
45+
4146
const mockUseWorkflowPacks = vi.mocked(useWorkflowPacks)
4247
const mockUseComfyManagerStore = vi.mocked(useComfyManagerStore)
4348
const mockUseNodeDefStore = vi.mocked(useNodeDefStore)
49+
const mockCollectAllNodes = vi.mocked(collectAllNodes)
4450

4551
describe('useMissingNodes', () => {
4652
const mockWorkflowPacks = [
@@ -95,6 +101,9 @@ describe('useMissingNodes', () => {
95101
// Reset app.graph.nodes
96102
// @ts-expect-error - app.graph.nodes is readonly, but we need to modify it for testing.
97103
app.graph.nodes = []
104+
105+
// Default mock for collectAllNodes - returns empty array
106+
mockCollectAllNodes.mockReturnValue([])
98107
})
99108

100109
describe('core filtering logic', () => {
@@ -286,14 +295,9 @@ describe('useMissingNodes', () => {
286295
it('identifies missing core nodes not in nodeDefStore', () => {
287296
const coreNode1 = createMockNode('CoreNode1', 'comfy-core', '1.2.0')
288297
const coreNode2 = createMockNode('CoreNode2', 'comfy-core', '1.2.0')
289-
const registeredNode = createMockNode(
290-
'RegisteredNode',
291-
'comfy-core',
292-
'1.0.0'
293-
)
294298

295-
// @ts-expect-error - app.graph.nodes is readonly, but we need to modify it for testing.
296-
app.graph.nodes = [coreNode1, coreNode2, registeredNode]
299+
// Mock collectAllNodes to return only the filtered nodes (missing core nodes)
300+
mockCollectAllNodes.mockReturnValue([coreNode1, coreNode2])
297301

298302
mockUseNodeDefStore.mockReturnValue({
299303
nodeDefsByName: {
@@ -316,8 +320,8 @@ describe('useMissingNodes', () => {
316320
const node130 = createMockNode('Node130', 'comfy-core', '1.3.0')
317321
const nodeNoVer = createMockNode('NodeNoVer', 'comfy-core')
318322

319-
// @ts-expect-error - app.graph.nodes is readonly, but we need to modify it for testing.
320-
app.graph.nodes = [node120, node130, nodeNoVer]
323+
// Mock collectAllNodes to return these nodes
324+
mockCollectAllNodes.mockReturnValue([node120, node130, nodeNoVer])
321325

322326
// @ts-expect-error - Mocking partial NodeDefStore for testing.
323327
mockUseNodeDefStore.mockReturnValue({
@@ -334,11 +338,9 @@ describe('useMissingNodes', () => {
334338

335339
it('ignores non-core nodes', () => {
336340
const coreNode = createMockNode('CoreNode', 'comfy-core', '1.2.0')
337-
const customNode = createMockNode('CustomNode', 'custom-pack', '1.0.0')
338-
const noPackNode = createMockNode('NoPackNode')
339341

340-
// @ts-expect-error - app.graph.nodes is readonly, but we need to modify it for testing.
341-
app.graph.nodes = [coreNode, customNode, noPackNode]
342+
// Mock collectAllNodes to return only the filtered nodes (core nodes only)
343+
mockCollectAllNodes.mockReturnValue([coreNode])
342344

343345
// @ts-expect-error - Mocking partial NodeDefStore for testing.
344346
mockUseNodeDefStore.mockReturnValue({
@@ -353,33 +355,218 @@ describe('useMissingNodes', () => {
353355
})
354356

355357
it('returns empty object when no core nodes are missing', () => {
356-
const registeredNode1 = createMockNode(
357-
'RegisteredNode1',
358+
// Mock collectAllNodes to return empty array (no missing nodes after filtering)
359+
mockCollectAllNodes.mockReturnValue([])
360+
361+
mockUseNodeDefStore.mockReturnValue({
362+
nodeDefsByName: {
363+
// @ts-expect-error - Creating minimal mock of ComfyNodeDefImpl for testing.
364+
// Only including required properties for our test assertions.
365+
RegisteredNode1: { name: 'RegisteredNode1' },
366+
// @ts-expect-error - Creating minimal mock of ComfyNodeDefImpl for testing.
367+
RegisteredNode2: { name: 'RegisteredNode2' }
368+
}
369+
})
370+
371+
const { missingCoreNodes } = useMissingNodes()
372+
373+
expect(Object.keys(missingCoreNodes.value)).toHaveLength(0)
374+
})
375+
})
376+
377+
describe('subgraph support', () => {
378+
const createMockNode = (
379+
type: string,
380+
packId?: string,
381+
version?: string
382+
): LGraphNode =>
383+
// @ts-expect-error - Creating a partial mock of LGraphNode for testing.
384+
// We only need specific properties for our tests, not the full LGraphNode interface.
385+
({
386+
type,
387+
properties: { cnr_id: packId, ver: version },
388+
id: 1,
389+
title: type,
390+
pos: [0, 0],
391+
size: [100, 100],
392+
flags: {},
393+
graph: null,
394+
mode: 0,
395+
inputs: [],
396+
outputs: []
397+
})
398+
399+
it('detects missing core nodes from subgraphs via collectAllNodes', () => {
400+
const mainNode = createMockNode('MainNode', 'comfy-core', '1.0.0')
401+
const subgraphNode1 = createMockNode(
402+
'SubgraphNode1',
358403
'comfy-core',
359404
'1.0.0'
360405
)
361-
const registeredNode2 = createMockNode(
362-
'RegisteredNode2',
406+
const subgraphNode2 = createMockNode(
407+
'SubgraphNode2',
363408
'comfy-core',
364409
'1.1.0'
365410
)
366411

367-
// @ts-expect-error - app.graph.nodes is readonly, but we need to modify it for testing.
368-
app.graph.nodes = [registeredNode1, registeredNode2]
412+
// Mock collectAllNodes to return all nodes including subgraph nodes
413+
mockCollectAllNodes.mockReturnValue([
414+
mainNode,
415+
subgraphNode1,
416+
subgraphNode2
417+
])
418+
419+
// Mock none of the nodes as registered
420+
// @ts-expect-error - Mocking partial NodeDefStore for testing.
421+
mockUseNodeDefStore.mockReturnValue({
422+
nodeDefsByName: {}
423+
})
424+
425+
const { missingCoreNodes } = useMissingNodes()
426+
427+
// Should detect all 3 nodes as missing
428+
expect(Object.keys(missingCoreNodes.value)).toHaveLength(2) // 2 versions: 1.0.0, 1.1.0
429+
expect(missingCoreNodes.value['1.0.0']).toHaveLength(2) // MainNode + SubgraphNode1
430+
expect(missingCoreNodes.value['1.1.0']).toHaveLength(1) // SubgraphNode2
431+
})
432+
433+
it('calls collectAllNodes with the app graph and filter function', () => {
434+
const mockGraph = { nodes: [], subgraphs: new Map() }
435+
// @ts-expect-error - Mocking app.graph for testing
436+
app.graph = mockGraph
437+
438+
const { missingCoreNodes } = useMissingNodes()
439+
// Access the computed to trigger the function
440+
void missingCoreNodes.value
441+
442+
expect(mockCollectAllNodes).toHaveBeenCalledWith(
443+
mockGraph,
444+
expect.any(Function)
445+
)
446+
})
447+
448+
it('handles collectAllNodes returning empty array', () => {
449+
mockCollectAllNodes.mockReturnValue([])
450+
451+
const { missingCoreNodes } = useMissingNodes()
452+
453+
expect(Object.keys(missingCoreNodes.value)).toHaveLength(0)
454+
})
455+
456+
it('filter function correctly identifies missing core nodes', () => {
457+
const mockGraph = { nodes: [], subgraphs: new Map() }
458+
// @ts-expect-error - Mocking app.graph for testing
459+
app.graph = mockGraph
369460

370461
mockUseNodeDefStore.mockReturnValue({
371462
nodeDefsByName: {
372463
// @ts-expect-error - Creating minimal mock of ComfyNodeDefImpl for testing.
373-
// Only including required properties for our test assertions.
374-
RegisteredNode1: { name: 'RegisteredNode1' },
464+
RegisteredCore: { name: 'RegisteredCore' }
465+
}
466+
})
467+
468+
let capturedFilterFunction: ((node: LGraphNode) => boolean) | undefined
469+
470+
mockCollectAllNodes.mockImplementation((_graph, filter) => {
471+
capturedFilterFunction = filter
472+
return []
473+
})
474+
475+
const { missingCoreNodes } = useMissingNodes()
476+
void missingCoreNodes.value
477+
478+
expect(capturedFilterFunction).toBeDefined()
479+
480+
if (capturedFilterFunction) {
481+
const missingCoreNode = createMockNode(
482+
'MissingCore',
483+
'comfy-core',
484+
'1.0.0'
485+
)
486+
const registeredCoreNode = createMockNode(
487+
'RegisteredCore',
488+
'comfy-core',
489+
'1.0.0'
490+
)
491+
const customNode = createMockNode('CustomNode', 'custom-pack', '1.0.0')
492+
const nodeWithoutPack = createMockNode('NodeWithoutPack')
493+
494+
expect(capturedFilterFunction(missingCoreNode)).toBe(true)
495+
expect(capturedFilterFunction(registeredCoreNode)).toBe(false)
496+
expect(capturedFilterFunction(customNode)).toBe(false)
497+
expect(capturedFilterFunction(nodeWithoutPack)).toBe(false)
498+
}
499+
})
500+
501+
it('integrates with collectAllNodes to find nodes from subgraphs', () => {
502+
mockCollectAllNodes.mockImplementation((graph, filter) => {
503+
const allNodes: LGraphNode[] = []
504+
505+
for (const node of graph.nodes) {
506+
if (node.isSubgraphNode?.() && node.subgraph) {
507+
for (const subNode of node.subgraph.nodes) {
508+
if (!filter || filter(subNode)) {
509+
allNodes.push(subNode)
510+
}
511+
}
512+
}
513+
514+
if (!filter || filter(node)) {
515+
allNodes.push(node)
516+
}
517+
}
518+
519+
return allNodes
520+
})
521+
522+
const mainMissingNode = createMockNode(
523+
'MainMissing',
524+
'comfy-core',
525+
'1.0.0'
526+
)
527+
const subgraphMissingNode = createMockNode(
528+
'SubgraphMissing',
529+
'comfy-core',
530+
'1.1.0'
531+
)
532+
const subgraphRegisteredNode = createMockNode(
533+
'SubgraphRegistered',
534+
'comfy-core',
535+
'1.0.0'
536+
)
537+
538+
const mockSubgraph = {
539+
nodes: [subgraphMissingNode, subgraphRegisteredNode]
540+
}
541+
542+
const mockSubgraphNode = {
543+
isSubgraphNode: () => true,
544+
subgraph: mockSubgraph,
545+
type: 'SubgraphContainer',
546+
properties: { cnr_id: 'custom-pack' }
547+
}
548+
549+
const mockMainGraph = {
550+
nodes: [mainMissingNode, mockSubgraphNode]
551+
}
552+
553+
// @ts-expect-error - Mocking app.graph for testing
554+
app.graph = mockMainGraph
555+
556+
mockUseNodeDefStore.mockReturnValue({
557+
nodeDefsByName: {
375558
// @ts-expect-error - Creating minimal mock of ComfyNodeDefImpl for testing.
376-
RegisteredNode2: { name: 'RegisteredNode2' }
559+
SubgraphRegistered: { name: 'SubgraphRegistered' }
377560
}
378561
})
379562

380563
const { missingCoreNodes } = useMissingNodes()
381564

382-
expect(Object.keys(missingCoreNodes.value)).toHaveLength(0)
565+
expect(Object.keys(missingCoreNodes.value)).toHaveLength(2)
566+
expect(missingCoreNodes.value['1.0.0']).toHaveLength(1)
567+
expect(missingCoreNodes.value['1.1.0']).toHaveLength(1)
568+
expect(missingCoreNodes.value['1.0.0'][0].type).toBe('MainMissing')
569+
expect(missingCoreNodes.value['1.1.0'][0].type).toBe('SubgraphMissing')
383570
})
384571
})
385572
})

0 commit comments

Comments
 (0)