mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
623 lines
16 KiB
TypeScript
623 lines
16 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
||
import type { INode, IRunExecutionData, NodeConnectionType } from 'n8n-workflow';
|
||
import { useAIAssistantHelpers } from './useAIAssistantHelpers';
|
||
import { createTestingPinia } from '@pinia/testing';
|
||
import { setActivePinia } from 'pinia';
|
||
import type { IWorkflowDb } from '@/Interface';
|
||
import type { ChatRequest } from '@/types/assistant.types';
|
||
import {
|
||
ERROR_HELPER_TEST_PAYLOAD,
|
||
PAYLOAD_SIZE_FOR_1_PASS,
|
||
PAYLOAD_SIZE_FOR_2_PASSES,
|
||
SUPPORT_CHAT_TEST_PAYLOAD,
|
||
} from './useAIAssistantHelpers.test.constants';
|
||
|
||
const referencedNodesTestCases: Array<{ caseName: string; node: INode; expected: string[] }> = [
|
||
{
|
||
caseName: 'Should return an empty array if no referenced nodes',
|
||
node: {
|
||
parameters: {
|
||
curlImport: '',
|
||
method: 'GET',
|
||
url: 'https://httpbin.org/get1',
|
||
authentication: 'none',
|
||
provideSslCertificates: false,
|
||
sendQuery: false,
|
||
sendHeaders: false,
|
||
sendBody: false,
|
||
options: {},
|
||
infoMessage: '',
|
||
},
|
||
type: 'n8n-nodes-base.httpRequest',
|
||
typeVersion: 4.2,
|
||
position: [220, 220],
|
||
id: 'edc36001-aee7-4052-b66e-cf127f4b6ea5',
|
||
name: 'HTTP Request',
|
||
},
|
||
expected: [],
|
||
},
|
||
{
|
||
caseName: 'Should return an array of references for regular node',
|
||
node: {
|
||
parameters: {
|
||
authentication: 'oAuth2',
|
||
resource: 'sheet',
|
||
operation: 'read',
|
||
documentId: {
|
||
__rl: true,
|
||
value: "={{ $('Edit Fields').item.json.document }}",
|
||
mode: 'id',
|
||
},
|
||
sheetName: {
|
||
__rl: true,
|
||
value: "={{ $('Edit Fields 2').item.json.sheet }}",
|
||
mode: 'id',
|
||
},
|
||
filtersUI: {},
|
||
combineFilters: 'AND',
|
||
options: {},
|
||
},
|
||
type: 'n8n-nodes-base.googleSheets',
|
||
typeVersion: 4.5,
|
||
position: [440, 0],
|
||
id: '9a95ad27-06cf-4076-af6b-52846a109a8b',
|
||
name: 'Google Sheets',
|
||
credentials: {
|
||
googleSheetsOAuth2Api: {
|
||
id: '8QEpi028oHDLXntS',
|
||
name: 'milorad@n8n.io',
|
||
},
|
||
},
|
||
},
|
||
expected: ['Edit Fields', 'Edit Fields 2'],
|
||
},
|
||
{
|
||
caseName: 'Should return an array of references for set node',
|
||
node: {
|
||
parameters: {
|
||
mode: 'manual',
|
||
duplicateItem: false,
|
||
assignments: {
|
||
assignments: [
|
||
{
|
||
id: '135e0eb0-f412-430d-8990-731c57cf43ae',
|
||
name: 'document',
|
||
value: "={{ $('Edit Fields 2').item.json.document}}",
|
||
type: 'string',
|
||
typeVersion: 1,
|
||
},
|
||
{
|
||
parameters: {},
|
||
id: 'b5942df6-0160-4ef7-965d-57583acdc8aa',
|
||
name: 'Replace me with your logic',
|
||
type: 'n8n-nodes-base.noOp',
|
||
position: [520, 340],
|
||
typeVersion: 1,
|
||
},
|
||
],
|
||
},
|
||
includeOtherFields: false,
|
||
options: {},
|
||
},
|
||
type: 'n8n-nodes-base.set',
|
||
typeVersion: 3.4,
|
||
position: [560, -140],
|
||
id: '7306745f-ba8c-451d-ae1a-c627f60fbdd3',
|
||
name: 'Edit Fields 2',
|
||
},
|
||
expected: ['Edit Fields 2'],
|
||
},
|
||
{
|
||
caseName: 'Should handle expressions with single quotes, double quotes and backticks',
|
||
node: {
|
||
parameters: {
|
||
authentication: 'oAuth2',
|
||
resource: 'sheet',
|
||
operation: 'read',
|
||
documentId: {
|
||
__rl: true,
|
||
value: "={{ $('Edit Fields').item.json.document }}",
|
||
mode: 'id',
|
||
},
|
||
sheetName: {
|
||
__rl: true,
|
||
value: '={{ $("Edit Fields 2").item.json.sheet }}',
|
||
mode: 'id',
|
||
},
|
||
rowName: {
|
||
__rl: true,
|
||
value: '={{ $(`Edit Fields 3`).item.json.row }}',
|
||
mode: 'id',
|
||
},
|
||
filtersUI: {},
|
||
combineFilters: 'AND',
|
||
options: {},
|
||
},
|
||
type: 'n8n-nodes-base.googleSheets',
|
||
typeVersion: 4.5,
|
||
position: [440, 0],
|
||
id: '9a95ad27-06cf-4076-af6b-52846a109a8b',
|
||
name: 'Google Sheets',
|
||
credentials: {
|
||
googleSheetsOAuth2Api: {
|
||
id: '8QEpi028oHDLXntS',
|
||
name: 'milorad@n8n.io',
|
||
},
|
||
},
|
||
},
|
||
expected: ['Edit Fields', 'Edit Fields 2', 'Edit Fields 3'],
|
||
},
|
||
{
|
||
caseName: 'Should only add one reference for each referenced node',
|
||
node: {
|
||
parameters: {
|
||
authentication: 'oAuth2',
|
||
resource: 'sheet',
|
||
operation: 'read',
|
||
documentId: {
|
||
__rl: true,
|
||
value: "={{ $('Edit Fields').item.json.document }}",
|
||
mode: 'id',
|
||
},
|
||
sheetName: {
|
||
__rl: true,
|
||
value: "={{ $('Edit Fields').item.json.sheet }}",
|
||
mode: 'id',
|
||
},
|
||
filtersUI: {},
|
||
combineFilters: 'AND',
|
||
options: {},
|
||
},
|
||
type: 'n8n-nodes-base.googleSheets',
|
||
typeVersion: 4.5,
|
||
position: [440, 0],
|
||
id: '9a95ad27-06cf-4076-af6b-52846a109a8b',
|
||
name: 'Google Sheets',
|
||
credentials: {
|
||
googleSheetsOAuth2Api: {
|
||
id: '8QEpi028oHDLXntS',
|
||
name: 'milorad@n8n.io',
|
||
},
|
||
},
|
||
},
|
||
expected: ['Edit Fields'],
|
||
},
|
||
{
|
||
caseName: 'Should handle multiple node references in one expression',
|
||
node: {
|
||
parameters: {
|
||
curlImport: '',
|
||
method: 'GET',
|
||
url: "={{ $('Edit Fields').item.json.one }} {{ $('Edit Fields 2').item.json.two }} {{ $('Edit Fields').item.json.three }}",
|
||
authentication: 'none',
|
||
provideSslCertificates: false,
|
||
sendQuery: false,
|
||
sendHeaders: false,
|
||
sendBody: false,
|
||
options: {},
|
||
infoMessage: '',
|
||
},
|
||
type: 'n8n-nodes-base.httpRequest',
|
||
typeVersion: 4.2,
|
||
position: [220, 220],
|
||
id: 'edc36001-aee7-4052-b66e-cf127f4b6ea5',
|
||
name: 'HTTP Request',
|
||
},
|
||
expected: ['Edit Fields', 'Edit Fields 2'],
|
||
},
|
||
{
|
||
caseName: 'Should respect whitespace around node references',
|
||
node: {
|
||
parameters: {
|
||
curlImport: '',
|
||
method: 'GET',
|
||
url: "={{ $(' Edit Fields ').item.json.one }}",
|
||
authentication: 'none',
|
||
provideSslCertificates: false,
|
||
sendQuery: false,
|
||
sendHeaders: false,
|
||
sendBody: false,
|
||
options: {},
|
||
infoMessage: '',
|
||
},
|
||
type: 'n8n-nodes-base.httpRequest',
|
||
typeVersion: 4.2,
|
||
position: [220, 220],
|
||
id: 'edc36001-aee7-4052-b66e-cf127f4b6ea5',
|
||
name: 'HTTP Request',
|
||
},
|
||
expected: [' Edit Fields '],
|
||
},
|
||
{
|
||
caseName: 'Should ignore whitespace inside expressions',
|
||
node: {
|
||
parameters: {
|
||
curlImport: '',
|
||
method: 'GET',
|
||
url: "={{ $( 'Edit Fields' ).item.json.one }}",
|
||
authentication: 'none',
|
||
provideSslCertificates: false,
|
||
sendQuery: false,
|
||
sendHeaders: false,
|
||
sendBody: false,
|
||
options: {},
|
||
infoMessage: '',
|
||
},
|
||
type: 'n8n-nodes-base.httpRequest',
|
||
typeVersion: 4.2,
|
||
position: [220, 220],
|
||
id: 'edc36001-aee7-4052-b66e-cf127f4b6ea5',
|
||
name: 'HTTP Request',
|
||
},
|
||
expected: ['Edit Fields'],
|
||
},
|
||
{
|
||
caseName: 'Should ignore special characters in node references',
|
||
node: {
|
||
parameters: {
|
||
curlImport: '',
|
||
method: 'GET',
|
||
url: "={{ $( 'Ignore ' this' ).item.json.document }",
|
||
authentication: 'none',
|
||
provideSslCertificates: false,
|
||
sendQuery: false,
|
||
sendHeaders: false,
|
||
sendBody: false,
|
||
options: {},
|
||
infoMessage: '',
|
||
},
|
||
type: 'n8n-nodes-base.httpRequest',
|
||
typeVersion: 4.2,
|
||
position: [220, 220],
|
||
id: 'edc36001-aee7-4052-b66e-cf127f4b6ea5',
|
||
name: 'HTTP Request',
|
||
},
|
||
expected: [],
|
||
},
|
||
{
|
||
caseName: 'Should correctly detect node names that contain single quotes',
|
||
node: {
|
||
parameters: {
|
||
curlImport: '',
|
||
method: 'GET',
|
||
// In order to carry over backslashes to test function, the string needs to be double escaped
|
||
url: "={{ $('Edit \\'Fields\\' 2').item.json.name }}",
|
||
authentication: 'none',
|
||
provideSslCertificates: false,
|
||
sendQuery: false,
|
||
sendHeaders: false,
|
||
sendBody: false,
|
||
options: {},
|
||
infoMessage: '',
|
||
},
|
||
type: 'n8n-nodes-base.httpRequest',
|
||
typeVersion: 4.2,
|
||
position: [220, 220],
|
||
id: 'edc36001-aee7-4052-b66e-cf127f4b6ea5',
|
||
name: 'HTTP Request',
|
||
},
|
||
expected: ["Edit 'Fields' 2"],
|
||
},
|
||
{
|
||
caseName: 'Should correctly detect node names with inner backticks',
|
||
node: {
|
||
parameters: {
|
||
curlImport: '',
|
||
method: 'GET',
|
||
url: "={{ $('Edit `Fields` 2').item.json.name }}",
|
||
authentication: 'none',
|
||
provideSslCertificates: false,
|
||
sendQuery: false,
|
||
sendHeaders: false,
|
||
sendBody: false,
|
||
options: {},
|
||
infoMessage: '',
|
||
},
|
||
type: 'n8n-nodes-base.httpRequest',
|
||
typeVersion: 4.2,
|
||
position: [220, 220],
|
||
id: 'edc36001-aee7-4052-b66e-cf127f4b6ea5',
|
||
name: 'HTTP Request',
|
||
},
|
||
expected: ['Edit `Fields` 2'],
|
||
},
|
||
{
|
||
caseName: 'Should correctly detect node names with inner escaped backticks',
|
||
node: {
|
||
parameters: {
|
||
curlImport: '',
|
||
method: 'GET',
|
||
url: '={{ $(`Edit \\`Fields\\` 2`).item.json.name }}',
|
||
authentication: 'none',
|
||
provideSslCertificates: false,
|
||
sendQuery: false,
|
||
sendHeaders: false,
|
||
sendBody: false,
|
||
options: {},
|
||
infoMessage: '',
|
||
},
|
||
type: 'n8n-nodes-base.httpRequest',
|
||
typeVersion: 4.2,
|
||
position: [220, 220],
|
||
id: 'edc36001-aee7-4052-b66e-cf127f4b6ea5',
|
||
name: 'HTTP Request',
|
||
},
|
||
expected: ['Edit `Fields` 2'],
|
||
},
|
||
{
|
||
caseName: 'Should correctly detect node names with inner escaped double quotes',
|
||
node: {
|
||
parameters: {
|
||
curlImport: '',
|
||
method: 'GET',
|
||
// In order to carry over backslashes to test function, the string needs to be double escaped
|
||
url: '={{ $("Edit \\"Fields\\" 2").item.json.name }}',
|
||
authentication: 'none',
|
||
provideSslCertificates: false,
|
||
sendQuery: false,
|
||
sendHeaders: false,
|
||
sendBody: false,
|
||
options: {},
|
||
infoMessage: '',
|
||
},
|
||
type: 'n8n-nodes-base.httpRequest',
|
||
typeVersion: 4.2,
|
||
position: [220, 220],
|
||
id: 'edc36001-aee7-4052-b66e-cf127f4b6ea5',
|
||
name: 'HTTP Request',
|
||
},
|
||
expected: ['Edit "Fields" 2'],
|
||
},
|
||
{
|
||
caseName: 'Should not detect invalid expressions',
|
||
node: {
|
||
parameters: {
|
||
curlImport: '',
|
||
method: 'GET',
|
||
// String not closed properly
|
||
url: "={{ $('Edit ' fields').item.json.document }",
|
||
// Mixed quotes
|
||
url2: '{{ $("Edit \'Fields" 2").item.json.name }}',
|
||
url3: '{{ $("Edit `Fields" 2").item.json.name }}',
|
||
// Quotes not escaped
|
||
url4: '{{ $("Edit "Fields" 2").item.json.name }}',
|
||
url5: "{{ $('Edit 'Fields' 2').item.json.name }}",
|
||
url6: '{{ $(`Edit `Fields` 2`).item.json.name }}',
|
||
},
|
||
type: 'n8n-nodes-base.httpRequest',
|
||
typeVersion: 4.2,
|
||
position: [220, 220],
|
||
id: 'edc36001-aee7-4052-b66e-cf127f4b6ea5',
|
||
name: 'HTTP Request',
|
||
},
|
||
expected: [],
|
||
},
|
||
];
|
||
|
||
const testWorkflow: IWorkflowDb = {
|
||
id: 'MokOcBHON6KkPq6Y',
|
||
name: 'My Sub-Workflow 3',
|
||
active: false,
|
||
createdAt: -1,
|
||
updatedAt: -1,
|
||
connections: {
|
||
'Execute Workflow Trigger': {
|
||
main: [
|
||
[
|
||
{
|
||
node: 'Replace me with your logic',
|
||
type: 'main' as NodeConnectionType,
|
||
index: 0,
|
||
},
|
||
],
|
||
],
|
||
},
|
||
},
|
||
nodes: [
|
||
{
|
||
parameters: {
|
||
notice: '',
|
||
events: 'worklfow_call',
|
||
},
|
||
id: 'c055762a-8fe7-4141-a639-df2372f30060',
|
||
name: 'Execute Workflow Trigger',
|
||
type: 'n8n-nodes-base.executeWorkflowTrigger',
|
||
position: [260, 340],
|
||
typeVersion: 0,
|
||
},
|
||
{
|
||
parameters: {},
|
||
id: 'b5942df6-0160-4ef7-965d-57583acdc8aa',
|
||
name: 'Replace me with your logic',
|
||
type: 'n8n-nodes-base.noOp',
|
||
position: [520, 340],
|
||
typeVersion: 1,
|
||
},
|
||
],
|
||
settings: {
|
||
executionOrder: 'v1',
|
||
},
|
||
tags: [],
|
||
pinData: {},
|
||
versionId: '9f3263e3-d23d-4cc8-bff0-0fdecfbd82bf',
|
||
usedCredentials: [],
|
||
scopes: [
|
||
'workflow:create',
|
||
'workflow:delete',
|
||
'workflow:execute',
|
||
'workflow:list',
|
||
'workflow:move',
|
||
'workflow:read',
|
||
'workflow:share',
|
||
'workflow:update',
|
||
],
|
||
sharedWithProjects: [],
|
||
};
|
||
|
||
const testExecutionData: IRunExecutionData['resultData'] = {
|
||
runData: {
|
||
'When clicking ‘Test workflow’': [
|
||
{
|
||
hints: [],
|
||
startTime: 1732882780588,
|
||
executionTime: 4,
|
||
source: [],
|
||
executionStatus: 'success',
|
||
data: {
|
||
main: [
|
||
[
|
||
{
|
||
json: {},
|
||
pairedItem: {
|
||
item: 0,
|
||
},
|
||
},
|
||
],
|
||
],
|
||
},
|
||
},
|
||
],
|
||
'Edit Fields': [
|
||
{
|
||
hints: [],
|
||
startTime: 1732882780593,
|
||
executionTime: 0,
|
||
source: [
|
||
{
|
||
previousNode: 'When clicking ‘Test workflow’',
|
||
},
|
||
],
|
||
executionStatus: 'success',
|
||
data: {
|
||
main: [
|
||
[
|
||
{
|
||
json: {
|
||
something: 'here',
|
||
},
|
||
pairedItem: {
|
||
item: 0,
|
||
},
|
||
},
|
||
],
|
||
],
|
||
},
|
||
},
|
||
],
|
||
},
|
||
pinData: {},
|
||
lastNodeExecuted: 'Edit Fields',
|
||
};
|
||
|
||
describe.each(referencedNodesTestCases)('getReferencedNodes', (testCase) => {
|
||
let aiAssistantHelpers: ReturnType<typeof useAIAssistantHelpers>;
|
||
|
||
beforeEach(() => {
|
||
setActivePinia(createTestingPinia());
|
||
aiAssistantHelpers = useAIAssistantHelpers();
|
||
});
|
||
|
||
const caseName = testCase.caseName;
|
||
it(`${caseName}`, () => {
|
||
expect(aiAssistantHelpers.getReferencedNodes(testCase.node)).toEqual(testCase.expected);
|
||
});
|
||
});
|
||
|
||
describe('Simplify assistant payloads', () => {
|
||
let aiAssistantHelpers: ReturnType<typeof useAIAssistantHelpers>;
|
||
|
||
beforeEach(() => {
|
||
setActivePinia(createTestingPinia());
|
||
aiAssistantHelpers = useAIAssistantHelpers();
|
||
});
|
||
|
||
it('simplifyWorkflowForAssistant: Should remove unnecessary properties from workflow object', () => {
|
||
const simplifiedWorkflow = aiAssistantHelpers.simplifyWorkflowForAssistant(testWorkflow);
|
||
const removedProperties = [
|
||
'createdAt',
|
||
'updatedAt',
|
||
'settings',
|
||
'versionId',
|
||
'usedCredentials',
|
||
'sharedWithProjects',
|
||
'pinData',
|
||
'scopes',
|
||
'tags',
|
||
];
|
||
removedProperties.forEach((property) => {
|
||
expect(simplifiedWorkflow).not.toHaveProperty(property);
|
||
});
|
||
});
|
||
|
||
it('simplifyResultData: Should remove data from nodes', () => {
|
||
const simplifiedResultData = aiAssistantHelpers.simplifyResultData(testExecutionData);
|
||
for (const nodeName of Object.keys(simplifiedResultData.runData)) {
|
||
expect(simplifiedResultData.runData[nodeName][0]).not.toHaveProperty('data');
|
||
}
|
||
});
|
||
});
|
||
|
||
describe('Trim Payload Size', () => {
|
||
let aiAssistantHelpers: ReturnType<typeof useAIAssistantHelpers>;
|
||
|
||
beforeEach(() => {
|
||
setActivePinia(createTestingPinia());
|
||
aiAssistantHelpers = useAIAssistantHelpers();
|
||
});
|
||
|
||
it('Should trim active node parameters in error helper payload', () => {
|
||
const payload = ERROR_HELPER_TEST_PAYLOAD;
|
||
aiAssistantHelpers.trimPayloadSize(payload);
|
||
expect((payload.payload as ChatRequest.InitErrorHelper).node.parameters).toEqual({});
|
||
});
|
||
|
||
it('Should trim all node parameters in support chat', () => {
|
||
// Testing the scenario where only one trimming pass is needed
|
||
// (payload is under the limit after removing all node parameters and execution data)
|
||
const payload: ChatRequest.RequestPayload = SUPPORT_CHAT_TEST_PAYLOAD;
|
||
const supportPayload: ChatRequest.InitSupportChat =
|
||
payload.payload as ChatRequest.InitSupportChat;
|
||
|
||
// Trimming to 4kb should be successful
|
||
expect(() =>
|
||
aiAssistantHelpers.trimPayloadSize(payload, PAYLOAD_SIZE_FOR_1_PASS),
|
||
).not.toThrow();
|
||
// All active node parameters should be removed
|
||
expect(supportPayload?.context?.activeNodeInfo?.node?.parameters).toEqual({});
|
||
// Also, all node parameters in the workflow should be removed
|
||
supportPayload.context?.currentWorkflow?.nodes?.forEach((node) => {
|
||
expect(node.parameters).toEqual({});
|
||
});
|
||
// Node parameters in the execution data should be removed
|
||
expect(supportPayload.context?.executionData?.runData).toEqual({});
|
||
if (
|
||
supportPayload.context?.executionData?.error &&
|
||
'node' in supportPayload.context.executionData.error
|
||
) {
|
||
expect(supportPayload.context?.executionData?.error?.node?.parameters).toEqual({});
|
||
}
|
||
// Context object should still be there
|
||
expect(supportPayload.context).to.be.an('object');
|
||
});
|
||
|
||
it('Should trim the whole context in support chat', () => {
|
||
// Testing the scenario where both trimming passes are needed
|
||
// (payload is over the limit after removing all node parameters and execution data)
|
||
const payload: ChatRequest.RequestPayload = SUPPORT_CHAT_TEST_PAYLOAD;
|
||
const supportPayload: ChatRequest.InitSupportChat =
|
||
payload.payload as ChatRequest.InitSupportChat;
|
||
|
||
// Trimming should be successful
|
||
expect(() =>
|
||
aiAssistantHelpers.trimPayloadSize(payload, PAYLOAD_SIZE_FOR_2_PASSES),
|
||
).not.toThrow();
|
||
// The whole context object should be removed
|
||
expect(supportPayload.context).not.toBeDefined();
|
||
});
|
||
|
||
it('Should throw an error if payload is too big after trimming', () => {
|
||
const payload = ERROR_HELPER_TEST_PAYLOAD;
|
||
expect(() => aiAssistantHelpers.trimPayloadSize(payload, 0.2)).toThrow();
|
||
});
|
||
});
|