Skip to main content
Glama
workflow-validator-tool-variants.test.ts18.6 kB
/** * Tests for WorkflowValidator - Tool Variant Validation * * Tests the validateAIToolSource() method which ensures that base nodes * with ai_tool connections use the correct Tool variant node type. * * Coverage: * - Langchain tool nodes pass validation * - Tool variant nodes pass validation * - Base nodes with Tool variants fail with WRONG_NODE_TYPE_FOR_AI_TOOL * - Error includes fix suggestion with tool-variant-correction type * - Unknown nodes don't cause errors */ import { describe, it, expect, beforeEach, vi } from 'vitest'; import { WorkflowValidator } from '@/services/workflow-validator'; import { NodeRepository } from '@/database/node-repository'; import { EnhancedConfigValidator } from '@/services/enhanced-config-validator'; // Mock dependencies vi.mock('@/database/node-repository'); vi.mock('@/services/enhanced-config-validator'); vi.mock('@/utils/logger'); describe('WorkflowValidator - Tool Variant Validation', () => { let validator: WorkflowValidator; let mockRepository: NodeRepository; let mockValidator: typeof EnhancedConfigValidator; beforeEach(() => { vi.clearAllMocks(); // Create mock repository mockRepository = { getNode: vi.fn((nodeType: string) => { // Mock base node with Tool variant available if (nodeType === 'nodes-base.supabase') { return { nodeType: 'nodes-base.supabase', displayName: 'Supabase', isAITool: true, hasToolVariant: true, isToolVariant: false, isTrigger: false, properties: [] }; } // Mock Tool variant node if (nodeType === 'nodes-base.supabaseTool') { return { nodeType: 'nodes-base.supabaseTool', displayName: 'Supabase Tool', isAITool: true, hasToolVariant: false, isToolVariant: true, toolVariantOf: 'nodes-base.supabase', isTrigger: false, properties: [] }; } // Mock langchain node (Calculator tool) if (nodeType === 'nodes-langchain.toolCalculator') { return { nodeType: 'nodes-langchain.toolCalculator', displayName: 'Calculator', isAITool: true, hasToolVariant: false, isToolVariant: false, isTrigger: false, properties: [] }; } // Mock HTTP Request Tool node if (nodeType === 'nodes-langchain.toolHttpRequest') { return { nodeType: 'nodes-langchain.toolHttpRequest', displayName: 'HTTP Request Tool', isAITool: true, hasToolVariant: false, isToolVariant: false, isTrigger: false, properties: [] }; } // Mock base node without Tool variant if (nodeType === 'nodes-base.httpRequest') { return { nodeType: 'nodes-base.httpRequest', displayName: 'HTTP Request', isAITool: false, hasToolVariant: false, isToolVariant: false, isTrigger: false, properties: [] }; } return null; // Unknown node }) } as any; mockValidator = EnhancedConfigValidator; validator = new WorkflowValidator(mockRepository, mockValidator); }); describe('validateAIToolSource - Langchain tool nodes', () => { it('should pass validation for Calculator tool node', async () => { const workflow = { nodes: [ { id: 'calculator-1', name: 'Calculator', type: 'n8n-nodes-langchain.toolCalculator', typeVersion: 1.2, position: [250, 300] as [number, number], parameters: {} }, { id: 'agent-1', name: 'AI Agent', type: '@n8n/n8n-nodes-langchain.agent', typeVersion: 1.7, position: [450, 300] as [number, number], parameters: {} } ], connections: { Calculator: { ai_tool: [[{ node: 'AI Agent', type: 'ai_tool', index: 0 }]] } } }; const result = await validator.validateWorkflow(workflow); // Should not have errors about wrong node type for AI tool const toolVariantErrors = result.errors.filter(e => e.code === 'WRONG_NODE_TYPE_FOR_AI_TOOL' ); expect(toolVariantErrors).toHaveLength(0); }); it('should pass validation for HTTP Request Tool node', async () => { const workflow = { nodes: [ { id: 'http-tool-1', name: 'HTTP Request Tool', type: '@n8n/n8n-nodes-langchain.toolHttpRequest', typeVersion: 1.2, position: [250, 300] as [number, number], parameters: { url: 'https://api.example.com', toolDescription: 'Fetch data from API' } }, { id: 'agent-1', name: 'AI Agent', type: '@n8n/n8n-nodes-langchain.agent', typeVersion: 1.7, position: [450, 300] as [number, number], parameters: {} } ], connections: { 'HTTP Request Tool': { ai_tool: [[{ node: 'AI Agent', type: 'ai_tool', index: 0 }]] } } }; const result = await validator.validateWorkflow(workflow); const toolVariantErrors = result.errors.filter(e => e.code === 'WRONG_NODE_TYPE_FOR_AI_TOOL' ); expect(toolVariantErrors).toHaveLength(0); }); }); describe('validateAIToolSource - Tool variant nodes', () => { it('should pass validation for Tool variant node (supabaseTool)', async () => { const workflow = { nodes: [ { id: 'supabase-tool-1', name: 'Supabase Tool', type: 'n8n-nodes-base.supabaseTool', typeVersion: 1, position: [250, 300] as [number, number], parameters: { toolDescription: 'Query Supabase database' } }, { id: 'agent-1', name: 'AI Agent', type: '@n8n/n8n-nodes-langchain.agent', typeVersion: 1.7, position: [450, 300] as [number, number], parameters: {} } ], connections: { 'Supabase Tool': { ai_tool: [[{ node: 'AI Agent', type: 'ai_tool', index: 0 }]] } } }; const result = await validator.validateWorkflow(workflow); const toolVariantErrors = result.errors.filter(e => e.code === 'WRONG_NODE_TYPE_FOR_AI_TOOL' ); expect(toolVariantErrors).toHaveLength(0); }); it('should verify Tool variant is marked correctly in database', async () => { const workflow = { nodes: [ { id: 'supabase-tool-1', name: 'Supabase Tool', type: 'n8n-nodes-base.supabaseTool', typeVersion: 1, position: [250, 300] as [number, number], parameters: {} } ], connections: { 'Supabase Tool': { ai_tool: [[{ node: 'AI Agent', type: 'ai_tool', index: 0 }]] } } }; await validator.validateWorkflow(workflow); // Verify repository was called to check if it's a Tool variant expect(mockRepository.getNode).toHaveBeenCalledWith('nodes-base.supabaseTool'); }); }); describe('validateAIToolSource - Base nodes with Tool variants', () => { it('should fail when base node is used instead of Tool variant', async () => { const workflow = { nodes: [ { id: 'supabase-1', name: 'Supabase', type: 'n8n-nodes-base.supabase', typeVersion: 1, position: [250, 300] as [number, number], parameters: {} }, { id: 'agent-1', name: 'AI Agent', type: '@n8n/n8n-nodes-langchain.agent', typeVersion: 1.7, position: [450, 300] as [number, number], parameters: {} } ], connections: { Supabase: { ai_tool: [[{ node: 'AI Agent', type: 'ai_tool', index: 0 }]] } } }; const result = await validator.validateWorkflow(workflow); // Should have error with WRONG_NODE_TYPE_FOR_AI_TOOL code const toolVariantErrors = result.errors.filter(e => e.code === 'WRONG_NODE_TYPE_FOR_AI_TOOL' ); expect(toolVariantErrors).toHaveLength(1); }); it('should include fix suggestion in error', async () => { const workflow = { nodes: [ { id: 'supabase-1', name: 'Supabase', type: 'n8n-nodes-base.supabase', typeVersion: 1, position: [250, 300] as [number, number], parameters: {} }, { id: 'agent-1', name: 'AI Agent', type: '@n8n/n8n-nodes-langchain.agent', typeVersion: 1.7, position: [450, 300] as [number, number], parameters: {} } ], connections: { Supabase: { ai_tool: [[{ node: 'AI Agent', type: 'ai_tool', index: 0 }]] } } }; const result = await validator.validateWorkflow(workflow); const toolVariantError = result.errors.find(e => e.code === 'WRONG_NODE_TYPE_FOR_AI_TOOL' ) as any; expect(toolVariantError).toBeDefined(); expect(toolVariantError.fix).toBeDefined(); expect(toolVariantError.fix.type).toBe('tool-variant-correction'); expect(toolVariantError.fix.currentType).toBe('n8n-nodes-base.supabase'); expect(toolVariantError.fix.suggestedType).toBe('n8n-nodes-base.supabaseTool'); expect(toolVariantError.fix.description).toContain('n8n-nodes-base.supabase'); expect(toolVariantError.fix.description).toContain('n8n-nodes-base.supabaseTool'); }); it('should provide clear error message', async () => { const workflow = { nodes: [ { id: 'supabase-1', name: 'Supabase', type: 'n8n-nodes-base.supabase', typeVersion: 1, position: [250, 300] as [number, number], parameters: {} }, { id: 'agent-1', name: 'AI Agent', type: '@n8n/n8n-nodes-langchain.agent', typeVersion: 1.7, position: [450, 300] as [number, number], parameters: {} } ], connections: { Supabase: { ai_tool: [[{ node: 'AI Agent', type: 'ai_tool', index: 0 }]] } } }; const result = await validator.validateWorkflow(workflow); const toolVariantError = result.errors.find(e => e.code === 'WRONG_NODE_TYPE_FOR_AI_TOOL' ); expect(toolVariantError).toBeDefined(); expect(toolVariantError!.message).toContain('cannot output ai_tool connections'); expect(toolVariantError!.message).toContain('Tool variant'); expect(toolVariantError!.message).toContain('n8n-nodes-base.supabaseTool'); }); it('should handle multiple base nodes incorrectly used as tools', async () => { mockRepository.getNode = vi.fn((nodeType: string) => { if (nodeType === 'nodes-base.postgres') { return { nodeType: 'nodes-base.postgres', displayName: 'Postgres', isAITool: true, hasToolVariant: true, isToolVariant: false, isTrigger: false, properties: [] }; } if (nodeType === 'nodes-base.supabase') { return { nodeType: 'nodes-base.supabase', displayName: 'Supabase', isAITool: true, hasToolVariant: true, isToolVariant: false, isTrigger: false, properties: [] }; } return null; }) as any; const workflow = { nodes: [ { id: 'postgres-1', name: 'Postgres', type: 'n8n-nodes-base.postgres', typeVersion: 1, position: [250, 300] as [number, number], parameters: {} }, { id: 'supabase-1', name: 'Supabase', type: 'n8n-nodes-base.supabase', typeVersion: 1, position: [250, 400] as [number, number], parameters: {} }, { id: 'agent-1', name: 'AI Agent', type: '@n8n/n8n-nodes-langchain.agent', typeVersion: 1.7, position: [450, 300] as [number, number], parameters: {} } ], connections: { Postgres: { ai_tool: [[{ node: 'AI Agent', type: 'ai_tool', index: 0 }]] }, Supabase: { ai_tool: [[{ node: 'AI Agent', type: 'ai_tool', index: 0 }]] } } }; const result = await validator.validateWorkflow(workflow); const toolVariantErrors = result.errors.filter(e => e.code === 'WRONG_NODE_TYPE_FOR_AI_TOOL' ); expect(toolVariantErrors).toHaveLength(2); }); }); describe('validateAIToolSource - Unknown nodes', () => { it('should not error for unknown node types', async () => { const workflow = { nodes: [ { id: 'unknown-1', name: 'Unknown Tool', type: 'custom-package.unknownTool', typeVersion: 1, position: [250, 300] as [number, number], parameters: {} }, { id: 'agent-1', name: 'AI Agent', type: '@n8n/n8n-nodes-langchain.agent', typeVersion: 1.7, position: [450, 300] as [number, number], parameters: {} } ], connections: { 'Unknown Tool': { ai_tool: [[{ node: 'AI Agent', type: 'ai_tool', index: 0 }]] } } }; const result = await validator.validateWorkflow(workflow); // Unknown nodes should not cause tool variant errors // Let other validation handle unknown node types const toolVariantErrors = result.errors.filter(e => e.code === 'WRONG_NODE_TYPE_FOR_AI_TOOL' ); expect(toolVariantErrors).toHaveLength(0); // But there might be an "Unknown node type" error from different validation const unknownNodeErrors = result.errors.filter(e => e.message && e.message.includes('Unknown node type') ); expect(unknownNodeErrors.length).toBeGreaterThan(0); }); it('should not error for community nodes', async () => { const workflow = { nodes: [ { id: 'community-1', name: 'Community Tool', type: 'community-package.customTool', typeVersion: 1, position: [250, 300] as [number, number], parameters: {} }, { id: 'agent-1', name: 'AI Agent', type: '@n8n/n8n-nodes-langchain.agent', typeVersion: 1.7, position: [450, 300] as [number, number], parameters: {} } ], connections: { 'Community Tool': { ai_tool: [[{ node: 'AI Agent', type: 'ai_tool', index: 0 }]] } } }; const result = await validator.validateWorkflow(workflow); // Community nodes should not cause tool variant errors const toolVariantErrors = result.errors.filter(e => e.code === 'WRONG_NODE_TYPE_FOR_AI_TOOL' ); expect(toolVariantErrors).toHaveLength(0); }); }); describe('validateAIToolSource - Edge cases', () => { it('should not error for base nodes without ai_tool connections', async () => { const workflow = { nodes: [ { id: 'supabase-1', name: 'Supabase', type: 'n8n-nodes-base.supabase', typeVersion: 1, position: [250, 300] as [number, number], parameters: {} }, { id: 'set-1', name: 'Set', type: 'n8n-nodes-base.set', typeVersion: 1, position: [450, 300] as [number, number], parameters: {} } ], connections: { Supabase: { main: [[{ node: 'Set', type: 'main', index: 0 }]] } } }; const result = await validator.validateWorkflow(workflow); // No ai_tool connections, so no tool variant validation errors const toolVariantErrors = result.errors.filter(e => e.code === 'WRONG_NODE_TYPE_FOR_AI_TOOL' ); expect(toolVariantErrors).toHaveLength(0); }); it('should not error when base node without Tool variant uses ai_tool', async () => { const workflow = { nodes: [ { id: 'http-1', name: 'HTTP Request', type: 'n8n-nodes-base.httpRequest', typeVersion: 1, position: [250, 300] as [number, number], parameters: {} }, { id: 'agent-1', name: 'AI Agent', type: '@n8n/n8n-nodes-langchain.agent', typeVersion: 1.7, position: [450, 300] as [number, number], parameters: {} } ], connections: { 'HTTP Request': { ai_tool: [[{ node: 'AI Agent', type: 'ai_tool', index: 0 }]] } } }; const result = await validator.validateWorkflow(workflow); // httpRequest has no Tool variant, so this should produce a different error const toolVariantErrors = result.errors.filter(e => e.code === 'WRONG_NODE_TYPE_FOR_AI_TOOL' ); expect(toolVariantErrors).toHaveLength(0); // Should have INVALID_AI_TOOL_SOURCE error instead const invalidToolErrors = result.errors.filter(e => e.code === 'INVALID_AI_TOOL_SOURCE' ); expect(invalidToolErrors.length).toBeGreaterThan(0); }); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/czlonkowski/n8n-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server