Add functions for mapping node locations to an id

These locations differ from either local ids (like `5`) or execution ids
(like `3:4:5`) in that all nodes which are 'linked' such that changes to
one node will apply changes to other nodes will map to the same id. This
is specifically relevant for multiple instances of the same subgraph.

These IDs will actually get used in a followup commit
This commit is contained in:
Jacob Segal
2025-07-06 20:41:58 -07:00
parent a70a81c9ae
commit c8371d6089
7 changed files with 765 additions and 4 deletions

View File

@@ -2,10 +2,11 @@ import { createPinia, setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { useExecutionStore } from '@/stores/executionStore'
import { useWorkflowStore } from '@/stores/workflowStore'
// Remove any previous global types
declare global {
// Empty interface to override any previous declarations
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
interface Window {}
}
@@ -80,3 +81,93 @@ describe('executionStore - display_component handling', () => {
expect(mockShowChatHistory).not.toHaveBeenCalled()
})
})
describe('useExecutionStore - NodeLocatorId conversions', () => {
let store: ReturnType<typeof useExecutionStore>
let workflowStore: ReturnType<typeof useWorkflowStore>
beforeEach(() => {
setActivePinia(createPinia())
store = useExecutionStore()
workflowStore = useWorkflowStore()
vi.clearAllMocks()
})
describe('executionIdToNodeLocatorId', () => {
it('should convert hierarchical execution ID to NodeLocatorId', () => {
const mockNodeLocatorId = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890:456'
vi.spyOn(workflowStore, 'hierarchicalIdToNodeLocatorId').mockReturnValue(
mockNodeLocatorId as any
)
const result = store.executionIdToNodeLocatorId('123:456')
expect(workflowStore.hierarchicalIdToNodeLocatorId).toHaveBeenCalledWith(
'123:456'
)
expect(result).toBe(mockNodeLocatorId)
})
it('should convert simple node ID to NodeLocatorId', () => {
const mockNodeLocatorId = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890:123'
vi.spyOn(workflowStore, 'nodeIdToNodeLocatorId').mockReturnValue(
mockNodeLocatorId as any
)
const result = store.executionIdToNodeLocatorId('123')
expect(workflowStore.nodeIdToNodeLocatorId).toHaveBeenCalledWith('123')
expect(result).toBe(mockNodeLocatorId)
})
it('should handle numeric node IDs', () => {
const mockNodeLocatorId = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890:123'
vi.spyOn(workflowStore, 'nodeIdToNodeLocatorId').mockReturnValue(
mockNodeLocatorId as any
)
const result = store.executionIdToNodeLocatorId(123)
expect(workflowStore.nodeIdToNodeLocatorId).toHaveBeenCalledWith('123')
expect(result).toBe(mockNodeLocatorId)
})
it('should return null when conversion fails', () => {
vi.spyOn(workflowStore, 'hierarchicalIdToNodeLocatorId').mockReturnValue(
null
)
const result = store.executionIdToNodeLocatorId('123:456')
expect(result).toBeNull()
})
})
describe('nodeLocatorIdToExecutionId', () => {
it('should convert NodeLocatorId to hierarchical execution ID', () => {
const mockHierarchicalId = '123:456'
vi.spyOn(workflowStore, 'nodeLocatorIdToHierarchicalId').mockReturnValue(
mockHierarchicalId as any
)
const result = store.nodeLocatorIdToExecutionId(
'a1b2c3d4-e5f6-7890-abcd-ef1234567890:456'
)
expect(workflowStore.nodeLocatorIdToHierarchicalId).toHaveBeenCalledWith(
'a1b2c3d4-e5f6-7890-abcd-ef1234567890:456'
)
expect(result).toBe(mockHierarchicalId)
})
it('should return null when conversion fails', () => {
vi.spyOn(workflowStore, 'nodeLocatorIdToHierarchicalId').mockReturnValue(
null
)
const result = store.nodeLocatorIdToExecutionId('invalid:format')
expect(result).toBeNull()
})
})
})

View File

@@ -1,3 +1,4 @@
import type { Subgraph } from '@comfyorg/litegraph'
import { createPinia, setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { nextTick } from 'vue'
@@ -11,6 +12,7 @@ import {
useWorkflowBookmarkStore,
useWorkflowStore
} from '@/stores/workflowStore'
import { isSubgraph } from '@/utils/typeGuardUtil'
// Add mock for api at the top of the file
vi.mock('@/scripts/api', () => ({
@@ -26,10 +28,15 @@ vi.mock('@/scripts/api', () => ({
// Mock comfyApp globally for the store setup
vi.mock('@/scripts/app', () => ({
app: {
canvas: null // Start with canvas potentially undefined or null
canvas: {} // Start with empty canvas object
}
}))
// Mock isSubgraph
vi.mock('@/utils/typeGuardUtil', () => ({
isSubgraph: vi.fn(() => false)
}))
describe('useWorkflowStore', () => {
let store: ReturnType<typeof useWorkflowStore>
let bookmarkStore: ReturnType<typeof useWorkflowBookmarkStore>
@@ -569,4 +576,131 @@ describe('useWorkflowStore', () => {
expect(store.activeSubgraph).toBeUndefined()
})
})
describe('NodeLocatorId conversions', () => {
beforeEach(() => {
// Setup mock graph structure with subgraphs
const mockSubgraph = {
id: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
_nodes: []
}
const mockNode = {
id: 123,
isSubgraphNode: () => true,
subgraph: mockSubgraph
}
const mockRootGraph = {
_nodes: [mockNode],
subgraphs: new Map([[mockSubgraph.id, mockSubgraph]]),
getNodeById: (id: string | number) => {
if (String(id) === '123') return mockNode
return null
}
}
vi.mocked(comfyApp).graph = mockRootGraph as any
vi.mocked(comfyApp.canvas).subgraph = mockSubgraph as any
store.activeSubgraph = mockSubgraph as any
})
describe('nodeIdToNodeLocatorId', () => {
it('should convert node ID to NodeLocatorId for subgraph nodes', () => {
const result = store.nodeIdToNodeLocatorId(456)
expect(result).toBe('a1b2c3d4-e5f6-7890-abcd-ef1234567890:456')
})
it('should return simple node ID for root graph nodes', () => {
store.activeSubgraph = undefined
const result = store.nodeIdToNodeLocatorId(123)
expect(result).toBe('123')
})
it('should use provided subgraph instead of active one', () => {
const customSubgraph = {
id: 'custom-uuid-1234-5678-90ab-cdef12345678'
} as any
const result = store.nodeIdToNodeLocatorId(789, customSubgraph)
expect(result).toBe('custom-uuid-1234-5678-90ab-cdef12345678:789')
})
})
describe('hierarchicalIdToNodeLocatorId', () => {
it('should convert hierarchical ID to NodeLocatorId', () => {
const result = store.hierarchicalIdToNodeLocatorId('123:456')
expect(result).toBe('a1b2c3d4-e5f6-7890-abcd-ef1234567890:456')
})
it('should return simple node ID for root level nodes', () => {
const result = store.hierarchicalIdToNodeLocatorId('123')
expect(result).toBe('123')
})
it('should return null for invalid hierarchical IDs', () => {
const result = store.hierarchicalIdToNodeLocatorId('999:456')
expect(result).toBeNull()
})
})
describe('nodeLocatorIdToNodeId', () => {
it('should extract node ID from NodeLocatorId', () => {
const result = store.nodeLocatorIdToNodeId(
'a1b2c3d4-e5f6-7890-abcd-ef1234567890:456'
)
expect(result).toBe(456)
})
it('should handle string node IDs', () => {
const result = store.nodeLocatorIdToNodeId(
'a1b2c3d4-e5f6-7890-abcd-ef1234567890:node_1'
)
expect(result).toBe('node_1')
})
it('should handle simple node IDs (root graph)', () => {
const result = store.nodeLocatorIdToNodeId('123')
expect(result).toBe(123)
const stringResult = store.nodeLocatorIdToNodeId('node_1')
expect(stringResult).toBe('node_1')
})
it('should return null for invalid NodeLocatorId', () => {
const result = store.nodeLocatorIdToNodeId('invalid:format')
expect(result).toBeNull()
})
})
describe('nodeLocatorIdToHierarchicalId', () => {
it('should convert NodeLocatorId to hierarchical ID', () => {
// Need to mock isSubgraph to identify our mockSubgraph
vi.mocked(isSubgraph).mockImplementation((obj): obj is Subgraph => {
return obj === store.activeSubgraph
})
const result = store.nodeLocatorIdToHierarchicalId(
'a1b2c3d4-e5f6-7890-abcd-ef1234567890:456'
)
expect(result).toBe('123:456')
})
it('should handle simple node IDs (root graph)', () => {
const result = store.nodeLocatorIdToHierarchicalId('123')
expect(result).toBe('123')
})
it('should return null for unknown subgraph UUID', () => {
const result = store.nodeLocatorIdToHierarchicalId(
'unknown-uuid-1234-5678-90ab-cdef12345678:456'
)
expect(result).toBeNull()
})
it('should return null for invalid NodeLocatorId', () => {
const result = store.nodeLocatorIdToHierarchicalId('invalid:format')
expect(result).toBeNull()
})
})
})
})

View File

@@ -0,0 +1,207 @@
import { describe, expect, it } from 'vitest'
import type { NodeId } from '@/schemas/comfyWorkflowSchema'
import {
type NodeLocatorId,
createHierarchicalNodeId,
createNodeLocatorId,
isHierarchicalNodeId,
isNodeLocatorId,
parseHierarchicalNodeId,
parseNodeLocatorId
} from '@/types/nodeIdentification'
describe('nodeIdentification', () => {
describe('NodeLocatorId', () => {
const validUuid = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890'
const validNodeId = '123'
const validNodeLocatorId = `${validUuid}:${validNodeId}` as NodeLocatorId
describe('isNodeLocatorId', () => {
it('should return true for valid NodeLocatorId', () => {
expect(isNodeLocatorId(validNodeLocatorId)).toBe(true)
expect(isNodeLocatorId(`${validUuid}:456`)).toBe(true)
expect(isNodeLocatorId(`${validUuid}:node_1`)).toBe(true)
// Simple node IDs (root graph)
expect(isNodeLocatorId('123')).toBe(true)
expect(isNodeLocatorId('node_1')).toBe(true)
expect(isNodeLocatorId('5')).toBe(true)
})
it('should return false for invalid formats', () => {
expect(isNodeLocatorId('123:456')).toBe(false) // No UUID in first part
expect(isNodeLocatorId('not-a-uuid:123')).toBe(false)
expect(isNodeLocatorId('')).toBe(false) // Empty string
expect(isNodeLocatorId(':123')).toBe(false) // Empty UUID
expect(isNodeLocatorId(`${validUuid}:`)).toBe(false) // Empty node ID
expect(isNodeLocatorId(`${validUuid}:123:456`)).toBe(false) // Too many parts
expect(isNodeLocatorId(123)).toBe(false) // Not a string
expect(isNodeLocatorId(null)).toBe(false)
expect(isNodeLocatorId(undefined)).toBe(false)
})
it('should validate UUID format correctly', () => {
// Valid UUID formats
expect(
isNodeLocatorId('00000000-0000-0000-0000-000000000000:123')
).toBe(true)
expect(
isNodeLocatorId('A1B2C3D4-E5F6-7890-ABCD-EF1234567890:123')
).toBe(true)
// Invalid UUID formats
expect(isNodeLocatorId('00000000-0000-0000-0000-00000000000:123')).toBe(
false
) // Too short
expect(
isNodeLocatorId('00000000-0000-0000-0000-0000000000000:123')
).toBe(false) // Too long
expect(
isNodeLocatorId('00000000_0000_0000_0000_000000000000:123')
).toBe(false) // Wrong separator
expect(
isNodeLocatorId('g0000000-0000-0000-0000-000000000000:123')
).toBe(false) // Invalid hex
})
})
describe('parseNodeLocatorId', () => {
it('should parse valid NodeLocatorId', () => {
const result = parseNodeLocatorId(validNodeLocatorId)
expect(result).toEqual({
subgraphUuid: validUuid,
localNodeId: 123
})
})
it('should handle string node IDs', () => {
const stringNodeId = `${validUuid}:node_1`
const result = parseNodeLocatorId(stringNodeId)
expect(result).toEqual({
subgraphUuid: validUuid,
localNodeId: 'node_1'
})
})
it('should handle simple node IDs (root graph)', () => {
const result = parseNodeLocatorId('123')
expect(result).toEqual({
subgraphUuid: null,
localNodeId: 123
})
const stringResult = parseNodeLocatorId('node_1')
expect(stringResult).toEqual({
subgraphUuid: null,
localNodeId: 'node_1'
})
})
it('should return null for invalid formats', () => {
expect(parseNodeLocatorId('123:456')).toBeNull() // No UUID in first part
expect(parseNodeLocatorId('')).toBeNull()
})
})
describe('createNodeLocatorId', () => {
it('should create NodeLocatorId from components', () => {
const result = createNodeLocatorId(validUuid, 123)
expect(result).toBe(validNodeLocatorId)
expect(isNodeLocatorId(result)).toBe(true)
})
it('should handle string node IDs', () => {
const result = createNodeLocatorId(validUuid, 'node_1')
expect(result).toBe(`${validUuid}:node_1`)
expect(isNodeLocatorId(result)).toBe(true)
})
})
})
describe('HierarchicalNodeId', () => {
describe('isHierarchicalNodeId', () => {
it('should return true for hierarchical IDs', () => {
expect(isHierarchicalNodeId('123:456')).toBe(true)
expect(isHierarchicalNodeId('123:456:789')).toBe(true)
expect(isHierarchicalNodeId('node_1:node_2')).toBe(true)
})
it('should return false for non-hierarchical IDs', () => {
expect(isHierarchicalNodeId('123')).toBe(false)
expect(isHierarchicalNodeId('node_1')).toBe(false)
expect(isHierarchicalNodeId('')).toBe(false)
expect(isHierarchicalNodeId(123)).toBe(false)
expect(isHierarchicalNodeId(null)).toBe(false)
expect(isHierarchicalNodeId(undefined)).toBe(false)
})
})
describe('parseHierarchicalNodeId', () => {
it('should parse hierarchical IDs correctly', () => {
expect(parseHierarchicalNodeId('123:456')).toEqual([123, 456])
expect(parseHierarchicalNodeId('123:456:789')).toEqual([123, 456, 789])
expect(parseHierarchicalNodeId('node_1:node_2')).toEqual([
'node_1',
'node_2'
])
expect(parseHierarchicalNodeId('123:node_2:456')).toEqual([
123,
'node_2',
456
])
})
it('should return null for non-hierarchical IDs', () => {
expect(parseHierarchicalNodeId('123')).toBeNull()
expect(parseHierarchicalNodeId('')).toBeNull()
})
})
describe('createHierarchicalNodeId', () => {
it('should create hierarchical IDs from node arrays', () => {
expect(createHierarchicalNodeId([123, 456])).toBe('123:456')
expect(createHierarchicalNodeId([123, 456, 789])).toBe('123:456:789')
expect(createHierarchicalNodeId(['node_1', 'node_2'])).toBe(
'node_1:node_2'
)
expect(createHierarchicalNodeId([123, 'node_2', 456])).toBe(
'123:node_2:456'
)
})
it('should handle single node ID', () => {
const result = createHierarchicalNodeId([123])
expect(result).toBe('123')
// Single node IDs are not hierarchical
expect(isHierarchicalNodeId(result)).toBe(false)
})
it('should handle empty array', () => {
expect(createHierarchicalNodeId([])).toBe('')
})
})
})
describe('Integration tests', () => {
it('should round-trip NodeLocatorId correctly', () => {
const uuid = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890'
const nodeId: NodeId = 123
const locatorId = createNodeLocatorId(uuid, nodeId)
const parsed = parseNodeLocatorId(locatorId)
expect(parsed).toBeTruthy()
expect(parsed!.subgraphUuid).toBe(uuid)
expect(parsed!.localNodeId).toBe(nodeId)
})
it('should round-trip HierarchicalNodeId correctly', () => {
const nodeIds: NodeId[] = [123, 'node_2', 456]
const hierarchicalId = createHierarchicalNodeId(nodeIds)
const parsed = parseHierarchicalNodeId(hierarchicalId)
expect(parsed).toEqual(nodeIds)
})
})
})