Skip to content

Commit 9d67c50

Browse files
committed
test: cover queue models and execution-store slices
1 parent 690e0e3 commit 9d67c50

9 files changed

Lines changed: 1535 additions & 1 deletion

src/stores/executionErrorStore.test.ts

Lines changed: 476 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import { createPinia, setActivePinia } from 'pinia'
2+
import { beforeEach, describe, expect, it, vi } from 'vitest'
3+
import { ref } from 'vue'
4+
5+
import type { ComfyWorkflow } from '@/platform/workflow/management/stores/workflowStore'
6+
import type { ComfyApiWorkflow } from '@/platform/workflow/validation/schemas/workflowSchema'
7+
import { useExecutionStore } from '@/stores/executionStore'
8+
9+
const { handlers, openSet } = vi.hoisted(() => ({
10+
handlers: {} as Record<string, (e: { detail: unknown }) => void>,
11+
openSet: new Set<unknown>()
12+
}))
13+
14+
vi.mock('@/scripts/app', () => ({ app: { rootGraph: {} } }))
15+
16+
vi.mock('@/scripts/api', () => ({
17+
api: {
18+
addEventListener: (name: string, fn: (e: { detail: unknown }) => void) => {
19+
handlers[name] = fn
20+
},
21+
removeEventListener: () => {}
22+
}
23+
}))
24+
25+
vi.mock('@/platform/workflow/management/stores/workflowStore', () => ({
26+
useWorkflowStore: () => ({
27+
isOpen: (workflow: unknown) => openSet.has(workflow),
28+
openWorkflows: [],
29+
nodeLocatorIdToNodeExecutionId: () => null
30+
})
31+
}))
32+
33+
vi.mock('@/renderer/core/canvas/canvasStore', () => ({
34+
useCanvasStore: () => ({ canvas: undefined })
35+
}))
36+
37+
vi.mock('@/stores/executionErrorStore', () => ({
38+
useExecutionErrorStore: () => ({
39+
clearExecutionStartErrors: () => {},
40+
clearPromptError: () => {}
41+
})
42+
}))
43+
44+
vi.mock('@/composables/useAppMode', () => ({
45+
useAppMode: () => ({ mode: ref('default'), isAppMode: ref(false) })
46+
}))
47+
48+
vi.mock('@/platform/telemetry', () => ({ useTelemetry: () => undefined }))
49+
50+
vi.mock('@/utils/appMode', () => ({
51+
getWorkflowMode: () => 'workflow',
52+
isAppModeValue: () => false
53+
}))
54+
55+
function workflow(path: string): ComfyWorkflow {
56+
return { path } as unknown as ComfyWorkflow
57+
}
58+
59+
function setup() {
60+
const store = useExecutionStore()
61+
store.bindExecutionEvents()
62+
return store
63+
}
64+
65+
function promptOutput(): ComfyApiWorkflow {
66+
return {}
67+
}
68+
69+
function startJob(
70+
store: ReturnType<typeof useExecutionStore>,
71+
id: string,
72+
wf: ComfyWorkflow,
73+
nodes: string[] = []
74+
) {
75+
openSet.add(wf)
76+
store.storeJob({ nodes, id, promptOutput: promptOutput(), workflow: wf })
77+
handlers['execution_start']?.({ detail: { prompt_id: id } })
78+
}
79+
80+
beforeEach(() => {
81+
setActivePinia(createPinia())
82+
for (const key of Object.keys(handlers)) delete handlers[key]
83+
openSet.clear()
84+
})
85+
86+
describe('executionStore interrupt and cached', () => {
87+
it('drops the workflow badge and goes idle on interruption', () => {
88+
const store = setup()
89+
const wf = workflow('a.json')
90+
startJob(store, 'job-1', wf)
91+
expect(store.getWorkflowStatus(wf)).toBe('running')
92+
93+
handlers['execution_interrupted']?.({ detail: { prompt_id: 'job-1' } })
94+
95+
expect(store.getWorkflowStatus(wf)).toBeUndefined()
96+
expect(store.isIdle).toBe(true)
97+
})
98+
99+
it('ends the active job when executing resolves to null', () => {
100+
const store = setup()
101+
startJob(store, 'job-2', workflow('b.json'))
102+
expect(store.isIdle).toBe(false)
103+
104+
handlers['executing']?.({ detail: null })
105+
106+
expect(store.isIdle).toBe(true)
107+
})
108+
109+
it('marks cached nodes as executed', () => {
110+
const store = setup()
111+
startJob(store, 'job-3', workflow('c.json'), ['a', 'b', 'c'])
112+
expect(store.nodesExecuted).toBe(0)
113+
114+
handlers['execution_cached']?.({
115+
detail: { prompt_id: 'job-3', nodes: ['a', 'b'] }
116+
})
117+
118+
expect(store.nodesExecuted).toBe(2)
119+
})
120+
})
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import { createPinia, setActivePinia } from 'pinia'
2+
import { beforeEach, describe, expect, it, vi } from 'vitest'
3+
import { ref } from 'vue'
4+
5+
import type { ComfyWorkflow } from '@/platform/workflow/management/stores/workflowStore'
6+
import type { ComfyApiWorkflow } from '@/platform/workflow/validation/schemas/workflowSchema'
7+
import { useExecutionStore } from '@/stores/executionStore'
8+
9+
const { handlers } = vi.hoisted(() => ({
10+
handlers: {} as Record<string, (e: { detail: unknown }) => void>
11+
}))
12+
13+
vi.mock('@/scripts/app', () => ({ app: { rootGraph: {} } }))
14+
15+
vi.mock('@/scripts/api', () => ({
16+
api: {
17+
addEventListener: (name: string, fn: (e: { detail: unknown }) => void) => {
18+
handlers[name] = fn
19+
},
20+
removeEventListener: () => {}
21+
}
22+
}))
23+
24+
vi.mock('@/platform/workflow/management/stores/workflowStore', () => ({
25+
useWorkflowStore: () => ({
26+
isOpen: () => false,
27+
openWorkflows: [],
28+
nodeLocatorIdToNodeExecutionId: () => null
29+
})
30+
}))
31+
32+
vi.mock('@/renderer/core/canvas/canvasStore', () => ({
33+
useCanvasStore: () => ({ canvas: undefined })
34+
}))
35+
36+
vi.mock('@/stores/executionErrorStore', () => ({
37+
useExecutionErrorStore: () => ({
38+
clearExecutionStartErrors: () => {},
39+
clearPromptError: () => {}
40+
})
41+
}))
42+
43+
vi.mock('@/composables/useAppMode', () => ({
44+
useAppMode: () => ({ mode: ref('default'), isAppMode: ref(false) })
45+
}))
46+
47+
vi.mock('@/platform/telemetry', () => ({ useTelemetry: () => undefined }))
48+
49+
vi.mock('@/utils/appMode', () => ({
50+
getWorkflowMode: () => 'workflow',
51+
isAppModeValue: () => false
52+
}))
53+
54+
function setup() {
55+
const store = useExecutionStore()
56+
store.bindExecutionEvents()
57+
return store
58+
}
59+
60+
function promptOutput(): ComfyApiWorkflow {
61+
return {}
62+
}
63+
64+
function startJob(
65+
store: ReturnType<typeof useExecutionStore>,
66+
id: string,
67+
nodes: string[]
68+
) {
69+
store.storeJob({
70+
nodes,
71+
id,
72+
promptOutput: promptOutput(),
73+
workflow: { path: `${id}.json` } as unknown as ComfyWorkflow
74+
})
75+
handlers['execution_start']?.({ detail: { prompt_id: id } })
76+
}
77+
78+
beforeEach(() => {
79+
setActivePinia(createPinia())
80+
for (const key of Object.keys(handlers)) delete handlers[key]
81+
})
82+
83+
describe('executionStore execution lifecycle', () => {
84+
it('reports zero progress while idle', () => {
85+
const store = setup()
86+
expect(store.totalNodesToExecute).toBe(0)
87+
expect(store.nodesExecuted).toBe(0)
88+
expect(store.executionProgress).toBe(0)
89+
})
90+
91+
it('counts the queued nodes once a job starts', () => {
92+
const store = setup()
93+
startJob(store, 'job-1', ['a', 'b', 'c'])
94+
95+
expect(store.totalNodesToExecute).toBe(3)
96+
expect(store.nodesExecuted).toBe(0)
97+
expect(store.executionProgress).toBe(0)
98+
})
99+
100+
it('advances progress as executed events arrive', () => {
101+
const store = setup()
102+
startJob(store, 'job-1', ['a', 'b', 'c'])
103+
104+
handlers['executed']?.({ detail: { node: 'a' } })
105+
expect(store.nodesExecuted).toBe(1)
106+
expect(store.executionProgress).toBeCloseTo(1 / 3)
107+
108+
handlers['executed']?.({ detail: { node: 'b' } })
109+
handlers['executed']?.({ detail: { node: 'c' } })
110+
expect(store.nodesExecuted).toBe(3)
111+
expect(store.executionProgress).toBe(1)
112+
})
113+
114+
it('ignores executed events when there is no active job', () => {
115+
const store = setup()
116+
handlers['executed']?.({ detail: { node: 'a' } })
117+
expect(store.nodesExecuted).toBe(0)
118+
})
119+
})
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import { createPinia, setActivePinia } from 'pinia'
2+
import { beforeEach, describe, expect, it, vi } from 'vitest'
3+
import { ref } from 'vue'
4+
5+
import type { NodeProgressState } from '@/schemas/apiSchema'
6+
import { useExecutionStore } from '@/stores/executionStore'
7+
8+
const { handlers } = vi.hoisted(() => ({
9+
handlers: {} as Record<string, (e: { detail: unknown }) => void>
10+
}))
11+
12+
vi.mock('@/scripts/app', () => ({ app: { rootGraph: {} } }))
13+
14+
vi.mock('@/scripts/api', () => ({
15+
api: {
16+
addEventListener: (name: string, fn: (e: { detail: unknown }) => void) => {
17+
handlers[name] = fn
18+
},
19+
removeEventListener: () => {}
20+
}
21+
}))
22+
23+
vi.mock('@/platform/workflow/management/stores/workflowStore', () => ({
24+
useWorkflowStore: () => ({
25+
isOpen: () => false,
26+
openWorkflows: [],
27+
nodeLocatorIdToNodeExecutionId: () => null
28+
})
29+
}))
30+
31+
vi.mock('@/renderer/core/canvas/canvasStore', () => ({
32+
useCanvasStore: () => ({ canvas: undefined })
33+
}))
34+
35+
vi.mock('@/stores/executionErrorStore', () => ({
36+
useExecutionErrorStore: () => ({
37+
clearExecutionStartErrors: () => {},
38+
clearPromptError: () => {}
39+
})
40+
}))
41+
42+
vi.mock('@/stores/nodeOutputStore', () => ({
43+
useNodeOutputStore: () => ({ revokePreviewsByExecutionId: () => {} })
44+
}))
45+
46+
vi.mock('@/composables/useAppMode', () => ({
47+
useAppMode: () => ({ mode: ref('default'), isAppMode: ref(false) })
48+
}))
49+
50+
vi.mock('@/platform/telemetry', () => ({ useTelemetry: () => undefined }))
51+
52+
vi.mock('@/utils/appMode', () => ({
53+
getWorkflowMode: () => 'workflow',
54+
isAppModeValue: () => false
55+
}))
56+
57+
function progressState(
58+
jobId: string,
59+
nodes: Record<string, Partial<NodeProgressState>>
60+
) {
61+
handlers['progress_state']?.({ detail: { prompt_id: jobId, nodes } })
62+
}
63+
64+
function setup() {
65+
const store = useExecutionStore()
66+
store.bindExecutionEvents()
67+
return store
68+
}
69+
70+
beforeEach(() => {
71+
setActivePinia(createPinia())
72+
for (const key of Object.keys(handlers)) delete handlers[key]
73+
})
74+
75+
describe('executionStore node progress', () => {
76+
it('is idle until an execution starts', () => {
77+
const store = setup()
78+
expect(store.isIdle).toBe(true)
79+
80+
handlers['execution_start']?.({ detail: { prompt_id: 'job-1' } })
81+
expect(store.isIdle).toBe(false)
82+
})
83+
84+
it('derives the running node ids from a progress_state event', () => {
85+
const store = setup()
86+
87+
progressState('job-1', {
88+
n1: { state: 'running', value: 1, max: 4 },
89+
n2: { state: 'finished' },
90+
n3: { state: 'pending' }
91+
})
92+
93+
expect(store.executingNodeIds).toEqual(['n1'])
94+
expect(store.executingNodeId).toBe('n1')
95+
})
96+
97+
it('exposes fractional progress for the executing node', () => {
98+
const store = setup()
99+
100+
progressState('job-1', {
101+
n1: { state: 'running', value: 1, max: 4 }
102+
})
103+
104+
expect(store.executingNodeProgress).toBe(0.25)
105+
})
106+
107+
it('reports no executing node when none are running', () => {
108+
const store = setup()
109+
110+
progressState('job-1', {
111+
n1: { state: 'finished' },
112+
n2: { state: 'pending' }
113+
})
114+
115+
expect(store.executingNodeIds).toEqual([])
116+
expect(store.executingNodeId).toBeNull()
117+
})
118+
119+
it('replaces progress state on each progress_state event', () => {
120+
const store = setup()
121+
122+
progressState('job-1', { n1: { state: 'running', value: 1, max: 4 } })
123+
expect(store.executingNodeId).toBe('n1')
124+
125+
progressState('job-1', { n2: { state: 'running', value: 2, max: 2 } })
126+
expect(store.executingNodeIds).toEqual(['n2'])
127+
})
128+
})

0 commit comments

Comments
 (0)