feat(core): Link 'Error Trigger' nodes to the parent execution that errored (#16016)

This commit is contained in:
Jaakko Husso
2025-06-04 19:09:13 +03:00
committed by GitHub
parent 52a9d4b3d9
commit d64f2e57f7
2 changed files with 124 additions and 1 deletions

View File

@@ -1,4 +1,5 @@
import type { User } from '@n8n/db';
import type { GlobalConfig } from '@n8n/config';
import type { Project, User, WorkflowEntity, WorkflowRepository } from '@n8n/db';
import { mock } from 'jest-mock-extended';
import {
NodeConnectionTypes,
@@ -7,8 +8,10 @@ import {
type INodeType,
type IWorkflowBase,
type IWorkflowExecuteAdditionalData,
type ExecutionError,
} from 'n8n-workflow';
import type { IWorkflowErrorData } from '@/interfaces';
import type { NodeTypes } from '@/node-types';
import * as WorkflowExecuteAdditionalData from '@/workflow-execute-additional-data';
import type { WorkflowRunner } from '@/workflow-runner';
@@ -490,6 +493,113 @@ describe('WorkflowExecutionService', () => {
});
});
});
describe('executeErrorWorkflow()', () => {
test('should call `WorkflowRunner.run()` with correct parameters', async () => {
const workflowErrorData: IWorkflowErrorData = {
workflow: { id: 'workflow-id', name: 'Test Workflow' },
execution: {
id: 'execution-id',
mode: 'manual',
error: new Error('Test error') as ExecutionError,
lastNodeExecuted: 'Node with error',
},
};
const workflowRunnerMock = mock<WorkflowRunner>();
workflowRunnerMock.run.mockResolvedValue('fake-execution-id');
const errorTriggerType = 'n8n-nodes-base.errorTrigger';
const globalConfig = mock<GlobalConfig>({
nodes: {
errorTriggerType,
},
});
const errorTriggerNode: INode = {
id: 'error-trigger-node-id',
name: 'Error Trigger',
type: errorTriggerType,
typeVersion: 1,
position: [0, 0],
parameters: {},
};
const errorWorkflow = mock<WorkflowEntity>({
id: 'error-workflow-id',
name: 'Error Workflow',
active: false,
isArchived: false,
pinData: {},
nodes: [errorTriggerNode],
connections: {},
createdAt: new Date(),
updatedAt: new Date(),
});
const workflowRepositoryMock = mock<WorkflowRepository>();
workflowRepositoryMock.findOneBy.mockResolvedValue(errorWorkflow);
const service = new WorkflowExecutionService(
mock(),
mock(),
mock(),
workflowRepositoryMock,
nodeTypes,
mock(),
workflowRunnerMock,
globalConfig,
mock(),
mock(),
);
await service.executeErrorWorkflow(
'error-workflow-id',
workflowErrorData,
mock<Project>({ id: 'project-id' }),
);
expect(workflowRunnerMock.run).toHaveBeenCalledTimes(1);
expect(workflowRunnerMock.run).toHaveBeenCalledWith({
executionMode: 'error',
executionData: {
executionData: {
contextData: {},
metadata: {},
nodeExecutionStack: [
{
node: errorTriggerNode,
data: {
main: [
[
{
json: workflowErrorData,
},
],
],
},
source: null,
metadata: {
parentExecution: {
executionId: 'execution-id',
workflowId: 'workflow-id',
},
},
},
],
waitingExecution: {},
waitingExecutionSource: {},
},
resultData: {
runData: {},
},
startData: {},
},
workflowData: errorWorkflow,
projectId: 'project-id',
});
});
});
});
function createMainConnection(targetNode: string, sourceNode: string): IConnections {

View File

@@ -320,6 +320,14 @@ export class WorkflowExecutionService {
return;
}
const parentExecution =
workflowErrorData.execution?.id && workflowErrorData.workflow?.id
? {
executionId: workflowErrorData.execution.id,
workflowId: workflowErrorData.workflow.id,
}
: undefined;
// Can execute without webhook so go on
// Initialize the data of the webhook node
const nodeExecutionStack: IExecuteData[] = [];
@@ -335,6 +343,11 @@ export class WorkflowExecutionService {
],
},
source: null,
...(parentExecution && {
metadata: {
parentExecution,
},
}),
});
const runExecutionData: IRunExecutionData = {