diff --git a/README.md b/README.md index 2926507..a84f67b 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,6 @@ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![GitHub stars](https://img.shields.io/github/stars/czlonkowski/n8n-mcp?style=social)](https://github.com/czlonkowski/n8n-mcp) -[![Version](https://img.shields.io/badge/version-2.13.0-blue.svg)](https://github.com/czlonkowski/n8n-mcp) [![npm version](https://img.shields.io/npm/v/n8n-mcp.svg)](https://www.npmjs.com/package/n8n-mcp) [![codecov](https://codecov.io/gh/czlonkowski/n8n-mcp/graph/badge.svg?token=YOUR_TOKEN)](https://codecov.io/gh/czlonkowski/n8n-mcp) [![Tests](https://img.shields.io/badge/tests-1728%20passing-brightgreen.svg)](https://github.com/czlonkowski/n8n-mcp/actions) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index f575f4f..34d6b4b 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [2.13.1] - 2025-01-24 + +### Changed +- **Removed 5-operation limit from n8n_update_partial_workflow**: The workflow diff engine now supports unlimited operations per request + - Previously limited to 5 operations for "transactional integrity" + - Analysis revealed the limit was unnecessary - the clone-validate-apply pattern already ensures atomicity + - All operations are validated before any are applied, maintaining data integrity + - Enables complex workflow refactoring in single API calls + - Updated documentation and examples to demonstrate large batch operations (26+ operations) + ## [2.13.0] - 2025-01-24 ### Added diff --git a/docs/workflow-diff-examples.md b/docs/workflow-diff-examples.md index c999e2e..4a349bf 100644 --- a/docs/workflow-diff-examples.md +++ b/docs/workflow-diff-examples.md @@ -296,6 +296,193 @@ The `n8n_update_partial_workflow` tool allows you to make targeted changes to wo } ``` +### Example 5: Large Batch Workflow Refactoring +Demonstrates handling many operations in a single request - no longer limited to 5 operations! + +```json +{ + "id": "workflow-batch", + "operations": [ + // Add 10 processing nodes + { + "type": "addNode", + "node": { + "name": "Filter Active Users", + "type": "n8n-nodes-base.filter", + "position": [400, 200], + "parameters": { "conditions": { "boolean": [{ "value1": "={{$json.active}}", "value2": true }] } } + } + }, + { + "type": "addNode", + "node": { + "name": "Transform User Data", + "type": "n8n-nodes-base.set", + "position": [600, 200], + "parameters": { "values": { "string": [{ "name": "formatted_name", "value": "={{$json.firstName}} {{$json.lastName}}" }] } } + } + }, + { + "type": "addNode", + "node": { + "name": "Validate Email", + "type": "n8n-nodes-base.if", + "position": [800, 200], + "parameters": { "conditions": { "string": [{ "value1": "={{$json.email}}", "operation": "contains", "value2": "@" }] } } + } + }, + { + "type": "addNode", + "node": { + "name": "Enrich with API", + "type": "n8n-nodes-base.httpRequest", + "position": [1000, 150], + "parameters": { "url": "https://api.example.com/enrich", "method": "POST" } + } + }, + { + "type": "addNode", + "node": { + "name": "Log Invalid Emails", + "type": "n8n-nodes-base.code", + "position": [1000, 350], + "parameters": { "jsCode": "console.log('Invalid email:', $json.email);\nreturn $json;" } + } + }, + { + "type": "addNode", + "node": { + "name": "Merge Results", + "type": "n8n-nodes-base.merge", + "position": [1200, 250] + } + }, + { + "type": "addNode", + "node": { + "name": "Deduplicate", + "type": "n8n-nodes-base.removeDuplicates", + "position": [1400, 250], + "parameters": { "propertyName": "id" } + } + }, + { + "type": "addNode", + "node": { + "name": "Sort by Date", + "type": "n8n-nodes-base.sort", + "position": [1600, 250], + "parameters": { "sortFieldsUi": { "sortField": [{ "fieldName": "created_at", "order": "descending" }] } } + } + }, + { + "type": "addNode", + "node": { + "name": "Batch for DB", + "type": "n8n-nodes-base.splitInBatches", + "position": [1800, 250], + "parameters": { "batchSize": 100 } + } + }, + { + "type": "addNode", + "node": { + "name": "Save to Database", + "type": "n8n-nodes-base.postgres", + "position": [2000, 250], + "parameters": { "operation": "insert", "table": "processed_users" } + } + }, + // Connect all the nodes + { + "type": "addConnection", + "source": "Get Users", + "target": "Filter Active Users" + }, + { + "type": "addConnection", + "source": "Filter Active Users", + "target": "Transform User Data" + }, + { + "type": "addConnection", + "source": "Transform User Data", + "target": "Validate Email" + }, + { + "type": "addConnection", + "source": "Validate Email", + "sourceOutput": "true", + "target": "Enrich with API" + }, + { + "type": "addConnection", + "source": "Validate Email", + "sourceOutput": "false", + "target": "Log Invalid Emails" + }, + { + "type": "addConnection", + "source": "Enrich with API", + "target": "Merge Results" + }, + { + "type": "addConnection", + "source": "Log Invalid Emails", + "target": "Merge Results", + "targetInput": "input2" + }, + { + "type": "addConnection", + "source": "Merge Results", + "target": "Deduplicate" + }, + { + "type": "addConnection", + "source": "Deduplicate", + "target": "Sort by Date" + }, + { + "type": "addConnection", + "source": "Sort by Date", + "target": "Batch for DB" + }, + { + "type": "addConnection", + "source": "Batch for DB", + "target": "Save to Database" + }, + // Update workflow metadata + { + "type": "updateName", + "name": "User Processing Pipeline v2" + }, + { + "type": "updateSettings", + "settings": { + "executionOrder": "v1", + "timezone": "UTC", + "saveDataSuccessExecution": "all" + } + }, + { + "type": "addTag", + "tag": "production" + }, + { + "type": "addTag", + "tag": "user-processing" + }, + { + "type": "addTag", + "tag": "v2" + } + ] +} +``` + +This example shows 26 operations in a single request, creating a complete data processing pipeline with proper error handling, validation, and batch processing. + ## Best Practices 1. **Use Descriptive Names**: Always provide clear node names and descriptions for operations diff --git a/package.json b/package.json index 8edc66a..771ce37 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "n8n-mcp", - "version": "2.13.0", + "version": "2.13.1", "description": "Integration between n8n workflow automation and Model Context Protocol (MCP)", "main": "dist/index.js", "bin": { diff --git a/src/mcp/tool-docs/validation/validate-workflow.ts b/src/mcp/tool-docs/validation/validate-workflow.ts index 5c7a735..a7d69db 100644 --- a/src/mcp/tool-docs/validation/validate-workflow.ts +++ b/src/mcp/tool-docs/validation/validate-workflow.ts @@ -76,6 +76,6 @@ export const validateWorkflowDoc: ToolDocumentation = { 'Validation cannot catch all runtime errors (e.g., API failures)', 'Profile setting only affects node validation, not connection/expression checks' ], - relatedTools: ['validate_workflow_connections', 'validate_workflow_expressions', 'validate_node_operation', 'n8n_create_workflow', 'n8n_update_partial_workflow'] + relatedTools: ['validate_workflow_connections', 'validate_workflow_expressions', 'validate_node_operation', 'n8n_create_workflow', 'n8n_update_partial_workflow', 'n8n_autofix_workflow'] } }; \ No newline at end of file diff --git a/src/mcp/tool-docs/workflow_management/n8n-update-partial-workflow.ts b/src/mcp/tool-docs/workflow_management/n8n-update-partial-workflow.ts index b5b99dd..190ec37 100644 --- a/src/mcp/tool-docs/workflow_management/n8n-update-partial-workflow.ts +++ b/src/mcp/tool-docs/workflow_management/n8n-update-partial-workflow.ts @@ -4,18 +4,18 @@ export const n8nUpdatePartialWorkflowDoc: ToolDocumentation = { name: 'n8n_update_partial_workflow', category: 'workflow_management', essentials: { - description: 'Update workflow incrementally with diff operations. Max 5 ops. Types: addNode, removeNode, updateNode, moveNode, enable/disableNode, addConnection, removeConnection, updateSettings, updateName, add/removeTag.', + description: 'Update workflow incrementally with diff operations. Types: addNode, removeNode, updateNode, moveNode, enable/disableNode, addConnection, removeConnection, updateSettings, updateName, add/removeTag.', keyParameters: ['id', 'operations'], example: 'n8n_update_partial_workflow({id: "wf_123", operations: [{type: "updateNode", ...}]})', performance: 'Fast (50-200ms)', tips: [ 'Use for targeted changes', - 'Supports up to 5 operations', + 'Supports multiple operations in one call', 'Validate with validateOnly first' ] }, full: { - description: `Updates workflows using surgical diff operations instead of full replacement. Supports 13 operation types for precise modifications. Operations are validated and applied atomically - all succeed or none are applied. Maximum 5 operations per call for safety. + description: `Updates workflows using surgical diff operations instead of full replacement. Supports 13 operation types for precise modifications. Operations are validated and applied atomically - all succeed or none are applied. ## Available Operations: @@ -42,7 +42,7 @@ export const n8nUpdatePartialWorkflowDoc: ToolDocumentation = { operations: { type: 'array', required: true, - description: 'Array of diff operations. Each must have "type" field and operation-specific properties. Max 5 operations. Nodes can be referenced by ID or name.' + description: 'Array of diff operations. Each must have "type" field and operation-specific properties. Nodes can be referenced by ID or name.' }, validateOnly: { type: 'boolean', description: 'If true, only validate operations without applying them' } }, @@ -64,12 +64,10 @@ export const n8nUpdatePartialWorkflowDoc: ToolDocumentation = { bestPractices: [ 'Use validateOnly to test operations', 'Group related changes in one call', - 'Keep operations under 5 for clarity', 'Check operation order for dependencies' ], pitfalls: [ '**REQUIRES N8N_API_URL and N8N_API_KEY environment variables** - will not work without n8n API access', - 'Maximum 5 operations per call - split larger updates', 'Operations validated together - all must be valid', 'Order matters for dependent operations (e.g., must add node before connecting to it)', 'Node references accept ID or name, but name must be unique', diff --git a/src/mcp/tool-docs/workflow_management/n8n-validate-workflow.ts b/src/mcp/tool-docs/workflow_management/n8n-validate-workflow.ts index e4db73c..9e66563 100644 --- a/src/mcp/tool-docs/workflow_management/n8n-validate-workflow.ts +++ b/src/mcp/tool-docs/workflow_management/n8n-validate-workflow.ts @@ -66,6 +66,6 @@ Requires N8N_API_URL and N8N_API_KEY environment variables to be configured.`, 'Profile affects validation time - strict is slower but more thorough', 'Expression validation may flag working but non-standard syntax' ], - relatedTools: ['validate_workflow', 'n8n_get_workflow', 'validate_workflow_expressions', 'n8n_health_check'] + relatedTools: ['validate_workflow', 'n8n_get_workflow', 'validate_workflow_expressions', 'n8n_health_check', 'n8n_autofix_workflow'] } }; \ No newline at end of file diff --git a/src/mcp/tools-n8n-manager.ts b/src/mcp/tools-n8n-manager.ts index 058fb7f..ae0093d 100644 --- a/src/mcp/tools-n8n-manager.ts +++ b/src/mcp/tools-n8n-manager.ts @@ -160,7 +160,7 @@ export const n8nManagementTools: ToolDefinition[] = [ }, { name: 'n8n_update_partial_workflow', - description: `Update workflow incrementally with diff operations. Max 5 ops. Types: addNode, removeNode, updateNode, moveNode, enable/disableNode, addConnection, removeConnection, updateSettings, updateName, add/removeTag. See tools_documentation("n8n_update_partial_workflow", "full") for details.`, + description: `Update workflow incrementally with diff operations. Types: addNode, removeNode, updateNode, moveNode, enable/disableNode, addConnection, removeConnection, updateSettings, updateName, add/removeTag. See tools_documentation("n8n_update_partial_workflow", "full") for details.`, inputSchema: { type: 'object', additionalProperties: true, // Allow any extra properties Claude Desktop might add diff --git a/src/scripts/test-autofix-documentation.ts b/src/scripts/test-autofix-documentation.ts new file mode 100644 index 0000000..3dd7bb2 --- /dev/null +++ b/src/scripts/test-autofix-documentation.ts @@ -0,0 +1,121 @@ +#!/usr/bin/env npx tsx + +/** + * Test script to verify n8n_autofix_workflow documentation is properly integrated + */ + +import { toolsDocumentation } from '../mcp/tool-docs'; +import { getToolDocumentation } from '../mcp/tools-documentation'; +import { Logger } from '../utils/logger'; + +const logger = new Logger({ prefix: '[AutofixDoc Test]' }); + +async function testAutofixDocumentation() { + logger.info('Testing n8n_autofix_workflow documentation...\n'); + + // Test 1: Check if documentation exists in the registry + logger.info('Test 1: Checking documentation registry'); + const hasDoc = 'n8n_autofix_workflow' in toolsDocumentation; + if (hasDoc) { + logger.info('✅ Documentation found in registry'); + } else { + logger.error('❌ Documentation NOT found in registry'); + logger.info('Available tools:', Object.keys(toolsDocumentation).filter(k => k.includes('autofix'))); + } + + // Test 2: Check documentation structure + if (hasDoc) { + logger.info('\nTest 2: Checking documentation structure'); + const doc = toolsDocumentation['n8n_autofix_workflow']; + + const hasEssentials = doc.essentials && + doc.essentials.description && + doc.essentials.keyParameters && + doc.essentials.example; + + const hasFull = doc.full && + doc.full.description && + doc.full.parameters && + doc.full.examples; + + if (hasEssentials) { + logger.info('✅ Essentials documentation complete'); + logger.info(` Description: ${doc.essentials.description.substring(0, 80)}...`); + logger.info(` Key params: ${doc.essentials.keyParameters.join(', ')}`); + } else { + logger.error('❌ Essentials documentation incomplete'); + } + + if (hasFull) { + logger.info('✅ Full documentation complete'); + logger.info(` Parameters: ${Object.keys(doc.full.parameters).join(', ')}`); + logger.info(` Examples: ${doc.full.examples.length} provided`); + } else { + logger.error('❌ Full documentation incomplete'); + } + } + + // Test 3: Test getToolDocumentation function + logger.info('\nTest 3: Testing getToolDocumentation function'); + + try { + const essentialsDoc = getToolDocumentation('n8n_autofix_workflow', 'essentials'); + if (essentialsDoc.includes("Tool 'n8n_autofix_workflow' not found")) { + logger.error('❌ Essentials documentation retrieval failed'); + } else { + logger.info('✅ Essentials documentation retrieved'); + const lines = essentialsDoc.split('\n').slice(0, 3); + lines.forEach(line => logger.info(` ${line}`)); + } + } catch (error) { + logger.error('❌ Error retrieving essentials documentation:', error); + } + + try { + const fullDoc = getToolDocumentation('n8n_autofix_workflow', 'full'); + if (fullDoc.includes("Tool 'n8n_autofix_workflow' not found")) { + logger.error('❌ Full documentation retrieval failed'); + } else { + logger.info('✅ Full documentation retrieved'); + const lines = fullDoc.split('\n').slice(0, 3); + lines.forEach(line => logger.info(` ${line}`)); + } + } catch (error) { + logger.error('❌ Error retrieving full documentation:', error); + } + + // Test 4: Check if tool is listed in workflow management tools + logger.info('\nTest 4: Checking workflow management tools listing'); + const workflowTools = Object.keys(toolsDocumentation).filter(k => k.startsWith('n8n_')); + const hasAutofix = workflowTools.includes('n8n_autofix_workflow'); + + if (hasAutofix) { + logger.info('✅ n8n_autofix_workflow is listed in workflow management tools'); + logger.info(` Total workflow tools: ${workflowTools.length}`); + + // Show related tools + const relatedTools = workflowTools.filter(t => + t.includes('validate') || t.includes('update') || t.includes('fix') + ); + logger.info(` Related tools: ${relatedTools.join(', ')}`); + } else { + logger.error('❌ n8n_autofix_workflow NOT listed in workflow management tools'); + } + + // Summary + logger.info('\n' + '='.repeat(60)); + logger.info('Summary:'); + + if (hasDoc && hasAutofix) { + logger.info('✨ Documentation integration successful!'); + logger.info('The n8n_autofix_workflow tool documentation is properly integrated.'); + logger.info('\nTo use in MCP:'); + logger.info(' - Essentials: tools_documentation({topic: "n8n_autofix_workflow"})'); + logger.info(' - Full: tools_documentation({topic: "n8n_autofix_workflow", depth: "full"})'); + } else { + logger.error('⚠️ Documentation integration incomplete'); + logger.info('Please check the implementation and rebuild the project.'); + } +} + +testAutofixDocumentation().catch(console.error); \ No newline at end of file diff --git a/src/scripts/test-webhook-autofix.ts b/src/scripts/test-webhook-autofix.ts new file mode 100644 index 0000000..b1c9b70 --- /dev/null +++ b/src/scripts/test-webhook-autofix.ts @@ -0,0 +1,149 @@ +#!/usr/bin/env node + +/** + * Test script for webhook path autofixer functionality + */ + +import { NodeRepository } from '../database/node-repository'; +import { createDatabaseAdapter } from '../database/database-adapter'; +import { WorkflowAutoFixer } from '../services/workflow-auto-fixer'; +import { WorkflowValidator } from '../services/workflow-validator'; +import { EnhancedConfigValidator } from '../services/enhanced-config-validator'; +import { Workflow } from '../types/n8n-api'; +import { Logger } from '../utils/logger'; +import { join } from 'path'; + +const logger = new Logger({ prefix: '[TestWebhookAutofix]' }); + +// Test workflow with webhook missing path +const testWorkflow: Workflow = { + id: 'test_webhook_fix', + name: 'Test Webhook Autofix', + active: false, + nodes: [ + { + id: '1', + name: 'Webhook', + type: 'n8n-nodes-base.webhook', + typeVersion: 2.1, + position: [250, 300], + parameters: {}, // Empty parameters - missing path + }, + { + id: '2', + name: 'HTTP Request', + type: 'n8n-nodes-base.httpRequest', + typeVersion: 4.2, + position: [450, 300], + parameters: { + url: 'https://api.example.com/data', + method: 'GET' + } + } + ], + connections: { + 'Webhook': { + main: [[{ + node: 'HTTP Request', + type: 'main', + index: 0 + }]] + } + }, + settings: { + executionOrder: 'v1' + }, + staticData: undefined +}; + +async function testWebhookAutofix() { + logger.info('Testing webhook path autofixer...'); + + // Initialize database and repository + const dbPath = join(process.cwd(), 'data', 'nodes.db'); + const adapter = await createDatabaseAdapter(dbPath); + const repository = new NodeRepository(adapter); + + // Create validators + const validator = new WorkflowValidator(repository, EnhancedConfigValidator); + const autoFixer = new WorkflowAutoFixer(repository); + + // Step 1: Validate workflow to identify issues + logger.info('Step 1: Validating workflow to identify issues...'); + const validationResult = await validator.validateWorkflow(testWorkflow); + + console.log('\n📋 Validation Summary:'); + console.log(`- Valid: ${validationResult.valid}`); + console.log(`- Errors: ${validationResult.errors.length}`); + console.log(`- Warnings: ${validationResult.warnings.length}`); + + if (validationResult.errors.length > 0) { + console.log('\n❌ Errors found:'); + validationResult.errors.forEach(error => { + console.log(` - [${error.nodeName || error.nodeId}] ${error.message}`); + }); + } + + // Step 2: Generate fixes (preview mode) + logger.info('\nStep 2: Generating fixes in preview mode...'); + + const fixResult = autoFixer.generateFixes( + testWorkflow, + validationResult, + [], // No expression format issues to pass + { + applyFixes: false, // Preview mode + fixTypes: ['webhook-missing-path'] // Only test webhook fixes + } + ); + + console.log('\n🔧 Fix Results:'); + console.log(`- Summary: ${fixResult.summary}`); + console.log(`- Total fixes: ${fixResult.stats.total}`); + console.log(`- Webhook path fixes: ${fixResult.stats.byType['webhook-missing-path']}`); + + if (fixResult.fixes.length > 0) { + console.log('\n📝 Detailed Fixes:'); + fixResult.fixes.forEach(fix => { + console.log(` - Node: ${fix.node}`); + console.log(` Field: ${fix.field}`); + console.log(` Type: ${fix.type}`); + console.log(` Before: ${fix.before || 'undefined'}`); + console.log(` After: ${fix.after}`); + console.log(` Confidence: ${fix.confidence}`); + console.log(` Description: ${fix.description}`); + }); + } + + if (fixResult.operations.length > 0) { + console.log('\n🔄 Operations to Apply:'); + fixResult.operations.forEach(op => { + if (op.type === 'updateNode') { + console.log(` - Update Node: ${op.nodeId}`); + console.log(` Updates: ${JSON.stringify(op.updates, null, 2)}`); + } + }); + } + + // Step 3: Verify UUID format + if (fixResult.fixes.length > 0) { + const webhookFix = fixResult.fixes.find(f => f.type === 'webhook-missing-path'); + if (webhookFix) { + const uuid = webhookFix.after as string; + const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; + const isValidUUID = uuidRegex.test(uuid); + + console.log('\n✅ UUID Validation:'); + console.log(` - Generated UUID: ${uuid}`); + console.log(` - Valid format: ${isValidUUID ? 'Yes' : 'No'}`); + } + } + + logger.info('\n✨ Webhook autofix test completed successfully!'); +} + +// Run test +testWebhookAutofix().catch(error => { + logger.error('Test failed:', error); + process.exit(1); +}); \ No newline at end of file diff --git a/src/services/workflow-diff-engine.ts b/src/services/workflow-diff-engine.ts index 6475942..5b2c01c 100644 --- a/src/services/workflow-diff-engine.ts +++ b/src/services/workflow-diff-engine.ts @@ -41,17 +41,6 @@ export class WorkflowDiffEngine { request: WorkflowDiffRequest ): Promise { try { - // Limit operations to keep complexity manageable - if (request.operations.length > 5) { - return { - success: false, - errors: [{ - operation: -1, - message: 'Too many operations. Maximum 5 operations allowed per request to ensure transactional integrity.' - }] - }; - } - // Clone workflow to avoid modifying original const workflowCopy = JSON.parse(JSON.stringify(workflow)); diff --git a/tests/unit/services/workflow-diff-engine.test.ts b/tests/unit/services/workflow-diff-engine.test.ts index ff2d91f..0bbe5cb 100644 --- a/tests/unit/services/workflow-diff-engine.test.ts +++ b/tests/unit/services/workflow-diff-engine.test.ts @@ -1,8 +1,9 @@ import { describe, it, expect, beforeEach } from 'vitest'; import { WorkflowDiffEngine } from '@/services/workflow-diff-engine'; import { createWorkflow, WorkflowBuilder } from '@tests/utils/builders/workflow.builder'; -import { +import { WorkflowDiffRequest, + WorkflowDiffOperation, AddNodeOperation, RemoveNodeOperation, UpdateNodeOperation, @@ -60,9 +61,10 @@ describe('WorkflowDiffEngine', () => { baseWorkflow.connections = newConnections; }); - describe('Operation Limits', () => { - it('should reject more than 5 operations', async () => { - const operations = Array(6).fill(null).map((_: any, i: number) => ({ + describe('Large Operation Batches', () => { + it('should handle many operations successfully', async () => { + // Test with 50 operations + const operations = Array(50).fill(null).map((_: any, i: number) => ({ type: 'updateName', name: `Name ${i}` } as UpdateNameOperation)); @@ -73,10 +75,47 @@ describe('WorkflowDiffEngine', () => { }; const result = await diffEngine.applyDiff(baseWorkflow, request); - - expect(result.success).toBe(false); - expect(result.errors).toHaveLength(1); - expect(result.errors![0].message).toContain('Too many operations'); + + expect(result.success).toBe(true); + expect(result.operationsApplied).toBe(50); + expect(result.workflow!.name).toBe('Name 49'); // Last operation wins + }); + + it('should handle 100+ mixed operations', async () => { + const operations: WorkflowDiffOperation[] = [ + // Add 30 nodes + ...Array(30).fill(null).map((_: any, i: number) => ({ + type: 'addNode', + node: { + name: `Node${i}`, + type: 'n8n-nodes-base.code', + position: [i * 100, 300], + parameters: {} + } + } as AddNodeOperation)), + // Update names 30 times + ...Array(30).fill(null).map((_: any, i: number) => ({ + type: 'updateName', + name: `Workflow Version ${i}` + } as UpdateNameOperation)), + // Add 40 tags + ...Array(40).fill(null).map((_: any, i: number) => ({ + type: 'addTag', + tag: `tag${i}` + } as AddTagOperation)) + ]; + + const request: WorkflowDiffRequest = { + id: 'test-workflow', + operations + }; + + const result = await diffEngine.applyDiff(baseWorkflow, request); + + expect(result.success).toBe(true); + expect(result.operationsApplied).toBe(100); + expect(result.workflow!.nodes.length).toBeGreaterThan(30); + expect(result.workflow!.name).toBe('Workflow Version 29'); }); });