Address PR review comments

- Flatten directory structure: remove barrel file, move files up to /jobs
- Remove .passthrough() from zPreviewOutput, zPaginationInfo, zJobsListResponse
- Align nullable/optional with OpenAPI spec (outputs_count now nullable)
- Remove big comment block sections
- Change extractWorkflow return type to unknown
- Move zWorkflowContainer schema to jobTypes.ts
- Add interface types to test helper functions
- Add fetchHistory offset test for priority calculation

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Richard Yu
2025-12-08 14:32:39 -08:00
parent 0855b85b4f
commit 5bd6bff4f0
4 changed files with 74 additions and 94 deletions

View File

@@ -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
}

View File

@@ -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'

View File

@@ -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<typeof zJobStatus>
export type RawJobListItem = z.infer<typeof zRawJobListItem>

View File

@@ -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<typeof zJobsListResponse>
// Helper to create a mock job
function createMockJob(
id: string,
status: 'pending' | 'in_progress' | 'completed' = 'completed',
overrides: Record<string, unknown> = {}
) {
overrides: Partial<RawJobListItem> = {}
): RawJobListItem {
return {
id,
status,
@@ -25,11 +31,10 @@ function createMockJob(
}
}
// Helper to create mock API response
function createMockResponse(
jobs: ReturnType<typeof createMockJob>[],
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,