Files
alighasami 3d5eaf9445
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
first commit
2026-03-17 16:22:57 +03:30

681 lines
18 KiB
TypeScript

import { mockDeep } from 'jest-mock-extended';
import type { IExecuteFunctions } from 'n8n-workflow';
import { z } from 'zod';
import * as helpers from '@utils/helpers';
import * as image from './actions/image';
import * as text from './actions/text';
import * as transport from './transport';
import type { OllamaChatResponse, OllamaMessage } from './helpers/interfaces';
describe('Ollama Node', () => {
const executeFunctionsMock = mockDeep<IExecuteFunctions>();
const apiRequestMock = jest.spyOn(transport, 'apiRequest');
const getConnectedToolsMock = jest.spyOn(helpers, 'getConnectedTools');
beforeEach(() => {
jest.resetAllMocks();
});
describe('Text -> Message', () => {
it('should call the API with correct parameters for basic message', async () => {
executeFunctionsMock.getNodeParameter.mockImplementation((parameter: string) => {
switch (parameter) {
case 'modelId':
return 'llama3.2:latest';
case 'messages.values':
return [{ role: 'user', content: 'Hello, world!' }];
case 'simplify':
return true;
case 'options':
return {
system: 'You are a helpful assistant.',
temperature: 0.7,
top_p: 0.9,
top_k: 40,
num_predict: 1024,
};
default:
return undefined;
}
});
executeFunctionsMock.getNodeInputs.mockReturnValue([{ type: 'main' }]);
getConnectedToolsMock.mockResolvedValue([]);
apiRequestMock.mockResolvedValue({
model: 'llama3.2:latest',
created_at: '2023-10-01T10:00:00Z',
message: { role: 'assistant', content: 'Hello! How can I help you today?' },
done: true,
} as OllamaChatResponse);
const result = await text.message.execute.call(executeFunctionsMock, 0);
expect(result).toEqual([
{
json: { content: 'Hello! How can I help you today?' },
pairedItem: { item: 0 },
},
]);
expect(apiRequestMock).toHaveBeenCalledWith('POST', '/api/chat', {
body: {
model: 'llama3.2:latest',
messages: [
{ role: 'system', content: 'You are a helpful assistant.' },
{ role: 'user', content: 'Hello, world!' },
],
stream: false,
tools: [],
options: {
temperature: 0.7,
top_p: 0.9,
top_k: 40,
num_predict: 1024,
},
},
});
});
it('should return full response when simplify is false', async () => {
executeFunctionsMock.getNodeParameter.mockImplementation((parameter: string) => {
switch (parameter) {
case 'modelId':
return 'llama3.2:latest';
case 'messages.values':
return [{ role: 'user', content: 'Test message' }];
case 'simplify':
return false;
case 'options':
return {};
default:
return undefined;
}
});
executeFunctionsMock.getNodeInputs.mockReturnValue([{ type: 'main' }]);
getConnectedToolsMock.mockResolvedValue([]);
const mockResponse = {
model: 'llama3.2:latest',
created_at: '2023-10-01T10:00:00Z',
message: { role: 'assistant', content: 'Test response' },
done: true,
total_duration: 5000000,
load_duration: 1000000,
eval_count: 10,
eval_duration: 2000000,
} as OllamaChatResponse;
apiRequestMock.mockResolvedValue(mockResponse);
const result = await text.message.execute.call(executeFunctionsMock, 0);
expect(result).toEqual([
{
json: mockResponse,
pairedItem: { item: 0 },
},
]);
});
it('should handle tool calls correctly', async () => {
const mockTool = {
name: 'calculator',
description: 'Performs calculations',
schema: z.object({
expression: z.string().describe('Mathematical expression to evaluate'),
}),
invoke: jest.fn().mockResolvedValue({ result: 42 }),
};
executeFunctionsMock.getNodeParameter.mockImplementation((parameter: string) => {
switch (parameter) {
case 'modelId':
return 'llama3.2:latest';
case 'messages.values':
return [{ role: 'user', content: 'What is 6 * 7?' }];
case 'simplify':
return true;
case 'options':
return {};
default:
return undefined;
}
});
executeFunctionsMock.getNodeInputs.mockReturnValue([{ type: 'main' }, { type: 'ai_tool' }]);
// @ts-expect-error: Mocking a tool, we do not implement the full interface
getConnectedToolsMock.mockResolvedValue([mockTool]);
apiRequestMock.mockResolvedValueOnce({
model: 'llama3.2:latest',
created_at: '2023-10-01T10:00:00Z',
message: {
role: 'assistant',
content: '',
tool_calls: [
{
function: {
name: 'calculator',
arguments: { expression: '6 * 7' },
},
},
],
},
done: true,
} as OllamaChatResponse);
apiRequestMock.mockResolvedValueOnce({
model: 'llama3.2:latest',
created_at: '2023-10-01T10:00:00Z',
message: { role: 'assistant', content: 'The result is 42.' },
done: true,
} as OllamaChatResponse);
const result = await text.message.execute.call(executeFunctionsMock, 0);
expect(result).toEqual([
{
json: { content: 'The result is 42.' },
pairedItem: { item: 0 },
},
]);
expect(mockTool.invoke).toHaveBeenCalledWith({ expression: '6 * 7' });
expect(apiRequestMock).toHaveBeenCalledTimes(2);
});
it('should handle tool execution errors gracefully', async () => {
const mockTool = {
name: 'failing_tool',
description: 'A tool that fails',
schema: z.object({}),
invoke: jest.fn().mockRejectedValue(new Error('Tool execution failed')),
};
executeFunctionsMock.getNodeParameter.mockImplementation((parameter: string) => {
switch (parameter) {
case 'modelId':
return 'llama3.2:latest';
case 'messages.values':
return [{ role: 'user', content: 'Use the failing tool' }];
case 'simplify':
return true;
case 'options':
return {};
default:
return undefined;
}
});
executeFunctionsMock.getNodeInputs.mockReturnValue([{ type: 'main' }, { type: 'ai_tool' }]);
// @ts-expect-error: Mocking a tool, we do not implement the full interface
getConnectedToolsMock.mockResolvedValue([mockTool]);
apiRequestMock.mockResolvedValueOnce({
model: 'llama3.2:latest',
created_at: '2023-10-01T10:00:00Z',
message: {
role: 'assistant',
content: '',
tool_calls: [
{
function: {
name: 'failing_tool',
arguments: {},
},
},
],
},
done: true,
} as OllamaChatResponse);
apiRequestMock.mockResolvedValueOnce({
model: 'llama3.2:latest',
created_at: '2023-10-01T10:00:00Z',
message: { role: 'assistant', content: 'I encountered an error with the tool.' },
done: true,
} as OllamaChatResponse);
const result = await text.message.execute.call(executeFunctionsMock, 0);
expect(result).toEqual([
{
json: { content: 'I encountered an error with the tool.' },
pairedItem: { item: 0 },
},
]);
const secondCallBody = apiRequestMock.mock.calls[1][2]?.body as any;
const toolMessage = secondCallBody.messages.find((msg: OllamaMessage) => msg.role === 'tool');
expect(toolMessage.content).toBe('Error executing tool: Tool execution failed');
});
it('should process stop sequences correctly', async () => {
executeFunctionsMock.getNodeParameter.mockImplementation((parameter: string) => {
switch (parameter) {
case 'modelId':
return 'llama3.2:latest';
case 'messages.values':
return [{ role: 'user', content: 'Generate text' }];
case 'simplify':
return true;
case 'options':
return {
stop: '###,END,STOP',
};
default:
return undefined;
}
});
executeFunctionsMock.getNodeInputs.mockReturnValue([{ type: 'main' }]);
getConnectedToolsMock.mockResolvedValue([]);
apiRequestMock.mockResolvedValue({
model: 'llama3.2:latest',
created_at: '2023-10-01T10:00:00Z',
message: { role: 'assistant', content: 'Generated text' },
done: true,
} as OllamaChatResponse);
await text.message.execute.call(executeFunctionsMock, 0);
expect(apiRequestMock).toHaveBeenCalledWith('POST', '/api/chat', {
body: {
model: 'llama3.2:latest',
messages: [{ role: 'user', content: 'Generate text' }],
stream: false,
tools: [],
options: {
stop: ['###', 'END', 'STOP'],
},
},
});
});
it('should handle various model-specific options', async () => {
executeFunctionsMock.getNodeParameter.mockImplementation((parameter: string) => {
switch (parameter) {
case 'modelId':
return 'llama3.2:latest';
case 'messages.values':
return [{ role: 'user', content: 'Test with options' }];
case 'simplify':
return true;
case 'options':
return {
temperature: 0.5,
top_p: 0.8,
top_k: 30,
num_predict: 512,
frequency_penalty: 0.1,
presence_penalty: 0.2,
repeat_penalty: 1.2,
num_ctx: 2048,
repeat_last_n: 32,
min_p: 0.1,
seed: 123,
low_vram: true,
main_gpu: 1,
num_batch: 256,
num_gpu: 2,
num_thread: 8,
penalize_newline: false,
use_mlock: true,
use_mmap: false,
vocab_only: false,
keep_alive: '10m',
format: 'json',
};
default:
return undefined;
}
});
executeFunctionsMock.getNodeInputs.mockReturnValue([{ type: 'main' }]);
getConnectedToolsMock.mockResolvedValue([]);
apiRequestMock.mockResolvedValue({
model: 'llama3.2:latest',
created_at: '2023-10-01T10:00:00Z',
message: { role: 'assistant', content: '{"response": "test"}' },
done: true,
} as OllamaChatResponse);
await text.message.execute.call(executeFunctionsMock, 0);
expect(apiRequestMock).toHaveBeenCalledWith('POST', '/api/chat', {
body: {
model: 'llama3.2:latest',
messages: [{ role: 'user', content: 'Test with options' }],
stream: false,
tools: [],
options: {
temperature: 0.5,
top_p: 0.8,
top_k: 30,
num_predict: 512,
frequency_penalty: 0.1,
presence_penalty: 0.2,
repeat_penalty: 1.2,
num_ctx: 2048,
repeat_last_n: 32,
min_p: 0.1,
seed: 123,
low_vram: true,
main_gpu: 1,
num_batch: 256,
num_gpu: 2,
num_thread: 8,
penalize_newline: false,
use_mlock: true,
use_mmap: false,
vocab_only: false,
keep_alive: '10m',
format: 'json',
},
},
});
});
});
describe('Image -> Analyze', () => {
it('should analyze image from binary data', async () => {
executeFunctionsMock.getNodeParameter.mockImplementation((parameter: string) => {
switch (parameter) {
case 'modelId':
return 'llava:latest';
case 'inputType':
return 'binary';
case 'binaryPropertyName':
return 'data';
case 'text':
return "What's in this image?";
case 'simplify':
return true;
case 'options':
return {
temperature: 0.3,
num_predict: 512,
};
default:
return undefined;
}
});
executeFunctionsMock.helpers.getBinaryDataBuffer.mockResolvedValue(
Buffer.from('test image data'),
);
apiRequestMock.mockResolvedValue({
model: 'llava:latest',
created_at: '2023-10-01T10:00:00Z',
message: {
role: 'assistant',
content: 'This image shows a beautiful mountain landscape with snow-capped peaks.',
},
done: true,
} as OllamaChatResponse);
const result = await image.analyze.execute.call(executeFunctionsMock, 0);
expect(result).toEqual([
{
json: {
content: 'This image shows a beautiful mountain landscape with snow-capped peaks.',
},
pairedItem: { item: 0 },
},
]);
expect(apiRequestMock).toHaveBeenCalledWith('POST', '/api/chat', {
body: {
model: 'llava:latest',
messages: [
{
role: 'user',
content: "What's in this image?",
images: ['dGVzdCBpbWFnZSBkYXRh'], // base64 encoded 'test image data'
},
],
stream: false,
options: {
temperature: 0.3,
num_predict: 512,
},
},
});
});
it('should analyze image from URL', async () => {
executeFunctionsMock.getNodeParameter.mockImplementation((parameter: string) => {
switch (parameter) {
case 'modelId':
return 'llava:latest';
case 'inputType':
return 'url';
case 'imageUrls':
return 'https://example.com/test-image.jpg';
case 'text':
return 'Describe this image';
case 'simplify':
return true;
case 'options':
return {};
default:
return undefined;
}
});
executeFunctionsMock.helpers.httpRequest.mockResolvedValue(
Buffer.from('downloaded image data'),
);
apiRequestMock.mockResolvedValue({
model: 'llava:latest',
created_at: '2023-10-01T10:00:00Z',
message: {
role: 'assistant',
content: 'This image contains a sunset over the ocean.',
},
done: true,
} as OllamaChatResponse);
const result = await image.analyze.execute.call(executeFunctionsMock, 0);
expect(result).toEqual([
{
json: { content: 'This image contains a sunset over the ocean.' },
pairedItem: { item: 0 },
},
]);
expect(executeFunctionsMock.helpers.httpRequest).toHaveBeenCalledWith({
method: 'GET',
url: 'https://example.com/test-image.jpg',
encoding: 'arraybuffer',
});
expect(apiRequestMock).toHaveBeenCalledWith('POST', '/api/chat', {
body: {
model: 'llava:latest',
messages: [
{
role: 'user',
content: 'Describe this image',
images: ['ZG93bmxvYWRlZCBpbWFnZSBkYXRh'], // base64 encoded 'downloaded image data'
},
],
stream: false,
options: {},
},
});
});
it('should handle multiple images from URLs', async () => {
executeFunctionsMock.getNodeParameter.mockImplementation((parameter: string) => {
switch (parameter) {
case 'modelId':
return 'llava:latest';
case 'inputType':
return 'url';
case 'imageUrls':
return 'https://example.com/image1.jpg, https://example.com/image2.png';
case 'text':
return 'Compare these images';
case 'simplify':
return true;
case 'options':
return {};
default:
return undefined;
}
});
executeFunctionsMock.helpers.httpRequest.mockResolvedValueOnce(
Buffer.from('first image data'),
);
executeFunctionsMock.helpers.httpRequest.mockResolvedValueOnce(
Buffer.from('second image data'),
);
apiRequestMock.mockResolvedValue({
model: 'llava:latest',
created_at: '2023-10-01T10:00:00Z',
message: {
role: 'assistant',
content: 'Both images show different landscapes.',
},
done: true,
} as OllamaChatResponse);
const result = await image.analyze.execute.call(executeFunctionsMock, 0);
expect(result).toEqual([
{
json: { content: 'Both images show different landscapes.' },
pairedItem: { item: 0 },
},
]);
expect(executeFunctionsMock.helpers.httpRequest).toHaveBeenCalledTimes(2);
expect(apiRequestMock).toHaveBeenCalledWith('POST', '/api/chat', {
body: {
model: 'llava:latest',
messages: [
{
role: 'user',
content: 'Compare these images',
images: [
'Zmlyc3QgaW1hZ2UgZGF0YQ==', // base64 encoded 'first image data'
'c2Vjb25kIGltYWdlIGRhdGE=', // base64 encoded 'second image data'
],
},
],
stream: false,
options: {},
},
});
});
it('should handle multiple binary images', async () => {
executeFunctionsMock.getNodeParameter.mockImplementation((parameter: string) => {
switch (parameter) {
case 'modelId':
return 'llava:latest';
case 'inputType':
return 'binary';
case 'binaryPropertyName':
return 'image1,image2';
case 'text':
return 'Analyze these images';
case 'simplify':
return false;
case 'options':
return {};
default:
return undefined;
}
});
executeFunctionsMock.helpers.getBinaryDataBuffer.mockResolvedValueOnce(
Buffer.from('first binary image'),
);
executeFunctionsMock.helpers.getBinaryDataBuffer.mockResolvedValueOnce(
Buffer.from('second binary image'),
);
const mockResponse = {
model: 'llava:latest',
created_at: '2023-10-01T10:00:00Z',
message: {
role: 'assistant',
content: 'Analysis complete for both images.',
},
done: true,
eval_count: 25,
eval_duration: 3000000,
} as OllamaChatResponse;
apiRequestMock.mockResolvedValue(mockResponse);
const result = await image.analyze.execute.call(executeFunctionsMock, 0);
expect(result).toEqual([
{
json: mockResponse,
pairedItem: { item: 0 },
},
]);
expect(executeFunctionsMock.helpers.getBinaryDataBuffer).toHaveBeenCalledTimes(2);
expect(executeFunctionsMock.helpers.getBinaryDataBuffer).toHaveBeenNthCalledWith(
1,
0,
'image1',
);
expect(executeFunctionsMock.helpers.getBinaryDataBuffer).toHaveBeenNthCalledWith(
2,
0,
'image2',
);
});
it('should process stop sequences for image analysis', async () => {
executeFunctionsMock.getNodeParameter.mockImplementation((parameter: string) => {
switch (parameter) {
case 'modelId':
return 'llava:latest';
case 'inputType':
return 'binary';
case 'binaryPropertyName':
return 'data';
case 'text':
return 'Describe briefly';
case 'simplify':
return true;
case 'options':
return {
stop: 'END,DONE',
temperature: 0.1,
};
default:
return undefined;
}
});
executeFunctionsMock.helpers.getBinaryDataBuffer.mockResolvedValue(Buffer.from('test image'));
apiRequestMock.mockResolvedValue({
model: 'llava:latest',
created_at: '2023-10-01T10:00:00Z',
message: {
role: 'assistant',
content: 'A simple image.',
},
done: true,
} as OllamaChatResponse);
await image.analyze.execute.call(executeFunctionsMock, 0);
expect(apiRequestMock).toHaveBeenCalledWith('POST', '/api/chat', {
body: {
model: 'llava:latest',
messages: [
{
role: 'user',
content: 'Describe briefly',
images: ['dGVzdCBpbWFnZQ=='], // base64 encoded 'test image'
},
],
stream: false,
options: {
stop: ['END', 'DONE'],
temperature: 0.1,
},
},
});
});
});
});