diff --git a/browser_tests/fixtures/ComfyPage.ts b/browser_tests/fixtures/ComfyPage.ts index 08cdd5ccea..81b482284d 100644 --- a/browser_tests/fixtures/ComfyPage.ts +++ b/browser_tests/fixtures/ComfyPage.ts @@ -21,7 +21,6 @@ import { import { Topbar } from './components/Topbar' import type { Position, Size } from './types' import { NodeReference, SubgraphSlotReference } from './utils/litegraphUtils' -import TaskHistory from './utils/taskHistory' dotenv.config() @@ -146,8 +145,6 @@ class ConfirmDialog { } export class ComfyPage { - private _history: TaskHistory | null = null - public readonly url: string // All canvas position operations are based on default view of canvas. public readonly canvas: Locator @@ -301,11 +298,6 @@ export class ComfyPage { } } - setupHistory(): TaskHistory { - this._history ??= new TaskHistory(this) - return this._history - } - async setup({ clearStorage = true, mockReleases = true diff --git a/src/components/queue/QueueProgressOverlay.vue b/src/components/queue/QueueProgressOverlay.vue index 78efb7a789..d6b3edcd8d 100644 --- a/src/components/queue/QueueProgressOverlay.vue +++ b/src/components/queue/QueueProgressOverlay.vue @@ -262,7 +262,7 @@ const focusAssetInSidebar = async (item: JobListItem) => { const inspectJobAsset = wrapWithErrorHandlingAsync( async (item: JobListItem) => { - openResultGallery(item) + await openResultGallery(item) await focusAssetInSidebar(item) } ) diff --git a/src/components/queue/job/JobDetailsPopover.stories.ts b/src/components/queue/job/JobDetailsPopover.stories.ts index 2343a8fa22..9b79c99e50 100644 --- a/src/components/queue/job/JobDetailsPopover.stories.ts +++ b/src/components/queue/job/JobDetailsPopover.stories.ts @@ -1,6 +1,9 @@ import type { Meta, StoryObj } from '@storybook/vue3-vite' -import type { TaskStatus } from '@/schemas/apiSchema' +import type { + JobListItem, + JobStatus +} from '@/platform/remote/comfyui/jobs/jobTypes' import { useExecutionStore } from '@/stores/executionStore' import { TaskItemImpl, useQueueStore } from '@/stores/queueStore' @@ -37,91 +40,86 @@ function resetStores() { exec.nodeProgressStatesByPrompt = {} } +function makeTask( + id: string, + priority: number, + fields: Partial & { status: JobStatus; create_time: number } +): TaskItemImpl { + const job: JobListItem = { + id, + priority, + last_state_update: null, + update_time: fields.create_time, + ...fields + } + return new TaskItemImpl(job) +} + function makePendingTask( id: string, - index: number, - createTimeMs?: number + priority: number, + createTimeMs: number ): TaskItemImpl { - const extraData = { - client_id: 'c1', - ...(typeof createTimeMs === 'number' ? { create_time: createTimeMs } : {}) - } - return new TaskItemImpl('Pending', [index, id, {}, extraData, []]) + return makeTask(id, priority, { + status: 'pending', + create_time: createTimeMs + }) } function makeRunningTask( id: string, - index: number, - createTimeMs?: number + priority: number, + createTimeMs: number ): TaskItemImpl { - const extraData = { - client_id: 'c1', - ...(typeof createTimeMs === 'number' ? { create_time: createTimeMs } : {}) - } - return new TaskItemImpl('Running', [index, id, {}, extraData, []]) + return makeTask(id, priority, { + status: 'in_progress', + create_time: createTimeMs + }) } function makeRunningTaskWithStart( id: string, - index: number, + priority: number, startedSecondsAgo: number ): TaskItemImpl { const start = Date.now() - startedSecondsAgo * 1000 - const status: TaskStatus = { - status_str: 'success', - completed: false, - messages: [['execution_start', { prompt_id: id, timestamp: start } as any]] - } - return new TaskItemImpl( - 'Running', - [index, id, {}, { client_id: 'c1', create_time: start - 5000 }, []], - status - ) + return makeTask(id, priority, { + status: 'in_progress', + create_time: start - 5000, + update_time: start + }) } function makeHistoryTask( id: string, - index: number, + priority: number, durationSec: number, ok: boolean, errorMessage?: string ): TaskItemImpl { - const start = Date.now() - durationSec * 1000 - 1000 - const end = start + durationSec * 1000 - const messages: TaskStatus['messages'] = ok - ? [ - ['execution_start', { prompt_id: id, timestamp: start } as any], - ['execution_success', { prompt_id: id, timestamp: end } as any] - ] - : [ - ['execution_start', { prompt_id: id, timestamp: start } as any], - [ - 'execution_error', - { - prompt_id: id, - timestamp: end, - node_id: '1', - node_type: 'Node', - executed: [], - exception_message: - errorMessage || 'Demo error: Node failed during execution', - exception_type: 'RuntimeError', - traceback: [], - current_inputs: {}, - current_outputs: {} - } as any - ] - ] - const status: TaskStatus = { - status_str: ok ? 'success' : 'error', - completed: true, - messages - } - return new TaskItemImpl( - 'History', - [index, id, {}, { client_id: 'c1', create_time: start }, []], - status - ) + const now = Date.now() + const executionEndTime = now + const executionStartTime = now - durationSec * 1000 + return makeTask(id, priority, { + status: ok ? 'completed' : 'failed', + create_time: executionStartTime - 5000, + update_time: now, + execution_start_time: executionStartTime, + execution_end_time: executionEndTime, + execution_error: errorMessage + ? { + prompt_id: id, + timestamp: now, + node_id: '1', + node_type: 'ExampleNode', + exception_message: errorMessage, + exception_type: 'RuntimeError', + traceback: [], + current_inputs: {}, + current_outputs: {} + } + : undefined + }) } export const Queued: Story = { @@ -140,8 +138,12 @@ export const Queued: Story = { makePendingTask(jobId, queueIndex, Date.now() - 90_000) ] // Add some other pending jobs to give context - queue.pendingTasks.push(makePendingTask('job-older-1', 100)) - queue.pendingTasks.push(makePendingTask('job-older-2', 101)) + queue.pendingTasks.push( + makePendingTask('job-older-1', 100, Date.now() - 60_000) + ) + queue.pendingTasks.push( + makePendingTask('job-older-2', 101, Date.now() - 30_000) + ) // Queued at (in metadata on prompt[4]) diff --git a/src/components/queue/job/JobGroupsList.vue b/src/components/queue/job/JobGroupsList.vue index 482016d346..3a7c6d6af9 100644 --- a/src/components/queue/job/JobGroupsList.vue +++ b/src/components/queue/job/JobGroupsList.vue @@ -12,7 +12,7 @@ v-for="ji in group.items" :key="ji.id" :job-id="ji.id" - :workflow-id="ji.taskRef?.workflow?.id" + :workflow-id="ji.taskRef?.workflowId" :state="ji.state" :title="ji.title" :right-text="ji.meta" diff --git a/src/components/queue/job/useJobErrorReporting.test.ts b/src/components/queue/job/useJobErrorReporting.test.ts index f6d0f8d360..8c8ac751ec 100644 --- a/src/components/queue/job/useJobErrorReporting.test.ts +++ b/src/components/queue/job/useJobErrorReporting.test.ts @@ -2,64 +2,43 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { computed, ref } from 'vue' import type { ComputedRef } from 'vue' -import type { ExecutionErrorWsMessage, TaskStatus } from '@/schemas/apiSchema' -import { TaskItemImpl } from '@/stores/queueStore' +import type { TaskItemImpl } from '@/stores/queueStore' import type { JobErrorDialogService } from '@/components/queue/job/useJobErrorReporting' import { useJobErrorReporting } from '@/components/queue/job/useJobErrorReporting' +import type { ExecutionError } from '@/platform/remote/comfyui/jobs/jobTypes' -const createExecutionErrorMessage = ( - overrides: Partial = {} -): ExecutionErrorWsMessage => ({ - prompt_id: 'prompt', - timestamp: 100, - node_id: 'node-1', - node_type: 'KSampler', - executed: [], - exception_message: 'default failure', - exception_type: 'RuntimeError', - traceback: ['Trace line'], - current_inputs: {}, - current_outputs: {}, - ...overrides -}) - -/** - * Creates a real TaskItemImpl with the given status messages. - * Uses the actual TaskItemImpl class to test the real errorMessage/executionError getters. - */ -function createTaskWithMessages( - messages: TaskStatus['messages'] = [] -): TaskItemImpl { - const status: TaskStatus = { - status_str: 'error', - completed: false, - messages - } - return new TaskItemImpl( - 'History', - [0, 'test-prompt-id', {}, { client_id: 'test-client' }, []], - status - ) -} +const createTaskWithError = ( + promptId: string, + errorMessage?: string, + executionError?: ExecutionError, + createTime?: number +): TaskItemImpl => + ({ + promptId, + errorMessage, + executionError, + createTime: createTime ?? Date.now() + }) as unknown as TaskItemImpl describe('useJobErrorReporting', () => { let taskState = ref(null) let taskForJob: ComputedRef let copyToClipboard: ReturnType - let showExecutionErrorDialog: ReturnType let showErrorDialog: ReturnType + let showExecutionErrorDialog: ReturnType let dialog: JobErrorDialogService let composable: ReturnType beforeEach(() => { + vi.clearAllMocks() taskState = ref(null) taskForJob = computed(() => taskState.value) copyToClipboard = vi.fn() - showExecutionErrorDialog = vi.fn() showErrorDialog = vi.fn() + showExecutionErrorDialog = vi.fn() dialog = { - showExecutionErrorDialog, - showErrorDialog + showErrorDialog, + showExecutionErrorDialog } as unknown as JobErrorDialogService composable = useJobErrorReporting({ taskForJob, @@ -75,81 +54,112 @@ describe('useJobErrorReporting', () => { }) it('exposes a computed message that reflects the current task error', () => { - taskState.value = createTaskWithMessages([ - [ - 'execution_error', - createExecutionErrorMessage({ exception_message: 'First failure' }) - ] - ]) + taskState.value = createTaskWithError('job-1', 'First failure') expect(composable.errorMessageValue.value).toBe('First failure') - taskState.value = createTaskWithMessages([ - [ - 'execution_error', - createExecutionErrorMessage({ exception_message: 'Second failure' }) - ] - ]) + taskState.value = createTaskWithError('job-2', 'Second failure') expect(composable.errorMessageValue.value).toBe('Second failure') }) - it('returns empty string when task has no error', () => { - taskState.value = null + it('returns empty string when no error message', () => { + taskState.value = createTaskWithError('job-1') expect(composable.errorMessageValue.value).toBe('') + }) - taskState.value = createTaskWithMessages([]) + it('returns empty string when task is null', () => { + taskState.value = null expect(composable.errorMessageValue.value).toBe('') }) it('only calls the copy handler when a message exists', () => { - taskState.value = createTaskWithMessages([ - [ - 'execution_error', - createExecutionErrorMessage({ exception_message: 'Clipboard failure' }) - ] - ]) + taskState.value = createTaskWithError('job-1', 'Clipboard failure') composable.copyErrorMessage() expect(copyToClipboard).toHaveBeenCalledTimes(1) expect(copyToClipboard).toHaveBeenCalledWith('Clipboard failure') copyToClipboard.mockClear() - taskState.value = createTaskWithMessages([]) + taskState.value = createTaskWithError('job-2') composable.copyErrorMessage() expect(copyToClipboard).not.toHaveBeenCalled() }) - it('prefers the detailed execution dialog when detail is available', () => { - const detail = createExecutionErrorMessage({ - exception_message: 'Detailed failure' - }) - taskState.value = createTaskWithMessages([['execution_error', detail]]) + it('shows simple error dialog when only errorMessage present', () => { + taskState.value = createTaskWithError('job-1', 'Queue job error') composable.reportJobError() - expect(showExecutionErrorDialog).toHaveBeenCalledTimes(1) - expect(showExecutionErrorDialog).toHaveBeenCalledWith(detail) - expect(showErrorDialog).not.toHaveBeenCalled() - }) - it('shows a fallback dialog when only a message is available', () => { - const message = 'Queue job error' - taskState.value = createTaskWithMessages([]) - const valueSpy = vi - .spyOn(composable.errorMessageValue, 'value', 'get') - .mockReturnValue(message) - - expect(composable.errorMessageValue.value).toBe(message) - composable.reportJobError() - expect(showExecutionErrorDialog).not.toHaveBeenCalled() expect(showErrorDialog).toHaveBeenCalledTimes(1) const [errorArg, optionsArg] = showErrorDialog.mock.calls[0] expect(errorArg).toBeInstanceOf(Error) - expect(errorArg.message).toBe(message) + expect(errorArg.message).toBe('Queue job error') expect(optionsArg).toEqual({ reportType: 'queueJobError' }) - valueSpy.mockRestore() + expect(showExecutionErrorDialog).not.toHaveBeenCalled() }) - it('does nothing when no error could be extracted', () => { - taskState.value = createTaskWithMessages([]) + it('does nothing when no task exists', () => { + taskState.value = null composable.reportJobError() + expect(showErrorDialog).not.toHaveBeenCalled() expect(showExecutionErrorDialog).not.toHaveBeenCalled() + }) + + it('shows rich error dialog when execution_error available on task', () => { + const executionError: ExecutionError = { + prompt_id: 'job-1', + timestamp: 12345, + node_id: '5', + node_type: 'KSampler', + executed: ['1', '2'], + exception_message: 'CUDA out of memory', + exception_type: 'RuntimeError', + traceback: ['line 1', 'line 2'], + current_inputs: {}, + current_outputs: {} + } + taskState.value = createTaskWithError( + 'job-1', + 'CUDA out of memory', + executionError, + 12345 + ) + + composable.reportJobError() + + expect(showExecutionErrorDialog).toHaveBeenCalledTimes(1) + expect(showExecutionErrorDialog).toHaveBeenCalledWith(executionError) expect(showErrorDialog).not.toHaveBeenCalled() }) + + it('passes execution_error directly to dialog', () => { + const executionError: ExecutionError = { + prompt_id: 'job-1', + timestamp: 12345, + node_id: '5', + node_type: 'KSampler', + exception_message: 'Error', + exception_type: 'RuntimeError', + traceback: ['line 1'], + current_inputs: {}, + current_outputs: {} + } + taskState.value = createTaskWithError( + 'job-1', + 'Error', + executionError, + 12345 + ) + + composable.reportJobError() + + expect(showExecutionErrorDialog).toHaveBeenCalledTimes(1) + expect(showExecutionErrorDialog).toHaveBeenCalledWith(executionError) + }) + + it('does nothing when no error message and no execution_error', () => { + taskState.value = createTaskWithError('job-1') + + composable.reportJobError() + + expect(showErrorDialog).not.toHaveBeenCalled() + expect(showExecutionErrorDialog).not.toHaveBeenCalled() + }) }) diff --git a/src/components/queue/job/useJobErrorReporting.ts b/src/components/queue/job/useJobErrorReporting.ts index 01fd245a9c..b008238a62 100644 --- a/src/components/queue/job/useJobErrorReporting.ts +++ b/src/components/queue/job/useJobErrorReporting.ts @@ -28,9 +28,7 @@ export const useJobErrorReporting = ({ copyToClipboard, dialog }: UseJobErrorReportingOptions) => { - const errorMessageValue = computed(() => { - return taskForJob.value?.executionError?.exception_message ?? '' - }) + const errorMessageValue = computed(() => taskForJob.value?.errorMessage ?? '') const copyErrorMessage = () => { if (errorMessageValue.value) { @@ -44,6 +42,7 @@ export const useJobErrorReporting = ({ dialog.showExecutionErrorDialog(executionError) return } + if (errorMessageValue.value) { dialog.showErrorDialog(new Error(errorMessageValue.value), { reportType: 'queueJobError' diff --git a/src/components/sidebar/tabs/AssetsSidebarTab.vue b/src/components/sidebar/tabs/AssetsSidebarTab.vue index 7da25bb8e1..c3c6fb6504 100644 --- a/src/components/sidebar/tabs/AssetsSidebarTab.vue +++ b/src/components/sidebar/tabs/AssetsSidebarTab.vue @@ -52,37 +52,13 @@ class="pb-1 px-2 2xl:px-4" :show-generation-time-sort="activeTab === 'output'" /> -
- - {{ activeJobsLabel }} - -
- - {{ t('sideToolbar.queueProgressOverlay.clearQueueTooltip') }} - - -
-
- + @@ -146,6 +110,7 @@