mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-01-26 19:09:52 +00:00
feat(historyV2): load workflows for images (#6384)
## Summary Hooked up the "Load Workflow" action to our `history_v2` API. Note: Our cloud envs were being stress tested right now so images are loading at time of recording. Images were loading for me during development before I had time to create the video. ## Screenshots 📷 https://github.com/user-attachments/assets/02145504-ceae-497b-9049-553796d698da ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-6384-Feat-history-v2-workflows-29b6d73d365081bcb706fe799b8ce66a) by [Unito](https://www.unito.io)
This commit is contained in:
@@ -104,6 +104,7 @@ import { useI18n } from 'vue-i18n'
|
||||
|
||||
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
|
||||
import VirtualGrid from '@/components/common/VirtualGrid.vue'
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import type { ComfyNode } from '@/platform/workflow/validation/schemas/workflowSchema'
|
||||
import { api } from '@/scripts/api'
|
||||
@@ -206,7 +207,9 @@ const menuItems = computed<MenuItem[]>(() => {
|
||||
label: t('g.loadWorkflow'),
|
||||
icon: 'pi pi-file-export',
|
||||
command: () => menuTargetTask.value?.loadWorkflow(app),
|
||||
disabled: !menuTargetTask.value?.workflow
|
||||
disabled: isCloud
|
||||
? !menuTargetTask.value?.isHistory
|
||||
: !menuTargetTask.value?.workflow
|
||||
},
|
||||
{
|
||||
label: t('g.goToNode'),
|
||||
|
||||
@@ -13,11 +13,6 @@
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import type { TaskItem } from '@/schemas/apiSchema'
|
||||
|
||||
interface ReconciliationResult {
|
||||
/** All items to display, sorted by queueIndex descending (newest first) */
|
||||
items: TaskItem[]
|
||||
}
|
||||
|
||||
/**
|
||||
* V1 reconciliation: QueueIndex-based filtering works because V1 has stable,
|
||||
* monotonically increasing queue indices.
|
||||
@@ -25,13 +20,15 @@ interface ReconciliationResult {
|
||||
* Sort order: Sorts serverHistory by queueIndex descending (newest first) to ensure
|
||||
* consistent ordering. JavaScript .filter() maintains iteration order, so filtered
|
||||
* results remain sorted. clientHistory is assumed already sorted from previous update.
|
||||
*
|
||||
* @returns All items to display, sorted by queueIndex descending (newest first)
|
||||
*/
|
||||
function reconcileHistoryV1(
|
||||
serverHistory: TaskItem[],
|
||||
clientHistory: TaskItem[],
|
||||
maxItems: number,
|
||||
lastKnownQueueIndex: number | undefined
|
||||
): ReconciliationResult {
|
||||
): TaskItem[] {
|
||||
const sortedServerHistory = serverHistory.sort(
|
||||
(a, b) => b.prompt[0] - a.prompt[0]
|
||||
)
|
||||
@@ -53,13 +50,9 @@ function reconcileHistoryV1(
|
||||
)
|
||||
|
||||
// Merge new and reused items, sort by queueIndex descending, limit to maxItems
|
||||
const allItems = [...itemsAddedSinceLastSync, ...clientItemsStillOnServer]
|
||||
return [...itemsAddedSinceLastSync, ...clientItemsStillOnServer]
|
||||
.sort((a, b) => b.prompt[0] - a.prompt[0])
|
||||
.slice(0, maxItems)
|
||||
|
||||
return {
|
||||
items: allItems
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -69,12 +62,14 @@ function reconcileHistoryV1(
|
||||
* Sort order: Sorts serverHistory by queueIndex descending (newest first) to ensure
|
||||
* consistent ordering. JavaScript .filter() maintains iteration order, so filtered
|
||||
* results remain sorted. clientHistory is assumed already sorted from previous update.
|
||||
*
|
||||
* @returns All items to display, sorted by queueIndex descending (newest first)
|
||||
*/
|
||||
function reconcileHistoryV2(
|
||||
serverHistory: TaskItem[],
|
||||
clientHistory: TaskItem[],
|
||||
maxItems: number
|
||||
): ReconciliationResult {
|
||||
): TaskItem[] {
|
||||
const sortedServerHistory = serverHistory.sort(
|
||||
(a, b) => b.prompt[0] - a.prompt[0]
|
||||
)
|
||||
@@ -84,29 +79,18 @@ function reconcileHistoryV2(
|
||||
)
|
||||
const clientPromptIds = new Set(clientHistory.map((item) => item.prompt[1]))
|
||||
|
||||
const newPromptIds = new Set(
|
||||
[...serverPromptIds].filter((id) => !clientPromptIds.has(id))
|
||||
const newItems = sortedServerHistory.filter(
|
||||
(item) => !clientPromptIds.has(item.prompt[1])
|
||||
)
|
||||
|
||||
const newItems = sortedServerHistory.filter((item) =>
|
||||
newPromptIds.has(item.prompt[1])
|
||||
)
|
||||
|
||||
const retainedPromptIds = new Set(
|
||||
[...serverPromptIds].filter((id) => clientPromptIds.has(id))
|
||||
)
|
||||
const clientItemsStillOnServer = clientHistory.filter((item) =>
|
||||
retainedPromptIds.has(item.prompt[1])
|
||||
serverPromptIds.has(item.prompt[1])
|
||||
)
|
||||
|
||||
// Merge new and reused items, sort by queueIndex descending, limit to maxItems
|
||||
const allItems = [...newItems, ...clientItemsStillOnServer]
|
||||
return [...newItems, ...clientItemsStillOnServer]
|
||||
.sort((a, b) => b.prompt[0] - a.prompt[0])
|
||||
.slice(0, maxItems)
|
||||
|
||||
return {
|
||||
items: allItems
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -125,7 +109,7 @@ export function reconcileHistory(
|
||||
clientHistory: TaskItem[],
|
||||
maxItems: number,
|
||||
lastKnownQueueIndex?: number
|
||||
): ReconciliationResult {
|
||||
): TaskItem[] {
|
||||
if (isCloud) {
|
||||
return reconcileHistoryV2(serverHistory, clientHistory, maxItems)
|
||||
}
|
||||
|
||||
21
src/platform/workflow/cloud/getWorkflowFromHistory.ts
Normal file
21
src/platform/workflow/cloud/getWorkflowFromHistory.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import type { ComfyWorkflowJSON } from '@/platform/workflow/validation/schemas/workflowSchema'
|
||||
import type { PromptId } from '@/schemas/apiSchema'
|
||||
|
||||
export async function getWorkflowFromHistory(
|
||||
fetchApi: (url: string) => Promise<Response>,
|
||||
promptId: PromptId
|
||||
): Promise<ComfyWorkflowJSON | undefined> {
|
||||
try {
|
||||
const res = await fetchApi(`/history_v2/${promptId}`)
|
||||
const json = await res.json()
|
||||
|
||||
const historyItem = json[promptId]
|
||||
if (!historyItem) return undefined
|
||||
|
||||
const workflow = historyItem.prompt?.extra_data?.extra_pnginfo?.workflow
|
||||
return workflow ?? undefined
|
||||
} catch (error) {
|
||||
console.error(`Failed to fetch workflow for prompt ${promptId}:`, error)
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
10
src/platform/workflow/cloud/index.ts
Normal file
10
src/platform/workflow/cloud/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* Cloud: Fetches workflow by prompt_id. Desktop: Returns undefined (workflows already in history).
|
||||
*/
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
|
||||
import { getWorkflowFromHistory as cloudImpl } from './getWorkflowFromHistory'
|
||||
|
||||
export const getWorkflowFromHistory = isCloud
|
||||
? cloudImpl
|
||||
: async () => undefined
|
||||
@@ -13,6 +13,7 @@ import { LinkReleaseTriggerAction } from '@/types/searchBoxTypes'
|
||||
const zNodeType = z.string()
|
||||
export const zQueueIndex = z.number()
|
||||
export const zPromptId = z.string()
|
||||
export type PromptId = z.infer<typeof zPromptId>
|
||||
export const resultItemType = z.enum(['input', 'output', 'temp'])
|
||||
export type ResultItemType = z.infer<typeof resultItemType>
|
||||
|
||||
|
||||
@@ -2,7 +2,9 @@ import _ from 'es-toolkit/compat'
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed, ref, shallowRef, toRaw, toValue } from 'vue'
|
||||
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import { reconcileHistory } from '@/platform/remote/comfyui/history/reconciliation'
|
||||
import { getWorkflowFromHistory } from '@/platform/workflow/cloud'
|
||||
import type {
|
||||
ComfyWorkflowJSON,
|
||||
NodeId
|
||||
@@ -379,24 +381,37 @@ export class TaskItemImpl {
|
||||
}
|
||||
|
||||
public async loadWorkflow(app: ComfyApp) {
|
||||
if (!this.workflow) {
|
||||
return
|
||||
}
|
||||
await app.loadGraphData(toRaw(this.workflow))
|
||||
if (this.outputs) {
|
||||
const nodeOutputsStore = useNodeOutputStore()
|
||||
const rawOutputs = toRaw(this.outputs)
|
||||
for (const nodeExecutionId in rawOutputs) {
|
||||
nodeOutputsStore.setNodeOutputsByExecutionId(
|
||||
nodeExecutionId,
|
||||
rawOutputs[nodeExecutionId]
|
||||
)
|
||||
}
|
||||
useExtensionService().invokeExtensions(
|
||||
'onNodeOutputsUpdated',
|
||||
app.nodeOutputs
|
||||
let workflowData = this.workflow
|
||||
|
||||
if (isCloud && !workflowData && this.isHistory) {
|
||||
workflowData = await getWorkflowFromHistory(
|
||||
(url) => app.api.fetchApi(url),
|
||||
this.promptId
|
||||
)
|
||||
}
|
||||
|
||||
if (!workflowData) {
|
||||
return
|
||||
}
|
||||
|
||||
await app.loadGraphData(toRaw(workflowData))
|
||||
|
||||
if (!this.outputs) {
|
||||
return
|
||||
}
|
||||
|
||||
const nodeOutputsStore = useNodeOutputStore()
|
||||
const rawOutputs = toRaw(this.outputs)
|
||||
for (const nodeExecutionId in rawOutputs) {
|
||||
nodeOutputsStore.setNodeOutputsByExecutionId(
|
||||
nodeExecutionId,
|
||||
rawOutputs[nodeExecutionId]
|
||||
)
|
||||
}
|
||||
useExtensionService().invokeExtensions(
|
||||
'onNodeOutputsUpdated',
|
||||
app.nodeOutputs
|
||||
)
|
||||
}
|
||||
|
||||
public flatten(): TaskItemImpl[] {
|
||||
@@ -492,7 +507,7 @@ export const useQueueStore = defineStore('queue', () => {
|
||||
|
||||
const currentHistory = toValue(historyTasks)
|
||||
|
||||
const { items } = reconcileHistory(
|
||||
const items = reconcileHistory(
|
||||
history.History,
|
||||
currentHistory.map((impl) => impl.toTaskItem()),
|
||||
toValue(maxHistoryItems),
|
||||
|
||||
@@ -21,8 +21,8 @@ function createHistoryItem(promptId: string, queueIndex = 0): TaskItem {
|
||||
}
|
||||
}
|
||||
|
||||
function getAllPromptIds(result: { items: TaskItem[] }): string[] {
|
||||
return result.items.map((item) => item.prompt[1])
|
||||
function getAllPromptIds(result: TaskItem[]): string[] {
|
||||
return result.map((item) => item.prompt[1])
|
||||
}
|
||||
|
||||
describe('reconcileHistory (V1)', () => {
|
||||
@@ -74,9 +74,9 @@ describe('reconcileHistory (V1)', () => {
|
||||
|
||||
const result = reconcileHistory(serverHistory, [], 10, undefined)
|
||||
|
||||
expect(result.items).toHaveLength(2)
|
||||
expect(result.items[0].prompt[1]).toBe('item-1')
|
||||
expect(result.items[1].prompt[1]).toBe('item-2')
|
||||
expect(result).toHaveLength(2)
|
||||
expect(result[0].prompt[1]).toBe('item-1')
|
||||
expect(result[1].prompt[1]).toBe('item-2')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -144,9 +144,9 @@ describe('reconcileHistory (V1)', () => {
|
||||
|
||||
const result = reconcileHistory(serverHistory, clientHistory, 2, 10)
|
||||
|
||||
expect(result.items).toHaveLength(2)
|
||||
expect(result.items[0].prompt[1]).toBe('new-1')
|
||||
expect(result.items[1].prompt[1]).toBe('new-2')
|
||||
expect(result).toHaveLength(2)
|
||||
expect(result[0].prompt[1]).toBe('new-1')
|
||||
expect(result[1].prompt[1]).toBe('new-2')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -168,13 +168,13 @@ describe('reconcileHistory (V1)', () => {
|
||||
|
||||
const result = reconcileHistory([], clientHistory, 10, 5)
|
||||
|
||||
expect(result.items).toHaveLength(0)
|
||||
expect(result).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('should return empty result when both collections are empty', () => {
|
||||
const result = reconcileHistory([], [], 10, undefined)
|
||||
|
||||
expect(result.items).toHaveLength(0)
|
||||
expect(result).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -295,9 +295,9 @@ describe('reconcileHistory (V2/Cloud)', () => {
|
||||
|
||||
const result = reconcileHistory(serverHistory, clientHistory, 2)
|
||||
|
||||
expect(result.items).toHaveLength(2)
|
||||
expect(result.items[0].prompt[1]).toBe('new-1')
|
||||
expect(result.items[1].prompt[1]).toBe('new-2')
|
||||
expect(result).toHaveLength(2)
|
||||
expect(result[0].prompt[1]).toBe('new-1')
|
||||
expect(result[1].prompt[1]).toBe('new-2')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -310,9 +310,9 @@ describe('reconcileHistory (V2/Cloud)', () => {
|
||||
|
||||
const result = reconcileHistory(serverHistory, [], 10)
|
||||
|
||||
expect(result.items).toHaveLength(2)
|
||||
expect(result.items[0].prompt[1]).toBe('item-1')
|
||||
expect(result.items[1].prompt[1]).toBe('item-2')
|
||||
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', () => {
|
||||
@@ -323,13 +323,13 @@ describe('reconcileHistory (V2/Cloud)', () => {
|
||||
|
||||
const result = reconcileHistory([], clientHistory, 10)
|
||||
|
||||
expect(result.items).toHaveLength(0)
|
||||
expect(result).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('should return empty result when both collections are empty', () => {
|
||||
const result = reconcileHistory([], [], 10)
|
||||
|
||||
expect(result.items).toHaveLength(0)
|
||||
expect(result).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import type { ComfyWorkflowJSON } from '@/platform/workflow/validation/schemas/workflowSchema'
|
||||
import { getWorkflowFromHistory } from '@/platform/workflow/cloud/getWorkflowFromHistory'
|
||||
|
||||
const mockWorkflow: ComfyWorkflowJSON = {
|
||||
id: 'test-workflow-id',
|
||||
revision: 0,
|
||||
last_node_id: 5,
|
||||
last_link_id: 3,
|
||||
nodes: [],
|
||||
links: [],
|
||||
groups: [],
|
||||
config: {},
|
||||
extra: {},
|
||||
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
|
||||
}
|
||||
}
|
||||
},
|
||||
outputs: {},
|
||||
status: {
|
||||
status_str: 'success',
|
||||
completed: true,
|
||||
messages: []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
describe('getWorkflowFromHistory', () => {
|
||||
it('should fetch workflow from /history_v2/{prompt_id} endpoint', async () => {
|
||||
const mockFetchApi = vi.fn().mockResolvedValue({
|
||||
json: async () => mockHistoryResponse
|
||||
})
|
||||
|
||||
await getWorkflowFromHistory(mockFetchApi, 'test-prompt-id')
|
||||
|
||||
expect(mockFetchApi).toHaveBeenCalledWith('/history_v2/test-prompt-id')
|
||||
})
|
||||
|
||||
it('should extract and return workflow from response', async () => {
|
||||
const mockFetchApi = vi.fn().mockResolvedValue({
|
||||
json: async () => mockHistoryResponse
|
||||
})
|
||||
|
||||
const result = await getWorkflowFromHistory(mockFetchApi, 'test-prompt-id')
|
||||
|
||||
expect(result).toEqual(mockWorkflow)
|
||||
})
|
||||
|
||||
it('should return undefined when prompt_id not found in response', async () => {
|
||||
const mockFetchApi = vi.fn().mockResolvedValue({
|
||||
json: async () => ({})
|
||||
})
|
||||
|
||||
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')
|
||||
|
||||
expect(result).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should handle fetch errors gracefully', async () => {
|
||||
const mockFetchApi = vi.fn().mockRejectedValue(new Error('Network error'))
|
||||
|
||||
const result = await getWorkflowFromHistory(mockFetchApi, 'test-prompt-id')
|
||||
|
||||
expect(result).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should handle malformed JSON responses', async () => {
|
||||
const mockFetchApi = vi.fn().mockResolvedValue({
|
||||
json: async () => {
|
||||
throw new Error('Invalid JSON')
|
||||
}
|
||||
})
|
||||
|
||||
const result = await getWorkflowFromHistory(mockFetchApi, 'test-prompt-id')
|
||||
|
||||
expect(result).toBeUndefined()
|
||||
})
|
||||
})
|
||||
175
tests-ui/tests/stores/queueStore.loadWorkflow.test.ts
Normal file
175
tests-ui/tests/stores/queueStore.loadWorkflow.test.ts
Normal file
@@ -0,0 +1,175 @@
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import type { ComfyApp } from '@/scripts/app'
|
||||
import type { ComfyWorkflowJSON } from '@/platform/workflow/validation/schemas/workflowSchema'
|
||||
import { TaskItemImpl } from '@/stores/queueStore'
|
||||
import * as getWorkflowModule from '@/platform/workflow/cloud'
|
||||
|
||||
vi.mock('@/platform/distribution/types', () => ({
|
||||
isCloud: true
|
||||
}))
|
||||
|
||||
vi.mock('@/services/extensionService', () => ({
|
||||
useExtensionService: vi.fn(() => ({
|
||||
invokeExtensions: vi.fn()
|
||||
}))
|
||||
}))
|
||||
|
||||
const mockWorkflow: ComfyWorkflowJSON = {
|
||||
id: 'test-workflow-id',
|
||||
revision: 0,
|
||||
last_node_id: 5,
|
||||
last_link_id: 3,
|
||||
nodes: [],
|
||||
links: [],
|
||||
groups: [],
|
||||
config: {},
|
||||
extra: {},
|
||||
version: 0.4
|
||||
}
|
||||
|
||||
const createHistoryTaskWithWorkflow = (): TaskItemImpl => {
|
||||
return new TaskItemImpl(
|
||||
'History',
|
||||
[
|
||||
0, // queueIndex
|
||||
'test-prompt-id', // promptId
|
||||
{}, // promptInputs
|
||||
{
|
||||
client_id: 'test-client',
|
||||
extra_pnginfo: {
|
||||
workflow: mockWorkflow
|
||||
}
|
||||
},
|
||||
[] // outputsToExecute
|
||||
],
|
||||
{
|
||||
status_str: 'success',
|
||||
completed: true,
|
||||
messages: []
|
||||
},
|
||||
{} // outputs
|
||||
)
|
||||
}
|
||||
|
||||
const createHistoryTaskWithoutWorkflow = (): TaskItemImpl => {
|
||||
return new TaskItemImpl(
|
||||
'History',
|
||||
[
|
||||
0,
|
||||
'test-prompt-id',
|
||||
{},
|
||||
{
|
||||
client_id: 'test-client'
|
||||
// No extra_pnginfo.workflow
|
||||
},
|
||||
[]
|
||||
],
|
||||
{
|
||||
status_str: 'success',
|
||||
completed: true,
|
||||
messages: []
|
||||
},
|
||||
{}
|
||||
)
|
||||
}
|
||||
|
||||
describe('TaskItemImpl.loadWorkflow - cloud history workflow fetching', () => {
|
||||
let mockApp: ComfyApp
|
||||
let mockFetchApi: ReturnType<typeof vi.fn>
|
||||
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
vi.clearAllMocks()
|
||||
|
||||
mockFetchApi = vi.fn()
|
||||
mockApp = {
|
||||
loadGraphData: vi.fn(),
|
||||
nodeOutputs: {},
|
||||
api: {
|
||||
fetchApi: mockFetchApi
|
||||
}
|
||||
} as unknown as ComfyApp
|
||||
|
||||
vi.spyOn(getWorkflowModule, 'getWorkflowFromHistory')
|
||||
})
|
||||
|
||||
it('should load workflow directly when workflow is in extra_pnginfo', async () => {
|
||||
const task = createHistoryTaskWithWorkflow()
|
||||
|
||||
await task.loadWorkflow(mockApp)
|
||||
|
||||
expect(mockApp.loadGraphData).toHaveBeenCalledWith(mockWorkflow)
|
||||
expect(mockFetchApi).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should fetch workflow from cloud when workflow is missing from history task', async () => {
|
||||
const task = createHistoryTaskWithoutWorkflow()
|
||||
|
||||
// Mock getWorkflowFromHistory to return workflow
|
||||
vi.spyOn(getWorkflowModule, 'getWorkflowFromHistory').mockResolvedValue(
|
||||
mockWorkflow
|
||||
)
|
||||
|
||||
await task.loadWorkflow(mockApp)
|
||||
|
||||
expect(getWorkflowModule.getWorkflowFromHistory).toHaveBeenCalledWith(
|
||||
expect.any(Function),
|
||||
'test-prompt-id'
|
||||
)
|
||||
expect(mockApp.loadGraphData).toHaveBeenCalledWith(mockWorkflow)
|
||||
})
|
||||
|
||||
it('should not load workflow when fetch returns undefined', async () => {
|
||||
const task = createHistoryTaskWithoutWorkflow()
|
||||
|
||||
vi.spyOn(getWorkflowModule, 'getWorkflowFromHistory').mockResolvedValue(
|
||||
undefined
|
||||
)
|
||||
|
||||
await task.loadWorkflow(mockApp)
|
||||
|
||||
expect(getWorkflowModule.getWorkflowFromHistory).toHaveBeenCalled()
|
||||
expect(mockApp.loadGraphData).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should only fetch for history tasks, not running tasks', async () => {
|
||||
const runningTask = new TaskItemImpl(
|
||||
'Running',
|
||||
[
|
||||
0,
|
||||
'test-prompt-id',
|
||||
{},
|
||||
{
|
||||
client_id: 'test-client'
|
||||
},
|
||||
[]
|
||||
],
|
||||
undefined,
|
||||
{}
|
||||
)
|
||||
|
||||
vi.spyOn(getWorkflowModule, 'getWorkflowFromHistory').mockResolvedValue(
|
||||
mockWorkflow
|
||||
)
|
||||
|
||||
await runningTask.loadWorkflow(mockApp)
|
||||
|
||||
expect(getWorkflowModule.getWorkflowFromHistory).not.toHaveBeenCalled()
|
||||
expect(mockApp.loadGraphData).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should handle fetch errors gracefully by returning undefined', async () => {
|
||||
const task = createHistoryTaskWithoutWorkflow()
|
||||
|
||||
vi.spyOn(getWorkflowModule, 'getWorkflowFromHistory').mockResolvedValue(
|
||||
undefined
|
||||
)
|
||||
|
||||
await task.loadWorkflow(mockApp)
|
||||
|
||||
expect(getWorkflowModule.getWorkflowFromHistory).toHaveBeenCalled()
|
||||
expect(mockApp.loadGraphData).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user