[feat] Update history API to v2 array format and add comprehensive tests

- Migrate from object-based to array-based history response format
- Update /history endpoint to /history_v2 with max_items parameter
- Add lazy loading of workflows via /history_v2/:prompt_id endpoint
- Implement comprehensive browser tests for history API functionality
- Add unit tests for API methods and queue store
- Update TaskItemImpl to support history workflow loading
- Add proper error handling and edge case coverage
- Follow established test patterns for better maintainability

This change improves performance by reducing initial payload size
and enables on-demand workflow loading for history items.
This commit is contained in:
Richard Yu
2025-07-14 18:27:06 -07:00
committed by Jennifer Weber
parent c27edb7e94
commit 7649feb47f
11 changed files with 645 additions and 60 deletions

View File

@@ -34,17 +34,23 @@ const getContentType = (filename: string, fileType: OutputFileType) => {
}
const setQueueIndex = (task: TaskItem) => {
task.prompt[0] = TaskHistory.queueIndex++
task.prompt.priority = TaskHistory.queueIndex++
}
const setPromptId = (task: TaskItem) => {
task.prompt[1] = uuidv4()
if (!task.prompt.prompt_id || task.prompt.prompt_id === 'prompt-id') {
task.prompt.prompt_id = uuidv4()
}
}
export default class TaskHistory {
static queueIndex = 0
static readonly defaultTask: Readonly<HistoryTaskItem> = {
prompt: [0, 'prompt-id', {}, { client_id: uuidv4() }, []],
prompt: {
priority: 0,
prompt_id: 'prompt-id',
extra_data: { client_id: uuidv4() }
},
outputs: {},
status: {
status_str: 'success',
@@ -66,10 +72,37 @@ export default class TaskHistory {
)
private async handleGetHistory(route: Route) {
const url = route.request().url()
// Handle history_v2/:prompt_id endpoint
const promptIdMatch = url.match(/history_v2\/([^?]+)/)
if (promptIdMatch) {
const promptId = promptIdMatch[1]
const task = this.tasks.find((t) => t.prompt.prompt_id === promptId)
const response: Record<string, any> = {}
if (task) {
response[promptId] = task
}
return route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(response)
})
}
// Handle history_v2 list endpoint
// Convert HistoryTaskItem to RawHistoryItem format expected by API
const rawHistoryItems = this.tasks.map((task) => ({
prompt_id: task.prompt.prompt_id,
prompt: task.prompt,
status: task.status,
outputs: task.outputs,
...(task.meta && { meta: task.meta })
}))
return route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(this.tasks)
body: JSON.stringify({ history: rawHistoryItems })
})
}
@@ -93,7 +126,7 @@ export default class TaskHistory {
async setupRoutes() {
return this.comfyPage.page.route(
/.*\/api\/(view|history)(\?.*)?$/,
/.*\/api\/(view|history_v2)(\/[^?]*)?(\?.*)?$/,
async (route) => {
const request = route.request()
const method = request.method()

View File

@@ -0,0 +1,131 @@
import {
comfyExpect as expect,
comfyPageFixture as test
} from '../fixtures/ComfyPage'
test.describe('History API v2', () => {
const TEST_PROMPT_ID = 'test-prompt-id'
const TEST_CLIENT_ID = 'test-client'
test('Can fetch history with new v2 format', async ({ comfyPage }) => {
// Set up mocked history with tasks
await comfyPage.setupHistory().withTask(['example.webp']).setupRoutes()
// Verify history_v2 API response format
const result = await comfyPage.page.evaluate(async () => {
try {
const response = await window['app'].api.getHistory()
return { success: true, data: response }
} catch (error) {
console.error('Failed to fetch history:', error)
return { success: false, error: error.message }
}
})
expect(result.success).toBe(true)
expect(result.data).toHaveProperty('History')
expect(Array.isArray(result.data.History)).toBe(true)
expect(result.data.History.length).toBeGreaterThan(0)
const historyItem = result.data.History[0]
// Verify the new prompt structure (object instead of array)
expect(historyItem.prompt).toHaveProperty('priority')
expect(historyItem.prompt).toHaveProperty('prompt_id')
expect(historyItem.prompt).toHaveProperty('extra_data')
expect(typeof historyItem.prompt.priority).toBe('number')
expect(typeof historyItem.prompt.prompt_id).toBe('string')
expect(historyItem.prompt.extra_data).toHaveProperty('client_id')
})
test('Can load workflow from history using history_v2 endpoint', async ({
comfyPage
}) => {
// Simple mock workflow for testing
const mockWorkflow = {
version: 0.4,
nodes: [{ id: 1, type: 'TestNode', pos: [100, 100], size: [200, 100] }],
links: [],
groups: [],
config: {},
extra: {}
}
// Set up history with workflow data
await comfyPage
.setupHistory()
.withTask(['example.webp'], 'images', {
prompt: {
priority: 0,
prompt_id: TEST_PROMPT_ID,
extra_data: {
client_id: TEST_CLIENT_ID,
extra_pnginfo: { workflow: mockWorkflow }
}
}
})
.setupRoutes()
// Load initial workflow to clear canvas
await comfyPage.loadWorkflow('simple_slider')
await comfyPage.nextFrame()
// Load workflow from history
const loadResult = await comfyPage.page.evaluate(async (promptId) => {
try {
const workflow =
await window['app'].api.getWorkflowFromHistory(promptId)
if (workflow) {
await window['app'].loadGraphData(workflow)
return { success: true }
}
return { success: false, error: 'No workflow found' }
} catch (error) {
console.error('Failed to load workflow from history:', error)
return { success: false, error: error.message }
}
}, TEST_PROMPT_ID)
expect(loadResult.success).toBe(true)
// Verify workflow loaded correctly
await comfyPage.nextFrame()
const nodeInfo = await comfyPage.page.evaluate(() => {
try {
const graph = window['app'].graph
return {
success: true,
nodeCount: graph.nodes?.length || 0,
firstNodeType: graph.nodes?.[0]?.type || null
}
} catch (error) {
return { success: false, error: error.message }
}
})
expect(nodeInfo.success).toBe(true)
expect(nodeInfo.nodeCount).toBe(1)
expect(nodeInfo.firstNodeType).toBe('TestNode')
})
test('Handles missing workflow data gracefully', async ({ comfyPage }) => {
// Set up empty history routes
await comfyPage.setupHistory().setupRoutes()
// Test loading from history with invalid prompt_id
const result = await comfyPage.page.evaluate(async () => {
try {
const workflow =
await window['app'].api.getWorkflowFromHistory('invalid-id')
return { success: true, workflow }
} catch (error) {
console.error('Expected error for invalid prompt_id:', error)
return { success: false, error: error.message }
}
})
// Should handle gracefully without throwing
expect(result.success).toBe(true)
expect(result.workflow).toBeNull()
})
})

View File

@@ -187,6 +187,7 @@ test.describe('Workflows sidebar', () => {
test('Can save workflow as with same name', async ({ comfyPage }) => {
await comfyPage.menu.topbar.saveWorkflow('workflow5.json')
await comfyPage.nextFrame()
expect(await comfyPage.menu.workflowsTab.getOpenedWorkflowNames()).toEqual([
'workflow5.json'
])

View File

@@ -106,8 +106,8 @@ import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
import VirtualGrid from '@/components/common/VirtualGrid.vue'
import { ComfyNode } from '@/schemas/comfyWorkflowSchema'
import { api } from '@/scripts/api'
import { app } from '@/scripts/app'
import { useLitegraphService } from '@/services/litegraphService'
import { useWorkflowService } from '@/services/workflowService'
import { useCommandStore } from '@/stores/commandStore'
import {
ResultItemImpl,
@@ -126,6 +126,7 @@ const toast = useToast()
const queueStore = useQueueStore()
const settingStore = useSettingStore()
const commandStore = useCommandStore()
const workflowService = useWorkflowService()
const { t } = useI18n()
// Expanded view: show all outputs in a flat list.
@@ -208,8 +209,16 @@ const menuItems = computed<MenuItem[]>(() => {
{
label: t('g.loadWorkflow'),
icon: 'pi pi-file-export',
command: () => menuTargetTask.value?.loadWorkflow(app),
disabled: !menuTargetTask.value?.workflow
command: () => {
if (menuTargetTask.value) {
void workflowService.loadTaskWorkflow(menuTargetTask.value)
}
},
disabled: !(
menuTargetTask.value?.workflow ||
(menuTargetTask.value?.isHistory &&
menuTargetTask.value?.prompt.prompt_id)
)
},
{
label: t('g.goToNode'),

View File

@@ -161,13 +161,6 @@ export type FeatureFlagsWsMessage = z.infer<typeof zFeatureFlagsWsMessage>
export type NotificationWsMessage = z.infer<typeof zNotificationWsMessage>
// End of ws messages
const zPromptInputItem = z.object({
inputs: z.record(z.string(), z.any()),
class_type: zNodeType
})
const zPromptInputs = z.record(zPromptInputItem)
const zExtraPngInfo = z
.object({
workflow: zComfyWorkflow
@@ -179,7 +172,6 @@ const zExtraData = z.object({
extra_pnginfo: zExtraPngInfo.optional(),
client_id: z.string()
})
const zOutputsToExecute = z.array(zNodeId)
const zExecutionStartMessage = z.tuple([
z.literal('execution_start'),
@@ -220,13 +212,11 @@ const zStatus = z.object({
messages: z.array(zStatusMessage)
})
const zTaskPrompt = z.tuple([
zQueueIndex,
zPromptId,
zPromptInputs,
zExtraData,
zOutputsToExecute
])
const zTaskPrompt = z.object({
priority: zQueueIndex,
prompt_id: zPromptId,
extra_data: zExtraData
})
const zRunningTaskItem = z.object({
taskType: z.literal('Running'),
@@ -262,6 +252,20 @@ const zHistoryTaskItem = z.object({
meta: zTaskMeta.optional()
})
// Raw history item from backend (without taskType)
const zRawHistoryItem = z.object({
prompt_id: zPromptId,
prompt: zTaskPrompt,
status: zStatus.optional(),
outputs: zTaskOutput,
meta: zTaskMeta.optional()
})
// New API response format: { history: [{prompt_id: "...", ...}, ...] }
const zHistoryResponse = z.object({
history: z.array(zRawHistoryItem)
})
const zTaskItem = z.union([
zRunningTaskItem,
zPendingTaskItem,
@@ -284,6 +288,8 @@ export type RunningTaskItem = z.infer<typeof zRunningTaskItem>
export type PendingTaskItem = z.infer<typeof zPendingTaskItem>
// `/history`
export type HistoryTaskItem = z.infer<typeof zHistoryTaskItem>
export type RawHistoryItem = z.infer<typeof zRawHistoryItem>
export type HistoryResponse = z.infer<typeof zHistoryResponse>
export type TaskItem = z.infer<typeof zTaskItem>
export function validateTaskItem(taskItem: unknown) {

View File

@@ -14,6 +14,7 @@ import type {
ExecutionSuccessWsMessage,
ExtensionsResponse,
FeatureFlagsWsMessage,
HistoryResponse,
HistoryTaskItem,
LogsRawResponse,
LogsWsMessage,
@@ -28,6 +29,7 @@ import type {
StatusWsMessage,
StatusWsMessageStatus,
SystemStats,
TaskPrompt,
User,
UserDataFullInfo
} from '@/schemas/apiSchema'
@@ -915,13 +917,13 @@ export class ComfyApi extends EventTarget {
const data = await res.json()
return {
// Running action uses a different endpoint for cancelling
Running: data.queue_running.map((prompt: Record<number, any>) => ({
Running: data.queue_running.map((prompt: TaskPrompt) => ({
taskType: 'Running',
prompt,
// prompt[1] is the prompt id
remove: { name: 'Cancel', cb: () => api.interrupt(prompt[1]) }
})),
Pending: data.queue_pending.map((prompt: Record<number, any>) => ({
Pending: data.queue_pending.map((prompt: TaskPrompt) => ({
taskType: 'Pending',
prompt
}))
@@ -940,13 +942,17 @@ export class ComfyApi extends EventTarget {
max_items: number = 200
): Promise<{ History: HistoryTaskItem[] }> {
try {
const res = await this.fetchApi(`/history?max_items=${max_items}`)
const json: Promise<HistoryTaskItem[]> = await res.json()
const res = await this.fetchApi(`/history_v2?max_items=${max_items}`)
const json: HistoryResponse = await res.json()
// Extract history data from new format: { history: [{prompt_id: "...", ...}, ...] }
return {
History: Object.values(json).map((item) => ({
...item,
taskType: 'History'
}))
History: json.history.map(
(item): HistoryTaskItem => ({
...item,
taskType: 'History'
})
)
}
} catch (error) {
console.error(error)
@@ -954,6 +960,33 @@ export class ComfyApi extends EventTarget {
}
}
/**
* Gets workflow data for a specific prompt from history
* @param prompt_id The prompt ID to fetch workflow for
* @returns Workflow data for the specific prompt
*/
async getWorkflowFromHistory(
prompt_id: string
): Promise<ComfyWorkflowJSON | null> {
try {
const res = await this.fetchApi(`/history_v2/${prompt_id}`)
const json = await res.json()
// The /history_v2/{prompt_id} endpoint returns data for a specific prompt
// The response format is: { prompt_id: { prompt: {priority, prompt_id, extra_data}, outputs: {...}, status: {...} } }
const historyItem = json[prompt_id]
if (!historyItem) return null
// Extract workflow from the prompt object
// prompt.extra_data contains extra_pnginfo.workflow
const workflow = historyItem.prompt?.extra_data?.extra_pnginfo?.workflow
return workflow || null
} catch (error) {
console.error(`Failed to fetch workflow for prompt ${prompt_id}:`, error)
return null
}
}
/**
* Gets system & device stats
* @returns System stats such as python version, OS, per device info

View File

@@ -264,15 +264,16 @@ class ComfyList {
? item.remove
: {
name: 'Delete',
cb: () => api.deleteItem(this.#type, item.prompt[1])
cb: () =>
api.deleteItem(this.#type, item.prompt.prompt_id)
}
return $el('div', { textContent: item.prompt[0] + ': ' }, [
return $el('div', { textContent: item.prompt.priority + ': ' }, [
$el('button', {
textContent: 'Load',
onclick: async () => {
await app.loadGraphData(
// @ts-expect-error fixme ts strict error
item.prompt[3].extra_pnginfo.workflow,
item.prompt.extra_data.extra_pnginfo.workflow,
true,
false
)

View File

@@ -5,10 +5,12 @@ import { LGraph, LGraphCanvas } from '@/lib/litegraph/src/litegraph'
import type { SerialisableGraph, Vector2 } from '@/lib/litegraph/src/litegraph'
import { useWorkflowThumbnail } from '@/renderer/thumbnail/composables/useWorkflowThumbnail'
import { ComfyWorkflowJSON } from '@/schemas/comfyWorkflowSchema'
import { api } from '@/scripts/api'
import { app } from '@/scripts/app'
import { blankGraph, defaultGraph } from '@/scripts/defaultGraph'
import { downloadBlob } from '@/scripts/utils'
import { useDomWidgetStore } from '@/stores/domWidgetStore'
import { TaskItemImpl } from '@/stores/queueStore'
import { useSettingStore } from '@/stores/settingStore'
import { useToastStore } from '@/stores/toastStore'
import { ComfyWorkflow, useWorkflowStore } from '@/stores/workflowStore'
@@ -154,6 +156,32 @@ export const useWorkflowService = () => {
await app.loadGraphData(blankGraph)
}
/**
* Load a workflow from a task item (queue/history)
* For history items, fetches workflow data from /history_v2/{prompt_id}
* @param task The task item to load the workflow from
*/
const loadTaskWorkflow = async (task: TaskItemImpl) => {
let workflowData = task.workflow
// History items don't include workflow data - fetch from API
if (task.isHistory) {
const promptId = task.prompt.prompt_id
if (promptId) {
workflowData = (await api.getWorkflowFromHistory(promptId)) || undefined
}
}
if (!workflowData) {
return
}
await app.loadGraphData(toRaw(workflowData))
if (task.outputs) {
app.nodeOutputs = toRaw(task.outputs)
}
}
/**
* Reload the current workflow
* This is used to refresh the node definitions update, e.g. when the locale changes.
@@ -402,6 +430,7 @@ export const useWorkflowService = () => {
saveWorkflow,
loadDefaultWorkflow,
loadBlankWorkflow,
loadTaskWorkflow,
reloadCurrentWorkflow,
openWorkflow,
closeWorkflow,

View File

@@ -276,23 +276,15 @@ export class TaskItemImpl {
}
get queueIndex() {
return this.prompt[0]
return this.prompt.priority
}
get promptId() {
return this.prompt[1]
}
get promptInputs() {
return this.prompt[2]
return this.prompt.prompt_id
}
get extraData() {
return this.prompt[3]
}
get outputsToExecute() {
return this.prompt[4]
return this.prompt.extra_data
}
get extraPngInfo() {
@@ -408,13 +400,11 @@ export class TaskItemImpl {
(output: ResultItemImpl, i: number) =>
new TaskItemImpl(
this.taskType,
[
this.queueIndex,
`${this.promptId}-${i}`,
this.promptInputs,
this.extraData,
this.outputsToExecute
],
{
priority: this.queueIndex,
prompt_id: `${this.promptId}-${i}`,
extra_data: this.extraData
},
this.status,
{
[output.nodeId]: {
@@ -479,11 +469,11 @@ export const useQueueStore = defineStore('queue', () => {
pendingTasks.value = toClassAll(queue.Pending)
const allIndex = new Set<number>(
history.History.map((item: TaskItem) => item.prompt[0])
history.History.map((item: TaskItem) => item.prompt.priority)
)
const newHistoryItems = toClassAll(
history.History.filter(
(item) => item.prompt[0] > lastHistoryQueueIndex.value
(item) => item.prompt.priority > lastHistoryQueueIndex.value
)
)
const existingHistoryItems = historyTasks.value.filter((item) =>

View File

@@ -0,0 +1,248 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type {
HistoryResponse,
RawHistoryItem
} from '../../../src/schemas/apiSchema'
import type { ComfyWorkflowJSON } from '../../../src/schemas/comfyWorkflowSchema'
import { ComfyApi } from '../../../src/scripts/api'
describe('ComfyApi getHistory', () => {
let api: ComfyApi
beforeEach(() => {
api = new ComfyApi()
})
const mockHistoryItem: RawHistoryItem = {
prompt_id: 'test_prompt_id',
prompt: {
priority: 0,
prompt_id: 'test_prompt_id',
extra_data: {
extra_pnginfo: {
workflow: {
last_node_id: 1,
last_link_id: 0,
nodes: [],
links: [],
groups: [],
config: {},
extra: {},
version: 0.4
}
},
client_id: 'test_client_id'
}
},
outputs: {},
status: {
status_str: 'success',
completed: true,
messages: []
}
}
describe('history v2 API format', () => {
it('should handle history array format from /history_v2', async () => {
const historyResponse: HistoryResponse = {
history: [
{ ...mockHistoryItem, prompt_id: 'prompt_id_1' },
{ ...mockHistoryItem, prompt_id: 'prompt_id_2' }
]
}
// Mock fetchApi to return the v2 format
const mockFetchApi = vi.fn().mockResolvedValue({
json: vi.fn().mockResolvedValue(historyResponse)
})
api.fetchApi = mockFetchApi
const result = await api.getHistory(10)
expect(result.History).toHaveLength(2)
expect(result.History[0]).toEqual({
...mockHistoryItem,
prompt_id: 'prompt_id_1',
taskType: 'History'
})
expect(result.History[1]).toEqual({
...mockHistoryItem,
prompt_id: 'prompt_id_2',
taskType: 'History'
})
})
it('should handle empty history array', async () => {
const historyResponse: HistoryResponse = {
history: []
}
const mockFetchApi = vi.fn().mockResolvedValue({
json: vi.fn().mockResolvedValue(historyResponse)
})
api.fetchApi = mockFetchApi
const result = await api.getHistory(10)
expect(result.History).toHaveLength(0)
expect(result.History).toEqual([])
})
})
describe('error handling', () => {
it('should return empty history on error', async () => {
const mockFetchApi = vi.fn().mockRejectedValue(new Error('Network error'))
api.fetchApi = mockFetchApi
const result = await api.getHistory()
expect(result.History).toEqual([])
})
})
describe('API call parameters', () => {
it('should call fetchApi with correct v2 endpoint and parameters', async () => {
const mockFetchApi = vi.fn().mockResolvedValue({
json: vi.fn().mockResolvedValue({ history: [] })
})
api.fetchApi = mockFetchApi
await api.getHistory(50)
expect(mockFetchApi).toHaveBeenCalledWith('/history_v2?max_items=50')
})
it('should use default max_items parameter with v2 endpoint', async () => {
const mockFetchApi = vi.fn().mockResolvedValue({
json: vi.fn().mockResolvedValue({ history: [] })
})
api.fetchApi = mockFetchApi
await api.getHistory()
expect(mockFetchApi).toHaveBeenCalledWith('/history_v2?max_items=200')
})
})
})
describe('ComfyApi getWorkflowFromHistory', () => {
let api: ComfyApi
beforeEach(() => {
api = new ComfyApi()
})
const mockWorkflow: ComfyWorkflowJSON = {
last_node_id: 1,
last_link_id: 0,
nodes: [],
links: [],
groups: [],
config: {},
extra: {},
version: 0.4
}
it('should fetch workflow data for a specific prompt', async () => {
const promptId = 'test_prompt_id'
const mockResponse = {
[promptId]: {
prompt: {
priority: 0,
prompt_id: promptId,
extra_data: {
extra_pnginfo: {
workflow: mockWorkflow
}
}
},
outputs: {},
status: {
status_str: 'success',
completed: true,
messages: []
}
}
}
const mockFetchApi = vi.fn().mockResolvedValue({
json: vi.fn().mockResolvedValue(mockResponse)
})
api.fetchApi = mockFetchApi
const result = await api.getWorkflowFromHistory(promptId)
expect(mockFetchApi).toHaveBeenCalledWith(`/history_v2/${promptId}`)
expect(result).toEqual(mockWorkflow)
})
it('should return null when prompt_id is not found', async () => {
const promptId = 'non_existent_prompt'
const mockResponse = {}
const mockFetchApi = vi.fn().mockResolvedValue({
json: vi.fn().mockResolvedValue(mockResponse)
})
api.fetchApi = mockFetchApi
const result = await api.getWorkflowFromHistory(promptId)
expect(mockFetchApi).toHaveBeenCalledWith(`/history_v2/${promptId}`)
expect(result).toBeNull()
})
it('should return null when workflow data is missing', async () => {
const promptId = 'test_prompt_id'
const mockResponse = {
[promptId]: {
prompt: {
priority: 0,
prompt_id: promptId,
extra_data: {}
},
outputs: {},
status: {
status_str: 'success',
completed: true,
messages: []
}
}
}
const mockFetchApi = vi.fn().mockResolvedValue({
json: vi.fn().mockResolvedValue(mockResponse)
})
api.fetchApi = mockFetchApi
const result = await api.getWorkflowFromHistory(promptId)
expect(result).toBeNull()
})
it('should handle API errors gracefully', async () => {
const promptId = 'test_prompt_id'
const mockFetchApi = vi.fn().mockRejectedValue(new Error('Network error'))
api.fetchApi = mockFetchApi
const result = await api.getWorkflowFromHistory(promptId)
expect(result).toBeNull()
})
it('should handle malformed response gracefully', async () => {
const promptId = 'test_prompt_id'
const mockResponse = {
[promptId]: null
}
const mockFetchApi = vi.fn().mockResolvedValue({
json: vi.fn().mockResolvedValue(mockResponse)
})
api.fetchApi = mockFetchApi
const result = await api.getWorkflowFromHistory(promptId)
expect(result).toBeNull()
})
})

View File

@@ -3,10 +3,94 @@ import { describe, expect, it } from 'vitest'
import { TaskItemImpl } from '@/stores/queueStore'
describe('TaskItemImpl', () => {
describe('prompt property accessors', () => {
it('should correctly access queueIndex from priority', () => {
const taskItem = new TaskItemImpl('Pending', {
priority: 5,
prompt_id: 'test-id',
extra_data: { client_id: 'client-id' }
})
expect(taskItem.queueIndex).toBe(5)
})
it('should correctly access promptId from prompt_id', () => {
const taskItem = new TaskItemImpl('History', {
priority: 0,
prompt_id: 'unique-prompt-id',
extra_data: { client_id: 'client-id' }
})
expect(taskItem.promptId).toBe('unique-prompt-id')
})
it('should correctly access extraData', () => {
const extraData = {
client_id: 'client-id',
extra_pnginfo: {
workflow: {
last_node_id: 1,
last_link_id: 0,
nodes: [],
links: [],
groups: [],
config: {},
extra: {},
version: 0.4
}
}
}
const taskItem = new TaskItemImpl('Running', {
priority: 1,
prompt_id: 'test-id',
extra_data: extraData
})
expect(taskItem.extraData).toEqual(extraData)
})
it('should correctly access workflow from extraPngInfo', () => {
const workflow = {
last_node_id: 1,
last_link_id: 0,
nodes: [],
links: [],
groups: [],
config: {},
extra: {},
version: 0.4
}
const taskItem = new TaskItemImpl('History', {
priority: 0,
prompt_id: 'test-id',
extra_data: {
client_id: 'client-id',
extra_pnginfo: { workflow }
}
})
expect(taskItem.workflow).toEqual(workflow)
})
it('should return undefined workflow when extraPngInfo is missing', () => {
const taskItem = new TaskItemImpl('History', {
priority: 0,
prompt_id: 'test-id',
extra_data: { client_id: 'client-id' }
})
expect(taskItem.workflow).toBeUndefined()
})
})
it('should remove animated property from outputs during construction', () => {
const taskItem = new TaskItemImpl(
'History',
[0, 'prompt-id', {}, { client_id: 'client-id' }, []],
{
priority: 0,
prompt_id: 'prompt-id',
extra_data: { client_id: 'client-id' }
},
{ status_str: 'success', messages: [], completed: true },
{
'node-1': {
@@ -26,7 +110,11 @@ describe('TaskItemImpl', () => {
it('should handle outputs without animated property', () => {
const taskItem = new TaskItemImpl(
'History',
[0, 'prompt-id', {}, { client_id: 'client-id' }, []],
{
priority: 0,
prompt_id: 'prompt-id',
extra_data: { client_id: 'client-id' }
},
{ status_str: 'success', messages: [], completed: true },
{
'node-1': {
@@ -42,7 +130,11 @@ describe('TaskItemImpl', () => {
it('should recognize webm video from core', () => {
const taskItem = new TaskItemImpl(
'History',
[0, 'prompt-id', {}, { client_id: 'client-id' }, []],
{
priority: 0,
prompt_id: 'prompt-id',
extra_data: { client_id: 'client-id' }
},
{ status_str: 'success', messages: [], completed: true },
{
'node-1': {
@@ -64,7 +156,11 @@ describe('TaskItemImpl', () => {
it('should recognize webm video from VHS', () => {
const taskItem = new TaskItemImpl(
'History',
[0, 'prompt-id', {}, { client_id: 'client-id' }, []],
{
priority: 0,
prompt_id: 'prompt-id',
extra_data: { client_id: 'client-id' }
},
{ status_str: 'success', messages: [], completed: true },
{
'node-1': {
@@ -93,7 +189,11 @@ describe('TaskItemImpl', () => {
it('should recognize mp4 video from core', () => {
const taskItem = new TaskItemImpl(
'History',
[0, 'prompt-id', {}, { client_id: 'client-id' }, []],
{
priority: 0,
prompt_id: 'prompt-id',
extra_data: { client_id: 'client-id' }
},
{ status_str: 'success', messages: [], completed: true },
{
'node-1': {
@@ -128,7 +228,11 @@ describe('TaskItemImpl', () => {
it(`should recognize ${extension} audio`, () => {
const taskItem = new TaskItemImpl(
'History',
[0, 'prompt-id', {}, { client_id: 'client-id' }, []],
{
priority: 0,
prompt_id: 'prompt-id',
extra_data: { client_id: 'client-id' }
},
{ status_str: 'success', messages: [], completed: true },
{
'node-1': {