From fe90453eedda947f2b94975df73c6f9c7a529f57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yanli=20=E7=9B=90=E7=B2=92?= Date: Tue, 10 Mar 2026 19:14:14 +0800 Subject: [PATCH 01/14] fix: preserve workflow tracing by execution id --- .../base/chat/chat/__tests__/hooks.spec.tsx | 72 +++++++++- web/app/components/base/chat/chat/hooks.ts | 91 +++++------- .../share/text-generation/result/index.tsx | 22 +-- .../use-workflow-run-event-store-only.spec.ts | 22 +++ ...e-workflow-run-event-with-viewport.spec.ts | 30 +++- .../use-workflow-agent-log.ts | 7 +- .../use-workflow-node-started.ts | 4 +- .../workflow/panel/debug-and-preview/hooks.ts | 53 +++---- .../workflow/utils/top-level-tracing.spec.ts | 133 ++++++++++++++++++ .../workflow/utils/top-level-tracing.ts | 23 +++ 10 files changed, 349 insertions(+), 108 deletions(-) create mode 100644 web/app/components/workflow/utils/top-level-tracing.spec.ts create mode 100644 web/app/components/workflow/utils/top-level-tracing.ts diff --git a/web/app/components/base/chat/chat/__tests__/hooks.spec.tsx b/web/app/components/base/chat/chat/__tests__/hooks.spec.tsx index 4bf1f60fbe..57205f38c4 100644 --- a/web/app/components/base/chat/chat/__tests__/hooks.spec.tsx +++ b/web/app/components/base/chat/chat/__tests__/hooks.spec.tsx @@ -448,6 +448,66 @@ describe('useChat', () => { expect(lastResponse.workflowProcess?.status).toBe('failed') }) + it('should keep separate iteration traces for repeated executions of the same iteration node', async () => { + let callbacks: HookCallbacks + + vi.mocked(ssePost).mockImplementation(async (_url, _params, options) => { + callbacks = options as HookCallbacks + }) + + const { result } = renderHook(() => useChat()) + + act(() => { + result.current.handleSend('test-url', { query: 'iteration trace test' }, {}) + }) + + act(() => { + callbacks.onWorkflowStarted({ workflow_run_id: 'wr-1', task_id: 't-1' }) + callbacks.onIterationStart({ data: { id: 'iter-run-1', node_id: 'iter-1' } }) + callbacks.onIterationStart({ data: { id: 'iter-run-2', node_id: 'iter-1' } }) + callbacks.onIterationFinish({ data: { id: 'iter-run-1', node_id: 'iter-1', status: 'succeeded' } }) + callbacks.onIterationFinish({ data: { id: 'iter-run-2', node_id: 'iter-1', status: 'succeeded' } }) + }) + + const tracing = result.current.chatList[1].workflowProcess?.tracing ?? [] + + expect(tracing).toHaveLength(2) + expect(tracing).toEqual(expect.arrayContaining([ + expect.objectContaining({ id: 'iter-run-1', status: 'succeeded' }), + expect.objectContaining({ id: 'iter-run-2', status: 'succeeded' }), + ])) + }) + + it('should keep separate top-level traces for repeated executions of the same node', async () => { + let callbacks: HookCallbacks + + vi.mocked(ssePost).mockImplementation(async (_url, _params, options) => { + callbacks = options as HookCallbacks + }) + + const { result } = renderHook(() => useChat()) + + act(() => { + result.current.handleSend('test-url', { query: 'top-level trace test' }, {}) + }) + + act(() => { + callbacks.onWorkflowStarted({ workflow_run_id: 'wr-1', task_id: 't-1' }) + callbacks.onNodeStarted({ data: { id: 'node-run-1', node_id: 'node-1', title: 'Node 1' } }) + callbacks.onNodeStarted({ data: { id: 'node-run-2', node_id: 'node-1', title: 'Node 1 retry' } }) + callbacks.onNodeFinished({ data: { id: 'node-run-1', node_id: 'node-1', status: 'succeeded' } }) + callbacks.onNodeFinished({ data: { id: 'node-run-2', node_id: 'node-1', status: 'succeeded' } }) + }) + + const tracing = result.current.chatList[1].workflowProcess?.tracing ?? [] + + expect(tracing).toHaveLength(2) + expect(tracing).toEqual(expect.arrayContaining([ + expect.objectContaining({ id: 'node-run-1', status: 'succeeded' }), + expect.objectContaining({ id: 'node-run-2', status: 'succeeded' }), + ])) + }) + it('should handle early exits in tracing events during iteration or loop', async () => { let callbacks: HookCallbacks @@ -483,7 +543,7 @@ describe('useChat', () => { callbacks.onNodeFinished({ data: { id: 'n-1', iteration_id: 'iter-1' } }) }) - const traceLen1 = result.current.chatList[result.current.chatList.length - 1].workflowProcess?.tracing?.length + const traceLen1 = result.current.chatList.at(-1)!.workflowProcess?.tracing?.length expect(traceLen1).toBe(0) // None added due to iteration early hits }) @@ -567,7 +627,7 @@ describe('useChat', () => { expect(result.current.chatList.some(item => item.id === 'question-m-child')).toBe(true) expect(result.current.chatList.some(item => item.id === 'm-child')).toBe(true) - expect(result.current.chatList[result.current.chatList.length - 1].content).toBe('child answer') + expect(result.current.chatList.at(-1)!.content).toBe('child answer') }) it('should strip local file urls before sending payload', () => { @@ -665,7 +725,7 @@ describe('useChat', () => { }) expect(onGetConversationMessages).toHaveBeenCalled() - expect(result.current.chatList[result.current.chatList.length - 1].content).toBe('streamed content') + expect(result.current.chatList.at(-1)!.content).toBe('streamed content') }) it('should clear suggested questions when suggestion fetch fails after completion', async () => { @@ -711,7 +771,7 @@ describe('useChat', () => { callbacks.onNodeFinished({ data: { node_id: 'n-loop', id: 'n-loop' } }) }) - const latestResponse = result.current.chatList[result.current.chatList.length - 1] + const latestResponse = result.current.chatList.at(-1)! expect(latestResponse.workflowProcess?.tracing).toHaveLength(0) }) @@ -738,7 +798,7 @@ describe('useChat', () => { callbacks.onTTSChunk('m-th-bind', '') }) - const latestResponse = result.current.chatList[result.current.chatList.length - 1] + const latestResponse = result.current.chatList.at(-1)! expect(latestResponse.id).toBe('m-th-bind') expect(latestResponse.conversationId).toBe('c-th-bind') expect(latestResponse.workflowProcess?.status).toBe('succeeded') @@ -831,7 +891,7 @@ describe('useChat', () => { callbacks.onCompleted() }) - const lastResponse = result.current.chatList[result.current.chatList.length - 1] + const lastResponse = result.current.chatList.at(-1)! expect(lastResponse.agent_thoughts![0].thought).toContain('resumed') expect(lastResponse.workflowProcess?.tracing?.length).toBeGreaterThan(0) diff --git a/web/app/components/base/chat/chat/hooks.ts b/web/app/components/base/chat/chat/hooks.ts index 307fd52443..e8ce744236 100644 --- a/web/app/components/base/chat/chat/hooks.ts +++ b/web/app/components/base/chat/chat/hooks.ts @@ -32,6 +32,7 @@ import { } from '@/app/components/base/file-uploader/utils' import { useToastContext } from '@/app/components/base/toast/context' import { NodeRunningStatus, WorkflowRunningStatus } from '@/app/components/workflow/types' +import { upsertTopLevelTracingNodeOnStart } from '@/app/components/workflow/utils/top-level-tracing' import useTimestamp from '@/hooks/use-timestamp' import { sseGet, @@ -395,8 +396,7 @@ export const useChat = ( if (!responseItem.workflowProcess?.tracing) return const tracing = responseItem.workflowProcess.tracing - const iterationIndex = tracing.findIndex(item => item.node_id === iterationFinishedData.node_id - && (item.execution_metadata?.parallel_id === iterationFinishedData.execution_metadata?.parallel_id || item.parallel_id === iterationFinishedData.execution_metadata?.parallel_id))! + const iterationIndex = tracing.findIndex(item => item.id === iterationFinishedData.id)! if (iterationIndex > -1) { tracing[iterationIndex] = { ...tracing[iterationIndex], @@ -413,33 +413,26 @@ export const useChat = ( if (!responseItem.workflowProcess.tracing) responseItem.workflowProcess.tracing = [] - const currentIndex = responseItem.workflowProcess.tracing.findIndex(item => item.node_id === nodeStartedData.node_id) - // if the node is already started, update the node - if (currentIndex > -1) { - responseItem.workflowProcess.tracing[currentIndex] = { - ...nodeStartedData, - status: NodeRunningStatus.Running, - } - } - else { - if (nodeStartedData.iteration_id) - return - - responseItem.workflowProcess.tracing.push({ - ...nodeStartedData, - status: WorkflowRunningStatus.Running, - }) - } + upsertTopLevelTracingNodeOnStart(responseItem.workflowProcess.tracing, { + ...nodeStartedData, + status: WorkflowRunningStatus.Running, + }) }) }, onNodeFinished: ({ data: nodeFinishedData }) => { updateChatTreeNode(messageId, (responseItem) => { + if (params.loop_id) + return + if (!responseItem.workflowProcess?.tracing) return if (nodeFinishedData.iteration_id) return + if (nodeFinishedData.loop_id) + return + const currentIndex = responseItem.workflowProcess.tracing.findIndex((item) => { if (!item.execution_metadata?.parallel_id) return item.id === nodeFinishedData.id @@ -481,8 +474,7 @@ export const useChat = ( if (!responseItem.workflowProcess?.tracing) return const tracing = responseItem.workflowProcess.tracing - const loopIndex = tracing.findIndex(item => item.node_id === loopFinishedData.node_id - && (item.execution_metadata?.parallel_id === loopFinishedData.execution_metadata?.parallel_id || item.parallel_id === loopFinishedData.execution_metadata?.parallel_id))! + const loopIndex = tracing.findIndex(item => item.id === loopFinishedData.id)! if (loopIndex > -1) { tracing[loopIndex] = { ...tracing[loopIndex], @@ -948,12 +940,13 @@ export const useChat = ( }, onIterationFinish: ({ data: iterationFinishedData }) => { const tracing = responseItem.workflowProcess!.tracing! - const iterationIndex = tracing.findIndex(item => item.node_id === iterationFinishedData.node_id - && (item.execution_metadata?.parallel_id === iterationFinishedData.execution_metadata?.parallel_id || item.parallel_id === iterationFinishedData.execution_metadata?.parallel_id))! - tracing[iterationIndex] = { - ...tracing[iterationIndex], - ...iterationFinishedData, - status: WorkflowRunningStatus.Succeeded, + const iterationIndex = tracing.findIndex(item => item.id === iterationFinishedData.id)! + if (iterationIndex > -1) { + tracing[iterationIndex] = { + ...tracing[iterationIndex], + ...iterationFinishedData, + status: WorkflowRunningStatus.Succeeded, + } } updateCurrentQAOnTree({ @@ -964,30 +957,18 @@ export const useChat = ( }) }, onNodeStarted: ({ data: nodeStartedData }) => { + if (data.loop_id) + return + if (!responseItem.workflowProcess) return if (!responseItem.workflowProcess.tracing) responseItem.workflowProcess.tracing = [] - const currentIndex = responseItem.workflowProcess.tracing.findIndex(item => item.node_id === nodeStartedData.node_id) - if (currentIndex > -1) { - responseItem.workflowProcess.tracing[currentIndex] = { - ...nodeStartedData, - status: NodeRunningStatus.Running, - } - } - else { - if (nodeStartedData.iteration_id) - return - - if (data.loop_id) - return - - responseItem.workflowProcess.tracing.push({ - ...nodeStartedData, - status: WorkflowRunningStatus.Running, - }) - } + upsertTopLevelTracingNodeOnStart(responseItem.workflowProcess.tracing, { + ...nodeStartedData, + status: WorkflowRunningStatus.Running, + }) updateCurrentQAOnTree({ placeholderQuestionId, questionItem, @@ -996,10 +977,13 @@ export const useChat = ( }) }, onNodeFinished: ({ data: nodeFinishedData }) => { + if (data.loop_id) + return + if (nodeFinishedData.iteration_id) return - if (data.loop_id) + if (nodeFinishedData.loop_id) return const currentIndex = responseItem.workflowProcess!.tracing!.findIndex((item) => { @@ -1045,12 +1029,13 @@ export const useChat = ( }, onLoopFinish: ({ data: loopFinishedData }) => { const tracing = responseItem.workflowProcess!.tracing! - const loopIndex = tracing.findIndex(item => item.node_id === loopFinishedData.node_id - && (item.execution_metadata?.parallel_id === loopFinishedData.execution_metadata?.parallel_id || item.parallel_id === loopFinishedData.execution_metadata?.parallel_id))! - tracing[loopIndex] = { - ...tracing[loopIndex], - ...loopFinishedData, - status: WorkflowRunningStatus.Succeeded, + const loopIndex = tracing.findIndex(item => item.id === loopFinishedData.id)! + if (loopIndex > -1) { + tracing[loopIndex] = { + ...tracing[loopIndex], + ...loopFinishedData, + status: WorkflowRunningStatus.Succeeded, + } } updateCurrentQAOnTree({ diff --git a/web/app/components/share/text-generation/result/index.tsx b/web/app/components/share/text-generation/result/index.tsx index 2bcd1c9d94..e3ffe9d209 100644 --- a/web/app/components/share/text-generation/result/index.tsx +++ b/web/app/components/share/text-generation/result/index.tsx @@ -337,11 +337,12 @@ const Result: FC = ({ onIterationFinish: ({ data }) => { setWorkflowProcessData(produce(getWorkflowProcessData()!, (draft) => { draft.expand = true - const iterationsIndex = draft.tracing.findIndex(item => item.node_id === data.node_id - && (item.execution_metadata?.parallel_id === data.execution_metadata?.parallel_id || item.parallel_id === data.execution_metadata?.parallel_id))! - draft.tracing[iterationsIndex] = { - ...data, - expand: !!data.error, + const iterationsIndex = draft.tracing.findIndex(item => item.id === data.id)! + if (iterationsIndex > -1) { + draft.tracing[iterationsIndex] = { + ...data, + expand: !!data.error, + } } })) }, @@ -366,11 +367,12 @@ const Result: FC = ({ onLoopFinish: ({ data }) => { setWorkflowProcessData(produce(getWorkflowProcessData()!, (draft) => { draft.expand = true - const loopsIndex = draft.tracing.findIndex(item => item.node_id === data.node_id - && (item.execution_metadata?.parallel_id === data.execution_metadata?.parallel_id || item.parallel_id === data.execution_metadata?.parallel_id))! - draft.tracing[loopsIndex] = { - ...data, - expand: !!data.error, + const loopsIndex = draft.tracing.findIndex(item => item.id === data.id)! + if (loopsIndex > -1) { + draft.tracing[loopsIndex] = { + ...data, + expand: !!data.error, + } } })) }, diff --git a/web/app/components/workflow/hooks/__tests__/use-workflow-run-event-store-only.spec.ts b/web/app/components/workflow/hooks/__tests__/use-workflow-run-event-store-only.spec.ts index 2085e5ab47..6a3347f77a 100644 --- a/web/app/components/workflow/hooks/__tests__/use-workflow-run-event-store-only.spec.ts +++ b/web/app/components/workflow/hooks/__tests__/use-workflow-run-event-store-only.spec.ts @@ -178,6 +178,28 @@ describe('useWorkflowAgentLog', () => { expect(store.getState().workflowRunningData!.tracing![0].execution_metadata!.agent_log).toHaveLength(1) }) + + it('should attach the log to the matching execution id when a node runs multiple times', () => { + const { result, store } = renderWorkflowHook(() => useWorkflowAgentLog(), { + initialStoreState: { + workflowRunningData: baseRunningData({ + tracing: [ + { id: 'trace-1', node_id: 'n1', execution_metadata: {} }, + { id: 'trace-2', node_id: 'n1', execution_metadata: {} }, + ], + }), + }, + }) + + result.current.handleWorkflowAgentLog({ + data: { node_id: 'n1', node_execution_id: 'trace-2', message_id: 'm2' }, + } as AgentLogResponse) + + const tracing = store.getState().workflowRunningData!.tracing! + expect(tracing[0].execution_metadata!.agent_log).toBeUndefined() + expect(tracing[1].execution_metadata!.agent_log).toHaveLength(1) + expect(tracing[1].execution_metadata!.agent_log![0].message_id).toBe('m2') + }) }) describe('useWorkflowNodeHumanInputFormFilled', () => { diff --git a/web/app/components/workflow/hooks/__tests__/use-workflow-run-event-with-viewport.spec.ts b/web/app/components/workflow/hooks/__tests__/use-workflow-run-event-with-viewport.spec.ts index 51d1ba5b74..1495c1b2b8 100644 --- a/web/app/components/workflow/hooks/__tests__/use-workflow-run-event-with-viewport.spec.ts +++ b/web/app/components/workflow/hooks/__tests__/use-workflow-run-event-with-viewport.spec.ts @@ -77,15 +77,15 @@ describe('useWorkflowNodeStarted', () => { initialStoreState: { workflowRunningData: baseRunningData({ tracing: [ - { node_id: 'n0', status: NodeRunningStatus.Succeeded }, - { node_id: 'n1', status: NodeRunningStatus.Succeeded }, + { id: 'trace-0', node_id: 'n0', status: NodeRunningStatus.Succeeded }, + { id: 'trace-1', node_id: 'n1', status: NodeRunningStatus.Succeeded }, ], }), }, }) result.current.handleWorkflowNodeStarted( - { data: { node_id: 'n1' } } as NodeStartedResponse, + { data: { id: 'trace-1', node_id: 'n1' } } as NodeStartedResponse, containerParams, ) @@ -93,6 +93,30 @@ describe('useWorkflowNodeStarted', () => { expect(tracing).toHaveLength(2) expect(tracing[1].status).toBe(NodeRunningStatus.Running) }) + + it('should append a new tracing entry when the same node starts a new execution id', () => { + const { result, store } = renderWorkflowHook(() => useWorkflowNodeStarted(), { + initialStoreState: { + workflowRunningData: baseRunningData({ + tracing: [ + { id: 'trace-0', node_id: 'n0', status: NodeRunningStatus.Succeeded }, + { id: 'trace-1', node_id: 'n1', status: NodeRunningStatus.Succeeded }, + ], + }), + }, + }) + + result.current.handleWorkflowNodeStarted( + { data: { id: 'trace-2', node_id: 'n1' } } as NodeStartedResponse, + containerParams, + ) + + const tracing = store.getState().workflowRunningData!.tracing! + expect(tracing).toHaveLength(3) + expect(tracing[2].id).toBe('trace-2') + expect(tracing[2].node_id).toBe('n1') + expect(tracing[2].status).toBe(NodeRunningStatus.Running) + }) }) describe('useWorkflowNodeIterationStarted', () => { diff --git a/web/app/components/workflow/hooks/use-workflow-run-event/use-workflow-agent-log.ts b/web/app/components/workflow/hooks/use-workflow-run-event/use-workflow-agent-log.ts index 08fb526000..31f82ffdc8 100644 --- a/web/app/components/workflow/hooks/use-workflow-run-event/use-workflow-agent-log.ts +++ b/web/app/components/workflow/hooks/use-workflow-run-event/use-workflow-agent-log.ts @@ -14,7 +14,12 @@ export const useWorkflowAgentLog = () => { } = workflowStore.getState() setWorkflowRunningData(produce(workflowRunningData!, (draft) => { - const currentIndex = draft.tracing!.findIndex(item => item.node_id === data.node_id) + const currentIndex = draft.tracing!.findIndex((item) => { + if (data.node_execution_id) + return item.id === data.node_execution_id + + return item.node_id === data.node_id + }) if (currentIndex > -1) { const current = draft.tracing![currentIndex] diff --git a/web/app/components/workflow/hooks/use-workflow-run-event/use-workflow-node-started.ts b/web/app/components/workflow/hooks/use-workflow-run-event/use-workflow-node-started.ts index 01f60e12e9..1528a99468 100644 --- a/web/app/components/workflow/hooks/use-workflow-run-event/use-workflow-node-started.ts +++ b/web/app/components/workflow/hooks/use-workflow-run-event/use-workflow-node-started.ts @@ -33,8 +33,8 @@ export const useWorkflowNodeStarted = () => { transform, } = store.getState() const nodes = getNodes() - const currentIndex = workflowRunningData?.tracing?.findIndex(item => item.node_id === data.node_id) - if (currentIndex && currentIndex > -1) { + const currentIndex = workflowRunningData?.tracing?.findIndex(item => item.id === data.id) + if (currentIndex !== undefined && currentIndex > -1) { setWorkflowRunningData(produce(workflowRunningData!, (draft) => { draft.tracing![currentIndex] = { ...data, diff --git a/web/app/components/workflow/panel/debug-and-preview/hooks.ts b/web/app/components/workflow/panel/debug-and-preview/hooks.ts index 3481733cd2..2807976bad 100644 --- a/web/app/components/workflow/panel/debug-and-preview/hooks.ts +++ b/web/app/components/workflow/panel/debug-and-preview/hooks.ts @@ -42,6 +42,7 @@ import { import { useHooksStore } from '../../hooks-store' import { useWorkflowStore } from '../../store' import { NodeRunningStatus, WorkflowRunningStatus } from '../../types' +import { upsertTopLevelTracingNodeOnStart } from '../../utils/top-level-tracing' type GetAbortController = (abortController: AbortController) => void type SendCallback = { @@ -486,19 +487,13 @@ export const useChat = ( } }, onNodeStarted: ({ data }) => { - const currentIndex = responseItem.workflowProcess!.tracing!.findIndex(item => item.node_id === data.node_id) - if (currentIndex > -1) { - responseItem.workflowProcess!.tracing![currentIndex] = { - ...data, - status: NodeRunningStatus.Running, - } - } - else { - responseItem.workflowProcess!.tracing!.push({ - ...data, - status: NodeRunningStatus.Running, - }) - } + if (params.loop_id) + return + + upsertTopLevelTracingNodeOnStart(responseItem.workflowProcess!.tracing!, { + ...data, + status: NodeRunningStatus.Running, + }) updateCurrentQAOnTree({ placeholderQuestionId, questionItem, @@ -517,6 +512,9 @@ export const useChat = ( }) }, onNodeFinished: ({ data }) => { + if (params.loop_id) + return + const currentTracingIndex = responseItem.workflowProcess!.tracing!.findIndex(item => item.id === data.id) if (currentTracingIndex > -1) { responseItem.workflowProcess!.tracing[currentTracingIndex] = { @@ -758,8 +756,7 @@ export const useChat = ( if (!responseItem.workflowProcess?.tracing) return const tracing = responseItem.workflowProcess.tracing - const iterationIndex = tracing.findIndex(item => item.node_id === iterationFinishedData.node_id - && (item.execution_metadata?.parallel_id === iterationFinishedData.execution_metadata?.parallel_id || item.parallel_id === iterationFinishedData.execution_metadata?.parallel_id))! + const iterationIndex = tracing.findIndex(item => item.id === iterationFinishedData.id)! if (iterationIndex > -1) { tracing[iterationIndex] = { ...tracing[iterationIndex], @@ -776,22 +773,10 @@ export const useChat = ( if (!responseItem.workflowProcess.tracing) responseItem.workflowProcess.tracing = [] - const currentIndex = responseItem.workflowProcess.tracing.findIndex(item => item.node_id === nodeStartedData.node_id) - if (currentIndex > -1) { - responseItem.workflowProcess.tracing[currentIndex] = { - ...nodeStartedData, - status: NodeRunningStatus.Running, - } - } - else { - if (nodeStartedData.iteration_id) - return - - responseItem.workflowProcess.tracing.push({ - ...nodeStartedData, - status: WorkflowRunningStatus.Running, - }) - } + upsertTopLevelTracingNodeOnStart(responseItem.workflowProcess.tracing, { + ...nodeStartedData, + status: WorkflowRunningStatus.Running, + }) }) }, onNodeFinished: ({ data: nodeFinishedData }) => { @@ -802,6 +787,9 @@ export const useChat = ( if (nodeFinishedData.iteration_id) return + if (nodeFinishedData.loop_id) + return + const currentIndex = responseItem.workflowProcess.tracing.findIndex((item) => { if (!item.execution_metadata?.parallel_id) return item.id === nodeFinishedData.id @@ -829,8 +817,7 @@ export const useChat = ( if (!responseItem.workflowProcess?.tracing) return const tracing = responseItem.workflowProcess.tracing - const loopIndex = tracing.findIndex(item => item.node_id === loopFinishedData.node_id - && (item.execution_metadata?.parallel_id === loopFinishedData.execution_metadata?.parallel_id || item.parallel_id === loopFinishedData.execution_metadata?.parallel_id))! + const loopIndex = tracing.findIndex(item => item.id === loopFinishedData.id)! if (loopIndex > -1) { tracing[loopIndex] = { ...tracing[loopIndex], diff --git a/web/app/components/workflow/utils/top-level-tracing.spec.ts b/web/app/components/workflow/utils/top-level-tracing.spec.ts new file mode 100644 index 0000000000..da01cc2a6c --- /dev/null +++ b/web/app/components/workflow/utils/top-level-tracing.spec.ts @@ -0,0 +1,133 @@ +import type { NodeTracing } from '@/types/workflow' +import { NodeRunningStatus } from '@/app/components/workflow/types' +import { upsertTopLevelTracingNodeOnStart } from './top-level-tracing' + +const createTrace = (overrides: Partial = {}): NodeTracing => ({ + id: 'trace-1', + index: 0, + predecessor_node_id: '', + node_id: 'node-1', + node_type: 'llm' as NodeTracing['node_type'], + title: 'Node 1', + inputs: {}, + inputs_truncated: false, + process_data: {}, + process_data_truncated: false, + outputs: {}, + outputs_truncated: false, + status: NodeRunningStatus.Succeeded, + elapsed_time: 0, + metadata: { + iterator_length: 0, + iterator_index: 0, + loop_length: 0, + loop_index: 0, + }, + created_at: 0, + created_by: { + id: 'user-1', + name: 'User', + email: 'user@example.com', + }, + finished_at: 0, + ...overrides, +}) + +describe('upsertTopLevelTracingNodeOnStart', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('should append a new top-level node when no matching trace exists', () => { + const tracing: NodeTracing[] = [] + const startedNode = createTrace({ + id: 'trace-2', + node_id: 'node-2', + status: NodeRunningStatus.Running, + }) + + const updated = upsertTopLevelTracingNodeOnStart(tracing, startedNode) + + expect(updated).toBe(true) + expect(tracing).toEqual([startedNode]) + }) + + it('should update an existing top-level node when the execution id matches', () => { + const tracing: NodeTracing[] = [ + createTrace({ + id: 'trace-1', + node_id: 'node-1', + status: NodeRunningStatus.Succeeded, + }), + ] + const startedNode = createTrace({ + id: 'trace-1', + node_id: 'node-1', + status: NodeRunningStatus.Running, + }) + + const updated = upsertTopLevelTracingNodeOnStart(tracing, startedNode) + + expect(updated).toBe(true) + expect(tracing).toEqual([startedNode]) + }) + + it('should append a new top-level node when the same node starts with a new execution id', () => { + const existingTrace = createTrace({ + id: 'trace-1', + node_id: 'node-1', + status: NodeRunningStatus.Succeeded, + }) + const tracing: NodeTracing[] = [existingTrace] + const startedNode = createTrace({ + id: 'trace-2', + node_id: 'node-1', + status: NodeRunningStatus.Running, + }) + + const updated = upsertTopLevelTracingNodeOnStart(tracing, startedNode) + + expect(updated).toBe(true) + expect(tracing).toEqual([existingTrace, startedNode]) + }) + + it('should ignore nested iteration node starts even when the node id matches a top-level trace', () => { + const existingTrace = createTrace({ + id: 'top-level-trace', + node_id: 'node-1', + status: NodeRunningStatus.Succeeded, + }) + const tracing: NodeTracing[] = [existingTrace] + const nestedIterationTrace = createTrace({ + id: 'iteration-trace', + node_id: 'node-1', + iteration_id: 'iteration-1', + status: NodeRunningStatus.Running, + }) + + const updated = upsertTopLevelTracingNodeOnStart(tracing, nestedIterationTrace) + + expect(updated).toBe(false) + expect(tracing).toEqual([existingTrace]) + }) + + it('should ignore nested loop node starts even when the node id matches a top-level trace', () => { + const existingTrace = createTrace({ + id: 'top-level-trace', + node_id: 'node-1', + status: NodeRunningStatus.Succeeded, + }) + const tracing: NodeTracing[] = [existingTrace] + const nestedLoopTrace = createTrace({ + id: 'loop-trace', + node_id: 'node-1', + loop_id: 'loop-1', + status: NodeRunningStatus.Running, + }) + + const updated = upsertTopLevelTracingNodeOnStart(tracing, nestedLoopTrace) + + expect(updated).toBe(false) + expect(tracing).toEqual([existingTrace]) + }) +}) diff --git a/web/app/components/workflow/utils/top-level-tracing.ts b/web/app/components/workflow/utils/top-level-tracing.ts new file mode 100644 index 0000000000..564812b105 --- /dev/null +++ b/web/app/components/workflow/utils/top-level-tracing.ts @@ -0,0 +1,23 @@ +import type { NodeTracing } from '@/types/workflow' + +const isNestedTracingNode = (trace: Pick) => { + return Boolean(trace.iteration_id || trace.loop_id) +} + +export const upsertTopLevelTracingNodeOnStart = ( + tracing: NodeTracing[], + startedNode: NodeTracing, +) => { + if (isNestedTracingNode(startedNode)) + return false + + const currentIndex = startedNode.id + ? tracing.findIndex(item => item.id === startedNode.id) + : tracing.findIndex(item => item.node_id === startedNode.node_id) + if (currentIndex > -1) + tracing[currentIndex] = startedNode + else + tracing.push(startedNode) + + return true +} From 4f73766a2112b84ccc0f3540bf3cee3d8f42ec84 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Tue, 10 Mar 2026 11:18:09 +0000 Subject: [PATCH 02/14] [autofix.ci] apply automated fixes --- web/eslint-suppressions.json | 5 ----- 1 file changed, 5 deletions(-) diff --git a/web/eslint-suppressions.json b/web/eslint-suppressions.json index dba3a08694..f7db746400 100644 --- a/web/eslint-suppressions.json +++ b/web/eslint-suppressions.json @@ -2033,11 +2033,6 @@ "count": 2 } }, - "app/components/base/chat/chat/__tests__/hooks.spec.tsx": { - "e18e/prefer-array-at": { - "count": 6 - } - }, "app/components/base/chat/chat/__tests__/index.spec.tsx": { "e18e/prefer-static-regex": { "count": 8 From 715f3affe559fe45d714b81580cec5e2e98080b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yanli=20=E7=9B=90=E7=B2=92?= Date: Tue, 10 Mar 2026 19:30:54 +0800 Subject: [PATCH 03/14] chore: address review feedback --- web/app/components/base/chat/chat/hooks.ts | 2 ++ web/app/components/share/text-generation/result/index.tsx | 4 ++-- web/app/components/workflow/utils/top-level-tracing.ts | 1 + 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/web/app/components/base/chat/chat/hooks.ts b/web/app/components/base/chat/chat/hooks.ts index e8ce744236..4cde06ed2b 100644 --- a/web/app/components/base/chat/chat/hooks.ts +++ b/web/app/components/base/chat/chat/hooks.ts @@ -957,6 +957,7 @@ export const useChat = ( }) }, onNodeStarted: ({ data: nodeStartedData }) => { + // `data` is the outer send payload for this request; loop child runs should not emit top-level node traces here. if (data.loop_id) return @@ -977,6 +978,7 @@ export const useChat = ( }) }, onNodeFinished: ({ data: nodeFinishedData }) => { + // Use the outer request payload here as well so loop child runs skip top-level finish handling entirely. if (data.loop_id) return diff --git a/web/app/components/share/text-generation/result/index.tsx b/web/app/components/share/text-generation/result/index.tsx index e3ffe9d209..a11e7436b5 100644 --- a/web/app/components/share/text-generation/result/index.tsx +++ b/web/app/components/share/text-generation/result/index.tsx @@ -337,7 +337,7 @@ const Result: FC = ({ onIterationFinish: ({ data }) => { setWorkflowProcessData(produce(getWorkflowProcessData()!, (draft) => { draft.expand = true - const iterationsIndex = draft.tracing.findIndex(item => item.id === data.id)! + const iterationsIndex = draft.tracing.findIndex(item => item.id === data.id) if (iterationsIndex > -1) { draft.tracing[iterationsIndex] = { ...data, @@ -367,7 +367,7 @@ const Result: FC = ({ onLoopFinish: ({ data }) => { setWorkflowProcessData(produce(getWorkflowProcessData()!, (draft) => { draft.expand = true - const loopsIndex = draft.tracing.findIndex(item => item.id === data.id)! + const loopsIndex = draft.tracing.findIndex(item => item.id === data.id) if (loopsIndex > -1) { draft.tracing[loopsIndex] = { ...data, diff --git a/web/app/components/workflow/utils/top-level-tracing.ts b/web/app/components/workflow/utils/top-level-tracing.ts index 564812b105..30e0b5c8f6 100644 --- a/web/app/components/workflow/utils/top-level-tracing.ts +++ b/web/app/components/workflow/utils/top-level-tracing.ts @@ -15,6 +15,7 @@ export const upsertTopLevelTracingNodeOnStart = ( ? tracing.findIndex(item => item.id === startedNode.id) : tracing.findIndex(item => item.node_id === startedNode.node_id) if (currentIndex > -1) + // Started events are the authoritative snapshot for an execution; merging would retain stale client-side fields. tracing[currentIndex] = startedNode else tracing.push(startedNode) From e6f00a2bf9fafe127091c333dc4b027506004a9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=9B=90=E7=B2=92=20Yanli?= Date: Tue, 10 Mar 2026 20:13:49 +0800 Subject: [PATCH 04/14] Update web/app/components/workflow/utils/top-level-tracing.ts Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- web/app/components/workflow/utils/top-level-tracing.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/web/app/components/workflow/utils/top-level-tracing.ts b/web/app/components/workflow/utils/top-level-tracing.ts index 30e0b5c8f6..8ddeb2a957 100644 --- a/web/app/components/workflow/utils/top-level-tracing.ts +++ b/web/app/components/workflow/utils/top-level-tracing.ts @@ -11,9 +11,7 @@ export const upsertTopLevelTracingNodeOnStart = ( if (isNestedTracingNode(startedNode)) return false - const currentIndex = startedNode.id - ? tracing.findIndex(item => item.id === startedNode.id) - : tracing.findIndex(item => item.node_id === startedNode.node_id) + const currentIndex = tracing.findIndex(item => item.id === startedNode.id) if (currentIndex > -1) // Started events are the authoritative snapshot for an execution; merging would retain stale client-side fields. tracing[currentIndex] = startedNode From e76fbcb045b134ed41275f8c1d01048096a599a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yanli=20=E7=9B=90=E7=B2=92?= Date: Tue, 10 Mar 2026 20:33:46 +0800 Subject: [PATCH 05/14] fix: guard loop child node starts --- web/app/components/base/chat/chat/hooks.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/web/app/components/base/chat/chat/hooks.ts b/web/app/components/base/chat/chat/hooks.ts index 4cde06ed2b..7e780079c5 100644 --- a/web/app/components/base/chat/chat/hooks.ts +++ b/web/app/components/base/chat/chat/hooks.ts @@ -408,6 +408,9 @@ export const useChat = ( }, onNodeStarted: ({ data: nodeStartedData }) => { updateChatTreeNode(messageId, (responseItem) => { + if (params.loop_id) + return + if (!responseItem.workflowProcess) return if (!responseItem.workflowProcess.tracing) @@ -550,7 +553,7 @@ export const useChat = ( {}, otherOptions, ) - }, [updateChatTreeNode, handleResponding, createAudioPlayerManager, config?.suggested_questions_after_answer]) + }, [updateChatTreeNode, handleResponding, createAudioPlayerManager, config?.suggested_questions_after_answer, params.loop_id]) const updateCurrentQAOnTree = useCallback(({ parentId, From 344f6be7cd7564b93f7280d720562107cdd565e6 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Fri, 13 Mar 2026 10:10:00 +0000 Subject: [PATCH 06/14] [autofix.ci] apply automated fixes --- web/eslint-suppressions.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/eslint-suppressions.json b/web/eslint-suppressions.json index 6d7b319621..abe626a576 100644 --- a/web/eslint-suppressions.json +++ b/web/eslint-suppressions.json @@ -9772,4 +9772,4 @@ "count": 2 } } -} +} \ No newline at end of file From 37df3899ff176a361b2d22512c8a7017b223f397 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yanli=20=E7=9B=90=E7=B2=92?= Date: Tue, 17 Mar 2026 18:44:42 +0800 Subject: [PATCH 07/14] fix: stabilize web test shard failures --- web/app/components/base/chat/chat/hooks.ts | 42 +++++++++++++++++-- .../jina-reader/__tests__/base.spec.tsx | 8 ++-- .../__tests__/use-dataset-card-state.spec.ts | 4 +- 3 files changed, 44 insertions(+), 10 deletions(-) diff --git a/web/app/components/base/chat/chat/hooks.ts b/web/app/components/base/chat/chat/hooks.ts index 7e780079c5..51e6ad7560 100644 --- a/web/app/components/base/chat/chat/hooks.ts +++ b/web/app/components/base/chat/chat/hooks.ts @@ -12,6 +12,7 @@ import type { IOnDataMoreInfo, IOtherOptions, } from '@/service/base' +import type { NodeTracing } from '@/types/workflow' import { uniqBy } from 'es-toolkit/compat' import { noop } from 'es-toolkit/function' import { produce, setAutoFreeze } from 'immer' @@ -53,6 +54,39 @@ type SendCallback = { isPublicAPI?: boolean } +type ParallelTraceLike = Pick + +const findParallelTraceIndex = ( + tracing: ParallelTraceLike[], + data: Partial, +) => { + const incomingParallelId = data.execution_metadata?.parallel_id ?? data.parallel_id + + if (data.id) { + const matchedByIdIndex = tracing.findIndex((item) => { + if (item.id !== data.id) + return false + + const existingParallelId = item.execution_metadata?.parallel_id ?? item.parallel_id + if (!existingParallelId || !incomingParallelId) + return true + + return existingParallelId === incomingParallelId + }) + + if (matchedByIdIndex > -1) + return matchedByIdIndex + } + + return tracing.findIndex((item) => { + if (item.node_id !== data.node_id) + return false + + const existingParallelId = item.execution_metadata?.parallel_id ?? item.parallel_id + return existingParallelId === incomingParallelId + }) +} + export const useChat = ( config?: ChatConfig, formSettings?: { @@ -396,7 +430,7 @@ export const useChat = ( if (!responseItem.workflowProcess?.tracing) return const tracing = responseItem.workflowProcess.tracing - const iterationIndex = tracing.findIndex(item => item.id === iterationFinishedData.id)! + const iterationIndex = findParallelTraceIndex(tracing, iterationFinishedData) if (iterationIndex > -1) { tracing[iterationIndex] = { ...tracing[iterationIndex], @@ -477,7 +511,7 @@ export const useChat = ( if (!responseItem.workflowProcess?.tracing) return const tracing = responseItem.workflowProcess.tracing - const loopIndex = tracing.findIndex(item => item.id === loopFinishedData.id)! + const loopIndex = findParallelTraceIndex(tracing, loopFinishedData) if (loopIndex > -1) { tracing[loopIndex] = { ...tracing[loopIndex], @@ -943,7 +977,7 @@ export const useChat = ( }, onIterationFinish: ({ data: iterationFinishedData }) => { const tracing = responseItem.workflowProcess!.tracing! - const iterationIndex = tracing.findIndex(item => item.id === iterationFinishedData.id)! + const iterationIndex = findParallelTraceIndex(tracing, iterationFinishedData) if (iterationIndex > -1) { tracing[iterationIndex] = { ...tracing[iterationIndex], @@ -1034,7 +1068,7 @@ export const useChat = ( }, onLoopFinish: ({ data: loopFinishedData }) => { const tracing = responseItem.workflowProcess!.tracing! - const loopIndex = tracing.findIndex(item => item.id === loopFinishedData.id)! + const loopIndex = findParallelTraceIndex(tracing, loopFinishedData) if (loopIndex > -1) { tracing[loopIndex] = { ...tracing[loopIndex], diff --git a/web/app/components/datasets/create/website/jina-reader/__tests__/base.spec.tsx b/web/app/components/datasets/create/website/jina-reader/__tests__/base.spec.tsx index bcfcf39060..953604e5c6 100644 --- a/web/app/components/datasets/create/website/jina-reader/__tests__/base.spec.tsx +++ b/web/app/components/datasets/create/website/jina-reader/__tests__/base.spec.tsx @@ -264,7 +264,7 @@ describe('UrlInput', () => { render() const input = screen.getByRole('textbox') - await userEvent.type(input, longUrl) + fireEvent.change(input, { target: { value: longUrl } }) expect(input).toHaveValue(longUrl) }) @@ -275,7 +275,7 @@ describe('UrlInput', () => { render() const input = screen.getByRole('textbox') - await userEvent.type(input, unicodeUrl) + fireEvent.change(input, { target: { value: unicodeUrl } }) expect(input).toHaveValue(unicodeUrl) }) @@ -285,7 +285,7 @@ describe('UrlInput', () => { render() const input = screen.getByRole('textbox') - await userEvent.type(input, 'https://rapid.com', { delay: 1 }) + fireEvent.change(input, { target: { value: 'https://rapid.com' } }) expect(input).toHaveValue('https://rapid.com') }) @@ -297,7 +297,7 @@ describe('UrlInput', () => { render() const input = screen.getByRole('textbox') - await userEvent.type(input, 'https://enter.com') + fireEvent.change(input, { target: { value: 'https://enter.com' } }) // Focus button and press enter const button = screen.getByRole('button', { name: /run/i }) diff --git a/web/app/components/datasets/list/dataset-card/hooks/__tests__/use-dataset-card-state.spec.ts b/web/app/components/datasets/list/dataset-card/hooks/__tests__/use-dataset-card-state.spec.ts index 63ac30630e..2af16ee5f9 100644 --- a/web/app/components/datasets/list/dataset-card/hooks/__tests__/use-dataset-card-state.spec.ts +++ b/web/app/components/datasets/list/dataset-card/hooks/__tests__/use-dataset-card-state.spec.ts @@ -151,7 +151,7 @@ describe('useDatasetCardState', () => { expect(result.current.modalState.showRenameModal).toBe(false) }) - it('should close confirm delete modal when closeConfirmDelete is called', () => { + it('should close confirm delete modal when closeConfirmDelete is called', async () => { const dataset = createMockDataset() const { result } = renderHook(() => useDatasetCardState({ dataset, onSuccess: vi.fn() }), @@ -162,7 +162,7 @@ describe('useDatasetCardState', () => { result.current.detectIsUsedByApp() }) - waitFor(() => { + await waitFor(() => { expect(result.current.modalState.showConfirmDelete).toBe(true) }) From 64308c3d0debd86606f55fafb9e83c94078157a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yanli=20=E7=9B=90=E7=B2=92?= Date: Tue, 17 Mar 2026 19:18:39 +0800 Subject: [PATCH 08/14] fix: address workflow tracing review feedback --- web/app/components/base/chat/chat/hooks.ts | 4 +-- .../workflow-stream-handlers.spec.ts | 26 ++++++++++++++++ .../result/workflow-stream-handlers.ts | 7 ++--- .../use-workflow-run-event-store-only.spec.ts | 30 +++++++++++++++---- ...e-workflow-run-event-with-viewport.spec.ts | 4 +-- .../use-workflow-agent-log.ts | 10 +++---- .../workflow/panel/debug-and-preview/hooks.ts | 7 +++-- 7 files changed, 65 insertions(+), 23 deletions(-) diff --git a/web/app/components/base/chat/chat/hooks.ts b/web/app/components/base/chat/chat/hooks.ts index 51e6ad7560..1da6b58bac 100644 --- a/web/app/components/base/chat/chat/hooks.ts +++ b/web/app/components/base/chat/chat/hooks.ts @@ -452,7 +452,7 @@ export const useChat = ( upsertTopLevelTracingNodeOnStart(responseItem.workflowProcess.tracing, { ...nodeStartedData, - status: WorkflowRunningStatus.Running, + status: NodeRunningStatus.Running, }) }) }, @@ -1005,7 +1005,7 @@ export const useChat = ( upsertTopLevelTracingNodeOnStart(responseItem.workflowProcess.tracing, { ...nodeStartedData, - status: WorkflowRunningStatus.Running, + status: NodeRunningStatus.Running, }) updateCurrentQAOnTree({ placeholderQuestionId, diff --git a/web/app/components/share/text-generation/result/__tests__/workflow-stream-handlers.spec.ts b/web/app/components/share/text-generation/result/__tests__/workflow-stream-handlers.spec.ts index 45d5ded302..4b61a8ffd9 100644 --- a/web/app/components/share/text-generation/result/__tests__/workflow-stream-handlers.spec.ts +++ b/web/app/components/share/text-generation/result/__tests__/workflow-stream-handlers.spec.ts @@ -101,6 +101,7 @@ const createHumanInput = (overrides: Partial = {}): HumanInp describe('workflow-stream-handlers helpers', () => { it('should update tracing, result text, and human input state', () => { const parallelTrace = createTrace({ + id: 'parallel-trace-1', node_id: 'parallel-node', execution_metadata: { parallel_id: 'parallel-1' }, details: [[]], @@ -109,11 +110,13 @@ describe('workflow-stream-handlers helpers', () => { let workflowProcessData = appendParallelStart(undefined, parallelTrace) workflowProcessData = appendParallelNext(workflowProcessData, parallelTrace) workflowProcessData = finishParallelTrace(workflowProcessData, createTrace({ + id: 'parallel-trace-1', node_id: 'parallel-node', execution_metadata: { parallel_id: 'parallel-1' }, error: 'failed', })) workflowProcessData = upsertWorkflowNode(workflowProcessData, createTrace({ + id: 'node-trace-1', node_id: 'node-1', execution_metadata: { parallel_id: 'parallel-2' }, }))! @@ -160,6 +163,29 @@ describe('workflow-stream-handlers helpers', () => { expect(nextProcess.tracing[0]?.details).toEqual([[], []]) }) + it('should append a new top-level trace when the same node starts with a different execution id', () => { + const process = createWorkflowProcess() + process.tracing = [ + createTrace({ + id: 'trace-1', + node_id: 'node-1', + status: NodeRunningStatus.Succeeded, + }), + ] + + const updatedProcess = upsertWorkflowNode(process, createTrace({ + id: 'trace-2', + node_id: 'node-1', + }))! + + expect(updatedProcess.tracing).toHaveLength(2) + expect(updatedProcess.tracing[1]).toEqual(expect.objectContaining({ + id: 'trace-2', + node_id: 'node-1', + status: NodeRunningStatus.Running, + })) + }) + it('should leave tracing unchanged when a parallel next event has no matching trace', () => { const process = createWorkflowProcess() process.tracing = [ diff --git a/web/app/components/share/text-generation/result/workflow-stream-handlers.ts b/web/app/components/share/text-generation/result/workflow-stream-handlers.ts index 48b132587e..7b50faba27 100644 --- a/web/app/components/share/text-generation/result/workflow-stream-handlers.ts +++ b/web/app/components/share/text-generation/result/workflow-stream-handlers.ts @@ -5,6 +5,7 @@ import type { HumanInputFormTimeoutData, NodeTracing, WorkflowFinishedResponse } import { produce } from 'immer' import { getFilesInLogs } from '@/app/components/base/file-uploader/utils' import { NodeRunningStatus, WorkflowRunningStatus } from '@/app/components/workflow/types' +import { upsertTopLevelTracingNodeOnStart } from '@/app/components/workflow/utils/top-level-tracing' import { sseGet } from '@/service/base' type Notify = (payload: { type: 'error' | 'warning', message: string }) => void @@ -96,17 +97,13 @@ const upsertWorkflowNode = (current: WorkflowProcess | undefined, data: NodeTrac return updateWorkflowProcess(current, (draft) => { draft.expand = true - const currentIndex = draft.tracing.findIndex(item => item.node_id === data.node_id) const nextTrace = { ...data, status: NodeRunningStatus.Running, expand: true, } - if (currentIndex > -1) - draft.tracing[currentIndex] = nextTrace - else - draft.tracing.push(nextTrace) + upsertTopLevelTracingNodeOnStart(draft.tracing, nextTrace) }) } diff --git a/web/app/components/workflow/hooks/__tests__/use-workflow-run-event-store-only.spec.ts b/web/app/components/workflow/hooks/__tests__/use-workflow-run-event-store-only.spec.ts index 6a3347f77a..57f7298e68 100644 --- a/web/app/components/workflow/hooks/__tests__/use-workflow-run-event-store-only.spec.ts +++ b/web/app/components/workflow/hooks/__tests__/use-workflow-run-event-store-only.spec.ts @@ -109,13 +109,13 @@ describe('useWorkflowAgentLog', () => { const { result, store } = renderWorkflowHook(() => useWorkflowAgentLog(), { initialStoreState: { workflowRunningData: baseRunningData({ - tracing: [{ node_id: 'n1', execution_metadata: {} }], + tracing: [{ id: 'trace-1', node_id: 'n1', execution_metadata: {} }], }), }, }) result.current.handleWorkflowAgentLog({ - data: { node_id: 'n1', message_id: 'm1' }, + data: { node_id: 'n1', node_execution_id: 'trace-1', message_id: 'm1' }, } as AgentLogResponse) const trace = store.getState().workflowRunningData!.tracing![0] @@ -128,6 +128,7 @@ describe('useWorkflowAgentLog', () => { initialStoreState: { workflowRunningData: baseRunningData({ tracing: [{ + id: 'trace-1', node_id: 'n1', execution_metadata: { agent_log: [{ message_id: 'm1', text: 'log1' }] }, }], @@ -136,7 +137,7 @@ describe('useWorkflowAgentLog', () => { }) result.current.handleWorkflowAgentLog({ - data: { node_id: 'n1', message_id: 'm2' }, + data: { node_id: 'n1', node_execution_id: 'trace-1', message_id: 'm2' }, } as AgentLogResponse) expect(store.getState().workflowRunningData!.tracing![0].execution_metadata!.agent_log).toHaveLength(2) @@ -147,6 +148,7 @@ describe('useWorkflowAgentLog', () => { initialStoreState: { workflowRunningData: baseRunningData({ tracing: [{ + id: 'trace-1', node_id: 'n1', execution_metadata: { agent_log: [{ message_id: 'm1', text: 'old' }] }, }], @@ -155,7 +157,7 @@ describe('useWorkflowAgentLog', () => { }) result.current.handleWorkflowAgentLog({ - data: { node_id: 'n1', message_id: 'm1', text: 'new' }, + data: { node_id: 'n1', node_execution_id: 'trace-1', message_id: 'm1', text: 'new' }, } as unknown as AgentLogResponse) const log = store.getState().workflowRunningData!.tracing![0].execution_metadata!.agent_log! @@ -167,13 +169,13 @@ describe('useWorkflowAgentLog', () => { const { result, store } = renderWorkflowHook(() => useWorkflowAgentLog(), { initialStoreState: { workflowRunningData: baseRunningData({ - tracing: [{ node_id: 'n1' }], + tracing: [{ id: 'trace-1', node_id: 'n1' }], }), }, }) result.current.handleWorkflowAgentLog({ - data: { node_id: 'n1', message_id: 'm1' }, + data: { node_id: 'n1', node_execution_id: 'trace-1', message_id: 'm1' }, } as AgentLogResponse) expect(store.getState().workflowRunningData!.tracing![0].execution_metadata!.agent_log).toHaveLength(1) @@ -200,6 +202,22 @@ describe('useWorkflowAgentLog', () => { expect(tracing[1].execution_metadata!.agent_log).toHaveLength(1) expect(tracing[1].execution_metadata!.agent_log![0].message_id).toBe('m2') }) + + it('should ignore agent logs when node_execution_id is missing', () => { + const { result, store } = renderWorkflowHook(() => useWorkflowAgentLog(), { + initialStoreState: { + workflowRunningData: baseRunningData({ + tracing: [{ id: 'trace-1', node_id: 'n1', execution_metadata: {} }], + }), + }, + }) + + result.current.handleWorkflowAgentLog({ + data: { node_id: 'n1', message_id: 'm1' }, + } as AgentLogResponse) + + expect(store.getState().workflowRunningData!.tracing![0].execution_metadata!.agent_log).toBeUndefined() + }) }) describe('useWorkflowNodeHumanInputFormFilled', () => { diff --git a/web/app/components/workflow/hooks/__tests__/use-workflow-run-event-with-viewport.spec.ts b/web/app/components/workflow/hooks/__tests__/use-workflow-run-event-with-viewport.spec.ts index 1495c1b2b8..7074aba148 100644 --- a/web/app/components/workflow/hooks/__tests__/use-workflow-run-event-with-viewport.spec.ts +++ b/web/app/components/workflow/hooks/__tests__/use-workflow-run-event-with-viewport.spec.ts @@ -41,7 +41,7 @@ describe('useWorkflowNodeStarted', () => { }) result.current.handleWorkflowNodeStarted( - { data: { node_id: 'n1' } } as NodeStartedResponse, + { data: { id: 'trace-n1', node_id: 'n1' } } as NodeStartedResponse, containerParams, ) @@ -65,7 +65,7 @@ describe('useWorkflowNodeStarted', () => { }) result.current.handleWorkflowNodeStarted( - { data: { node_id: 'n2' } } as NodeStartedResponse, + { data: { id: 'trace-n2', node_id: 'n2' } } as NodeStartedResponse, containerParams, ) diff --git a/web/app/components/workflow/hooks/use-workflow-run-event/use-workflow-agent-log.ts b/web/app/components/workflow/hooks/use-workflow-run-event/use-workflow-agent-log.ts index 31f82ffdc8..0f88572a0b 100644 --- a/web/app/components/workflow/hooks/use-workflow-run-event/use-workflow-agent-log.ts +++ b/web/app/components/workflow/hooks/use-workflow-run-event/use-workflow-agent-log.ts @@ -13,13 +13,11 @@ export const useWorkflowAgentLog = () => { setWorkflowRunningData, } = workflowStore.getState() - setWorkflowRunningData(produce(workflowRunningData!, (draft) => { - const currentIndex = draft.tracing!.findIndex((item) => { - if (data.node_execution_id) - return item.id === data.node_execution_id + if (!data.node_execution_id) + return - return item.node_id === data.node_id - }) + setWorkflowRunningData(produce(workflowRunningData!, (draft) => { + const currentIndex = draft.tracing!.findIndex(item => item.id === data.node_execution_id) if (currentIndex > -1) { const current = draft.tracing![currentIndex] diff --git a/web/app/components/workflow/panel/debug-and-preview/hooks.ts b/web/app/components/workflow/panel/debug-and-preview/hooks.ts index 2807976bad..f7c182867f 100644 --- a/web/app/components/workflow/panel/debug-and-preview/hooks.ts +++ b/web/app/components/workflow/panel/debug-and-preview/hooks.ts @@ -530,7 +530,10 @@ export const useChat = ( } }, onAgentLog: ({ data }) => { - const currentNodeIndex = responseItem.workflowProcess!.tracing!.findIndex(item => item.node_id === data.node_id) + if (!data.node_execution_id) + return + + const currentNodeIndex = responseItem.workflowProcess!.tracing!.findIndex(item => item.id === data.node_execution_id) if (currentNodeIndex > -1) { const current = responseItem.workflowProcess!.tracing![currentNodeIndex] @@ -775,7 +778,7 @@ export const useChat = ( upsertTopLevelTracingNodeOnStart(responseItem.workflowProcess.tracing, { ...nodeStartedData, - status: WorkflowRunningStatus.Running, + status: NodeRunningStatus.Running, }) }) }, From 5e22818296b8c7958713edb8507c789739a7a019 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yanli=20=E7=9B=90=E7=B2=92?= Date: Tue, 17 Mar 2026 19:33:49 +0800 Subject: [PATCH 09/14] fix: match repeated workflow node finishes by execution id --- .../workflow-stream-handlers.spec.ts | 33 +++++++++++++++++++ .../result/workflow-stream-handlers.ts | 11 ++++++- 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/web/app/components/share/text-generation/result/__tests__/workflow-stream-handlers.spec.ts b/web/app/components/share/text-generation/result/__tests__/workflow-stream-handlers.spec.ts index 4b61a8ffd9..703d43cf3f 100644 --- a/web/app/components/share/text-generation/result/__tests__/workflow-stream-handlers.spec.ts +++ b/web/app/components/share/text-generation/result/__tests__/workflow-stream-handlers.spec.ts @@ -186,6 +186,38 @@ describe('workflow-stream-handlers helpers', () => { })) }) + it('should finish the matching top-level trace when the same node runs again with a new execution id', () => { + const process = createWorkflowProcess() + process.tracing = [ + createTrace({ + id: 'trace-1', + node_id: 'node-1', + status: NodeRunningStatus.Succeeded, + }), + createTrace({ + id: 'trace-2', + node_id: 'node-1', + status: NodeRunningStatus.Running, + }), + ] + + const updatedProcess = finishWorkflowNode(process, createTrace({ + id: 'trace-2', + node_id: 'node-1', + status: NodeRunningStatus.Succeeded, + }))! + + expect(updatedProcess.tracing).toHaveLength(2) + expect(updatedProcess.tracing[0]).toEqual(expect.objectContaining({ + id: 'trace-1', + status: NodeRunningStatus.Succeeded, + })) + expect(updatedProcess.tracing[1]).toEqual(expect.objectContaining({ + id: 'trace-2', + status: NodeRunningStatus.Succeeded, + })) + }) + it('should leave tracing unchanged when a parallel next event has no matching trace', () => { const process = createWorkflowProcess() process.tracing = [ @@ -269,6 +301,7 @@ describe('workflow-stream-handlers helpers', () => { loop_id: 'loop-1', })) const unmatchedFinish = finishWorkflowNode(process, createTrace({ + id: 'trace-missing', node_id: 'missing', execution_metadata: { parallel_id: 'missing', diff --git a/web/app/components/share/text-generation/result/workflow-stream-handlers.ts b/web/app/components/share/text-generation/result/workflow-stream-handlers.ts index 7b50faba27..a89cef962a 100644 --- a/web/app/components/share/text-generation/result/workflow-stream-handlers.ts +++ b/web/app/components/share/text-generation/result/workflow-stream-handlers.ts @@ -107,12 +107,21 @@ const upsertWorkflowNode = (current: WorkflowProcess | undefined, data: NodeTrac }) } +const findWorkflowNodeTraceIndex = (tracing: WorkflowProcess['tracing'], data: NodeTracing) => { + return tracing.findIndex((trace) => { + if (trace.id && data.id) + return trace.id === data.id + + return matchParallelTrace(trace, data) + }) +} + const finishWorkflowNode = (current: WorkflowProcess | undefined, data: NodeTracing) => { if (data.iteration_id || data.loop_id) return current return updateWorkflowProcess(current, (draft) => { - const currentIndex = draft.tracing.findIndex(trace => matchParallelTrace(trace, data)) + const currentIndex = findWorkflowNodeTraceIndex(draft.tracing, data) if (currentIndex > -1) { draft.tracing[currentIndex] = { ...(draft.tracing[currentIndex].extras From f81e0c7c8dba77007b69c992142ee633ae943703 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yanli=20=E7=9B=90=E7=B2=92?= Date: Wed, 18 Mar 2026 18:18:36 +0800 Subject: [PATCH 10/14] fix: align agent log guards with workflow types --- .../use-workflow-run-event-store-only.spec.ts | 16 ---------------- .../use-workflow-agent-log.ts | 3 --- .../workflow/panel/debug-and-preview/hooks.ts | 3 --- 3 files changed, 22 deletions(-) diff --git a/web/app/components/workflow/hooks/__tests__/use-workflow-run-event-store-only.spec.ts b/web/app/components/workflow/hooks/__tests__/use-workflow-run-event-store-only.spec.ts index 57f7298e68..8574047e70 100644 --- a/web/app/components/workflow/hooks/__tests__/use-workflow-run-event-store-only.spec.ts +++ b/web/app/components/workflow/hooks/__tests__/use-workflow-run-event-store-only.spec.ts @@ -202,22 +202,6 @@ describe('useWorkflowAgentLog', () => { expect(tracing[1].execution_metadata!.agent_log).toHaveLength(1) expect(tracing[1].execution_metadata!.agent_log![0].message_id).toBe('m2') }) - - it('should ignore agent logs when node_execution_id is missing', () => { - const { result, store } = renderWorkflowHook(() => useWorkflowAgentLog(), { - initialStoreState: { - workflowRunningData: baseRunningData({ - tracing: [{ id: 'trace-1', node_id: 'n1', execution_metadata: {} }], - }), - }, - }) - - result.current.handleWorkflowAgentLog({ - data: { node_id: 'n1', message_id: 'm1' }, - } as AgentLogResponse) - - expect(store.getState().workflowRunningData!.tracing![0].execution_metadata!.agent_log).toBeUndefined() - }) }) describe('useWorkflowNodeHumanInputFormFilled', () => { diff --git a/web/app/components/workflow/hooks/use-workflow-run-event/use-workflow-agent-log.ts b/web/app/components/workflow/hooks/use-workflow-run-event/use-workflow-agent-log.ts index 0f88572a0b..a1a94df361 100644 --- a/web/app/components/workflow/hooks/use-workflow-run-event/use-workflow-agent-log.ts +++ b/web/app/components/workflow/hooks/use-workflow-run-event/use-workflow-agent-log.ts @@ -13,9 +13,6 @@ export const useWorkflowAgentLog = () => { setWorkflowRunningData, } = workflowStore.getState() - if (!data.node_execution_id) - return - setWorkflowRunningData(produce(workflowRunningData!, (draft) => { const currentIndex = draft.tracing!.findIndex(item => item.id === data.node_execution_id) if (currentIndex > -1) { diff --git a/web/app/components/workflow/panel/debug-and-preview/hooks.ts b/web/app/components/workflow/panel/debug-and-preview/hooks.ts index f7c182867f..04fd058056 100644 --- a/web/app/components/workflow/panel/debug-and-preview/hooks.ts +++ b/web/app/components/workflow/panel/debug-and-preview/hooks.ts @@ -530,9 +530,6 @@ export const useChat = ( } }, onAgentLog: ({ data }) => { - if (!data.node_execution_id) - return - const currentNodeIndex = responseItem.workflowProcess!.tracing!.findIndex(item => item.id === data.node_execution_id) if (currentNodeIndex > -1) { const current = responseItem.workflowProcess!.tracing![currentNodeIndex] From 84d1b0550183c05c23b7c94816f10d344ea5b76d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yanli=20=E7=9B=90=E7=B2=92?= Date: Wed, 18 Mar 2026 19:26:24 +0800 Subject: [PATCH 11/14] fix: preserve repeated shared workflow traces --- .../workflow-stream-handlers.spec.ts | 66 +++++++++++++++++++ .../result/workflow-stream-handlers.ts | 14 +++- 2 files changed, 78 insertions(+), 2 deletions(-) diff --git a/web/app/components/share/text-generation/result/__tests__/workflow-stream-handlers.spec.ts b/web/app/components/share/text-generation/result/__tests__/workflow-stream-handlers.spec.ts index 703d43cf3f..8785e77d1e 100644 --- a/web/app/components/share/text-generation/result/__tests__/workflow-stream-handlers.spec.ts +++ b/web/app/components/share/text-generation/result/__tests__/workflow-stream-handlers.spec.ts @@ -163,6 +163,72 @@ describe('workflow-stream-handlers helpers', () => { expect(nextProcess.tracing[0]?.details).toEqual([[], []]) }) + it('should keep separate iteration and loop traces for repeated executions with different ids', () => { + const process = createWorkflowProcess() + process.tracing = [ + createTrace({ + id: 'iter-trace-1', + node_id: 'iter-1', + details: [[]], + }), + createTrace({ + id: 'iter-trace-2', + node_id: 'iter-1', + details: [[]], + }), + createTrace({ + id: 'loop-trace-1', + node_id: 'loop-1', + details: [[]], + }), + createTrace({ + id: 'loop-trace-2', + node_id: 'loop-1', + details: [[]], + }), + ] + + const iterNextProcess = appendParallelNext(process, createTrace({ + id: 'iter-trace-2', + node_id: 'iter-1', + })) + const iterFinishedProcess = finishParallelTrace(iterNextProcess, createTrace({ + id: 'iter-trace-2', + node_id: 'iter-1', + status: NodeRunningStatus.Succeeded, + })) + const loopNextProcess = appendParallelNext(iterFinishedProcess, createTrace({ + id: 'loop-trace-2', + node_id: 'loop-1', + })) + const loopFinishedProcess = finishParallelTrace(loopNextProcess, createTrace({ + id: 'loop-trace-2', + node_id: 'loop-1', + status: NodeRunningStatus.Succeeded, + })) + + expect(loopFinishedProcess.tracing[0]).toEqual(expect.objectContaining({ + id: 'iter-trace-1', + details: [[]], + status: NodeRunningStatus.Running, + })) + expect(loopFinishedProcess.tracing[1]).toEqual(expect.objectContaining({ + id: 'iter-trace-2', + details: [[], []], + status: NodeRunningStatus.Succeeded, + })) + expect(loopFinishedProcess.tracing[2]).toEqual(expect.objectContaining({ + id: 'loop-trace-1', + details: [[]], + status: NodeRunningStatus.Running, + })) + expect(loopFinishedProcess.tracing[3]).toEqual(expect.objectContaining({ + id: 'loop-trace-2', + details: [[], []], + status: NodeRunningStatus.Succeeded, + })) + }) + it('should append a new top-level trace when the same node starts with a different execution id', () => { const process = createWorkflowProcess() process.tracing = [ diff --git a/web/app/components/share/text-generation/result/workflow-stream-handlers.ts b/web/app/components/share/text-generation/result/workflow-stream-handlers.ts index a89cef962a..d4ea59d51e 100644 --- a/web/app/components/share/text-generation/result/workflow-stream-handlers.ts +++ b/web/app/components/share/text-generation/result/workflow-stream-handlers.ts @@ -50,6 +50,15 @@ const matchParallelTrace = (trace: WorkflowProcess['tracing'][number], data: Nod || trace.parallel_id === data.execution_metadata?.parallel_id) } +const findParallelTraceIndex = (tracing: WorkflowProcess['tracing'], data: NodeTracing) => { + return tracing.findIndex((trace) => { + if (trace.id && data.id) + return trace.id === data.id + + return matchParallelTrace(trace, data) + }) +} + const ensureParallelTraceDetails = (details?: NodeTracing['details']) => { return details?.length ? details : [[]] } @@ -69,7 +78,8 @@ const appendParallelStart = (current: WorkflowProcess | undefined, data: NodeTra const appendParallelNext = (current: WorkflowProcess | undefined, data: NodeTracing) => { return updateWorkflowProcess(current, (draft) => { draft.expand = true - const trace = draft.tracing.find(item => matchParallelTrace(item, data)) + const traceIndex = findParallelTraceIndex(draft.tracing, data) + const trace = draft.tracing[traceIndex] if (!trace) return @@ -81,7 +91,7 @@ const appendParallelNext = (current: WorkflowProcess | undefined, data: NodeTrac const finishParallelTrace = (current: WorkflowProcess | undefined, data: NodeTracing) => { return updateWorkflowProcess(current, (draft) => { draft.expand = true - const traceIndex = draft.tracing.findIndex(item => matchParallelTrace(item, data)) + const traceIndex = findParallelTraceIndex(draft.tracing, data) if (traceIndex > -1) { draft.tracing[traceIndex] = { ...data, From cd9306d4f9d77fe3dce881cc48336344bba783ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yanli=20=E7=9B=90=E7=B2=92?= Date: Wed, 18 Mar 2026 19:36:03 +0800 Subject: [PATCH 12/14] fix: preserve repeated shared trace details --- .../result/__tests__/workflow-stream-handlers.spec.ts | 2 ++ .../share/text-generation/result/workflow-stream-handlers.ts | 3 +++ 2 files changed, 5 insertions(+) diff --git a/web/app/components/share/text-generation/result/__tests__/workflow-stream-handlers.spec.ts b/web/app/components/share/text-generation/result/__tests__/workflow-stream-handlers.spec.ts index 8785e77d1e..4c3f1f7652 100644 --- a/web/app/components/share/text-generation/result/__tests__/workflow-stream-handlers.spec.ts +++ b/web/app/components/share/text-generation/result/__tests__/workflow-stream-handlers.spec.ts @@ -196,6 +196,7 @@ describe('workflow-stream-handlers helpers', () => { id: 'iter-trace-2', node_id: 'iter-1', status: NodeRunningStatus.Succeeded, + details: undefined, })) const loopNextProcess = appendParallelNext(iterFinishedProcess, createTrace({ id: 'loop-trace-2', @@ -205,6 +206,7 @@ describe('workflow-stream-handlers helpers', () => { id: 'loop-trace-2', node_id: 'loop-1', status: NodeRunningStatus.Succeeded, + details: undefined, })) expect(loopFinishedProcess.tracing[0]).toEqual(expect.objectContaining({ diff --git a/web/app/components/share/text-generation/result/workflow-stream-handlers.ts b/web/app/components/share/text-generation/result/workflow-stream-handlers.ts index d4ea59d51e..ef8418f9fa 100644 --- a/web/app/components/share/text-generation/result/workflow-stream-handlers.ts +++ b/web/app/components/share/text-generation/result/workflow-stream-handlers.ts @@ -93,8 +93,11 @@ const finishParallelTrace = (current: WorkflowProcess | undefined, data: NodeTra draft.expand = true const traceIndex = findParallelTraceIndex(draft.tracing, data) if (traceIndex > -1) { + const currentTrace = draft.tracing[traceIndex] draft.tracing[traceIndex] = { + ...currentTrace, ...data, + details: data.details ?? currentTrace.details, expand: !!data.error, } } From 3193d7c9a57eec54072d864d332caaa2f7a79249 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yanli=20=E7=9B=90=E7=B2=92?= Date: Wed, 18 Mar 2026 19:52:08 +0800 Subject: [PATCH 13/14] test: fix unmatched shared trace fixtures --- .../result/__tests__/workflow-stream-handlers.spec.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/web/app/components/share/text-generation/result/__tests__/workflow-stream-handlers.spec.ts b/web/app/components/share/text-generation/result/__tests__/workflow-stream-handlers.spec.ts index 4c3f1f7652..db6af0dc84 100644 --- a/web/app/components/share/text-generation/result/__tests__/workflow-stream-handlers.spec.ts +++ b/web/app/components/share/text-generation/result/__tests__/workflow-stream-handlers.spec.ts @@ -297,6 +297,7 @@ describe('workflow-stream-handlers helpers', () => { ] const nextProcess = appendParallelNext(process, createTrace({ + id: 'trace-missing', node_id: 'missing-node', execution_metadata: { parallel_id: 'parallel-2' }, })) @@ -354,6 +355,7 @@ describe('workflow-stream-handlers helpers', () => { }, })) const notFinished = finishParallelTrace(process, createTrace({ + id: 'trace-missing', node_id: 'missing', execution_metadata: { parallel_id: 'parallel-missing', From a17f6f62bff0ffd7d7519e912905f99b1828a9ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yanli=20=E7=9B=90=E7=B2=92?= Date: Thu, 19 Mar 2026 21:57:48 +0800 Subject: [PATCH 14/14] test: restore mocks in rag pipeline tests --- .../rag-pipeline/components/__tests__/index.spec.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/web/app/components/rag-pipeline/components/__tests__/index.spec.tsx b/web/app/components/rag-pipeline/components/__tests__/index.spec.tsx index 36454d33e4..cf7e72d025 100644 --- a/web/app/components/rag-pipeline/components/__tests__/index.spec.tsx +++ b/web/app/components/rag-pipeline/components/__tests__/index.spec.tsx @@ -352,6 +352,10 @@ beforeEach(() => { vi.spyOn(console, 'error').mockImplementation(() => {}) }) +afterEach(() => { + vi.restoreAllMocks() +}) + // Helper to find the name input in PublishAsKnowledgePipelineModal function getNameInput() { return screen.getByPlaceholderText('pipeline.common.publishAsPipeline.namePlaceholder')