fix(core): Don't fail partial execution when an unrelated node is dirty (#13925)

This commit is contained in:
Danny Martini
2025-03-18 12:27:49 +01:00
committed by GitHub
parent d6d5a66f5d
commit 918cc51abc
4 changed files with 100 additions and 1 deletions

View File

@@ -540,6 +540,55 @@ describe('WorkflowExecute', () => {
);
});
// DR ►► DR
// ┌───────┐ ┌───────────┐ ┌─────────┐
// │trigger├───►destination├───►dirtyNode│
// └───────┘ └───────────┘ └─────────┘
test('passes pruned dirty nodes to `cleanRunData`', async () => {
// ARRANGE
const waitPromise = createDeferredPromise<IRun>();
const nodeExecutionOrder: string[] = [];
const additionalData = Helpers.WorkflowExecuteAdditionalData(waitPromise, nodeExecutionOrder);
const workflowExecute = new WorkflowExecute(additionalData, 'manual');
const trigger = createNodeData({ name: 'trigger', type: 'n8n-nodes-base.manualTrigger' });
const destination = createNodeData({ name: 'destination' });
const dirtyNode = createNodeData({ name: 'dirtyNode' });
const workflow = new DirectedGraph()
.addNodes(trigger, destination, dirtyNode)
.addConnections({ from: trigger, to: destination }, { from: destination, to: dirtyNode })
.toWorkflow({ name: '', active: false, nodeTypes });
const pinData: IPinData = {};
const runData: IRunData = {};
const dirtyNodeNames: string[] = [trigger.name, dirtyNode.name];
jest.spyOn(workflowExecute, 'processRunExecutionData').mockImplementationOnce(jest.fn());
const cleanRunDataSpy = jest.spyOn(partialExecutionUtils, 'cleanRunData');
// ACT
await workflowExecute.runPartialWorkflow2(
workflow,
runData,
pinData,
dirtyNodeNames,
destination.name,
);
// ASSERT
const subgraph = new DirectedGraph()
.addNodes(trigger, destination)
.addConnections({ from: trigger, to: destination });
expect(cleanRunDataSpy).toHaveBeenCalledTimes(2);
expect(cleanRunDataSpy).toHaveBeenNthCalledWith(
1,
runData,
subgraph,
// first call with the dirty nodes, which are an empty set in this case
new Set([trigger]),
);
});
// ►►
// ┌──────┐
// │orphan│

View File

@@ -492,4 +492,35 @@ describe('DirectedGraph', () => {
expect(graph.hasNode(node.name + 'foo')).toBe(false);
});
});
describe('getNodesByNames', () => {
test('returns empty Set when no names are provided', () => {
// ARRANGE
const node1 = createNodeData({ name: 'Node1' });
const node2 = createNodeData({ name: 'Node2' });
const graph = new DirectedGraph().addNodes(node1, node2);
// ACT
const result = graph.getNodesByNames([]);
// ASSERT
expect(result.size).toBe(0);
expect(result).toEqual(new Set());
});
test('returns Set with only nodes that exist in the graph', () => {
// ARRANGE
const node1 = createNodeData({ name: 'Node1' });
const node2 = createNodeData({ name: 'Node2' });
const node3 = createNodeData({ name: 'Node3' });
const graph = new DirectedGraph().addNodes(node1, node2, node3);
// ACT
const result = graph.getNodesByNames(['Node1', 'Node3', 'Node4']);
// ASSERT
expect(result.size).toBe(2);
expect(result).toEqual(new Set([node1, node3]));
});
});
});

View File

@@ -50,6 +50,25 @@ export class DirectedGraph {
return new Map(this.nodes.entries());
}
/**
* Returns a set of nodes whose names match the provided array of names.
*
* Only nodes that exist in the graph will be included in the result.
*/
getNodesByNames(names: string[]) {
const nodes: Set<INode> = new Set();
for (const name of names) {
const node = this.nodes.get(name);
if (node) {
nodes.add(node);
}
}
return nodes;
}
getConnections(filter: { to?: INode } = {}) {
const filteredCopy: GraphConnection[] = [];

View File

@@ -404,7 +404,7 @@ export class WorkflowExecute {
const filteredNodes = graph.getNodes();
// 3. Find the Start Nodes
const dirtyNodes = new Set(workflow.getNodes(dirtyNodeNames));
const dirtyNodes = graph.getNodesByNames(dirtyNodeNames);
runData = cleanRunData(runData, graph, dirtyNodes);
let startNodes = findStartNodes({ graph, trigger, destination, runData, pinData });