mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 01:56:46 +00:00
fix(editor): Fix partial chat executions (#15379)
This commit is contained in:
@@ -254,12 +254,10 @@ describe('ManualExecutionService', () => {
|
||||
|
||||
await manualExecutionService.runManually(data, workflow, additionalData, executionId);
|
||||
|
||||
expect(mockRun).toHaveBeenCalledWith(
|
||||
workflow,
|
||||
undefined, // startNode
|
||||
undefined, // destinationNode
|
||||
undefined, // pinData
|
||||
);
|
||||
expect(mockRun.mock.calls[0][0]).toBe(workflow);
|
||||
expect(mockRun.mock.calls[0][1]).toBeUndefined(); // startNode
|
||||
expect(mockRun.mock.calls[0][2]).toBeUndefined(); // destinationNode
|
||||
expect(mockRun.mock.calls[0][3]).toBeUndefined(); // pinData
|
||||
});
|
||||
|
||||
it('should use execution start node when available for full execution', async () => {
|
||||
@@ -297,11 +295,49 @@ describe('ManualExecutionService', () => {
|
||||
|
||||
expect(manualExecutionService.getExecutionStartNode).toHaveBeenCalledWith(data, workflow);
|
||||
|
||||
expect(mockRun.mock.calls[0][0]).toBe(workflow);
|
||||
expect(mockRun.mock.calls[0][1]).toBe(startNode); // startNode
|
||||
expect(mockRun.mock.calls[0][2]).toBeUndefined(); // destinationNode
|
||||
expect(mockRun.mock.calls[0][3]).toBe(data.pinData); // pinData
|
||||
});
|
||||
|
||||
it('should pass the triggerToStartFrom to workflowExecute.run for full execution', async () => {
|
||||
const mockTriggerData = mock<ITaskData>();
|
||||
const triggerNodeName = 'triggerNode';
|
||||
const data = mock<IWorkflowExecutionDataProcess>({
|
||||
executionMode: 'manual',
|
||||
destinationNode: undefined,
|
||||
pinData: undefined,
|
||||
triggerToStartFrom: {
|
||||
name: triggerNodeName,
|
||||
data: mockTriggerData,
|
||||
},
|
||||
});
|
||||
|
||||
const startNode = mock<INode>({ name: 'startNode' });
|
||||
const workflow = mock<Workflow>({
|
||||
getNode: jest.fn().mockReturnValue(startNode),
|
||||
});
|
||||
|
||||
const additionalData = mock<IWorkflowExecuteAdditionalData>();
|
||||
const executionId = 'test-execution-id';
|
||||
|
||||
jest.spyOn(manualExecutionService, 'getExecutionStartNode').mockReturnValue(startNode);
|
||||
|
||||
const mockRun = jest.fn().mockReturnValue('mockRunReturn');
|
||||
require('n8n-core').WorkflowExecute.mockImplementationOnce(() => ({
|
||||
run: mockRun,
|
||||
processRunExecutionData: jest.fn(),
|
||||
}));
|
||||
|
||||
await manualExecutionService.runManually(data, workflow, additionalData, executionId);
|
||||
|
||||
expect(mockRun).toHaveBeenCalledWith(
|
||||
workflow,
|
||||
startNode, // startNode
|
||||
undefined, // destinationNode
|
||||
data.pinData, // pinData
|
||||
undefined, // pinData
|
||||
data.triggerToStartFrom, // triggerToStartFrom
|
||||
);
|
||||
});
|
||||
|
||||
@@ -455,5 +491,110 @@ describe('ManualExecutionService', () => {
|
||||
}),
|
||||
);
|
||||
});
|
||||
it('should call runPartialWorkflow2 for V2 partial execution with runData and empty startNodes', async () => {
|
||||
const mockRunData = { nodeA: [{ data: { main: [[{ json: { value: 'test' } }]] } }] };
|
||||
const destinationNodeName = 'nodeB';
|
||||
const data = mock<IWorkflowExecutionDataProcess>({
|
||||
executionMode: 'manual',
|
||||
runData: mockRunData,
|
||||
startNodes: [],
|
||||
partialExecutionVersion: 2,
|
||||
destinationNode: destinationNodeName,
|
||||
pinData: {},
|
||||
dirtyNodeNames: [],
|
||||
agentRequest: undefined,
|
||||
});
|
||||
|
||||
const workflow = mock<Workflow>({
|
||||
getNode: jest.fn((name) => mock<INode>({ name })),
|
||||
});
|
||||
|
||||
const additionalData = mock<IWorkflowExecuteAdditionalData>();
|
||||
const executionId = 'test-exec-id-v2-empty-start';
|
||||
|
||||
const mockRunPartialWorkflow2 = jest.fn().mockReturnValue('mockPartial2Return-v2-empty');
|
||||
(core.WorkflowExecute as jest.Mock).mockImplementationOnce(() => ({
|
||||
runPartialWorkflow2: mockRunPartialWorkflow2,
|
||||
processRunExecutionData: jest.fn(),
|
||||
run: jest.fn(),
|
||||
runPartialWorkflow: jest.fn(),
|
||||
}));
|
||||
|
||||
await manualExecutionService.runManually(
|
||||
data,
|
||||
workflow,
|
||||
additionalData,
|
||||
executionId,
|
||||
data.pinData,
|
||||
);
|
||||
|
||||
expect(mockRunPartialWorkflow2).toHaveBeenCalledWith(
|
||||
workflow,
|
||||
mockRunData,
|
||||
data.pinData,
|
||||
data.dirtyNodeNames,
|
||||
destinationNodeName,
|
||||
data.agentRequest,
|
||||
);
|
||||
});
|
||||
|
||||
it('should call workflowExecute.run for V1 partial execution with runData and empty startNodes', async () => {
|
||||
const mockRunData = { nodeA: [{ data: { main: [[{ json: { value: 'test' } }]] } }] };
|
||||
const data = mock<IWorkflowExecutionDataProcess>({
|
||||
executionMode: 'manual',
|
||||
runData: mockRunData,
|
||||
startNodes: [],
|
||||
destinationNode: 'nodeC',
|
||||
pinData: { nodeX: [{ json: {} }] },
|
||||
triggerToStartFrom: undefined,
|
||||
});
|
||||
|
||||
const determinedStartNode = mock<INode>({ name: 'manualTrigger' });
|
||||
const destinationNodeMock = mock<INode>({ name: data.destinationNode });
|
||||
const workflow = mock<Workflow>({
|
||||
getNode: jest.fn((name) => {
|
||||
if (name === data.destinationNode) {
|
||||
return destinationNodeMock;
|
||||
}
|
||||
if (name === determinedStartNode.name) {
|
||||
return determinedStartNode;
|
||||
}
|
||||
return null;
|
||||
}),
|
||||
getTriggerNodes: jest.fn().mockReturnValue([determinedStartNode]),
|
||||
});
|
||||
|
||||
jest
|
||||
.spyOn(manualExecutionService, 'getExecutionStartNode')
|
||||
.mockReturnValue(determinedStartNode);
|
||||
|
||||
const additionalData = mock<IWorkflowExecuteAdditionalData>();
|
||||
const executionId = 'test-exec-id-v1-empty-start';
|
||||
|
||||
const mockRun = jest.fn().mockReturnValue('mockRunReturn-v1-empty');
|
||||
(core.WorkflowExecute as jest.Mock).mockImplementationOnce(() => ({
|
||||
run: mockRun,
|
||||
processRunExecutionData: jest.fn(),
|
||||
runPartialWorkflow: jest.fn(),
|
||||
runPartialWorkflow2: jest.fn(),
|
||||
}));
|
||||
|
||||
await manualExecutionService.runManually(
|
||||
data,
|
||||
workflow,
|
||||
additionalData,
|
||||
executionId,
|
||||
data.pinData,
|
||||
);
|
||||
|
||||
expect(manualExecutionService.getExecutionStartNode).toHaveBeenCalledWith(data, workflow);
|
||||
expect(mockRun).toHaveBeenCalledWith(
|
||||
workflow,
|
||||
determinedStartNode,
|
||||
data.destinationNode,
|
||||
data.pinData,
|
||||
data.triggerToStartFrom,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,9 +11,12 @@ import {
|
||||
} from 'n8n-core';
|
||||
import { MANUAL_TRIGGER_NODE_TYPE } from 'n8n-workflow';
|
||||
import type {
|
||||
IExecuteData,
|
||||
IPinData,
|
||||
IRun,
|
||||
IRunExecutionData,
|
||||
IWaitingForExecution,
|
||||
IWaitingForExecutionSource,
|
||||
IWorkflowExecuteAdditionalData,
|
||||
IWorkflowExecutionDataProcess,
|
||||
Workflow,
|
||||
@@ -59,7 +62,7 @@ export class ManualExecutionService {
|
||||
executionId: string,
|
||||
pinData?: IPinData,
|
||||
): PCancelable<IRun> {
|
||||
if (data.triggerToStartFrom?.data && data.startNodes) {
|
||||
if (data.triggerToStartFrom?.data && data.startNodes?.length) {
|
||||
this.logger.debug(
|
||||
`Execution ID ${executionId} had triggerToStartFrom. Starting from that trigger.`,
|
||||
{ executionId },
|
||||
@@ -71,13 +74,22 @@ export class ManualExecutionService {
|
||||
});
|
||||
const runData = { [data.triggerToStartFrom.name]: [data.triggerToStartFrom.data] };
|
||||
|
||||
const { nodeExecutionStack, waitingExecution, waitingExecutionSource } =
|
||||
recreateNodeExecutionStack(
|
||||
let nodeExecutionStack: IExecuteData[] = [];
|
||||
let waitingExecution: IWaitingForExecution = {};
|
||||
let waitingExecutionSource: IWaitingForExecutionSource = {};
|
||||
|
||||
if (data.destinationNode !== data.triggerToStartFrom.name) {
|
||||
const recreatedStack = recreateNodeExecutionStack(
|
||||
filterDisabledNodes(DirectedGraph.fromWorkflow(workflow)),
|
||||
new Set(startNodes),
|
||||
runData,
|
||||
data.pinData ?? {},
|
||||
);
|
||||
nodeExecutionStack = recreatedStack.nodeExecutionStack;
|
||||
waitingExecution = recreatedStack.waitingExecution;
|
||||
waitingExecutionSource = recreatedStack.waitingExecutionSource;
|
||||
}
|
||||
|
||||
const executionData: IRunExecutionData = {
|
||||
resultData: { runData, pinData },
|
||||
executionData: {
|
||||
@@ -101,8 +113,7 @@ export class ManualExecutionService {
|
||||
return workflowExecute.processRunExecutionData(workflow);
|
||||
} else if (
|
||||
data.runData === undefined ||
|
||||
data.startNodes === undefined ||
|
||||
data.startNodes.length === 0
|
||||
(data.partialExecutionVersion !== 2 && (!data.startNodes || data.startNodes.length === 0))
|
||||
) {
|
||||
// Full Execution
|
||||
// TODO: When the old partial execution logic is removed this block can
|
||||
@@ -143,7 +154,13 @@ export class ManualExecutionService {
|
||||
// Can execute without webhook so go on
|
||||
const workflowExecute = new WorkflowExecute(additionalData, data.executionMode);
|
||||
|
||||
return workflowExecute.run(workflow, startNode, data.destinationNode, data.pinData);
|
||||
return workflowExecute.run(
|
||||
workflow,
|
||||
startNode,
|
||||
data.destinationNode,
|
||||
data.pinData,
|
||||
data.triggerToStartFrom,
|
||||
);
|
||||
} else {
|
||||
// Partial Execution
|
||||
this.logger.debug(`Execution ID ${executionId} is a partial execution.`, { executionId });
|
||||
@@ -163,7 +180,7 @@ export class ManualExecutionService {
|
||||
return workflowExecute.runPartialWorkflow(
|
||||
workflow,
|
||||
data.runData,
|
||||
data.startNodes,
|
||||
data.startNodes ?? [],
|
||||
data.destinationNode,
|
||||
data.pinData,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user