409e92d6ae
Build and Push Docker Images / Build Docker Image (push) Has been cancelled
Build and Push Docker Images / Build Railway Docker Image (push) Has been cancelled
Build and Publish n8n Docker Image / test-image (push) Has been cancelled
Dependency Compatibility Check / Fresh Install Dependency Check (push) Has been cancelled
Build and Publish n8n Docker Image / build-and-push (push) Has been cancelled
Build and Publish n8n Docker Image / create-release (push) Has been cancelled
Automated Release / Detect Version Change (push) Has been cancelled
Automated Release / Generate Release Notes (push) Has been cancelled
Automated Release / Create GitHub Release (push) Has been cancelled
Automated Release / Package MCPB Bundle (push) Has been cancelled
Automated Release / Build and Verify (push) Has been cancelled
Automated Release / Publish to NPM (push) Has been cancelled
Automated Release / Build and Push Docker Images (push) Has been cancelled
Automated Release / Update Documentation (push) Has been cancelled
Automated Release / Notify Release Completion (push) Has been cancelled
Secret Scan / secretlint (push) Has been cancelled
Test Suite / test (push) Has been cancelled
Test Suite / cjs-runtime (push) Has been cancelled
Test Suite / publish-results (push) Has been cancelled
471 lines
14 KiB
TypeScript
471 lines
14 KiB
TypeScript
#!/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);
|
|
});
|