From b0e9085ffc9ed396c600a4495851916a03bbc861 Mon Sep 17 00:00:00 2001 From: Eugene Date: Mon, 2 Dec 2024 13:52:09 +0100 Subject: [PATCH] chore: Get past execution's start and destination nodes while running tests (no-changelog) (#11983) --- .../__tests__/create-pin-data.ee.test.ts | 24 +++++ .../__tests__/get-start-node.ee.test.ts | 40 ++++++++ .../execution-data.multiple-triggers-2.json | 95 +++++++++++++++++++ .../execution-data.multiple-triggers.json | 87 +++++++++++++++++ .../mock-data/workflow.multiple-triggers.json | 76 +++++++++++++++ .../test-runner/test-runner.service.ee.ts | 47 ++++----- .../src/evaluation/test-runner/utils.ee.ts | 34 +++++++ 7 files changed, 373 insertions(+), 30 deletions(-) create mode 100644 packages/cli/src/evaluation/test-runner/__tests__/create-pin-data.ee.test.ts create mode 100644 packages/cli/src/evaluation/test-runner/__tests__/get-start-node.ee.test.ts create mode 100644 packages/cli/src/evaluation/test-runner/__tests__/mock-data/execution-data.multiple-triggers-2.json create mode 100644 packages/cli/src/evaluation/test-runner/__tests__/mock-data/execution-data.multiple-triggers.json create mode 100644 packages/cli/src/evaluation/test-runner/__tests__/mock-data/workflow.multiple-triggers.json create mode 100644 packages/cli/src/evaluation/test-runner/utils.ee.ts diff --git a/packages/cli/src/evaluation/test-runner/__tests__/create-pin-data.ee.test.ts b/packages/cli/src/evaluation/test-runner/__tests__/create-pin-data.ee.test.ts new file mode 100644 index 0000000000..6da88f9c20 --- /dev/null +++ b/packages/cli/src/evaluation/test-runner/__tests__/create-pin-data.ee.test.ts @@ -0,0 +1,24 @@ +import { readFileSync } from 'fs'; +import path from 'path'; + +import { createPinData } from '../utils.ee'; + +const wfUnderTestJson = JSON.parse( + readFileSync(path.join(__dirname, './mock-data/workflow.under-test.json'), { encoding: 'utf-8' }), +); + +const executionDataJson = JSON.parse( + readFileSync(path.join(__dirname, './mock-data/execution-data.json'), { encoding: 'utf-8' }), +); + +describe('createPinData', () => { + test('should create pin data from past execution data', () => { + const pinData = createPinData(wfUnderTestJson, executionDataJson); + + expect(pinData).toEqual( + expect.objectContaining({ + 'When clicking ‘Test workflow’': expect.anything(), + }), + ); + }); +}); diff --git a/packages/cli/src/evaluation/test-runner/__tests__/get-start-node.ee.test.ts b/packages/cli/src/evaluation/test-runner/__tests__/get-start-node.ee.test.ts new file mode 100644 index 0000000000..20f75fcf57 --- /dev/null +++ b/packages/cli/src/evaluation/test-runner/__tests__/get-start-node.ee.test.ts @@ -0,0 +1,40 @@ +import { readFileSync } from 'fs'; +import path from 'path'; + +import { getPastExecutionStartNode } from '../utils.ee'; + +const executionDataJson = JSON.parse( + readFileSync(path.join(__dirname, './mock-data/execution-data.json'), { encoding: 'utf-8' }), +); + +const executionDataMultipleTriggersJson = JSON.parse( + readFileSync(path.join(__dirname, './mock-data/execution-data.multiple-triggers.json'), { + encoding: 'utf-8', + }), +); + +const executionDataMultipleTriggersJson2 = JSON.parse( + readFileSync(path.join(__dirname, './mock-data/execution-data.multiple-triggers-2.json'), { + encoding: 'utf-8', + }), +); + +describe('getPastExecutionStartNode', () => { + test('should return the start node of the past execution', () => { + const startNode = getPastExecutionStartNode(executionDataJson); + + expect(startNode).toEqual('When clicking ‘Test workflow’'); + }); + + test('should return the start node of the past execution with multiple triggers', () => { + const startNode = getPastExecutionStartNode(executionDataMultipleTriggersJson); + + expect(startNode).toEqual('When clicking ‘Test workflow’'); + }); + + test('should return the start node of the past execution with multiple triggers - chat trigger', () => { + const startNode = getPastExecutionStartNode(executionDataMultipleTriggersJson2); + + expect(startNode).toEqual('When chat message received'); + }); +}); diff --git a/packages/cli/src/evaluation/test-runner/__tests__/mock-data/execution-data.multiple-triggers-2.json b/packages/cli/src/evaluation/test-runner/__tests__/mock-data/execution-data.multiple-triggers-2.json new file mode 100644 index 0000000000..12bb837912 --- /dev/null +++ b/packages/cli/src/evaluation/test-runner/__tests__/mock-data/execution-data.multiple-triggers-2.json @@ -0,0 +1,95 @@ +{ + "startData": {}, + "resultData": { + "runData": { + "When chat message received": [ + { + "startTime": 1732882447976, + "executionTime": 0, + "executionStatus": "success", + "data": { + "main": [ + [ + { + "json": { + "sessionId": "192c5b3c0b0642d68eab1a747a59cb6e", + "action": "sendMessage", + "chatInput": "hey" + } + } + ] + ] + }, + "source": [null] + } + ], + "NoOp": [ + { + "hints": [], + "startTime": 1732882448034, + "executionTime": 0, + "source": [ + { + "previousNode": "When clicking ‘Test workflow’" + } + ], + "executionStatus": "success", + "data": { + "main": [ + [ + { + "json": { + "sessionId": "192c5b3c0b0642d68eab1a747a59cb6e", + "action": "sendMessage", + "chatInput": "hey" + }, + "pairedItem": { + "item": 0 + } + } + ] + ] + } + } + ], + "NoOp2": [ + { + "hints": [], + "startTime": 1732882448037, + "executionTime": 0, + "source": [ + { + "previousNode": "NoOp" + } + ], + "executionStatus": "success", + "data": { + "main": [ + [ + { + "json": { + "sessionId": "192c5b3c0b0642d68eab1a747a59cb6e", + "action": "sendMessage", + "chatInput": "hey" + }, + "pairedItem": { + "item": 0 + } + } + ] + ] + } + } + ] + }, + "pinData": {}, + "lastNodeExecuted": "NoOp2" + }, + "executionData": { + "contextData": {}, + "nodeExecutionStack": [], + "metadata": {}, + "waitingExecution": {}, + "waitingExecutionSource": {} + } +} diff --git a/packages/cli/src/evaluation/test-runner/__tests__/mock-data/execution-data.multiple-triggers.json b/packages/cli/src/evaluation/test-runner/__tests__/mock-data/execution-data.multiple-triggers.json new file mode 100644 index 0000000000..ec802fdc31 --- /dev/null +++ b/packages/cli/src/evaluation/test-runner/__tests__/mock-data/execution-data.multiple-triggers.json @@ -0,0 +1,87 @@ +{ + "startData": {}, + "resultData": { + "runData": { + "When clicking ‘Test workflow’": [ + { + "hints": [], + "startTime": 1732882424975, + "executionTime": 0, + "source": [], + "executionStatus": "success", + "data": { + "main": [ + [ + { + "json": {}, + "pairedItem": { + "item": 0 + } + } + ] + ] + } + } + ], + "NoOp": [ + { + "hints": [], + "startTime": 1732882424977, + "executionTime": 1, + "source": [ + { + "previousNode": "When clicking ‘Test workflow’" + } + ], + "executionStatus": "success", + "data": { + "main": [ + [ + { + "json": {}, + "pairedItem": { + "item": 0 + } + } + ] + ] + } + } + ], + "NoOp2": [ + { + "hints": [], + "startTime": 1732882424978, + "executionTime": 0, + "source": [ + { + "previousNode": "NoOp" + } + ], + "executionStatus": "success", + "data": { + "main": [ + [ + { + "json": {}, + "pairedItem": { + "item": 0 + } + } + ] + ] + } + } + ] + }, + "pinData": {}, + "lastNodeExecuted": "NoOp2" + }, + "executionData": { + "contextData": {}, + "nodeExecutionStack": [], + "metadata": {}, + "waitingExecution": {}, + "waitingExecutionSource": {} + } +} diff --git a/packages/cli/src/evaluation/test-runner/__tests__/mock-data/workflow.multiple-triggers.json b/packages/cli/src/evaluation/test-runner/__tests__/mock-data/workflow.multiple-triggers.json new file mode 100644 index 0000000000..73dbf2136f --- /dev/null +++ b/packages/cli/src/evaluation/test-runner/__tests__/mock-data/workflow.multiple-triggers.json @@ -0,0 +1,76 @@ +{ + "name": "Multiple Triggers Workflow", + "nodes": [ + { + "parameters": {}, + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [-20, -120], + "id": "19562c2d-d2c8-45c8-ae0a-1b1effe29817", + "name": "When clicking ‘Test workflow’" + }, + { + "parameters": { + "options": {} + }, + "type": "@n8n/n8n-nodes-langchain.chatTrigger", + "typeVersion": 1.1, + "position": [-20, 120], + "id": "9b4b833b-56f6-4099-9b7d-5e94b75a735c", + "name": "When chat message received", + "webhookId": "8aeccd03-d45f-48d2-a2c7-1fb8c53d2ad7" + }, + { + "parameters": {}, + "type": "n8n-nodes-base.noOp", + "typeVersion": 1, + "position": [260, -20], + "id": "d3ab7426-11e7-4f42-9a57-11b8de019783", + "name": "NoOp" + }, + { + "parameters": {}, + "type": "n8n-nodes-base.noOp", + "typeVersion": 1, + "position": [480, -20], + "id": "fb73bed6-ec2a-4283-b564-c96730b94889", + "name": "NoOp2" + } + ], + "connections": { + "When clicking ‘Test workflow’": { + "main": [ + [ + { + "node": "NoOp", + "type": "main", + "index": 0 + } + ] + ] + }, + "When chat message received": { + "main": [ + [ + { + "node": "NoOp", + "type": "main", + "index": 0 + } + ] + ] + }, + "NoOp": { + "main": [ + [ + { + "node": "NoOp2", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "pinData": {} +} diff --git a/packages/cli/src/evaluation/test-runner/test-runner.service.ee.ts b/packages/cli/src/evaluation/test-runner/test-runner.service.ee.ts index e717742b42..433a84c9dc 100644 --- a/packages/cli/src/evaluation/test-runner/test-runner.service.ee.ts +++ b/packages/cli/src/evaluation/test-runner/test-runner.service.ee.ts @@ -1,9 +1,9 @@ import { parse } from 'flatted'; import type { IDataObject, - IPinData, IRun, IRunData, + IRunExecutionData, IWorkflowExecutionDataProcess, } from 'n8n-workflow'; import assert from 'node:assert'; @@ -17,10 +17,11 @@ import type { WorkflowEntity } from '@/databases/entities/workflow-entity'; import { ExecutionRepository } from '@/databases/repositories/execution.repository'; import { TestRunRepository } from '@/databases/repositories/test-run.repository.ee'; import { WorkflowRepository } from '@/databases/repositories/workflow.repository'; -import type { IExecutionResponse } from '@/interfaces'; import { getRunData } from '@/workflow-execute-additional-data'; import { WorkflowRunner } from '@/workflow-runner'; +import { createPinData, getPastExecutionStartNode } from './utils.ee'; + /** * This service orchestrates the running of test cases. * It uses the test definitions to find @@ -41,43 +42,30 @@ export class TestRunnerService { private readonly testRunRepository: TestRunRepository, ) {} - /** - * Extracts the execution data from the past execution. - * Creates a pin data object from the past execution data - * for the given workflow. - * For now, it only pins trigger nodes. - */ - private createTestDataFromExecution(workflow: WorkflowEntity, execution: ExecutionEntity) { - const executionData = parse(execution.executionData.data) as IExecutionResponse['data']; - - const triggerNodes = workflow.nodes.filter((node) => /trigger$/i.test(node.type)); - - const pinData = {} as IPinData; - - for (const triggerNode of triggerNodes) { - const triggerData = executionData.resultData.runData[triggerNode.name]; - if (triggerData?.[0]?.data?.main?.[0]) { - pinData[triggerNode.name] = triggerData[0]?.data?.main?.[0]; - } - } - - return { pinData, executionData }; - } - /** * Runs a test case with the given pin data. * Waits for the workflow under test to finish execution. */ private async runTestCase( workflow: WorkflowEntity, - testCasePinData: IPinData, + pastExecutionData: IRunExecutionData, userId: string, ): Promise { + // Create pin data from the past execution data + const pinData = createPinData(workflow, pastExecutionData); + + // Determine the start node of the past execution + const pastExecutionStartNode = getPastExecutionStartNode(pastExecutionData); + // Prepare the data to run the workflow const data: IWorkflowExecutionDataProcess = { + destinationNode: pastExecutionData.startData?.destinationNode, + startNodes: pastExecutionStartNode + ? [{ name: pastExecutionStartNode, sourceData: null }] + : undefined, executionMode: 'evaluation', runData: {}, - pinData: testCasePinData, + pinData, workflowData: workflow, partialExecutionVersion: '-1', userId, @@ -178,11 +166,10 @@ export class TestRunnerService { }); assert(pastExecution, 'Execution not found'); - const testData = this.createTestDataFromExecution(workflow, pastExecution); - const { pinData, executionData } = testData; + const executionData = parse(pastExecution.executionData.data) as IRunExecutionData; // Run the test case and wait for it to finish - const testCaseExecution = await this.runTestCase(workflow, pinData, user.id); + const testCaseExecution = await this.runTestCase(workflow, executionData, user.id); // In case of a permission check issue, the test case execution will be undefined. // Skip them and continue with the next test case diff --git a/packages/cli/src/evaluation/test-runner/utils.ee.ts b/packages/cli/src/evaluation/test-runner/utils.ee.ts new file mode 100644 index 0000000000..a6a4dc5ec2 --- /dev/null +++ b/packages/cli/src/evaluation/test-runner/utils.ee.ts @@ -0,0 +1,34 @@ +import type { IRunExecutionData, IPinData } from 'n8n-workflow'; + +import type { WorkflowEntity } from '@/databases/entities/workflow-entity'; + +/** + * Extracts the execution data from the past execution + * and creates a pin data object from it for the given workflow. + * For now, it only pins trigger nodes. + */ +export function createPinData(workflow: WorkflowEntity, executionData: IRunExecutionData) { + const triggerNodes = workflow.nodes.filter((node) => /trigger$/i.test(node.type)); + + const pinData = {} as IPinData; + + for (const triggerNode of triggerNodes) { + const triggerData = executionData.resultData.runData[triggerNode.name]; + if (triggerData?.[0]?.data?.main?.[0]) { + pinData[triggerNode.name] = triggerData[0]?.data?.main?.[0]; + } + } + + return pinData; +} + +/** + * Returns the start node of the past execution. + * The start node is the node that has no source and has run data. + */ +export function getPastExecutionStartNode(executionData: IRunExecutionData) { + return Object.keys(executionData.resultData.runData).find((nodeName) => { + const data = executionData.resultData.runData[nodeName]; + return !data[0].source || data[0].source.length === 0 || data[0].source[0] === null; + }); +}