diff --git a/src/platform/remote/comfyui/history/adapters/v2ToV1Adapter.ts b/src/platform/remote/comfyui/history/adapters/v2ToV1Adapter.ts index 5b0e9f2665..bf3cbdcfbe 100644 --- a/src/platform/remote/comfyui/history/adapters/v2ToV1Adapter.ts +++ b/src/platform/remote/comfyui/history/adapters/v2ToV1Adapter.ts @@ -1,10 +1,7 @@ /** * @fileoverview Adapter to convert V2 history format to V1 format * @module platform/remote/comfyui/history/adapters/v2ToV1Adapter - * - * Converts cloud API V2 response format to the V1 format expected by the app. */ - import type { HistoryTaskItem, TaskPrompt } from '../types/historyV1Types' import type { HistoryResponseV2, @@ -13,31 +10,55 @@ import type { TaskPromptV2 } from '../types/historyV2Types' -/** - * Maps V2 prompt format to V1 prompt tuple format. - */ function mapPromptV2toV1( promptV2: TaskPromptV2, - outputs: TaskOutput + outputs: TaskOutput, + syntheticPriority: number ): TaskPrompt { - const outputNodesIds = Object.keys(outputs) - const { priority, prompt_id, extra_data } = promptV2 - return [priority, prompt_id, {}, extra_data, outputNodesIds] + return [ + syntheticPriority, + promptV2.prompt_id, + {}, + promptV2.extra_data, + Object.keys(outputs) + ] +} + +function getExecutionSuccessTimestamp(item: RawHistoryItemV2): number { + return ( + item.status?.messages?.find((m) => m[0] === 'execution_success')?.[1] + ?.timestamp ?? 0 + ) } -/** - * Maps V2 history format to V1 history format. - */ export function mapHistoryV2toHistory( historyV2Response: HistoryResponseV2 ): HistoryTaskItem[] { - return historyV2Response.history.map( - ({ prompt, status, outputs, meta }: RawHistoryItemV2): HistoryTaskItem => ({ + const history = historyV2Response.history + + // Sort by execution_success timestamp, descending (newest first) + history.sort((a, b) => { + return getExecutionSuccessTimestamp(b) - getExecutionSuccessTimestamp(a) + }) + + // Count items with valid timestamps for synthetic priority calculation + const countWithTimestamps = history.filter( + (item) => getExecutionSuccessTimestamp(item) > 0 + ).length + + return history.map((item, index): HistoryTaskItem => { + const { prompt, outputs, status, meta } = item + const timestamp = getExecutionSuccessTimestamp(item) + + // Items with timestamps get priority based on sorted position (highest first) + const syntheticPriority = timestamp > 0 ? countWithTimestamps - index : 0 + + return { taskType: 'History' as const, - prompt: mapPromptV2toV1(prompt, outputs), + prompt: mapPromptV2toV1(prompt, outputs, syntheticPriority), status, outputs, meta - }) - ) + } + }) } diff --git a/tests-ui/fixtures/historyFixtures.ts b/tests-ui/fixtures/historyFixtures.ts index 5ae3ff998a..3a930a1add 100644 --- a/tests-ui/fixtures/historyFixtures.ts +++ b/tests-ui/fixtures/historyFixtures.ts @@ -233,12 +233,17 @@ export const historyV2Fixture: HistoryResponseV2 = { /** * Expected V1 transformation of historyV2Fixture + * Priority is now synthetic based on execution_success timestamp: + * - complete-item-id: has timestamp → priority 1 (only one with timestamp) + * - no-status-id: no status → priority 0 + * - no-meta-id: empty messages → priority 0 + * - multi-output-id: empty messages → priority 0 */ export const expectedV1Fixture: HistoryTaskItem[] = [ { taskType: 'History', prompt: [ - 24, + 1, 'complete-item-id', {}, { @@ -295,7 +300,7 @@ export const expectedV1Fixture: HistoryTaskItem[] = [ { taskType: 'History', prompt: [ - 23, + 0, 'no-status-id', {}, { @@ -319,7 +324,7 @@ export const expectedV1Fixture: HistoryTaskItem[] = [ { taskType: 'History', prompt: [ - 22, + 0, 'no-meta-id', {}, { @@ -342,7 +347,7 @@ export const expectedV1Fixture: HistoryTaskItem[] = [ { taskType: 'History', prompt: [ - 21, + 0, 'multi-output-id', {}, { diff --git a/tests-ui/fixtures/historySortingFixtures.ts b/tests-ui/fixtures/historySortingFixtures.ts new file mode 100644 index 0000000000..797aec2437 --- /dev/null +++ b/tests-ui/fixtures/historySortingFixtures.ts @@ -0,0 +1,199 @@ +/** + * @fileoverview Test fixtures for history V2 timestamp-based sorting + */ +import type { HistoryResponseV2 } from '@/platform/remote/comfyui/history/types/historyV2Types' + +export const historyV2WithMissingTimestamp: HistoryResponseV2 = { + history: [ + { + prompt_id: 'item-timestamp-1000', + prompt: { + priority: 0, + prompt_id: 'item-timestamp-1000', + extra_data: { + client_id: 'test-client' + } + }, + outputs: { + '1': { + images: [{ filename: 'test1.png', type: 'output', subfolder: '' }] + } + }, + status: { + status_str: 'success', + completed: true, + messages: [ + [ + 'execution_success', + { prompt_id: 'item-timestamp-1000', timestamp: 1000 } + ] + ] + } + }, + { + prompt_id: 'item-timestamp-2000', + prompt: { + priority: 0, + prompt_id: 'item-timestamp-2000', + extra_data: { + client_id: 'test-client' + } + }, + outputs: { + '2': { + images: [{ filename: 'test2.png', type: 'output', subfolder: '' }] + } + }, + status: { + status_str: 'success', + completed: true, + messages: [ + [ + 'execution_success', + { prompt_id: 'item-timestamp-2000', timestamp: 2000 } + ] + ] + } + }, + { + prompt_id: 'item-no-timestamp', + prompt: { + priority: 0, + prompt_id: 'item-no-timestamp', + extra_data: { + client_id: 'test-client' + } + }, + outputs: { + '3': { + images: [{ filename: 'test3.png', type: 'output', subfolder: '' }] + } + }, + status: { + status_str: 'success', + completed: true, + messages: [] + } + } + ] +} + +export const historyV2FiveItemsSorting: HistoryResponseV2 = { + history: [ + { + prompt_id: 'item-timestamp-3000', + prompt: { + priority: 0, + prompt_id: 'item-timestamp-3000', + extra_data: { client_id: 'test-client' } + }, + outputs: { + '1': { + images: [{ filename: 'test1.png', type: 'output', subfolder: '' }] + } + }, + status: { + status_str: 'success', + completed: true, + messages: [ + [ + 'execution_success', + { prompt_id: 'item-timestamp-3000', timestamp: 3000 } + ] + ] + } + }, + { + prompt_id: 'item-timestamp-1000', + prompt: { + priority: 0, + prompt_id: 'item-timestamp-1000', + extra_data: { client_id: 'test-client' } + }, + outputs: { + '2': { + images: [{ filename: 'test2.png', type: 'output', subfolder: '' }] + } + }, + status: { + status_str: 'success', + completed: true, + messages: [ + [ + 'execution_success', + { prompt_id: 'item-timestamp-1000', timestamp: 1000 } + ] + ] + } + }, + { + prompt_id: 'item-timestamp-5000', + prompt: { + priority: 0, + prompt_id: 'item-timestamp-5000', + extra_data: { client_id: 'test-client' } + }, + outputs: { + '3': { + images: [{ filename: 'test3.png', type: 'output', subfolder: '' }] + } + }, + status: { + status_str: 'success', + completed: true, + messages: [ + [ + 'execution_success', + { prompt_id: 'item-timestamp-5000', timestamp: 5000 } + ] + ] + } + }, + { + prompt_id: 'item-timestamp-2000', + prompt: { + priority: 0, + prompt_id: 'item-timestamp-2000', + extra_data: { client_id: 'test-client' } + }, + outputs: { + '4': { + images: [{ filename: 'test4.png', type: 'output', subfolder: '' }] + } + }, + status: { + status_str: 'success', + completed: true, + messages: [ + [ + 'execution_success', + { prompt_id: 'item-timestamp-2000', timestamp: 2000 } + ] + ] + } + }, + { + prompt_id: 'item-timestamp-4000', + prompt: { + priority: 0, + prompt_id: 'item-timestamp-4000', + extra_data: { client_id: 'test-client' } + }, + outputs: { + '5': { + images: [{ filename: 'test5.png', type: 'output', subfolder: '' }] + } + }, + status: { + status_str: 'success', + completed: true, + messages: [ + [ + 'execution_success', + { prompt_id: 'item-timestamp-4000', timestamp: 4000 } + ] + ] + } + } + ] +} diff --git a/tests-ui/tests/platform/remote/comfyui/history/adapters/v2ToV1Adapter.test.ts b/tests-ui/tests/platform/remote/comfyui/history/adapters/v2ToV1Adapter.test.ts index f77a5e1588..0d6cef8c26 100644 --- a/tests-ui/tests/platform/remote/comfyui/history/adapters/v2ToV1Adapter.test.ts +++ b/tests-ui/tests/platform/remote/comfyui/history/adapters/v2ToV1Adapter.test.ts @@ -11,6 +11,10 @@ import { expectedV1Fixture, historyV2Fixture } from '@tests-ui/fixtures/historyFixtures' +import { + historyV2FiveItemsSorting, + historyV2WithMissingTimestamp +} from '@tests-ui/fixtures/historySortingFixtures' describe('mapHistoryV2toHistory', () => { describe('fixture validation', () => { @@ -38,7 +42,7 @@ describe('mapHistoryV2toHistory', () => { it('should transform prompt to V1 tuple [priority, id, {}, extra_data, outputNodeIds]', () => { const firstItem = history[0] - expect(firstItem.prompt[0]).toBe(24) + 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' }) @@ -117,4 +121,70 @@ describe('mapHistoryV2toHistory', () => { 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 = result.find( + (item) => item.prompt[1] === 'item-timestamp-1000' + ) + const item2000 = result.find( + (item) => item.prompt[1] === 'item-timestamp-2000' + ) + const itemNoTimestamp = result.find( + (item) => item.prompt[1] === 'item-no-timestamp' + ) + + expect(item1000).toBeDefined() + expect(item2000).toBeDefined() + expect(itemNoTimestamp).toBeDefined() + if (!item1000 || !item2000 || !itemNoTimestamp) { + throw new Error('Expected items not found in result') + } + + 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 = result.find( + (item) => item.prompt[1] === 'item-timestamp-1000' + ) + const item2000 = result.find( + (item) => item.prompt[1] === 'item-timestamp-2000' + ) + const item3000 = result.find( + (item) => item.prompt[1] === 'item-timestamp-3000' + ) + const item4000 = result.find( + (item) => item.prompt[1] === 'item-timestamp-4000' + ) + const item5000 = result.find( + (item) => item.prompt[1] === 'item-timestamp-5000' + ) + + expect(item1000).toBeDefined() + expect(item2000).toBeDefined() + expect(item3000).toBeDefined() + expect(item4000).toBeDefined() + expect(item5000).toBeDefined() + if (!item1000 || !item2000 || !item3000 || !item4000 || !item5000) { + throw new Error('Expected items not found in result') + } + + 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) + }) + }) })