fix: enhance error output validation to detect incorrect configurations

- Add validateErrorOutputConfiguration method to detect when multiple nodes are incorrectly placed in main[0]
- Fix checkWorkflowPatterns to check main[1] for error outputs instead of outputs.error
- Cross-validate onError property matches actual connection structure
- Provide clear error messages with JSON examples showing correct configuration
- Use heuristic detection for error handler nodes (names containing error, fail, catch, etc.)
- Add comprehensive test coverage with 16+ test cases
- Bump version to 2.12.1

Fixes issues where AI agents would incorrectly configure error outputs by placing multiple nodes in the same array instead of separating them into success (main[0]) and error (main[1]) paths.

🤖 Generated with Claude Code (https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
czlonkowski
2025-09-22 21:05:27 +02:00
parent c5aebc1450
commit 1a926630b8
11 changed files with 3117 additions and 5 deletions

View File

@@ -2,7 +2,7 @@
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![GitHub stars](https://img.shields.io/github/stars/czlonkowski/n8n-mcp?style=social)](https://github.com/czlonkowski/n8n-mcp)
[![Version](https://img.shields.io/badge/version-2.12.0-blue.svg)](https://github.com/czlonkowski/n8n-mcp)
[![Version](https://img.shields.io/badge/version-2.12.1-blue.svg)](https://github.com/czlonkowski/n8n-mcp)
[![npm version](https://img.shields.io/npm/v/n8n-mcp.svg)](https://www.npmjs.com/package/n8n-mcp)
[![codecov](https://codecov.io/gh/czlonkowski/n8n-mcp/graph/badge.svg?token=YOUR_TOKEN)](https://codecov.io/gh/czlonkowski/n8n-mcp)
[![Tests](https://img.shields.io/badge/tests-1728%20passing-brightgreen.svg)](https://github.com/czlonkowski/n8n-mcp/actions)

Binary file not shown.

View File

@@ -7,6 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [2.12.1] - 2025-09-22
### Fixed
- **Error Output Validation**: Enhanced workflow validator to detect incorrect error output configurations
- Detects when multiple nodes are incorrectly placed in the same output array (main[0])
- Validates that error handlers are properly connected to main[1] (error output) instead of main[0]
- Cross-validates onError property ('continueErrorOutput') matches actual connection structure
- Provides clear, actionable error messages with JSON examples showing correct configuration
- Uses heuristic detection for error handler nodes (names containing "error", "fail", "catch", etc.)
- Added comprehensive test coverage with 16+ test cases
### Improved
- **Validation Messages**: Error messages now include detailed JSON examples showing both incorrect and correct configurations
- **Pattern Detection**: Fixed `checkWorkflowPatterns` to check main[1] for error outputs instead of non-existent outputs.error
- **Test Coverage**: Added new test file `workflow-validator-error-outputs.test.ts` with extensive error output validation scenarios
## [2.12.0] - 2025-09-19
### Added

View File

@@ -1,6 +1,6 @@
{
"name": "n8n-mcp",
"version": "2.12.0",
"version": "2.12.1",
"description": "Integration between n8n workflow automation and Model Context Protocol (MCP)",
"main": "dist/index.js",
"bin": {

View File

@@ -0,0 +1,274 @@
#!/usr/bin/env npx tsx
/**
* Test script for error output validation improvements
* Tests both incorrect and correct error output configurations
*/
import { WorkflowValidator } from '../dist/services/workflow-validator.js';
import { NodeRepository } from '../dist/database/node-repository.js';
import { EnhancedConfigValidator } from '../dist/services/enhanced-config-validator.js';
import { DatabaseAdapter } from '../dist/database/database-adapter.js';
import { Logger } from '../dist/utils/logger.js';
import path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const logger = new Logger({ prefix: '[TestErrorValidation]' });
async function runTests() {
// Initialize database
const dbPath = path.join(__dirname, '..', 'data', 'n8n-nodes.db');
const adapter = new DatabaseAdapter();
adapter.initialize({
type: 'better-sqlite3',
filename: dbPath
});
const db = adapter.getDatabase();
const nodeRepository = new NodeRepository(db);
const validator = new WorkflowValidator(nodeRepository, EnhancedConfigValidator);
console.log('\n🧪 Testing Error Output Validation Improvements\n');
console.log('=' .repeat(60));
// Test 1: Incorrect configuration - multiple nodes in same array
console.log('\n📝 Test 1: INCORRECT - Multiple nodes in main[0]');
console.log('-'.repeat(40));
const incorrectWorkflow = {
nodes: [
{
id: '132ef0dc-87af-41de-a95d-cabe3a0a5342',
name: 'Validate Input',
type: 'n8n-nodes-base.set',
typeVersion: 3.4,
position: [-400, 64] as [number, number],
parameters: {}
},
{
id: '5dedf217-63f9-409f-b34e-7780b22e199a',
name: 'Filter URLs',
type: 'n8n-nodes-base.filter',
typeVersion: 2.2,
position: [-176, 64] as [number, number],
parameters: {}
},
{
id: '9d5407cc-ca5a-4966-b4b7-0e5dfbf54ad3',
name: 'Error Response1',
type: 'n8n-nodes-base.respondToWebhook',
typeVersion: 1.5,
position: [-160, 240] as [number, number],
parameters: {}
}
],
connections: {
'Validate Input': {
main: [
[
{ node: 'Filter URLs', type: 'main', index: 0 },
{ node: 'Error Response1', type: 'main', index: 0 } // WRONG!
]
]
}
}
};
const result1 = await validator.validateWorkflow(incorrectWorkflow);
if (result1.errors.length > 0) {
console.log('❌ ERROR DETECTED (as expected):');
const errorMessage = result1.errors.find(e =>
e.message.includes('Incorrect error output configuration')
);
if (errorMessage) {
console.log('\n' + errorMessage.message);
}
} else {
console.log('✅ No errors found (but should have detected the issue!)');
}
// Test 2: Correct configuration - separate arrays
console.log('\n📝 Test 2: CORRECT - Separate main[0] and main[1]');
console.log('-'.repeat(40));
const correctWorkflow = {
nodes: [
{
id: '132ef0dc-87af-41de-a95d-cabe3a0a5342',
name: 'Validate Input',
type: 'n8n-nodes-base.set',
typeVersion: 3.4,
position: [-400, 64] as [number, number],
parameters: {},
onError: 'continueErrorOutput' as const
},
{
id: '5dedf217-63f9-409f-b34e-7780b22e199a',
name: 'Filter URLs',
type: 'n8n-nodes-base.filter',
typeVersion: 2.2,
position: [-176, 64] as [number, number],
parameters: {}
},
{
id: '9d5407cc-ca5a-4966-b4b7-0e5dfbf54ad3',
name: 'Error Response1',
type: 'n8n-nodes-base.respondToWebhook',
typeVersion: 1.5,
position: [-160, 240] as [number, number],
parameters: {}
}
],
connections: {
'Validate Input': {
main: [
[
{ node: 'Filter URLs', type: 'main', index: 0 }
],
[
{ node: 'Error Response1', type: 'main', index: 0 } // CORRECT!
]
]
}
}
};
const result2 = await validator.validateWorkflow(correctWorkflow);
const hasIncorrectError = result2.errors.some(e =>
e.message.includes('Incorrect error output configuration')
);
if (!hasIncorrectError) {
console.log('✅ No error output configuration issues (correct!)');
} else {
console.log('❌ Unexpected error found');
}
// Test 3: onError without error connections
console.log('\n📝 Test 3: onError without error connections');
console.log('-'.repeat(40));
const mismatchWorkflow = {
nodes: [
{
id: '1',
name: 'HTTP Request',
type: 'n8n-nodes-base.httpRequest',
typeVersion: 4,
position: [100, 100] as [number, number],
parameters: {},
onError: 'continueErrorOutput' as const
},
{
id: '2',
name: 'Process Data',
type: 'n8n-nodes-base.set',
typeVersion: 2,
position: [300, 100] as [number, number],
parameters: {}
}
],
connections: {
'HTTP Request': {
main: [
[
{ node: 'Process Data', type: 'main', index: 0 }
]
// No main[1] for error output
]
}
}
};
const result3 = await validator.validateWorkflow(mismatchWorkflow);
const mismatchError = result3.errors.find(e =>
e.message.includes("has onError: 'continueErrorOutput' but no error output connections")
);
if (mismatchError) {
console.log('❌ ERROR DETECTED (as expected):');
console.log(`Node: ${mismatchError.nodeName}`);
console.log(`Message: ${mismatchError.message}`);
} else {
console.log('✅ No mismatch detected (but should have!)');
}
// Test 4: Error connections without onError
console.log('\n📝 Test 4: Error connections without onError property');
console.log('-'.repeat(40));
const missingOnErrorWorkflow = {
nodes: [
{
id: '1',
name: 'HTTP Request',
type: 'n8n-nodes-base.httpRequest',
typeVersion: 4,
position: [100, 100] as [number, number],
parameters: {}
// Missing onError property
},
{
id: '2',
name: 'Process Data',
type: 'n8n-nodes-base.set',
position: [300, 100] as [number, number],
parameters: {}
},
{
id: '3',
name: 'Error Handler',
type: 'n8n-nodes-base.set',
position: [300, 300] as [number, number],
parameters: {}
}
],
connections: {
'HTTP Request': {
main: [
[
{ node: 'Process Data', type: 'main', index: 0 }
],
[
{ node: 'Error Handler', type: 'main', index: 0 }
]
]
}
}
};
const result4 = await validator.validateWorkflow(missingOnErrorWorkflow);
const missingOnErrorWarning = result4.warnings.find(w =>
w.message.includes('error output connections in main[1] but missing onError')
);
if (missingOnErrorWarning) {
console.log('⚠️ WARNING DETECTED (as expected):');
console.log(`Node: ${missingOnErrorWarning.nodeName}`);
console.log(`Message: ${missingOnErrorWarning.message}`);
} else {
console.log('✅ No warning (but should have warned!)');
}
console.log('\n' + '='.repeat(60));
console.log('\n📊 Summary:');
console.log('- Error output validation is working correctly');
console.log('- Detects incorrect configurations (multiple nodes in main[0])');
console.log('- Validates onError property matches connections');
console.log('- Provides clear error messages with fix examples');
// Close database
adapter.close();
}
runTests().catch(error => {
console.error('Test failed:', error);
process.exit(1);
});

View File

@@ -0,0 +1,158 @@
#!/usr/bin/env node
/**
* Test script for error output validation improvements
*/
const { WorkflowValidator } = require('../dist/services/workflow-validator.js');
const { NodeRepository } = require('../dist/database/node-repository.js');
const { EnhancedConfigValidator } = require('../dist/services/enhanced-config-validator.js');
const Database = require('better-sqlite3');
const path = require('path');
async function runTests() {
// Initialize database
const dbPath = path.join(__dirname, '..', 'data', 'nodes.db');
const db = new Database(dbPath, { readonly: true });
const nodeRepository = new NodeRepository(db);
const validator = new WorkflowValidator(nodeRepository, EnhancedConfigValidator);
console.log('\n🧪 Testing Error Output Validation Improvements\n');
console.log('=' .repeat(60));
// Test 1: Incorrect configuration - multiple nodes in same array
console.log('\n📝 Test 1: INCORRECT - Multiple nodes in main[0]');
console.log('-'.repeat(40));
const incorrectWorkflow = {
nodes: [
{
id: '132ef0dc-87af-41de-a95d-cabe3a0a5342',
name: 'Validate Input',
type: 'n8n-nodes-base.set',
typeVersion: 3.4,
position: [-400, 64],
parameters: {}
},
{
id: '5dedf217-63f9-409f-b34e-7780b22e199a',
name: 'Filter URLs',
type: 'n8n-nodes-base.filter',
typeVersion: 2.2,
position: [-176, 64],
parameters: {}
},
{
id: '9d5407cc-ca5a-4966-b4b7-0e5dfbf54ad3',
name: 'Error Response1',
type: 'n8n-nodes-base.respondToWebhook',
typeVersion: 1.5,
position: [-160, 240],
parameters: {}
}
],
connections: {
'Validate Input': {
main: [
[
{ node: 'Filter URLs', type: 'main', index: 0 },
{ node: 'Error Response1', type: 'main', index: 0 } // WRONG!
]
]
}
}
};
const result1 = await validator.validateWorkflow(incorrectWorkflow);
if (result1.errors.length > 0) {
console.log('❌ ERROR DETECTED (as expected):');
const errorMessage = result1.errors.find(e =>
e.message.includes('Incorrect error output configuration')
);
if (errorMessage) {
console.log('\nError Summary:');
console.log(`Node: ${errorMessage.nodeName || 'Validate Input'}`);
console.log('\nFull Error Message:');
console.log(errorMessage.message);
} else {
console.log('Other errors found:', result1.errors.map(e => e.message));
}
} else {
console.log('⚠️ No errors found - validation may not be working correctly');
}
// Test 2: Correct configuration - separate arrays
console.log('\n📝 Test 2: CORRECT - Separate main[0] and main[1]');
console.log('-'.repeat(40));
const correctWorkflow = {
nodes: [
{
id: '132ef0dc-87af-41de-a95d-cabe3a0a5342',
name: 'Validate Input',
type: 'n8n-nodes-base.set',
typeVersion: 3.4,
position: [-400, 64],
parameters: {},
onError: 'continueErrorOutput'
},
{
id: '5dedf217-63f9-409f-b34e-7780b22e199a',
name: 'Filter URLs',
type: 'n8n-nodes-base.filter',
typeVersion: 2.2,
position: [-176, 64],
parameters: {}
},
{
id: '9d5407cc-ca5a-4966-b4b7-0e5dfbf54ad3',
name: 'Error Response1',
type: 'n8n-nodes-base.respondToWebhook',
typeVersion: 1.5,
position: [-160, 240],
parameters: {}
}
],
connections: {
'Validate Input': {
main: [
[
{ node: 'Filter URLs', type: 'main', index: 0 }
],
[
{ node: 'Error Response1', type: 'main', index: 0 } // CORRECT!
]
]
}
}
};
const result2 = await validator.validateWorkflow(correctWorkflow);
const hasIncorrectError = result2.errors.some(e =>
e.message.includes('Incorrect error output configuration')
);
if (!hasIncorrectError) {
console.log('✅ No error output configuration issues (correct!)');
} else {
console.log('❌ Unexpected error found');
}
console.log('\n' + '='.repeat(60));
console.log('\n✨ Error output validation is working correctly!');
console.log('The validator now properly detects:');
console.log(' 1. Multiple nodes incorrectly placed in main[0]');
console.log(' 2. Provides clear JSON examples for fixing issues');
console.log(' 3. Validates onError property matches connections');
// Close database
db.close();
}
runTests().catch(error => {
console.error('Test failed:', error);
process.exit(1);
});

View File

@@ -7,7 +7,6 @@ import { NodeRepository } from '../database/node-repository';
import { EnhancedConfigValidator } from './enhanced-config-validator';
import { ExpressionValidator } from './expression-validator';
import { Logger } from '../utils/logger';
const logger = new Logger({ prefix: '[WorkflowValidator]' });
interface WorkflowNode {
@@ -653,6 +652,11 @@ export class WorkflowValidator {
): void {
// Get source node for special validation
const sourceNode = nodeMap.get(sourceName);
// Special validation for main outputs with error handling
if (outputType === 'main' && sourceNode) {
this.validateErrorOutputConfiguration(sourceName, sourceNode, outputs, nodeMap, result);
}
outputs.forEach((outputConnections, outputIndex) => {
if (!outputConnections) return;
@@ -726,6 +730,90 @@ export class WorkflowValidator {
});
}
/**
* Validate error output configuration
*/
private validateErrorOutputConfiguration(
sourceName: string,
sourceNode: WorkflowNode,
outputs: Array<Array<{ node: string; type: string; index: number }>>,
nodeMap: Map<string, WorkflowNode>,
result: WorkflowValidationResult
): void {
// Check if node has onError: 'continueErrorOutput'
const hasErrorOutputSetting = sourceNode.onError === 'continueErrorOutput';
const hasErrorConnections = outputs.length > 1 && outputs[1] && outputs[1].length > 0;
// Validate mismatch between onError setting and connections
if (hasErrorOutputSetting && !hasErrorConnections) {
result.errors.push({
type: 'error',
nodeId: sourceNode.id,
nodeName: sourceNode.name,
message: `Node has onError: 'continueErrorOutput' but no error output connections in main[1]. Add error handler connections to main[1] or change onError to 'continueRegularOutput' or 'stopWorkflow'.`
});
}
if (!hasErrorOutputSetting && hasErrorConnections) {
result.warnings.push({
type: 'warning',
nodeId: sourceNode.id,
nodeName: sourceNode.name,
message: `Node has error output connections in main[1] but missing onError: 'continueErrorOutput'. Add this property to properly handle errors.`
});
}
// Check for common mistake: multiple nodes in main[0] when error handling is intended
if (outputs.length >= 1 && outputs[0] && outputs[0].length > 1) {
// Check if any of the nodes in main[0] look like error handlers
const potentialErrorHandlers = outputs[0].filter(conn => {
const targetNode = nodeMap.get(conn.node);
if (!targetNode) return false;
const nodeName = targetNode.name.toLowerCase();
const nodeType = targetNode.type.toLowerCase();
// Common patterns for error handler nodes
return nodeName.includes('error') ||
nodeName.includes('fail') ||
nodeName.includes('catch') ||
nodeName.includes('exception') ||
nodeType.includes('respondtowebhook') ||
nodeType.includes('emailsend');
});
if (potentialErrorHandlers.length > 0) {
const errorHandlerNames = potentialErrorHandlers.map(conn => `"${conn.node}"`).join(', ');
result.errors.push({
type: 'error',
nodeId: sourceNode.id,
nodeName: sourceNode.name,
message: `Incorrect error output configuration. Nodes ${errorHandlerNames} appear to be error handlers but are in main[0] (success output) along with other nodes.\n\n` +
`INCORRECT (current):\n` +
`"${sourceName}": {\n` +
` "main": [\n` +
` [ // main[0] has multiple nodes mixed together\n` +
outputs[0].map(conn => ` {"node": "${conn.node}", "type": "${conn.type}", "index": ${conn.index}}`).join(',\n') + '\n' +
` ]\n` +
` ]\n` +
`}\n\n` +
`CORRECT (should be):\n` +
`"${sourceName}": {\n` +
` "main": [\n` +
` [ // main[0] = success output\n` +
outputs[0].filter(conn => !potentialErrorHandlers.includes(conn)).map(conn => ` {"node": "${conn.node}", "type": "${conn.type}", "index": ${conn.index}}`).join(',\n') + '\n' +
` ],\n` +
` [ // main[1] = error output\n` +
potentialErrorHandlers.map(conn => ` {"node": "${conn.node}", "type": "${conn.type}", "index": ${conn.index}}`).join(',\n') + '\n' +
` ]\n` +
` ]\n` +
`}\n\n` +
`Also add: "onError": "continueErrorOutput" to the "${sourceName}" node.`
});
}
}
}
/**
* Validate AI tool connections
*/
@@ -957,9 +1045,9 @@ export class WorkflowValidator {
result: WorkflowValidationResult,
profile: string = 'runtime'
): void {
// Check for error handling
// Check for error handling (n8n uses main[1] for error outputs, not outputs.error)
const hasErrorHandling = Object.values(workflow.connections).some(
outputs => outputs.error && outputs.error.length > 0
outputs => outputs.main && outputs.main.length > 1 && outputs.main[1] && outputs.main[1].length > 0
);
// Only suggest error handling in stricter profiles

View File

@@ -0,0 +1,535 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js';
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { TestableN8NMCPServer } from './test-helpers';
describe('MCP Workflow Error Output Validation Integration', () => {
let mcpServer: TestableN8NMCPServer;
let client: Client;
beforeEach(async () => {
mcpServer = new TestableN8NMCPServer();
await mcpServer.initialize();
const [serverTransport, clientTransport] = InMemoryTransport.createLinkedPair();
await mcpServer.connectToTransport(serverTransport);
client = new Client({
name: 'test-client',
version: '1.0.0'
}, {
capabilities: {}
});
await client.connect(clientTransport);
});
afterEach(async () => {
await client.close();
await mcpServer.close();
});
describe('validate_workflow tool - Error Output Configuration', () => {
it('should detect incorrect error output configuration via MCP', async () => {
const workflow = {
nodes: [
{
id: '1',
name: 'Validate Input',
type: 'n8n-nodes-base.set',
typeVersion: 3.4,
position: [-400, 64],
parameters: {}
},
{
id: '2',
name: 'Filter URLs',
type: 'n8n-nodes-base.filter',
typeVersion: 2.2,
position: [-176, 64],
parameters: {}
},
{
id: '3',
name: 'Error Response1',
type: 'n8n-nodes-base.respondToWebhook',
typeVersion: 1.5,
position: [-160, 240],
parameters: {}
}
],
connections: {
'Validate Input': {
main: [
[
{ node: 'Filter URLs', type: 'main', index: 0 },
{ node: 'Error Response1', type: 'main', index: 0 } // WRONG! Both in main[0]
]
]
}
}
};
const response = await client.callTool({
name: 'validate_workflow',
arguments: { workflow }
});
expect((response as any).content).toHaveLength(1);
expect((response as any).content[0].type).toBe('text');
const result = JSON.parse(((response as any).content[0]).text);
expect(result.valid).toBe(false);
expect(Array.isArray(result.errors)).toBe(true);
// Check for the specific error message about incorrect configuration
const hasIncorrectConfigError = result.errors.some((e: any) =>
e.message.includes('Incorrect error output configuration') &&
e.message.includes('Error Response1') &&
e.message.includes('appear to be error handlers but are in main[0]')
);
expect(hasIncorrectConfigError).toBe(true);
// Verify the error message includes the JSON examples
const errorMsg = result.errors.find((e: any) =>
e.message.includes('Incorrect error output configuration')
);
expect(errorMsg?.message).toContain('INCORRECT (current)');
expect(errorMsg?.message).toContain('CORRECT (should be)');
expect(errorMsg?.message).toContain('main[1] = error output');
});
it('should validate correct error output configuration via MCP', async () => {
const workflow = {
nodes: [
{
id: '1',
name: 'Validate Input',
type: 'n8n-nodes-base.set',
typeVersion: 3.4,
position: [-400, 64],
parameters: {},
onError: 'continueErrorOutput'
},
{
id: '2',
name: 'Filter URLs',
type: 'n8n-nodes-base.filter',
typeVersion: 2.2,
position: [-176, 64],
parameters: {}
},
{
id: '3',
name: 'Error Response1',
type: 'n8n-nodes-base.respondToWebhook',
typeVersion: 1.5,
position: [-160, 240],
parameters: {}
}
],
connections: {
'Validate Input': {
main: [
[
{ node: 'Filter URLs', type: 'main', index: 0 }
],
[
{ node: 'Error Response1', type: 'main', index: 0 } // Correctly in main[1]
]
]
}
}
};
const response = await client.callTool({
name: 'validate_workflow',
arguments: { workflow }
});
expect((response as any).content).toHaveLength(1);
expect((response as any).content[0].type).toBe('text');
const result = JSON.parse(((response as any).content[0]).text);
// Should not have the specific error about incorrect configuration
const hasIncorrectConfigError = result.errors?.some((e: any) =>
e.message.includes('Incorrect error output configuration')
) ?? false;
expect(hasIncorrectConfigError).toBe(false);
});
it('should detect onError and connection mismatches via MCP', async () => {
// Test case 1: onError set but no error connections
const workflow1 = {
nodes: [
{
id: '1',
name: 'HTTP Request',
type: 'n8n-nodes-base.httpRequest',
typeVersion: 4,
position: [100, 100],
parameters: {},
onError: 'continueErrorOutput'
},
{
id: '2',
name: 'Process Data',
type: 'n8n-nodes-base.set',
position: [300, 100],
parameters: {}
}
],
connections: {
'HTTP Request': {
main: [
[
{ node: 'Process Data', type: 'main', index: 0 }
]
]
}
}
};
// Test case 2: error connections but no onError
const workflow2 = {
nodes: [
{
id: '1',
name: 'HTTP Request',
type: 'n8n-nodes-base.httpRequest',
typeVersion: 4,
position: [100, 100],
parameters: {}
// No onError property
},
{
id: '2',
name: 'Process Data',
type: 'n8n-nodes-base.set',
position: [300, 100],
parameters: {}
},
{
id: '3',
name: 'Error Handler',
type: 'n8n-nodes-base.set',
position: [300, 200],
parameters: {}
}
],
connections: {
'HTTP Request': {
main: [
[
{ node: 'Process Data', type: 'main', index: 0 }
],
[
{ node: 'Error Handler', type: 'main', index: 0 }
]
]
}
}
};
// Test both scenarios
const workflows = [workflow1, workflow2];
for (const workflow of workflows) {
const response = await client.callTool({
name: 'validate_workflow',
arguments: { workflow }
});
const result = JSON.parse(((response as any).content[0]).text);
// Should detect some kind of validation issue
expect(result).toHaveProperty('valid');
expect(Array.isArray(result.errors || [])).toBe(true);
expect(Array.isArray(result.warnings || [])).toBe(true);
}
});
it('should handle large workflows with complex error patterns via MCP', async () => {
// Create a large workflow with multiple error handling scenarios
const nodes = [];
const connections: any = {};
// Create 50 nodes with various error handling patterns
for (let i = 1; i <= 50; i++) {
nodes.push({
id: i.toString(),
name: `Node${i}`,
type: i % 5 === 0 ? 'n8n-nodes-base.httpRequest' : 'n8n-nodes-base.set',
typeVersion: 1,
position: [i * 100, 100],
parameters: {},
...(i % 3 === 0 ? { onError: 'continueErrorOutput' } : {})
});
}
// Create connections with mixed correct and incorrect error handling
for (let i = 1; i < 50; i++) {
const hasErrorHandling = i % 3 === 0;
const nextNode = `Node${i + 1}`;
if (hasErrorHandling && i % 6 === 0) {
// Incorrect: error handler in main[0] with success node
connections[`Node${i}`] = {
main: [
[
{ node: nextNode, type: 'main', index: 0 },
{ node: 'Error Handler', type: 'main', index: 0 } // Wrong placement
]
]
};
} else if (hasErrorHandling) {
// Correct: separate success and error outputs
connections[`Node${i}`] = {
main: [
[
{ node: nextNode, type: 'main', index: 0 }
],
[
{ node: 'Error Handler', type: 'main', index: 0 }
]
]
};
} else {
// Normal connection
connections[`Node${i}`] = {
main: [
[
{ node: nextNode, type: 'main', index: 0 }
]
]
};
}
}
// Add error handler node
nodes.push({
id: '51',
name: 'Error Handler',
type: 'n8n-nodes-base.set',
typeVersion: 1,
position: [2600, 200],
parameters: {}
});
const workflow = { nodes, connections };
const startTime = Date.now();
const response = await client.callTool({
name: 'validate_workflow',
arguments: { workflow }
});
const endTime = Date.now();
// Validation should complete quickly even for large workflows
expect(endTime - startTime).toBeLessThan(5000); // Less than 5 seconds
const result = JSON.parse(((response as any).content[0]).text);
// Should detect the incorrect error configurations
const hasErrors = result.errors && result.errors.length > 0;
expect(hasErrors).toBe(true);
// Specifically check for incorrect error output configuration errors
const incorrectConfigErrors = result.errors.filter((e: any) =>
e.message.includes('Incorrect error output configuration')
);
expect(incorrectConfigErrors.length).toBeGreaterThan(0);
});
it('should handle edge cases gracefully via MCP', async () => {
const edgeCaseWorkflows = [
// Empty workflow
{ nodes: [], connections: {} },
// Single isolated node
{
nodes: [{
id: '1',
name: 'Isolated',
type: 'n8n-nodes-base.set',
position: [100, 100],
parameters: {}
}],
connections: {}
},
// Node with null/undefined connections
{
nodes: [{
id: '1',
name: 'Source',
type: 'n8n-nodes-base.httpRequest',
position: [100, 100],
parameters: {}
}],
connections: {
'Source': {
main: [null, undefined]
}
}
}
];
for (const workflow of edgeCaseWorkflows) {
const response = await client.callTool({
name: 'validate_workflow',
arguments: { workflow }
});
expect((response as any).content).toHaveLength(1);
const result = JSON.parse(((response as any).content[0]).text);
// Should not crash and should return a valid validation result
expect(result).toHaveProperty('valid');
expect(typeof result.valid).toBe('boolean');
expect(Array.isArray(result.errors || [])).toBe(true);
expect(Array.isArray(result.warnings || [])).toBe(true);
}
});
it('should validate with different validation profiles via MCP', async () => {
const workflow = {
nodes: [
{
id: '1',
name: 'API Call',
type: 'n8n-nodes-base.httpRequest',
position: [100, 100],
parameters: {}
},
{
id: '2',
name: 'Success Handler',
type: 'n8n-nodes-base.set',
position: [300, 100],
parameters: {}
},
{
id: '3',
name: 'Error Response',
type: 'n8n-nodes-base.respondToWebhook',
position: [300, 200],
parameters: {}
}
],
connections: {
'API Call': {
main: [
[
{ node: 'Success Handler', type: 'main', index: 0 },
{ node: 'Error Response', type: 'main', index: 0 } // Incorrect placement
]
]
}
}
};
const profiles = ['minimal', 'runtime', 'ai-friendly', 'strict'];
for (const profile of profiles) {
const response = await client.callTool({
name: 'validate_workflow',
arguments: {
workflow,
options: { profile }
}
});
const result = JSON.parse(((response as any).content[0]).text);
// All profiles should detect this error output configuration issue
const hasIncorrectConfigError = result.errors?.some((e: any) =>
e.message.includes('Incorrect error output configuration')
);
expect(hasIncorrectConfigError).toBe(true);
}
});
});
describe('Error Message Format Consistency', () => {
it('should format error messages consistently across different scenarios', async () => {
const scenarios = [
{
name: 'Single error handler in wrong place',
workflow: {
nodes: [
{ id: '1', name: 'Source', type: 'n8n-nodes-base.httpRequest', position: [0, 0], parameters: {} },
{ id: '2', name: 'Success', type: 'n8n-nodes-base.set', position: [200, 0], parameters: {} },
{ id: '3', name: 'Error Handler', type: 'n8n-nodes-base.set', position: [200, 100], parameters: {} }
],
connections: {
'Source': {
main: [[
{ node: 'Success', type: 'main', index: 0 },
{ node: 'Error Handler', type: 'main', index: 0 }
]]
}
}
}
},
{
name: 'Multiple error handlers in wrong place',
workflow: {
nodes: [
{ id: '1', name: 'Source', type: 'n8n-nodes-base.httpRequest', position: [0, 0], parameters: {} },
{ id: '2', name: 'Success', type: 'n8n-nodes-base.set', position: [200, 0], parameters: {} },
{ id: '3', name: 'Error Handler 1', type: 'n8n-nodes-base.set', position: [200, 100], parameters: {} },
{ id: '4', name: 'Error Handler 2', type: 'n8n-nodes-base.emailSend', position: [200, 200], parameters: {} }
],
connections: {
'Source': {
main: [[
{ node: 'Success', type: 'main', index: 0 },
{ node: 'Error Handler 1', type: 'main', index: 0 },
{ node: 'Error Handler 2', type: 'main', index: 0 }
]]
}
}
}
}
];
for (const scenario of scenarios) {
const response = await client.callTool({
name: 'validate_workflow',
arguments: { workflow: scenario.workflow }
});
const result = JSON.parse(((response as any).content[0]).text);
const errorConfigError = result.errors.find((e: any) =>
e.message.includes('Incorrect error output configuration')
);
expect(errorConfigError).toBeDefined();
// Check that error message follows consistent format
expect(errorConfigError.message).toContain('INCORRECT (current):');
expect(errorConfigError.message).toContain('CORRECT (should be):');
expect(errorConfigError.message).toContain('main[0] = success output');
expect(errorConfigError.message).toContain('main[1] = error output');
expect(errorConfigError.message).toContain('Also add: "onError": "continueErrorOutput"');
// Check JSON format is valid
const incorrectSection = errorConfigError.message.match(/INCORRECT \(current\):\n([\s\S]*?)\n\nCORRECT/);
const correctSection = errorConfigError.message.match(/CORRECT \(should be\):\n([\s\S]*?)\n\nAlso add/);
expect(incorrectSection).toBeDefined();
expect(correctSection).toBeDefined();
// Verify JSON structure is present (but don't parse due to comments)
expect(incorrectSection).toBeDefined();
expect(correctSection).toBeDefined();
expect(incorrectSection![1]).toContain('main');
expect(correctSection![1]).toContain('main');
}
});
});
});

View File

@@ -0,0 +1,793 @@
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';
vi.mock('@/utils/logger');
describe('WorkflowValidator - Error Output Validation', () => {
let validator: WorkflowValidator;
let mockNodeRepository: any;
beforeEach(() => {
vi.clearAllMocks();
// Create mock repository
mockNodeRepository = {
getNode: vi.fn((type: string) => {
// Return mock node info for common node types
if (type.includes('httpRequest') || type.includes('webhook') || type.includes('set')) {
return {
node_type: type,
display_name: 'Mock Node',
isVersioned: true,
version: 1
};
}
return null;
})
};
validator = new WorkflowValidator(mockNodeRepository, EnhancedConfigValidator);
});
describe('Error Output Configuration', () => {
it('should detect incorrect configuration - multiple nodes in same array', async () => {
const workflow = {
nodes: [
{
id: '1',
name: 'Validate Input',
type: 'n8n-nodes-base.set',
typeVersion: 3.4,
position: [-400, 64],
parameters: {}
},
{
id: '2',
name: 'Filter URLs',
type: 'n8n-nodes-base.filter',
typeVersion: 2.2,
position: [-176, 64],
parameters: {}
},
{
id: '3',
name: 'Error Response1',
type: 'n8n-nodes-base.respondToWebhook',
typeVersion: 1.5,
position: [-160, 240],
parameters: {}
}
],
connections: {
'Validate Input': {
main: [
[
{ node: 'Filter URLs', type: 'main', index: 0 },
{ node: 'Error Response1', type: 'main', index: 0 } // WRONG! Both in main[0]
]
]
}
}
};
const result = await validator.validateWorkflow(workflow as any);
expect(result.valid).toBe(false);
expect(result.errors.some(e =>
e.message.includes('Incorrect error output configuration') &&
e.message.includes('Error Response1') &&
e.message.includes('appear to be error handlers but are in main[0]')
)).toBe(true);
// Check that the error message includes the fix
const errorMsg = result.errors.find(e => e.message.includes('Incorrect error output configuration'));
expect(errorMsg?.message).toContain('INCORRECT (current)');
expect(errorMsg?.message).toContain('CORRECT (should be)');
expect(errorMsg?.message).toContain('main[1] = error output');
});
it('should validate correct configuration - separate arrays', async () => {
const workflow = {
nodes: [
{
id: '1',
name: 'Validate Input',
type: 'n8n-nodes-base.set',
typeVersion: 3.4,
position: [-400, 64],
parameters: {},
onError: 'continueErrorOutput'
},
{
id: '2',
name: 'Filter URLs',
type: 'n8n-nodes-base.filter',
typeVersion: 2.2,
position: [-176, 64],
parameters: {}
},
{
id: '3',
name: 'Error Response1',
type: 'n8n-nodes-base.respondToWebhook',
typeVersion: 1.5,
position: [-160, 240],
parameters: {}
}
],
connections: {
'Validate Input': {
main: [
[
{ node: 'Filter URLs', type: 'main', index: 0 }
],
[
{ node: 'Error Response1', type: 'main', index: 0 } // Correctly in main[1]
]
]
}
}
};
const result = await validator.validateWorkflow(workflow as any);
// Should not have the specific error about incorrect configuration
expect(result.errors.some(e =>
e.message.includes('Incorrect error output configuration')
)).toBe(false);
});
it('should detect onError without error connections', async () => {
const workflow = {
nodes: [
{
id: '1',
name: 'HTTP Request',
type: 'n8n-nodes-base.httpRequest',
typeVersion: 4,
position: [100, 100],
parameters: {},
onError: 'continueErrorOutput' // Has onError
},
{
id: '2',
name: 'Process Data',
type: 'n8n-nodes-base.set',
position: [300, 100],
parameters: {}
}
],
connections: {
'HTTP Request': {
main: [
[
{ node: 'Process Data', type: 'main', index: 0 }
]
// No main[1] for error output
]
}
}
};
const result = await validator.validateWorkflow(workflow as any);
expect(result.errors.some(e =>
e.nodeName === 'HTTP Request' &&
e.message.includes("has onError: 'continueErrorOutput' but no error output connections")
)).toBe(true);
});
it('should warn about error connections without onError', async () => {
const workflow = {
nodes: [
{
id: '1',
name: 'HTTP Request',
type: 'n8n-nodes-base.httpRequest',
typeVersion: 4,
position: [100, 100],
parameters: {}
// Missing onError property
},
{
id: '2',
name: 'Process Data',
type: 'n8n-nodes-base.set',
position: [300, 100],
parameters: {}
},
{
id: '3',
name: 'Error Handler',
type: 'n8n-nodes-base.set',
position: [300, 300],
parameters: {}
}
],
connections: {
'HTTP Request': {
main: [
[
{ node: 'Process Data', type: 'main', index: 0 }
],
[
{ node: 'Error Handler', type: 'main', index: 0 } // Has error connection
]
]
}
}
};
const result = await validator.validateWorkflow(workflow as any);
expect(result.warnings.some(w =>
w.nodeName === 'HTTP Request' &&
w.message.includes('error output connections in main[1] but missing onError')
)).toBe(true);
});
});
describe('Error Handler Detection', () => {
it('should detect error handler nodes by name', async () => {
const workflow = {
nodes: [
{
id: '1',
name: 'API Call',
type: 'n8n-nodes-base.httpRequest',
position: [100, 100],
parameters: {}
},
{
id: '2',
name: 'Process Success',
type: 'n8n-nodes-base.set',
position: [300, 100],
parameters: {}
},
{
id: '3',
name: 'Handle Error', // Contains 'error'
type: 'n8n-nodes-base.set',
position: [300, 300],
parameters: {}
}
],
connections: {
'API Call': {
main: [
[
{ node: 'Process Success', type: 'main', index: 0 },
{ node: 'Handle Error', type: 'main', index: 0 } // Wrong placement
]
]
}
}
};
const result = await validator.validateWorkflow(workflow as any);
expect(result.errors.some(e =>
e.message.includes('Handle Error') &&
e.message.includes('appear to be error handlers')
)).toBe(true);
});
it('should detect error handler nodes by type', async () => {
const workflow = {
nodes: [
{
id: '1',
name: 'Webhook',
type: 'n8n-nodes-base.webhook',
position: [100, 100],
parameters: {}
},
{
id: '2',
name: 'Process',
type: 'n8n-nodes-base.set',
position: [300, 100],
parameters: {}
},
{
id: '3',
name: 'Respond',
type: 'n8n-nodes-base.respondToWebhook', // Common error handler type
position: [300, 300],
parameters: {}
}
],
connections: {
'Webhook': {
main: [
[
{ node: 'Process', type: 'main', index: 0 },
{ node: 'Respond', type: 'main', index: 0 } // Wrong placement
]
]
}
}
};
const result = await validator.validateWorkflow(workflow as any);
expect(result.errors.some(e =>
e.message.includes('Respond') &&
e.message.includes('appear to be error handlers')
)).toBe(true);
});
it('should not flag non-error nodes in main[0]', async () => {
const workflow = {
nodes: [
{
id: '1',
name: 'Start',
type: 'n8n-nodes-base.manualTrigger',
position: [100, 100],
parameters: {}
},
{
id: '2',
name: 'First Process',
type: 'n8n-nodes-base.set',
position: [300, 100],
parameters: {}
},
{
id: '3',
name: 'Second Process',
type: 'n8n-nodes-base.set',
position: [300, 200],
parameters: {}
}
],
connections: {
'Start': {
main: [
[
{ node: 'First Process', type: 'main', index: 0 },
{ node: 'Second Process', type: 'main', index: 0 } // Both are valid success paths
]
]
}
}
};
const result = await validator.validateWorkflow(workflow as any);
// Should not have error about incorrect error configuration
expect(result.errors.some(e =>
e.message.includes('Incorrect error output configuration')
)).toBe(false);
});
});
describe('Complex Error Patterns', () => {
it('should handle multiple error handlers correctly', async () => {
const workflow = {
nodes: [
{
id: '1',
name: 'HTTP Request',
type: 'n8n-nodes-base.httpRequest',
position: [100, 100],
parameters: {},
onError: 'continueErrorOutput'
},
{
id: '2',
name: 'Process',
type: 'n8n-nodes-base.set',
position: [300, 100],
parameters: {}
},
{
id: '3',
name: 'Log Error',
type: 'n8n-nodes-base.set',
position: [300, 200],
parameters: {}
},
{
id: '4',
name: 'Send Error Email',
type: 'n8n-nodes-base.emailSend',
position: [300, 300],
parameters: {}
}
],
connections: {
'HTTP Request': {
main: [
[
{ node: 'Process', type: 'main', index: 0 }
],
[
{ node: 'Log Error', type: 'main', index: 0 },
{ node: 'Send Error Email', type: 'main', index: 0 } // Multiple error handlers OK in main[1]
]
]
}
}
};
const result = await validator.validateWorkflow(workflow as any);
// Should not have errors about the configuration
expect(result.errors.some(e =>
e.message.includes('Incorrect error output configuration')
)).toBe(false);
});
it('should detect mixed success and error handlers in main[0]', async () => {
const workflow = {
nodes: [
{
id: '1',
name: 'API Request',
type: 'n8n-nodes-base.httpRequest',
position: [100, 100],
parameters: {}
},
{
id: '2',
name: 'Transform Data',
type: 'n8n-nodes-base.set',
position: [300, 100],
parameters: {}
},
{
id: '3',
name: 'Store Data',
type: 'n8n-nodes-base.set',
position: [500, 100],
parameters: {}
},
{
id: '4',
name: 'Error Notification',
type: 'n8n-nodes-base.emailSend',
position: [300, 300],
parameters: {}
}
],
connections: {
'API Request': {
main: [
[
{ node: 'Transform Data', type: 'main', index: 0 },
{ node: 'Store Data', type: 'main', index: 0 },
{ node: 'Error Notification', type: 'main', index: 0 } // Error handler mixed with success nodes
]
]
}
}
};
const result = await validator.validateWorkflow(workflow as any);
expect(result.errors.some(e =>
e.message.includes('Error Notification') &&
e.message.includes('appear to be error handlers but are in main[0]')
)).toBe(true);
});
it('should handle nested error handling (error handlers with their own errors)', async () => {
const workflow = {
nodes: [
{
id: '1',
name: 'Primary API',
type: 'n8n-nodes-base.httpRequest',
position: [100, 100],
parameters: {},
onError: 'continueErrorOutput'
},
{
id: '2',
name: 'Success Handler',
type: 'n8n-nodes-base.set',
position: [300, 100],
parameters: {}
},
{
id: '3',
name: 'Error Logger',
type: 'n8n-nodes-base.httpRequest',
position: [300, 200],
parameters: {},
onError: 'continueErrorOutput'
},
{
id: '4',
name: 'Fallback Error',
type: 'n8n-nodes-base.set',
position: [500, 250],
parameters: {}
}
],
connections: {
'Primary API': {
main: [
[
{ node: 'Success Handler', type: 'main', index: 0 }
],
[
{ node: 'Error Logger', type: 'main', index: 0 }
]
]
},
'Error Logger': {
main: [
[],
[
{ node: 'Fallback Error', type: 'main', index: 0 }
]
]
}
}
};
const result = await validator.validateWorkflow(workflow as any);
// Should not have errors about incorrect configuration
expect(result.errors.some(e =>
e.message.includes('Incorrect error output configuration')
)).toBe(false);
});
});
describe('Edge Cases', () => {
it('should handle workflows with no connections at all', async () => {
const workflow = {
nodes: [
{
id: '1',
name: 'Isolated Node',
type: 'n8n-nodes-base.set',
position: [100, 100],
parameters: {},
onError: 'continueErrorOutput'
}
],
connections: {}
};
const result = await validator.validateWorkflow(workflow as any);
// Should have warning about orphaned node but not error about connections
expect(result.warnings.some(w =>
w.nodeName === 'Isolated Node' &&
w.message.includes('not connected to any other nodes')
)).toBe(true);
// Should not have error about error output configuration
expect(result.errors.some(e =>
e.message.includes('Incorrect error output configuration')
)).toBe(false);
});
it('should handle nodes with empty main arrays', async () => {
const workflow = {
nodes: [
{
id: '1',
name: 'Source Node',
type: 'n8n-nodes-base.httpRequest',
position: [100, 100],
parameters: {},
onError: 'continueErrorOutput'
},
{
id: '2',
name: 'Target Node',
type: 'n8n-nodes-base.set',
position: [300, 100],
parameters: {}
}
],
connections: {
'Source Node': {
main: [
[], // Empty success array
[] // Empty error array
]
}
}
};
const result = await validator.validateWorkflow(workflow as any);
// Should detect that onError is set but no error connections exist
expect(result.errors.some(e =>
e.nodeName === 'Source Node' &&
e.message.includes("has onError: 'continueErrorOutput' but no error output connections")
)).toBe(true);
});
it('should handle workflows with only error outputs (no success path)', async () => {
const workflow = {
nodes: [
{
id: '1',
name: 'Risky Operation',
type: 'n8n-nodes-base.httpRequest',
position: [100, 100],
parameters: {},
onError: 'continueErrorOutput'
},
{
id: '2',
name: 'Error Handler Only',
type: 'n8n-nodes-base.set',
position: [300, 200],
parameters: {}
}
],
connections: {
'Risky Operation': {
main: [
[], // No success connections
[
{ node: 'Error Handler Only', type: 'main', index: 0 }
]
]
}
}
};
const result = await validator.validateWorkflow(workflow as any);
// Should not have errors about incorrect configuration - this is valid
expect(result.errors.some(e =>
e.message.includes('Incorrect error output configuration')
)).toBe(false);
// Should not have errors about missing error connections
expect(result.errors.some(e =>
e.message.includes("has onError: 'continueErrorOutput' but no error output connections")
)).toBe(false);
});
it('should handle undefined or null connection arrays gracefully', async () => {
const workflow = {
nodes: [
{
id: '1',
name: 'Source Node',
type: 'n8n-nodes-base.httpRequest',
position: [100, 100],
parameters: {}
}
],
connections: {
'Source Node': {
main: [
null, // Null array
undefined // Undefined array
]
}
}
};
const result = await validator.validateWorkflow(workflow as any);
// Should not crash and should not have configuration errors
expect(result.errors.some(e =>
e.message.includes('Incorrect error output configuration')
)).toBe(false);
});
it('should detect all variations of error-related node names', async () => {
const workflow = {
nodes: [
{
id: '1',
name: 'Source',
type: 'n8n-nodes-base.httpRequest',
position: [100, 100],
parameters: {}
},
{
id: '2',
name: 'Handle Failure',
type: 'n8n-nodes-base.set',
position: [300, 100],
parameters: {}
},
{
id: '3',
name: 'Catch Exception',
type: 'n8n-nodes-base.set',
position: [300, 200],
parameters: {}
},
{
id: '4',
name: 'Success Path',
type: 'n8n-nodes-base.set',
position: [500, 100],
parameters: {}
}
],
connections: {
'Source': {
main: [
[
{ node: 'Handle Failure', type: 'main', index: 0 },
{ node: 'Catch Exception', type: 'main', index: 0 },
{ node: 'Success Path', type: 'main', index: 0 }
]
]
}
}
};
const result = await validator.validateWorkflow(workflow as any);
// Should detect both 'Handle Failure' and 'Catch Exception' as error handlers
expect(result.errors.some(e =>
e.message.includes('Handle Failure') &&
e.message.includes('Catch Exception') &&
e.message.includes('appear to be error handlers but are in main[0]')
)).toBe(true);
});
it('should not flag legitimate parallel processing nodes', async () => {
const workflow = {
nodes: [
{
id: '1',
name: 'Data Source',
type: 'n8n-nodes-base.webhook',
position: [100, 100],
parameters: {}
},
{
id: '2',
name: 'Process A',
type: 'n8n-nodes-base.set',
position: [300, 50],
parameters: {}
},
{
id: '3',
name: 'Process B',
type: 'n8n-nodes-base.set',
position: [300, 150],
parameters: {}
},
{
id: '4',
name: 'Transform Data',
type: 'n8n-nodes-base.set',
position: [300, 250],
parameters: {}
}
],
connections: {
'Data Source': {
main: [
[
{ node: 'Process A', type: 'main', index: 0 },
{ node: 'Process B', type: 'main', index: 0 },
{ node: 'Transform Data', type: 'main', index: 0 }
]
]
}
}
};
const result = await validator.validateWorkflow(workflow as any);
// Should not flag these as error configuration issues
expect(result.errors.some(e =>
e.message.includes('Incorrect error output configuration')
)).toBe(false);
});
});
});

View File

@@ -0,0 +1,720 @@
import { describe, it, expect, beforeEach, vi, type Mock } from 'vitest';
import { WorkflowValidator } from '@/services/workflow-validator';
import { NodeRepository } from '@/database/node-repository';
import { EnhancedConfigValidator } from '@/services/enhanced-config-validator';
vi.mock('@/utils/logger');
describe('WorkflowValidator - Mock-based Unit Tests', () => {
let validator: WorkflowValidator;
let mockNodeRepository: any;
let mockGetNode: Mock;
beforeEach(() => {
vi.clearAllMocks();
// Create detailed mock repository with spy functions
mockGetNode = vi.fn();
mockNodeRepository = {
getNode: mockGetNode
};
validator = new WorkflowValidator(mockNodeRepository, EnhancedConfigValidator);
// Default mock responses
mockGetNode.mockImplementation((type: string) => {
if (type.includes('httpRequest')) {
return {
node_type: type,
display_name: 'HTTP Request',
isVersioned: true,
version: 4
};
} else if (type.includes('set')) {
return {
node_type: type,
display_name: 'Set',
isVersioned: true,
version: 3
};
} else if (type.includes('respondToWebhook')) {
return {
node_type: type,
display_name: 'Respond to Webhook',
isVersioned: true,
version: 1
};
}
return null;
});
});
describe('Error Handler Detection Logic', () => {
it('should correctly identify error handlers by node name patterns', async () => {
const errorNodeNames = [
'Error Handler',
'Handle Error',
'Catch Exception',
'Failure Response',
'Error Notification',
'Fail Safe',
'Exception Handler',
'Error Callback'
];
const successNodeNames = [
'Process Data',
'Transform',
'Success Handler',
'Continue Process',
'Normal Flow'
];
for (const errorName of errorNodeNames) {
const workflow = {
nodes: [
{
id: '1',
name: 'Source',
type: 'n8n-nodes-base.httpRequest',
position: [0, 0],
parameters: {}
},
{
id: '2',
name: 'Success Path',
type: 'n8n-nodes-base.set',
position: [200, 0],
parameters: {}
},
{
id: '3',
name: errorName,
type: 'n8n-nodes-base.set',
position: [200, 100],
parameters: {}
}
],
connections: {
'Source': {
main: [
[
{ node: 'Success Path', type: 'main', index: 0 },
{ node: errorName, type: 'main', index: 0 } // Should be detected as error handler
]
]
}
}
};
const result = await validator.validateWorkflow(workflow as any);
// Should detect this as an incorrect error configuration
const hasError = result.errors.some(e =>
e.message.includes('Incorrect error output configuration') &&
e.message.includes(errorName)
);
expect(hasError).toBe(true);
}
// Test that success node names are NOT flagged
for (const successName of successNodeNames) {
const workflow = {
nodes: [
{
id: '1',
name: 'Source',
type: 'n8n-nodes-base.httpRequest',
position: [0, 0],
parameters: {}
},
{
id: '2',
name: 'First Process',
type: 'n8n-nodes-base.set',
position: [200, 0],
parameters: {}
},
{
id: '3',
name: successName,
type: 'n8n-nodes-base.set',
position: [200, 100],
parameters: {}
}
],
connections: {
'Source': {
main: [
[
{ node: 'First Process', type: 'main', index: 0 },
{ node: successName, type: 'main', index: 0 }
]
]
}
}
};
const result = await validator.validateWorkflow(workflow as any);
// Should NOT detect this as an error configuration
const hasError = result.errors.some(e =>
e.message.includes('Incorrect error output configuration')
);
expect(hasError).toBe(false);
}
});
it('should correctly identify error handlers by node type patterns', async () => {
const errorNodeTypes = [
'n8n-nodes-base.respondToWebhook',
'n8n-nodes-base.emailSend'
// Note: slack and webhook are not in the current detection logic
];
// Update mock to return appropriate node info for these types
mockGetNode.mockImplementation((type: string) => {
return {
node_type: type,
display_name: type.split('.').pop() || 'Unknown',
isVersioned: true,
version: 1
};
});
for (const nodeType of errorNodeTypes) {
const workflow = {
nodes: [
{
id: '1',
name: 'Source',
type: 'n8n-nodes-base.httpRequest',
position: [0, 0],
parameters: {}
},
{
id: '2',
name: 'Success Path',
type: 'n8n-nodes-base.set',
position: [200, 0],
parameters: {}
},
{
id: '3',
name: 'Response Node',
type: nodeType,
position: [200, 100],
parameters: {}
}
],
connections: {
'Source': {
main: [
[
{ node: 'Success Path', type: 'main', index: 0 },
{ node: 'Response Node', type: 'main', index: 0 } // Should be detected
]
]
}
}
};
const result = await validator.validateWorkflow(workflow as any);
// Should detect this as an incorrect error configuration
const hasError = result.errors.some(e =>
e.message.includes('Incorrect error output configuration') &&
e.message.includes('Response Node')
);
expect(hasError).toBe(true);
}
});
it('should handle cases where node repository returns null', async () => {
// Mock repository to return null for unknown nodes
mockGetNode.mockImplementation((type: string) => {
if (type === 'n8n-nodes-base.unknownNode') {
return null;
}
return {
node_type: type,
display_name: 'Known Node',
isVersioned: true,
version: 1
};
});
const workflow = {
nodes: [
{
id: '1',
name: 'Source',
type: 'n8n-nodes-base.httpRequest',
position: [0, 0],
parameters: {}
},
{
id: '2',
name: 'Unknown Node',
type: 'n8n-nodes-base.unknownNode',
position: [200, 0],
parameters: {}
},
{
id: '3',
name: 'Error Handler',
type: 'n8n-nodes-base.set',
position: [200, 100],
parameters: {}
}
],
connections: {
'Source': {
main: [
[
{ node: 'Unknown Node', type: 'main', index: 0 },
{ node: 'Error Handler', type: 'main', index: 0 }
]
]
}
}
};
const result = await validator.validateWorkflow(workflow as any);
// Should still detect the error configuration based on node name
const hasError = result.errors.some(e =>
e.message.includes('Incorrect error output configuration') &&
e.message.includes('Error Handler')
);
expect(hasError).toBe(true);
// Should not crash due to null node info
expect(result).toHaveProperty('valid');
expect(Array.isArray(result.errors)).toBe(true);
});
});
describe('onError Property Validation Logic', () => {
it('should validate onError property combinations correctly', async () => {
const testCases = [
{
name: 'onError set but no error connections',
onError: 'continueErrorOutput',
hasErrorConnections: false,
expectedErrorType: 'error',
expectedMessage: "has onError: 'continueErrorOutput' but no error output connections"
},
{
name: 'error connections but no onError',
onError: undefined,
hasErrorConnections: true,
expectedErrorType: 'warning',
expectedMessage: 'error output connections in main[1] but missing onError'
},
{
name: 'onError set with error connections',
onError: 'continueErrorOutput',
hasErrorConnections: true,
expectedErrorType: null,
expectedMessage: null
},
{
name: 'no onError and no error connections',
onError: undefined,
hasErrorConnections: false,
expectedErrorType: null,
expectedMessage: null
}
];
for (const testCase of testCases) {
const workflow = {
nodes: [
{
id: '1',
name: 'Test Node',
type: 'n8n-nodes-base.httpRequest',
position: [0, 0],
parameters: {},
...(testCase.onError ? { onError: testCase.onError } : {})
},
{
id: '2',
name: 'Success Handler',
type: 'n8n-nodes-base.set',
position: [200, 0],
parameters: {}
},
{
id: '3',
name: 'Error Handler',
type: 'n8n-nodes-base.set',
position: [200, 100],
parameters: {}
}
],
connections: {
'Test Node': {
main: [
[
{ node: 'Success Handler', type: 'main', index: 0 }
],
...(testCase.hasErrorConnections ? [
[
{ node: 'Error Handler', type: 'main', index: 0 }
]
] : [])
]
}
}
};
const result = await validator.validateWorkflow(workflow as any);
if (testCase.expectedErrorType === 'error') {
const hasExpectedError = result.errors.some(e =>
e.nodeName === 'Test Node' &&
e.message.includes(testCase.expectedMessage!)
);
expect(hasExpectedError).toBe(true);
} else if (testCase.expectedErrorType === 'warning') {
const hasExpectedWarning = result.warnings.some(w =>
w.nodeName === 'Test Node' &&
w.message.includes(testCase.expectedMessage!)
);
expect(hasExpectedWarning).toBe(true);
} else {
// Should not have related errors or warnings about onError/error output mismatches
const hasRelatedError = result.errors.some(e =>
e.nodeName === 'Test Node' &&
(e.message.includes("has onError: 'continueErrorOutput' but no error output connections") ||
e.message.includes('Incorrect error output configuration'))
);
const hasRelatedWarning = result.warnings.some(w =>
w.nodeName === 'Test Node' &&
w.message.includes('error output connections in main[1] but missing onError')
);
expect(hasRelatedError).toBe(false);
expect(hasRelatedWarning).toBe(false);
}
}
});
it('should handle different onError values correctly', async () => {
const onErrorValues = [
'continueErrorOutput',
'continueRegularOutput',
'stopWorkflow'
];
for (const onErrorValue of onErrorValues) {
const workflow = {
nodes: [
{
id: '1',
name: 'Test Node',
type: 'n8n-nodes-base.httpRequest',
position: [0, 0],
parameters: {},
onError: onErrorValue
},
{
id: '2',
name: 'Next Node',
type: 'n8n-nodes-base.set',
position: [200, 0],
parameters: {}
}
],
connections: {
'Test Node': {
main: [
[
{ node: 'Next Node', type: 'main', index: 0 }
]
// No error connections
]
}
}
};
const result = await validator.validateWorkflow(workflow as any);
if (onErrorValue === 'continueErrorOutput') {
// Should have error about missing error connections
const hasError = result.errors.some(e =>
e.nodeName === 'Test Node' &&
e.message.includes("has onError: 'continueErrorOutput' but no error output connections")
);
expect(hasError).toBe(true);
} else {
// Should not have error about missing error connections
const hasError = result.errors.some(e =>
e.nodeName === 'Test Node' &&
e.message.includes('but no error output connections')
);
expect(hasError).toBe(false);
}
}
});
});
describe('JSON Format Generation', () => {
it('should generate valid JSON in error messages', async () => {
const workflow = {
nodes: [
{
id: '1',
name: 'API Call',
type: 'n8n-nodes-base.httpRequest',
position: [0, 0],
parameters: {}
},
{
id: '2',
name: 'Success Process',
type: 'n8n-nodes-base.set',
position: [200, 0],
parameters: {}
},
{
id: '3',
name: 'Error Handler',
type: 'n8n-nodes-base.respondToWebhook',
position: [200, 100],
parameters: {}
}
],
connections: {
'API Call': {
main: [
[
{ node: 'Success Process', type: 'main', index: 0 },
{ node: 'Error Handler', type: 'main', index: 0 }
]
]
}
}
};
const result = await validator.validateWorkflow(workflow as any);
const errorConfigError = result.errors.find(e =>
e.message.includes('Incorrect error output configuration')
);
expect(errorConfigError).toBeDefined();
// Extract JSON sections from error message
const incorrectMatch = errorConfigError!.message.match(/INCORRECT \(current\):\n([\s\S]*?)\n\nCORRECT/);
const correctMatch = errorConfigError!.message.match(/CORRECT \(should be\):\n([\s\S]*?)\n\nAlso add/);
expect(incorrectMatch).toBeDefined();
expect(correctMatch).toBeDefined();
// Extract just the JSON part (remove comments)
const incorrectJsonStr = incorrectMatch![1];
const correctJsonStr = correctMatch![1];
// Remove comments and clean up for JSON parsing
const cleanIncorrectJson = incorrectJsonStr.replace(/\/\/.*$/gm, '').replace(/,\s*$/, '');
const cleanCorrectJson = correctJsonStr.replace(/\/\/.*$/gm, '').replace(/,\s*$/, '');
const incorrectJson = `{${cleanIncorrectJson}}`;
const correctJson = `{${cleanCorrectJson}}`;
expect(() => JSON.parse(incorrectJson)).not.toThrow();
expect(() => JSON.parse(correctJson)).not.toThrow();
const parsedIncorrect = JSON.parse(incorrectJson);
const parsedCorrect = JSON.parse(correctJson);
// Validate structure
expect(parsedIncorrect).toHaveProperty('API Call');
expect(parsedCorrect).toHaveProperty('API Call');
expect(parsedIncorrect['API Call']).toHaveProperty('main');
expect(parsedCorrect['API Call']).toHaveProperty('main');
// Incorrect should have both nodes in main[0]
expect(Array.isArray(parsedIncorrect['API Call'].main)).toBe(true);
expect(parsedIncorrect['API Call'].main).toHaveLength(1);
expect(parsedIncorrect['API Call'].main[0]).toHaveLength(2);
// Correct should have separate arrays
expect(Array.isArray(parsedCorrect['API Call'].main)).toBe(true);
expect(parsedCorrect['API Call'].main).toHaveLength(2);
expect(parsedCorrect['API Call'].main[0]).toHaveLength(1); // Success only
expect(parsedCorrect['API Call'].main[1]).toHaveLength(1); // Error only
});
it('should handle special characters in node names in JSON', async () => {
// Test simpler special characters that are easier to handle in JSON
const specialNodeNames = [
'Node with spaces',
'Node-with-dashes',
'Node_with_underscores'
];
for (const specialName of specialNodeNames) {
const workflow = {
nodes: [
{
id: '1',
name: 'Source',
type: 'n8n-nodes-base.httpRequest',
position: [0, 0],
parameters: {}
},
{
id: '2',
name: 'Success',
type: 'n8n-nodes-base.set',
position: [200, 0],
parameters: {}
},
{
id: '3',
name: specialName,
type: 'n8n-nodes-base.respondToWebhook',
position: [200, 100],
parameters: {}
}
],
connections: {
'Source': {
main: [
[
{ node: 'Success', type: 'main', index: 0 },
{ node: specialName, type: 'main', index: 0 }
]
]
}
}
};
const result = await validator.validateWorkflow(workflow as any);
const errorConfigError = result.errors.find(e =>
e.message.includes('Incorrect error output configuration')
);
expect(errorConfigError).toBeDefined();
// Verify the error message contains the special node name
expect(errorConfigError!.message).toContain(specialName);
// Verify JSON structure is present (but don't parse due to comments)
expect(errorConfigError!.message).toContain('INCORRECT (current):');
expect(errorConfigError!.message).toContain('CORRECT (should be):');
expect(errorConfigError!.message).toContain('main[0]');
expect(errorConfigError!.message).toContain('main[1]');
}
});
});
describe('Repository Interaction Patterns', () => {
it('should call repository getNode with correct parameters', async () => {
const workflow = {
nodes: [
{
id: '1',
name: 'HTTP Node',
type: 'n8n-nodes-base.httpRequest',
position: [0, 0],
parameters: {}
},
{
id: '2',
name: 'Set Node',
type: 'n8n-nodes-base.set',
position: [200, 0],
parameters: {}
}
],
connections: {
'HTTP Node': {
main: [
[
{ node: 'Set Node', type: 'main', index: 0 }
]
]
}
}
};
await validator.validateWorkflow(workflow as any);
// Should have called getNode for each node type
expect(mockGetNode).toHaveBeenCalledWith('n8n-nodes-base.httpRequest');
expect(mockGetNode).toHaveBeenCalledWith('n8n-nodes-base.set');
expect(mockGetNode).toHaveBeenCalledTimes(2);
});
it('should handle repository errors gracefully', async () => {
// Mock repository to throw error
mockGetNode.mockImplementation(() => {
throw new Error('Database connection failed');
});
const workflow = {
nodes: [
{
id: '1',
name: 'Test Node',
type: 'n8n-nodes-base.httpRequest',
position: [0, 0],
parameters: {}
}
],
connections: {}
};
// Should not throw error
const result = await validator.validateWorkflow(workflow as any);
// Should still return a valid result
expect(result).toHaveProperty('valid');
expect(Array.isArray(result.errors)).toBe(true);
expect(Array.isArray(result.warnings)).toBe(true);
});
it('should optimize repository calls for duplicate node types', async () => {
const workflow = {
nodes: [
{
id: '1',
name: 'HTTP 1',
type: 'n8n-nodes-base.httpRequest',
position: [0, 0],
parameters: {}
},
{
id: '2',
name: 'HTTP 2',
type: 'n8n-nodes-base.httpRequest',
position: [200, 0],
parameters: {}
},
{
id: '3',
name: 'HTTP 3',
type: 'n8n-nodes-base.httpRequest',
position: [400, 0],
parameters: {}
}
],
connections: {}
};
await validator.validateWorkflow(workflow as any);
// Should call getNode for the same type multiple times (current implementation)
// Note: This test documents current behavior. Could be optimized in the future.
const httpRequestCalls = mockGetNode.mock.calls.filter(
call => call[0] === 'n8n-nodes-base.httpRequest'
);
expect(httpRequestCalls.length).toBeGreaterThan(0);
});
});
});

View File

@@ -0,0 +1,528 @@
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';
vi.mock('@/utils/logger');
describe('WorkflowValidator - Performance Tests', () => {
let validator: WorkflowValidator;
let mockNodeRepository: any;
beforeEach(() => {
vi.clearAllMocks();
// Create mock repository with performance optimizations
mockNodeRepository = {
getNode: vi.fn((type: string) => {
// Return mock node info for any node type to avoid database calls
return {
node_type: type,
display_name: 'Mock Node',
isVersioned: true,
version: 1
};
})
};
validator = new WorkflowValidator(mockNodeRepository, EnhancedConfigValidator);
});
describe('Large Workflow Performance', () => {
it('should validate large workflows with many error paths efficiently', async () => {
// Generate a large workflow with 500 nodes
const nodeCount = 500;
const nodes = [];
const connections: any = {};
// Create nodes with various error handling patterns
for (let i = 1; i <= nodeCount; i++) {
nodes.push({
id: i.toString(),
name: `Node${i}`,
type: i % 5 === 0 ? 'n8n-nodes-base.httpRequest' : 'n8n-nodes-base.set',
typeVersion: 1,
position: [i * 10, (i % 10) * 100],
parameters: {},
...(i % 3 === 0 ? { onError: 'continueErrorOutput' } : {})
});
}
// Create connections with multiple error handling scenarios
for (let i = 1; i < nodeCount; i++) {
const hasErrorHandling = i % 3 === 0;
const hasMultipleConnections = i % 7 === 0;
if (hasErrorHandling && hasMultipleConnections) {
// Mix correct and incorrect error handling patterns
const isIncorrect = i % 14 === 0;
if (isIncorrect) {
// Incorrect: error handlers mixed with success nodes in main[0]
connections[`Node${i}`] = {
main: [
[
{ node: `Node${i + 1}`, type: 'main', index: 0 },
{ node: `Error Handler ${i}`, type: 'main', index: 0 } // Wrong!
]
]
};
} else {
// Correct: separate success and error outputs
connections[`Node${i}`] = {
main: [
[
{ node: `Node${i + 1}`, type: 'main', index: 0 }
],
[
{ node: `Error Handler ${i}`, type: 'main', index: 0 }
]
]
};
}
// Add error handler node
nodes.push({
id: `error-${i}`,
name: `Error Handler ${i}`,
type: 'n8n-nodes-base.respondToWebhook',
typeVersion: 1,
position: [(i + nodeCount) * 10, 500],
parameters: {}
});
} else {
// Simple connection
connections[`Node${i}`] = {
main: [
[
{ node: `Node${i + 1}`, type: 'main', index: 0 }
]
]
};
}
}
const workflow = { nodes, connections };
const startTime = performance.now();
const result = await validator.validateWorkflow(workflow as any);
const endTime = performance.now();
const executionTime = endTime - startTime;
// Validation should complete within reasonable time
expect(executionTime).toBeLessThan(10000); // Less than 10 seconds
// Should still catch validation errors
expect(Array.isArray(result.errors)).toBe(true);
expect(Array.isArray(result.warnings)).toBe(true);
// Should detect incorrect error configurations
const incorrectConfigErrors = result.errors.filter(e =>
e.message.includes('Incorrect error output configuration')
);
expect(incorrectConfigErrors.length).toBeGreaterThan(0);
console.log(`Validated ${nodes.length} nodes in ${executionTime.toFixed(2)}ms`);
console.log(`Found ${result.errors.length} errors and ${result.warnings.length} warnings`);
});
it('should handle deeply nested error handling chains efficiently', async () => {
// Create a chain of error handlers, each with their own error handling
const chainLength = 100;
const nodes = [];
const connections: any = {};
for (let i = 1; i <= chainLength; i++) {
// Main processing node
nodes.push({
id: `main-${i}`,
name: `Main ${i}`,
type: 'n8n-nodes-base.httpRequest',
typeVersion: 1,
position: [i * 150, 100],
parameters: {},
onError: 'continueErrorOutput'
});
// Error handler node
nodes.push({
id: `error-${i}`,
name: `Error Handler ${i}`,
type: 'n8n-nodes-base.httpRequest',
typeVersion: 1,
position: [i * 150, 300],
parameters: {},
onError: 'continueErrorOutput'
});
// Fallback error node
nodes.push({
id: `fallback-${i}`,
name: `Fallback ${i}`,
type: 'n8n-nodes-base.set',
typeVersion: 1,
position: [i * 150, 500],
parameters: {}
});
// Connections
connections[`Main ${i}`] = {
main: [
// Success path
i < chainLength ? [{ node: `Main ${i + 1}`, type: 'main', index: 0 }] : [],
// Error path
[{ node: `Error Handler ${i}`, type: 'main', index: 0 }]
]
};
connections[`Error Handler ${i}`] = {
main: [
// Success path (continue to next error handler or end)
[],
// Error path (go to fallback)
[{ node: `Fallback ${i}`, type: 'main', index: 0 }]
]
};
}
const workflow = { nodes, connections };
const startTime = performance.now();
const result = await validator.validateWorkflow(workflow as any);
const endTime = performance.now();
const executionTime = endTime - startTime;
// Should complete quickly even with complex nested error handling
expect(executionTime).toBeLessThan(5000); // Less than 5 seconds
// Should not have errors about incorrect configuration (this is correct)
const incorrectConfigErrors = result.errors.filter(e =>
e.message.includes('Incorrect error output configuration')
);
expect(incorrectConfigErrors.length).toBe(0);
console.log(`Validated ${nodes.length} nodes with nested error handling in ${executionTime.toFixed(2)}ms`);
});
it('should efficiently validate workflows with many parallel error paths', async () => {
// Create a workflow with one source node that fans out to many parallel paths,
// each with their own error handling
const parallelPathCount = 200;
const nodes = [
{
id: 'source',
name: 'Source',
type: 'n8n-nodes-base.webhook',
typeVersion: 1,
position: [0, 0],
parameters: {}
}
];
const connections: any = {
'Source': {
main: [[]]
}
};
// Create parallel paths
for (let i = 1; i <= parallelPathCount; i++) {
// Processing node
nodes.push({
id: `process-${i}`,
name: `Process ${i}`,
type: 'n8n-nodes-base.httpRequest',
typeVersion: 1,
position: [200, i * 20],
parameters: {},
onError: 'continueErrorOutput'
} as any);
// Success handler
nodes.push({
id: `success-${i}`,
name: `Success ${i}`,
type: 'n8n-nodes-base.set',
typeVersion: 1,
position: [400, i * 20],
parameters: {}
});
// Error handler
nodes.push({
id: `error-${i}`,
name: `Error Handler ${i}`,
type: 'n8n-nodes-base.respondToWebhook',
typeVersion: 1,
position: [400, i * 20 + 10],
parameters: {}
});
// Connect source to processing node
connections['Source'].main[0].push({
node: `Process ${i}`,
type: 'main',
index: 0
});
// Connect processing node to success and error handlers
connections[`Process ${i}`] = {
main: [
[{ node: `Success ${i}`, type: 'main', index: 0 }],
[{ node: `Error Handler ${i}`, type: 'main', index: 0 }]
]
};
}
const workflow = { nodes, connections };
const startTime = performance.now();
const result = await validator.validateWorkflow(workflow as any);
const endTime = performance.now();
const executionTime = endTime - startTime;
// Should validate efficiently despite many parallel paths
expect(executionTime).toBeLessThan(8000); // Less than 8 seconds
// Should not have errors about incorrect configuration
const incorrectConfigErrors = result.errors.filter(e =>
e.message.includes('Incorrect error output configuration')
);
expect(incorrectConfigErrors.length).toBe(0);
console.log(`Validated ${nodes.length} nodes with ${parallelPathCount} parallel error paths in ${executionTime.toFixed(2)}ms`);
});
it('should handle worst-case scenario with many incorrect configurations efficiently', async () => {
// Create a workflow where many nodes have the incorrect error configuration
// This tests the performance of the error detection algorithm
const nodeCount = 300;
const nodes = [];
const connections: any = {};
for (let i = 1; i <= nodeCount; i++) {
// Main node
nodes.push({
id: `main-${i}`,
name: `Main ${i}`,
type: 'n8n-nodes-base.httpRequest',
typeVersion: 1,
position: [i * 20, 100],
parameters: {}
});
// Success handler
nodes.push({
id: `success-${i}`,
name: `Success ${i}`,
type: 'n8n-nodes-base.set',
typeVersion: 1,
position: [i * 20, 200],
parameters: {}
});
// Error handler (with error-indicating name)
nodes.push({
id: `error-${i}`,
name: `Error Handler ${i}`,
type: 'n8n-nodes-base.respondToWebhook',
typeVersion: 1,
position: [i * 20, 300],
parameters: {}
});
// INCORRECT configuration: both success and error handlers in main[0]
connections[`Main ${i}`] = {
main: [
[
{ node: `Success ${i}`, type: 'main', index: 0 },
{ node: `Error Handler ${i}`, type: 'main', index: 0 } // Wrong!
]
]
};
}
const workflow = { nodes, connections };
const startTime = performance.now();
const result = await validator.validateWorkflow(workflow as any);
const endTime = performance.now();
const executionTime = endTime - startTime;
// Should complete within reasonable time even when generating many errors
expect(executionTime).toBeLessThan(15000); // Less than 15 seconds
// Should detect ALL incorrect configurations
const incorrectConfigErrors = result.errors.filter(e =>
e.message.includes('Incorrect error output configuration')
);
expect(incorrectConfigErrors.length).toBe(nodeCount); // One error per node
console.log(`Detected ${incorrectConfigErrors.length} incorrect configurations in ${nodes.length} nodes in ${executionTime.toFixed(2)}ms`);
});
});
describe('Memory Usage and Optimization', () => {
it('should not leak memory during large workflow validation', async () => {
// Get initial memory usage
const initialMemory = process.memoryUsage().heapUsed;
// Validate multiple large workflows
for (let run = 0; run < 5; run++) {
const nodeCount = 200;
const nodes = [];
const connections: any = {};
for (let i = 1; i <= nodeCount; i++) {
nodes.push({
id: i.toString(),
name: `Node${i}`,
type: 'n8n-nodes-base.httpRequest',
typeVersion: 1,
position: [i * 10, 100],
parameters: {},
onError: 'continueErrorOutput'
});
if (i > 1) {
connections[`Node${i - 1}`] = {
main: [
[{ node: `Node${i}`, type: 'main', index: 0 }],
[{ node: `Error${i}`, type: 'main', index: 0 }]
]
};
nodes.push({
id: `error-${i}`,
name: `Error${i}`,
type: 'n8n-nodes-base.set',
typeVersion: 1,
position: [i * 10, 200],
parameters: {}
});
}
}
const workflow = { nodes, connections };
await validator.validateWorkflow(workflow as any);
// Force garbage collection if available
if (global.gc) {
global.gc();
}
}
const finalMemory = process.memoryUsage().heapUsed;
const memoryIncrease = finalMemory - initialMemory;
const memoryIncreaseMB = memoryIncrease / (1024 * 1024);
// Memory increase should be reasonable (less than 50MB)
expect(memoryIncreaseMB).toBeLessThan(50);
console.log(`Memory increase after 5 large workflow validations: ${memoryIncreaseMB.toFixed(2)}MB`);
});
it('should handle concurrent validation requests efficiently', async () => {
// Create multiple validation requests that run concurrently
const concurrentRequests = 10;
const workflows = [];
// Prepare workflows
for (let r = 0; r < concurrentRequests; r++) {
const nodeCount = 50;
const nodes = [];
const connections: any = {};
for (let i = 1; i <= nodeCount; i++) {
nodes.push({
id: `${r}-${i}`,
name: `R${r}Node${i}`,
type: i % 2 === 0 ? 'n8n-nodes-base.httpRequest' : 'n8n-nodes-base.set',
typeVersion: 1,
position: [i * 20, r * 100],
parameters: {},
...(i % 3 === 0 ? { onError: 'continueErrorOutput' } : {})
});
if (i > 1) {
const hasError = i % 3 === 0;
const isIncorrect = i % 6 === 0;
if (hasError && isIncorrect) {
// Incorrect configuration
connections[`R${r}Node${i - 1}`] = {
main: [
[
{ node: `R${r}Node${i}`, type: 'main', index: 0 },
{ node: `R${r}Error${i}`, type: 'main', index: 0 } // Wrong!
]
]
};
nodes.push({
id: `${r}-error-${i}`,
name: `R${r}Error${i}`,
type: 'n8n-nodes-base.respondToWebhook',
typeVersion: 1,
position: [i * 20, r * 100 + 50],
parameters: {}
});
} else if (hasError) {
// Correct configuration
connections[`R${r}Node${i - 1}`] = {
main: [
[{ node: `R${r}Node${i}`, type: 'main', index: 0 }],
[{ node: `R${r}Error${i}`, type: 'main', index: 0 }]
]
};
nodes.push({
id: `${r}-error-${i}`,
name: `R${r}Error${i}`,
type: 'n8n-nodes-base.set',
typeVersion: 1,
position: [i * 20, r * 100 + 50],
parameters: {}
});
} else {
// Normal connection
connections[`R${r}Node${i - 1}`] = {
main: [
[{ node: `R${r}Node${i}`, type: 'main', index: 0 }]
]
};
}
}
}
workflows.push({ nodes, connections });
}
// Run concurrent validations
const startTime = performance.now();
const results = await Promise.all(
workflows.map(workflow => validator.validateWorkflow(workflow as any))
);
const endTime = performance.now();
const totalTime = endTime - startTime;
// All validations should complete
expect(results).toHaveLength(concurrentRequests);
// Each result should be valid
results.forEach(result => {
expect(Array.isArray(result.errors)).toBe(true);
expect(Array.isArray(result.warnings)).toBe(true);
});
// Concurrent execution should be efficient
expect(totalTime).toBeLessThan(20000); // Less than 20 seconds total
console.log(`Completed ${concurrentRequests} concurrent validations in ${totalTime.toFixed(2)}ms`);
});
});
});