diff --git a/src/services/jobOutputCache.test.ts b/src/services/jobOutputCache.test.ts index 38e47e973e..3b90ab2fec 100644 --- a/src/services/jobOutputCache.test.ts +++ b/src/services/jobOutputCache.test.ts @@ -180,6 +180,87 @@ describe('jobOutputCache', () => { }) }) + describe('getPreviewableOutputsFromJobDetail', () => { + it('returns empty array when job detail or outputs are missing', async () => { + const { getPreviewableOutputsFromJobDetail } = + await import('@/services/jobOutputCache') + + expect(getPreviewableOutputsFromJobDetail(undefined)).toEqual([]) + + const jobDetail: JobDetail = { + id: 'job-empty', + status: 'completed', + create_time: Date.now(), + priority: 0 + } + + expect(getPreviewableOutputsFromJobDetail(jobDetail)).toEqual([]) + }) + + it('maps previewable outputs and skips animated/text entries', async () => { + const { getPreviewableOutputsFromJobDetail } = + await import('@/services/jobOutputCache') + const jobDetail: JobDetail = { + id: 'job-previewable', + status: 'completed', + create_time: Date.now(), + priority: 0, + outputs: { + 'node-1': { + images: [ + { filename: 'image.png', subfolder: '', type: 'output' }, + { filename: 'image.webp', subfolder: '', type: 'temp' } + ], + animated: [true], + text: 'hello' + }, + 'node-2': { + video: [{ filename: 'clip.mp4', subfolder: '', type: 'output' }], + audio: [{ filename: 'sound.mp3', subfolder: '', type: 'output' }] + } + } + } + + const result = getPreviewableOutputsFromJobDetail(jobDetail) + + expect(result).toHaveLength(4) + expect(result.map((item) => item.filename).sort()).toEqual( + ['image.png', 'image.webp', 'clip.mp4', 'sound.mp3'].sort() + ) + + const image = result.find((item) => item.filename === 'image.png') + const video = result.find((item) => item.filename === 'clip.mp4') + + expect(image).toBeInstanceOf(ResultItemImpl) + expect(image?.nodeId).toBe('node-1') + expect(image?.mediaType).toBe('images') + expect(video?.nodeId).toBe('node-2') + expect(video?.mediaType).toBe('video') + }) + + it('filters non-previewable outputs and non-object items', async () => { + const { getPreviewableOutputsFromJobDetail } = + await import('@/services/jobOutputCache') + const jobDetail: JobDetail = { + id: 'job-filter', + status: 'completed', + create_time: Date.now(), + priority: 0, + outputs: { + 'node-3': { + images: [{ filename: 'valid.png', subfolder: '', type: 'output' }], + text: ['not-object'], + unknown: [{ filename: 'data.bin', subfolder: '', type: 'output' }] + } + } + } + + const result = getPreviewableOutputsFromJobDetail(jobDetail) + + expect(result.map((item) => item.filename)).toEqual(['valid.png']) + }) + }) + describe('getJobDetail', () => { it('fetches and caches job detail', async () => { const { getJobDetail } = await import('@/services/jobOutputCache') diff --git a/src/services/jobOutputCache.ts b/src/services/jobOutputCache.ts index 72722afd99..4ae7140efa 100644 --- a/src/services/jobOutputCache.ts +++ b/src/services/jobOutputCache.ts @@ -11,6 +11,7 @@ import QuickLRU from '@alloc/quick-lru' import type { JobDetail } from '@/platform/remote/comfyui/jobs/jobTypes' import { extractWorkflow } from '@/platform/remote/comfyui/jobs/fetchJobs' import type { ComfyWorkflowJSON } from '@/platform/workflow/validation/schemas/workflowSchema' +import type { ResultItem, TaskOutput } from '@/schemas/apiSchema' import { api } from '@/scripts/api' import { ResultItemImpl } from '@/stores/queueStore' import type { TaskItemImpl } from '@/stores/queueStore' @@ -75,6 +76,40 @@ export async function getOutputsForTask( } } +function getPreviewableOutputs(outputs?: TaskOutput): ResultItemImpl[] { + if (!outputs) return [] + const resultItems = Object.entries(outputs).flatMap(([nodeId, nodeOutputs]) => + Object.entries(nodeOutputs) + .filter(([mediaType, items]) => mediaType !== 'animated' && items) + .flatMap(([mediaType, items]) => { + if (!Array.isArray(items)) { + return [] + } + + return items.filter(isResultItem).map( + (item) => + new ResultItemImpl({ + ...item, + nodeId, + mediaType + }) + ) + }) + ) + + return ResultItemImpl.filterPreviewable(resultItems) +} + +function isResultItem(item: unknown): item is ResultItem { + return typeof item === 'object' && item !== null +} + +export function getPreviewableOutputsFromJobDetail( + jobDetail?: JobDetail +): ResultItemImpl[] { + return getPreviewableOutputs(jobDetail?.outputs) +} + // ===== Job Detail Caching ===== export async function getJobDetail(