mirror of
https://github.com/czlonkowski/n8n-mcp.git
synced 2026-01-30 06:22:04 +00:00
When n8n connects any node to an AI Agent's tool slot, it creates a dynamic Tool variant at runtime (e.g., googleDrive → googleDriveTool). These don't exist in npm packages, causing false "unknown node type" errors. Added validation-time inference: when a *Tool node type is not found, check if the base node exists. If yes, treat as valid with warning. Changes: - workflow-validator.ts: Add INFERRED_TOOL_VARIANT logic - node-similarity-service.ts: Add 98% confidence for valid Tool variants - Added 7 unit tests for inferred tool variant functionality Fixes #522 Conceived by Romuald Członkowski - https://www.aiadvisors.pl/en 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Romuald Członkowski <romualdczlonkowski@MacBook-Pro-Romuald.local> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
893 lines
27 KiB
TypeScript
893 lines
27 KiB
TypeScript
/**
|
|
* 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);
|
|
});
|
|
});
|
|
|
|
describe('validateAllNodes - Inferred Tool Variants (Issue #522)', () => {
|
|
/**
|
|
* Tests for dynamic AI Tool nodes that are created at runtime by n8n
|
|
* when ANY node is used in an AI Agent's tool slot.
|
|
*
|
|
* These nodes (e.g., googleDriveTool, googleSheetsTool) don't exist in npm packages
|
|
* but are valid when the base node exists.
|
|
*/
|
|
|
|
beforeEach(() => {
|
|
// Update mock repository to include Google nodes
|
|
mockRepository.getNode = vi.fn((nodeType: string) => {
|
|
// Base node with Tool variant
|
|
if (nodeType === 'nodes-base.supabase') {
|
|
return {
|
|
nodeType: 'nodes-base.supabase',
|
|
displayName: 'Supabase',
|
|
isAITool: true,
|
|
hasToolVariant: true,
|
|
isToolVariant: false,
|
|
isTrigger: false,
|
|
properties: []
|
|
};
|
|
}
|
|
|
|
// Tool variant in database
|
|
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: []
|
|
};
|
|
}
|
|
|
|
// Google Drive base node (exists, but no Tool variant in DB)
|
|
if (nodeType === 'nodes-base.googleDrive') {
|
|
return {
|
|
nodeType: 'nodes-base.googleDrive',
|
|
displayName: 'Google Drive',
|
|
isAITool: false, // Not marked as AI tool in npm package
|
|
hasToolVariant: false, // No Tool variant in database
|
|
isToolVariant: false,
|
|
isTrigger: false,
|
|
properties: [],
|
|
category: 'files'
|
|
};
|
|
}
|
|
|
|
// Google Sheets base node (exists, but no Tool variant in DB)
|
|
if (nodeType === 'nodes-base.googleSheets') {
|
|
return {
|
|
nodeType: 'nodes-base.googleSheets',
|
|
displayName: 'Google Sheets',
|
|
isAITool: false,
|
|
hasToolVariant: false,
|
|
isToolVariant: false,
|
|
isTrigger: false,
|
|
properties: [],
|
|
category: 'productivity'
|
|
};
|
|
}
|
|
|
|
// AI Agent node
|
|
if (nodeType === 'nodes-langchain.agent') {
|
|
return {
|
|
nodeType: 'nodes-langchain.agent',
|
|
displayName: 'AI Agent',
|
|
isAITool: false,
|
|
hasToolVariant: false,
|
|
isToolVariant: false,
|
|
isTrigger: false,
|
|
properties: []
|
|
};
|
|
}
|
|
|
|
return null; // Unknown node
|
|
}) as any;
|
|
});
|
|
|
|
it('should pass validation for googleDriveTool when googleDrive exists', async () => {
|
|
const workflow = {
|
|
nodes: [
|
|
{
|
|
id: 'drive-tool-1',
|
|
name: 'Google Drive Tool',
|
|
type: 'n8n-nodes-base.googleDriveTool',
|
|
typeVersion: 3,
|
|
position: [250, 300] as [number, number],
|
|
parameters: {}
|
|
}
|
|
],
|
|
connections: {}
|
|
};
|
|
|
|
const result = await validator.validateWorkflow(workflow);
|
|
|
|
// Should NOT have "Unknown node type" error
|
|
const unknownErrors = result.errors.filter(e =>
|
|
e.message && e.message.includes('Unknown node type')
|
|
);
|
|
expect(unknownErrors).toHaveLength(0);
|
|
|
|
// Should have INFERRED_TOOL_VARIANT warning
|
|
const inferredWarnings = result.warnings.filter(e =>
|
|
(e as any).code === 'INFERRED_TOOL_VARIANT'
|
|
);
|
|
expect(inferredWarnings).toHaveLength(1);
|
|
expect(inferredWarnings[0].message).toContain('googleDriveTool');
|
|
expect(inferredWarnings[0].message).toContain('Google Drive');
|
|
});
|
|
|
|
it('should pass validation for googleSheetsTool when googleSheets exists', async () => {
|
|
const workflow = {
|
|
nodes: [
|
|
{
|
|
id: 'sheets-tool-1',
|
|
name: 'Google Sheets Tool',
|
|
type: 'n8n-nodes-base.googleSheetsTool',
|
|
typeVersion: 4,
|
|
position: [250, 300] as [number, number],
|
|
parameters: {}
|
|
}
|
|
],
|
|
connections: {}
|
|
};
|
|
|
|
const result = await validator.validateWorkflow(workflow);
|
|
|
|
// Should NOT have "Unknown node type" error
|
|
const unknownErrors = result.errors.filter(e =>
|
|
e.message && e.message.includes('Unknown node type')
|
|
);
|
|
expect(unknownErrors).toHaveLength(0);
|
|
|
|
// Should have INFERRED_TOOL_VARIANT warning
|
|
const inferredWarnings = result.warnings.filter(e =>
|
|
(e as any).code === 'INFERRED_TOOL_VARIANT'
|
|
);
|
|
expect(inferredWarnings).toHaveLength(1);
|
|
expect(inferredWarnings[0].message).toContain('googleSheetsTool');
|
|
expect(inferredWarnings[0].message).toContain('Google Sheets');
|
|
});
|
|
|
|
it('should report error for unknownNodeTool when base node does not exist', async () => {
|
|
const workflow = {
|
|
nodes: [
|
|
{
|
|
id: 'unknown-tool-1',
|
|
name: 'Unknown Tool',
|
|
type: 'n8n-nodes-base.nonExistentNodeTool',
|
|
typeVersion: 1,
|
|
position: [250, 300] as [number, number],
|
|
parameters: {}
|
|
}
|
|
],
|
|
connections: {}
|
|
};
|
|
|
|
const result = await validator.validateWorkflow(workflow);
|
|
|
|
// Should have "Unknown node type" error
|
|
const unknownErrors = result.errors.filter(e =>
|
|
e.message && e.message.includes('Unknown node type')
|
|
);
|
|
expect(unknownErrors).toHaveLength(1);
|
|
|
|
// Should NOT have INFERRED_TOOL_VARIANT warning
|
|
const inferredWarnings = result.warnings.filter(e =>
|
|
(e as any).code === 'INFERRED_TOOL_VARIANT'
|
|
);
|
|
expect(inferredWarnings).toHaveLength(0);
|
|
});
|
|
|
|
it('should handle multiple inferred tool variants in same workflow', async () => {
|
|
const workflow = {
|
|
nodes: [
|
|
{
|
|
id: 'drive-tool-1',
|
|
name: 'Google Drive Tool',
|
|
type: 'n8n-nodes-base.googleDriveTool',
|
|
typeVersion: 3,
|
|
position: [250, 300] as [number, number],
|
|
parameters: {}
|
|
},
|
|
{
|
|
id: 'sheets-tool-1',
|
|
name: 'Google Sheets Tool',
|
|
type: 'n8n-nodes-base.googleSheetsTool',
|
|
typeVersion: 4,
|
|
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: {
|
|
'Google Drive Tool': {
|
|
ai_tool: [[{ node: 'AI Agent', type: 'ai_tool', index: 0 }]]
|
|
},
|
|
'Google Sheets Tool': {
|
|
ai_tool: [[{ node: 'AI Agent', type: 'ai_tool', index: 0 }]]
|
|
}
|
|
}
|
|
};
|
|
|
|
const result = await validator.validateWorkflow(workflow);
|
|
|
|
// Should NOT have "Unknown node type" errors
|
|
const unknownErrors = result.errors.filter(e =>
|
|
e.message && e.message.includes('Unknown node type')
|
|
);
|
|
expect(unknownErrors).toHaveLength(0);
|
|
|
|
// Should have 2 INFERRED_TOOL_VARIANT warnings
|
|
const inferredWarnings = result.warnings.filter(e =>
|
|
(e as any).code === 'INFERRED_TOOL_VARIANT'
|
|
);
|
|
expect(inferredWarnings).toHaveLength(2);
|
|
});
|
|
|
|
it('should prefer database record over inference for 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: {}
|
|
}
|
|
],
|
|
connections: {}
|
|
};
|
|
|
|
const result = await validator.validateWorkflow(workflow);
|
|
|
|
// Should NOT have "Unknown node type" error
|
|
const unknownErrors = result.errors.filter(e =>
|
|
e.message && e.message.includes('Unknown node type')
|
|
);
|
|
expect(unknownErrors).toHaveLength(0);
|
|
|
|
// Should NOT have INFERRED_TOOL_VARIANT warning (it's in database)
|
|
const inferredWarnings = result.warnings.filter(e =>
|
|
(e as any).code === 'INFERRED_TOOL_VARIANT'
|
|
);
|
|
expect(inferredWarnings).toHaveLength(0);
|
|
});
|
|
|
|
it('should include helpful message in warning', async () => {
|
|
const workflow = {
|
|
nodes: [
|
|
{
|
|
id: 'drive-tool-1',
|
|
name: 'Google Drive Tool',
|
|
type: 'n8n-nodes-base.googleDriveTool',
|
|
typeVersion: 3,
|
|
position: [250, 300] as [number, number],
|
|
parameters: {}
|
|
}
|
|
],
|
|
connections: {}
|
|
};
|
|
|
|
const result = await validator.validateWorkflow(workflow);
|
|
|
|
const inferredWarning = result.warnings.find(e =>
|
|
(e as any).code === 'INFERRED_TOOL_VARIANT'
|
|
);
|
|
|
|
expect(inferredWarning).toBeDefined();
|
|
expect(inferredWarning!.message).toContain('inferred as a dynamic AI Tool variant');
|
|
expect(inferredWarning!.message).toContain('nodes-base.googleDrive');
|
|
expect(inferredWarning!.message).toContain('Google Drive');
|
|
expect(inferredWarning!.message).toContain('AI Agent');
|
|
});
|
|
});
|
|
});
|