import { Logger } from '@n8n/backend-common'; import type { IExecutionResponse } from '@n8n/db'; import { ExecutionRepository } from '@n8n/db'; import { mock } from 'jest-mock-extended'; import { ErrorReporter } from 'n8n-core'; import type { IRunExecutionData, ITaskData } from 'n8n-workflow'; import { mockInstance } from '@test/mocking'; import { saveExecutionProgress } from '../save-execution-progress'; describe('saveExecutionProgress', () => { mockInstance(Logger); const errorReporter = mockInstance(ErrorReporter); const executionRepository = mockInstance(ExecutionRepository); afterEach(() => { jest.resetAllMocks(); }); const workflowId = 'some-workflow-id'; const executionId = 'some-execution-id'; const nodeName = 'My Node'; const taskData = mock(); const runExecutionData = mock(); const commonArgs = [workflowId, executionId, nodeName, taskData, runExecutionData] as const; test('should not try to update non-existent executions', async () => { executionRepository.findSingleExecution.mockResolvedValue(undefined); await saveExecutionProgress(...commonArgs); expect(executionRepository.updateExistingExecution).not.toHaveBeenCalled(); }); test('should handle DB errors on execution lookup', async () => { const error = new Error('Something went wrong'); executionRepository.findSingleExecution.mockImplementation(() => { throw error; }); await saveExecutionProgress(...commonArgs); expect(executionRepository.updateExistingExecution).not.toHaveBeenCalled(); expect(errorReporter.error).toHaveBeenCalledWith(error); }); test('should handle DB errors when updating the execution', async () => { const error = new Error('Something went wrong'); executionRepository.findSingleExecution.mockResolvedValue({} as IExecutionResponse); executionRepository.updateExistingExecution.mockImplementation(() => { throw error; }); await saveExecutionProgress(...commonArgs); expect(executionRepository.findSingleExecution).toHaveBeenCalled(); expect(executionRepository.updateExistingExecution).toHaveBeenCalled(); expect(errorReporter.error).toHaveBeenCalledWith(error); }); test('should not try to update finished executions', async () => { executionRepository.findSingleExecution.mockResolvedValue( mock({ finished: true, }), ); await saveExecutionProgress(...commonArgs); expect(executionRepository.updateExistingExecution).not.toHaveBeenCalled(); }); test('should populate `.data` when it is missing', async () => { const fullExecutionData = {} as IExecutionResponse; executionRepository.findSingleExecution.mockResolvedValue(fullExecutionData); await saveExecutionProgress(...commonArgs); expect(fullExecutionData).toEqual({ data: { executionData: runExecutionData.executionData, resultData: { lastNodeExecuted: nodeName, runData: { [nodeName]: [taskData], }, }, startData: {}, }, status: 'running', }); expect(executionRepository.updateExistingExecution).toHaveBeenCalledWith( executionId, fullExecutionData, ); expect(errorReporter.error).not.toHaveBeenCalled(); }); test('should augment `.data` if it already exists', async () => { const fullExecutionData = { data: { startData: {}, resultData: { runData: { [nodeName]: [{}], }, }, }, } as unknown as IExecutionResponse; executionRepository.findSingleExecution.mockResolvedValue(fullExecutionData); await saveExecutionProgress(...commonArgs); expect(fullExecutionData).toEqual({ data: { executionData: runExecutionData.executionData, resultData: { lastNodeExecuted: nodeName, runData: { [nodeName]: [{}, taskData], }, }, startData: {}, }, status: 'running', }); expect(executionRepository.updateExistingExecution).toHaveBeenCalledWith( executionId, fullExecutionData, ); }); test('should set last executed node correctly', async () => { const fullExecutionData = { data: { resultData: { lastNodeExecuted: 'Another Node', runData: {}, }, }, } as unknown as IExecutionResponse; executionRepository.findSingleExecution.mockResolvedValue(fullExecutionData); await saveExecutionProgress(...commonArgs); expect(fullExecutionData.data.resultData.lastNodeExecuted).toEqual(nodeName); }); });