Skip to main content
Glama
test-structure-validation.ts14.1 kB
#!/usr/bin/env ts-node /** * Phase 3: Real-World Type Structure Validation * * Tests type structure validation against real workflow templates from n8n.io * to ensure production readiness. Validates filter, resourceMapper, * assignmentCollection, and resourceLocator types. * * Usage: * npm run build && node dist/scripts/test-structure-validation.js * * or with ts-node: * npx ts-node scripts/test-structure-validation.ts */ import { createDatabaseAdapter } from '../src/database/database-adapter'; import { EnhancedConfigValidator } from '../src/services/enhanced-config-validator'; import type { NodePropertyTypes } from 'n8n-workflow'; import { gunzipSync } from 'zlib'; interface ValidationResult { templateId: number; templateName: string; templateViews: number; nodeId: string; nodeName: string; nodeType: string; propertyName: string; propertyType: NodePropertyTypes; valid: boolean; errors: Array<{ type: string; property?: string; message: string }>; warnings: Array<{ type: string; property?: string; message: string }>; validationTimeMs: number; } interface ValidationStats { totalTemplates: number; totalNodes: number; totalValidations: number; passedValidations: number; failedValidations: number; byType: Record<string, { passed: number; failed: number }>; byError: Record<string, number>; avgValidationTimeMs: number; maxValidationTimeMs: number; } // Special types we want to validate const SPECIAL_TYPES: NodePropertyTypes[] = [ 'filter', 'resourceMapper', 'assignmentCollection', 'resourceLocator', ]; function decompressWorkflow(compressed: string): any { try { const buffer = Buffer.from(compressed, 'base64'); const decompressed = gunzipSync(buffer); return JSON.parse(decompressed.toString('utf-8')); } catch (error: any) { throw new Error(`Failed to decompress workflow: ${error.message}`); } } async function loadTopTemplates(db: any, limit: number = 100) { console.log(`📥 Loading top ${limit} templates by popularity...\n`); const stmt = db.prepare(` SELECT id, name, workflow_json_compressed, views FROM templates WHERE workflow_json_compressed IS NOT NULL ORDER BY views DESC LIMIT ? `); const templates = stmt.all(limit); console.log(`✓ Loaded ${templates.length} templates\n`); return templates; } function extractNodesWithSpecialTypes(workflowJson: any): Array<{ nodeId: string; nodeName: string; nodeType: string; properties: Array<{ name: string; type: NodePropertyTypes; value: any }>; }> { const results: Array<any> = []; if (!workflowJson || !workflowJson.nodes || !Array.isArray(workflowJson.nodes)) { return results; } for (const node of workflowJson.nodes) { // Check if node has parameters with special types if (!node.parameters || typeof node.parameters !== 'object') { continue; } const specialProperties: Array<{ name: string; type: NodePropertyTypes; value: any }> = []; // Check each parameter against our special types for (const [paramName, paramValue] of Object.entries(node.parameters)) { // Try to infer type from structure const inferredType = inferPropertyType(paramValue); if (inferredType && SPECIAL_TYPES.includes(inferredType)) { specialProperties.push({ name: paramName, type: inferredType, value: paramValue, }); } } if (specialProperties.length > 0) { results.push({ nodeId: node.id, nodeName: node.name, nodeType: node.type, properties: specialProperties, }); } } return results; } function inferPropertyType(value: any): NodePropertyTypes | null { if (!value || typeof value !== 'object') { return null; } // Filter type: has combinator and conditions if (value.combinator && value.conditions) { return 'filter'; } // ResourceMapper type: has mappingMode if (value.mappingMode) { return 'resourceMapper'; } // AssignmentCollection type: has assignments array if (value.assignments && Array.isArray(value.assignments)) { return 'assignmentCollection'; } // ResourceLocator type: has mode and value if (value.mode && value.hasOwnProperty('value')) { return 'resourceLocator'; } return null; } async function validateTemplate( templateId: number, templateName: string, templateViews: number, workflowJson: any ): Promise<ValidationResult[]> { const results: ValidationResult[] = []; // Extract nodes with special types const nodesWithSpecialTypes = extractNodesWithSpecialTypes(workflowJson); for (const node of nodesWithSpecialTypes) { for (const prop of node.properties) { const startTime = Date.now(); // Create property definition for validation const properties = [ { name: prop.name, type: prop.type, required: true, displayName: prop.name, default: {}, }, ]; // Create config with just this property const config = { [prop.name]: prop.value, }; try { // Run validation const validationResult = EnhancedConfigValidator.validateWithMode( node.nodeType, config, properties, 'operation', 'ai-friendly' ); const validationTimeMs = Date.now() - startTime; results.push({ templateId, templateName, templateViews, nodeId: node.nodeId, nodeName: node.nodeName, nodeType: node.nodeType, propertyName: prop.name, propertyType: prop.type, valid: validationResult.valid, errors: validationResult.errors || [], warnings: validationResult.warnings || [], validationTimeMs, }); } catch (error: any) { const validationTimeMs = Date.now() - startTime; results.push({ templateId, templateName, templateViews, nodeId: node.nodeId, nodeName: node.nodeName, nodeType: node.nodeType, propertyName: prop.name, propertyType: prop.type, valid: false, errors: [ { type: 'exception', property: prop.name, message: `Validation threw exception: ${error.message}`, }, ], warnings: [], validationTimeMs, }); } } } return results; } function calculateStats(results: ValidationResult[]): ValidationStats { const stats: ValidationStats = { totalTemplates: new Set(results.map(r => r.templateId)).size, totalNodes: new Set(results.map(r => `${r.templateId}-${r.nodeId}`)).size, totalValidations: results.length, passedValidations: results.filter(r => r.valid).length, failedValidations: results.filter(r => !r.valid).length, byType: {}, byError: {}, avgValidationTimeMs: 0, maxValidationTimeMs: 0, }; // Stats by type for (const type of SPECIAL_TYPES) { const typeResults = results.filter(r => r.propertyType === type); stats.byType[type] = { passed: typeResults.filter(r => r.valid).length, failed: typeResults.filter(r => !r.valid).length, }; } // Error frequency for (const result of results.filter(r => !r.valid)) { for (const error of result.errors) { const key = `${error.type}: ${error.message}`; stats.byError[key] = (stats.byError[key] || 0) + 1; } } // Performance stats if (results.length > 0) { stats.avgValidationTimeMs = results.reduce((sum, r) => sum + r.validationTimeMs, 0) / results.length; stats.maxValidationTimeMs = Math.max(...results.map(r => r.validationTimeMs)); } return stats; } function printStats(stats: ValidationStats) { console.log('\n' + '='.repeat(80)); console.log('VALIDATION STATISTICS'); console.log('='.repeat(80) + '\n'); console.log(`📊 Total Templates Tested: ${stats.totalTemplates}`); console.log(`📊 Total Nodes with Special Types: ${stats.totalNodes}`); console.log(`📊 Total Property Validations: ${stats.totalValidations}\n`); const passRate = (stats.passedValidations / stats.totalValidations * 100).toFixed(2); const failRate = (stats.failedValidations / stats.totalValidations * 100).toFixed(2); console.log(`✅ Passed: ${stats.passedValidations} (${passRate}%)`); console.log(`❌ Failed: ${stats.failedValidations} (${failRate}%)\n`); console.log('By Property Type:'); console.log('-'.repeat(80)); for (const [type, counts] of Object.entries(stats.byType)) { const total = counts.passed + counts.failed; if (total === 0) { console.log(` ${type}: No occurrences found`); } else { const typePassRate = (counts.passed / total * 100).toFixed(2); console.log(` ${type}: ${counts.passed}/${total} passed (${typePassRate}%)`); } } console.log('\n⚡ Performance:'); console.log('-'.repeat(80)); console.log(` Average validation time: ${stats.avgValidationTimeMs.toFixed(2)}ms`); console.log(` Maximum validation time: ${stats.maxValidationTimeMs.toFixed(2)}ms`); const meetsTarget = stats.avgValidationTimeMs < 50; console.log(` Target (<50ms): ${meetsTarget ? '✅ MET' : '❌ NOT MET'}\n`); if (Object.keys(stats.byError).length > 0) { console.log('🔍 Most Common Errors:'); console.log('-'.repeat(80)); const sortedErrors = Object.entries(stats.byError) .sort((a, b) => b[1] - a[1]) .slice(0, 10); for (const [error, count] of sortedErrors) { console.log(` ${count}x: ${error}`); } } } function printFailures(results: ValidationResult[], maxFailures: number = 20) { const failures = results.filter(r => !r.valid); if (failures.length === 0) { console.log('\n✨ No failures! All validations passed.\n'); return; } console.log('\n' + '='.repeat(80)); console.log(`VALIDATION FAILURES (showing first ${Math.min(maxFailures, failures.length)})` ); console.log('='.repeat(80) + '\n'); for (let i = 0; i < Math.min(maxFailures, failures.length); i++) { const failure = failures[i]; console.log(`Failure ${i + 1}/${failures.length}:`); console.log(` Template: ${failure.templateName} (ID: ${failure.templateId}, Views: ${failure.templateViews})`); console.log(` Node: ${failure.nodeName} (${failure.nodeType})`); console.log(` Property: ${failure.propertyName} (type: ${failure.propertyType})`); console.log(` Errors:`); for (const error of failure.errors) { console.log(` - [${error.type}] ${error.property}: ${error.message}`); } if (failure.warnings.length > 0) { console.log(` Warnings:`); for (const warning of failure.warnings) { console.log(` - [${warning.type}] ${warning.property}: ${warning.message}`); } } console.log(''); } if (failures.length > maxFailures) { console.log(`... and ${failures.length - maxFailures} more failures\n`); } } async function main() { console.log('='.repeat(80)); console.log('PHASE 3: REAL-WORLD TYPE STRUCTURE VALIDATION'); console.log('='.repeat(80) + '\n'); // Initialize database console.log('🔌 Connecting to database...'); const db = await createDatabaseAdapter('./data/nodes.db'); console.log('✓ Database connected\n'); // Load templates const templates = await loadTopTemplates(db, 100); // Validate each template console.log('🔍 Validating templates...\n'); const allResults: ValidationResult[] = []; let processedCount = 0; let nodesFound = 0; for (const template of templates) { processedCount++; let workflowJson; try { workflowJson = decompressWorkflow(template.workflow_json_compressed); } catch (error) { console.warn(`⚠️ Template ${template.id}: Decompression failed, skipping`); continue; } const results = await validateTemplate( template.id, template.name, template.views, workflowJson ); if (results.length > 0) { nodesFound += new Set(results.map(r => r.nodeId)).size; allResults.push(...results); const passedCount = results.filter(r => r.valid).length; const status = passedCount === results.length ? '✓' : '✗'; console.log( `${status} Template ${processedCount}/${templates.length}: ` + `"${template.name}" (${results.length} validations, ${passedCount} passed)` ); } } console.log(`\n✓ Processed ${processedCount} templates`); console.log(`✓ Found ${nodesFound} nodes with special types\n`); // Calculate and print statistics const stats = calculateStats(allResults); printStats(stats); // Print detailed failures printFailures(allResults); // Success criteria check console.log('='.repeat(80)); console.log('SUCCESS CRITERIA CHECK'); console.log('='.repeat(80) + '\n'); const passRate = (stats.passedValidations / stats.totalValidations * 100); const falsePositiveRate = (stats.failedValidations / stats.totalValidations * 100); const avgTime = stats.avgValidationTimeMs; console.log(`Pass Rate: ${passRate.toFixed(2)}% (target: >95%) ${passRate > 95 ? '✅' : '❌'}`); console.log(`False Positive Rate: ${falsePositiveRate.toFixed(2)}% (target: <5%) ${falsePositiveRate < 5 ? '✅' : '❌'}`); console.log(`Avg Validation Time: ${avgTime.toFixed(2)}ms (target: <50ms) ${avgTime < 50 ? '✅' : '❌'}\n`); const allCriteriaMet = passRate > 95 && falsePositiveRate < 5 && avgTime < 50; if (allCriteriaMet) { console.log('🎉 ALL SUCCESS CRITERIA MET! Phase 3 validation complete.\n'); } else { console.log('⚠️ Some success criteria not met. Iteration required.\n'); } // Close database db.close(); process.exit(allCriteriaMet ? 0 : 1); } // Run the script main().catch((error) => { console.error('Fatal error:', error); process.exit(1); });

Latest Blog Posts

MCP directory API

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

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

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