mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-23 07:50:15 +00:00
use execution_error field
This commit is contained in:
@@ -107,7 +107,6 @@ import { useCopyToClipboard } from '@/composables/useCopyToClipboard'
|
||||
import { t } from '@/i18n'
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
|
||||
import { api } from '@/scripts/api'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useExecutionStore } from '@/stores/executionStore'
|
||||
import { useQueueStore } from '@/stores/queueStore'
|
||||
@@ -355,7 +354,6 @@ const { errorMessageValue, copyErrorMessage, reportJobError } =
|
||||
useJobErrorReporting({
|
||||
taskForJob,
|
||||
copyToClipboard,
|
||||
dialog,
|
||||
fetchApi: (url) => api.fetchApi(url)
|
||||
dialog
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import { computed } from 'vue'
|
||||
import type { ComputedRef } from 'vue'
|
||||
|
||||
import { fetchJobDetail } from '@/platform/remote/comfyui/jobs'
|
||||
import type { ExecutionErrorWsMessage } from '@/schemas/apiSchema'
|
||||
import type { TaskItemImpl } from '@/stores/queueStore'
|
||||
|
||||
type CopyHandler = (value: string) => void | Promise<void>
|
||||
type FetchApi = (url: string) => Promise<Response>
|
||||
|
||||
export type JobErrorDialogService = {
|
||||
showExecutionErrorDialog: (executionError: ExecutionErrorWsMessage) => void
|
||||
showErrorDialog: (
|
||||
error: Error,
|
||||
options?: {
|
||||
@@ -16,22 +15,18 @@ export type JobErrorDialogService = {
|
||||
[key: string]: unknown
|
||||
}
|
||||
) => void
|
||||
showExecutionErrorDialog?: (executionError: ExecutionErrorWsMessage) => void
|
||||
}
|
||||
|
||||
type UseJobErrorReportingOptions = {
|
||||
taskForJob: ComputedRef<TaskItemImpl | null>
|
||||
copyToClipboard: CopyHandler
|
||||
dialog: JobErrorDialogService
|
||||
/** Optional fetch function to enable rich error dialogs with traceback */
|
||||
fetchApi?: FetchApi
|
||||
}
|
||||
|
||||
export const useJobErrorReporting = ({
|
||||
taskForJob,
|
||||
copyToClipboard,
|
||||
dialog,
|
||||
fetchApi
|
||||
dialog
|
||||
}: UseJobErrorReportingOptions) => {
|
||||
const errorMessageValue = computed(() => taskForJob.value?.errorMessage ?? '')
|
||||
|
||||
@@ -41,23 +36,15 @@ export const useJobErrorReporting = ({
|
||||
}
|
||||
}
|
||||
|
||||
const reportJobError = async () => {
|
||||
const reportJobError = () => {
|
||||
const task = taskForJob.value
|
||||
if (!task) return
|
||||
|
||||
// Try to fetch rich error details if fetchApi is provided
|
||||
if (fetchApi && dialog.showExecutionErrorDialog) {
|
||||
const jobDetail = await fetchJobDetail(fetchApi, task.promptId)
|
||||
const executionError = jobDetail?.execution_error
|
||||
|
||||
if (executionError) {
|
||||
dialog.showExecutionErrorDialog({
|
||||
prompt_id: task.promptId,
|
||||
timestamp: jobDetail?.create_time ?? Date.now(),
|
||||
...executionError
|
||||
})
|
||||
return
|
||||
}
|
||||
// Use execution_error from list response if available (includes prompt_id, timestamp)
|
||||
const executionError = task.executionError
|
||||
if (executionError) {
|
||||
dialog.showExecutionErrorDialog(executionError as ExecutionErrorWsMessage)
|
||||
return
|
||||
}
|
||||
|
||||
// Fall back to simple error dialog
|
||||
|
||||
@@ -93,21 +93,15 @@ export function useJobMenu(
|
||||
if (message) await copyToClipboard(message)
|
||||
}
|
||||
|
||||
const reportError = async () => {
|
||||
const reportError = () => {
|
||||
const item = currentMenuItem()
|
||||
if (!item) return
|
||||
|
||||
// Try to fetch rich error details from job detail
|
||||
const jobDetail = await fetchJobDetail((url) => api.fetchApi(url), item.id)
|
||||
const executionError = jobDetail?.execution_error
|
||||
// Use execution_error from list response if available
|
||||
const executionError = item.taskRef?.executionError
|
||||
|
||||
if (executionError) {
|
||||
// Use rich error dialog with traceback, node info, etc.
|
||||
useDialogService().showExecutionErrorDialog({
|
||||
prompt_id: item.id,
|
||||
timestamp: jobDetail?.create_time ?? Date.now(),
|
||||
...executionError
|
||||
})
|
||||
useDialogService().showExecutionErrorDialog(executionError)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ async function fetchJobsRaw(
|
||||
return { jobs: [], total: 0, offset: 0 }
|
||||
}
|
||||
const data = zJobsListResponse.parse(await res.json())
|
||||
return { jobs: data.jobs, total: data.total, offset }
|
||||
return { jobs: data.jobs, total: data.pagination.total, offset }
|
||||
} catch (error) {
|
||||
console.error('[Jobs API] Error fetching jobs:', error)
|
||||
return { jobs: [], total: 0, offset: 0 }
|
||||
@@ -135,14 +135,17 @@ export async function fetchJobDetail(
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts workflow from job detail response
|
||||
* Extracts workflow from job detail response.
|
||||
* The workflow is nested at: workflow.extra_data.extra_pnginfo.workflow
|
||||
*/
|
||||
export function extractWorkflow(
|
||||
job: JobDetail | undefined
|
||||
): ComfyWorkflowJSON | undefined {
|
||||
// Cast is safe - workflow will be validated by loadGraphData -> validateComfyWorkflow
|
||||
// Path: extra_data.extra_pnginfo.workflow (at top level of job detail)
|
||||
return job?.extra_data?.extra_pnginfo?.workflow as
|
||||
const workflowData = job?.workflow as
|
||||
| { extra_data?: { extra_pnginfo?: { workflow?: unknown } } }
|
||||
| undefined
|
||||
return workflowData?.extra_data?.extra_pnginfo?.workflow as
|
||||
| ComfyWorkflowJSON
|
||||
| undefined
|
||||
}
|
||||
|
||||
@@ -30,6 +30,25 @@ const zPreviewOutput = z
|
||||
})
|
||||
.passthrough() // Allow extra fields like nodeId, mediaType
|
||||
|
||||
/**
|
||||
* Execution error details for error jobs.
|
||||
* Contains the same structure as ExecutionErrorWsMessage from WebSocket.
|
||||
*/
|
||||
const zExecutionError = z
|
||||
.object({
|
||||
node_id: z.string(),
|
||||
node_type: z.string(),
|
||||
executed: z.array(z.string()).optional(),
|
||||
exception_message: z.string(),
|
||||
exception_type: z.string(),
|
||||
traceback: z.array(z.string()),
|
||||
current_inputs: z.unknown(),
|
||||
current_outputs: z.unknown()
|
||||
})
|
||||
.passthrough()
|
||||
|
||||
export type ExecutionError = z.infer<typeof zExecutionError>
|
||||
|
||||
/**
|
||||
* Raw job from API - uses passthrough to allow extra fields
|
||||
*/
|
||||
@@ -41,9 +60,11 @@ const zRawJobListItem = z
|
||||
preview_output: zPreviewOutput.nullable().optional(),
|
||||
outputs_count: z.number().optional(),
|
||||
error_message: z.string().nullable().optional(),
|
||||
execution_error: zExecutionError.nullable().optional(),
|
||||
workflow_id: z.string().nullable().optional(),
|
||||
priority: z.number().optional()
|
||||
})
|
||||
.passthrough() // Allow extra fields like execution_time, workflow_id, update_time
|
||||
.passthrough()
|
||||
|
||||
/**
|
||||
* Job list item with priority always set (either from server or synthetic)
|
||||
@@ -52,61 +73,41 @@ const zJobListItem = zRawJobListItem.extend({
|
||||
priority: z.number() // Always set: server-provided or synthetic (total - offset - index)
|
||||
})
|
||||
|
||||
/**
|
||||
* Extra data structure containing workflow
|
||||
* Note: workflow is z.unknown() because it goes through validateComfyWorkflow separately
|
||||
*/
|
||||
const zExtraData = z
|
||||
.object({
|
||||
extra_pnginfo: z
|
||||
.object({
|
||||
workflow: z.unknown()
|
||||
})
|
||||
.optional()
|
||||
})
|
||||
.passthrough()
|
||||
|
||||
/**
|
||||
* Execution error details for failed jobs.
|
||||
* Contains the same structure as ExecutionErrorWsMessage from WebSocket.
|
||||
*/
|
||||
const zExecutionError = z.object({
|
||||
node_id: z.string(),
|
||||
node_type: z.string(),
|
||||
executed: z.array(z.string()),
|
||||
exception_message: z.string(),
|
||||
exception_type: z.string(),
|
||||
traceback: z.array(z.string()),
|
||||
current_inputs: z.unknown(),
|
||||
current_outputs: z.unknown()
|
||||
})
|
||||
|
||||
/**
|
||||
* Job detail - returned by GET /api/jobs/{job_id} (detail endpoint)
|
||||
* Includes full workflow and outputs for re-execution and downloads
|
||||
*
|
||||
* Note: workflow is at extra_data.extra_pnginfo.workflow (not in a separate workflow object)
|
||||
*/
|
||||
export const zJobDetail = zRawJobListItem
|
||||
.extend({
|
||||
extra_data: zExtraData.optional(),
|
||||
prompt: z.record(z.string(), z.unknown()).optional(),
|
||||
workflow: z.unknown().optional(),
|
||||
outputs: zTaskOutput.optional(),
|
||||
execution_time: z.number().optional(),
|
||||
workflow_id: z.string().nullable().optional(),
|
||||
execution_error: zExecutionError.nullable().optional()
|
||||
update_time: z.number().optional(),
|
||||
execution_status: z.unknown().optional(),
|
||||
execution_meta: z.unknown().optional()
|
||||
})
|
||||
.passthrough()
|
||||
|
||||
/**
|
||||
* Jobs list response structure - raw from API (before synthetic priority)
|
||||
* Pagination info from API
|
||||
*/
|
||||
const zPaginationInfo = z
|
||||
.object({
|
||||
offset: z.number(),
|
||||
limit: z.number(),
|
||||
total: z.number(),
|
||||
has_more: z.boolean()
|
||||
})
|
||||
.passthrough()
|
||||
|
||||
/**
|
||||
* Jobs list response structure
|
||||
*/
|
||||
export const zJobsListResponse = z
|
||||
.object({
|
||||
jobs: z.array(zRawJobListItem),
|
||||
total: z.number()
|
||||
pagination: zPaginationInfo
|
||||
})
|
||||
.passthrough() // Allow extra fields like has_more, offset, limit
|
||||
.passthrough()
|
||||
|
||||
// ============================================================================
|
||||
// TypeScript Types (derived from Zod schemas)
|
||||
|
||||
@@ -287,6 +287,13 @@ export class TaskItemImpl {
|
||||
return this.job.error_message ?? undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Execution error details if job failed with traceback
|
||||
*/
|
||||
get executionError() {
|
||||
return this.job.execution_error ?? undefined
|
||||
}
|
||||
|
||||
get workflow(): ComfyWorkflowJSON | undefined {
|
||||
// Workflow is only available after lazy loading via getWorkflowFromHistory
|
||||
return undefined
|
||||
|
||||
@@ -5,16 +5,20 @@ import type { ComputedRef } from 'vue'
|
||||
import type { TaskItemImpl } from '@/stores/queueStore'
|
||||
import type { JobErrorDialogService } from '@/components/queue/job/useJobErrorReporting'
|
||||
import { useJobErrorReporting } from '@/components/queue/job/useJobErrorReporting'
|
||||
|
||||
const fetchJobDetailMock = vi.fn()
|
||||
vi.mock('@/platform/remote/comfyui/jobs', () => ({
|
||||
fetchJobDetail: (...args: unknown[]) => fetchJobDetailMock(...args)
|
||||
}))
|
||||
import type { ExecutionError } from '@/platform/remote/comfyui/jobs/types/jobTypes'
|
||||
|
||||
const createTaskWithError = (
|
||||
promptId: string,
|
||||
errorMessage?: string
|
||||
): TaskItemImpl => ({ promptId, errorMessage }) as unknown as TaskItemImpl
|
||||
errorMessage?: string,
|
||||
executionError?: ExecutionError,
|
||||
createTime?: number
|
||||
): TaskItemImpl =>
|
||||
({
|
||||
promptId,
|
||||
errorMessage,
|
||||
executionError,
|
||||
createTime: createTime ?? Date.now()
|
||||
}) as unknown as TaskItemImpl
|
||||
|
||||
describe('useJobErrorReporting', () => {
|
||||
let taskState = ref<TaskItemImpl | null>(null)
|
||||
@@ -33,7 +37,6 @@ describe('useJobErrorReporting', () => {
|
||||
showErrorDialog = vi.fn()
|
||||
showExecutionErrorDialog = vi.fn()
|
||||
dialog = { showErrorDialog, showExecutionErrorDialog }
|
||||
fetchJobDetailMock.mockResolvedValue(undefined)
|
||||
composable = useJobErrorReporting({
|
||||
taskForJob,
|
||||
copyToClipboard,
|
||||
@@ -70,107 +73,83 @@ describe('useJobErrorReporting', () => {
|
||||
expect(copyToClipboard).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('shows simple error dialog when no fetchApi provided', async () => {
|
||||
it('shows simple error dialog when only errorMessage present', () => {
|
||||
taskState.value = createTaskWithError('job-1', 'Queue job error')
|
||||
await composable.reportJobError()
|
||||
composable.reportJobError()
|
||||
|
||||
expect(fetchJobDetailMock).not.toHaveBeenCalled()
|
||||
expect(showErrorDialog).toHaveBeenCalledTimes(1)
|
||||
const [errorArg, optionsArg] = showErrorDialog.mock.calls[0]
|
||||
expect(errorArg).toBeInstanceOf(Error)
|
||||
expect(errorArg.message).toBe('Queue job error')
|
||||
expect(optionsArg).toEqual({ reportType: 'queueJobError' })
|
||||
expect(showExecutionErrorDialog).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('does nothing when no task exists', async () => {
|
||||
it('does nothing when no task exists', () => {
|
||||
taskState.value = null
|
||||
await composable.reportJobError()
|
||||
composable.reportJobError()
|
||||
expect(showErrorDialog).not.toHaveBeenCalled()
|
||||
expect(showExecutionErrorDialog).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
describe('with fetchApi provided', () => {
|
||||
let fetchApi: ReturnType<typeof vi.fn>
|
||||
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
|
||||
)
|
||||
|
||||
beforeEach(() => {
|
||||
fetchApi = vi.fn()
|
||||
composable = useJobErrorReporting({
|
||||
taskForJob,
|
||||
copyToClipboard,
|
||||
dialog,
|
||||
fetchApi
|
||||
})
|
||||
})
|
||||
composable.reportJobError()
|
||||
|
||||
it('shows rich error dialog when execution_error available', async () => {
|
||||
const executionError = {
|
||||
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: {}
|
||||
}
|
||||
fetchJobDetailMock.mockResolvedValue({
|
||||
id: 'job-1',
|
||||
create_time: 12345,
|
||||
execution_error: executionError
|
||||
})
|
||||
taskState.value = createTaskWithError('job-1', 'CUDA out of memory')
|
||||
expect(showExecutionErrorDialog).toHaveBeenCalledTimes(1)
|
||||
expect(showExecutionErrorDialog).toHaveBeenCalledWith(executionError)
|
||||
expect(showErrorDialog).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
await composable.reportJobError()
|
||||
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
|
||||
)
|
||||
|
||||
expect(fetchJobDetailMock).toHaveBeenCalledWith(fetchApi, 'job-1')
|
||||
expect(showExecutionErrorDialog).toHaveBeenCalledTimes(1)
|
||||
expect(showExecutionErrorDialog).toHaveBeenCalledWith({
|
||||
prompt_id: 'job-1',
|
||||
timestamp: 12345,
|
||||
...executionError
|
||||
})
|
||||
expect(showErrorDialog).not.toHaveBeenCalled()
|
||||
})
|
||||
composable.reportJobError()
|
||||
|
||||
it('falls back to simple error dialog when no execution_error', async () => {
|
||||
fetchJobDetailMock.mockResolvedValue({
|
||||
id: 'job-1',
|
||||
execution_error: null
|
||||
})
|
||||
taskState.value = createTaskWithError('job-1', 'Job failed')
|
||||
expect(showExecutionErrorDialog).toHaveBeenCalledTimes(1)
|
||||
expect(showExecutionErrorDialog).toHaveBeenCalledWith(executionError)
|
||||
})
|
||||
|
||||
await composable.reportJobError()
|
||||
it('does nothing when no error message and no execution_error', () => {
|
||||
taskState.value = createTaskWithError('job-1')
|
||||
|
||||
expect(fetchJobDetailMock).toHaveBeenCalledWith(fetchApi, 'job-1')
|
||||
expect(showExecutionErrorDialog).not.toHaveBeenCalled()
|
||||
expect(showErrorDialog).toHaveBeenCalledTimes(1)
|
||||
const [errorArg, optionsArg] = showErrorDialog.mock.calls[0]
|
||||
expect(errorArg).toBeInstanceOf(Error)
|
||||
expect(errorArg.message).toBe('Job failed')
|
||||
expect(optionsArg).toEqual({ reportType: 'queueJobError' })
|
||||
})
|
||||
composable.reportJobError()
|
||||
|
||||
it('falls back to simple error dialog when fetch fails', async () => {
|
||||
fetchJobDetailMock.mockResolvedValue(undefined)
|
||||
taskState.value = createTaskWithError('job-1', 'Job failed')
|
||||
|
||||
await composable.reportJobError()
|
||||
|
||||
expect(showExecutionErrorDialog).not.toHaveBeenCalled()
|
||||
expect(showErrorDialog).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('does nothing when no error message and no execution_error', async () => {
|
||||
fetchJobDetailMock.mockResolvedValue({
|
||||
id: 'job-1',
|
||||
execution_error: null
|
||||
})
|
||||
taskState.value = createTaskWithError('job-1')
|
||||
|
||||
await composable.reportJobError()
|
||||
|
||||
expect(showErrorDialog).not.toHaveBeenCalled()
|
||||
expect(showExecutionErrorDialog).not.toHaveBeenCalled()
|
||||
})
|
||||
expect(showErrorDialog).not.toHaveBeenCalled()
|
||||
expect(showExecutionErrorDialog).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -290,6 +290,8 @@ describe('useJobMenu', () => {
|
||||
|
||||
it('reports error via rich dialog when execution_error available', async () => {
|
||||
const executionError = {
|
||||
prompt_id: 'job-1',
|
||||
timestamp: 12345,
|
||||
node_id: '5',
|
||||
node_type: 'KSampler',
|
||||
executed: ['1', '2'],
|
||||
@@ -299,16 +301,15 @@ describe('useJobMenu', () => {
|
||||
current_inputs: {},
|
||||
current_outputs: {}
|
||||
}
|
||||
fetchJobDetailMock.mockResolvedValue({
|
||||
id: 'job-1',
|
||||
create_time: 12345,
|
||||
execution_error: executionError
|
||||
})
|
||||
const { jobMenuEntries } = mountJobMenu()
|
||||
setCurrentItem(
|
||||
createJobItem({
|
||||
state: 'failed',
|
||||
taskRef: { errorMessage: 'CUDA out of memory' } as any
|
||||
taskRef: {
|
||||
errorMessage: 'CUDA out of memory',
|
||||
executionError,
|
||||
createTime: 12345
|
||||
} as any
|
||||
})
|
||||
)
|
||||
|
||||
@@ -316,24 +317,14 @@ describe('useJobMenu', () => {
|
||||
const entry = findActionEntry(jobMenuEntries.value, 'report-error')
|
||||
await entry?.onClick?.()
|
||||
|
||||
expect(fetchJobDetailMock).toHaveBeenCalledWith(
|
||||
expect.any(Function),
|
||||
'job-1'
|
||||
)
|
||||
expect(dialogServiceMock.showExecutionErrorDialog).toHaveBeenCalledTimes(1)
|
||||
expect(dialogServiceMock.showExecutionErrorDialog).toHaveBeenCalledWith({
|
||||
prompt_id: 'job-1',
|
||||
timestamp: 12345,
|
||||
...executionError
|
||||
})
|
||||
expect(dialogServiceMock.showExecutionErrorDialog).toHaveBeenCalledWith(
|
||||
executionError
|
||||
)
|
||||
expect(dialogServiceMock.showErrorDialog).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('falls back to simple error dialog when no execution_error', async () => {
|
||||
fetchJobDetailMock.mockResolvedValue({
|
||||
id: 'job-1',
|
||||
execution_error: null
|
||||
})
|
||||
const { jobMenuEntries } = mountJobMenu()
|
||||
setCurrentItem(
|
||||
createJobItem({
|
||||
@@ -356,7 +347,6 @@ describe('useJobMenu', () => {
|
||||
})
|
||||
|
||||
it('ignores error actions when message missing', async () => {
|
||||
fetchJobDetailMock.mockResolvedValue({ id: 'job-1', execution_error: null })
|
||||
const { jobMenuEntries } = mountJobMenu()
|
||||
setCurrentItem(
|
||||
createJobItem({
|
||||
|
||||
@@ -17,17 +17,19 @@ const mockWorkflow: ComfyWorkflowJSON = {
|
||||
}
|
||||
|
||||
// Jobs API detail response structure (matches actual /jobs/{id} response)
|
||||
// workflow is nested at: workflow.extra_data.extra_pnginfo.workflow
|
||||
const mockJobDetailResponse = {
|
||||
id: 'test-prompt-id',
|
||||
status: 'completed',
|
||||
create_time: 1234567890,
|
||||
execution_time: 18.5,
|
||||
extra_data: {
|
||||
extra_pnginfo: {
|
||||
workflow: mockWorkflow
|
||||
update_time: 1234567900,
|
||||
workflow: {
|
||||
extra_data: {
|
||||
extra_pnginfo: {
|
||||
workflow: mockWorkflow
|
||||
}
|
||||
}
|
||||
},
|
||||
prompt: {},
|
||||
outputs: {
|
||||
'20': {
|
||||
images: [
|
||||
@@ -61,7 +63,7 @@ describe('fetchJobDetail', () => {
|
||||
expect(result).toBeDefined()
|
||||
expect(result?.id).toBe('test-prompt-id')
|
||||
expect(result?.outputs).toEqual(mockJobDetailResponse.outputs)
|
||||
expect(result?.extra_data).toBeDefined()
|
||||
expect(result?.workflow).toBeDefined()
|
||||
})
|
||||
|
||||
it('should return undefined when job not found (non-OK response)', async () => {
|
||||
@@ -110,10 +112,10 @@ describe('extractWorkflow', () => {
|
||||
expect(result).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should return undefined when workflow is missing extra_pnginfo', () => {
|
||||
it('should return undefined when workflow is missing', () => {
|
||||
const jobWithoutWorkflow = {
|
||||
...mockJobDetailResponse,
|
||||
extra_data: {}
|
||||
workflow: {}
|
||||
}
|
||||
|
||||
const result = extractWorkflow(jobWithoutWorkflow as any)
|
||||
|
||||
@@ -27,17 +27,19 @@ const mockWorkflow: ComfyWorkflowJSON = {
|
||||
}
|
||||
|
||||
// Mock job detail response (matches actual /jobs/{id} API response structure)
|
||||
// workflow is nested at: workflow.extra_data.extra_pnginfo.workflow
|
||||
const mockJobDetail = {
|
||||
id: 'test-prompt-id',
|
||||
status: 'completed' as const,
|
||||
create_time: Date.now(),
|
||||
execution_time: 10.5,
|
||||
extra_data: {
|
||||
extra_pnginfo: {
|
||||
workflow: mockWorkflow
|
||||
update_time: Date.now(),
|
||||
workflow: {
|
||||
extra_data: {
|
||||
extra_pnginfo: {
|
||||
workflow: mockWorkflow
|
||||
}
|
||||
}
|
||||
},
|
||||
prompt: {},
|
||||
outputs: {
|
||||
'1': { images: [{ filename: 'test.png', subfolder: '', type: 'output' }] }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user