diff --git a/cypress/utils/executions.ts b/cypress/utils/executions.ts index 12f4d2454a458..11eb5bba2cce9 100644 --- a/cypress/utils/executions.ts +++ b/cypress/utils/executions.ts @@ -1,5 +1,6 @@ import { stringify } from 'flatted'; -import type { IDataObject, IPinData, ITaskData, ITaskDataConnections } from 'n8n-workflow'; +import type { IDataObject, ITaskData, ITaskDataConnections } from 'n8n-workflow'; +import { nanoid } from 'nanoid'; import { clickExecuteWorkflowButton } from '../composables/workflow'; @@ -39,38 +40,6 @@ export function createMockNodeExecutionData( }; } -function createMockWorkflowExecutionData({ - runData, - lastNodeExecuted, -}: { - runData: Record; - pinData?: IPinData; - lastNodeExecuted: string; -}) { - return { - data: stringify({ - startData: {}, - resultData: { - runData, - pinData: {}, - lastNodeExecuted, - }, - executionData: { - contextData: {}, - nodeExecutionStack: [], - metadata: {}, - waitingExecution: {}, - waitingExecutionSource: {}, - }, - }), - mode: 'manual', - startedAt: new Date().toISOString(), - stoppedAt: new Date().toISOString(), - status: 'success', - finished: true, - }; -} - export function runMockWorkflowExecution({ trigger, lastNodeExecuted, @@ -80,6 +49,7 @@ export function runMockWorkflowExecution({ lastNodeExecuted: string; runData: Array>; }) { + const workflowId = nanoid(); const executionId = Math.floor(Math.random() * 1_000_000).toString(); cy.intercept('POST', '/rest/workflows/**/run?**', { @@ -117,17 +87,24 @@ export function runMockWorkflowExecution({ resolvedRunData[nodeName] = nodeExecution[nodeName]; }); - cy.intercept('GET', `/rest/executions/${executionId}`, { - statusCode: 200, - body: { - data: createMockWorkflowExecutionData({ + cy.push('executionFinished', { + executionId, + workflowId, + status: 'success', + rawData: stringify({ + startData: {}, + resultData: { + runData, + pinData: {}, lastNodeExecuted, - runData: resolvedRunData, - }), - }, - }).as('getExecution'); - - cy.push('executionFinished', { executionId }); - - cy.wait('@getExecution'); + }, + executionData: { + contextData: {}, + nodeExecutionStack: [], + metadata: {}, + waitingExecution: {}, + waitingExecutionSource: {}, + }, + }), + }); } diff --git a/packages/@n8n/api-types/src/push/execution.ts b/packages/@n8n/api-types/src/push/execution.ts index 9c723e2817f8a..7a1c1377d9bb4 100644 --- a/packages/@n8n/api-types/src/push/execution.ts +++ b/packages/@n8n/api-types/src/push/execution.ts @@ -1,4 +1,4 @@ -import type { ITaskData, WorkflowExecuteMode } from 'n8n-workflow'; +import type { ExecutionStatus, ITaskData, WorkflowExecuteMode } from 'n8n-workflow'; type ExecutionStarted = { type: 'executionStarted'; @@ -23,6 +23,10 @@ type ExecutionFinished = { type: 'executionFinished'; data: { executionId: string; + workflowId: string; + status: ExecutionStatus; + /** @deprecated: Please construct execution data in the frontend from the data pushed in previous messages, instead of depending on this additional payload serialization */ + rawData?: string; }; }; diff --git a/packages/cli/src/workflow-execute-additional-data.ts b/packages/cli/src/workflow-execute-additional-data.ts index 49df7abea635f..1d73ca379bd34 100644 --- a/packages/cli/src/workflow-execute-additional-data.ts +++ b/packages/cli/src/workflow-execute-additional-data.ts @@ -5,6 +5,7 @@ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ import type { PushType } from '@n8n/api-types'; import { GlobalConfig } from '@n8n/config'; +import { stringify } from 'flatted'; import { WorkflowExecute } from 'n8n-core'; import { ApplicationError, @@ -318,9 +319,17 @@ function hookFunctionsPush(): IWorkflowExecuteHooks { workflowId, }); - const pushType = - fullRunData.status === 'waiting' ? 'executionWaiting' : 'executionFinished'; - pushInstance.send(pushType, { executionId }, pushRef); + const { status } = fullRunData; + if (status === 'waiting') { + pushInstance.send('executionWaiting', { executionId }, pushRef); + } else { + const rawData = stringify(fullRunData.data); + pushInstance.send( + 'executionFinished', + { executionId, workflowId, status, rawData }, + pushRef, + ); + } }, ], }; diff --git a/packages/editor-ui/src/composables/usePushConnection.test.ts b/packages/editor-ui/src/composables/usePushConnection.test.ts index baccb40887b31..888c36a45df6e 100644 --- a/packages/editor-ui/src/composables/usePushConnection.test.ts +++ b/packages/editor-ui/src/composables/usePushConnection.test.ts @@ -2,7 +2,6 @@ import { stringify } from 'flatted'; import { useRouter } from 'vue-router'; import { createPinia, setActivePinia } from 'pinia'; import type { PushMessage, PushPayload } from '@n8n/api-types'; -import { mock } from 'vitest-mock-extended'; import type { ITaskData, WorkflowOperationError } from 'n8n-workflow'; import { usePushConnection } from '@/composables/usePushConnection'; @@ -11,7 +10,6 @@ import { useOrchestrationStore } from '@/stores/orchestration.store'; import { useUIStore } from '@/stores/ui.store'; import { useWorkflowsStore } from '@/stores/workflows.store'; import { useToast } from '@/composables/useToast'; -import type { IExecutionResponse } from '@/Interface'; vi.mock('vue-router', () => { return { @@ -140,10 +138,7 @@ describe('usePushConnection()', () => { describe('executionFinished', () => { const executionId = '1'; - const event: PushMessage = { - type: 'executionFinished', - data: { executionId: '1' }, - }; + const workflowId = 'abc'; beforeEach(() => { workflowsStore.activeExecutionId = executionId; @@ -151,28 +146,23 @@ describe('usePushConnection()', () => { }); it('should handle executionFinished event correctly', async () => { - const spy = vi.spyOn(workflowsStore, 'fetchExecutionDataById').mockResolvedValue( - mock({ - id: executionId, - data: stringify({ + const result = await pushConnection.pushMessageReceived({ + type: 'executionFinished', + data: { + executionId, + workflowId, + status: 'success', + rawData: stringify({ resultData: { runData: {}, }, - }) as unknown as IExecutionResponse['data'], - finished: true, - mode: 'manual', - startedAt: new Date(), - stoppedAt: new Date(), - status: 'success', - }), - ); - - const result = await pushConnection.pushMessageReceived(event); + }), + }, + }); expect(result).toBeTruthy(); expect(workflowsStore.workflowExecutionData).toBeDefined(); expect(uiStore.isActionActive['workflowRunning']).toBeTruthy(); - expect(spy).toHaveBeenCalledWith(executionId); expect(toast.showMessage).toHaveBeenCalledWith({ title: 'Workflow executed successfully', @@ -181,10 +171,13 @@ describe('usePushConnection()', () => { }); it('should handle isManualExecutionCancelled correctly', async () => { - const spy = vi.spyOn(workflowsStore, 'fetchExecutionDataById').mockResolvedValue( - mock({ - id: executionId, - data: stringify({ + const result = await pushConnection.pushMessageReceived({ + type: 'executionFinished', + data: { + executionId, + workflowId, + status: 'error', + rawData: stringify({ startData: {}, resultData: { runData: { @@ -198,14 +191,9 @@ describe('usePushConnection()', () => { node: 'Last Node', } as unknown as WorkflowOperationError, }, - }) as unknown as IExecutionResponse['data'], - mode: 'manual', - startedAt: new Date(), - status: 'running', - }), - ); - - const result = await pushConnection.pushMessageReceived(event); + }), + }, + }); expect(useToast().showMessage).toHaveBeenCalledWith({ message: @@ -219,7 +207,6 @@ describe('usePushConnection()', () => { expect(result).toBeTruthy(); expect(workflowsStore.workflowExecutionData).toBeDefined(); expect(uiStore.isActionActive.workflowRunning).toBeTruthy(); - expect(spy).toHaveBeenCalledWith(executionId); }); }); diff --git a/packages/editor-ui/src/composables/usePushConnection.ts b/packages/editor-ui/src/composables/usePushConnection.ts index 6e9808c36a436..d5ecdefc71365 100644 --- a/packages/editor-ui/src/composables/usePushConnection.ts +++ b/packages/editor-ui/src/composables/usePushConnection.ts @@ -35,6 +35,7 @@ import { useTelemetry } from '@/composables/useTelemetry'; import type { PushMessageQueueItem } from '@/types'; import { useAssistantStore } from '@/stores/assistant.store'; import NodeExecutionErrorMessage from '@/components/NodeExecutionErrorMessage.vue'; +import type { IExecutionResponse } from '@/Interface'; export function usePushConnection({ router }: { router: ReturnType }) { const workflowHelpers = useWorkflowHelpers({ router }); @@ -205,11 +206,19 @@ export function usePushConnection({ router }: { router: ReturnType; + if (receivedData.type === 'executionFinished' && receivedData.data.rawData) { + const { workflowId, status, rawData } = receivedData.data; + executionData = { workflowId, data: parse(rawData), status }; + } else { + const execution = await workflowsStore.fetchExecutionDataById(executionId); + if (!execution?.data) return false; + executionData = { + workflowId: execution.workflowId, + data: parse(execution.data as unknown as string), + status: execution.status, + }; + } const iRunExecutionData: IRunExecutionData = { startData: executionData.data?.startData, @@ -265,7 +274,7 @@ export function usePushConnection({ router }: { router: ReturnType