flowengine-n8n-workflow-builder
Version:
Build n8n workflows from text using AI. Connect to Claude, Cursor, or any LLM to generate and validate n8n workflows with expert knowledge and intelligent auto-fixing. Built by FlowEngine. Now with real node parameter schemas from n8n packages!
137 lines (115 loc) • 3.77 kB
text/typescript
/**
* Workflow Tester - Dry-run and Simulation
*
* Test workflows without executing them, with mock data injection
* and validation of execution paths.
*/
import type { Workflow } from './generator.js';
export interface TestResult {
success: boolean;
errors: string[];
warnings: string[];
executionPath: string[];
estimatedDuration?: number;
}
/**
* Perform dry-run test of workflow
*/
export function testWorkflow(workflow: Workflow, mockData?: Record<string, any>): TestResult {
const errors: string[] = [];
const warnings: string[] = [];
const executionPath: string[] = [];
// Find trigger node
const trigger = workflow.nodes.find(n =>
n.type.includes('Trigger') || n.type.includes('trigger')
);
if (!trigger) {
errors.push('No trigger node found - workflow cannot start');
return { success: false, errors, warnings, executionPath };
}
executionPath.push(trigger.name);
// Trace execution path
let currentNode = trigger.name;
const visited = new Set<string>();
let iterations = 0;
const maxIterations = 100; // Prevent infinite loops
while (currentNode && iterations < maxIterations) {
if (visited.has(currentNode)) {
warnings.push(`Potential infinite loop detected at ${currentNode}`);
break;
}
visited.add(currentNode);
iterations++;
// Find next nodes
const connections = workflow.connections[currentNode];
if (!connections || !connections.main || connections.main.length === 0) {
break; // End of workflow
}
// Take first path (main output 0)
const nextConnections = connections.main[0];
if (!nextConnections || nextConnections.length === 0) {
break;
}
currentNode = nextConnections[0].node;
executionPath.push(currentNode);
}
// Check for orphaned nodes (not in execution path)
const orphaned = workflow.nodes.filter(n =>
!n.type.includes('Trigger') &&
!n.type.includes('trigger') &&
!executionPath.includes(n.name)
);
if (orphaned.length > 0) {
warnings.push(`${orphaned.length} node(s) not in execution path: ${orphaned.map(n => n.name).join(', ')}`);
}
// Validate required credentials
workflow.nodes.forEach(node => {
if (node.credentials && Object.keys(node.credentials).length > 0) {
Object.keys(node.credentials).forEach(credType => {
const cred = (node.credentials as any)?.[credType];
if (cred?.id?.includes('placeholder')) {
errors.push(`Node "${node.name}" requires ${credType} credentials`);
}
});
}
});
// Estimate duration (rough calculation)
const estimatedDuration = executionPath.length * 1000; // 1 second per node
return {
success: errors.length === 0,
errors,
warnings,
executionPath,
estimatedDuration,
};
}
/**
* Validate workflow can execute end-to-end
*/
export function validateExecutionPath(workflow: Workflow): {
valid: boolean;
issues: string[];
} {
const issues: string[] = [];
// Check all connections point to valid nodes
Object.entries(workflow.connections).forEach(([sourceName, connections]) => {
const sourceNode = workflow.nodes.find(n => n.name === sourceName);
if (!sourceNode) {
issues.push(`Connection source "${sourceName}" not found in nodes`);
}
Object.values(connections as any).forEach((connArray: any) => {
connArray?.forEach?.((conns: any) => {
conns?.forEach?.((conn: any) => {
const targetNode = workflow.nodes.find(n => n.name === conn?.node);
if (!targetNode && conn?.node) {
issues.push(`Connection target "${conn.node}" not found in nodes`);
}
});
});
});
});
return {
valid: issues.length === 0,
issues,
};
}