diff --git a/src/platform/remote/comfyui/jobs/fetchers/fetchJobs.ts b/src/platform/remote/comfyui/jobs/fetchJobs.ts similarity index 76% rename from src/platform/remote/comfyui/jobs/fetchers/fetchJobs.ts rename to src/platform/remote/comfyui/jobs/fetchJobs.ts index bcaab87c7..136f683d6 100644 --- a/src/platform/remote/comfyui/jobs/fetchers/fetchJobs.ts +++ b/src/platform/remote/comfyui/jobs/fetchJobs.ts @@ -1,14 +1,11 @@ /** * @fileoverview Jobs API Fetchers - * @module platform/remote/comfyui/jobs/fetchers/fetchJobs + * @module platform/remote/comfyui/jobs/fetchJobs * * Unified jobs API fetcher for history, queue, and job details. * All distributions use the /jobs endpoint. */ -import { z } from 'zod' - -import type { ComfyWorkflowJSON } from '@/platform/workflow/validation/schemas/workflowSchema' import type { PromptId } from '@/schemas/apiSchema' import type { @@ -16,12 +13,8 @@ import type { JobListItem, JobStatus, RawJobListItem -} from '../types/jobTypes' -import { zJobDetail, zJobsListResponse } from '../types/jobTypes' - -// ============================================================================ -// Job List Fetchers -// ============================================================================ +} from './jobTypes' +import { zJobDetail, zJobsListResponse, zWorkflowContainer } from './jobTypes' interface FetchJobsRawResult { jobs: RawJobListItem[] @@ -119,10 +112,6 @@ export async function fetchQueue( } } -// ============================================================================ -// Job Detail Fetcher -// ============================================================================ - /** * Fetches full job details from /jobs/{job_id} */ @@ -145,33 +134,13 @@ export async function fetchJobDetail( } } -/** - * Schema for workflow container structure. - * Full workflow validation happens downstream via validateComfyWorkflow. - */ -const zWorkflowContainer = z.object({ - extra_data: z - .object({ - extra_pnginfo: z - .object({ - workflow: z.unknown() - }) - .optional() - }) - .optional() -}) - /** * Extracts workflow from job detail response. * The workflow is nested at: workflow.extra_data.extra_pnginfo.workflow + * Full workflow validation happens downstream via validateComfyWorkflow. */ -export function extractWorkflow( - job: JobDetail | undefined -): ComfyWorkflowJSON | undefined { +export function extractWorkflow(job: JobDetail | undefined): unknown { const parsed = zWorkflowContainer.safeParse(job?.workflow) if (!parsed.success) return undefined - // Full workflow validation happens downstream via validateComfyWorkflow - return parsed.data.extra_data?.extra_pnginfo?.workflow as - | ComfyWorkflowJSON - | undefined + return parsed.data.extra_data?.extra_pnginfo?.workflow } diff --git a/src/platform/remote/comfyui/jobs/index.ts b/src/platform/remote/comfyui/jobs/index.ts deleted file mode 100644 index b401c3457..000000000 --- a/src/platform/remote/comfyui/jobs/index.ts +++ /dev/null @@ -1,13 +0,0 @@ -/** - * @fileoverview Jobs API module - * @module platform/remote/comfyui/jobs - * - * Unified jobs API for history, queue, and job details. - */ - -export { - extractWorkflow, - fetchHistory, - fetchJobDetail, - fetchQueue -} from './fetchers/fetchJobs' diff --git a/src/platform/remote/comfyui/jobs/types/jobTypes.ts b/src/platform/remote/comfyui/jobs/jobTypes.ts similarity index 66% rename from src/platform/remote/comfyui/jobs/types/jobTypes.ts rename to src/platform/remote/comfyui/jobs/jobTypes.ts index 106963cff..23515f7a2 100644 --- a/src/platform/remote/comfyui/jobs/types/jobTypes.ts +++ b/src/platform/remote/comfyui/jobs/jobTypes.ts @@ -1,6 +1,6 @@ /** * @fileoverview Jobs API types - Backend job API format - * @module platform/remote/comfyui/jobs/types/jobTypes + * @module platform/remote/comfyui/jobs/jobTypes * * These types represent the jobs API format returned by the backend. * Jobs API provides a memory-optimized alternative to history API. @@ -10,10 +10,6 @@ import { z } from 'zod' import { resultItemType, zTaskOutput } from '@/schemas/apiSchema' -// ============================================================================ -// Zod Schemas -// ============================================================================ - const zJobStatus = z.enum([ 'pending', 'in_progress', @@ -22,13 +18,11 @@ const zJobStatus = z.enum([ 'cancelled' ]) -const zPreviewOutput = z - .object({ - filename: z.string(), - subfolder: z.string(), - type: resultItemType - }) - .passthrough() // Allow extra fields like nodeId, mediaType +const zPreviewOutput = z.object({ + filename: z.string(), + subfolder: z.string(), + type: resultItemType +}) /** * Execution error details for error jobs. @@ -60,8 +54,8 @@ const zRawJobListItem = z execution_start_time: z.number().nullable().optional(), execution_end_time: z.number().nullable().optional(), preview_output: zPreviewOutput.nullable().optional(), - outputs_count: z.number().optional(), - execution_error: zExecutionError.nullable().optional(), + outputs_count: z.number().nullable().optional(), + execution_error: zExecutionError.optional(), workflow_id: z.string().nullable().optional(), priority: z.number().optional() }) @@ -81,31 +75,30 @@ export const zJobDetail = zRawJobListItem }) .passthrough() -/** - * Pagination info from API - */ -const zPaginationInfo = z - .object({ - offset: z.number(), - limit: z.number(), - total: z.number(), - has_more: z.boolean() - }) - .passthrough() +const zPaginationInfo = z.object({ + offset: z.number(), + limit: z.number(), + total: z.number(), + has_more: z.boolean() +}) -/** - * Jobs list response structure - */ -export const zJobsListResponse = z - .object({ - jobs: z.array(zRawJobListItem), - pagination: zPaginationInfo - }) - .passthrough() +export const zJobsListResponse = z.object({ + jobs: z.array(zRawJobListItem), + pagination: zPaginationInfo +}) -// ============================================================================ -// TypeScript Types (derived from Zod schemas) -// ============================================================================ +/** Schema for workflow container structure in job detail responses */ +export const zWorkflowContainer = z.object({ + extra_data: z + .object({ + extra_pnginfo: z + .object({ + workflow: z.unknown() + }) + .optional() + }) + .optional() +}) export type JobStatus = z.infer export type RawJobListItem = z.infer diff --git a/tests-ui/tests/platform/remote/comfyui/jobs/fetchers/fetchJobs.test.ts b/tests-ui/tests/platform/remote/comfyui/jobs/fetchJobs.test.ts similarity index 85% rename from tests-ui/tests/platform/remote/comfyui/jobs/fetchers/fetchJobs.test.ts rename to tests-ui/tests/platform/remote/comfyui/jobs/fetchJobs.test.ts index 5b961f8cf..16466aace 100644 --- a/tests-ui/tests/platform/remote/comfyui/jobs/fetchers/fetchJobs.test.ts +++ b/tests-ui/tests/platform/remote/comfyui/jobs/fetchJobs.test.ts @@ -1,18 +1,24 @@ import { describe, expect, it, vi } from 'vitest' +import type { z } from 'zod' import { extractWorkflow, fetchHistory, fetchJobDetail, fetchQueue -} from '@/platform/remote/comfyui/jobs' +} from '@/platform/remote/comfyui/jobs/fetchJobs' +import type { + RawJobListItem, + zJobsListResponse +} from '@/platform/remote/comfyui/jobs/jobTypes' + +type JobsListResponse = z.infer -// Helper to create a mock job function createMockJob( id: string, status: 'pending' | 'in_progress' | 'completed' = 'completed', - overrides: Record = {} -) { + overrides: Partial = {} +): RawJobListItem { return { id, status, @@ -25,11 +31,10 @@ function createMockJob( } } -// Helper to create mock API response function createMockResponse( - jobs: ReturnType[], + jobs: RawJobListItem[], total: number = jobs.length -) { +): JobsListResponse { return { jobs, pagination: { @@ -89,6 +94,32 @@ describe('fetchJobs', () => { expect(result[2].priority).toBe(1) // total - 0 - 2 }) + it('calculates priority correctly with non-zero offset', async () => { + const mockFetch = vi.fn().mockResolvedValue({ + ok: true, + json: () => + Promise.resolve( + createMockResponse( + [ + createMockJob('job4', 'completed'), + createMockJob('job5', 'completed') + ], + 10 // total of 10 jobs + ) + ) + }) + + // Fetch page 2 (offset=5) + const result = await fetchHistory(mockFetch, 200, 5) + + expect(mockFetch).toHaveBeenCalledWith( + '/jobs?status=completed&limit=200&offset=5' + ) + // Priority base is total - offset = 10 - 5 = 5 + expect(result[0].priority).toBe(5) // (total - offset) - 0 + expect(result[1].priority).toBe(4) // (total - offset) - 1 + }) + it('preserves server-provided priority', async () => { const mockFetch = vi.fn().mockResolvedValue({ ok: true,