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 { parse } from 'flatted';
|
||||||
import type {
|
import type {
|
||||||
IDataObject,
|
IDataObject,
|
||||||
IPinData,
|
|
||||||
IRun,
|
IRun,
|
||||||
IRunData,
|
IRunData,
|
||||||
|
IRunExecutionData,
|
||||||
IWorkflowExecutionDataProcess,
|
IWorkflowExecutionDataProcess,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
import assert from 'node:assert';
|
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 { ExecutionRepository } from '@/databases/repositories/execution.repository';
|
||||||
import { TestRunRepository } from '@/databases/repositories/test-run.repository.ee';
|
import { TestRunRepository } from '@/databases/repositories/test-run.repository.ee';
|
||||||
import { WorkflowRepository } from '@/databases/repositories/workflow.repository';
|
import { WorkflowRepository } from '@/databases/repositories/workflow.repository';
|
||||||
import type { IExecutionResponse } from '@/interfaces';
|
|
||||||
import { getRunData } from '@/workflow-execute-additional-data';
|
import { getRunData } from '@/workflow-execute-additional-data';
|
||||||
import { WorkflowRunner } from '@/workflow-runner';
|
import { WorkflowRunner } from '@/workflow-runner';
|
||||||
|
|
||||||
|
import { createPinData, getPastExecutionStartNode } from './utils.ee';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This service orchestrates the running of test cases.
|
* This service orchestrates the running of test cases.
|
||||||
* It uses the test definitions to find
|
* It uses the test definitions to find
|
||||||
|
@ -41,43 +42,30 @@ export class TestRunnerService {
|
||||||
private readonly testRunRepository: TestRunRepository,
|
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.
|
* Runs a test case with the given pin data.
|
||||||
* Waits for the workflow under test to finish execution.
|
* Waits for the workflow under test to finish execution.
|
||||||
*/
|
*/
|
||||||
private async runTestCase(
|
private async runTestCase(
|
||||||
workflow: WorkflowEntity,
|
workflow: WorkflowEntity,
|
||||||
testCasePinData: IPinData,
|
pastExecutionData: IRunExecutionData,
|
||||||
userId: string,
|
userId: string,
|
||||||
): Promise<IRun | undefined> {
|
): 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
|
// Prepare the data to run the workflow
|
||||||
const data: IWorkflowExecutionDataProcess = {
|
const data: IWorkflowExecutionDataProcess = {
|
||||||
|
destinationNode: pastExecutionData.startData?.destinationNode,
|
||||||
|
startNodes: pastExecutionStartNode
|
||||||
|
? [{ name: pastExecutionStartNode, sourceData: null }]
|
||||||
|
: undefined,
|
||||||
executionMode: 'evaluation',
|
executionMode: 'evaluation',
|
||||||
runData: {},
|
runData: {},
|
||||||
pinData: testCasePinData,
|
pinData,
|
||||||
workflowData: workflow,
|
workflowData: workflow,
|
||||||
partialExecutionVersion: '-1',
|
partialExecutionVersion: '-1',
|
||||||
userId,
|
userId,
|
||||||
|
@ -178,11 +166,10 @@ export class TestRunnerService {
|
||||||
});
|
});
|
||||||
assert(pastExecution, 'Execution not found');
|
assert(pastExecution, 'Execution not found');
|
||||||
|
|
||||||
const testData = this.createTestDataFromExecution(workflow, pastExecution);
|
const executionData = parse(pastExecution.executionData.data) as IRunExecutionData;
|
||||||
const { pinData, executionData } = testData;
|
|
||||||
|
|
||||||
// Run the test case and wait for it to finish
|
// 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.
|
// In case of a permission check issue, the test case execution will be undefined.
|
||||||
// Skip them and continue with the next test case
|
// 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