mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-12 16:40:05 +00:00
fix: make zPreviewOutput accept text-only job outputs (#9724)
## Summary Fixes Zod validation crash when the jobs batch contains text-only preview outputs (e.g. from LLM nodes), which caused the entire Assets sidebar to show nothing. ## Changes - **What**: Made `filename`, `subfolder`, and `type` optional in `zPreviewOutput` and added `.passthrough()` for extra fields like `content`. Text-only jobs are safely filtered out downstream by `supportsPreview`. - Added tests in `fetchJobs.test.ts` verifying a mixed batch (image + text-only + no-preview) parses successfully. - Added test in `assetsStore.test.ts` verifying text-only jobs are skipped without breaking sibling image jobs. Improved `TaskItemImpl` mock to realistically handle media types. ## Review Focus - The `zPreviewOutput` schema now uses `.passthrough()` to allow extra fields from new preview types (like `content` on text previews). This is consistent with `zRawJobListItem` and `zExecutionError` which also use `.passthrough()`. ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-9724-fix-make-zPreviewOutput-accept-text-only-job-outputs-31f6d73d36508119a7aef99f9b765ecd) by [Unito](https://www.unito.io)
This commit is contained in:
committed by
GitHub
parent
dc3e455993
commit
9ecb100d11
@@ -150,6 +150,41 @@ describe('fetchJobs', () => {
|
||||
|
||||
expect(result).toEqual([])
|
||||
})
|
||||
|
||||
it('parses batch containing text-only preview outputs', async () => {
|
||||
const mockFetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve(
|
||||
createMockResponse([
|
||||
createMockJob('image-job', 'completed', {
|
||||
preview_output: {
|
||||
filename: 'output.png',
|
||||
subfolder: '',
|
||||
type: 'output',
|
||||
nodeId: '1',
|
||||
mediaType: 'images'
|
||||
}
|
||||
}),
|
||||
createMockJob('text-job', 'completed', {
|
||||
preview_output: {
|
||||
content: 'some generated text',
|
||||
nodeId: '5',
|
||||
mediaType: 'text'
|
||||
}
|
||||
}),
|
||||
createMockJob('no-preview-job', 'completed')
|
||||
])
|
||||
)
|
||||
})
|
||||
|
||||
const result = await fetchHistory(mockFetch)
|
||||
|
||||
expect(result).toHaveLength(3)
|
||||
expect(result[0].id).toBe('image-job')
|
||||
expect(result[1].id).toBe('text-job')
|
||||
expect(result[2].id).toBe('no-preview-job')
|
||||
})
|
||||
})
|
||||
|
||||
describe('fetchQueue', () => {
|
||||
|
||||
@@ -18,14 +18,16 @@ const zJobStatus = z.enum([
|
||||
'cancelled'
|
||||
])
|
||||
|
||||
const zPreviewOutput = z.object({
|
||||
filename: z.string(),
|
||||
subfolder: z.string(),
|
||||
type: resultItemType,
|
||||
nodeId: z.string(),
|
||||
mediaType: z.string(),
|
||||
display_name: z.string().optional()
|
||||
})
|
||||
const zPreviewOutput = z
|
||||
.object({
|
||||
filename: z.string().optional(),
|
||||
subfolder: z.string().optional(),
|
||||
type: resultItemType.optional(),
|
||||
nodeId: z.string(),
|
||||
mediaType: z.string(),
|
||||
display_name: z.string().optional()
|
||||
})
|
||||
.passthrough()
|
||||
|
||||
/**
|
||||
* Execution error from Jobs API.
|
||||
|
||||
@@ -72,6 +72,8 @@ vi.mock('@/stores/modelToNodeStore', () => ({
|
||||
}))
|
||||
|
||||
// Mock TaskItemImpl
|
||||
const PREVIEWABLE_MEDIA_TYPES = new Set(['images', 'video', 'audio'])
|
||||
|
||||
vi.mock('@/stores/queueStore', () => ({
|
||||
TaskItemImpl: class {
|
||||
public flatOutputs: Array<{
|
||||
@@ -91,19 +93,28 @@ vi.mock('@/stores/queueStore', () => ({
|
||||
}
|
||||
| undefined
|
||||
public jobId: string
|
||||
public outputsCount: number | null
|
||||
|
||||
constructor(public job: JobListItem) {
|
||||
this.jobId = job.id
|
||||
this.flatOutputs = [
|
||||
{
|
||||
this.outputsCount = job.outputs_count ?? null
|
||||
const preview = job.preview_output
|
||||
const isPreviewable =
|
||||
!!preview?.filename && PREVIEWABLE_MEDIA_TYPES.has(preview.mediaType)
|
||||
if (preview && isPreviewable) {
|
||||
const item = {
|
||||
supportsPreview: true,
|
||||
filename: 'test.png',
|
||||
subfolder: '',
|
||||
type: 'output',
|
||||
url: 'http://test.com/test.png'
|
||||
filename: preview.filename!,
|
||||
subfolder: preview.subfolder ?? '',
|
||||
type: preview.type ?? 'output',
|
||||
url: `http://test.com/${preview.filename}`
|
||||
}
|
||||
]
|
||||
this.previewOutput = this.flatOutputs[0]
|
||||
this.flatOutputs = [item]
|
||||
this.previewOutput = item
|
||||
} else {
|
||||
this.flatOutputs = []
|
||||
this.previewOutput = undefined
|
||||
}
|
||||
}
|
||||
|
||||
get previewableOutputs() {
|
||||
@@ -200,6 +211,33 @@ describe('assetsStore - Refactored (Option A)', () => {
|
||||
expect(store.historyError).toBe(error)
|
||||
expect(store.historyLoading).toBe(false)
|
||||
})
|
||||
|
||||
it('should skip text-only jobs without breaking sibling image jobs', async () => {
|
||||
const mockHistory: JobListItem[] = [
|
||||
createMockJobItem(0),
|
||||
{
|
||||
id: 'text-only-job',
|
||||
status: 'completed',
|
||||
create_time: 2000,
|
||||
priority: 2000,
|
||||
preview_output: {
|
||||
content: 'some generated text',
|
||||
nodeId: '5',
|
||||
mediaType: 'text'
|
||||
} satisfies JobListItem['preview_output']
|
||||
},
|
||||
createMockJobItem(2)
|
||||
]
|
||||
vi.mocked(api.getHistory).mockResolvedValue(mockHistory)
|
||||
|
||||
await store.updateHistory()
|
||||
|
||||
expect(store.historyAssets).toHaveLength(2)
|
||||
expect(store.historyAssets.map((a) => a.id)).toEqual([
|
||||
'prompt_0',
|
||||
'prompt_2'
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('Pagination', () => {
|
||||
|
||||
@@ -191,6 +191,23 @@ describe('TaskItemImpl', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('should produce no previewable outputs for text-only preview_output', () => {
|
||||
const job: JobListItem = {
|
||||
...createHistoryJob(0, 'text-job'),
|
||||
preview_output: {
|
||||
nodeId: '5',
|
||||
mediaType: 'text'
|
||||
} satisfies JobListItem['preview_output']
|
||||
}
|
||||
|
||||
const task = new TaskItemImpl(job)
|
||||
|
||||
expect(task.flatOutputs).toHaveLength(1)
|
||||
expect(task.flatOutputs[0].filename).toBe('')
|
||||
expect(task.previewableOutputs).toHaveLength(0)
|
||||
expect(task.previewOutput).toBeUndefined()
|
||||
})
|
||||
|
||||
describe('error extraction getters', () => {
|
||||
it('errorMessage returns undefined when no execution_error', () => {
|
||||
const job = createHistoryJob(0, 'job-id')
|
||||
|
||||
Reference in New Issue
Block a user