chore: Get past execution's start and destination nodes while running tests (no-changelog) (#11983)

This commit is contained in:
Eugene 2024-12-02 13:52:09 +01:00 committed by GitHub
parent e68c9da30c
commit b0e9085ffc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 373 additions and 30 deletions

View file

@ -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(),
}),
);
});
});

View file

@ -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');
});
});

View file

@ -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": {}
}
}

View file

@ -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": {}
}
}

View file

@ -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": {}
}

View file

@ -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<IRun | undefined> {
// 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

View file

@ -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;
});
}