mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-02 14:27:40 +00:00
## Summary - Add `errorMessage` and `executionError` getters to `TaskItemImpl` that extract error info from status messages - Update `useJobErrorReporting` composable to use these getters instead of standalone function - Remove the standalone `extractExecutionError` function This encapsulates error extraction within `TaskItemImpl`, preparing for the Jobs API migration where the underlying data format will change but the getter interface will remain stable. ## Test plan - [x] All existing tests pass - [x] New tests added for `TaskItemImpl.errorMessage` and `TaskItemImpl.executionError` getters - [x] TypeScript, lint, and knip checks pass 🤖 Generated with [Claude Code](https://claude.com/claude-code) ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-7650-refactor-encapsulate-error-extraction-in-TaskItemImpl-getters-2ce6d73d365081caae33dcc7e1e07720) by [Unito](https://www.unito.io) --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Christian Byrne <cbyrne@comfy.org>
279 lines
8.9 KiB
TypeScript
279 lines
8.9 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
import type {
|
|
JobDetail,
|
|
JobListItem
|
|
} from '@/platform/remote/comfyui/jobs/jobTypes'
|
|
import { ResultItemImpl, TaskItemImpl } from '@/stores/queueStore'
|
|
|
|
vi.mock('@/platform/remote/comfyui/jobs/fetchJobs', () => ({
|
|
fetchJobDetail: vi.fn(),
|
|
extractWorkflow: vi.fn()
|
|
}))
|
|
|
|
function createResultItem(url: string, supportsPreview = true): ResultItemImpl {
|
|
const item = new ResultItemImpl({
|
|
filename: url,
|
|
subfolder: '',
|
|
type: 'output',
|
|
nodeId: 'node-1',
|
|
mediaType: supportsPreview ? 'images' : 'unknown'
|
|
})
|
|
Object.defineProperty(item, 'url', { get: () => url })
|
|
Object.defineProperty(item, 'supportsPreview', { get: () => supportsPreview })
|
|
return item
|
|
}
|
|
|
|
function createMockJob(id: string, outputsCount = 1): JobListItem {
|
|
return {
|
|
id,
|
|
status: 'completed',
|
|
create_time: Date.now(),
|
|
preview_output: null,
|
|
outputs_count: outputsCount,
|
|
priority: 0
|
|
}
|
|
}
|
|
|
|
function createTask(
|
|
preview?: ResultItemImpl,
|
|
allOutputs?: ResultItemImpl[],
|
|
outputsCount = 1
|
|
): TaskItemImpl {
|
|
const job = createMockJob(
|
|
`task-${Math.random().toString(36).slice(2)}`,
|
|
outputsCount
|
|
)
|
|
const flatOutputs = allOutputs ?? (preview ? [preview] : [])
|
|
return new TaskItemImpl(job, {}, flatOutputs)
|
|
}
|
|
|
|
describe('jobOutputCache', () => {
|
|
beforeEach(() => {
|
|
vi.resetModules()
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
describe('findActiveIndex', () => {
|
|
it('returns index of matching URL', async () => {
|
|
const { findActiveIndex } = await import('@/services/jobOutputCache')
|
|
const items = [
|
|
createResultItem('a'),
|
|
createResultItem('b'),
|
|
createResultItem('c')
|
|
]
|
|
|
|
expect(findActiveIndex(items, 'b')).toBe(1)
|
|
})
|
|
|
|
it('returns 0 when URL not found', async () => {
|
|
const { findActiveIndex } = await import('@/services/jobOutputCache')
|
|
const items = [createResultItem('a'), createResultItem('b')]
|
|
|
|
expect(findActiveIndex(items, 'missing')).toBe(0)
|
|
})
|
|
|
|
it('returns 0 when URL is undefined', async () => {
|
|
const { findActiveIndex } = await import('@/services/jobOutputCache')
|
|
const items = [createResultItem('a'), createResultItem('b')]
|
|
|
|
expect(findActiveIndex(items, undefined)).toBe(0)
|
|
})
|
|
})
|
|
|
|
describe('getOutputsForTask', () => {
|
|
it('returns previewable outputs directly when no lazy load needed', async () => {
|
|
const { getOutputsForTask } = await import('@/services/jobOutputCache')
|
|
const outputs = [createResultItem('p-1'), createResultItem('p-2')]
|
|
const task = createTask(undefined, outputs, 1)
|
|
|
|
const result = await getOutputsForTask(task)
|
|
|
|
expect(result).toEqual(outputs)
|
|
})
|
|
|
|
it('lazy loads when outputsCount > 1', async () => {
|
|
const { getOutputsForTask } = await import('@/services/jobOutputCache')
|
|
const previewOutput = createResultItem('preview')
|
|
const fullOutputs = [
|
|
createResultItem('full-1'),
|
|
createResultItem('full-2')
|
|
]
|
|
|
|
const job = createMockJob('task-1', 3)
|
|
const task = new TaskItemImpl(job, {}, [previewOutput])
|
|
const loadedTask = new TaskItemImpl(job, {}, fullOutputs)
|
|
task.loadFullOutputs = vi.fn().mockResolvedValue(loadedTask)
|
|
|
|
const result = await getOutputsForTask(task)
|
|
|
|
expect(result).toEqual(fullOutputs)
|
|
expect(task.loadFullOutputs).toHaveBeenCalled()
|
|
})
|
|
|
|
it('caches loaded tasks', async () => {
|
|
const { getOutputsForTask } = await import('@/services/jobOutputCache')
|
|
const fullOutputs = [createResultItem('full-1')]
|
|
|
|
const job = createMockJob('task-1', 3)
|
|
const task = new TaskItemImpl(job, {}, [createResultItem('preview')])
|
|
const loadedTask = new TaskItemImpl(job, {}, fullOutputs)
|
|
task.loadFullOutputs = vi.fn().mockResolvedValue(loadedTask)
|
|
|
|
// First call should load
|
|
await getOutputsForTask(task)
|
|
expect(task.loadFullOutputs).toHaveBeenCalledTimes(1)
|
|
|
|
// Second call should use cache
|
|
await getOutputsForTask(task)
|
|
expect(task.loadFullOutputs).toHaveBeenCalledTimes(1)
|
|
})
|
|
|
|
it('falls back to preview outputs on load error', async () => {
|
|
const { getOutputsForTask } = await import('@/services/jobOutputCache')
|
|
const previewOutput = createResultItem('preview')
|
|
|
|
const job = createMockJob('task-1', 3)
|
|
const task = new TaskItemImpl(job, {}, [previewOutput])
|
|
task.loadFullOutputs = vi
|
|
.fn()
|
|
.mockRejectedValue(new Error('Network error'))
|
|
|
|
const result = await getOutputsForTask(task)
|
|
|
|
expect(result).toEqual([previewOutput])
|
|
})
|
|
|
|
it('returns null when request is superseded', async () => {
|
|
const { getOutputsForTask } = await import('@/services/jobOutputCache')
|
|
const job1 = createMockJob('task-1', 3)
|
|
const job2 = createMockJob('task-2', 3)
|
|
|
|
const task1 = new TaskItemImpl(job1, {}, [createResultItem('preview-1')])
|
|
const task2 = new TaskItemImpl(job2, {}, [createResultItem('preview-2')])
|
|
|
|
const loadedTask1 = new TaskItemImpl(job1, {}, [
|
|
createResultItem('full-1')
|
|
])
|
|
const loadedTask2 = new TaskItemImpl(job2, {}, [
|
|
createResultItem('full-2')
|
|
])
|
|
|
|
// Task1 loads slowly, task2 loads quickly
|
|
task1.loadFullOutputs = vi.fn().mockImplementation(
|
|
() =>
|
|
new Promise((resolve) => {
|
|
setTimeout(() => resolve(loadedTask1), 50)
|
|
})
|
|
)
|
|
task2.loadFullOutputs = vi.fn().mockResolvedValue(loadedTask2)
|
|
|
|
// Start task1, then immediately start task2
|
|
const promise1 = getOutputsForTask(task1)
|
|
const promise2 = getOutputsForTask(task2)
|
|
|
|
const [result1, result2] = await Promise.all([promise1, promise2])
|
|
|
|
// Task2 should succeed, task1 should return null (superseded)
|
|
expect(result1).toBeNull()
|
|
expect(result2).toEqual([createResultItem('full-2')])
|
|
})
|
|
})
|
|
|
|
describe('getJobDetail', () => {
|
|
it('fetches and caches job detail', async () => {
|
|
const { getJobDetail } = await import('@/services/jobOutputCache')
|
|
const { fetchJobDetail } =
|
|
await import('@/platform/remote/comfyui/jobs/fetchJobs')
|
|
|
|
const mockDetail: JobDetail = {
|
|
id: 'job-1',
|
|
status: 'completed',
|
|
create_time: Date.now(),
|
|
priority: 0,
|
|
outputs: {}
|
|
}
|
|
vi.mocked(fetchJobDetail).mockResolvedValue(mockDetail)
|
|
|
|
const result = await getJobDetail('job-1')
|
|
|
|
expect(result).toEqual(mockDetail)
|
|
expect(fetchJobDetail).toHaveBeenCalledWith(expect.any(Function), 'job-1')
|
|
})
|
|
|
|
it('returns cached job detail on subsequent calls', async () => {
|
|
const { getJobDetail } = await import('@/services/jobOutputCache')
|
|
const { fetchJobDetail } =
|
|
await import('@/platform/remote/comfyui/jobs/fetchJobs')
|
|
|
|
const mockDetail: JobDetail = {
|
|
id: 'job-2',
|
|
status: 'completed',
|
|
create_time: Date.now(),
|
|
priority: 0,
|
|
outputs: {}
|
|
}
|
|
vi.mocked(fetchJobDetail).mockResolvedValue(mockDetail)
|
|
|
|
// First call
|
|
await getJobDetail('job-2')
|
|
expect(fetchJobDetail).toHaveBeenCalledTimes(1)
|
|
|
|
// Second call should use cache
|
|
const result = await getJobDetail('job-2')
|
|
expect(result).toEqual(mockDetail)
|
|
expect(fetchJobDetail).toHaveBeenCalledTimes(1)
|
|
})
|
|
|
|
it('returns undefined on fetch error', async () => {
|
|
const { getJobDetail } = await import('@/services/jobOutputCache')
|
|
const { fetchJobDetail } =
|
|
await import('@/platform/remote/comfyui/jobs/fetchJobs')
|
|
|
|
vi.mocked(fetchJobDetail).mockRejectedValue(new Error('Network error'))
|
|
|
|
const result = await getJobDetail('job-error')
|
|
|
|
expect(result).toBeUndefined()
|
|
})
|
|
})
|
|
|
|
describe('getJobWorkflow', () => {
|
|
it('fetches job detail and extracts workflow', async () => {
|
|
const { getJobWorkflow } = await import('@/services/jobOutputCache')
|
|
const { fetchJobDetail, extractWorkflow } =
|
|
await import('@/platform/remote/comfyui/jobs/fetchJobs')
|
|
|
|
const mockDetail: JobDetail = {
|
|
id: 'job-wf',
|
|
status: 'completed',
|
|
create_time: Date.now(),
|
|
priority: 0,
|
|
outputs: {}
|
|
}
|
|
const mockWorkflow = { version: 1 }
|
|
|
|
vi.mocked(fetchJobDetail).mockResolvedValue(mockDetail)
|
|
vi.mocked(extractWorkflow).mockResolvedValue(mockWorkflow as any)
|
|
|
|
const result = await getJobWorkflow('job-wf')
|
|
|
|
expect(result).toEqual(mockWorkflow)
|
|
expect(extractWorkflow).toHaveBeenCalledWith(mockDetail)
|
|
})
|
|
|
|
it('returns undefined when job detail not found', async () => {
|
|
const { getJobWorkflow } = await import('@/services/jobOutputCache')
|
|
const { fetchJobDetail, extractWorkflow } =
|
|
await import('@/platform/remote/comfyui/jobs/fetchJobs')
|
|
|
|
vi.mocked(fetchJobDetail).mockResolvedValue(undefined)
|
|
vi.mocked(extractWorkflow).mockResolvedValue(undefined)
|
|
|
|
const result = await getJobWorkflow('missing')
|
|
|
|
expect(result).toBeUndefined()
|
|
})
|
|
})
|
|
})
|