From ddfe594cf0486ed64d0ddc58e634ae6dbceb72e7 Mon Sep 17 00:00:00 2001 From: Danny Martini Date: Thu, 17 Apr 2025 11:09:54 +0200 Subject: [PATCH] fix(core): Prefer triggers with run data during partial executions (#14691) --- ...find-trigger-for-partial-execution.test.ts | 33 +++++++++++++++---- .../find-trigger-for-partial-execution.ts | 10 +++++- .../src/execution-engine/workflow-execute.ts | 2 +- 3 files changed, 37 insertions(+), 8 deletions(-) diff --git a/packages/core/src/execution-engine/partial-execution-utils/__tests__/find-trigger-for-partial-execution.test.ts b/packages/core/src/execution-engine/partial-execution-utils/__tests__/find-trigger-for-partial-execution.test.ts index b9d1ae9eb9..c0bc6d51d5 100644 --- a/packages/core/src/execution-engine/partial-execution-utils/__tests__/find-trigger-for-partial-execution.test.ts +++ b/packages/core/src/execution-engine/partial-execution-utils/__tests__/find-trigger-for-partial-execution.test.ts @@ -1,8 +1,9 @@ import { mock } from 'jest-mock-extended'; -import type { IConnections, INode, INodeType, INodeTypes, IPinData } from 'n8n-workflow'; +import type { IConnections, INode, INodeType, INodeTypes, IPinData, IRunData } from 'n8n-workflow'; import { Workflow } from 'n8n-workflow'; -import { toIConnections } from './helpers'; +import { createNodeData, toIConnections, toITaskData } from './helpers'; +import { DirectedGraph } from '../directed-graph'; import { findTriggerForPartialExecution } from '../find-trigger-for-partial-execution'; describe('findTriggerForPartialExecution', () => { @@ -188,7 +189,7 @@ describe('findTriggerForPartialExecution', () => { '$description', ({ nodes, connections, destinationNodeName, expectedTrigger, pinData }) => { const workflow = createMockWorkflow(nodes, toIConnections(connections), pinData); - expect(findTriggerForPartialExecution(workflow, destinationNodeName)).toBe( + expect(findTriggerForPartialExecution(workflow, destinationNodeName, {})).toBe( expectedTrigger, ); }, @@ -199,19 +200,39 @@ describe('findTriggerForPartialExecution', () => { describe('Error and Edge Case Handling', () => { it('should handle non-existent destination node gracefully', () => { const workflow = createMockWorkflow([], {}); - expect(findTriggerForPartialExecution(workflow, 'NonExistentNode')).toBeUndefined(); + expect(findTriggerForPartialExecution(workflow, 'NonExistentNode', {})).toBeUndefined(); }); it('should handle empty workflow', () => { const workflow = createMockWorkflow([], {}); - expect(findTriggerForPartialExecution(workflow, '')).toBeUndefined(); + expect(findTriggerForPartialExecution(workflow, '', {})).toBeUndefined(); }); it('should handle workflow with no connections', () => { const workflow = createMockWorkflow([manualTriggerNode], {}); - expect(findTriggerForPartialExecution(workflow, manualTriggerNode.name)).toBe( + expect(findTriggerForPartialExecution(workflow, manualTriggerNode.name, {})).toBe( manualTriggerNode, ); }); + + it('should prefer triggers that have run data', () => { + // ARRANGE + const trigger1 = createNodeData({ name: 'trigger1', type: 'n8n-nodes-base.manualTrigger' }); + const trigger2 = createNodeData({ name: 'trigger2', type: 'n8n-nodes-base.manualTrigger' }); + const node = createNodeData({ name: 'node' }); + const workflow = new DirectedGraph() + .addNodes(trigger1, trigger2, node) + .addConnections({ from: trigger1, to: node }, { from: trigger2, to: node }) + .toWorkflow({ name: '', active: false, nodeTypes }); + const runData: IRunData = { + [trigger1.name]: [toITaskData([{ data: { nodeName: 'trigger1' } }])], + }; + + // ACT + const chosenTrigger = findTriggerForPartialExecution(workflow, node.name, runData); + + // ASSERT + expect(chosenTrigger).toBe(trigger1); + }); }); }); diff --git a/packages/core/src/execution-engine/partial-execution-utils/find-trigger-for-partial-execution.ts b/packages/core/src/execution-engine/partial-execution-utils/find-trigger-for-partial-execution.ts index a788d958c9..f753ddbae0 100644 --- a/packages/core/src/execution-engine/partial-execution-utils/find-trigger-for-partial-execution.ts +++ b/packages/core/src/execution-engine/partial-execution-utils/find-trigger-for-partial-execution.ts @@ -1,5 +1,5 @@ import * as assert from 'assert/strict'; -import type { INode, INodeType, Workflow } from 'n8n-workflow'; +import type { INode, INodeType, IRunData, Workflow } from 'n8n-workflow'; const isTriggerNode = (nodeType: INodeType) => nodeType.description.group.includes('trigger'); @@ -29,6 +29,7 @@ function findAllParentTriggers(workflow: Workflow, destinationNodeName: string) export function findTriggerForPartialExecution( workflow: Workflow, destinationNodeName: string, + runData: IRunData, ): INode | undefined { // First, check if the destination node itself is a trigger const destinationNode = workflow.getNode(destinationNodeName); @@ -48,6 +49,13 @@ export function findTriggerForPartialExecution( (trigger) => !trigger.disabled, ); + // prefer triggers that have run data + for (const trigger of parentTriggers) { + if (runData[trigger.name]) { + return trigger; + } + } + // Prioritize webhook triggers with pinned-data const pinnedTriggers = parentTriggers // TODO: add the other filters here from `findAllPinnedActivators`, see diff --git a/packages/core/src/execution-engine/workflow-execute.ts b/packages/core/src/execution-engine/workflow-execute.ts index 50fc4a9fe7..b735103ce0 100644 --- a/packages/core/src/execution-engine/workflow-execute.ts +++ b/packages/core/src/execution-engine/workflow-execute.ts @@ -394,7 +394,7 @@ export class WorkflowExecute { } // 1. Find the Trigger - const trigger = findTriggerForPartialExecution(workflow, destinationNodeName); + const trigger = findTriggerForPartialExecution(workflow, destinationNodeName, runData); if (trigger === undefined) { throw new UserError('Connect a trigger to run this node'); }