refactor(core): Add test for workflow issue check and extract logic (#18845)

This commit is contained in:
Danny Martini
2025-08-27 11:33:00 +02:00
committed by GitHub
parent 7f77436c20
commit dfab6ce2a7
2 changed files with 90 additions and 62 deletions

View File

@@ -46,38 +46,6 @@ describe('processRunExecutionData', () => {
);
});
test('throws if workflow contains nodes with missing required properties', () => {
// ARRANGE
const node = createNodeData({ name: 'node', type: types.testNodeWithRequiredProperty });
const workflow = new DirectedGraph()
.addNodes(node)
.toWorkflow({ name: '', active: false, nodeTypes, settings: { executionOrder: 'v1' } });
const taskDataConnection = { main: [[{ json: { foo: 1 } }]] };
const executionData: IRunExecutionData = {
startData: { startNodes: [{ name: node.name, sourceData: null }] },
resultData: { runData: {} },
executionData: {
contextData: {},
nodeExecutionStack: [{ data: taskDataConnection, node, source: null }],
metadata: {},
waitingExecution: {},
waitingExecutionSource: {},
},
};
const workflowExecute = new WorkflowExecute(additionalData, executionMode, executionData);
// ACT & ASSERT
// The function returns a Promise, but throws synchronously, so we can't await it.
// eslint-disable-next-line @typescript-eslint/promise-function-async
expect(() => workflowExecute.processRunExecutionData(workflow)).toThrowError(
new ApplicationError(
'The workflow has issues and cannot be executed for that reason. Please fix them first.',
),
);
});
test('returns input data verbatim', async () => {
// ARRANGE
const node = createNodeData({ name: 'node', type: types.passThrough });
@@ -183,4 +151,71 @@ describe('processRunExecutionData', () => {
expect(result.data.resultData.runData.waitingNode[0].executionStatus).toEqual('success');
});
});
describe('workflow issues', () => {
test('throws if workflow contains nodes with missing required properties', () => {
// ARRANGE
const node = createNodeData({ name: 'node', type: types.testNodeWithRequiredProperty });
const workflow = new DirectedGraph()
.addNodes(node)
.toWorkflow({ name: '', active: false, nodeTypes, settings: { executionOrder: 'v1' } });
const taskDataConnection = { main: [[{ json: { foo: 1 } }]] };
const executionData: IRunExecutionData = {
startData: { startNodes: [{ name: node.name, sourceData: null }] },
resultData: { runData: {} },
executionData: {
contextData: {},
nodeExecutionStack: [{ data: taskDataConnection, node, source: null }],
metadata: {},
waitingExecution: {},
waitingExecutionSource: {},
},
};
const workflowExecute = new WorkflowExecute(additionalData, executionMode, executionData);
// ACT & ASSERT
// The function returns a Promise, but throws synchronously, so we can't await it.
// eslint-disable-next-line @typescript-eslint/promise-function-async
expect(() => workflowExecute.processRunExecutionData(workflow)).toThrowError(
new ApplicationError(
'The workflow has issues and cannot be executed for that reason. Please fix them first.',
),
);
});
test('does not complain about nodes with issue past the destination node', async () => {
// ARRANGE
const node1 = createNodeData({ name: 'node1', type: types.passThrough });
const node2 = createNodeData({ name: 'node2', type: types.testNodeWithRequiredProperty });
const workflow = new DirectedGraph()
.addNodes(node1, node2)
.addConnection({ from: node1, to: node2 })
.toWorkflow({ name: '', active: false, nodeTypes, settings: { executionOrder: 'v1' } });
const taskDataConnection = { main: [[{ json: { foo: 1 } }]] };
const executionData: IRunExecutionData = {
startData: {
startNodes: [{ name: node1.name, sourceData: null }],
destinationNode: node1.name,
},
resultData: { runData: {} },
executionData: {
contextData: {},
nodeExecutionStack: [{ data: taskDataConnection, node: node1, source: null }],
metadata: {},
waitingExecution: {},
waitingExecutionSource: {},
},
};
const workflowExecute = new WorkflowExecute(additionalData, executionMode, executionData);
// ACT & ASSERT
// The function returns a Promise, but throws synchronously, so we can't await it.
// eslint-disable-next-line @typescript-eslint/promise-function-async
expect(() => workflowExecute.processRunExecutionData(workflow)).not.toThrowError();
});
});
});

View File

@@ -1513,6 +1513,27 @@ export class WorkflowExecute {
}
}
private checkForWorkflowIssues(workflow: Workflow): void {
this.assertExecutionDataExists(this.runExecutionData.executionData, workflow);
// Node execution stack will be empty for an execution containing only Chat
// Trigger.
const startNode = this.runExecutionData.executionData.nodeExecutionStack.at(0)?.node.name;
let destinationNode: string | undefined;
if (this.runExecutionData.startData && this.runExecutionData.startData.destinationNode) {
destinationNode = this.runExecutionData.startData.destinationNode;
}
const pinDataNodeNames = Object.keys(this.runExecutionData.resultData.pinData ?? {});
const workflowIssues = this.checkReadyForExecution(workflow, {
startNode,
destinationNode,
pinDataNodeNames,
});
if (workflowIssues !== null) {
throw new WorkflowHasIssuesError();
}
}
/**
* Runs the given execution data.
*
@@ -1528,38 +1549,9 @@ export class WorkflowExecute {
this.status = 'running';
const { hooks, executionId } = this.additionalData;
const { hooks } = this.additionalData;
assert.ok(hooks, 'Failed to run workflow due to missing execution lifecycle hooks');
if (!this.runExecutionData.executionData) {
throw new ApplicationError('Failed to run workflow due to missing execution data', {
extra: {
workflowId: workflow.id,
executionId,
mode: this.mode,
},
});
}
/** Node execution stack will be empty for an execution containing only Chat Trigger. */
const startNode = this.runExecutionData.executionData.nodeExecutionStack.at(0)?.node.name;
let destinationNode: string | undefined;
if (this.runExecutionData.startData && this.runExecutionData.startData.destinationNode) {
destinationNode = this.runExecutionData.startData.destinationNode;
}
const pinDataNodeNames = Object.keys(this.runExecutionData.resultData.pinData ?? {});
const workflowIssues = this.checkReadyForExecution(workflow, {
startNode,
destinationNode,
pinDataNodeNames,
});
if (workflowIssues !== null) {
throw new WorkflowHasIssuesError();
}
// Variables which hold temporary data for each node-execution
let executionData: IExecuteData;
let executionError: ExecutionBaseError | undefined;
@@ -1570,6 +1562,7 @@ export class WorkflowExecute {
this.runExecutionData.startData = {};
}
this.checkForWorkflowIssues(workflow);
this.handleWaitingState(workflow);
let currentExecutionTry = '';