mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-29 10:42:44 +00:00
[feat] Add Jobs API infrastructure (PR 1 of 3)
Adds Jobs API types, fetchers, and new API methods without breaking existing code. This is the foundation for migrating from legacy /history and /queue endpoints to the unified /jobs endpoint. New files: - src/platform/remote/comfyui/jobs/types/jobTypes.ts - Zod schemas for Jobs API - src/platform/remote/comfyui/jobs/fetchers/fetchJobs.ts - Fetchers for /jobs endpoint - src/platform/remote/comfyui/jobs/index.ts - Barrel exports - tests-ui/tests/platform/remote/comfyui/jobs/fetchers/fetchJobs.test.ts API additions (non-breaking): - api.getQueueFromJobsApi() - Queue from /jobs endpoint - api.getHistoryFromJobsApi() - History from /jobs endpoint - api.getJobDetail() - Full job details including workflow and outputs Part of Jobs API migration. See docs/JOBS_API_MIGRATION_PLAN.md for details. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,260 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import {
|
||||
extractWorkflow,
|
||||
fetchHistory,
|
||||
fetchJobDetail,
|
||||
fetchQueue
|
||||
} from '@/platform/remote/comfyui/jobs'
|
||||
|
||||
// Helper to create a mock job
|
||||
function createMockJob(
|
||||
id: string,
|
||||
status: 'pending' | 'in_progress' | 'completed' = 'completed',
|
||||
overrides: Record<string, unknown> = {}
|
||||
) {
|
||||
return {
|
||||
id,
|
||||
status,
|
||||
create_time: Date.now(),
|
||||
execution_start_time: null,
|
||||
execution_end_time: null,
|
||||
preview_output: null,
|
||||
outputs_count: 0,
|
||||
...overrides
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to create mock API response
|
||||
function createMockResponse(
|
||||
jobs: ReturnType<typeof createMockJob>[],
|
||||
total: number = jobs.length
|
||||
) {
|
||||
return {
|
||||
jobs,
|
||||
pagination: {
|
||||
offset: 0,
|
||||
limit: 200,
|
||||
total,
|
||||
has_more: false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
describe('fetchJobs', () => {
|
||||
describe('fetchHistory', () => {
|
||||
it('fetches completed jobs', async () => {
|
||||
const mockFetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve(
|
||||
createMockResponse([
|
||||
createMockJob('job1', 'completed'),
|
||||
createMockJob('job2', 'completed')
|
||||
])
|
||||
)
|
||||
})
|
||||
|
||||
const result = await fetchHistory(mockFetch)
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith(
|
||||
'/jobs?status=completed&limit=200&offset=0'
|
||||
)
|
||||
expect(result).toHaveLength(2)
|
||||
expect(result[0].id).toBe('job1')
|
||||
expect(result[1].id).toBe('job2')
|
||||
})
|
||||
|
||||
it('assigns synthetic priorities', async () => {
|
||||
const mockFetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve(
|
||||
createMockResponse(
|
||||
[
|
||||
createMockJob('job1', 'completed'),
|
||||
createMockJob('job2', 'completed'),
|
||||
createMockJob('job3', 'completed')
|
||||
],
|
||||
3
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
const result = await fetchHistory(mockFetch)
|
||||
|
||||
// Priority should be assigned from total down
|
||||
expect(result[0].priority).toBe(3) // total - 0 - 0
|
||||
expect(result[1].priority).toBe(2) // total - 0 - 1
|
||||
expect(result[2].priority).toBe(1) // total - 0 - 2
|
||||
})
|
||||
|
||||
it('preserves server-provided priority', async () => {
|
||||
const mockFetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve(
|
||||
createMockResponse([
|
||||
createMockJob('job1', 'completed', { priority: 999 })
|
||||
])
|
||||
)
|
||||
})
|
||||
|
||||
const result = await fetchHistory(mockFetch)
|
||||
|
||||
expect(result[0].priority).toBe(999)
|
||||
})
|
||||
|
||||
it('returns empty array on error', async () => {
|
||||
const mockFetch = vi.fn().mockRejectedValue(new Error('Network error'))
|
||||
|
||||
const result = await fetchHistory(mockFetch)
|
||||
|
||||
expect(result).toEqual([])
|
||||
})
|
||||
|
||||
it('returns empty array on non-ok response', async () => {
|
||||
const mockFetch = vi.fn().mockResolvedValue({
|
||||
ok: false,
|
||||
status: 500
|
||||
})
|
||||
|
||||
const result = await fetchHistory(mockFetch)
|
||||
|
||||
expect(result).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('fetchQueue', () => {
|
||||
it('fetches running and pending jobs', async () => {
|
||||
const mockFetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve(
|
||||
createMockResponse([
|
||||
createMockJob('running1', 'in_progress'),
|
||||
createMockJob('pending1', 'pending'),
|
||||
createMockJob('pending2', 'pending')
|
||||
])
|
||||
)
|
||||
})
|
||||
|
||||
const result = await fetchQueue(mockFetch)
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith(
|
||||
'/jobs?status=in_progress,pending&limit=200&offset=0'
|
||||
)
|
||||
expect(result.Running).toHaveLength(1)
|
||||
expect(result.Pending).toHaveLength(2)
|
||||
expect(result.Running[0].id).toBe('running1')
|
||||
expect(result.Pending[0].id).toBe('pending1')
|
||||
})
|
||||
|
||||
it('assigns queue priorities above history', async () => {
|
||||
const mockFetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve(
|
||||
createMockResponse([
|
||||
createMockJob('running1', 'in_progress'),
|
||||
createMockJob('pending1', 'pending')
|
||||
])
|
||||
)
|
||||
})
|
||||
|
||||
const result = await fetchQueue(mockFetch)
|
||||
|
||||
// Queue priorities should be above 1_000_000 (QUEUE_PRIORITY_BASE)
|
||||
expect(result.Running[0].priority).toBeGreaterThan(1_000_000)
|
||||
expect(result.Pending[0].priority).toBeGreaterThan(1_000_000)
|
||||
// Pending should have higher priority than running
|
||||
expect(result.Pending[0].priority).toBeGreaterThan(
|
||||
result.Running[0].priority
|
||||
)
|
||||
})
|
||||
|
||||
it('returns empty arrays on error', async () => {
|
||||
const mockFetch = vi.fn().mockRejectedValue(new Error('Network error'))
|
||||
|
||||
const result = await fetchQueue(mockFetch)
|
||||
|
||||
expect(result).toEqual({ Running: [], Pending: [] })
|
||||
})
|
||||
})
|
||||
|
||||
describe('fetchJobDetail', () => {
|
||||
it('fetches job detail by id', async () => {
|
||||
const jobDetail = {
|
||||
...createMockJob('job1', 'completed'),
|
||||
workflow: { extra_data: { extra_pnginfo: { workflow: {} } } },
|
||||
outputs: {
|
||||
'1': {
|
||||
images: [{ filename: 'test.png', subfolder: '', type: 'output' }]
|
||||
}
|
||||
}
|
||||
}
|
||||
const mockFetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(jobDetail)
|
||||
})
|
||||
|
||||
const result = await fetchJobDetail(mockFetch, 'job1')
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith('/jobs/job1')
|
||||
expect(result?.id).toBe('job1')
|
||||
expect(result?.outputs).toBeDefined()
|
||||
})
|
||||
|
||||
it('returns undefined for non-ok response', async () => {
|
||||
const mockFetch = vi.fn().mockResolvedValue({
|
||||
ok: false,
|
||||
status: 404
|
||||
})
|
||||
|
||||
const result = await fetchJobDetail(mockFetch, 'nonexistent')
|
||||
|
||||
expect(result).toBeUndefined()
|
||||
})
|
||||
|
||||
it('returns undefined on error', async () => {
|
||||
const mockFetch = vi.fn().mockRejectedValue(new Error('Network error'))
|
||||
|
||||
const result = await fetchJobDetail(mockFetch, 'job1')
|
||||
|
||||
expect(result).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('extractWorkflow', () => {
|
||||
it('extracts workflow from nested structure', () => {
|
||||
const jobDetail = {
|
||||
...createMockJob('job1', 'completed'),
|
||||
workflow: {
|
||||
extra_data: {
|
||||
extra_pnginfo: {
|
||||
workflow: { nodes: [], links: [] }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const workflow = extractWorkflow(jobDetail)
|
||||
|
||||
expect(workflow).toEqual({ nodes: [], links: [] })
|
||||
})
|
||||
|
||||
it('returns undefined if workflow not present', () => {
|
||||
const jobDetail = createMockJob('job1', 'completed')
|
||||
|
||||
const workflow = extractWorkflow(jobDetail)
|
||||
|
||||
expect(workflow).toBeUndefined()
|
||||
})
|
||||
|
||||
it('returns undefined for undefined input', () => {
|
||||
const workflow = extractWorkflow(undefined)
|
||||
|
||||
expect(workflow).toBeUndefined()
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user