mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 10:02:05 +00:00
refactor(Basic LLM Chain Node): Refactor Basic LLM Chain & add tests (#13850)
This commit is contained in:
@@ -0,0 +1,262 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||
import { HumanMessage } from '@langchain/core/messages';
|
||||
import { ChatGoogleGenerativeAI } from '@langchain/google-genai';
|
||||
import { ChatOllama } from '@langchain/ollama';
|
||||
import { mock } from 'jest-mock-extended';
|
||||
import type { IExecuteFunctions, IBinaryData, INode } from 'n8n-workflow';
|
||||
import { NodeOperationError } from 'n8n-workflow';
|
||||
|
||||
import {
|
||||
createImageMessage,
|
||||
dataUriFromImageData,
|
||||
UnsupportedMimeTypeError,
|
||||
} from '../methods/imageUtils';
|
||||
import type { MessageTemplate } from '../methods/types';
|
||||
|
||||
// Mock ChatGoogleGenerativeAI and ChatOllama
|
||||
jest.mock('@langchain/google-genai', () => ({
|
||||
ChatGoogleGenerativeAI: class MockChatGoogleGenerativeAI {},
|
||||
}));
|
||||
|
||||
jest.mock('@langchain/ollama', () => ({
|
||||
ChatOllama: class MockChatOllama {},
|
||||
}));
|
||||
|
||||
// Create a better mock for IExecuteFunctions that includes helpers
|
||||
const createMockExecuteFunctions = () => {
|
||||
const mockExec = mock<IExecuteFunctions>();
|
||||
// Add missing helpers property with mocked getBinaryDataBuffer
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
mockExec.helpers = {
|
||||
getBinaryDataBuffer: jest.fn().mockResolvedValue(Buffer.from('Test image data')),
|
||||
} as any;
|
||||
return mockExec;
|
||||
};
|
||||
|
||||
describe('imageUtils', () => {
|
||||
describe('dataUriFromImageData', () => {
|
||||
it('should convert image data to data URI', () => {
|
||||
const mockBuffer = Buffer.from('Test data');
|
||||
const mockBinaryData = mock<IBinaryData>({ mimeType: 'image/jpeg' });
|
||||
|
||||
const dataUri = dataUriFromImageData(mockBinaryData, mockBuffer);
|
||||
expect(dataUri).toBe('data:image/jpeg;base64,VGVzdCBkYXRh');
|
||||
});
|
||||
|
||||
it('should throw UnsupportedMimeTypeError for non-images', () => {
|
||||
const mockBuffer = Buffer.from('Test data');
|
||||
const mockBinaryData = mock<IBinaryData>({ mimeType: 'text/plain' });
|
||||
|
||||
expect(() => {
|
||||
dataUriFromImageData(mockBinaryData, mockBuffer);
|
||||
}).toThrow(UnsupportedMimeTypeError);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createImageMessage', () => {
|
||||
let mockContext: jest.Mocked<IExecuteFunctions>;
|
||||
let mockBuffer: Buffer;
|
||||
let mockBinaryData: jest.Mocked<IBinaryData>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockContext = createMockExecuteFunctions();
|
||||
mockBuffer = Buffer.from('Test image data');
|
||||
mockBinaryData = mock<IBinaryData>({ mimeType: 'image/png' });
|
||||
|
||||
// Mock required methods
|
||||
mockContext.getInputData.mockReturnValue([{ binary: { data: mockBinaryData }, json: {} }]);
|
||||
(mockContext.helpers.getBinaryDataBuffer as jest.Mock).mockResolvedValue(mockBuffer);
|
||||
mockContext.getInputConnectionData.mockResolvedValue({});
|
||||
mockContext.getNode.mockReturnValue({ name: 'TestNode' } as INode);
|
||||
});
|
||||
|
||||
it('should throw an error for invalid message type', async () => {
|
||||
const message: MessageTemplate = {
|
||||
type: 'HumanMessagePromptTemplate',
|
||||
message: '',
|
||||
messageType: 'text', // Invalid for this test case
|
||||
};
|
||||
|
||||
await expect(
|
||||
createImageMessage({
|
||||
context: mockContext,
|
||||
itemIndex: 0,
|
||||
message,
|
||||
}),
|
||||
).rejects.toThrow(NodeOperationError);
|
||||
});
|
||||
|
||||
it('should handle image URL messages', async () => {
|
||||
const message: MessageTemplate = {
|
||||
type: 'HumanMessagePromptTemplate',
|
||||
message: '',
|
||||
messageType: 'imageUrl',
|
||||
imageUrl: 'https://example.com/image.jpg',
|
||||
imageDetail: 'high',
|
||||
};
|
||||
|
||||
const result = await createImageMessage({
|
||||
context: mockContext,
|
||||
itemIndex: 0,
|
||||
message,
|
||||
});
|
||||
|
||||
expect(result).toBeInstanceOf(HumanMessage);
|
||||
expect(result.content).toEqual([
|
||||
{
|
||||
type: 'image_url',
|
||||
image_url: {
|
||||
url: 'https://example.com/image.jpg',
|
||||
detail: 'high',
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle image URL messages with auto detail', async () => {
|
||||
const message: MessageTemplate = {
|
||||
type: 'HumanMessagePromptTemplate',
|
||||
message: '',
|
||||
messageType: 'imageUrl',
|
||||
imageUrl: 'https://example.com/image.jpg',
|
||||
imageDetail: 'auto',
|
||||
};
|
||||
|
||||
const result = await createImageMessage({
|
||||
context: mockContext,
|
||||
itemIndex: 0,
|
||||
message,
|
||||
});
|
||||
|
||||
expect(result).toBeInstanceOf(HumanMessage);
|
||||
expect(result.content).toEqual([
|
||||
{
|
||||
type: 'image_url',
|
||||
image_url: {
|
||||
url: 'https://example.com/image.jpg',
|
||||
detail: undefined, // Auto becomes undefined
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should throw an error when binary data is missing', async () => {
|
||||
// Set up missing binary data
|
||||
mockContext.getInputData.mockReturnValue([{ json: {} }]); // No binary data
|
||||
|
||||
const message: MessageTemplate = {
|
||||
type: 'HumanMessagePromptTemplate',
|
||||
message: '',
|
||||
messageType: 'imageBinary',
|
||||
binaryImageDataKey: 'data',
|
||||
};
|
||||
|
||||
await expect(
|
||||
createImageMessage({
|
||||
context: mockContext,
|
||||
itemIndex: 0,
|
||||
message,
|
||||
}),
|
||||
).rejects.toThrow('No binary data set.');
|
||||
});
|
||||
|
||||
it('should handle binary image data for regular models', async () => {
|
||||
const message: MessageTemplate = {
|
||||
type: 'HumanMessagePromptTemplate',
|
||||
message: '',
|
||||
messageType: 'imageBinary',
|
||||
binaryImageDataKey: 'data',
|
||||
imageDetail: 'low',
|
||||
};
|
||||
|
||||
const result = await createImageMessage({
|
||||
context: mockContext,
|
||||
itemIndex: 0,
|
||||
message,
|
||||
});
|
||||
|
||||
expect(result).toBeInstanceOf(HumanMessage);
|
||||
expect(result.content).toEqual([
|
||||
{
|
||||
type: 'image_url',
|
||||
image_url: {
|
||||
url: 'data:image/png;base64,VGVzdCBpbWFnZSBkYXRh',
|
||||
detail: 'low',
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle image data differently for GoogleGenerativeAI models', async () => {
|
||||
// Mock a Google model - using our mocked class
|
||||
mockContext.getInputConnectionData.mockResolvedValue(new ChatGoogleGenerativeAI());
|
||||
|
||||
const message: MessageTemplate = {
|
||||
type: 'HumanMessagePromptTemplate',
|
||||
message: '',
|
||||
messageType: 'imageBinary',
|
||||
binaryImageDataKey: 'data',
|
||||
};
|
||||
|
||||
const result = await createImageMessage({
|
||||
context: mockContext,
|
||||
itemIndex: 0,
|
||||
message,
|
||||
});
|
||||
|
||||
expect(result).toBeInstanceOf(HumanMessage);
|
||||
expect(result.content).toEqual([
|
||||
{
|
||||
type: 'image_url',
|
||||
image_url: 'data:image/png;base64,VGVzdCBpbWFnZSBkYXRh',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle image data differently for Ollama models', async () => {
|
||||
// Mock an Ollama model - using our mocked class
|
||||
mockContext.getInputConnectionData.mockResolvedValue(new ChatOllama());
|
||||
|
||||
const message: MessageTemplate = {
|
||||
type: 'HumanMessagePromptTemplate',
|
||||
message: '',
|
||||
messageType: 'imageBinary',
|
||||
binaryImageDataKey: 'data',
|
||||
};
|
||||
|
||||
const result = await createImageMessage({
|
||||
context: mockContext,
|
||||
itemIndex: 0,
|
||||
message,
|
||||
});
|
||||
|
||||
expect(result).toBeInstanceOf(HumanMessage);
|
||||
expect(result.content).toEqual([
|
||||
{
|
||||
type: 'image_url',
|
||||
image_url: 'data:image/png;base64,VGVzdCBpbWFnZSBkYXRh',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should pass through UnsupportedMimeTypeError', async () => {
|
||||
// Mock a non-image mime type
|
||||
mockBinaryData.mimeType = 'application/pdf';
|
||||
|
||||
const message: MessageTemplate = {
|
||||
type: 'HumanMessagePromptTemplate',
|
||||
message: '',
|
||||
messageType: 'imageBinary',
|
||||
binaryImageDataKey: 'data',
|
||||
};
|
||||
|
||||
await expect(
|
||||
createImageMessage({
|
||||
context: mockContext,
|
||||
itemIndex: 0,
|
||||
message,
|
||||
}),
|
||||
).rejects.toThrow(NodeOperationError);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user