mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-18 02:21:13 +00:00
640 lines
17 KiB
TypeScript
640 lines
17 KiB
TypeScript
import FormData from 'form-data';
|
|
import get from 'lodash/get';
|
|
import type { IDataObject, IExecuteFunctions } from 'n8n-workflow';
|
|
|
|
import * as assistant from '../actions/assistant';
|
|
import * as audio from '../actions/audio';
|
|
import * as file from '../actions/file';
|
|
import * as image from '../actions/image';
|
|
import * as text from '../actions/text';
|
|
import * as transport from '../transport';
|
|
|
|
const createExecuteFunctionsMock = (parameters: IDataObject) => {
|
|
const nodeParameters = parameters;
|
|
return {
|
|
getExecutionCancelSignal() {
|
|
return new AbortController().signal;
|
|
},
|
|
getNodeParameter(parameter: string) {
|
|
return get(nodeParameters, parameter);
|
|
},
|
|
getNode() {
|
|
return {};
|
|
},
|
|
getInputConnectionData() {
|
|
return undefined;
|
|
},
|
|
helpers: {
|
|
prepareBinaryData() {
|
|
return {};
|
|
},
|
|
assertBinaryData() {
|
|
return {
|
|
filename: 'filenale.flac',
|
|
contentType: 'audio/flac',
|
|
};
|
|
},
|
|
getBinaryDataBuffer() {
|
|
return 'data buffer data';
|
|
},
|
|
},
|
|
} as unknown as IExecuteFunctions;
|
|
};
|
|
|
|
describe('OpenAi, Assistant resource', () => {
|
|
beforeEach(() => {
|
|
(transport as any).apiRequest = jest.fn();
|
|
});
|
|
|
|
it('create => should throw an error if an assistant with the same name already exists', async () => {
|
|
(transport.apiRequest as jest.Mock).mockResolvedValueOnce({
|
|
data: [{ name: 'name' }],
|
|
has_more: false,
|
|
});
|
|
|
|
try {
|
|
await assistant.create.execute.call(
|
|
createExecuteFunctionsMock({
|
|
name: 'name',
|
|
options: {
|
|
failIfExists: true,
|
|
},
|
|
}),
|
|
0,
|
|
);
|
|
expect(true).toBe(false);
|
|
} catch (error) {
|
|
expect(error.message).toBe("An assistant with the same name 'name' already exists");
|
|
}
|
|
});
|
|
|
|
it('create => should call apiRequest with correct parameters', async () => {
|
|
(transport.apiRequest as jest.Mock).mockResolvedValueOnce({});
|
|
|
|
await assistant.create.execute.call(
|
|
createExecuteFunctionsMock({
|
|
modelId: 'gpt-model',
|
|
name: 'name',
|
|
description: 'description',
|
|
instructions: 'some instructions',
|
|
codeInterpreter: true,
|
|
knowledgeRetrieval: true,
|
|
file_ids: [],
|
|
options: {},
|
|
}),
|
|
0,
|
|
);
|
|
|
|
expect(transport.apiRequest).toHaveBeenCalledWith('POST', '/assistants', {
|
|
body: {
|
|
description: 'description',
|
|
instructions: 'some instructions',
|
|
model: 'gpt-model',
|
|
name: 'name',
|
|
tool_resources: {
|
|
code_interpreter: {
|
|
file_ids: [],
|
|
},
|
|
file_search: {
|
|
vector_stores: [
|
|
{
|
|
file_ids: [],
|
|
},
|
|
],
|
|
},
|
|
},
|
|
tools: [{ type: 'code_interpreter' }, { type: 'file_search' }],
|
|
},
|
|
headers: { 'OpenAI-Beta': 'assistants=v2' },
|
|
});
|
|
});
|
|
|
|
it('create => should throw error if more then 20 files selected', async () => {
|
|
(transport.apiRequest as jest.Mock).mockResolvedValueOnce({});
|
|
|
|
try {
|
|
await assistant.create.execute.call(
|
|
createExecuteFunctionsMock({
|
|
file_ids: Array.from({ length: 25 }),
|
|
options: {},
|
|
}),
|
|
0,
|
|
);
|
|
expect(true).toBe(false);
|
|
} catch (error) {
|
|
expect(error.message).toBe(
|
|
'The maximum number of files that can be attached to the assistant is 20',
|
|
);
|
|
}
|
|
});
|
|
|
|
it('delete => should call apiRequest with correct parameters', async () => {
|
|
(transport.apiRequest as jest.Mock).mockResolvedValueOnce({});
|
|
|
|
await assistant.deleteAssistant.execute.call(
|
|
createExecuteFunctionsMock({
|
|
assistantId: 'assistant-id',
|
|
}),
|
|
0,
|
|
);
|
|
|
|
expect(transport.apiRequest).toHaveBeenCalledWith('DELETE', '/assistants/assistant-id', {
|
|
headers: { 'OpenAI-Beta': 'assistants=v2' },
|
|
});
|
|
});
|
|
|
|
it('list => should call apiRequest with correct parameters', async () => {
|
|
(transport.apiRequest as jest.Mock).mockResolvedValueOnce({
|
|
data: [
|
|
{ name: 'name1', id: 'id-1', model: 'gpt-model', other: 'other' },
|
|
{ name: 'name2', id: 'id-2', model: 'gpt-model', other: 'other' },
|
|
{ name: 'name3', id: 'id-3', model: 'gpt-model', other: 'other' },
|
|
],
|
|
has_more: false,
|
|
});
|
|
|
|
const response = await assistant.list.execute.call(
|
|
createExecuteFunctionsMock({
|
|
simplify: true,
|
|
}),
|
|
0,
|
|
);
|
|
|
|
expect(response).toEqual([
|
|
{
|
|
json: { name: 'name1', id: 'id-1', model: 'gpt-model' },
|
|
pairedItem: { item: 0 },
|
|
},
|
|
{
|
|
json: { name: 'name2', id: 'id-2', model: 'gpt-model' },
|
|
pairedItem: { item: 0 },
|
|
},
|
|
{
|
|
json: { name: 'name3', id: 'id-3', model: 'gpt-model' },
|
|
pairedItem: { item: 0 },
|
|
},
|
|
]);
|
|
});
|
|
|
|
it('update => should call apiRequest with correct parameters', async () => {
|
|
(transport.apiRequest as jest.Mock).mockResolvedValueOnce({
|
|
tools: [{ type: 'existing_tool' }],
|
|
});
|
|
(transport.apiRequest as jest.Mock).mockResolvedValueOnce({});
|
|
|
|
await assistant.update.execute.call(
|
|
createExecuteFunctionsMock({
|
|
assistantId: 'assistant-id',
|
|
options: {
|
|
modelId: 'gpt-model',
|
|
name: 'name',
|
|
instructions: 'some instructions',
|
|
codeInterpreter: true,
|
|
knowledgeRetrieval: true,
|
|
file_ids: [],
|
|
removeCustomTools: false,
|
|
},
|
|
}),
|
|
0,
|
|
);
|
|
|
|
expect(transport.apiRequest).toHaveBeenCalledTimes(2);
|
|
expect(transport.apiRequest).toHaveBeenCalledWith('GET', '/assistants/assistant-id', {
|
|
headers: { 'OpenAI-Beta': 'assistants=v2' },
|
|
});
|
|
expect(transport.apiRequest).toHaveBeenCalledWith('POST', '/assistants/assistant-id', {
|
|
body: {
|
|
instructions: 'some instructions',
|
|
model: 'gpt-model',
|
|
name: 'name',
|
|
tool_resources: {
|
|
code_interpreter: {
|
|
file_ids: [],
|
|
},
|
|
},
|
|
tools: [{ type: 'existing_tool' }, { type: 'code_interpreter' }, { type: 'file_search' }],
|
|
},
|
|
headers: { 'OpenAI-Beta': 'assistants=v2' },
|
|
});
|
|
});
|
|
|
|
it('update => should call apiRequest with file_ids as an array for search', async () => {
|
|
(transport.apiRequest as jest.Mock).mockResolvedValueOnce({
|
|
tools: [{ type: 'existing_tool' }],
|
|
});
|
|
(transport.apiRequest as jest.Mock).mockResolvedValueOnce({});
|
|
|
|
await assistant.update.execute.call(
|
|
createExecuteFunctionsMock({
|
|
assistantId: 'assistant-id',
|
|
options: {
|
|
modelId: 'gpt-model',
|
|
name: 'name',
|
|
instructions: 'some instructions',
|
|
codeInterpreter: true,
|
|
knowledgeRetrieval: true,
|
|
file_ids: ['1234'],
|
|
removeCustomTools: false,
|
|
},
|
|
}),
|
|
0,
|
|
);
|
|
|
|
expect(transport.apiRequest).toHaveBeenCalledTimes(2);
|
|
expect(transport.apiRequest).toHaveBeenCalledWith('GET', '/assistants/assistant-id', {
|
|
headers: { 'OpenAI-Beta': 'assistants=v2' },
|
|
});
|
|
expect(transport.apiRequest).toHaveBeenCalledWith('POST', '/assistants/assistant-id', {
|
|
body: {
|
|
instructions: 'some instructions',
|
|
model: 'gpt-model',
|
|
name: 'name',
|
|
tool_resources: {
|
|
code_interpreter: {
|
|
file_ids: ['1234'],
|
|
},
|
|
},
|
|
tools: [{ type: 'existing_tool' }, { type: 'code_interpreter' }, { type: 'file_search' }],
|
|
},
|
|
headers: { 'OpenAI-Beta': 'assistants=v2' },
|
|
});
|
|
});
|
|
|
|
it('update => should call apiRequest with file_ids as strings for search', async () => {
|
|
(transport.apiRequest as jest.Mock).mockResolvedValueOnce({
|
|
tools: [{ type: 'existing_tool' }],
|
|
});
|
|
(transport.apiRequest as jest.Mock).mockResolvedValueOnce({});
|
|
|
|
await assistant.update.execute.call(
|
|
createExecuteFunctionsMock({
|
|
assistantId: 'assistant-id',
|
|
options: {
|
|
modelId: 'gpt-model',
|
|
name: 'name',
|
|
instructions: 'some instructions',
|
|
codeInterpreter: true,
|
|
knowledgeRetrieval: true,
|
|
file_ids: '1234, 5678, 90',
|
|
removeCustomTools: false,
|
|
},
|
|
}),
|
|
0,
|
|
);
|
|
|
|
expect(transport.apiRequest).toHaveBeenCalledTimes(2);
|
|
expect(transport.apiRequest).toHaveBeenCalledWith('GET', '/assistants/assistant-id', {
|
|
headers: { 'OpenAI-Beta': 'assistants=v2' },
|
|
});
|
|
expect(transport.apiRequest).toHaveBeenCalledWith('POST', '/assistants/assistant-id', {
|
|
body: {
|
|
instructions: 'some instructions',
|
|
model: 'gpt-model',
|
|
name: 'name',
|
|
tool_resources: {
|
|
code_interpreter: {
|
|
file_ids: ['1234', '5678', '90'],
|
|
},
|
|
},
|
|
tools: [{ type: 'existing_tool' }, { type: 'code_interpreter' }, { type: 'file_search' }],
|
|
},
|
|
headers: { 'OpenAI-Beta': 'assistants=v2' },
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('OpenAi, Audio resource', () => {
|
|
beforeEach(() => {
|
|
(transport as any).apiRequest = jest.fn();
|
|
});
|
|
|
|
it('generate => should call apiRequest with correct parameters', async () => {
|
|
(transport.apiRequest as jest.Mock).mockResolvedValueOnce({});
|
|
|
|
const returnData = await audio.generate.execute.call(
|
|
createExecuteFunctionsMock({
|
|
model: 'tts-model',
|
|
input: 'input',
|
|
voice: 'fable',
|
|
options: {
|
|
response_format: 'flac',
|
|
speed: 1.25,
|
|
binaryPropertyOutput: 'myData',
|
|
},
|
|
}),
|
|
0,
|
|
);
|
|
|
|
expect(returnData.length).toEqual(1);
|
|
expect(returnData[0].binary?.myData).toBeDefined();
|
|
expect(returnData[0].pairedItem).toBeDefined();
|
|
|
|
expect(transport.apiRequest).toHaveBeenCalledWith('POST', '/audio/speech', {
|
|
body: {
|
|
input: 'input',
|
|
model: 'tts-model',
|
|
response_format: 'flac',
|
|
speed: 1.25,
|
|
voice: 'fable',
|
|
},
|
|
option: { encoding: 'arraybuffer', json: false, returnFullResponse: true, useStream: true },
|
|
});
|
|
});
|
|
|
|
it('transcribe => should call apiRequest with correct parameters', async () => {
|
|
(transport.apiRequest as jest.Mock).mockResolvedValueOnce({ text: 'transcribtion' });
|
|
|
|
const returnData = await audio.transcribe.execute.call(
|
|
createExecuteFunctionsMock({
|
|
binaryPropertyName: 'myData',
|
|
options: {
|
|
language: 'en',
|
|
temperature: 1.1,
|
|
},
|
|
}),
|
|
0,
|
|
);
|
|
|
|
expect(returnData.length).toEqual(1);
|
|
expect(returnData[0].pairedItem).toBeDefined();
|
|
expect(returnData[0].json).toEqual({ text: 'transcribtion' });
|
|
|
|
expect(transport.apiRequest).toHaveBeenCalledWith(
|
|
'POST',
|
|
'/audio/transcriptions',
|
|
expect.objectContaining({
|
|
headers: expect.objectContaining({
|
|
'content-type': expect.stringMatching(/^multipart\/form-data; boundary=/),
|
|
}),
|
|
option: expect.objectContaining({
|
|
formData: expect.any(FormData),
|
|
}),
|
|
}),
|
|
);
|
|
});
|
|
|
|
it('translate => should call apiRequest with correct parameters', async () => {
|
|
(transport.apiRequest as jest.Mock).mockResolvedValueOnce({ text: 'translations' });
|
|
|
|
const returnData = await audio.translate.execute.call(
|
|
createExecuteFunctionsMock({
|
|
binaryPropertyName: 'myData',
|
|
options: {},
|
|
}),
|
|
0,
|
|
);
|
|
|
|
expect(returnData.length).toEqual(1);
|
|
expect(returnData[0].pairedItem).toBeDefined();
|
|
expect(returnData[0].json).toEqual({ text: 'translations' });
|
|
|
|
expect(transport.apiRequest).toHaveBeenCalledWith(
|
|
'POST',
|
|
'/audio/translations',
|
|
expect.objectContaining({
|
|
headers: expect.objectContaining({
|
|
'content-type': expect.stringMatching(/^multipart\/form-data; boundary=/),
|
|
}),
|
|
option: expect.objectContaining({
|
|
formData: expect.any(FormData),
|
|
}),
|
|
}),
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('OpenAi, File resource', () => {
|
|
beforeEach(() => {
|
|
(transport as any).apiRequest = jest.fn();
|
|
});
|
|
|
|
it('deleteFile => should call apiRequest with correct parameters', async () => {
|
|
(transport.apiRequest as jest.Mock).mockResolvedValueOnce({});
|
|
|
|
await file.deleteFile.execute.call(
|
|
createExecuteFunctionsMock({
|
|
fileId: 'file-id',
|
|
}),
|
|
0,
|
|
);
|
|
|
|
expect(transport.apiRequest).toHaveBeenCalledWith('DELETE', '/files/file-id');
|
|
});
|
|
|
|
it('list => should return list of files', async () => {
|
|
(transport.apiRequest as jest.Mock).mockResolvedValueOnce({
|
|
data: [{ file: 'file1' }, { file: 'file2' }, { file: 'file3' }],
|
|
});
|
|
|
|
const returnData = await file.list.execute.call(createExecuteFunctionsMock({ options: {} }), 2);
|
|
|
|
expect(returnData.length).toEqual(3);
|
|
expect(returnData).toEqual([
|
|
{
|
|
json: { file: 'file1' },
|
|
pairedItem: { item: 2 },
|
|
},
|
|
{
|
|
json: { file: 'file2' },
|
|
pairedItem: { item: 2 },
|
|
},
|
|
{
|
|
json: { file: 'file3' },
|
|
pairedItem: { item: 2 },
|
|
},
|
|
]);
|
|
});
|
|
|
|
it('upload => should call apiRequest with correct parameters', async () => {
|
|
(transport.apiRequest as jest.Mock).mockResolvedValueOnce({ success: true });
|
|
|
|
const returnData = await file.upload.execute.call(
|
|
createExecuteFunctionsMock({
|
|
binaryPropertyName: 'myData',
|
|
options: {},
|
|
}),
|
|
0,
|
|
);
|
|
|
|
expect(returnData.length).toEqual(1);
|
|
expect(returnData[0].pairedItem).toBeDefined();
|
|
expect(returnData[0].json).toEqual({ success: true });
|
|
|
|
expect(transport.apiRequest).toHaveBeenCalledWith(
|
|
'POST',
|
|
'/files',
|
|
expect.objectContaining({
|
|
headers: expect.objectContaining({
|
|
'content-type': expect.stringMatching(/^multipart\/form-data; boundary=/),
|
|
}),
|
|
option: expect.objectContaining({
|
|
formData: expect.any(FormData),
|
|
}),
|
|
}),
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('OpenAi, Image resource', () => {
|
|
beforeEach(() => {
|
|
(transport as any).apiRequest = jest.fn();
|
|
});
|
|
|
|
it('generate => should call apiRequest with correct parameters, return binary', async () => {
|
|
(transport.apiRequest as jest.Mock).mockResolvedValueOnce({ data: [{ b64_json: 'image1' }] });
|
|
|
|
const returnData = await image.generate.execute.call(
|
|
createExecuteFunctionsMock({
|
|
model: 'dall-e-3',
|
|
prompt: 'cat with a hat',
|
|
options: {
|
|
size: '1024x1024',
|
|
style: 'vivid',
|
|
quality: 'hd',
|
|
binaryPropertyOutput: 'myData',
|
|
},
|
|
}),
|
|
0,
|
|
);
|
|
|
|
expect(returnData.length).toEqual(1);
|
|
expect(returnData[0].binary?.myData).toBeDefined();
|
|
expect(returnData[0].pairedItem).toBeDefined();
|
|
|
|
expect(transport.apiRequest).toHaveBeenCalledWith('POST', '/images/generations', {
|
|
body: {
|
|
model: 'dall-e-3',
|
|
prompt: 'cat with a hat',
|
|
quality: 'hd',
|
|
response_format: 'b64_json',
|
|
size: '1024x1024',
|
|
style: 'vivid',
|
|
},
|
|
});
|
|
});
|
|
|
|
it('generate => should call apiRequest with correct parameters, return urls', async () => {
|
|
(transport.apiRequest as jest.Mock).mockResolvedValueOnce({ data: [{ url: 'image-url' }] });
|
|
|
|
const returnData = await image.generate.execute.call(
|
|
createExecuteFunctionsMock({
|
|
model: 'dall-e-3',
|
|
prompt: 'cat with a hat',
|
|
options: {
|
|
size: '1024x1024',
|
|
style: 'vivid',
|
|
quality: 'hd',
|
|
binaryPropertyOutput: 'myData',
|
|
returnImageUrls: true,
|
|
},
|
|
}),
|
|
0,
|
|
);
|
|
|
|
expect(returnData.length).toEqual(1);
|
|
expect(returnData[0].pairedItem).toBeDefined();
|
|
expect(returnData).toEqual([{ json: { url: 'image-url' }, pairedItem: { item: 0 } }]);
|
|
|
|
expect(transport.apiRequest).toHaveBeenCalledWith('POST', '/images/generations', {
|
|
body: {
|
|
model: 'dall-e-3',
|
|
prompt: 'cat with a hat',
|
|
quality: 'hd',
|
|
response_format: 'url',
|
|
size: '1024x1024',
|
|
style: 'vivid',
|
|
},
|
|
});
|
|
});
|
|
|
|
it('analyze => should call apiRequest with correct parameters', async () => {
|
|
(transport.apiRequest as jest.Mock).mockResolvedValueOnce({ success: true });
|
|
|
|
const returnData = await image.analyze.execute.call(
|
|
createExecuteFunctionsMock({
|
|
text: 'image text',
|
|
inputType: 'url',
|
|
imageUrls: 'image-url1, image-url2',
|
|
options: {
|
|
detail: 'low',
|
|
},
|
|
}),
|
|
0,
|
|
);
|
|
|
|
expect(returnData.length).toEqual(1);
|
|
expect(returnData[0].pairedItem).toBeDefined();
|
|
expect(returnData[0].json).toEqual({ success: true });
|
|
|
|
expect(transport.apiRequest).toHaveBeenCalledWith('POST', '/chat/completions', {
|
|
body: {
|
|
max_tokens: 300,
|
|
messages: [
|
|
{
|
|
content: [
|
|
{ text: 'image text', type: 'text' },
|
|
{ image_url: { detail: 'low', url: 'image-url1' }, type: 'image_url' },
|
|
{ image_url: { detail: 'low', url: 'image-url2' }, type: 'image_url' },
|
|
],
|
|
role: 'user',
|
|
},
|
|
],
|
|
model: 'gpt-4-vision-preview',
|
|
},
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('OpenAi, Text resource', () => {
|
|
beforeEach(() => {
|
|
(transport as any).apiRequest = jest.fn();
|
|
});
|
|
|
|
it('classify => should call apiRequest with correct parameters', async () => {
|
|
(transport.apiRequest as jest.Mock).mockResolvedValueOnce({ results: [{ flagged: true }] });
|
|
|
|
const returnData = await text.classify.execute.call(
|
|
createExecuteFunctionsMock({
|
|
input: 'input',
|
|
options: { useStableModel: true },
|
|
}),
|
|
0,
|
|
);
|
|
|
|
expect(returnData.length).toEqual(1);
|
|
expect(returnData[0].pairedItem).toBeDefined();
|
|
expect(returnData[0].json).toEqual({ flagged: true });
|
|
|
|
expect(transport.apiRequest).toHaveBeenCalledWith('POST', '/moderations', {
|
|
body: { input: 'input', model: 'text-moderation-stable' },
|
|
});
|
|
});
|
|
|
|
it('message => should call apiRequest with correct parameters, no tool call', async () => {
|
|
(transport.apiRequest as jest.Mock).mockResolvedValueOnce({
|
|
choices: [{ message: { tool_calls: undefined } }],
|
|
});
|
|
|
|
await text.message.execute.call(
|
|
createExecuteFunctionsMock({
|
|
modelId: 'gpt-model',
|
|
messages: {
|
|
values: [{ role: 'user', content: 'message' }],
|
|
},
|
|
|
|
options: {},
|
|
}),
|
|
0,
|
|
);
|
|
|
|
expect(transport.apiRequest).toHaveBeenCalledWith('POST', '/chat/completions', {
|
|
body: {
|
|
messages: [{ content: 'message', role: 'user' }],
|
|
model: 'gpt-model',
|
|
response_format: undefined,
|
|
tools: undefined,
|
|
},
|
|
});
|
|
});
|
|
});
|