feat: enhance workflow validation to prevent broken workflows (v2.6.2)
- Add node type existence validation that catches invalid types - Fix critical issue: now catches 'nodes-base.webhook' BEFORE database lookup - Add smart suggestions for common mistakes (webhook → n8n-nodes-base.webhook) - Add minimum viable workflow validation (prevents single-node workflows) - Add empty connection detection for multi-node workflows - Add helper functions for workflow structure examples and fix suggestions - Prevent AI agents from creating workflows with question mark nodes This fixes the exact issue where workflows created with 'nodes-base.webhook' would show as broken (question marks) in the n8n UI. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
157
scripts/test-empty-connection-validation.ts
Normal file
157
scripts/test-empty-connection-validation.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
#!/usr/bin/env tsx
|
||||
|
||||
/**
|
||||
* Test script for empty connection validation
|
||||
* Tests the improvements to prevent broken workflows like the one in the logs
|
||||
*/
|
||||
|
||||
import { WorkflowValidator } from '../src/services/workflow-validator';
|
||||
import { EnhancedConfigValidator } from '../src/services/enhanced-config-validator';
|
||||
import { NodeRepository } from '../src/database/node-repository';
|
||||
import { createDatabaseAdapter } from '../src/database/database-adapter';
|
||||
import { validateWorkflowStructure, getWorkflowFixSuggestions, getWorkflowStructureExample } from '../src/services/n8n-validation';
|
||||
import { Logger } from '../src/utils/logger';
|
||||
|
||||
const logger = new Logger({ prefix: '[TestEmptyConnectionValidation]' });
|
||||
|
||||
async function testValidation() {
|
||||
const adapter = await createDatabaseAdapter('./data/nodes.db');
|
||||
const repository = new NodeRepository(adapter);
|
||||
const validator = new WorkflowValidator(repository, EnhancedConfigValidator);
|
||||
|
||||
logger.info('Testing empty connection validation...\n');
|
||||
|
||||
// Test 1: The broken workflow from the logs
|
||||
const brokenWorkflow = {
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"id": "webhook_node",
|
||||
"name": "Webhook",
|
||||
"type": "nodes-base.webhook",
|
||||
"typeVersion": 2,
|
||||
"position": [260, 300] as [number, number]
|
||||
}
|
||||
],
|
||||
"connections": {},
|
||||
"pinData": {},
|
||||
"meta": {
|
||||
"instanceId": "74e11c77e266f2c77f6408eb6c88e3fec63c9a5d8c4a3a2ea4c135c542012d6b"
|
||||
}
|
||||
};
|
||||
|
||||
logger.info('Test 1: Broken single-node workflow with empty connections');
|
||||
const result1 = await validator.validateWorkflow(brokenWorkflow as any);
|
||||
|
||||
logger.info('Validation result:');
|
||||
logger.info(`Valid: ${result1.valid}`);
|
||||
logger.info(`Errors: ${result1.errors.length}`);
|
||||
result1.errors.forEach(err => {
|
||||
if (typeof err === 'string') {
|
||||
logger.error(` - ${err}`);
|
||||
} else if (err && typeof err === 'object' && 'message' in err) {
|
||||
logger.error(` - ${err.message}`);
|
||||
} else {
|
||||
logger.error(` - ${JSON.stringify(err)}`);
|
||||
}
|
||||
});
|
||||
logger.info(`Warnings: ${result1.warnings.length}`);
|
||||
result1.warnings.forEach(warn => logger.warn(` - ${warn.message || JSON.stringify(warn)}`));
|
||||
logger.info(`Suggestions: ${result1.suggestions.length}`);
|
||||
result1.suggestions.forEach(sug => logger.info(` - ${sug}`));
|
||||
|
||||
// Test 2: Multi-node workflow with no connections
|
||||
const multiNodeNoConnections = {
|
||||
"name": "Test Workflow",
|
||||
"nodes": [
|
||||
{
|
||||
"id": "manual-1",
|
||||
"name": "Manual Trigger",
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [250, 300] as [number, number],
|
||||
"parameters": {}
|
||||
},
|
||||
{
|
||||
"id": "set-1",
|
||||
"name": "Set",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3.4,
|
||||
"position": [450, 300] as [number, number],
|
||||
"parameters": {}
|
||||
}
|
||||
],
|
||||
"connections": {}
|
||||
};
|
||||
|
||||
logger.info('\nTest 2: Multi-node workflow with empty connections');
|
||||
const result2 = await validator.validateWorkflow(multiNodeNoConnections as any);
|
||||
|
||||
logger.info('Validation result:');
|
||||
logger.info(`Valid: ${result2.valid}`);
|
||||
logger.info(`Errors: ${result2.errors.length}`);
|
||||
result2.errors.forEach(err => logger.error(` - ${err.message || JSON.stringify(err)}`));
|
||||
logger.info(`Suggestions: ${result2.suggestions.length}`);
|
||||
result2.suggestions.forEach(sug => logger.info(` - ${sug}`));
|
||||
|
||||
// Test 3: Using n8n-validation functions
|
||||
logger.info('\nTest 3: Testing n8n-validation.ts functions');
|
||||
|
||||
const errors = validateWorkflowStructure(brokenWorkflow as any);
|
||||
logger.info('Validation errors:');
|
||||
errors.forEach(err => logger.error(` - ${err}`));
|
||||
|
||||
const suggestions = getWorkflowFixSuggestions(errors);
|
||||
logger.info('Fix suggestions:');
|
||||
suggestions.forEach(sug => logger.info(` - ${sug}`));
|
||||
|
||||
logger.info('\nExample of proper workflow structure:');
|
||||
logger.info(getWorkflowStructureExample());
|
||||
|
||||
// Test 4: Workflow using IDs instead of names in connections
|
||||
const workflowWithIdConnections = {
|
||||
"name": "Test Workflow",
|
||||
"nodes": [
|
||||
{
|
||||
"id": "manual-1",
|
||||
"name": "Manual Trigger",
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [250, 300] as [number, number],
|
||||
"parameters": {}
|
||||
},
|
||||
{
|
||||
"id": "set-1",
|
||||
"name": "Set Data",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3.4,
|
||||
"position": [450, 300] as [number, number],
|
||||
"parameters": {}
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"manual-1": { // Using ID instead of name!
|
||||
"main": [[{
|
||||
"node": "set-1", // Using ID instead of name!
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}]]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
logger.info('\nTest 4: Workflow using IDs instead of names in connections');
|
||||
const result4 = await validator.validateWorkflow(workflowWithIdConnections as any);
|
||||
|
||||
logger.info('Validation result:');
|
||||
logger.info(`Valid: ${result4.valid}`);
|
||||
logger.info(`Errors: ${result4.errors.length}`);
|
||||
result4.errors.forEach(err => logger.error(` - ${err.message || JSON.stringify(err)}`));
|
||||
|
||||
adapter.close();
|
||||
}
|
||||
|
||||
testValidation().catch(err => {
|
||||
logger.error('Test failed:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
187
scripts/test-node-type-validation.ts
Normal file
187
scripts/test-node-type-validation.ts
Normal file
@@ -0,0 +1,187 @@
|
||||
#!/usr/bin/env tsx
|
||||
|
||||
/**
|
||||
* Test script for node type validation
|
||||
* Tests the improvements to catch invalid node types like "nodes-base.webhook"
|
||||
*/
|
||||
|
||||
import { WorkflowValidator } from '../src/services/workflow-validator';
|
||||
import { EnhancedConfigValidator } from '../src/services/enhanced-config-validator';
|
||||
import { NodeRepository } from '../src/database/node-repository';
|
||||
import { createDatabaseAdapter } from '../src/database/database-adapter';
|
||||
import { validateWorkflowStructure } from '../src/services/n8n-validation';
|
||||
import { Logger } from '../src/utils/logger';
|
||||
|
||||
const logger = new Logger({ prefix: '[TestNodeTypeValidation]' });
|
||||
|
||||
async function testValidation() {
|
||||
const adapter = await createDatabaseAdapter('./data/nodes.db');
|
||||
const repository = new NodeRepository(adapter);
|
||||
const validator = new WorkflowValidator(repository, EnhancedConfigValidator);
|
||||
|
||||
logger.info('Testing node type validation...\n');
|
||||
|
||||
// Test 1: The exact broken workflow from Claude Desktop
|
||||
const brokenWorkflowFromLogs = {
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"id": "webhook_node",
|
||||
"name": "Webhook",
|
||||
"type": "nodes-base.webhook", // WRONG! Missing n8n- prefix
|
||||
"typeVersion": 2,
|
||||
"position": [260, 300] as [number, number]
|
||||
}
|
||||
],
|
||||
"connections": {},
|
||||
"pinData": {},
|
||||
"meta": {
|
||||
"instanceId": "74e11c77e266f2c77f6408eb6c88e3fec63c9a5d8c4a3a2ea4c135c542012d6b"
|
||||
}
|
||||
};
|
||||
|
||||
logger.info('Test 1: Invalid node type "nodes-base.webhook" (missing n8n- prefix)');
|
||||
const result1 = await validator.validateWorkflow(brokenWorkflowFromLogs as any);
|
||||
|
||||
logger.info('Validation result:');
|
||||
logger.info(`Valid: ${result1.valid}`);
|
||||
logger.info(`Errors: ${result1.errors.length}`);
|
||||
result1.errors.forEach(err => {
|
||||
if (typeof err === 'string') {
|
||||
logger.error(` - ${err}`);
|
||||
} else if (err && typeof err === 'object' && 'message' in err) {
|
||||
logger.error(` - ${err.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Check if the specific error about nodes-base.webhook was caught
|
||||
const hasNodeBaseError = result1.errors.some(err =>
|
||||
err && typeof err === 'object' && 'message' in err &&
|
||||
err.message.includes('nodes-base.webhook') &&
|
||||
err.message.includes('n8n-nodes-base.webhook')
|
||||
);
|
||||
logger.info(`Caught nodes-base.webhook error: ${hasNodeBaseError ? 'YES ✅' : 'NO ❌'}`);
|
||||
|
||||
// Test 2: Node type without any prefix
|
||||
const noPrefixWorkflow = {
|
||||
"name": "Test Workflow",
|
||||
"nodes": [
|
||||
{
|
||||
"id": "webhook-1",
|
||||
"name": "My Webhook",
|
||||
"type": "webhook", // WRONG! No package prefix
|
||||
"typeVersion": 2,
|
||||
"position": [250, 300] as [number, number],
|
||||
"parameters": {}
|
||||
},
|
||||
{
|
||||
"id": "set-1",
|
||||
"name": "Set Data",
|
||||
"type": "set", // WRONG! No package prefix
|
||||
"typeVersion": 3.4,
|
||||
"position": [450, 300] as [number, number],
|
||||
"parameters": {}
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"My Webhook": {
|
||||
"main": [[{
|
||||
"node": "Set Data",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}]]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
logger.info('\nTest 2: Node types without package prefix ("webhook", "set")');
|
||||
const result2 = await validator.validateWorkflow(noPrefixWorkflow as any);
|
||||
|
||||
logger.info('Validation result:');
|
||||
logger.info(`Valid: ${result2.valid}`);
|
||||
logger.info(`Errors: ${result2.errors.length}`);
|
||||
result2.errors.forEach(err => {
|
||||
if (typeof err === 'string') {
|
||||
logger.error(` - ${err}`);
|
||||
} else if (err && typeof err === 'object' && 'message' in err) {
|
||||
logger.error(` - ${err.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Test 3: Completely invalid node type
|
||||
const invalidNodeWorkflow = {
|
||||
"name": "Test Workflow",
|
||||
"nodes": [
|
||||
{
|
||||
"id": "fake-1",
|
||||
"name": "Fake Node",
|
||||
"type": "n8n-nodes-base.fakeNodeThatDoesNotExist",
|
||||
"typeVersion": 1,
|
||||
"position": [250, 300] as [number, number],
|
||||
"parameters": {}
|
||||
}
|
||||
],
|
||||
"connections": {}
|
||||
};
|
||||
|
||||
logger.info('\nTest 3: Completely invalid node type');
|
||||
const result3 = await validator.validateWorkflow(invalidNodeWorkflow as any);
|
||||
|
||||
logger.info('Validation result:');
|
||||
logger.info(`Valid: ${result3.valid}`);
|
||||
logger.info(`Errors: ${result3.errors.length}`);
|
||||
result3.errors.forEach(err => {
|
||||
if (typeof err === 'string') {
|
||||
logger.error(` - ${err}`);
|
||||
} else if (err && typeof err === 'object' && 'message' in err) {
|
||||
logger.error(` - ${err.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Test 4: Using n8n-validation.ts function
|
||||
logger.info('\nTest 4: Testing n8n-validation.ts with invalid node types');
|
||||
|
||||
const errors = validateWorkflowStructure(brokenWorkflowFromLogs as any);
|
||||
logger.info('Validation errors:');
|
||||
errors.forEach(err => logger.error(` - ${err}`));
|
||||
|
||||
// Test 5: Valid workflow (should pass)
|
||||
const validWorkflow = {
|
||||
"name": "Valid Webhook Workflow",
|
||||
"nodes": [
|
||||
{
|
||||
"id": "webhook-1",
|
||||
"name": "Webhook",
|
||||
"type": "n8n-nodes-base.webhook", // CORRECT!
|
||||
"typeVersion": 2,
|
||||
"position": [250, 300] as [number, number],
|
||||
"parameters": {
|
||||
"path": "my-webhook",
|
||||
"responseMode": "onReceived",
|
||||
"responseData": "allEntries"
|
||||
}
|
||||
}
|
||||
],
|
||||
"connections": {}
|
||||
};
|
||||
|
||||
logger.info('\nTest 5: Valid workflow with correct node type');
|
||||
const result5 = await validator.validateWorkflow(validWorkflow as any);
|
||||
|
||||
logger.info('Validation result:');
|
||||
logger.info(`Valid: ${result5.valid}`);
|
||||
logger.info(`Errors: ${result5.errors.length}`);
|
||||
logger.info(`Warnings: ${result5.warnings.length}`);
|
||||
result5.warnings.forEach(warn => {
|
||||
if (warn && typeof warn === 'object' && 'message' in warn) {
|
||||
logger.warn(` - ${warn.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
adapter.close();
|
||||
}
|
||||
|
||||
testValidation().catch(err => {
|
||||
logger.error('Test failed:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
100
scripts/test-nodes-base-prefix.ts
Normal file
100
scripts/test-nodes-base-prefix.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
#!/usr/bin/env tsx
|
||||
|
||||
/**
|
||||
* Specific test for nodes-base. prefix validation
|
||||
*/
|
||||
|
||||
import { WorkflowValidator } from '../src/services/workflow-validator';
|
||||
import { EnhancedConfigValidator } from '../src/services/enhanced-config-validator';
|
||||
import { NodeRepository } from '../src/database/node-repository';
|
||||
import { createDatabaseAdapter } from '../src/database/database-adapter';
|
||||
import { Logger } from '../src/utils/logger';
|
||||
|
||||
const logger = new Logger({ prefix: '[TestNodesBasePrefix]' });
|
||||
|
||||
async function testValidation() {
|
||||
const adapter = await createDatabaseAdapter('./data/nodes.db');
|
||||
const repository = new NodeRepository(adapter);
|
||||
const validator = new WorkflowValidator(repository, EnhancedConfigValidator);
|
||||
|
||||
logger.info('Testing nodes-base. prefix validation...\n');
|
||||
|
||||
// Test various nodes-base. prefixed types
|
||||
const testCases = [
|
||||
{ type: 'nodes-base.webhook', expected: 'n8n-nodes-base.webhook' },
|
||||
{ type: 'nodes-base.httpRequest', expected: 'n8n-nodes-base.httpRequest' },
|
||||
{ type: 'nodes-base.set', expected: 'n8n-nodes-base.set' },
|
||||
{ type: 'nodes-base.code', expected: 'n8n-nodes-base.code' },
|
||||
{ type: 'nodes-base.slack', expected: 'n8n-nodes-base.slack' },
|
||||
];
|
||||
|
||||
for (const testCase of testCases) {
|
||||
const workflow = {
|
||||
name: `Test ${testCase.type}`,
|
||||
nodes: [{
|
||||
id: 'test-node',
|
||||
name: 'Test Node',
|
||||
type: testCase.type,
|
||||
typeVersion: 1,
|
||||
position: [100, 100] as [number, number],
|
||||
parameters: {}
|
||||
}],
|
||||
connections: {}
|
||||
};
|
||||
|
||||
logger.info(`Testing: "${testCase.type}"`);
|
||||
const result = await validator.validateWorkflow(workflow as any);
|
||||
|
||||
const nodeTypeError = result.errors.find(err =>
|
||||
err && typeof err === 'object' && 'message' in err &&
|
||||
err.message.includes(testCase.type) &&
|
||||
err.message.includes(testCase.expected)
|
||||
);
|
||||
|
||||
if (nodeTypeError) {
|
||||
logger.info(`✅ Caught and suggested: "${testCase.expected}"`);
|
||||
} else {
|
||||
logger.error(`❌ Failed to catch invalid type: "${testCase.type}"`);
|
||||
result.errors.forEach(err => {
|
||||
if (err && typeof err === 'object' && 'message' in err) {
|
||||
logger.error(` Error: ${err.message}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Test that n8n-nodes-base. prefix still works
|
||||
const validWorkflow = {
|
||||
name: 'Valid Workflow',
|
||||
nodes: [{
|
||||
id: 'webhook',
|
||||
name: 'Webhook',
|
||||
type: 'n8n-nodes-base.webhook',
|
||||
typeVersion: 2,
|
||||
position: [100, 100] as [number, number],
|
||||
parameters: {}
|
||||
}],
|
||||
connections: {}
|
||||
};
|
||||
|
||||
logger.info('\nTesting valid n8n-nodes-base.webhook:');
|
||||
const validResult = await validator.validateWorkflow(validWorkflow as any);
|
||||
|
||||
const hasNodeTypeError = validResult.errors.some(err =>
|
||||
err && typeof err === 'object' && 'message' in err &&
|
||||
err.message.includes('node type')
|
||||
);
|
||||
|
||||
if (!hasNodeTypeError) {
|
||||
logger.info('✅ Correctly accepted n8n-nodes-base.webhook');
|
||||
} else {
|
||||
logger.error('❌ Incorrectly rejected valid n8n-nodes-base.webhook');
|
||||
}
|
||||
|
||||
adapter.close();
|
||||
}
|
||||
|
||||
testValidation().catch(err => {
|
||||
logger.error('Test failed:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user