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

189 lines
4.9 KiB
TypeScript

import { ChatPromptTemplate, SystemMessagePromptTemplate } from '@langchain/core/prompts';
import { FakeChatModel } from '@langchain/core/utils/testing';
import { mock } from 'jest-mock-extended';
import type { IExecuteFunctions } from 'n8n-workflow';
import { NodeOperationError } from 'n8n-workflow';
import * as tracing from '@utils/tracing';
import { processItem } from '../processItem';
jest.mock('@utils/tracing', () => ({
getTracingConfig: jest.fn(() => ({})),
}));
jest.mock('@langchain/core/prompts', () => ({
ChatPromptTemplate: {
fromMessages: jest.fn(),
},
SystemMessagePromptTemplate: {
fromTemplate: jest.fn().mockReturnValue({
format: jest.fn(),
}),
},
}));
describe('processItem', () => {
let mockContext: jest.Mocked<IExecuteFunctions>;
let fakeLLM: FakeChatModel;
beforeEach(() => {
mockContext = mock<IExecuteFunctions>();
fakeLLM = new FakeChatModel({});
mockContext.getNodeParameter.mockImplementation((param, _itemIndex, defaultValue) => {
if (param === 'inputText') return 'Test input';
if (param === 'options.systemPromptTemplate') return 'Test system prompt';
return defaultValue;
});
jest.clearAllMocks();
});
it('should throw error for empty input text', async () => {
mockContext.getNodeParameter.mockImplementation((param, _itemIndex, defaultValue) => {
if (param === 'inputText') return '';
return defaultValue;
});
await expect(
processItem(
mockContext,
0,
{ json: {} },
fakeLLM,
{ getFormatInstructions: () => 'format instructions' } as any,
[{ category: 'test', description: 'test category' }],
'multi class prompt',
undefined,
),
).rejects.toThrow(NodeOperationError);
});
it('should process item with correct parameters', async () => {
const mockParser = {
getFormatInstructions: () => '[format instructions]',
};
const mockChain = {
invoke: jest.fn().mockResolvedValue({ test: true }),
};
const mockPipe = jest.fn().mockReturnValue({
pipe: jest.fn().mockReturnValue({
withConfig: jest.fn().mockReturnValue(mockChain),
}),
});
const mockPrompt = {
pipe: mockPipe,
};
jest.mocked(ChatPromptTemplate.fromMessages).mockReturnValue(mockPrompt as any);
const result = await processItem(
mockContext,
0,
{ json: {} },
fakeLLM,
mockParser as any,
[{ category: 'test', description: 'test category' }],
'multi class prompt',
undefined,
);
expect(result).toEqual({ test: true });
expect(mockContext.getNodeParameter).toHaveBeenCalledWith('inputText', 0);
expect(mockContext.getNodeParameter).toHaveBeenCalledWith(
'options.systemPromptTemplate',
0,
expect.any(String),
);
expect(tracing.getTracingConfig).toHaveBeenCalledWith(mockContext);
});
it('should escape curly braces in custom system prompt template', async () => {
const templateWithBraces = 'Classify using format {"category": "value"}: {categories}';
mockContext.getNodeParameter.mockImplementation((param, _itemIndex, defaultValue) => {
if (param === 'inputText') return 'Test input';
if (param === 'options.systemPromptTemplate') return templateWithBraces;
return defaultValue;
});
const mockParser = {
getFormatInstructions: () => '[format instructions]',
};
const mockChain = {
invoke: jest.fn().mockResolvedValue({ category: 'test' }),
};
const mockPipe = jest.fn().mockReturnValue({
pipe: jest.fn().mockReturnValue({
withConfig: jest.fn().mockReturnValue(mockChain),
}),
});
jest.mocked(ChatPromptTemplate.fromMessages).mockReturnValue({ pipe: mockPipe } as any);
await processItem(
mockContext,
0,
{ json: {} },
fakeLLM,
mockParser as any,
[{ category: 'test', description: 'test category' }],
'multi class prompt',
undefined,
);
expect(SystemMessagePromptTemplate.fromTemplate).toHaveBeenCalledWith(
expect.stringContaining('{{"category": "value"}}'),
);
expect(SystemMessagePromptTemplate.fromTemplate).toHaveBeenCalledWith(
expect.stringContaining('{categories}'),
);
});
it('should handle fallback prompt', async () => {
const mockParser = {
getFormatInstructions: () => '[format instructions]',
};
const mockChain = {
invoke: jest.fn().mockResolvedValue({ category: 'test' }),
};
const mockPipe = jest.fn().mockReturnValue({
pipe: jest.fn().mockReturnValue({
withConfig: jest.fn().mockReturnValue(mockChain),
}),
});
const mockPrompt = {
pipe: mockPipe,
};
jest.mocked(ChatPromptTemplate.fromMessages).mockReturnValue(mockPrompt as any);
await processItem(
mockContext,
0,
{ json: {} },
fakeLLM,
mockParser as any,
[{ category: 'test', description: 'test category' }],
'multi class prompt',
'fallback prompt',
);
expect(mockContext.getNodeParameter).toHaveBeenCalledWith(
'options.systemPromptTemplate',
0,
expect.any(String),
);
expect(tracing.getTracingConfig).toHaveBeenCalledWith(mockContext);
});
});