Files
Agent-n8n/n8n-n8n-1.109.2/packages/@n8n/nodes-langchain/nodes/vendors/Anthropic/Anthropic.node.test.ts
2025-09-08 04:48:28 +08:00

1009 lines
25 KiB
TypeScript
Executable File

import { mockDeep } from 'jest-mock-extended';
import type { IExecuteFunctions, IBinaryData } from 'n8n-workflow';
import * as helpers from '@utils/helpers';
import * as file from './actions/file';
import * as image from './actions/image';
import * as prompt from './actions/prompt';
import * as text from './actions/text';
import * as utils from './helpers/utils';
import * as transport from './transport';
import type { File } from './helpers/interfaces';
describe('Anthropic Node', () => {
const executeFunctionsMock = mockDeep<IExecuteFunctions>();
const apiRequestMock = jest.spyOn(transport, 'apiRequest');
const getConnectedToolsMock = jest.spyOn(helpers, 'getConnectedTools');
const downloadFileMock = jest.spyOn(utils, 'downloadFile');
const uploadFileMock = jest.spyOn(utils, 'uploadFile');
const getBaseUrlMock = jest.spyOn(utils, 'getBaseUrl');
beforeEach(() => {
jest.resetAllMocks();
});
describe('Text -> Message', () => {
it('should call the api with the correct parameters', async () => {
executeFunctionsMock.getNodeParameter.mockImplementation((parameter: string) => {
switch (parameter) {
case 'modelId':
return 'claude-sonnet-4-20250514';
case 'messages.values':
return [{ role: 'user', content: 'Hello, world!' }];
case 'simplify':
return true;
case 'addAttachments':
return false;
case 'options':
return {
system: 'You are a helpful assistant.',
codeExecution: true,
webSearch: true,
allowedDomains: 'https://example.com',
maxTokens: 1024,
temperature: 0.7,
topP: 0.9,
topK: 40,
};
default:
return undefined;
}
});
executeFunctionsMock.getNodeInputs.mockReturnValue([{ type: 'main' }, { type: 'ai_tool' }]);
getConnectedToolsMock.mockResolvedValue([]);
apiRequestMock.mockResolvedValue({
content: [{ type: 'text', text: 'Hello! How can I help you today?' }],
stop_reason: 'end_turn',
});
const result = await text.message.execute.call(executeFunctionsMock, 0);
expect(result).toEqual([
{
json: {
content: [
{
type: 'text',
text: 'Hello! How can I help you today?',
},
],
},
pairedItem: { item: 0 },
},
]);
expect(apiRequestMock).toHaveBeenCalledWith('POST', '/v1/messages', {
body: {
model: 'claude-sonnet-4-20250514',
max_tokens: 1024,
temperature: 0.7,
top_p: 0.9,
top_k: 40,
system: 'You are a helpful assistant.',
messages: [
{
role: 'user',
content: 'Hello, world!',
},
],
tools: [
{
type: 'code_execution_20250522',
name: 'code_execution',
},
{
type: 'web_search_20250305',
name: 'web_search',
allowed_domains: ['https://example.com'],
},
],
},
enableAnthropicBetas: {
codeExecution: true,
},
});
});
it('should add code execution attachments', async () => {
executeFunctionsMock.getNodeParameter.mockImplementation((parameter: string) => {
switch (parameter) {
case 'modelId':
return 'claude-sonnet-4-20250514';
case 'messages.values':
return [{ role: 'user', content: 'Hello, world!' }];
case 'simplify':
return true;
case 'addAttachments':
return true;
case 'attachmentsInputType':
return 'url';
case 'attachmentsUrls':
return 'https://example.com/file.pdf';
case 'options':
return {
codeExecution: true,
};
default:
return undefined;
}
});
executeFunctionsMock.getNodeInputs.mockReturnValue([{ type: 'main' }, { type: 'ai_tool' }]);
getBaseUrlMock.mockResolvedValue('https://api.anthropic.com');
downloadFileMock.mockResolvedValue({
fileContent: Buffer.from('abcdefgh'),
mimeType: 'application/pdf',
});
uploadFileMock.mockResolvedValue({
id: 'file_123',
} as File);
getConnectedToolsMock.mockResolvedValue([]);
apiRequestMock.mockResolvedValue({
content: [{ type: 'text', text: 'Hello! How can I help you today?' }],
stop_reason: 'end_turn',
});
const result = await text.message.execute.call(executeFunctionsMock, 0);
expect(result).toEqual([
{
json: {
content: [
{
type: 'text',
text: 'Hello! How can I help you today?',
},
],
},
pairedItem: { item: 0 },
},
]);
expect(downloadFileMock).toHaveBeenCalledWith('https://example.com/file.pdf');
expect(uploadFileMock).toHaveBeenCalledWith(Buffer.from('abcdefgh'), 'application/pdf');
expect(apiRequestMock).toHaveBeenCalledWith('POST', '/v1/messages', {
body: {
model: 'claude-sonnet-4-20250514',
max_tokens: 1024,
messages: [
{
role: 'user',
content: 'Hello, world!',
},
{
role: 'user',
content: [
{
type: 'container_upload',
file_id: 'file_123',
},
],
},
],
tools: [
{
type: 'code_execution_20250522',
name: 'code_execution',
},
],
},
enableAnthropicBetas: {
codeExecution: true,
},
});
});
it('should add regular attachments', async () => {
executeFunctionsMock.getNodeParameter.mockImplementation((parameter: string) => {
switch (parameter) {
case 'modelId':
return 'claude-sonnet-4-20250514';
case 'messages.values':
return [{ role: 'user', content: 'Hello, world!' }];
case 'simplify':
return true;
case 'addAttachments':
return true;
case 'attachmentsInputType':
return 'url';
case 'attachmentsUrls':
return 'https://example.com/file.pdf';
case 'options':
return {};
default:
return undefined;
}
});
executeFunctionsMock.getNodeInputs.mockReturnValue([{ type: 'main' }, { type: 'ai_tool' }]);
getBaseUrlMock.mockResolvedValue('https://api.anthropic.com');
executeFunctionsMock.helpers.httpRequest.mockResolvedValue({
headers: {
'content-type': 'application/pdf',
},
});
getConnectedToolsMock.mockResolvedValue([]);
apiRequestMock.mockResolvedValue({
content: [{ type: 'text', text: 'Hello! How can I help you today?' }],
stop_reason: 'end_turn',
});
const result = await text.message.execute.call(executeFunctionsMock, 0);
expect(result).toEqual([
{
json: {
content: [
{
type: 'text',
text: 'Hello! How can I help you today?',
},
],
},
pairedItem: { item: 0 },
},
]);
expect(executeFunctionsMock.helpers.httpRequest).toHaveBeenCalledWith({
method: 'HEAD',
url: 'https://example.com/file.pdf',
returnFullResponse: true,
});
expect(apiRequestMock).toHaveBeenCalledWith('POST', '/v1/messages', {
body: {
model: 'claude-sonnet-4-20250514',
max_tokens: 1024,
messages: [
{
role: 'user',
content: 'Hello, world!',
},
{
role: 'user',
content: [
{
type: 'document',
source: {
type: 'url',
url: 'https://example.com/file.pdf',
},
},
],
},
],
tools: [],
},
enableAnthropicBetas: {},
});
});
});
describe('File -> Upload', () => {
it('should upload file from URL', async () => {
executeFunctionsMock.getNodeParameter.mockImplementation((parameter: string) => {
switch (parameter) {
case 'inputType':
return 'url';
case 'fileUrl':
return 'https://example.com/file.pdf';
case 'options.fileName':
return 'test.pdf';
default:
return undefined;
}
});
executeFunctionsMock.getCredentials.mockResolvedValue({
url: 'https://api.anthropic.com',
});
downloadFileMock.mockResolvedValue({
fileContent: Buffer.from('test file content'),
mimeType: 'application/pdf',
});
uploadFileMock.mockResolvedValue({
created_at: '2025-01-01T10:00:00Z',
downloadable: true,
filename: 'test.pdf',
id: 'file_123',
mime_type: 'application/pdf',
size_bytes: 17,
type: 'file',
});
const result = await file.upload.execute.call(executeFunctionsMock, 0);
expect(result).toEqual([
{
json: {
created_at: '2025-01-01T10:00:00Z',
downloadable: true,
filename: 'test.pdf',
id: 'file_123',
mime_type: 'application/pdf',
size_bytes: 17,
type: 'file',
url: 'https://api.anthropic.com/v1/files/file_123',
},
pairedItem: { item: 0 },
},
]);
expect(downloadFileMock).toHaveBeenCalledWith('https://example.com/file.pdf');
expect(uploadFileMock).toHaveBeenCalledWith(
Buffer.from('test file content'),
'application/pdf',
'test.pdf',
);
});
it('should upload file from binary data', async () => {
executeFunctionsMock.getNodeParameter.mockImplementation((parameter: string) => {
switch (parameter) {
case 'inputType':
return 'binary';
case 'binaryPropertyName':
return 'data';
case 'options.fileName':
return 'file';
default:
return undefined;
}
});
executeFunctionsMock.getCredentials.mockResolvedValue({});
const mockBinaryData: IBinaryData = {
mimeType: 'application/pdf',
fileName: 'test.pdf',
fileSize: '1024',
fileExtension: 'pdf',
data: 'test',
};
executeFunctionsMock.helpers.assertBinaryData.mockReturnValue(mockBinaryData);
executeFunctionsMock.helpers.getBinaryDataBuffer.mockResolvedValue(
Buffer.from('test file content'),
);
uploadFileMock.mockResolvedValue({
created_at: '2025-01-01T10:00:00Z',
downloadable: true,
filename: 'file',
id: 'file_456',
mime_type: 'application/pdf',
size_bytes: 17,
type: 'file',
});
const result = await file.upload.execute.call(executeFunctionsMock, 0);
expect(result).toEqual([
{
json: {
created_at: '2025-01-01T10:00:00Z',
downloadable: true,
filename: 'file',
id: 'file_456',
mime_type: 'application/pdf',
size_bytes: 17,
type: 'file',
url: 'https://api.anthropic.com/v1/files/file_456',
},
pairedItem: { item: 0 },
},
]);
expect(uploadFileMock).toHaveBeenCalledWith(
Buffer.from('test file content'),
'application/pdf',
'file',
);
});
});
describe('File -> List', () => {
it('should list files with a limit', async () => {
executeFunctionsMock.getNodeParameter.mockImplementation((parameter: string) => {
switch (parameter) {
case 'returnAll':
return false;
case 'limit':
return 10;
default:
return undefined;
}
});
getBaseUrlMock.mockResolvedValue('https://api.anthropic.com');
apiRequestMock.mockResolvedValue({
data: [
{
id: 'file_123',
filename: 'test.pdf',
mime_type: 'application/pdf',
},
{
id: 'file_456',
filename: 'test.png',
mime_type: 'image/png',
},
],
first_id: '',
last_id: '',
has_more: false,
});
const result = await file.list.execute.call(executeFunctionsMock, 0);
expect(result).toEqual([
{
json: {
id: 'file_123',
filename: 'test.pdf',
mime_type: 'application/pdf',
url: 'https://api.anthropic.com/v1/files/file_123',
},
pairedItem: { item: 0 },
},
{
json: {
id: 'file_456',
filename: 'test.png',
mime_type: 'image/png',
url: 'https://api.anthropic.com/v1/files/file_456',
},
pairedItem: { item: 0 },
},
]);
expect(apiRequestMock).toHaveBeenCalledWith('GET', '/v1/files', {
qs: {
limit: 10,
},
});
});
it('should list all files', async () => {
executeFunctionsMock.getNodeParameter.mockImplementation((parameter: string) => {
switch (parameter) {
case 'returnAll':
return true;
default:
return undefined;
}
});
getBaseUrlMock.mockResolvedValue('https://api.anthropic.com');
apiRequestMock.mockResolvedValueOnce({
data: [
{
id: 'file_001',
filename: 'test-1.pdf',
mime_type: 'application/pdf',
},
{
id: 'file_002',
filename: 'test-2.pdf',
mime_type: 'application/pdf',
},
],
first_id: 'file_001',
last_id: 'file_002',
has_more: true,
});
apiRequestMock.mockResolvedValueOnce({
data: [
{
id: 'file_003',
filename: 'test-3.pdf',
mime_type: 'application/pdf',
},
],
first_id: 'file_003',
last_id: 'file_003',
has_more: false,
});
const result = await file.list.execute.call(executeFunctionsMock, 0);
expect(result).toEqual([
{
json: {
id: 'file_001',
filename: 'test-1.pdf',
mime_type: 'application/pdf',
url: 'https://api.anthropic.com/v1/files/file_001',
},
pairedItem: { item: 0 },
},
{
json: {
id: 'file_002',
filename: 'test-2.pdf',
mime_type: 'application/pdf',
url: 'https://api.anthropic.com/v1/files/file_002',
},
pairedItem: { item: 0 },
},
{
json: {
id: 'file_003',
filename: 'test-3.pdf',
mime_type: 'application/pdf',
url: 'https://api.anthropic.com/v1/files/file_003',
},
pairedItem: { item: 0 },
},
]);
});
});
describe('File -> Delete', () => {
it('should delete file', async () => {
executeFunctionsMock.getNodeParameter.mockImplementation((parameter: string) => {
switch (parameter) {
case 'fileId':
return 'file_123';
default:
return undefined;
}
});
apiRequestMock.mockResolvedValue({
id: 'file_123',
});
const result = await file.deleteFile.execute.call(executeFunctionsMock, 0);
expect(result).toEqual([{ json: { id: 'file_123' }, pairedItem: { item: 0 } }]);
expect(apiRequestMock).toHaveBeenCalledWith('DELETE', '/v1/files/file_123');
});
});
describe('File -> Get', () => {
it('should get file', async () => {
executeFunctionsMock.getNodeParameter.mockImplementation((parameter: string) => {
switch (parameter) {
case 'fileId':
return 'file_123';
default:
return undefined;
}
});
getBaseUrlMock.mockResolvedValue('https://api.anthropic.com');
apiRequestMock.mockResolvedValue({
id: 'file_123',
filename: 'test.pdf',
mime_type: 'application/pdf',
});
const result = await file.get.execute.call(executeFunctionsMock, 0);
expect(result).toEqual([
{
json: {
id: 'file_123',
filename: 'test.pdf',
mime_type: 'application/pdf',
url: 'https://api.anthropic.com/v1/files/file_123',
},
pairedItem: { item: 0 },
},
]);
});
});
describe('Image -> Analyze', () => {
it('should analyze image from URL', async () => {
executeFunctionsMock.getNodeParameter.mockImplementation((parameter: string) => {
switch (parameter) {
case 'modelId':
return 'claude-sonnet-4-20250514';
case 'inputType':
return 'url';
case 'imageUrls':
return 'https://example.com/image.png';
case 'text':
return "What's in this image?";
case 'simplify':
return true;
case 'options':
return {
maxTokens: 1024,
};
default:
return undefined;
}
});
executeFunctionsMock.getCredentials.mockResolvedValue({});
apiRequestMock.mockResolvedValue({
content: [
{
type: 'text',
text: 'This image shows a beautiful sunset over a mountain landscape.',
},
],
stop_reason: 'end_turn',
});
const result = await image.analyze.execute.call(executeFunctionsMock, 0);
expect(result).toEqual([
{
json: {
content: [
{
type: 'text',
text: 'This image shows a beautiful sunset over a mountain landscape.',
},
],
},
pairedItem: { item: 0 },
},
]);
expect(apiRequestMock).toHaveBeenCalledWith('POST', '/v1/messages', {
body: {
model: 'claude-sonnet-4-20250514',
max_tokens: 1024,
messages: [
{
role: 'user',
content: [
{
type: 'image',
source: {
type: 'url',
url: 'https://example.com/image.png',
},
},
{
type: 'text',
text: "What's in this image?",
},
],
},
],
},
});
});
it('should analyze image from binary data', async () => {
executeFunctionsMock.getNodeParameter.mockImplementation((parameter: string) => {
switch (parameter) {
case 'modelId':
return 'claude-sonnet-4-20250514';
case 'inputType':
return 'binary';
case 'binaryPropertyName':
return 'data';
case 'text':
return "What's in this image?";
case 'simplify':
return true;
case 'options':
return {
maxTokens: 1024,
};
default:
return undefined;
}
});
executeFunctionsMock.getCredentials.mockResolvedValue({});
const mockBinaryData: IBinaryData = {
mimeType: 'image/png',
fileName: 'test.png',
fileSize: '2048',
fileExtension: 'png',
data: 'test',
};
executeFunctionsMock.helpers.assertBinaryData.mockReturnValue(mockBinaryData);
executeFunctionsMock.helpers.getBinaryDataBuffer.mockResolvedValue(
Buffer.from('test image data'),
);
apiRequestMock.mockResolvedValue({
content: [
{
type: 'text',
text: 'This image shows a beautiful sunset over a mountain landscape.',
},
],
stop_reason: 'end_turn',
});
const result = await image.analyze.execute.call(executeFunctionsMock, 0);
expect(result).toEqual([
{
json: {
content: [
{
type: 'text',
text: 'This image shows a beautiful sunset over a mountain landscape.',
},
],
},
pairedItem: { item: 0 },
},
]);
expect(apiRequestMock).toHaveBeenCalledWith('POST', '/v1/messages', {
body: {
model: 'claude-sonnet-4-20250514',
max_tokens: 1024,
messages: [
{
role: 'user',
content: [
{
type: 'image',
source: {
type: 'base64',
media_type: 'image/png',
data: 'dGVzdCBpbWFnZSBkYXRh',
},
},
{
type: 'text',
text: "What's in this image?",
},
],
},
],
},
});
});
it('should analyze image from Anthropic file URL', async () => {
executeFunctionsMock.getNodeParameter.mockImplementation((parameter: string) => {
switch (parameter) {
case 'modelId':
return 'claude-sonnet-4-20250514';
case 'inputType':
return 'url';
case 'imageUrls':
return 'https://api.anthropic.com/v1/files/file_123';
case 'text':
return "What's in this image?";
case 'simplify':
return true;
case 'options':
return {
maxTokens: 1024,
};
default:
return undefined;
}
});
executeFunctionsMock.getCredentials.mockResolvedValue({});
apiRequestMock.mockResolvedValue({
content: [
{
type: 'text',
text: 'This image shows a beautiful sunset over a mountain landscape.',
},
],
stop_reason: 'end_turn',
});
const result = await image.analyze.execute.call(executeFunctionsMock, 0);
expect(result).toEqual([
{
json: {
content: [
{
type: 'text',
text: 'This image shows a beautiful sunset over a mountain landscape.',
},
],
},
pairedItem: { item: 0 },
},
]);
expect(apiRequestMock).toHaveBeenCalledWith('POST', '/v1/messages', {
body: {
model: 'claude-sonnet-4-20250514',
max_tokens: 1024,
messages: [
{
role: 'user',
content: [
{
type: 'image',
source: {
type: 'file',
file_id: 'file_123',
},
},
{
type: 'text',
text: "What's in this image?",
},
],
},
],
},
});
});
});
describe('Prompt -> Generate', () => {
it('should generate prompt from task description', async () => {
executeFunctionsMock.getNodeParameter.mockImplementation((parameter: string) => {
switch (parameter) {
case 'task':
return 'A chef for a meal prep planning service';
case 'simplify':
return true;
default:
return undefined;
}
});
apiRequestMock.mockResolvedValue({
messages: [
{
role: 'user',
content: 'Plan a healthy weekly meal prep menu for busy professionals',
},
],
system:
'You are a professional chef specializing in meal prep services for busy professionals. Create balanced, nutritious, and convenient meal plans.',
});
const result = await prompt.generate.execute.call(executeFunctionsMock, 0);
expect(result).toEqual([
{
json: {
messages: [
{
role: 'user',
content: 'Plan a healthy weekly meal prep menu for busy professionals',
},
],
system:
'You are a professional chef specializing in meal prep services for busy professionals. Create balanced, nutritious, and convenient meal plans.',
},
pairedItem: { item: 0 },
},
]);
expect(apiRequestMock).toHaveBeenCalledWith('POST', '/v1/experimental/generate_prompt', {
body: {
task: 'A chef for a meal prep planning service',
},
enableAnthropicBetas: {
promptTools: true,
},
});
});
});
describe('Prompt -> Improve', () => {
it('should improve existing prompt with feedback', async () => {
executeFunctionsMock.getNodeParameter.mockImplementation((parameter: string) => {
switch (parameter) {
case 'messages.values':
return [
{
role: 'user',
content: 'Plan meals for the week',
},
];
case 'simplify':
return true;
case 'options':
return {
system: 'You are a chef',
feedback: 'Make it more detailed and include cooking times',
};
default:
return undefined;
}
});
apiRequestMock.mockResolvedValue({
messages: [
{
role: 'user',
content:
'Plan a detailed weekly meal prep menu with cooking times and nutritional information for busy professionals',
},
],
system:
'You are a professional chef and nutritionist specializing in meal prep services. Provide detailed cooking instructions, prep times, and nutritional breakdowns.',
});
const result = await prompt.improve.execute.call(executeFunctionsMock, 0);
expect(result).toEqual([
{
json: {
messages: [
{
role: 'user',
content:
'Plan a detailed weekly meal prep menu with cooking times and nutritional information for busy professionals',
},
],
system:
'You are a professional chef and nutritionist specializing in meal prep services. Provide detailed cooking instructions, prep times, and nutritional breakdowns.',
},
pairedItem: { item: 0 },
},
]);
expect(apiRequestMock).toHaveBeenCalledWith('POST', '/v1/experimental/improve_prompt', {
body: {
messages: [
{
role: 'user',
content: 'Plan meals for the week',
},
],
system: 'You are a chef',
feedback: 'Make it more detailed and include cooking times',
},
enableAnthropicBetas: {
promptTools: true,
},
});
});
});
describe('Prompt -> Templatize', () => {
it('should templatize prompt with variables', async () => {
executeFunctionsMock.getNodeParameter.mockImplementation((parameter: string) => {
switch (parameter) {
case 'messages.values':
return [
{
role: 'user',
content: 'Translate hello to German',
},
];
case 'simplify':
return true;
case 'options':
return {
system: 'You are a professional English to German translator',
};
default:
return undefined;
}
});
apiRequestMock.mockResolvedValue({
messages: [
{
role: 'user',
content: 'Translate {{WORD}} to {{TARGET_LANGUAGE}}',
},
],
system: 'You are a professional {{SOURCE_LANGUAGE}} to {{TARGET_LANGUAGE}} translator',
variable_values: {
WORD: 'hello',
TARGET_LANGUAGE: 'German',
SOURCE_LANGUAGE: 'English',
},
});
const result = await prompt.templatize.execute.call(executeFunctionsMock, 0);
expect(result).toEqual([
{
json: {
messages: [
{
role: 'user',
content: 'Translate {{WORD}} to {{TARGET_LANGUAGE}}',
},
],
system: 'You are a professional {{SOURCE_LANGUAGE}} to {{TARGET_LANGUAGE}} translator',
variable_values: {
WORD: 'hello',
TARGET_LANGUAGE: 'German',
SOURCE_LANGUAGE: 'English',
},
},
pairedItem: { item: 0 },
},
]);
expect(apiRequestMock).toHaveBeenCalledWith('POST', '/v1/experimental/templatize_prompt', {
body: {
messages: [
{
role: 'user',
content: 'Translate hello to German',
},
],
system: 'You are a professional English to German translator',
},
enableAnthropicBetas: {
promptTools: true,
},
});
});
});
});