mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-20 06:20:11 +00:00
Merge branch 'main' into deepme987/feat/missing-node-telemetry
This commit is contained in:
@@ -1312,4 +1312,96 @@ describe('linearOutputStore', () => {
|
||||
).toHaveLength(2)
|
||||
})
|
||||
})
|
||||
|
||||
describe('deferred path mapping (WebSocket/HTTP race)', () => {
|
||||
it('starts tracking when path mapping arrives after activeJobId', async () => {
|
||||
const { nextTick } = await import('vue')
|
||||
const store = useLinearOutputStore()
|
||||
|
||||
// activeJobId set before path mapping exists (WebSocket race)
|
||||
activeJobIdRef.value = 'job-1'
|
||||
await nextTick()
|
||||
|
||||
// No skeleton yet — path mapping is missing
|
||||
expect(store.inProgressItems).toHaveLength(0)
|
||||
|
||||
// Path mapping arrives (HTTP response from queuePrompt)
|
||||
setJobWorkflowPath('job-1', 'workflows/test-workflow.json')
|
||||
await nextTick()
|
||||
|
||||
// Now onJobStart should have fired
|
||||
expect(store.inProgressItems).toHaveLength(1)
|
||||
expect(store.inProgressItems[0].state).toBe('skeleton')
|
||||
expect(store.inProgressItems[0].jobId).toBe('job-1')
|
||||
})
|
||||
|
||||
it('processes executed events after deferred start', async () => {
|
||||
const { nextTick } = await import('vue')
|
||||
const store = useLinearOutputStore()
|
||||
|
||||
activeJobIdRef.value = 'job-1'
|
||||
await nextTick()
|
||||
|
||||
setJobWorkflowPath('job-1', 'workflows/test-workflow.json')
|
||||
await nextTick()
|
||||
|
||||
// Executed event arrives — should create an image item
|
||||
store.onNodeExecuted('job-1', makeExecutedDetail('job-1'))
|
||||
|
||||
const imageItems = store.inProgressItems.filter(
|
||||
(i) => i.state === 'image'
|
||||
)
|
||||
expect(imageItems).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('does not double-start if path mapping is already available', async () => {
|
||||
const { nextTick } = await import('vue')
|
||||
const store = useLinearOutputStore()
|
||||
|
||||
// Path mapping set before activeJobId (normal case, no race)
|
||||
setJobWorkflowPath('job-1', 'workflows/test-workflow.json')
|
||||
activeJobIdRef.value = 'job-1'
|
||||
await nextTick()
|
||||
|
||||
expect(store.inProgressItems).toHaveLength(1)
|
||||
|
||||
// Trigger path mapping update again — should not create a second skeleton
|
||||
setJobWorkflowPath('job-1', 'workflows/test-workflow.json')
|
||||
await nextTick()
|
||||
|
||||
expect(store.inProgressItems).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('ignores deferred mapping if activeJobId changed', async () => {
|
||||
const { nextTick } = await import('vue')
|
||||
const store = useLinearOutputStore()
|
||||
|
||||
activeJobIdRef.value = 'job-1'
|
||||
await nextTick()
|
||||
|
||||
// Job changes before path mapping arrives
|
||||
activeJobIdRef.value = null
|
||||
await nextTick()
|
||||
|
||||
setJobWorkflowPath('job-1', 'workflows/test-workflow.json')
|
||||
await nextTick()
|
||||
|
||||
// Should not have started job-1
|
||||
expect(store.inProgressItems).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('ignores deferred mapping for a different workflow', async () => {
|
||||
const { nextTick } = await import('vue')
|
||||
const store = useLinearOutputStore()
|
||||
|
||||
activeJobIdRef.value = 'job-1'
|
||||
await nextTick()
|
||||
|
||||
// Path maps to a different workflow than the active one
|
||||
setJobWorkflowPath('job-1', 'workflows/other-workflow.json')
|
||||
await nextTick()
|
||||
|
||||
expect(store.inProgressItems).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -262,17 +262,27 @@ export const useLinearOutputStore = defineStore('linearOutput', () => {
|
||||
onNodeExecuted(jobId, detail)
|
||||
}
|
||||
|
||||
// Watch both activeJobId and the path mapping together. The path mapping
|
||||
// may arrive after activeJobId due to a race between WebSocket
|
||||
// (execution_start) and the HTTP response (queuePrompt > storeJob).
|
||||
// Watching both ensures onJobStart fires once the mapping is available.
|
||||
watch(
|
||||
() => executionStore.activeJobId,
|
||||
(jobId, oldJobId) => {
|
||||
[
|
||||
() => executionStore.activeJobId,
|
||||
() => executionStore.jobIdToSessionWorkflowPath
|
||||
],
|
||||
([jobId], [oldJobId]) => {
|
||||
if (!isAppMode.value) return
|
||||
if (oldJobId && oldJobId !== jobId) {
|
||||
onJobComplete(oldJobId)
|
||||
}
|
||||
// Start tracking only if the job belongs to this workflow.
|
||||
// Jobs from other workflows are picked up by reconcileOnEnter
|
||||
// when the user switches to that workflow's tab.
|
||||
if (jobId && isJobForActiveWorkflow(jobId)) {
|
||||
// Guard with trackedJobId to avoid double-starting when the
|
||||
// path mapping arrives after activeJobId was already set.
|
||||
if (
|
||||
jobId &&
|
||||
trackedJobId.value !== jobId &&
|
||||
isJobForActiveWorkflow(jobId)
|
||||
) {
|
||||
onJobStart(jobId)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,6 +178,71 @@ describe('useModelToNodeStore', () => {
|
||||
).toBeUndefined()
|
||||
})
|
||||
|
||||
describe('progressive hierarchical fallback', () => {
|
||||
it('should resolve 1-level path via exact match', () => {
|
||||
const modelToNodeStore = useModelToNodeStore()
|
||||
modelToNodeStore.quickRegister('level1', 'UNETLoader', 'key1')
|
||||
|
||||
const provider = modelToNodeStore.getNodeProvider('level1')
|
||||
expect(provider?.nodeDef?.name).toBe('UNETLoader')
|
||||
})
|
||||
|
||||
it('should resolve 2-level path to registered parent', () => {
|
||||
const modelToNodeStore = useModelToNodeStore()
|
||||
modelToNodeStore.quickRegister('level1', 'UNETLoader', 'key1')
|
||||
|
||||
const provider = modelToNodeStore.getNodeProvider('level1/child')
|
||||
expect(provider?.nodeDef?.name).toBe('UNETLoader')
|
||||
})
|
||||
|
||||
it('should resolve 3-level path to nearest registered ancestor', () => {
|
||||
const modelToNodeStore = useModelToNodeStore()
|
||||
modelToNodeStore.quickRegister('level1', 'UNETLoader', 'key1')
|
||||
modelToNodeStore.quickRegister('level1/level2', 'VAELoader', 'key2')
|
||||
|
||||
// 3 levels: should match level1/level2 (nearest), not level1
|
||||
const provider = modelToNodeStore.getNodeProvider('level1/level2/child')
|
||||
expect(provider?.nodeDef?.name).toBe('VAELoader')
|
||||
})
|
||||
|
||||
it('should resolve 4-level path to nearest registered ancestor', () => {
|
||||
const modelToNodeStore = useModelToNodeStore()
|
||||
modelToNodeStore.quickRegister('a', 'UNETLoader', 'k1')
|
||||
modelToNodeStore.quickRegister('a/b', 'VAELoader', 'k2')
|
||||
modelToNodeStore.quickRegister('a/b/c', 'StyleModelLoader', 'k3')
|
||||
|
||||
// 4 levels: should match a/b/c (nearest), not a/b or a
|
||||
const provider = modelToNodeStore.getNodeProvider('a/b/c/d')
|
||||
expect(provider?.nodeDef?.name).toBe('StyleModelLoader')
|
||||
})
|
||||
|
||||
it('should skip intermediate unregistered levels', () => {
|
||||
const modelToNodeStore = useModelToNodeStore()
|
||||
modelToNodeStore.quickRegister('a', 'UNETLoader', 'k1')
|
||||
// a/b is NOT registered
|
||||
|
||||
// 3 levels: a/b not found, falls back to a
|
||||
const provider = modelToNodeStore.getNodeProvider('a/b/c')
|
||||
expect(provider?.nodeDef?.name).toBe('UNETLoader')
|
||||
})
|
||||
|
||||
it('should prefer exact match over any fallback', () => {
|
||||
const modelToNodeStore = useModelToNodeStore()
|
||||
modelToNodeStore.quickRegister('a', 'UNETLoader', 'k1')
|
||||
modelToNodeStore.quickRegister('a/b/c', 'VAELoader', 'k2')
|
||||
|
||||
const provider = modelToNodeStore.getNodeProvider('a/b/c')
|
||||
expect(provider?.nodeDef?.name).toBe('VAELoader')
|
||||
})
|
||||
|
||||
it('should return undefined when no ancestor is registered', () => {
|
||||
const modelToNodeStore = useModelToNodeStore()
|
||||
modelToNodeStore.quickRegister('x', 'UNETLoader', 'k1')
|
||||
|
||||
expect(modelToNodeStore.getNodeProvider('y/z/w')).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
it('should return provider for chatterbox nodes with empty key', () => {
|
||||
const modelToNodeStore = useModelToNodeStore()
|
||||
modelToNodeStore.registerDefaults()
|
||||
|
||||
@@ -75,8 +75,8 @@ export const useModelToNodeStore = defineStore('modelToNode', () => {
|
||||
|
||||
/**
|
||||
* Find providers for modelType with hierarchical fallback.
|
||||
* Tries exact match first, then falls back to top-level segment (e.g., "parent/child" → "parent").
|
||||
* Note: Only falls back one level; "a/b/c" tries "a/b/c" then "a", not "a/b".
|
||||
* Tries exact match first, then progressively shorter parent paths.
|
||||
* e.g., "a/b/c" tries "a/b/c" → "a/b" → "a".
|
||||
*/
|
||||
function findProvidersWithFallback(
|
||||
modelType: string
|
||||
@@ -85,15 +85,12 @@ export const useModelToNodeStore = defineStore('modelToNode', () => {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const exactMatch = modelToNodeMap.value[modelType]
|
||||
if (exactMatch && exactMatch.length > 0) return exactMatch
|
||||
|
||||
const topLevel = modelType.split('/')[0]
|
||||
if (topLevel === modelType) return undefined
|
||||
|
||||
const fallback = modelToNodeMap.value[topLevel]
|
||||
|
||||
if (fallback && fallback.length > 0) return fallback
|
||||
const segments = modelType.split('/')
|
||||
for (let i = segments.length; i >= 1; i--) {
|
||||
const path = segments.slice(0, i).join('/')
|
||||
const providers = modelToNodeMap.value[path]
|
||||
if (providers && providers.length > 0) return providers
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
@@ -361,10 +358,11 @@ export const useModelToNodeStore = defineStore('modelToNode', () => {
|
||||
|
||||
// CogVideoX models (comfyui-cogvideoxwrapper)
|
||||
quickRegister('CogVideo/GGUF', 'DownloadAndLoadCogVideoGGUFModel', 'model')
|
||||
// Empty key: HF-download node — don't activate asset browser for the combo widget
|
||||
quickRegister(
|
||||
'CogVideo/ControlNet',
|
||||
'DownloadAndLoadCogVideoControlNet',
|
||||
'model'
|
||||
''
|
||||
)
|
||||
|
||||
// DynamiCrafter models (ComfyUI-DynamiCrafterWrapper)
|
||||
@@ -386,7 +384,8 @@ export const useModelToNodeStore = defineStore('modelToNode', () => {
|
||||
quickRegister('lama', 'LaMa', 'lama_model')
|
||||
|
||||
// CogVideoX video generation models (comfyui-cogvideoxwrapper)
|
||||
quickRegister('CogVideo', 'DownloadAndLoadCogVideoModel', 'model')
|
||||
// Empty key: HF-download node — don't activate asset browser for the combo widget
|
||||
quickRegister('CogVideo', 'DownloadAndLoadCogVideoModel', '')
|
||||
|
||||
// Inpaint models (comfyui-inpaint-nodes)
|
||||
quickRegister('inpaint', 'INPAINT_LoadInpaintModel', 'model_name')
|
||||
|
||||
Reference in New Issue
Block a user