fix: address critical code review issues for validation improvements
- Fix type safety vulnerability in enhanced-config-validator.ts - Added proper type checking before string operations - Return early when nodeType is invalid instead of using empty string - Improve error handling robustness in MCP server - Wrapped validation in try-catch to handle unexpected errors - Properly re-throw ValidationError instances - Add user-friendly error messages for internal errors - Write comprehensive CHANGELOG entry for v2.10.3 - Document fixes for issues #58, #68, #70, #73 - Detail new validation system features - List all enhancements and test coverage Addressed HIGH priority issues from code review: - Type safety holes in config validator - Missing error handling for validation system failures - Consistent error types across validation tools
This commit is contained in:
@@ -223,7 +223,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
|
||||
it('should error when nodes array is missing', async () => {
|
||||
const workflow = { connections: {} } as any;
|
||||
|
||||
const result = await validator.validateWorkflow(workflow);
|
||||
const result = await validator.validateWorkflow(workflow as any);
|
||||
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors.some(e => e.message === 'Workflow must have a nodes array')).toBe(true);
|
||||
@@ -232,7 +232,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
|
||||
it('should error when connections object is missing', async () => {
|
||||
const workflow = { nodes: [] } as any;
|
||||
|
||||
const result = await validator.validateWorkflow(workflow);
|
||||
const result = await validator.validateWorkflow(workflow as any);
|
||||
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors.some(e => e.message === 'Workflow must have a connections object')).toBe(true);
|
||||
@@ -241,7 +241,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
|
||||
it('should warn when workflow has no nodes', async () => {
|
||||
const workflow = { nodes: [], connections: {} } as any;
|
||||
|
||||
const result = await validator.validateWorkflow(workflow);
|
||||
const result = await validator.validateWorkflow(workflow as any);
|
||||
|
||||
expect(result.valid).toBe(true); // Empty workflows are valid but get a warning
|
||||
expect(result.warnings).toHaveLength(1);
|
||||
@@ -260,7 +260,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
|
||||
connections: {}
|
||||
} as any;
|
||||
|
||||
const result = await validator.validateWorkflow(workflow);
|
||||
const result = await validator.validateWorkflow(workflow as any);
|
||||
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors.some(e => e.message.includes('Single-node workflows are only valid for webhook endpoints'))).toBe(true);
|
||||
@@ -279,7 +279,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
|
||||
connections: {}
|
||||
} as any;
|
||||
|
||||
const result = await validator.validateWorkflow(workflow);
|
||||
const result = await validator.validateWorkflow(workflow as any);
|
||||
|
||||
expect(result.valid).toBe(true);
|
||||
expect(result.warnings.some(w => w.message.includes('Webhook node has no connections'))).toBe(true);
|
||||
@@ -306,7 +306,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
|
||||
connections: {}
|
||||
} as any;
|
||||
|
||||
const result = await validator.validateWorkflow(workflow);
|
||||
const result = await validator.validateWorkflow(workflow as any);
|
||||
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors.some(e => e.message.includes('Multi-node workflow has no connections'))).toBe(true);
|
||||
@@ -333,7 +333,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
|
||||
connections: {}
|
||||
} as any;
|
||||
|
||||
const result = await validator.validateWorkflow(workflow);
|
||||
const result = await validator.validateWorkflow(workflow as any);
|
||||
|
||||
expect(result.errors.some(e => e.message.includes('Duplicate node name: "Webhook"'))).toBe(true);
|
||||
});
|
||||
@@ -359,7 +359,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
|
||||
connections: {}
|
||||
} as any;
|
||||
|
||||
const result = await validator.validateWorkflow(workflow);
|
||||
const result = await validator.validateWorkflow(workflow as any);
|
||||
|
||||
expect(result.errors.some(e => e.message.includes('Duplicate node ID: "1"'))).toBe(true);
|
||||
});
|
||||
@@ -392,7 +392,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
|
||||
connections: {}
|
||||
} as any;
|
||||
|
||||
const result = await validator.validateWorkflow(workflow);
|
||||
const result = await validator.validateWorkflow(workflow as any);
|
||||
|
||||
expect(result.statistics.triggerNodes).toBe(3);
|
||||
});
|
||||
@@ -422,7 +422,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
|
||||
}
|
||||
} as any;
|
||||
|
||||
const result = await validator.validateWorkflow(workflow);
|
||||
const result = await validator.validateWorkflow(workflow as any);
|
||||
|
||||
expect(result.warnings.some(w => w.message.includes('Workflow has no trigger nodes'))).toBe(true);
|
||||
});
|
||||
@@ -449,7 +449,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
|
||||
connections: {}
|
||||
} as any;
|
||||
|
||||
const result = await validator.validateWorkflow(workflow);
|
||||
const result = await validator.validateWorkflow(workflow as any);
|
||||
|
||||
expect(result.statistics.totalNodes).toBe(2);
|
||||
expect(result.statistics.enabledNodes).toBe(1);
|
||||
@@ -472,7 +472,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
|
||||
connections: {}
|
||||
} as any;
|
||||
|
||||
const result = await validator.validateWorkflow(workflow);
|
||||
const result = await validator.validateWorkflow(workflow as any);
|
||||
|
||||
expect(mockNodeRepository.getNode).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -491,7 +491,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
|
||||
connections: {}
|
||||
} as any;
|
||||
|
||||
const result = await validator.validateWorkflow(workflow);
|
||||
const result = await validator.validateWorkflow(workflow as any);
|
||||
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors.some(e => e.message.includes('Invalid node type: "nodes-base.webhook"'))).toBe(true);
|
||||
@@ -512,7 +512,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
|
||||
connections: {}
|
||||
} as any;
|
||||
|
||||
const result = await validator.validateWorkflow(workflow);
|
||||
const result = await validator.validateWorkflow(workflow as any);
|
||||
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors.some(e => e.message.includes('Unknown node type: "httpRequest"'))).toBe(true);
|
||||
@@ -533,7 +533,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
|
||||
connections: {}
|
||||
} as any;
|
||||
|
||||
const result = await validator.validateWorkflow(workflow);
|
||||
const result = await validator.validateWorkflow(workflow as any);
|
||||
|
||||
expect(mockNodeRepository.getNode).toHaveBeenCalledWith('n8n-nodes-base.webhook');
|
||||
expect(mockNodeRepository.getNode).toHaveBeenCalledWith('nodes-base.webhook');
|
||||
@@ -553,7 +553,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
|
||||
connections: {}
|
||||
} as any;
|
||||
|
||||
const result = await validator.validateWorkflow(workflow);
|
||||
const result = await validator.validateWorkflow(workflow as any);
|
||||
|
||||
expect(mockNodeRepository.getNode).toHaveBeenCalledWith('@n8n/n8n-nodes-langchain.agent');
|
||||
expect(mockNodeRepository.getNode).toHaveBeenCalledWith('nodes-langchain.agent');
|
||||
@@ -574,7 +574,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
|
||||
connections: {}
|
||||
} as any;
|
||||
|
||||
const result = await validator.validateWorkflow(workflow);
|
||||
const result = await validator.validateWorkflow(workflow as any);
|
||||
|
||||
expect(result.errors.some(e => e.message.includes('Missing required property \'typeVersion\''))).toBe(true);
|
||||
});
|
||||
@@ -594,7 +594,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
|
||||
connections: {}
|
||||
} as any;
|
||||
|
||||
const result = await validator.validateWorkflow(workflow);
|
||||
const result = await validator.validateWorkflow(workflow as any);
|
||||
|
||||
expect(result.errors.some(e => e.message.includes('Invalid typeVersion: invalid'))).toBe(true);
|
||||
});
|
||||
@@ -614,7 +614,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
|
||||
connections: {}
|
||||
} as any;
|
||||
|
||||
const result = await validator.validateWorkflow(workflow);
|
||||
const result = await validator.validateWorkflow(workflow as any);
|
||||
|
||||
expect(result.warnings.some(w => w.message.includes('Outdated typeVersion: 1. Latest is 2'))).toBe(true);
|
||||
});
|
||||
@@ -634,7 +634,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
|
||||
connections: {}
|
||||
} as any;
|
||||
|
||||
const result = await validator.validateWorkflow(workflow);
|
||||
const result = await validator.validateWorkflow(workflow as any);
|
||||
|
||||
expect(result.errors.some(e => e.message.includes('typeVersion 10 exceeds maximum supported version 2'))).toBe(true);
|
||||
});
|
||||
@@ -664,7 +664,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
|
||||
connections: {}
|
||||
} as any;
|
||||
|
||||
const result = await validator.validateWorkflow(workflow);
|
||||
const result = await validator.validateWorkflow(workflow as any);
|
||||
|
||||
expect(result.errors.some(e => e.message.includes('Missing required field: url'))).toBe(true);
|
||||
expect(result.warnings.some(w => w.message.includes('Consider using HTTPS'))).toBe(true);
|
||||
@@ -689,7 +689,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
|
||||
connections: {}
|
||||
} as any;
|
||||
|
||||
const result = await validator.validateWorkflow(workflow);
|
||||
const result = await validator.validateWorkflow(workflow as any);
|
||||
|
||||
expect(result.errors.some(e => e.message.includes('Failed to validate node: Validation error'))).toBe(true);
|
||||
});
|
||||
@@ -721,7 +721,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
|
||||
}
|
||||
} as any;
|
||||
|
||||
const result = await validator.validateWorkflow(workflow);
|
||||
const result = await validator.validateWorkflow(workflow as any);
|
||||
|
||||
expect(result.statistics.validConnections).toBe(1);
|
||||
expect(result.statistics.invalidConnections).toBe(0);
|
||||
@@ -745,7 +745,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
|
||||
}
|
||||
} as any;
|
||||
|
||||
const result = await validator.validateWorkflow(workflow);
|
||||
const result = await validator.validateWorkflow(workflow as any);
|
||||
|
||||
expect(result.errors.some(e => e.message.includes('Connection from non-existent node: "NonExistent"'))).toBe(true);
|
||||
expect(result.statistics.invalidConnections).toBe(1);
|
||||
@@ -776,7 +776,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
|
||||
}
|
||||
} as any;
|
||||
|
||||
const result = await validator.validateWorkflow(workflow);
|
||||
const result = await validator.validateWorkflow(workflow as any);
|
||||
|
||||
expect(result.errors.some(e => e.message.includes('Connection uses node ID \'webhook-id\' instead of node name \'Webhook\''))).toBe(true);
|
||||
});
|
||||
@@ -799,7 +799,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
|
||||
}
|
||||
} as any;
|
||||
|
||||
const result = await validator.validateWorkflow(workflow);
|
||||
const result = await validator.validateWorkflow(workflow as any);
|
||||
|
||||
expect(result.errors.some(e => e.message.includes('Connection to non-existent node: "NonExistent"'))).toBe(true);
|
||||
expect(result.statistics.invalidConnections).toBe(1);
|
||||
@@ -830,7 +830,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
|
||||
}
|
||||
} as any;
|
||||
|
||||
const result = await validator.validateWorkflow(workflow);
|
||||
const result = await validator.validateWorkflow(workflow as any);
|
||||
|
||||
expect(result.errors.some(e => e.message.includes('Connection target uses node ID \'set-id\' instead of node name \'Set\''))).toBe(true);
|
||||
});
|
||||
@@ -861,7 +861,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
|
||||
}
|
||||
} as any;
|
||||
|
||||
const result = await validator.validateWorkflow(workflow);
|
||||
const result = await validator.validateWorkflow(workflow as any);
|
||||
|
||||
expect(result.warnings.some(w => w.message.includes('Connection to disabled node: "Set"'))).toBe(true);
|
||||
});
|
||||
@@ -891,7 +891,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
|
||||
}
|
||||
} as any;
|
||||
|
||||
const result = await validator.validateWorkflow(workflow);
|
||||
const result = await validator.validateWorkflow(workflow as any);
|
||||
|
||||
expect(result.statistics.validConnections).toBe(1);
|
||||
});
|
||||
@@ -921,7 +921,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
|
||||
}
|
||||
} as any;
|
||||
|
||||
const result = await validator.validateWorkflow(workflow);
|
||||
const result = await validator.validateWorkflow(workflow as any);
|
||||
|
||||
expect(result.statistics.validConnections).toBe(1);
|
||||
});
|
||||
@@ -953,7 +953,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
|
||||
}
|
||||
} as any;
|
||||
|
||||
const result = await validator.validateWorkflow(workflow);
|
||||
const result = await validator.validateWorkflow(workflow as any);
|
||||
|
||||
expect(result.warnings.some(w => w.message.includes('Community node "CustomTool" is being used as an AI tool'))).toBe(true);
|
||||
});
|
||||
@@ -990,7 +990,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
|
||||
}
|
||||
} as any;
|
||||
|
||||
const result = await validator.validateWorkflow(workflow);
|
||||
const result = await validator.validateWorkflow(workflow as any);
|
||||
|
||||
expect(result.warnings.some(w => w.message.includes('Node is not connected to any other nodes') && w.nodeName === 'Orphaned')).toBe(true);
|
||||
});
|
||||
@@ -1033,7 +1033,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
|
||||
}
|
||||
} as any;
|
||||
|
||||
const result = await validator.validateWorkflow(workflow);
|
||||
const result = await validator.validateWorkflow(workflow as any);
|
||||
|
||||
expect(result.errors.some(e => e.message.includes('Workflow contains a cycle'))).toBe(true);
|
||||
});
|
||||
@@ -1068,7 +1068,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
|
||||
}
|
||||
} as any;
|
||||
|
||||
const result = await validator.validateWorkflow(workflow);
|
||||
const result = await validator.validateWorkflow(workflow as any);
|
||||
|
||||
expect(result.statistics.validConnections).toBe(1);
|
||||
expect(result.valid).toBe(true);
|
||||
@@ -1110,7 +1110,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
|
||||
}
|
||||
} as any;
|
||||
|
||||
const result = await validator.validateWorkflow(workflow);
|
||||
const result = await validator.validateWorkflow(workflow as any);
|
||||
|
||||
expect(ExpressionValidator.validateNodeExpressions).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ values: expect.any(Object) }),
|
||||
@@ -1146,7 +1146,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
|
||||
connections: {}
|
||||
} as any;
|
||||
|
||||
const result = await validator.validateWorkflow(workflow);
|
||||
const result = await validator.validateWorkflow(workflow as any);
|
||||
|
||||
expect(result.errors.some(e => e.message.includes('Expression error: Invalid expression syntax'))).toBe(true);
|
||||
expect(result.warnings.some(w => w.message.includes('Expression warning: Deprecated variable usage'))).toBe(true);
|
||||
@@ -1170,7 +1170,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
|
||||
connections: {}
|
||||
} as any;
|
||||
|
||||
const result = await validator.validateWorkflow(workflow);
|
||||
const result = await validator.validateWorkflow(workflow as any);
|
||||
|
||||
expect(ExpressionValidator.validateNodeExpressions).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -1187,7 +1187,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
|
||||
|
||||
const workflow = builder.build() as any;
|
||||
|
||||
const result = await validator.validateWorkflow(workflow);
|
||||
const result = await validator.validateWorkflow(workflow as any);
|
||||
|
||||
expect(result.warnings.some(w => w.message.includes('Consider adding error handling'))).toBe(true);
|
||||
});
|
||||
@@ -1208,7 +1208,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
|
||||
|
||||
const workflow = builder.build() as any;
|
||||
|
||||
const result = await validator.validateWorkflow(workflow);
|
||||
const result = await validator.validateWorkflow(workflow as any);
|
||||
|
||||
expect(result.warnings.some(w => w.message.includes('Long linear chain detected'))).toBe(true);
|
||||
});
|
||||
@@ -1230,7 +1230,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
|
||||
connections: {}
|
||||
} as any;
|
||||
|
||||
const result = await validator.validateWorkflow(workflow);
|
||||
const result = await validator.validateWorkflow(workflow as any);
|
||||
|
||||
expect(result.warnings.some(w => w.message.includes('Missing credentials configuration for slackApi'))).toBe(true);
|
||||
});
|
||||
@@ -1249,7 +1249,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
|
||||
connections: {}
|
||||
} as any;
|
||||
|
||||
const result = await validator.validateWorkflow(workflow);
|
||||
const result = await validator.validateWorkflow(workflow as any);
|
||||
|
||||
expect(result.warnings.some(w => w.message.includes('AI Agent has no tools connected'))).toBe(true);
|
||||
});
|
||||
@@ -1279,7 +1279,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
|
||||
}
|
||||
} as any;
|
||||
|
||||
const result = await validator.validateWorkflow(workflow);
|
||||
const result = await validator.validateWorkflow(workflow as any);
|
||||
|
||||
expect(result.suggestions.some(s => s.includes('N8N_COMMUNITY_PACKAGES_ALLOW_TOOL_USAGE'))).toBe(true);
|
||||
});
|
||||
@@ -1306,7 +1306,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
|
||||
connections: {}
|
||||
} as any;
|
||||
|
||||
const result = await validator.validateWorkflow(workflow);
|
||||
const result = await validator.validateWorkflow(workflow as any);
|
||||
|
||||
expect(result.errors.some(e => e.message.includes('Node-level properties onError, retryOnFail, credentials are in the wrong location'))).toBe(true);
|
||||
expect(result.errors.some(e => e.details?.fix?.includes('Move these properties from node.parameters to the node level'))).toBe(true);
|
||||
@@ -1327,7 +1327,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
|
||||
connections: {}
|
||||
} as any;
|
||||
|
||||
const result = await validator.validateWorkflow(workflow);
|
||||
const result = await validator.validateWorkflow(workflow as any);
|
||||
|
||||
expect(result.errors.some(e => e.message.includes('Invalid onError value: "invalidValue"'))).toBe(true);
|
||||
});
|
||||
@@ -1347,7 +1347,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
|
||||
connections: {}
|
||||
} as any;
|
||||
|
||||
const result = await validator.validateWorkflow(workflow);
|
||||
const result = await validator.validateWorkflow(workflow as any);
|
||||
|
||||
expect(result.warnings.some(w => w.message.includes('Using deprecated "continueOnFail: true"'))).toBe(true);
|
||||
});
|
||||
@@ -1368,7 +1368,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
|
||||
connections: {}
|
||||
} as any;
|
||||
|
||||
const result = await validator.validateWorkflow(workflow);
|
||||
const result = await validator.validateWorkflow(workflow as any);
|
||||
|
||||
expect(result.errors.some(e => e.message.includes('Cannot use both "continueOnFail" and "onError" properties'))).toBe(true);
|
||||
});
|
||||
@@ -1390,7 +1390,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
|
||||
connections: {}
|
||||
} as any;
|
||||
|
||||
const result = await validator.validateWorkflow(workflow);
|
||||
const result = await validator.validateWorkflow(workflow as any);
|
||||
|
||||
expect(result.errors.some(e => e.message.includes('maxTries must be a positive number'))).toBe(true);
|
||||
expect(result.errors.some(e => e.message.includes('waitBetweenTries must be a non-negative number'))).toBe(true);
|
||||
@@ -1413,7 +1413,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
|
||||
connections: {}
|
||||
} as any;
|
||||
|
||||
const result = await validator.validateWorkflow(workflow);
|
||||
const result = await validator.validateWorkflow(workflow as any);
|
||||
|
||||
expect(result.warnings.some(w => w.message.includes('maxTries is set to 15'))).toBe(true);
|
||||
expect(result.warnings.some(w => w.message.includes('waitBetweenTries is set to 400000ms'))).toBe(true);
|
||||
@@ -1434,7 +1434,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
|
||||
connections: {}
|
||||
} as any;
|
||||
|
||||
const result = await validator.validateWorkflow(workflow);
|
||||
const result = await validator.validateWorkflow(workflow as any);
|
||||
|
||||
expect(result.warnings.some(w => w.message.includes('retryOnFail is enabled but maxTries is not specified'))).toBe(true);
|
||||
});
|
||||
@@ -1459,7 +1459,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
|
||||
connections: {}
|
||||
} as any;
|
||||
|
||||
const result = await validator.validateWorkflow(workflow);
|
||||
const result = await validator.validateWorkflow(workflow as any);
|
||||
|
||||
|
||||
expect(result.errors.some(e => e.message.includes('alwaysOutputData must be a boolean'))).toBe(true);
|
||||
@@ -1484,7 +1484,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
|
||||
connections: {}
|
||||
} as any;
|
||||
|
||||
const result = await validator.validateWorkflow(workflow);
|
||||
const result = await validator.validateWorkflow(workflow as any);
|
||||
|
||||
expect(result.warnings.some(w => w.message.includes('executeOnce is enabled'))).toBe(true);
|
||||
});
|
||||
@@ -1512,7 +1512,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
|
||||
connections: {}
|
||||
} as any;
|
||||
|
||||
const result = await validator.validateWorkflow(workflow);
|
||||
const result = await validator.validateWorkflow(workflow as any);
|
||||
|
||||
expect(result.warnings.some(w => w.message.includes(nodeInfo.message) && w.message.includes('without error handling'))).toBe(true);
|
||||
}
|
||||
@@ -1534,7 +1534,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
|
||||
connections: {}
|
||||
} as any;
|
||||
|
||||
const result = await validator.validateWorkflow(workflow);
|
||||
const result = await validator.validateWorkflow(workflow as any);
|
||||
|
||||
expect(result.warnings.some(w => w.message.includes('Both continueOnFail and retryOnFail are enabled'))).toBe(true);
|
||||
});
|
||||
@@ -1554,7 +1554,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
|
||||
connections: {}
|
||||
} as any;
|
||||
|
||||
const result = await validator.validateWorkflow(workflow);
|
||||
const result = await validator.validateWorkflow(workflow as any);
|
||||
|
||||
expect(result.suggestions.some(s => s.includes('Consider enabling alwaysOutputData'))).toBe(true);
|
||||
});
|
||||
@@ -1569,7 +1569,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
|
||||
|
||||
const workflow = builder.build() as any;
|
||||
|
||||
const result = await validator.validateWorkflow(workflow);
|
||||
const result = await validator.validateWorkflow(workflow as any);
|
||||
|
||||
expect(result.suggestions.some(s => s.includes('Most nodes lack error handling'))).toBe(true);
|
||||
});
|
||||
@@ -1589,7 +1589,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
|
||||
connections: {}
|
||||
} as any;
|
||||
|
||||
const result = await validator.validateWorkflow(workflow);
|
||||
const result = await validator.validateWorkflow(workflow as any);
|
||||
|
||||
expect(result.suggestions.some(s => s.includes('Replace "continueOnFail: true" with "onError:'))).toBe(true);
|
||||
});
|
||||
@@ -1610,7 +1610,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
|
||||
connections: {}
|
||||
} as any;
|
||||
|
||||
const result = await validator.validateWorkflow(workflow);
|
||||
const result = await validator.validateWorkflow(workflow as any);
|
||||
|
||||
expect(result.suggestions.some(s => s.includes('Add a trigger node'))).toBe(true);
|
||||
});
|
||||
@@ -1636,7 +1636,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
|
||||
connections: {} // Missing connections
|
||||
} as any;
|
||||
|
||||
const result = await validator.validateWorkflow(workflow);
|
||||
const result = await validator.validateWorkflow(workflow as any);
|
||||
|
||||
expect(result.suggestions.some(s => s.includes('Example connection structure'))).toBe(true);
|
||||
expect(result.suggestions.some(s => s.includes('Use node NAMES (not IDs) in connections'))).toBe(true);
|
||||
@@ -1667,7 +1667,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
|
||||
}
|
||||
} as any;
|
||||
|
||||
const result = await validator.validateWorkflow(workflow);
|
||||
const result = await validator.validateWorkflow(workflow as any);
|
||||
|
||||
expect(result.suggestions.some(s => s.includes('Add error handling'))).toBe(true);
|
||||
});
|
||||
@@ -1682,7 +1682,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
|
||||
|
||||
const workflow = builder.build() as any;
|
||||
|
||||
const result = await validator.validateWorkflow(workflow);
|
||||
const result = await validator.validateWorkflow(workflow as any);
|
||||
|
||||
expect(result.suggestions.some(s => s.includes('Consider breaking this workflow into smaller sub-workflows'))).toBe(true);
|
||||
});
|
||||
@@ -1708,7 +1708,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
|
||||
connections: {}
|
||||
} as any;
|
||||
|
||||
const result = await validator.validateWorkflow(workflow);
|
||||
const result = await validator.validateWorkflow(workflow as any);
|
||||
|
||||
expect(result.suggestions.some(s => s.includes('Consider using a Code node for complex data transformations'))).toBe(true);
|
||||
});
|
||||
@@ -1727,7 +1727,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
|
||||
connections: {}
|
||||
} as any;
|
||||
|
||||
const result = await validator.validateWorkflow(workflow);
|
||||
const result = await validator.validateWorkflow(workflow as any);
|
||||
|
||||
expect(result.suggestions.some(s => s.includes('A minimal workflow needs'))).toBe(true);
|
||||
});
|
||||
@@ -1756,7 +1756,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
|
||||
connections: {}
|
||||
} as any;
|
||||
|
||||
const result = await validator.validateWorkflow(workflow);
|
||||
const result = await validator.validateWorkflow(workflow as any);
|
||||
|
||||
expect(result.errors.some(e => e.message.includes(`Did you mean`) && e.message.includes(testCase.suggestion))).toBe(true);
|
||||
}
|
||||
@@ -1848,7 +1848,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
|
||||
}
|
||||
} as any;
|
||||
|
||||
const result = await validator.validateWorkflow(workflow);
|
||||
const result = await validator.validateWorkflow(workflow as any);
|
||||
|
||||
// Should have multiple errors
|
||||
expect(result.valid).toBe(false);
|
||||
@@ -1940,7 +1940,7 @@ describe('WorkflowValidator - Comprehensive Tests', () => {
|
||||
}
|
||||
} as any;
|
||||
|
||||
const result = await validator.validateWorkflow(workflow);
|
||||
const result = await validator.validateWorkflow(workflow as any);
|
||||
|
||||
expect(result.valid).toBe(true);
|
||||
expect(result.errors).toHaveLength(0);
|
||||
|
||||
@@ -157,7 +157,7 @@ describe('WorkflowValidator - Edge Cases', () => {
|
||||
nodes: [],
|
||||
connections: {}
|
||||
};
|
||||
const result = await validator.validateWorkflow(workflow);
|
||||
const result = await validator.validateWorkflow(workflow as any);
|
||||
expect(result.valid).toBe(true);
|
||||
expect(result.warnings.some(w => w.message.includes('empty'))).toBe(true);
|
||||
});
|
||||
@@ -181,7 +181,7 @@ describe('WorkflowValidator - Edge Cases', () => {
|
||||
const workflow = { nodes, connections };
|
||||
|
||||
const start = Date.now();
|
||||
const result = await validator.validateWorkflow(workflow);
|
||||
const result = await validator.validateWorkflow(workflow as any);
|
||||
const duration = Date.now() - start;
|
||||
|
||||
expect(result).toBeDefined();
|
||||
@@ -207,7 +207,7 @@ describe('WorkflowValidator - Edge Cases', () => {
|
||||
}
|
||||
};
|
||||
|
||||
const result = await validator.validateWorkflow(workflow);
|
||||
const result = await validator.validateWorkflow(workflow as any);
|
||||
expect(result.statistics.invalidConnections).toBe(0);
|
||||
});
|
||||
|
||||
@@ -228,7 +228,7 @@ describe('WorkflowValidator - Edge Cases', () => {
|
||||
}
|
||||
};
|
||||
|
||||
const result = await validator.validateWorkflow(workflow);
|
||||
const result = await validator.validateWorkflow(workflow as any);
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -264,7 +264,7 @@ describe('WorkflowValidator - Edge Cases', () => {
|
||||
connections: {}
|
||||
};
|
||||
|
||||
const result = await validator.validateWorkflow(workflow);
|
||||
const result = await validator.validateWorkflow(workflow as any);
|
||||
expect(result.errors.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
@@ -292,7 +292,7 @@ describe('WorkflowValidator - Edge Cases', () => {
|
||||
}
|
||||
};
|
||||
|
||||
const result = await validator.validateWorkflow(workflow);
|
||||
const result = await validator.validateWorkflow(workflow as any);
|
||||
expect(result.warnings.some(w => w.message.includes('self-referencing'))).toBe(true);
|
||||
});
|
||||
|
||||
@@ -308,7 +308,7 @@ describe('WorkflowValidator - Edge Cases', () => {
|
||||
}
|
||||
};
|
||||
|
||||
const result = await validator.validateWorkflow(workflow);
|
||||
const result = await validator.validateWorkflow(workflow as any);
|
||||
expect(result.errors.some(e => e.message.includes('non-existent'))).toBe(true);
|
||||
});
|
||||
|
||||
@@ -324,7 +324,7 @@ describe('WorkflowValidator - Edge Cases', () => {
|
||||
}
|
||||
};
|
||||
|
||||
const result = await validator.validateWorkflow(workflow);
|
||||
const result = await validator.validateWorkflow(workflow as any);
|
||||
expect(result.errors.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
@@ -341,7 +341,7 @@ describe('WorkflowValidator - Edge Cases', () => {
|
||||
} as any
|
||||
};
|
||||
|
||||
const result = await validator.validateWorkflow(workflow);
|
||||
const result = await validator.validateWorkflow(workflow as any);
|
||||
// Should still work as type and index can have defaults
|
||||
expect(result.statistics.validConnections).toBeGreaterThan(0);
|
||||
});
|
||||
@@ -359,7 +359,7 @@ describe('WorkflowValidator - Edge Cases', () => {
|
||||
}
|
||||
};
|
||||
|
||||
const result = await validator.validateWorkflow(workflow);
|
||||
const result = await validator.validateWorkflow(workflow as any);
|
||||
expect(result.errors.some(e => e.message.includes('Invalid'))).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -382,7 +382,7 @@ describe('WorkflowValidator - Edge Cases', () => {
|
||||
}
|
||||
};
|
||||
|
||||
const result = await validator.validateWorkflow(workflow);
|
||||
const result = await validator.validateWorkflow(workflow as any);
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
@@ -395,7 +395,7 @@ describe('WorkflowValidator - Edge Cases', () => {
|
||||
connections: {}
|
||||
};
|
||||
|
||||
const result = await validator.validateWorkflow(workflow);
|
||||
const result = await validator.validateWorkflow(workflow as any);
|
||||
expect(result.warnings.some(w => w.message.includes('very long'))).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -479,7 +479,7 @@ describe('WorkflowValidator - Edge Cases', () => {
|
||||
}
|
||||
};
|
||||
|
||||
const result = await validator.validateWorkflow(workflow);
|
||||
const result = await validator.validateWorkflow(workflow as any);
|
||||
expect(result.statistics.validConnections).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
@@ -499,7 +499,7 @@ describe('WorkflowValidator - Edge Cases', () => {
|
||||
}
|
||||
};
|
||||
|
||||
const result = await validator.validateWorkflow(workflow);
|
||||
const result = await validator.validateWorkflow(workflow as any);
|
||||
expect(result.errors.length).toBeGreaterThan(0);
|
||||
expect(result.statistics.validConnections).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
@@ -77,7 +77,7 @@ describe('WorkflowValidator - Simple Unit Tests', () => {
|
||||
};
|
||||
|
||||
// Act
|
||||
const result = await validator.validateWorkflow(workflow);
|
||||
const result = await validator.validateWorkflow(workflow as any);
|
||||
|
||||
// Assert
|
||||
expect(result.valid).toBe(true);
|
||||
@@ -113,7 +113,7 @@ describe('WorkflowValidator - Simple Unit Tests', () => {
|
||||
};
|
||||
|
||||
// Act
|
||||
const result = await validator.validateWorkflow(workflow);
|
||||
const result = await validator.validateWorkflow(workflow as any);
|
||||
|
||||
// Assert
|
||||
expect(result.valid).toBe(false);
|
||||
@@ -154,7 +154,7 @@ describe('WorkflowValidator - Simple Unit Tests', () => {
|
||||
};
|
||||
|
||||
// Act
|
||||
const result = await validator.validateWorkflow(workflow);
|
||||
const result = await validator.validateWorkflow(workflow as any);
|
||||
|
||||
// Assert
|
||||
expect(result.valid).toBe(false);
|
||||
@@ -229,7 +229,7 @@ describe('WorkflowValidator - Simple Unit Tests', () => {
|
||||
};
|
||||
|
||||
// Act
|
||||
const result = await validator.validateWorkflow(workflow);
|
||||
const result = await validator.validateWorkflow(workflow as any);
|
||||
|
||||
// Assert
|
||||
expect(result.valid).toBe(true);
|
||||
@@ -297,7 +297,7 @@ describe('WorkflowValidator - Simple Unit Tests', () => {
|
||||
};
|
||||
|
||||
// Act
|
||||
const result = await validator.validateWorkflow(workflow);
|
||||
const result = await validator.validateWorkflow(workflow as any);
|
||||
|
||||
// Assert
|
||||
expect(result.valid).toBe(false);
|
||||
@@ -386,7 +386,7 @@ describe('WorkflowValidator - Simple Unit Tests', () => {
|
||||
};
|
||||
|
||||
// Act
|
||||
const result = await validator.validateWorkflow(workflow);
|
||||
const result = await validator.validateWorkflow(workflow as any);
|
||||
|
||||
// Assert
|
||||
expect(result.valid).toBe(false);
|
||||
@@ -438,7 +438,7 @@ describe('WorkflowValidator - Simple Unit Tests', () => {
|
||||
};
|
||||
|
||||
// Act
|
||||
const result = await validator.validateWorkflow(workflow);
|
||||
const result = await validator.validateWorkflow(workflow as any);
|
||||
|
||||
// Assert
|
||||
expect(result.warnings.some(w => w.message.includes('Outdated typeVersion'))).toBe(true);
|
||||
@@ -471,7 +471,7 @@ describe('WorkflowValidator - Simple Unit Tests', () => {
|
||||
};
|
||||
|
||||
// Act
|
||||
const result = await validator.validateWorkflow(workflow);
|
||||
const result = await validator.validateWorkflow(workflow as any);
|
||||
|
||||
// Assert
|
||||
expect(result.valid).toBe(false);
|
||||
|
||||
410
tests/unit/validation-fixes.test.ts
Normal file
410
tests/unit/validation-fixes.test.ts
Normal file
@@ -0,0 +1,410 @@
|
||||
/**
|
||||
* Test suite for validation system fixes
|
||||
* Covers issues #58, #68, #70, #73
|
||||
*/
|
||||
|
||||
import { describe, test, expect, beforeAll, afterAll } from 'vitest';
|
||||
import { WorkflowValidator } from '../../src/services/workflow-validator';
|
||||
import { EnhancedConfigValidator } from '../../src/services/enhanced-config-validator';
|
||||
import { ToolValidation, Validator, ValidationError } from '../../src/utils/validation-schemas';
|
||||
|
||||
describe('Validation System Fixes', () => {
|
||||
let workflowValidator: WorkflowValidator;
|
||||
let mockNodeRepository: any;
|
||||
|
||||
beforeAll(async () => {
|
||||
// Initialize test environment
|
||||
process.env.NODE_ENV = 'test';
|
||||
|
||||
// Mock repository for testing
|
||||
mockNodeRepository = {
|
||||
getNode: (nodeType: string) => {
|
||||
if (nodeType === 'nodes-base.webhook' || nodeType === 'n8n-nodes-base.webhook') {
|
||||
return {
|
||||
nodeType: 'nodes-base.webhook',
|
||||
displayName: 'Webhook',
|
||||
properties: [
|
||||
{ name: 'path', required: true, displayName: 'Path' },
|
||||
{ name: 'httpMethod', required: true, displayName: 'HTTP Method' }
|
||||
]
|
||||
};
|
||||
}
|
||||
if (nodeType === 'nodes-base.set' || nodeType === 'n8n-nodes-base.set') {
|
||||
return {
|
||||
nodeType: 'nodes-base.set',
|
||||
displayName: 'Set',
|
||||
properties: [
|
||||
{ name: 'values', required: false, displayName: 'Values' }
|
||||
]
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
} as any;
|
||||
|
||||
workflowValidator = new WorkflowValidator(mockNodeRepository, EnhancedConfigValidator);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
delete process.env.NODE_ENV;
|
||||
});
|
||||
|
||||
describe('Issue #73: validate_node_minimal crashes without input validation', () => {
|
||||
test('should handle empty config in validation schemas', () => {
|
||||
// Test the validation schema handles empty config
|
||||
const result = ToolValidation.validateNodeMinimal({
|
||||
nodeType: 'nodes-base.webhook',
|
||||
config: undefined
|
||||
});
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors.length).toBeGreaterThan(0);
|
||||
expect(result.errors[0].field).toBe('config');
|
||||
});
|
||||
|
||||
test('should handle null config in validation schemas', () => {
|
||||
const result = ToolValidation.validateNodeMinimal({
|
||||
nodeType: 'nodes-base.webhook',
|
||||
config: null
|
||||
});
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors.length).toBeGreaterThan(0);
|
||||
expect(result.errors[0].field).toBe('config');
|
||||
});
|
||||
|
||||
test('should accept valid config object', () => {
|
||||
const result = ToolValidation.validateNodeMinimal({
|
||||
nodeType: 'nodes-base.webhook',
|
||||
config: { path: '/webhook', httpMethod: 'POST' }
|
||||
});
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.valid).toBe(true);
|
||||
expect(result.errors).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Issue #58: validate_node_operation crashes on nested input', () => {
|
||||
test('should handle invalid nodeType gracefully', () => {
|
||||
expect(() => {
|
||||
EnhancedConfigValidator.validateWithMode(
|
||||
undefined as any,
|
||||
{ resource: 'channel', operation: 'create' },
|
||||
[],
|
||||
'operation',
|
||||
'ai-friendly'
|
||||
);
|
||||
}).toThrow(Error);
|
||||
});
|
||||
|
||||
test('should handle null nodeType gracefully', () => {
|
||||
expect(() => {
|
||||
EnhancedConfigValidator.validateWithMode(
|
||||
null as any,
|
||||
{ resource: 'channel', operation: 'create' },
|
||||
[],
|
||||
'operation',
|
||||
'ai-friendly'
|
||||
);
|
||||
}).toThrow(Error);
|
||||
});
|
||||
|
||||
test('should handle non-string nodeType gracefully', () => {
|
||||
expect(() => {
|
||||
EnhancedConfigValidator.validateWithMode(
|
||||
{ type: 'nodes-base.slack' } as any,
|
||||
{ resource: 'channel', operation: 'create' },
|
||||
[],
|
||||
'operation',
|
||||
'ai-friendly'
|
||||
);
|
||||
}).toThrow(Error);
|
||||
});
|
||||
|
||||
test('should handle valid nodeType properly', () => {
|
||||
const result = EnhancedConfigValidator.validateWithMode(
|
||||
'nodes-base.set',
|
||||
{ values: {} },
|
||||
[],
|
||||
'operation',
|
||||
'ai-friendly'
|
||||
);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(typeof result.valid).toBe('boolean');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Issue #70: Profile settings not respected', () => {
|
||||
test('should pass profile parameter to all validation phases', async () => {
|
||||
const workflow = {
|
||||
nodes: [
|
||||
{
|
||||
id: '1',
|
||||
name: 'Webhook',
|
||||
type: 'n8n-nodes-base.webhook',
|
||||
position: [100, 200],
|
||||
parameters: { path: '/test', httpMethod: 'POST' },
|
||||
typeVersion: 1
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'Set',
|
||||
type: 'n8n-nodes-base.set',
|
||||
position: [300, 200],
|
||||
parameters: { values: {} },
|
||||
typeVersion: 1
|
||||
}
|
||||
],
|
||||
connections: {
|
||||
'Webhook': {
|
||||
main: [[{ node: 'Set', type: 'main', index: 0 }]]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const result = await workflowValidator.validateWorkflow(workflow, {
|
||||
validateNodes: true,
|
||||
validateConnections: true,
|
||||
validateExpressions: true,
|
||||
profile: 'minimal'
|
||||
});
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.valid).toBe(true);
|
||||
// In minimal profile, should have fewer warnings/errors - just check it's reasonable
|
||||
expect(result.warnings.length).toBeLessThanOrEqual(5);
|
||||
});
|
||||
|
||||
test('should filter out sticky notes from validation', async () => {
|
||||
const workflow = {
|
||||
nodes: [
|
||||
{
|
||||
id: '1',
|
||||
name: 'Webhook',
|
||||
type: 'n8n-nodes-base.webhook',
|
||||
position: [100, 200],
|
||||
parameters: { path: '/test', httpMethod: 'POST' },
|
||||
typeVersion: 1
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'Sticky Note',
|
||||
type: 'n8n-nodes-base.stickyNote',
|
||||
position: [300, 100],
|
||||
parameters: { content: 'This is a note' },
|
||||
typeVersion: 1
|
||||
}
|
||||
],
|
||||
connections: {}
|
||||
};
|
||||
|
||||
const result = await workflowValidator.validateWorkflow(workflow);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.statistics.totalNodes).toBe(1); // Only webhook, sticky note excluded
|
||||
expect(result.statistics.enabledNodes).toBe(1);
|
||||
});
|
||||
|
||||
test('should allow legitimate loops in cycle detection', async () => {
|
||||
const workflow = {
|
||||
nodes: [
|
||||
{
|
||||
id: '1',
|
||||
name: 'Manual Trigger',
|
||||
type: 'n8n-nodes-base.manualTrigger',
|
||||
position: [100, 200],
|
||||
parameters: {},
|
||||
typeVersion: 1
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'SplitInBatches',
|
||||
type: 'n8n-nodes-base.splitInBatches',
|
||||
position: [300, 200],
|
||||
parameters: { batchSize: 1 },
|
||||
typeVersion: 1
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: 'Set',
|
||||
type: 'n8n-nodes-base.set',
|
||||
position: [500, 200],
|
||||
parameters: { values: {} },
|
||||
typeVersion: 1
|
||||
}
|
||||
],
|
||||
connections: {
|
||||
'Manual Trigger': {
|
||||
main: [[{ node: 'SplitInBatches', type: 'main', index: 0 }]]
|
||||
},
|
||||
'SplitInBatches': {
|
||||
main: [
|
||||
[{ node: 'Set', type: 'main', index: 0 }], // Done output
|
||||
[{ node: 'Set', type: 'main', index: 0 }] // Loop output
|
||||
]
|
||||
},
|
||||
'Set': {
|
||||
main: [[{ node: 'SplitInBatches', type: 'main', index: 0 }]] // Loop back
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const result = await workflowValidator.validateWorkflow(workflow);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
// Should not report cycle error for legitimate SplitInBatches loop
|
||||
const cycleErrors = result.errors.filter(e => e.message.includes('cycle'));
|
||||
expect(cycleErrors).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Issue #68: Better error recovery suggestions', () => {
|
||||
test('should provide recovery suggestions for invalid node types', async () => {
|
||||
const workflow = {
|
||||
nodes: [
|
||||
{
|
||||
id: '1',
|
||||
name: 'Invalid Node',
|
||||
type: 'invalid-node-type',
|
||||
position: [100, 200],
|
||||
parameters: {},
|
||||
typeVersion: 1
|
||||
}
|
||||
],
|
||||
connections: {}
|
||||
};
|
||||
|
||||
const result = await workflowValidator.validateWorkflow(workflow);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.suggestions.length).toBeGreaterThan(0);
|
||||
|
||||
// Should contain recovery suggestions
|
||||
const recoveryStarted = result.suggestions.some(s => s.includes('🔧 RECOVERY'));
|
||||
expect(recoveryStarted).toBe(true);
|
||||
});
|
||||
|
||||
test('should provide recovery suggestions for connection errors', async () => {
|
||||
const workflow = {
|
||||
nodes: [
|
||||
{
|
||||
id: '1',
|
||||
name: 'Webhook',
|
||||
type: 'n8n-nodes-base.webhook',
|
||||
position: [100, 200],
|
||||
parameters: { path: '/test', httpMethod: 'POST' },
|
||||
typeVersion: 1
|
||||
}
|
||||
],
|
||||
connections: {
|
||||
'Webhook': {
|
||||
main: [[{ node: 'NonExistentNode', type: 'main', index: 0 }]]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const result = await workflowValidator.validateWorkflow(workflow);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.suggestions.length).toBeGreaterThan(0);
|
||||
|
||||
// Should contain connection recovery suggestions
|
||||
const connectionRecovery = result.suggestions.some(s =>
|
||||
s.includes('Connection errors detected') || s.includes('connection')
|
||||
);
|
||||
expect(connectionRecovery).toBe(true);
|
||||
});
|
||||
|
||||
test('should provide workflow for multiple errors', async () => {
|
||||
const workflow = {
|
||||
nodes: [
|
||||
{
|
||||
id: '1',
|
||||
name: 'Invalid Node 1',
|
||||
type: 'invalid-type-1',
|
||||
position: [100, 200],
|
||||
parameters: {}
|
||||
// Missing typeVersion
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'Invalid Node 2',
|
||||
type: 'invalid-type-2',
|
||||
position: [300, 200],
|
||||
parameters: {}
|
||||
// Missing typeVersion
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: 'Invalid Node 3',
|
||||
type: 'invalid-type-3',
|
||||
position: [500, 200],
|
||||
parameters: {}
|
||||
// Missing typeVersion
|
||||
}
|
||||
],
|
||||
connections: {
|
||||
'Invalid Node 1': {
|
||||
main: [[{ node: 'NonExistent', type: 'main', index: 0 }]]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const result = await workflowValidator.validateWorkflow(workflow);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors.length).toBeGreaterThan(3);
|
||||
|
||||
// Should provide step-by-step recovery workflow
|
||||
const workflowSuggestion = result.suggestions.some(s =>
|
||||
s.includes('SUGGESTED WORKFLOW') && s.includes('Too many errors detected')
|
||||
);
|
||||
expect(workflowSuggestion).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Enhanced Input Validation', () => {
|
||||
test('should validate tool parameters with schemas', () => {
|
||||
// Test validate_node_operation parameters
|
||||
const validationResult = ToolValidation.validateNodeOperation({
|
||||
nodeType: 'nodes-base.webhook',
|
||||
config: { path: '/test' },
|
||||
profile: 'ai-friendly'
|
||||
});
|
||||
|
||||
expect(validationResult.valid).toBe(true);
|
||||
expect(validationResult.errors).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('should reject invalid parameters', () => {
|
||||
const validationResult = ToolValidation.validateNodeOperation({
|
||||
nodeType: 123, // Invalid type
|
||||
config: 'not an object', // Invalid type
|
||||
profile: 'invalid-profile' // Invalid enum value
|
||||
});
|
||||
|
||||
expect(validationResult.valid).toBe(false);
|
||||
expect(validationResult.errors.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('should format validation errors properly', () => {
|
||||
const validationResult = ToolValidation.validateNodeOperation({
|
||||
nodeType: null,
|
||||
config: null
|
||||
});
|
||||
|
||||
const errorMessage = Validator.formatErrors(validationResult, 'validate_node_operation');
|
||||
|
||||
expect(errorMessage).toContain('validate_node_operation: Validation failed:');
|
||||
expect(errorMessage).toContain('nodeType');
|
||||
expect(errorMessage).toContain('config');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user