import { mockDeep } from 'jest-mock-extended'; import type { IExecuteFunctions, ExecuteAgentData } from 'n8n-workflow'; import { NodeOperationError } from 'n8n-workflow'; import { MessageAnAgent } from '../MessageAnAgent.node'; describe('MessageAnAgent Node', () => { let node: MessageAnAgent; let executeFunctions: jest.Mocked; const mockSession = { agentId: 'agent-1', projectId: 'project-1', sessionId: 'exec-123-0', }; const mockAgentResult: ExecuteAgentData = { response: 'Hello from agent', structuredOutput: null, usage: { promptTokens: 10, completionTokens: 20, totalTokens: 30, }, toolCalls: [], finishReason: 'stop', session: mockSession, }; beforeEach(() => { node = new MessageAnAgent(); executeFunctions = mockDeep(); jest.clearAllMocks(); executeFunctions.getNode.mockReturnValue({ id: 'test-node-id', name: 'Message an Agent', type: 'n8n-nodes-base.messageAnAgent', typeVersion: 1, position: [0, 0], parameters: {}, }); executeFunctions.getExecutionId.mockReturnValue('exec-123'); }); it('should send a message and return the agent response', async () => { executeFunctions.getInputData.mockReturnValue([{ json: {} }]); executeFunctions.getNodeParameter.mockImplementation( (param: string, _itemIndex?: number, fallback?: unknown) => { if (param === 'agentId') return { mode: 'id', value: 'agent-1' }; if (param === 'message') return 'Hello agent'; if (param === 'advanced') return fallback ?? {}; return undefined; }, ); executeFunctions.executeAgent.mockResolvedValue(mockAgentResult); const result = await node.execute.call(executeFunctions); expect(executeFunctions.executeAgent).toHaveBeenCalledWith( { agentId: 'agent-1', sessionId: undefined }, 'Hello agent', 'exec-123', 0, ); expect(result).toEqual([ [ { json: { response: 'Hello from agent', structuredOutput: null, usage: { promptTokens: 10, completionTokens: 20, totalTokens: 30 }, toolCalls: [], finishReason: 'stop', session: mockSession, }, pairedItem: { item: 0 }, }, ], ]); }); it('should forward a user-supplied sessionId from the Advanced collection', async () => { executeFunctions.getInputData.mockReturnValue([{ json: {} }]); executeFunctions.getNodeParameter.mockImplementation((param: string) => { if (param === 'agentId') return { mode: 'id', value: 'agent-1' }; if (param === 'message') return 'Hello agent'; if (param === 'advanced') return { sessionId: ' thread-42 ' }; return undefined as unknown as string; }); executeFunctions.executeAgent.mockResolvedValue(mockAgentResult); await node.execute.call(executeFunctions); expect(executeFunctions.executeAgent).toHaveBeenCalledWith( { agentId: 'agent-1', sessionId: 'thread-42' }, 'Hello agent', 'exec-123', 0, ); }); it('should treat a whitespace-only sessionId as no override', async () => { executeFunctions.getInputData.mockReturnValue([{ json: {} }]); executeFunctions.getNodeParameter.mockImplementation((param: string) => { if (param === 'agentId') return { mode: 'id', value: 'agent-1' }; if (param === 'message') return 'Hello agent'; if (param === 'advanced') return { sessionId: ' ' }; return undefined as unknown as string; }); executeFunctions.executeAgent.mockResolvedValue(mockAgentResult); await node.execute.call(executeFunctions); expect(executeFunctions.executeAgent).toHaveBeenCalledWith( { agentId: 'agent-1', sessionId: undefined }, 'Hello agent', 'exec-123', 0, ); }); it('should throw NodeOperationError when message is empty', async () => { executeFunctions.getInputData.mockReturnValue([{ json: {} }]); executeFunctions.getNodeParameter.mockImplementation( (param: string, _itemIndex?: number, fallback?: unknown) => { if (param === 'agentId') return { mode: 'id', value: 'agent-1' }; if (param === 'message') return ' '; if (param === 'advanced') return fallback ?? {}; return undefined; }, ); executeFunctions.continueOnFail.mockReturnValue(false); await expect(node.execute.call(executeFunctions)).rejects.toThrow(NodeOperationError); await expect(node.execute.call(executeFunctions)).rejects.toThrow('Message cannot be empty'); }); it('should process multiple items with different itemIndex values', async () => { executeFunctions.getInputData.mockReturnValue([{ json: {} }, { json: {} }]); executeFunctions.getNodeParameter.mockImplementation( (param: string, itemIndex?: number, fallback?: unknown) => { if (param === 'agentId') return { mode: 'id', value: `agent-${(itemIndex ?? 0) + 1}` }; if (param === 'message') return `Message ${(itemIndex ?? 0) + 1}`; if (param === 'advanced') return fallback ?? {}; return undefined; }, ); const resultForItem0: ExecuteAgentData = { ...mockAgentResult, response: 'Response 1', }; const resultForItem1: ExecuteAgentData = { ...mockAgentResult, response: 'Response 2', }; executeFunctions.executeAgent .mockResolvedValueOnce(resultForItem0) .mockResolvedValueOnce(resultForItem1); const result = await node.execute.call(executeFunctions); expect(executeFunctions.executeAgent).toHaveBeenCalledTimes(2); expect(executeFunctions.executeAgent).toHaveBeenCalledWith( { agentId: 'agent-1', sessionId: undefined }, 'Message 1', 'exec-123', 0, ); expect(executeFunctions.executeAgent).toHaveBeenCalledWith( { agentId: 'agent-2', sessionId: undefined }, 'Message 2', 'exec-123', 1, ); expect(result[0]).toHaveLength(2); expect(result[0][0].json.response).toBe('Response 1'); expect(result[0][0].pairedItem).toEqual({ item: 0 }); expect(result[0][1].json.response).toBe('Response 2'); expect(result[0][1].pairedItem).toEqual({ item: 1 }); }); it('should return error item instead of throwing when continueOnFail is true', async () => { executeFunctions.getInputData.mockReturnValue([{ json: {} }]); executeFunctions.getNodeParameter.mockImplementation( (param: string, _itemIndex?: number, fallback?: unknown) => { if (param === 'agentId') return { mode: 'id', value: 'agent-1' }; if (param === 'message') return 'Hello'; if (param === 'advanced') return fallback ?? {}; return undefined; }, ); executeFunctions.continueOnFail.mockReturnValue(true); executeFunctions.executeAgent.mockRejectedValue(new Error('Agent unavailable')); const result = await node.execute.call(executeFunctions); expect(result).toEqual([ [ { json: { error: 'Agent unavailable' }, pairedItem: { item: 0 }, }, ], ]); }); it('should pass through structuredOutput from agent result', async () => { const structuredResult: ExecuteAgentData = { ...mockAgentResult, structuredOutput: { key: 'value', nested: { data: 123 } }, toolCalls: [{ toolName: 'search', input: { query: 'test' }, result: { found: true } }], }; executeFunctions.getInputData.mockReturnValue([{ json: {} }]); executeFunctions.getNodeParameter.mockImplementation( (param: string, _itemIndex?: number, fallback?: unknown) => { if (param === 'agentId') return { mode: 'list', value: 'agent-1' }; if (param === 'message') return 'Structured query'; if (param === 'advanced') return fallback ?? {}; return undefined; }, ); executeFunctions.executeAgent.mockResolvedValue(structuredResult); const result = await node.execute.call(executeFunctions); expect(result[0][0].json.structuredOutput).toEqual({ key: 'value', nested: { data: 123 }, }); expect(result[0][0].json.toolCalls).toEqual([ { toolName: 'search', input: { query: 'test' }, result: { found: true } }, ]); }); });