mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
chore: Get past execution's start and destination nodes while running tests (no-changelog) (#11983)
This commit is contained in:
parent
e68c9da30c
commit
b0e9085ffc
|
@ -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(),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
|
@ -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');
|
||||
});
|
||||
});
|
|
@ -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": {}
|
||||
}
|
||||
}
|
|
@ -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": {}
|
||||
}
|
||||
}
|
|
@ -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": {}
|
||||
}
|
|
@ -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
|
||||
|
|
34
packages/cli/src/evaluation/test-runner/utils.ee.ts
Normal file
34
packages/cli/src/evaluation/test-runner/utils.ee.ts
Normal 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;
|
||||
});
|
||||
}
|
Loading…
Reference in a new issue