mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-24 08:19:51 +00:00
[feat] Migrate to Jobs API (PR 2 of 3)
This PR switches the frontend from legacy /history and /queue endpoints to the unified /jobs API. Key changes: - Rewrite TaskItemImpl in queueStore.ts to wrap JobListItem - Update api.ts getQueue()/getHistory() to use Jobs API - Update all queue composables (useJobList, useJobMenu, useResultGallery) - Update useJobErrorReporting to use execution_error.exception_message - Update JobGroupsList.vue workflowId access - Update reconciliation.ts to work with JobListItem - Update all related tests Breaking changes: - getQueue() now returns JobListItem[] instead of legacy tuple format - getHistory() now returns JobListItem[] instead of HistoryTaskItem[] - TaskItemImpl.outputs now lazily loads via loadFullOutputs() Part of Jobs API migration. Depends on PR 1 (jobs-api-pr1-infrastructure). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,185 +0,0 @@
|
||||
/**
|
||||
* @fileoverview Unit tests for V2 to V1 history adapter.
|
||||
*/
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { mapHistoryV2toHistory } from '@/platform/remote/comfyui/history/adapters/v2ToV1Adapter'
|
||||
import { zRawHistoryItemV2 } from '@/platform/remote/comfyui/history/types/historyV2Types'
|
||||
import type { HistoryResponseV2 } from '@/platform/remote/comfyui/history/types/historyV2Types'
|
||||
|
||||
import {
|
||||
expectedV1Fixture,
|
||||
historyV2Fixture
|
||||
} from '@tests-ui/fixtures/historyFixtures'
|
||||
import {
|
||||
historyV2FiveItemsSorting,
|
||||
historyV2MultipleNoTimestamp,
|
||||
historyV2WithMissingTimestamp
|
||||
} from '@tests-ui/fixtures/historySortingFixtures'
|
||||
import type { HistoryTaskItem } from '@/platform/remote/comfyui/history/types/historyV1Types'
|
||||
|
||||
function findResultByPromptId(
|
||||
result: HistoryTaskItem[],
|
||||
promptId: string
|
||||
): HistoryTaskItem {
|
||||
const item = result.find((item) => item.prompt[1] === promptId)
|
||||
if (!item) {
|
||||
throw new Error(`Expected item with promptId ${promptId} not found`)
|
||||
}
|
||||
return item
|
||||
}
|
||||
|
||||
describe('mapHistoryV2toHistory', () => {
|
||||
describe('fixture validation', () => {
|
||||
it('should have valid fixture data', () => {
|
||||
// Validate all items in the fixture to ensure test data is correct
|
||||
historyV2Fixture.history.forEach((item: unknown) => {
|
||||
expect(() => zRawHistoryItemV2.parse(item)).not.toThrow()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('given a complete V2 history response with edge cases', () => {
|
||||
const history = mapHistoryV2toHistory(historyV2Fixture)
|
||||
|
||||
it('should transform all items to V1 format with correct structure', () => {
|
||||
expect(history).toEqual(expectedV1Fixture)
|
||||
})
|
||||
|
||||
it('should add taskType "History" to all items', () => {
|
||||
history.forEach((item) => {
|
||||
expect(item.taskType).toBe('History')
|
||||
})
|
||||
})
|
||||
|
||||
it('should transform prompt to V1 tuple [priority, id, {}, extra_data, outputNodeIds]', () => {
|
||||
const firstItem = history[0]
|
||||
|
||||
expect(firstItem.prompt[0]).toBe(1) // Synthetic priority based on timestamp
|
||||
expect(firstItem.prompt[1]).toBe('complete-item-id')
|
||||
expect(firstItem.prompt[2]).toEqual({}) // history v2 does not return this data
|
||||
expect(firstItem.prompt[3]).toMatchObject({ client_id: 'test-client' })
|
||||
expect(firstItem.prompt[4]).toEqual(['9'])
|
||||
})
|
||||
|
||||
it('should handle missing optional status field', () => {
|
||||
expect(history[1].prompt[1]).toBe('no-status-id')
|
||||
expect(history[1].status).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should handle missing optional meta field', () => {
|
||||
expect(history[2].prompt[1]).toBe('no-meta-id')
|
||||
expect(history[2].meta).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should derive output node IDs from outputs object keys', () => {
|
||||
const multiOutputItem = history[3]
|
||||
|
||||
expect(multiOutputItem.prompt[4]).toEqual(
|
||||
expect.arrayContaining(['3', '9', '12'])
|
||||
)
|
||||
expect(multiOutputItem.prompt[4]).toHaveLength(3)
|
||||
})
|
||||
})
|
||||
|
||||
describe('given empty history array', () => {
|
||||
it('should return empty array', () => {
|
||||
const emptyResponse: HistoryResponseV2 = { history: [] }
|
||||
const history = mapHistoryV2toHistory(emptyResponse)
|
||||
|
||||
expect(history).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('given empty outputs object', () => {
|
||||
it('should return empty array for output node IDs', () => {
|
||||
const v2Response: HistoryResponseV2 = {
|
||||
history: [
|
||||
{
|
||||
prompt_id: 'test-id',
|
||||
prompt: {
|
||||
priority: 0,
|
||||
prompt_id: 'test-id',
|
||||
extra_data: { client_id: 'test' }
|
||||
},
|
||||
outputs: {}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const history = mapHistoryV2toHistory(v2Response)
|
||||
|
||||
expect(history[0].prompt[4]).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('given missing client_id', () => {
|
||||
it('should accept history items without client_id', () => {
|
||||
const v2Response: HistoryResponseV2 = {
|
||||
history: [
|
||||
{
|
||||
prompt_id: 'test-id',
|
||||
prompt: {
|
||||
priority: 0,
|
||||
prompt_id: 'test-id',
|
||||
extra_data: {}
|
||||
},
|
||||
outputs: {}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const history = mapHistoryV2toHistory(v2Response)
|
||||
|
||||
expect(history[0].prompt[3].client_id).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('timestamp-based priority assignment', () => {
|
||||
it('assigns priority 0 to items without execution_success timestamp', () => {
|
||||
const result = mapHistoryV2toHistory(historyV2WithMissingTimestamp)
|
||||
|
||||
expect(result).toHaveLength(3)
|
||||
|
||||
const item1000 = findResultByPromptId(result, 'item-timestamp-1000')
|
||||
const item2000 = findResultByPromptId(result, 'item-timestamp-2000')
|
||||
const itemNoTimestamp = findResultByPromptId(result, 'item-no-timestamp')
|
||||
|
||||
expect(item2000.prompt[0]).toBe(2)
|
||||
expect(item1000.prompt[0]).toBe(1)
|
||||
expect(itemNoTimestamp.prompt[0]).toBe(0)
|
||||
})
|
||||
|
||||
it('correctly sorts and assigns priorities for multiple items', () => {
|
||||
const result = mapHistoryV2toHistory(historyV2FiveItemsSorting)
|
||||
|
||||
expect(result).toHaveLength(5)
|
||||
|
||||
const item1000 = findResultByPromptId(result, 'item-timestamp-1000')
|
||||
const item2000 = findResultByPromptId(result, 'item-timestamp-2000')
|
||||
const item3000 = findResultByPromptId(result, 'item-timestamp-3000')
|
||||
const item4000 = findResultByPromptId(result, 'item-timestamp-4000')
|
||||
const item5000 = findResultByPromptId(result, 'item-timestamp-5000')
|
||||
|
||||
expect(item5000.prompt[0]).toBe(5)
|
||||
expect(item4000.prompt[0]).toBe(4)
|
||||
expect(item3000.prompt[0]).toBe(3)
|
||||
expect(item2000.prompt[0]).toBe(2)
|
||||
expect(item1000.prompt[0]).toBe(1)
|
||||
})
|
||||
|
||||
it('assigns priority 0 to all items when multiple items lack timestamps', () => {
|
||||
const result = mapHistoryV2toHistory(historyV2MultipleNoTimestamp)
|
||||
|
||||
expect(result).toHaveLength(3)
|
||||
|
||||
const item1 = findResultByPromptId(result, 'item-no-timestamp-1')
|
||||
const item2 = findResultByPromptId(result, 'item-no-timestamp-2')
|
||||
const item3 = findResultByPromptId(result, 'item-no-timestamp-3')
|
||||
|
||||
expect(item1.prompt[0]).toBe(0)
|
||||
expect(item2.prompt[0]).toBe(0)
|
||||
expect(item3.prompt[0]).toBe(0)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,52 +0,0 @@
|
||||
/**
|
||||
* @fileoverview Unit tests for V1 history fetcher.
|
||||
*/
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { fetchHistoryV1 } from '@/platform/remote/comfyui/history/fetchers/fetchHistoryV1'
|
||||
|
||||
import { historyV1RawResponse } from '@tests-ui/fixtures/historyFixtures'
|
||||
|
||||
describe('fetchHistoryV1', () => {
|
||||
const mockFetchApi = vi.fn().mockResolvedValue({
|
||||
json: async () => historyV1RawResponse
|
||||
})
|
||||
|
||||
it('should fetch from /history endpoint with default max_items', async () => {
|
||||
await fetchHistoryV1(mockFetchApi)
|
||||
|
||||
expect(mockFetchApi).toHaveBeenCalledWith('/history?max_items=200')
|
||||
})
|
||||
|
||||
it('should fetch with custom max_items parameter', async () => {
|
||||
await fetchHistoryV1(mockFetchApi, 50)
|
||||
|
||||
expect(mockFetchApi).toHaveBeenCalledWith('/history?max_items=50')
|
||||
})
|
||||
|
||||
it('should transform object response to array with taskType and preserve fields', async () => {
|
||||
const result = await fetchHistoryV1(mockFetchApi)
|
||||
|
||||
expect(result.History).toHaveLength(2)
|
||||
result.History.forEach((item) => {
|
||||
expect(item.taskType).toBe('History')
|
||||
})
|
||||
expect(result.History[0]).toMatchObject({
|
||||
taskType: 'History',
|
||||
prompt: [24, 'complete-item-id', {}, expect.any(Object), ['9']],
|
||||
outputs: expect.any(Object),
|
||||
status: expect.any(Object),
|
||||
meta: expect.any(Object)
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle empty response object', async () => {
|
||||
const emptyMock = vi.fn().mockResolvedValue({
|
||||
json: async () => ({})
|
||||
})
|
||||
|
||||
const result = await fetchHistoryV1(emptyMock)
|
||||
|
||||
expect(result.History).toEqual([])
|
||||
})
|
||||
})
|
||||
@@ -1,41 +0,0 @@
|
||||
/**
|
||||
* @fileoverview Unit tests for V2 history fetcher.
|
||||
*/
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { fetchHistoryV2 } from '@/platform/remote/comfyui/history/fetchers/fetchHistoryV2'
|
||||
|
||||
import {
|
||||
expectedV1Fixture,
|
||||
historyV2Fixture
|
||||
} from '@tests-ui/fixtures/historyFixtures'
|
||||
|
||||
describe('fetchHistoryV2', () => {
|
||||
const mockFetchApi = vi.fn().mockResolvedValue({
|
||||
json: async () => historyV2Fixture
|
||||
})
|
||||
|
||||
it('should fetch from /history_v2 endpoint with default max_items', async () => {
|
||||
await fetchHistoryV2(mockFetchApi)
|
||||
|
||||
expect(mockFetchApi).toHaveBeenCalledWith('/history_v2?max_items=200')
|
||||
})
|
||||
|
||||
it('should fetch with custom max_items parameter', async () => {
|
||||
await fetchHistoryV2(mockFetchApi, 50)
|
||||
|
||||
expect(mockFetchApi).toHaveBeenCalledWith('/history_v2?max_items=50')
|
||||
})
|
||||
|
||||
it('should adapt V2 response to V1-compatible format', async () => {
|
||||
const result = await fetchHistoryV2(mockFetchApi)
|
||||
|
||||
expect(result.History).toEqual(expectedV1Fixture)
|
||||
expect(result).toHaveProperty('History')
|
||||
expect(Array.isArray(result.History)).toBe(true)
|
||||
result.History.forEach((item) => {
|
||||
expect(item.taskType).toBe('History')
|
||||
expect(item.prompt).toHaveLength(5)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,333 +1,129 @@
|
||||
/**
|
||||
* @fileoverview Tests for history reconciliation (V1 and V2)
|
||||
* @fileoverview Tests for job list reconciliation
|
||||
*/
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { reconcileHistory } from '@/platform/remote/comfyui/history/reconciliation'
|
||||
import type { TaskItem } from '@/schemas/apiSchema'
|
||||
import { reconcileJobs } from '@/platform/remote/comfyui/history/reconciliation'
|
||||
import type { JobListItem } from '@/platform/remote/comfyui/jobs/jobTypes'
|
||||
|
||||
// Mock distribution types
|
||||
vi.mock('@/platform/distribution/types', () => ({
|
||||
isCloud: false,
|
||||
isDesktop: true
|
||||
}))
|
||||
|
||||
function createHistoryItem(promptId: string, queueIndex = 0): TaskItem {
|
||||
function createJob(id: string, createTime = 0, priority?: number): JobListItem {
|
||||
return {
|
||||
taskType: 'History',
|
||||
prompt: [queueIndex, promptId, {}, {}, []],
|
||||
status: { status_str: 'success', completed: true, messages: [] },
|
||||
outputs: {}
|
||||
id,
|
||||
status: 'completed',
|
||||
create_time: createTime,
|
||||
priority: priority ?? createTime
|
||||
}
|
||||
}
|
||||
|
||||
function getAllPromptIds(result: TaskItem[]): string[] {
|
||||
return result.map((item) => item.prompt[1])
|
||||
function getAllIds(result: JobListItem[]): string[] {
|
||||
return result.map((item) => item.id)
|
||||
}
|
||||
|
||||
describe('reconcileHistory (V1)', () => {
|
||||
beforeEach(async () => {
|
||||
const distTypes = await import('@/platform/distribution/types')
|
||||
vi.mocked(distTypes).isCloud = false
|
||||
})
|
||||
|
||||
describe('when filtering by queueIndex', () => {
|
||||
it('should retain items with queueIndex greater than lastKnownQueueIndex', () => {
|
||||
const serverHistory = [
|
||||
createHistoryItem('new-1', 11),
|
||||
createHistoryItem('new-2', 10),
|
||||
createHistoryItem('old', 5)
|
||||
]
|
||||
const clientHistory = [createHistoryItem('old', 5)]
|
||||
|
||||
const result = reconcileHistory(serverHistory, clientHistory, 10, 9)
|
||||
|
||||
const promptIds = getAllPromptIds(result)
|
||||
expect(promptIds).toHaveLength(3)
|
||||
expect(promptIds).toContain('new-1')
|
||||
expect(promptIds).toContain('new-2')
|
||||
expect(promptIds).toContain('old')
|
||||
})
|
||||
|
||||
it('should evict items with queueIndex less than or equal to lastKnownQueueIndex', () => {
|
||||
const serverHistory = [
|
||||
createHistoryItem('new', 11),
|
||||
createHistoryItem('existing', 10),
|
||||
createHistoryItem('old-should-not-appear', 5)
|
||||
]
|
||||
const clientHistory = [createHistoryItem('existing', 10)]
|
||||
|
||||
const result = reconcileHistory(serverHistory, clientHistory, 10, 10)
|
||||
|
||||
const promptIds = getAllPromptIds(result)
|
||||
expect(promptIds).toHaveLength(2)
|
||||
expect(promptIds).toContain('new')
|
||||
expect(promptIds).toContain('existing')
|
||||
expect(promptIds).not.toContain('old-should-not-appear')
|
||||
})
|
||||
|
||||
it('should retain all server items when lastKnownQueueIndex is undefined', () => {
|
||||
const serverHistory = [
|
||||
createHistoryItem('item-1', 5),
|
||||
createHistoryItem('item-2', 4)
|
||||
]
|
||||
|
||||
const result = reconcileHistory(serverHistory, [], 10, undefined)
|
||||
|
||||
expect(result).toHaveLength(2)
|
||||
expect(result[0].prompt[1]).toBe('item-1')
|
||||
expect(result[1].prompt[1]).toBe('item-2')
|
||||
})
|
||||
})
|
||||
|
||||
describe('when reconciling with existing client items', () => {
|
||||
it('should retain client items that still exist on server', () => {
|
||||
const serverHistory = [
|
||||
createHistoryItem('new', 11),
|
||||
createHistoryItem('existing-1', 9),
|
||||
createHistoryItem('existing-2', 8)
|
||||
]
|
||||
const clientHistory = [
|
||||
createHistoryItem('existing-1', 9),
|
||||
createHistoryItem('existing-2', 8)
|
||||
]
|
||||
|
||||
const result = reconcileHistory(serverHistory, clientHistory, 10, 10)
|
||||
|
||||
const promptIds = getAllPromptIds(result)
|
||||
expect(promptIds).toHaveLength(3)
|
||||
expect(promptIds).toContain('new')
|
||||
expect(promptIds).toContain('existing-1')
|
||||
expect(promptIds).toContain('existing-2')
|
||||
})
|
||||
|
||||
it('should evict client items that no longer exist on server', () => {
|
||||
const serverHistory = [
|
||||
createHistoryItem('new', 11),
|
||||
createHistoryItem('keep', 9)
|
||||
]
|
||||
const clientHistory = [
|
||||
createHistoryItem('keep', 9),
|
||||
createHistoryItem('removed-from-server', 8)
|
||||
]
|
||||
|
||||
const result = reconcileHistory(serverHistory, clientHistory, 10, 10)
|
||||
|
||||
const promptIds = getAllPromptIds(result)
|
||||
expect(promptIds).toHaveLength(2)
|
||||
expect(promptIds).toContain('new')
|
||||
expect(promptIds).toContain('keep')
|
||||
expect(promptIds).not.toContain('removed-from-server')
|
||||
})
|
||||
})
|
||||
|
||||
describe('when limiting the result count', () => {
|
||||
it('should respect the maxItems constraint', () => {
|
||||
const serverHistory = Array.from({ length: 10 }, (_, i) =>
|
||||
createHistoryItem(`item-${i}`, 20 + i)
|
||||
)
|
||||
|
||||
const result = reconcileHistory(serverHistory, [], 5, 15)
|
||||
|
||||
const promptIds = getAllPromptIds(result)
|
||||
expect(promptIds).toHaveLength(5)
|
||||
})
|
||||
|
||||
it('should evict lowest priority items when exceeding capacity', () => {
|
||||
const serverHistory = [
|
||||
createHistoryItem('new-1', 13),
|
||||
createHistoryItem('new-2', 12),
|
||||
createHistoryItem('new-3', 11),
|
||||
createHistoryItem('existing', 9)
|
||||
]
|
||||
const clientHistory = [createHistoryItem('existing', 9)]
|
||||
|
||||
const result = reconcileHistory(serverHistory, clientHistory, 2, 10)
|
||||
|
||||
expect(result).toHaveLength(2)
|
||||
expect(result[0].prompt[1]).toBe('new-1')
|
||||
expect(result[1].prompt[1]).toBe('new-2')
|
||||
})
|
||||
})
|
||||
|
||||
describe('when handling empty collections', () => {
|
||||
it('should return all server items when client history is empty', () => {
|
||||
const serverHistory = [
|
||||
createHistoryItem('item-1', 10),
|
||||
createHistoryItem('item-2', 9)
|
||||
]
|
||||
|
||||
const result = reconcileHistory(serverHistory, [], 10, 8)
|
||||
|
||||
const promptIds = getAllPromptIds(result)
|
||||
expect(promptIds).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('should return empty result when server history is empty', () => {
|
||||
const clientHistory = [createHistoryItem('item-1', 5)]
|
||||
|
||||
const result = reconcileHistory([], clientHistory, 10, 5)
|
||||
|
||||
expect(result).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('should return empty result when both collections are empty', () => {
|
||||
const result = reconcileHistory([], [], 10, undefined)
|
||||
|
||||
expect(result).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('reconcileHistory (V2/Cloud)', () => {
|
||||
beforeEach(async () => {
|
||||
const distTypes = await import('@/platform/distribution/types')
|
||||
vi.mocked(distTypes).isCloud = true
|
||||
})
|
||||
|
||||
describe('reconcileJobs', () => {
|
||||
describe('when adding new items from server', () => {
|
||||
it('should retain items with promptIds not present in client history', () => {
|
||||
it('should retain items with IDs not present in client history', () => {
|
||||
const serverHistory = [
|
||||
createHistoryItem('new-item'),
|
||||
createHistoryItem('existing-item')
|
||||
createJob('new-item', 10),
|
||||
createJob('existing-item', 5)
|
||||
]
|
||||
const clientHistory = [createHistoryItem('existing-item')]
|
||||
const clientHistory = [createJob('existing-item', 5)]
|
||||
|
||||
const result = reconcileHistory(serverHistory, clientHistory, 10)
|
||||
const result = reconcileJobs(serverHistory, clientHistory, 10)
|
||||
|
||||
const promptIds = getAllPromptIds(result)
|
||||
expect(promptIds).toHaveLength(2)
|
||||
expect(promptIds).toContain('new-item')
|
||||
expect(promptIds).toContain('existing-item')
|
||||
const ids = getAllIds(result)
|
||||
expect(ids).toHaveLength(2)
|
||||
expect(ids).toContain('new-item')
|
||||
expect(ids).toContain('existing-item')
|
||||
})
|
||||
|
||||
it('should respect priority ordering when retaining multiple new items', () => {
|
||||
it('should respect create_time ordering when adding multiple new items', () => {
|
||||
const serverHistory = [
|
||||
createHistoryItem('new-1'),
|
||||
createHistoryItem('new-2'),
|
||||
createHistoryItem('existing')
|
||||
createJob('new-1', 20),
|
||||
createJob('new-2', 15),
|
||||
createJob('existing', 10)
|
||||
]
|
||||
const clientHistory = [createHistoryItem('existing')]
|
||||
const clientHistory = [createJob('existing', 10)]
|
||||
|
||||
const result = reconcileHistory(serverHistory, clientHistory, 10)
|
||||
const result = reconcileJobs(serverHistory, clientHistory, 10)
|
||||
|
||||
const promptIds = getAllPromptIds(result)
|
||||
expect(promptIds).toHaveLength(3)
|
||||
expect(promptIds).toContain('new-1')
|
||||
expect(promptIds).toContain('new-2')
|
||||
expect(promptIds).toContain('existing')
|
||||
expect(result).toHaveLength(3)
|
||||
expect(result[0].id).toBe('new-1')
|
||||
expect(result[1].id).toBe('new-2')
|
||||
expect(result[2].id).toBe('existing')
|
||||
})
|
||||
})
|
||||
|
||||
describe('when reconciling with existing client items', () => {
|
||||
it('should retain client items that still exist on server', () => {
|
||||
const serverHistory = [
|
||||
createHistoryItem('item-1'),
|
||||
createHistoryItem('item-2')
|
||||
]
|
||||
const clientHistory = [
|
||||
createHistoryItem('item-1'),
|
||||
createHistoryItem('item-2')
|
||||
]
|
||||
const serverHistory = [createJob('item-1', 10), createJob('item-2', 5)]
|
||||
const clientHistory = [createJob('item-1', 10), createJob('item-2', 5)]
|
||||
|
||||
const result = reconcileHistory(serverHistory, clientHistory, 10)
|
||||
const result = reconcileJobs(serverHistory, clientHistory, 10)
|
||||
|
||||
const promptIds = getAllPromptIds(result)
|
||||
expect(promptIds).toHaveLength(2)
|
||||
expect(promptIds).toContain('item-1')
|
||||
expect(promptIds).toContain('item-2')
|
||||
const ids = getAllIds(result)
|
||||
expect(ids).toHaveLength(2)
|
||||
expect(ids).toContain('item-1')
|
||||
expect(ids).toContain('item-2')
|
||||
})
|
||||
|
||||
it('should evict client items that no longer exist on server', () => {
|
||||
const serverHistory = [createHistoryItem('item-1')]
|
||||
const clientHistory = [
|
||||
createHistoryItem('item-1'),
|
||||
createHistoryItem('old-item')
|
||||
]
|
||||
const serverHistory = [createJob('item-1', 10)]
|
||||
const clientHistory = [createJob('item-1', 10), createJob('old-item', 5)]
|
||||
|
||||
const result = reconcileHistory(serverHistory, clientHistory, 10)
|
||||
const result = reconcileJobs(serverHistory, clientHistory, 10)
|
||||
|
||||
const promptIds = getAllPromptIds(result)
|
||||
expect(promptIds).toHaveLength(1)
|
||||
expect(promptIds).toContain('item-1')
|
||||
expect(promptIds).not.toContain('old-item')
|
||||
})
|
||||
})
|
||||
|
||||
describe('when detecting new items by promptId', () => {
|
||||
it('should retain new items regardless of queueIndex values', () => {
|
||||
const serverHistory = [
|
||||
createHistoryItem('existing', 100),
|
||||
createHistoryItem('new-item', 50)
|
||||
]
|
||||
const clientHistory = [createHistoryItem('existing', 100)]
|
||||
|
||||
const result = reconcileHistory(serverHistory, clientHistory, 10)
|
||||
|
||||
const promptIds = getAllPromptIds(result)
|
||||
expect(promptIds).toContain('new-item')
|
||||
expect(promptIds).toContain('existing')
|
||||
const ids = getAllIds(result)
|
||||
expect(ids).toHaveLength(1)
|
||||
expect(ids).toContain('item-1')
|
||||
expect(ids).not.toContain('old-item')
|
||||
})
|
||||
})
|
||||
|
||||
describe('when limiting the result count', () => {
|
||||
it('should respect the maxItems constraint', () => {
|
||||
const serverHistory = Array.from({ length: 10 }, (_, i) =>
|
||||
createHistoryItem(`server-${i}`)
|
||||
)
|
||||
const clientHistory = Array.from({ length: 5 }, (_, i) =>
|
||||
createHistoryItem(`client-${i}`)
|
||||
createJob(`item-${i}`, 100 - i)
|
||||
)
|
||||
|
||||
const result = reconcileHistory(serverHistory, clientHistory, 5)
|
||||
const result = reconcileJobs(serverHistory, [], 5)
|
||||
|
||||
const promptIds = getAllPromptIds(result)
|
||||
expect(promptIds).toHaveLength(5)
|
||||
expect(result).toHaveLength(5)
|
||||
})
|
||||
|
||||
it('should evict lowest priority items when exceeding capacity', () => {
|
||||
const serverHistory = [
|
||||
createHistoryItem('new-1'),
|
||||
createHistoryItem('new-2'),
|
||||
createHistoryItem('existing')
|
||||
createJob('high', 30),
|
||||
createJob('medium', 20),
|
||||
createJob('low', 10)
|
||||
]
|
||||
const clientHistory = [createHistoryItem('existing')]
|
||||
|
||||
const result = reconcileHistory(serverHistory, clientHistory, 2)
|
||||
const result = reconcileJobs(serverHistory, [], 2)
|
||||
|
||||
expect(result).toHaveLength(2)
|
||||
expect(result[0].prompt[1]).toBe('new-1')
|
||||
expect(result[1].prompt[1]).toBe('new-2')
|
||||
expect(result[0].id).toBe('high')
|
||||
expect(result[1].id).toBe('medium')
|
||||
})
|
||||
})
|
||||
|
||||
describe('when handling empty collections', () => {
|
||||
it('should return all server items when client history is empty', () => {
|
||||
const serverHistory = [
|
||||
createHistoryItem('item-1'),
|
||||
createHistoryItem('item-2')
|
||||
]
|
||||
const serverHistory = [createJob('item-1', 10), createJob('item-2', 5)]
|
||||
|
||||
const result = reconcileHistory(serverHistory, [], 10)
|
||||
const result = reconcileJobs(serverHistory, [], 10)
|
||||
|
||||
expect(result).toHaveLength(2)
|
||||
expect(result[0].prompt[1]).toBe('item-1')
|
||||
expect(result[1].prompt[1]).toBe('item-2')
|
||||
})
|
||||
|
||||
it('should return empty result when server history is empty', () => {
|
||||
const clientHistory = [
|
||||
createHistoryItem('item-1'),
|
||||
createHistoryItem('item-2')
|
||||
]
|
||||
const clientHistory = [createJob('item-1', 10)]
|
||||
|
||||
const result = reconcileHistory([], clientHistory, 10)
|
||||
const result = reconcileJobs([], clientHistory, 10)
|
||||
|
||||
expect(result).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('should return empty result when both collections are empty', () => {
|
||||
const result = reconcileHistory([], [], 10)
|
||||
const result = reconcileJobs([], [], 10)
|
||||
|
||||
expect(result).toHaveLength(0)
|
||||
})
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import type { ComfyWorkflowJSON } from '@/platform/workflow/validation/schemas/workflowSchema'
|
||||
import { getWorkflowFromHistory } from '@/platform/workflow/cloud/getWorkflowFromHistory'
|
||||
import {
|
||||
extractWorkflow,
|
||||
fetchJobDetail
|
||||
} from '@/platform/remote/comfyui/jobs/fetchJobs'
|
||||
|
||||
const mockWorkflow: ComfyWorkflowJSON = {
|
||||
id: 'test-workflow-id',
|
||||
@@ -16,75 +19,63 @@ const mockWorkflow: ComfyWorkflowJSON = {
|
||||
version: 0.4
|
||||
}
|
||||
|
||||
const mockHistoryResponse = {
|
||||
'test-prompt-id': {
|
||||
prompt: {
|
||||
priority: 1,
|
||||
prompt_id: 'test-prompt-id',
|
||||
extra_data: {
|
||||
client_id: 'test-client',
|
||||
extra_pnginfo: {
|
||||
workflow: mockWorkflow
|
||||
}
|
||||
// Jobs API detail response structure (matches actual /jobs/{id} response)
|
||||
// workflow is nested at: workflow.extra_data.extra_pnginfo.workflow
|
||||
const mockJobDetailResponse = {
|
||||
id: 'test-prompt-id',
|
||||
status: 'completed',
|
||||
create_time: 1234567890,
|
||||
update_time: 1234567900,
|
||||
workflow: {
|
||||
extra_data: {
|
||||
extra_pnginfo: {
|
||||
workflow: mockWorkflow
|
||||
}
|
||||
},
|
||||
outputs: {},
|
||||
status: {
|
||||
status_str: 'success',
|
||||
completed: true,
|
||||
messages: []
|
||||
}
|
||||
},
|
||||
outputs: {
|
||||
'20': {
|
||||
images: [
|
||||
{ filename: 'test.png', subfolder: '', type: 'output' },
|
||||
{ filename: 'test2.png', subfolder: '', type: 'output' }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
describe('getWorkflowFromHistory', () => {
|
||||
it('should fetch workflow from /history_v2/{prompt_id} endpoint', async () => {
|
||||
describe('fetchJobDetail', () => {
|
||||
it('should fetch job detail from /jobs/{prompt_id} endpoint', async () => {
|
||||
const mockFetchApi = vi.fn().mockResolvedValue({
|
||||
json: async () => mockHistoryResponse
|
||||
ok: true,
|
||||
json: async () => mockJobDetailResponse
|
||||
})
|
||||
|
||||
await getWorkflowFromHistory(mockFetchApi, 'test-prompt-id')
|
||||
await fetchJobDetail(mockFetchApi, 'test-prompt-id')
|
||||
|
||||
expect(mockFetchApi).toHaveBeenCalledWith('/history_v2/test-prompt-id')
|
||||
expect(mockFetchApi).toHaveBeenCalledWith('/jobs/test-prompt-id')
|
||||
})
|
||||
|
||||
it('should extract and return workflow from response', async () => {
|
||||
it('should return job detail with workflow and outputs', async () => {
|
||||
const mockFetchApi = vi.fn().mockResolvedValue({
|
||||
json: async () => mockHistoryResponse
|
||||
ok: true,
|
||||
json: async () => mockJobDetailResponse
|
||||
})
|
||||
|
||||
const result = await getWorkflowFromHistory(mockFetchApi, 'test-prompt-id')
|
||||
const result = await fetchJobDetail(mockFetchApi, 'test-prompt-id')
|
||||
|
||||
expect(result).toEqual(mockWorkflow)
|
||||
expect(result).toBeDefined()
|
||||
expect(result?.id).toBe('test-prompt-id')
|
||||
expect(result?.outputs).toEqual(mockJobDetailResponse.outputs)
|
||||
expect(result?.workflow).toBeDefined()
|
||||
})
|
||||
|
||||
it('should return undefined when prompt_id not found in response', async () => {
|
||||
it('should return undefined when job not found (non-OK response)', async () => {
|
||||
const mockFetchApi = vi.fn().mockResolvedValue({
|
||||
json: async () => ({})
|
||||
ok: false,
|
||||
status: 404
|
||||
})
|
||||
|
||||
const result = await getWorkflowFromHistory(mockFetchApi, 'nonexistent-id')
|
||||
|
||||
expect(result).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should return undefined when workflow is missing from extra_pnginfo', async () => {
|
||||
const mockFetchApi = vi.fn().mockResolvedValue({
|
||||
json: async () => ({
|
||||
'test-prompt-id': {
|
||||
prompt: {
|
||||
priority: 1,
|
||||
prompt_id: 'test-prompt-id',
|
||||
extra_data: {
|
||||
client_id: 'test-client'
|
||||
}
|
||||
},
|
||||
outputs: {}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const result = await getWorkflowFromHistory(mockFetchApi, 'test-prompt-id')
|
||||
const result = await fetchJobDetail(mockFetchApi, 'nonexistent-id')
|
||||
|
||||
expect(result).toBeUndefined()
|
||||
})
|
||||
@@ -92,19 +83,45 @@ describe('getWorkflowFromHistory', () => {
|
||||
it('should handle fetch errors gracefully', async () => {
|
||||
const mockFetchApi = vi.fn().mockRejectedValue(new Error('Network error'))
|
||||
|
||||
const result = await getWorkflowFromHistory(mockFetchApi, 'test-prompt-id')
|
||||
const result = await fetchJobDetail(mockFetchApi, 'test-prompt-id')
|
||||
|
||||
expect(result).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should handle malformed JSON responses', async () => {
|
||||
const mockFetchApi = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => {
|
||||
throw new Error('Invalid JSON')
|
||||
}
|
||||
})
|
||||
|
||||
const result = await getWorkflowFromHistory(mockFetchApi, 'test-prompt-id')
|
||||
const result = await fetchJobDetail(mockFetchApi, 'test-prompt-id')
|
||||
|
||||
expect(result).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('extractWorkflow', () => {
|
||||
it('should extract workflow from job detail', () => {
|
||||
const result = extractWorkflow(mockJobDetailResponse as any)
|
||||
|
||||
expect(result).toEqual(mockWorkflow)
|
||||
})
|
||||
|
||||
it('should return undefined when job is undefined', () => {
|
||||
const result = extractWorkflow(undefined)
|
||||
|
||||
expect(result).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should return undefined when workflow is missing', () => {
|
||||
const jobWithoutWorkflow = {
|
||||
...mockJobDetailResponse,
|
||||
workflow: {}
|
||||
}
|
||||
|
||||
const result = extractWorkflow(jobWithoutWorkflow as any)
|
||||
|
||||
expect(result).toBeUndefined()
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user