fix(web): settle running nodes after workflow failure (#39173)

This commit is contained in:
yyh 2026-07-17 13:15:01 +08:00 committed by GitHub
parent af7e59de7c
commit f962c9e47a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 148 additions and 9 deletions

View File

@ -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)
})
})

View File

@ -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,