mirror of
https://github.com/n8n-io/n8n.git
synced 2025-12-05 19:27:26 -06:00
389 lines
11 KiB
TypeScript
389 lines
11 KiB
TypeScript
import { mock } from 'jest-mock-extended';
|
|
import {
|
|
sleep,
|
|
type IExecuteFunctions,
|
|
type INode,
|
|
type EngineRequest,
|
|
type EngineResponse,
|
|
} from 'n8n-workflow';
|
|
|
|
import { toolsAgentExecute } from '../../agents/ToolsAgent/V3/execute';
|
|
import type { RequestResponseMetadata } from '../../agents/ToolsAgent/V3/execute';
|
|
import * as helpers from '../../agents/ToolsAgent/V3/helpers';
|
|
|
|
// Mock the helper modules
|
|
jest.mock('../../agents/ToolsAgent/V3/helpers', () => ({
|
|
buildExecutionContext: jest.fn(),
|
|
executeBatch: jest.fn(),
|
|
checkMaxIterations: jest.fn(),
|
|
buildResponseMetadata: jest.fn(),
|
|
}));
|
|
|
|
// Mock langchain modules
|
|
jest.mock('@langchain/classic/agents', () => ({
|
|
createToolCallingAgent: jest.fn(),
|
|
}));
|
|
|
|
jest.mock('@langchain/core/runnables', () => ({
|
|
RunnableSequence: {
|
|
from: jest.fn(),
|
|
},
|
|
}));
|
|
|
|
jest.mock('n8n-workflow', () => ({
|
|
...jest.requireActual('n8n-workflow'),
|
|
sleep: jest.fn(),
|
|
}));
|
|
|
|
const mockContext = mock<IExecuteFunctions>();
|
|
const mockNode = mock<INode>();
|
|
|
|
beforeEach(() => {
|
|
jest.clearAllMocks();
|
|
mockContext.getNode.mockReturnValue(mockNode);
|
|
mockContext.logger = {
|
|
debug: jest.fn(),
|
|
info: jest.fn(),
|
|
warn: jest.fn(),
|
|
error: jest.fn(),
|
|
};
|
|
});
|
|
|
|
describe('toolsAgentExecute V3 - Execute Function Logic', () => {
|
|
it('should build execution context and process single batch', async () => {
|
|
const mockExecutionContext = {
|
|
items: [{ json: { text: 'test input 1' } }],
|
|
batchSize: 1,
|
|
delayBetweenBatches: 0,
|
|
needsFallback: false,
|
|
model: {} as any,
|
|
fallbackModel: null,
|
|
memory: undefined,
|
|
};
|
|
|
|
const mockBatchResult = {
|
|
returnData: [{ json: { output: 'success 1' }, pairedItem: { item: 0 } }],
|
|
request: undefined,
|
|
};
|
|
|
|
jest.spyOn(helpers, 'buildExecutionContext').mockResolvedValue(mockExecutionContext);
|
|
jest.spyOn(helpers, 'executeBatch').mockResolvedValue(mockBatchResult);
|
|
|
|
const result = await toolsAgentExecute.call(mockContext);
|
|
|
|
expect(helpers.buildExecutionContext).toHaveBeenCalledWith(mockContext);
|
|
expect(helpers.executeBatch).toHaveBeenCalledTimes(1);
|
|
expect(helpers.executeBatch).toHaveBeenCalledWith(
|
|
mockContext,
|
|
mockExecutionContext.items.slice(0, 1),
|
|
0,
|
|
mockExecutionContext.model,
|
|
mockExecutionContext.fallbackModel,
|
|
mockExecutionContext.memory,
|
|
undefined,
|
|
);
|
|
expect(result).toEqual([[{ json: { output: 'success 1' }, pairedItem: { item: 0 } }]]);
|
|
});
|
|
|
|
it('should process multiple batches sequentially', async () => {
|
|
const mockExecutionContext = {
|
|
items: [
|
|
{ json: { text: 'test input 1' } },
|
|
{ json: { text: 'test input 2' } },
|
|
{ json: { text: 'test input 3' } },
|
|
],
|
|
batchSize: 2,
|
|
delayBetweenBatches: 0,
|
|
needsFallback: false,
|
|
model: {} as any,
|
|
fallbackModel: null,
|
|
memory: undefined,
|
|
};
|
|
|
|
const mockBatchResult1 = {
|
|
returnData: [
|
|
{ json: { output: 'success 1' }, pairedItem: { item: 0 } },
|
|
{ json: { output: 'success 2' }, pairedItem: { item: 1 } },
|
|
],
|
|
request: undefined,
|
|
};
|
|
|
|
const mockBatchResult2 = {
|
|
returnData: [{ json: { output: 'success 3' }, pairedItem: { item: 2 } }],
|
|
request: undefined,
|
|
};
|
|
|
|
jest.spyOn(helpers, 'buildExecutionContext').mockResolvedValue(mockExecutionContext);
|
|
jest
|
|
.spyOn(helpers, 'executeBatch')
|
|
.mockResolvedValueOnce(mockBatchResult1)
|
|
.mockResolvedValueOnce(mockBatchResult2);
|
|
|
|
const result = await toolsAgentExecute.call(mockContext);
|
|
|
|
expect(helpers.executeBatch).toHaveBeenCalledTimes(2);
|
|
expect(helpers.executeBatch).toHaveBeenNthCalledWith(
|
|
1,
|
|
mockContext,
|
|
mockExecutionContext.items.slice(0, 2),
|
|
0,
|
|
mockExecutionContext.model,
|
|
mockExecutionContext.fallbackModel,
|
|
mockExecutionContext.memory,
|
|
undefined,
|
|
);
|
|
expect(helpers.executeBatch).toHaveBeenNthCalledWith(
|
|
2,
|
|
mockContext,
|
|
mockExecutionContext.items.slice(2, 3),
|
|
2,
|
|
mockExecutionContext.model,
|
|
mockExecutionContext.fallbackModel,
|
|
mockExecutionContext.memory,
|
|
undefined,
|
|
);
|
|
expect(result).toEqual([
|
|
[
|
|
{ json: { output: 'success 1' }, pairedItem: { item: 0 } },
|
|
{ json: { output: 'success 2' }, pairedItem: { item: 1 } },
|
|
{ json: { output: 'success 3' }, pairedItem: { item: 2 } },
|
|
],
|
|
]);
|
|
});
|
|
|
|
it('should return request when batch returns tool call request', async () => {
|
|
const mockExecutionContext = {
|
|
items: [{ json: { text: 'test input 1' } }],
|
|
batchSize: 1,
|
|
delayBetweenBatches: 0,
|
|
needsFallback: false,
|
|
model: {} as any,
|
|
fallbackModel: null,
|
|
memory: undefined,
|
|
};
|
|
|
|
const mockRequest: EngineRequest<RequestResponseMetadata> = {
|
|
actions: [
|
|
{
|
|
actionType: 'ExecutionNodeAction' as const,
|
|
nodeName: 'Test Tool',
|
|
input: { input: 'test data' },
|
|
type: 'ai_tool',
|
|
id: 'call_123',
|
|
metadata: { itemIndex: 0 },
|
|
},
|
|
],
|
|
metadata: { previousRequests: [] },
|
|
};
|
|
|
|
const mockBatchResult = {
|
|
returnData: [],
|
|
request: mockRequest,
|
|
};
|
|
|
|
jest.spyOn(helpers, 'buildExecutionContext').mockResolvedValue(mockExecutionContext);
|
|
jest.spyOn(helpers, 'executeBatch').mockResolvedValue(mockBatchResult);
|
|
|
|
const result = await toolsAgentExecute.call(mockContext);
|
|
|
|
expect(result).toEqual(mockRequest);
|
|
});
|
|
|
|
it('should merge requests from multiple batches', async () => {
|
|
const mockExecutionContext = {
|
|
items: [{ json: { text: 'test input 1' } }, { json: { text: 'test input 2' } }],
|
|
batchSize: 1,
|
|
delayBetweenBatches: 0,
|
|
needsFallback: false,
|
|
model: {} as any,
|
|
fallbackModel: null,
|
|
memory: undefined,
|
|
};
|
|
|
|
const mockRequest1: EngineRequest<RequestResponseMetadata> = {
|
|
actions: [
|
|
{
|
|
actionType: 'ExecutionNodeAction' as const,
|
|
nodeName: 'Test Tool 1',
|
|
input: { input: 'test data 1' },
|
|
type: 'ai_tool',
|
|
id: 'call_123',
|
|
metadata: { itemIndex: 0 },
|
|
},
|
|
],
|
|
metadata: { previousRequests: [] },
|
|
};
|
|
|
|
const mockRequest2: EngineRequest<RequestResponseMetadata> = {
|
|
actions: [
|
|
{
|
|
actionType: 'ExecutionNodeAction' as const,
|
|
nodeName: 'Test Tool 2',
|
|
input: { input: 'test data 2' },
|
|
type: 'ai_tool',
|
|
id: 'call_456',
|
|
metadata: { itemIndex: 1 },
|
|
},
|
|
],
|
|
metadata: { previousRequests: [] },
|
|
};
|
|
|
|
jest.spyOn(helpers, 'buildExecutionContext').mockResolvedValue(mockExecutionContext);
|
|
jest
|
|
.spyOn(helpers, 'executeBatch')
|
|
.mockResolvedValueOnce({ returnData: [], request: mockRequest1 })
|
|
.mockResolvedValueOnce({ returnData: [], request: mockRequest2 });
|
|
|
|
const result = (await toolsAgentExecute.call(
|
|
mockContext,
|
|
)) as EngineRequest<RequestResponseMetadata>;
|
|
|
|
expect(result.actions).toHaveLength(2);
|
|
expect(result.actions[0].nodeName).toBe('Test Tool 1');
|
|
expect(result.actions[1].nodeName).toBe('Test Tool 2');
|
|
});
|
|
|
|
it('should apply delay between batches when configured', async () => {
|
|
const sleepMock = sleep as jest.MockedFunction<typeof sleep>;
|
|
sleepMock.mockResolvedValue(undefined);
|
|
|
|
const mockExecutionContext = {
|
|
items: [{ json: { text: 'test input 1' } }, { json: { text: 'test input 2' } }],
|
|
batchSize: 1,
|
|
delayBetweenBatches: 1000,
|
|
needsFallback: false,
|
|
model: {} as any,
|
|
fallbackModel: null,
|
|
memory: undefined,
|
|
};
|
|
|
|
const mockBatchResult = {
|
|
returnData: [{ json: { output: 'success' }, pairedItem: { item: 0 } }],
|
|
request: undefined,
|
|
};
|
|
|
|
jest.spyOn(helpers, 'buildExecutionContext').mockResolvedValue(mockExecutionContext);
|
|
jest.spyOn(helpers, 'executeBatch').mockResolvedValue(mockBatchResult);
|
|
|
|
await toolsAgentExecute.call(mockContext);
|
|
|
|
expect(sleepMock).toHaveBeenCalledWith(1000);
|
|
expect(sleepMock).toHaveBeenCalledTimes(1); // Only between batches, not after the last one
|
|
});
|
|
|
|
it('should not apply delay after last batch', async () => {
|
|
const sleepMock = sleep as jest.MockedFunction<typeof sleep>;
|
|
sleepMock.mockResolvedValue(undefined);
|
|
|
|
const mockExecutionContext = {
|
|
items: [{ json: { text: 'test input 1' } }],
|
|
batchSize: 1,
|
|
delayBetweenBatches: 1000,
|
|
needsFallback: false,
|
|
model: {} as any,
|
|
fallbackModel: null,
|
|
memory: undefined,
|
|
};
|
|
|
|
const mockBatchResult = {
|
|
returnData: [{ json: { output: 'success' }, pairedItem: { item: 0 } }],
|
|
request: undefined,
|
|
};
|
|
|
|
jest.spyOn(helpers, 'buildExecutionContext').mockResolvedValue(mockExecutionContext);
|
|
jest.spyOn(helpers, 'executeBatch').mockResolvedValue(mockBatchResult);
|
|
|
|
await toolsAgentExecute.call(mockContext);
|
|
|
|
expect(sleepMock).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should pass response parameter to executeBatch', async () => {
|
|
const mockExecutionContext = {
|
|
items: [{ json: { text: 'test input 1' } }],
|
|
batchSize: 1,
|
|
delayBetweenBatches: 0,
|
|
needsFallback: false,
|
|
model: {} as any,
|
|
fallbackModel: null,
|
|
memory: undefined,
|
|
};
|
|
|
|
const mockBatchResult = {
|
|
returnData: [{ json: { output: 'success' }, pairedItem: { item: 0 } }],
|
|
request: undefined,
|
|
};
|
|
|
|
const mockResponse: EngineResponse<RequestResponseMetadata> = {
|
|
actionResponses: [
|
|
{
|
|
action: {
|
|
id: 'call_123',
|
|
nodeName: 'Test Tool',
|
|
input: { input: 'test data', id: 'call_123' },
|
|
metadata: { itemIndex: 0 },
|
|
actionType: 'ExecutionNodeAction',
|
|
type: 'ai_tool',
|
|
},
|
|
data: {
|
|
data: { ai_tool: [[{ json: { result: 'tool result' } }]] },
|
|
executionTime: 0,
|
|
startTime: 0,
|
|
executionIndex: 0,
|
|
source: [],
|
|
},
|
|
},
|
|
],
|
|
metadata: { itemIndex: 0, previousRequests: [] },
|
|
};
|
|
|
|
jest.spyOn(helpers, 'buildExecutionContext').mockResolvedValue(mockExecutionContext);
|
|
jest.spyOn(helpers, 'executeBatch').mockResolvedValue(mockBatchResult);
|
|
|
|
await toolsAgentExecute.call(mockContext, mockResponse);
|
|
|
|
expect(helpers.executeBatch).toHaveBeenCalledWith(
|
|
mockContext,
|
|
mockExecutionContext.items.slice(0, 1),
|
|
0,
|
|
mockExecutionContext.model,
|
|
mockExecutionContext.fallbackModel,
|
|
mockExecutionContext.memory,
|
|
mockResponse,
|
|
);
|
|
});
|
|
|
|
it('should collect return data from multiple batches', async () => {
|
|
const mockExecutionContext = {
|
|
items: [{ json: { text: 'test input 1' } }, { json: { text: 'test input 2' } }],
|
|
batchSize: 1,
|
|
delayBetweenBatches: 0,
|
|
needsFallback: false,
|
|
model: {} as any,
|
|
fallbackModel: null,
|
|
memory: undefined,
|
|
};
|
|
|
|
jest.spyOn(helpers, 'buildExecutionContext').mockResolvedValue(mockExecutionContext);
|
|
jest
|
|
.spyOn(helpers, 'executeBatch')
|
|
.mockResolvedValueOnce({
|
|
returnData: [{ json: { output: 'success 1' }, pairedItem: { item: 0 } }],
|
|
request: undefined,
|
|
})
|
|
.mockResolvedValueOnce({
|
|
returnData: [{ json: { output: 'success 2' }, pairedItem: { item: 1 } }],
|
|
request: undefined,
|
|
});
|
|
|
|
const result = await toolsAgentExecute.call(mockContext);
|
|
|
|
expect(result).toEqual([
|
|
[
|
|
{ json: { output: 'success 1' }, pairedItem: { item: 0 } },
|
|
{ json: { output: 'success 2' }, pairedItem: { item: 1 } },
|
|
],
|
|
]);
|
|
});
|
|
});
|