diff --git a/web/app/components/workflow/hooks/use-workflow-run-event/__tests__/use-workflow-failed.spec.ts b/web/app/components/workflow/hooks/use-workflow-run-event/__tests__/use-workflow-failed.spec.ts index 53ee281f7e9..b381801aabd 100644 --- a/web/app/components/workflow/hooks/use-workflow-run-event/__tests__/use-workflow-failed.spec.ts +++ b/web/app/components/workflow/hooks/use-workflow-run-event/__tests__/use-workflow-failed.spec.ts @@ -1,15 +1,118 @@ -import { baseRunningData, renderWorkflowHook } from '../../../__tests__/workflow-test-env' -import { WorkflowRunningStatus } from '../../../types' +import { act, waitFor } from '@testing-library/react' +import { createEdge, createNode, createNodeTracing } from '../../../__tests__/fixtures' +import { baseRunningData } from '../../../__tests__/workflow-test-env' +import { NodeRunningStatus, WorkflowRunningStatus } from '../../../types' import { useWorkflowFailed } from '../use-workflow-failed' +import { getEdgeRuntimeState, getNodeRuntimeState, renderRunEventHook } from './test-helpers' describe('useWorkflowFailed', () => { - it('sets status to Failed', () => { - const { result, store } = renderWorkflowHook(() => useWorkflowFailed(), { - initialStoreState: { workflowRunningData: baseRunningData() }, + it('settles active workflow and canvas state as failed', async () => { + const { result, store } = renderRunEventHook(() => useWorkflowFailed(), { + nodes: [ + createNode({ + id: 'running-node', + data: { + _runningStatus: NodeRunningStatus.Running, + _waitingRun: false, + }, + }), + createNode({ + id: 'succeeded-node', + data: { + _runningStatus: NodeRunningStatus.Succeeded, + _waitingRun: false, + }, + }), + createNode({ + id: 'waiting-node', + data: { _waitingRun: true }, + }), + ], + edges: [ + createEdge({ + id: 'running-edge', + source: 'succeeded-node', + target: 'running-node', + data: { + _sourceRunningStatus: NodeRunningStatus.Succeeded, + _targetRunningStatus: NodeRunningStatus.Running, + _waitingRun: false, + }, + }), + createEdge({ + id: 'waiting-edge', + source: 'running-node', + target: 'waiting-node', + data: { + _sourceRunningStatus: NodeRunningStatus.Running, + _waitingRun: true, + }, + }), + ], + initialStoreState: { + workflowRunningData: baseRunningData({ + tracing: [ + createNodeTracing({ + node_id: 'running-node', + status: NodeRunningStatus.Running, + }), + createNodeTracing({ + node_id: 'succeeded-node', + status: NodeRunningStatus.Succeeded, + }), + ], + }), + }, }) - result.current.handleWorkflowFailed() + act(() => { + result.current.handleWorkflowFailed() + }) expect(store.getState().workflowRunningData!.result.status).toBe(WorkflowRunningStatus.Failed) + expect(store.getState().workflowRunningData!.tracing!.map((trace) => trace.status)).toEqual([ + NodeRunningStatus.Failed, + NodeRunningStatus.Succeeded, + ]) + + await waitFor(() => { + const runningNode = result.current.nodes.find((node) => node.id === 'running-node') + const succeededNode = result.current.nodes.find((node) => node.id === 'succeeded-node') + const waitingNode = result.current.nodes.find((node) => node.id === 'waiting-node') + const runningEdge = result.current.edges.find((edge) => edge.id === 'running-edge') + const waitingEdge = result.current.edges.find((edge) => edge.id === 'waiting-edge') + + expect(getNodeRuntimeState(runningNode)).toMatchObject({ + _runningStatus: NodeRunningStatus.Failed, + _waitingRun: false, + }) + expect(getNodeRuntimeState(succeededNode)._runningStatus).toBe(NodeRunningStatus.Succeeded) + expect(getNodeRuntimeState(waitingNode)._waitingRun).toBe(false) + expect(getEdgeRuntimeState(runningEdge)).toMatchObject({ + _sourceRunningStatus: NodeRunningStatus.Succeeded, + _targetRunningStatus: NodeRunningStatus.Failed, + _waitingRun: false, + }) + expect(getEdgeRuntimeState(waitingEdge)).toMatchObject({ + _sourceRunningStatus: NodeRunningStatus.Failed, + _waitingRun: false, + }) + }) + }) + + it('ignores a late failure after the workflow has stopped', () => { + const { result, store } = renderRunEventHook(() => useWorkflowFailed(), { + initialStoreState: { + workflowRunningData: baseRunningData({ + result: { status: WorkflowRunningStatus.Stopped }, + }), + }, + }) + + act(() => { + result.current.handleWorkflowFailed() + }) + + expect(store.getState().workflowRunningData!.result.status).toBe(WorkflowRunningStatus.Stopped) }) }) diff --git a/web/app/components/workflow/hooks/use-workflow-run-event/use-workflow-failed.ts b/web/app/components/workflow/hooks/use-workflow-run-event/use-workflow-failed.ts index 2e890fb9de8..b50663dc214 100644 --- a/web/app/components/workflow/hooks/use-workflow-run-event/use-workflow-failed.ts +++ b/web/app/components/workflow/hooks/use-workflow-run-event/use-workflow-failed.ts @@ -1,23 +1,59 @@ import { produce } from 'immer' import { useCallback } from 'react' +import { useStoreApi } from 'reactflow' import { useWorkflowStore } from '@/app/components/workflow/store' -import { WorkflowRunningStatus } from '@/app/components/workflow/types' +import { NodeRunningStatus, WorkflowRunningStatus } from '@/app/components/workflow/types' export const useWorkflowFailed = () => { + const store = useStoreApi() const workflowStore = useWorkflowStore() const handleWorkflowFailed = useCallback(() => { const { workflowRunningData, setWorkflowRunningData } = workflowStore.getState() + if (!workflowRunningData) return + if ( + workflowRunningData.result.status === WorkflowRunningStatus.Succeeded || + workflowRunningData.result.status === WorkflowRunningStatus.Failed || + workflowRunningData.result.status === WorkflowRunningStatus.Stopped + ) + return + + const { getNodes, setNodes, edges, setEdges } = store.getState() setWorkflowRunningData( - produce(workflowRunningData!, (draft) => { + produce(workflowRunningData, (draft) => { draft.result = { ...draft.result, status: WorkflowRunningStatus.Failed, } + draft.tracing?.forEach((trace) => { + if (trace.status === NodeRunningStatus.Running) trace.status = NodeRunningStatus.Failed + }) }), ) - }, [workflowStore]) + + setNodes( + produce(getNodes(), (draft) => { + draft.forEach((node) => { + if (node.data._runningStatus === NodeRunningStatus.Running) + node.data._runningStatus = NodeRunningStatus.Failed + node.data._waitingRun = false + }) + }), + ) + setEdges( + produce(edges, (draft) => { + draft.forEach((edge) => { + if (!edge.data) return + if (edge.data._sourceRunningStatus === NodeRunningStatus.Running) + edge.data._sourceRunningStatus = NodeRunningStatus.Failed + if (edge.data._targetRunningStatus === NodeRunningStatus.Running) + edge.data._targetRunningStatus = NodeRunningStatus.Failed + edge.data._waitingRun = false + }) + }), + ) + }, [store, workflowStore]) return { handleWorkflowFailed,