#
tokens: 50054/50000 1/614 files (page 57/59)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 57 of 59. Use http://codebase.md/czlonkowski/n8n-mcp?lines=true&page={x} to view the full context.

# Directory Structure

```
├── _config.yml
├── .claude
│   └── agents
│       ├── code-reviewer.md
│       ├── context-manager.md
│       ├── debugger.md
│       ├── deployment-engineer.md
│       ├── mcp-backend-engineer.md
│       ├── n8n-mcp-tester.md
│       ├── technical-researcher.md
│       └── test-automator.md
├── .dockerignore
├── .env.docker
├── .env.example
├── .env.n8n.example
├── .env.test
├── .env.test.example
├── .github
│   ├── ABOUT.md
│   ├── BENCHMARK_THRESHOLDS.md
│   ├── FUNDING.yml
│   ├── gh-pages.yml
│   ├── secret_scanning.yml
│   └── workflows
│       ├── benchmark-pr.yml
│       ├── benchmark.yml
│       ├── docker-build-fast.yml
│       ├── docker-build-n8n.yml
│       ├── docker-build.yml
│       ├── release.yml
│       ├── test.yml
│       └── update-n8n-deps.yml
├── .gitignore
├── .npmignore
├── ATTRIBUTION.md
├── CHANGELOG.md
├── CLAUDE.md
├── codecov.yml
├── coverage.json
├── data
│   ├── .gitkeep
│   ├── nodes.db
│   ├── nodes.db-shm
│   ├── nodes.db-wal
│   └── templates.db
├── deploy
│   └── quick-deploy-n8n.sh
├── docker
│   ├── docker-entrypoint.sh
│   ├── n8n-mcp
│   ├── parse-config.js
│   └── README.md
├── docker-compose.buildkit.yml
├── docker-compose.extract.yml
├── docker-compose.n8n.yml
├── docker-compose.override.yml.example
├── docker-compose.test-n8n.yml
├── docker-compose.yml
├── Dockerfile
├── Dockerfile.railway
├── Dockerfile.test
├── docs
│   ├── AUTOMATED_RELEASES.md
│   ├── BENCHMARKS.md
│   ├── CHANGELOG.md
│   ├── CLAUDE_CODE_SETUP.md
│   ├── CLAUDE_INTERVIEW.md
│   ├── CODECOV_SETUP.md
│   ├── CODEX_SETUP.md
│   ├── CURSOR_SETUP.md
│   ├── DEPENDENCY_UPDATES.md
│   ├── DOCKER_README.md
│   ├── DOCKER_TROUBLESHOOTING.md
│   ├── FINAL_AI_VALIDATION_SPEC.md
│   ├── FLEXIBLE_INSTANCE_CONFIGURATION.md
│   ├── HTTP_DEPLOYMENT.md
│   ├── img
│   │   ├── cc_command.png
│   │   ├── cc_connected.png
│   │   ├── codex_connected.png
│   │   ├── cursor_tut.png
│   │   ├── Railway_api.png
│   │   ├── Railway_server_address.png
│   │   ├── vsc_ghcp_chat_agent_mode.png
│   │   ├── vsc_ghcp_chat_instruction_files.png
│   │   ├── vsc_ghcp_chat_thinking_tool.png
│   │   └── windsurf_tut.png
│   ├── INSTALLATION.md
│   ├── LIBRARY_USAGE.md
│   ├── local
│   │   ├── DEEP_DIVE_ANALYSIS_2025-10-02.md
│   │   ├── DEEP_DIVE_ANALYSIS_README.md
│   │   ├── Deep_dive_p1_p2.md
│   │   ├── integration-testing-plan.md
│   │   ├── integration-tests-phase1-summary.md
│   │   ├── N8N_AI_WORKFLOW_BUILDER_ANALYSIS.md
│   │   ├── P0_IMPLEMENTATION_PLAN.md
│   │   └── TEMPLATE_MINING_ANALYSIS.md
│   ├── MCP_ESSENTIALS_README.md
│   ├── MCP_QUICK_START_GUIDE.md
│   ├── N8N_DEPLOYMENT.md
│   ├── RAILWAY_DEPLOYMENT.md
│   ├── README_CLAUDE_SETUP.md
│   ├── README.md
│   ├── tools-documentation-usage.md
│   ├── VS_CODE_PROJECT_SETUP.md
│   ├── WINDSURF_SETUP.md
│   └── workflow-diff-examples.md
├── examples
│   └── enhanced-documentation-demo.js
├── fetch_log.txt
├── LICENSE
├── MEMORY_N8N_UPDATE.md
├── MEMORY_TEMPLATE_UPDATE.md
├── monitor_fetch.sh
├── N8N_HTTP_STREAMABLE_SETUP.md
├── n8n-nodes.db
├── P0-R3-TEST-PLAN.md
├── package-lock.json
├── package.json
├── package.runtime.json
├── PRIVACY.md
├── railway.json
├── README.md
├── renovate.json
├── scripts
│   ├── analyze-optimization.sh
│   ├── audit-schema-coverage.ts
│   ├── build-optimized.sh
│   ├── compare-benchmarks.js
│   ├── demo-optimization.sh
│   ├── deploy-http.sh
│   ├── deploy-to-vm.sh
│   ├── export-webhook-workflows.ts
│   ├── extract-changelog.js
│   ├── extract-from-docker.js
│   ├── extract-nodes-docker.sh
│   ├── extract-nodes-simple.sh
│   ├── format-benchmark-results.js
│   ├── generate-benchmark-stub.js
│   ├── generate-detailed-reports.js
│   ├── generate-test-summary.js
│   ├── http-bridge.js
│   ├── mcp-http-client.js
│   ├── migrate-nodes-fts.ts
│   ├── migrate-tool-docs.ts
│   ├── n8n-docs-mcp.service
│   ├── nginx-n8n-mcp.conf
│   ├── prebuild-fts5.ts
│   ├── prepare-release.js
│   ├── publish-npm-quick.sh
│   ├── publish-npm.sh
│   ├── quick-test.ts
│   ├── run-benchmarks-ci.js
│   ├── sync-runtime-version.js
│   ├── test-ai-validation-debug.ts
│   ├── test-code-node-enhancements.ts
│   ├── test-code-node-fixes.ts
│   ├── test-docker-config.sh
│   ├── test-docker-fingerprint.ts
│   ├── test-docker-optimization.sh
│   ├── test-docker.sh
│   ├── test-empty-connection-validation.ts
│   ├── test-error-message-tracking.ts
│   ├── test-error-output-validation.ts
│   ├── test-error-validation.js
│   ├── test-essentials.ts
│   ├── test-expression-code-validation.ts
│   ├── test-expression-format-validation.js
│   ├── test-fts5-search.ts
│   ├── test-fuzzy-fix.ts
│   ├── test-fuzzy-simple.ts
│   ├── test-helpers-validation.ts
│   ├── test-http-search.ts
│   ├── test-http.sh
│   ├── test-jmespath-validation.ts
│   ├── test-multi-tenant-simple.ts
│   ├── test-multi-tenant.ts
│   ├── test-n8n-integration.sh
│   ├── test-node-info.js
│   ├── test-node-type-validation.ts
│   ├── test-nodes-base-prefix.ts
│   ├── test-operation-validation.ts
│   ├── test-optimized-docker.sh
│   ├── test-release-automation.js
│   ├── test-search-improvements.ts
│   ├── test-security.ts
│   ├── test-single-session.sh
│   ├── test-sqljs-triggers.ts
│   ├── test-telemetry-debug.ts
│   ├── test-telemetry-direct.ts
│   ├── test-telemetry-env.ts
│   ├── test-telemetry-integration.ts
│   ├── test-telemetry-no-select.ts
│   ├── test-telemetry-security.ts
│   ├── test-telemetry-simple.ts
│   ├── test-typeversion-validation.ts
│   ├── test-url-configuration.ts
│   ├── test-user-id-persistence.ts
│   ├── test-webhook-validation.ts
│   ├── test-workflow-insert.ts
│   ├── test-workflow-sanitizer.ts
│   ├── test-workflow-tracking-debug.ts
│   ├── update-and-publish-prep.sh
│   ├── update-n8n-deps.js
│   ├── update-readme-version.js
│   ├── vitest-benchmark-json-reporter.js
│   └── vitest-benchmark-reporter.ts
├── SECURITY.md
├── src
│   ├── config
│   │   └── n8n-api.ts
│   ├── data
│   │   └── canonical-ai-tool-examples.json
│   ├── database
│   │   ├── database-adapter.ts
│   │   ├── migrations
│   │   │   └── add-template-node-configs.sql
│   │   ├── node-repository.ts
│   │   ├── nodes.db
│   │   ├── schema-optimized.sql
│   │   └── schema.sql
│   ├── errors
│   │   └── validation-service-error.ts
│   ├── http-server-single-session.ts
│   ├── http-server.ts
│   ├── index.ts
│   ├── loaders
│   │   └── node-loader.ts
│   ├── mappers
│   │   └── docs-mapper.ts
│   ├── mcp
│   │   ├── handlers-n8n-manager.ts
│   │   ├── handlers-workflow-diff.ts
│   │   ├── index.ts
│   │   ├── server.ts
│   │   ├── stdio-wrapper.ts
│   │   ├── tool-docs
│   │   │   ├── configuration
│   │   │   │   ├── get-node-as-tool-info.ts
│   │   │   │   ├── get-node-documentation.ts
│   │   │   │   ├── get-node-essentials.ts
│   │   │   │   ├── get-node-info.ts
│   │   │   │   ├── get-property-dependencies.ts
│   │   │   │   ├── index.ts
│   │   │   │   └── search-node-properties.ts
│   │   │   ├── discovery
│   │   │   │   ├── get-database-statistics.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── list-ai-tools.ts
│   │   │   │   ├── list-nodes.ts
│   │   │   │   └── search-nodes.ts
│   │   │   ├── guides
│   │   │   │   ├── ai-agents-guide.ts
│   │   │   │   └── index.ts
│   │   │   ├── index.ts
│   │   │   ├── system
│   │   │   │   ├── index.ts
│   │   │   │   ├── n8n-diagnostic.ts
│   │   │   │   ├── n8n-health-check.ts
│   │   │   │   ├── n8n-list-available-tools.ts
│   │   │   │   └── tools-documentation.ts
│   │   │   ├── templates
│   │   │   │   ├── get-template.ts
│   │   │   │   ├── get-templates-for-task.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── list-node-templates.ts
│   │   │   │   ├── list-tasks.ts
│   │   │   │   ├── search-templates-by-metadata.ts
│   │   │   │   └── search-templates.ts
│   │   │   ├── types.ts
│   │   │   ├── validation
│   │   │   │   ├── index.ts
│   │   │   │   ├── validate-node-minimal.ts
│   │   │   │   ├── validate-node-operation.ts
│   │   │   │   ├── validate-workflow-connections.ts
│   │   │   │   ├── validate-workflow-expressions.ts
│   │   │   │   └── validate-workflow.ts
│   │   │   └── workflow_management
│   │   │       ├── index.ts
│   │   │       ├── n8n-autofix-workflow.ts
│   │   │       ├── n8n-create-workflow.ts
│   │   │       ├── n8n-delete-execution.ts
│   │   │       ├── n8n-delete-workflow.ts
│   │   │       ├── n8n-get-execution.ts
│   │   │       ├── n8n-get-workflow-details.ts
│   │   │       ├── n8n-get-workflow-minimal.ts
│   │   │       ├── n8n-get-workflow-structure.ts
│   │   │       ├── n8n-get-workflow.ts
│   │   │       ├── n8n-list-executions.ts
│   │   │       ├── n8n-list-workflows.ts
│   │   │       ├── n8n-trigger-webhook-workflow.ts
│   │   │       ├── n8n-update-full-workflow.ts
│   │   │       ├── n8n-update-partial-workflow.ts
│   │   │       └── n8n-validate-workflow.ts
│   │   ├── tools-documentation.ts
│   │   ├── tools-n8n-friendly.ts
│   │   ├── tools-n8n-manager.ts
│   │   ├── tools.ts
│   │   └── workflow-examples.ts
│   ├── mcp-engine.ts
│   ├── mcp-tools-engine.ts
│   ├── n8n
│   │   ├── MCPApi.credentials.ts
│   │   └── MCPNode.node.ts
│   ├── parsers
│   │   ├── node-parser.ts
│   │   ├── property-extractor.ts
│   │   └── simple-parser.ts
│   ├── scripts
│   │   ├── debug-http-search.ts
│   │   ├── extract-from-docker.ts
│   │   ├── fetch-templates-robust.ts
│   │   ├── fetch-templates.ts
│   │   ├── rebuild-database.ts
│   │   ├── rebuild-optimized.ts
│   │   ├── rebuild.ts
│   │   ├── sanitize-templates.ts
│   │   ├── seed-canonical-ai-examples.ts
│   │   ├── test-autofix-documentation.ts
│   │   ├── test-autofix-workflow.ts
│   │   ├── test-execution-filtering.ts
│   │   ├── test-node-suggestions.ts
│   │   ├── test-protocol-negotiation.ts
│   │   ├── test-summary.ts
│   │   ├── test-webhook-autofix.ts
│   │   ├── validate.ts
│   │   └── validation-summary.ts
│   ├── services
│   │   ├── ai-node-validator.ts
│   │   ├── ai-tool-validators.ts
│   │   ├── confidence-scorer.ts
│   │   ├── config-validator.ts
│   │   ├── enhanced-config-validator.ts
│   │   ├── example-generator.ts
│   │   ├── execution-processor.ts
│   │   ├── expression-format-validator.ts
│   │   ├── expression-validator.ts
│   │   ├── n8n-api-client.ts
│   │   ├── n8n-validation.ts
│   │   ├── node-documentation-service.ts
│   │   ├── node-similarity-service.ts
│   │   ├── node-specific-validators.ts
│   │   ├── operation-similarity-service.ts
│   │   ├── property-dependencies.ts
│   │   ├── property-filter.ts
│   │   ├── resource-similarity-service.ts
│   │   ├── sqlite-storage-service.ts
│   │   ├── task-templates.ts
│   │   ├── universal-expression-validator.ts
│   │   ├── workflow-auto-fixer.ts
│   │   ├── workflow-diff-engine.ts
│   │   └── workflow-validator.ts
│   ├── telemetry
│   │   ├── batch-processor.ts
│   │   ├── config-manager.ts
│   │   ├── early-error-logger.ts
│   │   ├── error-sanitization-utils.ts
│   │   ├── error-sanitizer.ts
│   │   ├── event-tracker.ts
│   │   ├── event-validator.ts
│   │   ├── index.ts
│   │   ├── performance-monitor.ts
│   │   ├── rate-limiter.ts
│   │   ├── startup-checkpoints.ts
│   │   ├── telemetry-error.ts
│   │   ├── telemetry-manager.ts
│   │   ├── telemetry-types.ts
│   │   └── workflow-sanitizer.ts
│   ├── templates
│   │   ├── batch-processor.ts
│   │   ├── metadata-generator.ts
│   │   ├── README.md
│   │   ├── template-fetcher.ts
│   │   ├── template-repository.ts
│   │   └── template-service.ts
│   ├── types
│   │   ├── index.ts
│   │   ├── instance-context.ts
│   │   ├── n8n-api.ts
│   │   ├── node-types.ts
│   │   └── workflow-diff.ts
│   └── utils
│       ├── auth.ts
│       ├── bridge.ts
│       ├── cache-utils.ts
│       ├── console-manager.ts
│       ├── documentation-fetcher.ts
│       ├── enhanced-documentation-fetcher.ts
│       ├── error-handler.ts
│       ├── example-generator.ts
│       ├── fixed-collection-validator.ts
│       ├── logger.ts
│       ├── mcp-client.ts
│       ├── n8n-errors.ts
│       ├── node-source-extractor.ts
│       ├── node-type-normalizer.ts
│       ├── node-type-utils.ts
│       ├── node-utils.ts
│       ├── npm-version-checker.ts
│       ├── protocol-version.ts
│       ├── simple-cache.ts
│       ├── ssrf-protection.ts
│       ├── template-node-resolver.ts
│       ├── template-sanitizer.ts
│       ├── url-detector.ts
│       ├── validation-schemas.ts
│       └── version.ts
├── test-output.txt
├── test-reinit-fix.sh
├── tests
│   ├── __snapshots__
│   │   └── .gitkeep
│   ├── auth.test.ts
│   ├── benchmarks
│   │   ├── database-queries.bench.ts
│   │   ├── index.ts
│   │   ├── mcp-tools.bench.ts
│   │   ├── mcp-tools.bench.ts.disabled
│   │   ├── mcp-tools.bench.ts.skip
│   │   ├── node-loading.bench.ts.disabled
│   │   ├── README.md
│   │   ├── search-operations.bench.ts.disabled
│   │   └── validation-performance.bench.ts.disabled
│   ├── bridge.test.ts
│   ├── comprehensive-extraction-test.js
│   ├── data
│   │   └── .gitkeep
│   ├── debug-slack-doc.js
│   ├── demo-enhanced-documentation.js
│   ├── docker-tests-README.md
│   ├── error-handler.test.ts
│   ├── examples
│   │   └── using-database-utils.test.ts
│   ├── extracted-nodes-db
│   │   ├── database-import.json
│   │   ├── extraction-report.json
│   │   ├── insert-nodes.sql
│   │   ├── n8n-nodes-base__Airtable.json
│   │   ├── n8n-nodes-base__Discord.json
│   │   ├── n8n-nodes-base__Function.json
│   │   ├── n8n-nodes-base__HttpRequest.json
│   │   ├── n8n-nodes-base__If.json
│   │   ├── n8n-nodes-base__Slack.json
│   │   ├── n8n-nodes-base__SplitInBatches.json
│   │   └── n8n-nodes-base__Webhook.json
│   ├── factories
│   │   ├── node-factory.ts
│   │   └── property-definition-factory.ts
│   ├── fixtures
│   │   ├── .gitkeep
│   │   ├── database
│   │   │   └── test-nodes.json
│   │   ├── factories
│   │   │   ├── node.factory.ts
│   │   │   └── parser-node.factory.ts
│   │   └── template-configs.ts
│   ├── helpers
│   │   └── env-helpers.ts
│   ├── http-server-auth.test.ts
│   ├── integration
│   │   ├── ai-validation
│   │   │   ├── ai-agent-validation.test.ts
│   │   │   ├── ai-tool-validation.test.ts
│   │   │   ├── chat-trigger-validation.test.ts
│   │   │   ├── e2e-validation.test.ts
│   │   │   ├── helpers.ts
│   │   │   ├── llm-chain-validation.test.ts
│   │   │   ├── README.md
│   │   │   └── TEST_REPORT.md
│   │   ├── ci
│   │   │   └── database-population.test.ts
│   │   ├── database
│   │   │   ├── connection-management.test.ts
│   │   │   ├── empty-database.test.ts
│   │   │   ├── fts5-search.test.ts
│   │   │   ├── node-fts5-search.test.ts
│   │   │   ├── node-repository.test.ts
│   │   │   ├── performance.test.ts
│   │   │   ├── template-node-configs.test.ts
│   │   │   ├── template-repository.test.ts
│   │   │   ├── test-utils.ts
│   │   │   └── transactions.test.ts
│   │   ├── database-integration.test.ts
│   │   ├── docker
│   │   │   ├── docker-config.test.ts
│   │   │   ├── docker-entrypoint.test.ts
│   │   │   └── test-helpers.ts
│   │   ├── flexible-instance-config.test.ts
│   │   ├── mcp
│   │   │   └── template-examples-e2e.test.ts
│   │   ├── mcp-protocol
│   │   │   ├── basic-connection.test.ts
│   │   │   ├── error-handling.test.ts
│   │   │   ├── performance.test.ts
│   │   │   ├── protocol-compliance.test.ts
│   │   │   ├── README.md
│   │   │   ├── session-management.test.ts
│   │   │   ├── test-helpers.ts
│   │   │   ├── tool-invocation.test.ts
│   │   │   └── workflow-error-validation.test.ts
│   │   ├── msw-setup.test.ts
│   │   ├── n8n-api
│   │   │   ├── executions
│   │   │   │   ├── delete-execution.test.ts
│   │   │   │   ├── get-execution.test.ts
│   │   │   │   ├── list-executions.test.ts
│   │   │   │   └── trigger-webhook.test.ts
│   │   │   ├── scripts
│   │   │   │   └── cleanup-orphans.ts
│   │   │   ├── system
│   │   │   │   ├── diagnostic.test.ts
│   │   │   │   ├── health-check.test.ts
│   │   │   │   └── list-tools.test.ts
│   │   │   ├── test-connection.ts
│   │   │   ├── types
│   │   │   │   └── mcp-responses.ts
│   │   │   ├── utils
│   │   │   │   ├── cleanup-helpers.ts
│   │   │   │   ├── credentials.ts
│   │   │   │   ├── factories.ts
│   │   │   │   ├── fixtures.ts
│   │   │   │   ├── mcp-context.ts
│   │   │   │   ├── n8n-client.ts
│   │   │   │   ├── node-repository.ts
│   │   │   │   ├── response-types.ts
│   │   │   │   ├── test-context.ts
│   │   │   │   └── webhook-workflows.ts
│   │   │   └── workflows
│   │   │       ├── autofix-workflow.test.ts
│   │   │       ├── create-workflow.test.ts
│   │   │       ├── delete-workflow.test.ts
│   │   │       ├── get-workflow-details.test.ts
│   │   │       ├── get-workflow-minimal.test.ts
│   │   │       ├── get-workflow-structure.test.ts
│   │   │       ├── get-workflow.test.ts
│   │   │       ├── list-workflows.test.ts
│   │   │       ├── smart-parameters.test.ts
│   │   │       ├── update-partial-workflow.test.ts
│   │   │       ├── update-workflow.test.ts
│   │   │       └── validate-workflow.test.ts
│   │   ├── security
│   │   │   ├── command-injection-prevention.test.ts
│   │   │   └── rate-limiting.test.ts
│   │   ├── setup
│   │   │   ├── integration-setup.ts
│   │   │   └── msw-test-server.ts
│   │   ├── telemetry
│   │   │   ├── docker-user-id-stability.test.ts
│   │   │   └── mcp-telemetry.test.ts
│   │   ├── templates
│   │   │   └── metadata-operations.test.ts
│   │   └── workflow-creation-node-type-format.test.ts
│   ├── logger.test.ts
│   ├── MOCKING_STRATEGY.md
│   ├── mocks
│   │   ├── n8n-api
│   │   │   ├── data
│   │   │   │   ├── credentials.ts
│   │   │   │   ├── executions.ts
│   │   │   │   └── workflows.ts
│   │   │   ├── handlers.ts
│   │   │   └── index.ts
│   │   └── README.md
│   ├── node-storage-export.json
│   ├── setup
│   │   ├── global-setup.ts
│   │   ├── msw-setup.ts
│   │   ├── TEST_ENV_DOCUMENTATION.md
│   │   └── test-env.ts
│   ├── test-database-extraction.js
│   ├── test-direct-extraction.js
│   ├── test-enhanced-documentation.js
│   ├── test-enhanced-integration.js
│   ├── test-mcp-extraction.js
│   ├── test-mcp-server-extraction.js
│   ├── test-mcp-tools-integration.js
│   ├── test-node-documentation-service.js
│   ├── test-node-list.js
│   ├── test-package-info.js
│   ├── test-parsing-operations.js
│   ├── test-slack-node-complete.js
│   ├── test-small-rebuild.js
│   ├── test-sqlite-search.js
│   ├── test-storage-system.js
│   ├── unit
│   │   ├── __mocks__
│   │   │   ├── n8n-nodes-base.test.ts
│   │   │   ├── n8n-nodes-base.ts
│   │   │   └── README.md
│   │   ├── database
│   │   │   ├── __mocks__
│   │   │   │   └── better-sqlite3.ts
│   │   │   ├── database-adapter-unit.test.ts
│   │   │   ├── node-repository-core.test.ts
│   │   │   ├── node-repository-operations.test.ts
│   │   │   ├── node-repository-outputs.test.ts
│   │   │   ├── README.md
│   │   │   └── template-repository-core.test.ts
│   │   ├── docker
│   │   │   ├── config-security.test.ts
│   │   │   ├── edge-cases.test.ts
│   │   │   ├── parse-config.test.ts
│   │   │   └── serve-command.test.ts
│   │   ├── errors
│   │   │   └── validation-service-error.test.ts
│   │   ├── examples
│   │   │   └── using-n8n-nodes-base-mock.test.ts
│   │   ├── flexible-instance-security-advanced.test.ts
│   │   ├── flexible-instance-security.test.ts
│   │   ├── http-server
│   │   │   └── multi-tenant-support.test.ts
│   │   ├── http-server-n8n-mode.test.ts
│   │   ├── http-server-n8n-reinit.test.ts
│   │   ├── http-server-session-management.test.ts
│   │   ├── loaders
│   │   │   └── node-loader.test.ts
│   │   ├── mappers
│   │   │   └── docs-mapper.test.ts
│   │   ├── mcp
│   │   │   ├── get-node-essentials-examples.test.ts
│   │   │   ├── handlers-n8n-manager-simple.test.ts
│   │   │   ├── handlers-n8n-manager.test.ts
│   │   │   ├── handlers-workflow-diff.test.ts
│   │   │   ├── lru-cache-behavior.test.ts
│   │   │   ├── multi-tenant-tool-listing.test.ts.disabled
│   │   │   ├── parameter-validation.test.ts
│   │   │   ├── search-nodes-examples.test.ts
│   │   │   ├── tools-documentation.test.ts
│   │   │   └── tools.test.ts
│   │   ├── monitoring
│   │   │   └── cache-metrics.test.ts
│   │   ├── MULTI_TENANT_TEST_COVERAGE.md
│   │   ├── multi-tenant-integration.test.ts
│   │   ├── parsers
│   │   │   ├── node-parser-outputs.test.ts
│   │   │   ├── node-parser.test.ts
│   │   │   ├── property-extractor.test.ts
│   │   │   └── simple-parser.test.ts
│   │   ├── scripts
│   │   │   └── fetch-templates-extraction.test.ts
│   │   ├── services
│   │   │   ├── ai-node-validator.test.ts
│   │   │   ├── ai-tool-validators.test.ts
│   │   │   ├── confidence-scorer.test.ts
│   │   │   ├── config-validator-basic.test.ts
│   │   │   ├── config-validator-edge-cases.test.ts
│   │   │   ├── config-validator-node-specific.test.ts
│   │   │   ├── config-validator-security.test.ts
│   │   │   ├── debug-validator.test.ts
│   │   │   ├── enhanced-config-validator-integration.test.ts
│   │   │   ├── enhanced-config-validator-operations.test.ts
│   │   │   ├── enhanced-config-validator.test.ts
│   │   │   ├── example-generator.test.ts
│   │   │   ├── execution-processor.test.ts
│   │   │   ├── expression-format-validator.test.ts
│   │   │   ├── expression-validator-edge-cases.test.ts
│   │   │   ├── expression-validator.test.ts
│   │   │   ├── fixed-collection-validation.test.ts
│   │   │   ├── loop-output-edge-cases.test.ts
│   │   │   ├── n8n-api-client.test.ts
│   │   │   ├── n8n-validation.test.ts
│   │   │   ├── node-similarity-service.test.ts
│   │   │   ├── node-specific-validators.test.ts
│   │   │   ├── operation-similarity-service-comprehensive.test.ts
│   │   │   ├── operation-similarity-service.test.ts
│   │   │   ├── property-dependencies.test.ts
│   │   │   ├── property-filter-edge-cases.test.ts
│   │   │   ├── property-filter.test.ts
│   │   │   ├── resource-similarity-service-comprehensive.test.ts
│   │   │   ├── resource-similarity-service.test.ts
│   │   │   ├── task-templates.test.ts
│   │   │   ├── template-service.test.ts
│   │   │   ├── universal-expression-validator.test.ts
│   │   │   ├── validation-fixes.test.ts
│   │   │   ├── workflow-auto-fixer.test.ts
│   │   │   ├── workflow-diff-engine.test.ts
│   │   │   ├── workflow-fixed-collection-validation.test.ts
│   │   │   ├── workflow-validator-comprehensive.test.ts
│   │   │   ├── workflow-validator-edge-cases.test.ts
│   │   │   ├── workflow-validator-error-outputs.test.ts
│   │   │   ├── workflow-validator-expression-format.test.ts
│   │   │   ├── workflow-validator-loops-simple.test.ts
│   │   │   ├── workflow-validator-loops.test.ts
│   │   │   ├── workflow-validator-mocks.test.ts
│   │   │   ├── workflow-validator-performance.test.ts
│   │   │   ├── workflow-validator-with-mocks.test.ts
│   │   │   └── workflow-validator.test.ts
│   │   ├── telemetry
│   │   │   ├── batch-processor.test.ts
│   │   │   ├── config-manager.test.ts
│   │   │   ├── event-tracker.test.ts
│   │   │   ├── event-validator.test.ts
│   │   │   ├── rate-limiter.test.ts
│   │   │   ├── telemetry-error.test.ts
│   │   │   ├── telemetry-manager.test.ts
│   │   │   ├── v2.18.3-fixes-verification.test.ts
│   │   │   └── workflow-sanitizer.test.ts
│   │   ├── templates
│   │   │   ├── batch-processor.test.ts
│   │   │   ├── metadata-generator.test.ts
│   │   │   ├── template-repository-metadata.test.ts
│   │   │   └── template-repository-security.test.ts
│   │   ├── test-env-example.test.ts
│   │   ├── test-infrastructure.test.ts
│   │   ├── types
│   │   │   ├── instance-context-coverage.test.ts
│   │   │   └── instance-context-multi-tenant.test.ts
│   │   ├── utils
│   │   │   ├── auth-timing-safe.test.ts
│   │   │   ├── cache-utils.test.ts
│   │   │   ├── console-manager.test.ts
│   │   │   ├── database-utils.test.ts
│   │   │   ├── fixed-collection-validator.test.ts
│   │   │   ├── n8n-errors.test.ts
│   │   │   ├── node-type-normalizer.test.ts
│   │   │   ├── node-type-utils.test.ts
│   │   │   ├── node-utils.test.ts
│   │   │   ├── simple-cache-memory-leak-fix.test.ts
│   │   │   ├── ssrf-protection.test.ts
│   │   │   └── template-node-resolver.test.ts
│   │   └── validation-fixes.test.ts
│   └── utils
│       ├── assertions.ts
│       ├── builders
│       │   └── workflow.builder.ts
│       ├── data-generators.ts
│       ├── database-utils.ts
│       ├── README.md
│       └── test-helpers.ts
├── thumbnail.png
├── tsconfig.build.json
├── tsconfig.json
├── types
│   ├── mcp.d.ts
│   └── test-env.d.ts
├── verify-telemetry-fix.js
├── versioned-nodes.md
├── vitest.config.benchmark.ts
├── vitest.config.integration.ts
└── vitest.config.ts
```

# Files

--------------------------------------------------------------------------------
/tests/unit/services/workflow-diff-engine.test.ts:
--------------------------------------------------------------------------------

```typescript
   1 | import { describe, it, expect, beforeEach } from 'vitest';
   2 | import { WorkflowDiffEngine } from '@/services/workflow-diff-engine';
   3 | import { createWorkflow, WorkflowBuilder } from '@tests/utils/builders/workflow.builder';
   4 | import {
   5 |   WorkflowDiffRequest,
   6 |   WorkflowDiffOperation,
   7 |   AddNodeOperation,
   8 |   RemoveNodeOperation,
   9 |   UpdateNodeOperation,
  10 |   MoveNodeOperation,
  11 |   EnableNodeOperation,
  12 |   DisableNodeOperation,
  13 |   AddConnectionOperation,
  14 |   RemoveConnectionOperation,
  15 |   UpdateSettingsOperation,
  16 |   UpdateNameOperation,
  17 |   AddTagOperation,
  18 |   RemoveTagOperation,
  19 |   CleanStaleConnectionsOperation,
  20 |   ReplaceConnectionsOperation
  21 | } from '@/types/workflow-diff';
  22 | import { Workflow } from '@/types/n8n-api';
  23 | 
  24 | describe('WorkflowDiffEngine', () => {
  25 |   let diffEngine: WorkflowDiffEngine;
  26 |   let baseWorkflow: Workflow;
  27 |   let builder: WorkflowBuilder;
  28 | 
  29 |   beforeEach(() => {
  30 |     diffEngine = new WorkflowDiffEngine();
  31 |     
  32 |     // Create a base workflow with some nodes
  33 |     builder = createWorkflow('Test Workflow')
  34 |       .addWebhookNode({ id: 'webhook-1', name: 'Webhook' })
  35 |       .addHttpRequestNode({ id: 'http-1', name: 'HTTP Request' })
  36 |       .addSlackNode({ id: 'slack-1', name: 'Slack' })
  37 |       .connect('webhook-1', 'http-1')
  38 |       .connect('http-1', 'slack-1')
  39 |       .addTags('test', 'automation');
  40 |     
  41 |     baseWorkflow = builder.build() as Workflow;
  42 |     
  43 |     // Convert connections from ID-based to name-based (as n8n expects)
  44 |     const newConnections: any = {};
  45 |     for (const [nodeId, outputs] of Object.entries(baseWorkflow.connections)) {
  46 |       const node = baseWorkflow.nodes.find((n: any) => n.id === nodeId);
  47 |       if (node) {
  48 |         newConnections[node.name] = {};
  49 |         for (const [outputName, connections] of Object.entries(outputs)) {
  50 |           newConnections[node.name][outputName] = (connections as any[]).map((conns: any) =>
  51 |             conns.map((conn: any) => {
  52 |               const targetNode = baseWorkflow.nodes.find((n: any) => n.id === conn.node);
  53 |               return {
  54 |                 ...conn,
  55 |                 node: targetNode ? targetNode.name : conn.node
  56 |               };
  57 |             })
  58 |           );
  59 |         }
  60 |       }
  61 |     }
  62 |     baseWorkflow.connections = newConnections;
  63 |   });
  64 | 
  65 |   describe('Large Operation Batches', () => {
  66 |     it('should handle many operations successfully', async () => {
  67 |       // Test with 50 operations
  68 |       const operations = Array(50).fill(null).map((_: any, i: number) => ({
  69 |         type: 'updateName',
  70 |         name: `Name ${i}`
  71 |       } as UpdateNameOperation));
  72 | 
  73 |       const request: WorkflowDiffRequest = {
  74 |         id: 'test-workflow',
  75 |         operations
  76 |       };
  77 | 
  78 |       const result = await diffEngine.applyDiff(baseWorkflow, request);
  79 | 
  80 |       expect(result.success).toBe(true);
  81 |       expect(result.operationsApplied).toBe(50);
  82 |       expect(result.workflow!.name).toBe('Name 49'); // Last operation wins
  83 |     });
  84 | 
  85 |     it('should handle 100+ mixed operations', async () => {
  86 |       const operations: WorkflowDiffOperation[] = [
  87 |         // Add 30 nodes
  88 |         ...Array(30).fill(null).map((_: any, i: number) => ({
  89 |           type: 'addNode',
  90 |           node: {
  91 |             name: `Node${i}`,
  92 |             type: 'n8n-nodes-base.code',
  93 |             position: [i * 100, 300],
  94 |             parameters: {}
  95 |           }
  96 |         } as AddNodeOperation)),
  97 |         // Update names 30 times
  98 |         ...Array(30).fill(null).map((_: any, i: number) => ({
  99 |           type: 'updateName',
 100 |           name: `Workflow Version ${i}`
 101 |         } as UpdateNameOperation)),
 102 |         // Add 40 tags
 103 |         ...Array(40).fill(null).map((_: any, i: number) => ({
 104 |           type: 'addTag',
 105 |           tag: `tag${i}`
 106 |         } as AddTagOperation))
 107 |       ];
 108 | 
 109 |       const request: WorkflowDiffRequest = {
 110 |         id: 'test-workflow',
 111 |         operations
 112 |       };
 113 | 
 114 |       const result = await diffEngine.applyDiff(baseWorkflow, request);
 115 | 
 116 |       expect(result.success).toBe(true);
 117 |       expect(result.operationsApplied).toBe(100);
 118 |       expect(result.workflow!.nodes.length).toBeGreaterThan(30);
 119 |       expect(result.workflow!.name).toBe('Workflow Version 29');
 120 |     });
 121 |   });
 122 | 
 123 |   describe('AddNode Operation', () => {
 124 |     it('should add a new node successfully', async () => {
 125 |       const operation: AddNodeOperation = {
 126 |         type: 'addNode',
 127 |         node: {
 128 |           name: 'New Code Node',
 129 |           type: 'n8n-nodes-base.code',
 130 |           position: [800, 300],
 131 |           typeVersion: 2,
 132 |           parameters: {
 133 |             mode: 'runOnceForAllItems',
 134 |             language: 'javaScript',
 135 |             jsCode: 'return items;'
 136 |           }
 137 |         }
 138 |       };
 139 | 
 140 |       const request: WorkflowDiffRequest = {
 141 |         id: 'test-workflow',
 142 |         operations: [operation]
 143 |       };
 144 | 
 145 |       const result = await diffEngine.applyDiff(baseWorkflow, request);
 146 |       
 147 |       expect(result.success).toBe(true);
 148 |       expect(result.workflow!.nodes).toHaveLength(4);
 149 |       expect(result.workflow!.nodes[3].name).toBe('New Code Node');
 150 |       expect(result.workflow!.nodes[3].type).toBe('n8n-nodes-base.code');
 151 |       expect(result.workflow!.nodes[3].id).toBeDefined();
 152 |     });
 153 | 
 154 |     it('should reject duplicate node names', async () => {
 155 |       const operation: AddNodeOperation = {
 156 |         type: 'addNode',
 157 |         node: {
 158 |           name: 'Webhook', // Duplicate name
 159 |           type: 'n8n-nodes-base.webhook',
 160 |           position: [800, 300]
 161 |         }
 162 |       };
 163 | 
 164 |       const request: WorkflowDiffRequest = {
 165 |         id: 'test-workflow',
 166 |         operations: [operation]
 167 |       };
 168 | 
 169 |       const result = await diffEngine.applyDiff(baseWorkflow, request);
 170 |       
 171 |       expect(result.success).toBe(false);
 172 |       expect(result.errors![0].message).toContain('already exists');
 173 |     });
 174 | 
 175 |     it('should reject invalid node type format', async () => {
 176 |       const operation: AddNodeOperation = {
 177 |         type: 'addNode',
 178 |         node: {
 179 |           name: 'Invalid Node',
 180 |           type: 'webhook', // Missing package prefix
 181 |           position: [800, 300]
 182 |         }
 183 |       };
 184 | 
 185 |       const request: WorkflowDiffRequest = {
 186 |         id: 'test-workflow',
 187 |         operations: [operation]
 188 |       };
 189 | 
 190 |       const result = await diffEngine.applyDiff(baseWorkflow, request);
 191 |       
 192 |       expect(result.success).toBe(false);
 193 |       expect(result.errors![0].message).toContain('Invalid node type');
 194 |     });
 195 | 
 196 |     it('should correct nodes-base prefix to n8n-nodes-base', async () => {
 197 |       const operation: AddNodeOperation = {
 198 |         type: 'addNode',
 199 |         node: {
 200 |           name: 'Test Node',
 201 |           type: 'nodes-base.webhook', // Wrong prefix
 202 |           position: [800, 300]
 203 |         }
 204 |       };
 205 | 
 206 |       const request: WorkflowDiffRequest = {
 207 |         id: 'test-workflow',
 208 |         operations: [operation]
 209 |       };
 210 | 
 211 |       const result = await diffEngine.applyDiff(baseWorkflow, request);
 212 |       
 213 |       expect(result.success).toBe(false);
 214 |       expect(result.errors![0].message).toContain('Use "n8n-nodes-base.');
 215 |     });
 216 | 
 217 |     it('should generate node ID if not provided', async () => {
 218 |       const operation: AddNodeOperation = {
 219 |         type: 'addNode',
 220 |         node: {
 221 |           name: 'No ID Node',
 222 |           type: 'n8n-nodes-base.code',
 223 |           position: [800, 300]
 224 |         }
 225 |       };
 226 | 
 227 |       const request: WorkflowDiffRequest = {
 228 |         id: 'test-workflow',
 229 |         operations: [operation]
 230 |       };
 231 | 
 232 |       const result = await diffEngine.applyDiff(baseWorkflow, request);
 233 |       
 234 |       expect(result.success).toBe(true);
 235 |       expect(result.workflow!.nodes[3].id).toBeDefined();
 236 |       expect(result.workflow!.nodes[3].id).toMatch(/^[0-9a-f-]+$/);
 237 |     });
 238 |   });
 239 | 
 240 |   describe('RemoveNode Operation', () => {
 241 |     it('should remove node by ID', async () => {
 242 |       const operation: RemoveNodeOperation = {
 243 |         type: 'removeNode',
 244 |         nodeId: 'http-1'
 245 |       };
 246 | 
 247 |       const request: WorkflowDiffRequest = {
 248 |         id: 'test-workflow',
 249 |         operations: [operation]
 250 |       };
 251 | 
 252 |       const result = await diffEngine.applyDiff(baseWorkflow, request);
 253 |       
 254 |       expect(result.success).toBe(true);
 255 |       expect(result.workflow!.nodes).toHaveLength(2);
 256 |       expect(result.workflow!.nodes.find((n: any) => n.id === 'http-1')).toBeUndefined();
 257 |     });
 258 | 
 259 |     it('should remove node by name', async () => {
 260 |       const operation: RemoveNodeOperation = {
 261 |         type: 'removeNode',
 262 |         nodeName: 'HTTP Request'
 263 |       };
 264 | 
 265 |       const request: WorkflowDiffRequest = {
 266 |         id: 'test-workflow',
 267 |         operations: [operation]
 268 |       };
 269 | 
 270 |       const result = await diffEngine.applyDiff(baseWorkflow, request);
 271 |       
 272 |       expect(result.success).toBe(true);
 273 |       expect(result.workflow!.nodes).toHaveLength(2);
 274 |       expect(result.workflow!.nodes.find((n: any) => n.name === 'HTTP Request')).toBeUndefined();
 275 |     });
 276 | 
 277 |     it('should clean up connections when removing node', async () => {
 278 |       const operation: RemoveNodeOperation = {
 279 |         type: 'removeNode',
 280 |         nodeId: 'http-1'
 281 |       };
 282 | 
 283 |       const request: WorkflowDiffRequest = {
 284 |         id: 'test-workflow',
 285 |         operations: [operation]
 286 |       };
 287 | 
 288 |       const result = await diffEngine.applyDiff(baseWorkflow, request);
 289 |       
 290 |       expect(result.success).toBe(true);
 291 |       expect(result.workflow!.connections['HTTP Request']).toBeUndefined();
 292 |       // Check that connections from Webhook were cleaned up
 293 |       if (result.workflow!.connections['Webhook'] && result.workflow!.connections['Webhook'].main && result.workflow!.connections['Webhook'].main[0]) {
 294 |         expect(result.workflow!.connections['Webhook'].main[0]).toHaveLength(0);
 295 |       } else {
 296 |         // Webhook connections should be cleaned up entirely
 297 |         expect(result.workflow!.connections['Webhook']).toBeUndefined();
 298 |       }
 299 |     });
 300 | 
 301 |     it('should reject removing non-existent node', async () => {
 302 |       const operation: RemoveNodeOperation = {
 303 |         type: 'removeNode',
 304 |         nodeId: 'non-existent'
 305 |       };
 306 | 
 307 |       const request: WorkflowDiffRequest = {
 308 |         id: 'test-workflow',
 309 |         operations: [operation]
 310 |       };
 311 | 
 312 |       const result = await diffEngine.applyDiff(baseWorkflow, request);
 313 |       
 314 |       expect(result.success).toBe(false);
 315 |       expect(result.errors![0].message).toContain('Node not found');
 316 |     });
 317 |   });
 318 | 
 319 |   describe('UpdateNode Operation', () => {
 320 |     it('should update node parameters', async () => {
 321 |       const operation: UpdateNodeOperation = {
 322 |         type: 'updateNode',
 323 |         nodeId: 'http-1',
 324 |         updates: {
 325 |           'parameters.method': 'POST',
 326 |           'parameters.url': 'https://new-api.example.com'
 327 |         }
 328 |       };
 329 | 
 330 |       const request: WorkflowDiffRequest = {
 331 |         id: 'test-workflow',
 332 |         operations: [operation]
 333 |       };
 334 | 
 335 |       const result = await diffEngine.applyDiff(baseWorkflow, request);
 336 |       
 337 |       expect(result.success).toBe(true);
 338 |       const updatedNode = result.workflow!.nodes.find((n: any) => n.id === 'http-1');
 339 |       expect(updatedNode!.parameters.method).toBe('POST');
 340 |       expect(updatedNode!.parameters.url).toBe('https://new-api.example.com');
 341 |     });
 342 | 
 343 |     it('should update nested properties using dot notation', async () => {
 344 |       const operation: UpdateNodeOperation = {
 345 |         type: 'updateNode',
 346 |         nodeName: 'Slack',
 347 |         updates: {
 348 |           'parameters.resource': 'channel',
 349 |           'parameters.operation': 'create',
 350 |           'credentials.slackApi.name': 'New Slack Account'
 351 |         }
 352 |       };
 353 | 
 354 |       const request: WorkflowDiffRequest = {
 355 |         id: 'test-workflow',
 356 |         operations: [operation]
 357 |       };
 358 | 
 359 |       const result = await diffEngine.applyDiff(baseWorkflow, request);
 360 |       
 361 |       expect(result.success).toBe(true);
 362 |       const updatedNode = result.workflow!.nodes.find((n: any) => n.name === 'Slack');
 363 |       expect(updatedNode!.parameters.resource).toBe('channel');
 364 |       expect(updatedNode!.parameters.operation).toBe('create');
 365 |       expect((updatedNode!.credentials as any).slackApi.name).toBe('New Slack Account');
 366 |     });
 367 | 
 368 |     it('should reject updating non-existent node', async () => {
 369 |       const operation: UpdateNodeOperation = {
 370 |         type: 'updateNode',
 371 |         nodeId: 'non-existent',
 372 |         updates: {
 373 |           'parameters.test': 'value'
 374 |         }
 375 |       };
 376 | 
 377 |       const request: WorkflowDiffRequest = {
 378 |         id: 'test-workflow',
 379 |         operations: [operation]
 380 |       };
 381 | 
 382 |       const result = await diffEngine.applyDiff(baseWorkflow, request);
 383 |       
 384 |       expect(result.success).toBe(false);
 385 |       expect(result.errors![0].message).toContain('Node not found');
 386 |     });
 387 |   });
 388 | 
 389 |   describe('MoveNode Operation', () => {
 390 |     it('should move node to new position', async () => {
 391 |       const operation: MoveNodeOperation = {
 392 |         type: 'moveNode',
 393 |         nodeId: 'http-1',
 394 |         position: [1000, 500]
 395 |       };
 396 | 
 397 |       const request: WorkflowDiffRequest = {
 398 |         id: 'test-workflow',
 399 |         operations: [operation]
 400 |       };
 401 | 
 402 |       const result = await diffEngine.applyDiff(baseWorkflow, request);
 403 |       
 404 |       expect(result.success).toBe(true);
 405 |       const movedNode = result.workflow!.nodes.find((n: any) => n.id === 'http-1');
 406 |       expect(movedNode!.position).toEqual([1000, 500]);
 407 |     });
 408 | 
 409 |     it('should move node by name', async () => {
 410 |       const operation: MoveNodeOperation = {
 411 |         type: 'moveNode',
 412 |         nodeName: 'Webhook',
 413 |         position: [100, 100]
 414 |       };
 415 | 
 416 |       const request: WorkflowDiffRequest = {
 417 |         id: 'test-workflow',
 418 |         operations: [operation]
 419 |       };
 420 | 
 421 |       const result = await diffEngine.applyDiff(baseWorkflow, request);
 422 |       
 423 |       expect(result.success).toBe(true);
 424 |       const movedNode = result.workflow!.nodes.find((n: any) => n.name === 'Webhook');
 425 |       expect(movedNode!.position).toEqual([100, 100]);
 426 |     });
 427 |   });
 428 | 
 429 |   describe('Enable/Disable Node Operations', () => {
 430 |     it('should disable a node', async () => {
 431 |       const operation: DisableNodeOperation = {
 432 |         type: 'disableNode',
 433 |         nodeId: 'http-1'
 434 |       };
 435 | 
 436 |       const request: WorkflowDiffRequest = {
 437 |         id: 'test-workflow',
 438 |         operations: [operation]
 439 |       };
 440 | 
 441 |       const result = await diffEngine.applyDiff(baseWorkflow, request);
 442 |       
 443 |       expect(result.success).toBe(true);
 444 |       const disabledNode = result.workflow!.nodes.find((n: any) => n.id === 'http-1');
 445 |       expect(disabledNode!.disabled).toBe(true);
 446 |     });
 447 | 
 448 |     it('should enable a disabled node', async () => {
 449 |       // First disable the node
 450 |       baseWorkflow.nodes[1].disabled = true;
 451 | 
 452 |       const operation: EnableNodeOperation = {
 453 |         type: 'enableNode',
 454 |         nodeId: 'http-1'
 455 |       };
 456 | 
 457 |       const request: WorkflowDiffRequest = {
 458 |         id: 'test-workflow',
 459 |         operations: [operation]
 460 |       };
 461 | 
 462 |       const result = await diffEngine.applyDiff(baseWorkflow, request);
 463 |       
 464 |       expect(result.success).toBe(true);
 465 |       const enabledNode = result.workflow!.nodes.find((n: any) => n.id === 'http-1');
 466 |       expect(enabledNode!.disabled).toBe(false);
 467 |     });
 468 |   });
 469 | 
 470 |   describe('AddConnection Operation', () => {
 471 |     it('should add a new connection', async () => {
 472 |       // First add a new node to connect to
 473 |       const addNodeOp: AddNodeOperation = {
 474 |         type: 'addNode',
 475 |         node: {
 476 |           name: 'Code',
 477 |           type: 'n8n-nodes-base.code',
 478 |           position: [1000, 300]
 479 |         }
 480 |       };
 481 | 
 482 |       const addConnectionOp: AddConnectionOperation = {
 483 |         type: 'addConnection',
 484 |         source: 'slack-1',
 485 |         target: 'Code'
 486 |       };
 487 | 
 488 |       const request: WorkflowDiffRequest = {
 489 |         id: 'test-workflow',
 490 |         operations: [addNodeOp, addConnectionOp]
 491 |       };
 492 | 
 493 |       const result = await diffEngine.applyDiff(baseWorkflow, request);
 494 |       
 495 |       expect(result.success).toBe(true);
 496 |       expect(result.workflow!.connections['Slack']).toBeDefined();
 497 |       expect(result.workflow!.connections['Slack'].main[0]).toHaveLength(1);
 498 |       expect(result.workflow!.connections['Slack'].main[0][0].node).toBe('Code');
 499 |     });
 500 | 
 501 |     it('should reject duplicate connections', async () => {
 502 |       const operation: AddConnectionOperation = {
 503 |         type: 'addConnection',
 504 |         source: 'Webhook',  // Use node name not ID
 505 |         target: 'HTTP Request'  // Use node name not ID
 506 |       };
 507 | 
 508 |       const request: WorkflowDiffRequest = {
 509 |         id: 'test-workflow',
 510 |         operations: [operation]
 511 |       };
 512 | 
 513 |       const result = await diffEngine.applyDiff(baseWorkflow, request);
 514 |       
 515 |       expect(result.success).toBe(false);
 516 |       expect(result.errors![0].message).toContain('Connection already exists');
 517 |     });
 518 | 
 519 |     it('should reject connection to non-existent source node', async () => {
 520 |       const operation: AddConnectionOperation = {
 521 |         type: 'addConnection',
 522 |         source: 'non-existent',
 523 |         target: 'http-1'
 524 |       };
 525 | 
 526 |       const request: WorkflowDiffRequest = {
 527 |         id: 'test-workflow',
 528 |         operations: [operation]
 529 |       };
 530 | 
 531 |       const result = await diffEngine.applyDiff(baseWorkflow, request);
 532 |       
 533 |       expect(result.success).toBe(false);
 534 |       expect(result.errors![0].message).toContain('Source node not found');
 535 |     });
 536 | 
 537 |     it('should reject connection to non-existent target node', async () => {
 538 |       const operation: AddConnectionOperation = {
 539 |         type: 'addConnection',
 540 |         source: 'webhook-1',
 541 |         target: 'non-existent'
 542 |       };
 543 | 
 544 |       const request: WorkflowDiffRequest = {
 545 |         id: 'test-workflow',
 546 |         operations: [operation]
 547 |       };
 548 | 
 549 |       const result = await diffEngine.applyDiff(baseWorkflow, request);
 550 |       
 551 |       expect(result.success).toBe(false);
 552 |       expect(result.errors![0].message).toContain('Target node not found');
 553 |     });
 554 | 
 555 |     it('should support custom output and input types', async () => {
 556 |       // Add an IF node that has multiple outputs
 557 |       const addNodeOp: AddNodeOperation = {
 558 |         type: 'addNode',
 559 |         node: {
 560 |           name: 'IF',
 561 |           type: 'n8n-nodes-base.if',
 562 |           position: [600, 400]
 563 |         }
 564 |       };
 565 | 
 566 |       const addConnectionOp: AddConnectionOperation = {
 567 |         type: 'addConnection',
 568 |         source: 'IF',
 569 |         target: 'slack-1',
 570 |         sourceOutput: 'false',
 571 |         targetInput: 'main',
 572 |         sourceIndex: 0,
 573 |         targetIndex: 0
 574 |       };
 575 | 
 576 |       const request: WorkflowDiffRequest = {
 577 |         id: 'test-workflow',
 578 |         operations: [addNodeOp, addConnectionOp]
 579 |       };
 580 | 
 581 |       const result = await diffEngine.applyDiff(baseWorkflow, request);
 582 | 
 583 |       expect(result.success).toBe(true);
 584 |       expect(result.workflow!.connections['IF'].false).toBeDefined();
 585 |       expect(result.workflow!.connections['IF'].false[0][0].node).toBe('Slack');
 586 |     });
 587 | 
 588 |     it('should reject addConnection with wrong parameter sourceNodeId instead of source (Issue #249)', async () => {
 589 |       const operation: any = {
 590 |         type: 'addConnection',
 591 |         sourceNodeId: 'webhook-1', // Wrong parameter name!
 592 |         target: 'http-1'
 593 |       };
 594 | 
 595 |       const request: WorkflowDiffRequest = {
 596 |         id: 'test-workflow',
 597 |         operations: [operation]
 598 |       };
 599 | 
 600 |       const result = await diffEngine.applyDiff(baseWorkflow, request);
 601 | 
 602 |       expect(result.success).toBe(false);
 603 |       expect(result.errors![0].message).toContain('Invalid parameter(s): sourceNodeId');
 604 |       expect(result.errors![0].message).toContain("Use 'source' and 'target' instead");
 605 |     });
 606 | 
 607 |     it('should reject addConnection with wrong parameter targetNodeId instead of target (Issue #249)', async () => {
 608 |       const operation: any = {
 609 |         type: 'addConnection',
 610 |         source: 'webhook-1',
 611 |         targetNodeId: 'http-1' // Wrong parameter name!
 612 |       };
 613 | 
 614 |       const request: WorkflowDiffRequest = {
 615 |         id: 'test-workflow',
 616 |         operations: [operation]
 617 |       };
 618 | 
 619 |       const result = await diffEngine.applyDiff(baseWorkflow, request);
 620 | 
 621 |       expect(result.success).toBe(false);
 622 |       expect(result.errors![0].message).toContain('Invalid parameter(s): targetNodeId');
 623 |       expect(result.errors![0].message).toContain("Use 'source' and 'target' instead");
 624 |     });
 625 | 
 626 |     it('should reject addConnection with both wrong parameters (Issue #249)', async () => {
 627 |       const operation: any = {
 628 |         type: 'addConnection',
 629 |         sourceNodeId: 'webhook-1', // Wrong!
 630 |         targetNodeId: 'http-1' // Wrong!
 631 |       };
 632 | 
 633 |       const request: WorkflowDiffRequest = {
 634 |         id: 'test-workflow',
 635 |         operations: [operation]
 636 |       };
 637 | 
 638 |       const result = await diffEngine.applyDiff(baseWorkflow, request);
 639 | 
 640 |       expect(result.success).toBe(false);
 641 |       expect(result.errors![0].message).toContain('Invalid parameter(s): sourceNodeId, targetNodeId');
 642 |       expect(result.errors![0].message).toContain("Use 'source' and 'target' instead");
 643 |     });
 644 | 
 645 |     it('should show helpful error with available nodes when source is missing (Issue #249)', async () => {
 646 |       const operation: any = {
 647 |         type: 'addConnection',
 648 |         // source is missing entirely
 649 |         target: 'http-1'
 650 |       };
 651 | 
 652 |       const request: WorkflowDiffRequest = {
 653 |         id: 'test-workflow',
 654 |         operations: [operation]
 655 |       };
 656 | 
 657 |       const result = await diffEngine.applyDiff(baseWorkflow, request);
 658 | 
 659 |       expect(result.success).toBe(false);
 660 |       expect(result.errors![0].message).toContain("Missing required parameter 'source'");
 661 |       expect(result.errors![0].message).toContain("not 'sourceNodeId'");
 662 |     });
 663 | 
 664 |     it('should show helpful error with available nodes when target is missing (Issue #249)', async () => {
 665 |       const operation: any = {
 666 |         type: 'addConnection',
 667 |         source: 'webhook-1',
 668 |         // target is missing entirely
 669 |       };
 670 | 
 671 |       const request: WorkflowDiffRequest = {
 672 |         id: 'test-workflow',
 673 |         operations: [operation]
 674 |       };
 675 | 
 676 |       const result = await diffEngine.applyDiff(baseWorkflow, request);
 677 | 
 678 |       expect(result.success).toBe(false);
 679 |       expect(result.errors![0].message).toContain("Missing required parameter 'target'");
 680 |       expect(result.errors![0].message).toContain("not 'targetNodeId'");
 681 |     });
 682 | 
 683 |     it('should list available nodes when source node not found (Issue #249)', async () => {
 684 |       const operation: AddConnectionOperation = {
 685 |         type: 'addConnection',
 686 |         source: 'non-existent-node',
 687 |         target: 'http-1'
 688 |       };
 689 | 
 690 |       const request: WorkflowDiffRequest = {
 691 |         id: 'test-workflow',
 692 |         operations: [operation]
 693 |       };
 694 | 
 695 |       const result = await diffEngine.applyDiff(baseWorkflow, request);
 696 | 
 697 |       expect(result.success).toBe(false);
 698 |       expect(result.errors![0].message).toContain('Source node not found: "non-existent-node"');
 699 |       expect(result.errors![0].message).toContain('Available nodes:');
 700 |       expect(result.errors![0].message).toContain('Webhook');
 701 |       expect(result.errors![0].message).toContain('HTTP Request');
 702 |       expect(result.errors![0].message).toContain('Slack');
 703 |     });
 704 | 
 705 |     it('should list available nodes when target node not found (Issue #249)', async () => {
 706 |       const operation: AddConnectionOperation = {
 707 |         type: 'addConnection',
 708 |         source: 'webhook-1',
 709 |         target: 'non-existent-node'
 710 |       };
 711 | 
 712 |       const request: WorkflowDiffRequest = {
 713 |         id: 'test-workflow',
 714 |         operations: [operation]
 715 |       };
 716 | 
 717 |       const result = await diffEngine.applyDiff(baseWorkflow, request);
 718 | 
 719 |       expect(result.success).toBe(false);
 720 |       expect(result.errors![0].message).toContain('Target node not found: "non-existent-node"');
 721 |       expect(result.errors![0].message).toContain('Available nodes:');
 722 |       expect(result.errors![0].message).toContain('Webhook');
 723 |       expect(result.errors![0].message).toContain('HTTP Request');
 724 |       expect(result.errors![0].message).toContain('Slack');
 725 |     });
 726 |   });
 727 | 
 728 |   describe('RemoveConnection Operation', () => {
 729 |     it('should remove an existing connection', async () => {
 730 |       const operation: RemoveConnectionOperation = {
 731 |         type: 'removeConnection',
 732 |         source: 'Webhook',  // Use node name
 733 |         target: 'HTTP Request'  // Use node name
 734 |       };
 735 | 
 736 |       const request: WorkflowDiffRequest = {
 737 |         id: 'test-workflow',
 738 |         operations: [operation]
 739 |       };
 740 | 
 741 |       const result = await diffEngine.applyDiff(baseWorkflow, request);
 742 |       
 743 |       expect(result.success).toBe(true);
 744 |       // After removing the connection, the array should be empty or cleaned up
 745 |       if (result.workflow!.connections['Webhook']) {
 746 |         if (result.workflow!.connections['Webhook'].main && result.workflow!.connections['Webhook'].main.length > 0) {
 747 |           expect(result.workflow!.connections['Webhook'].main[0]).toHaveLength(0);
 748 |         } else {
 749 |           expect(result.workflow!.connections['Webhook'].main).toHaveLength(0);
 750 |         }
 751 |       } else {
 752 |         // Connection was cleaned up entirely
 753 |         expect(result.workflow!.connections['Webhook']).toBeUndefined();
 754 |       }
 755 |     });
 756 | 
 757 |     it('should reject removing non-existent connection', async () => {
 758 |       const operation: RemoveConnectionOperation = {
 759 |         type: 'removeConnection',
 760 |         source: 'Slack',  // Use node name
 761 |         target: 'Webhook'  // Use node name
 762 |       };
 763 | 
 764 |       const request: WorkflowDiffRequest = {
 765 |         id: 'test-workflow',
 766 |         operations: [operation]
 767 |       };
 768 | 
 769 |       const result = await diffEngine.applyDiff(baseWorkflow, request);
 770 |       
 771 |       expect(result.success).toBe(false);
 772 |       expect(result.errors![0].message).toContain('No connections found');
 773 |     });
 774 |   });
 775 | 
 776 | 
 777 |   describe('RewireConnection Operation (Phase 1)', () => {
 778 |     it('should rewire connection from one target to another', async () => {
 779 |       // Setup: Create a connection Webhook → HTTP Request
 780 |       // Then rewire it to Webhook → Slack instead
 781 |       const rewireOp: any = {
 782 |         type: 'rewireConnection',
 783 |         source: 'Webhook',
 784 |         from: 'HTTP Request',
 785 |         to: 'Slack'
 786 |       };
 787 | 
 788 |       const request: WorkflowDiffRequest = {
 789 |         id: 'test-workflow',
 790 |         operations: [rewireOp]
 791 |       };
 792 | 
 793 |       const result = await diffEngine.applyDiff(baseWorkflow, request);
 794 | 
 795 |       expect(result.success).toBe(true);
 796 |       expect(result.workflow).toBeDefined();
 797 | 
 798 |       // Old connection should be removed
 799 |       const webhookConnections = result.workflow!.connections['Webhook']['main'][0];
 800 |       expect(webhookConnections.some((c: any) => c.node === 'HTTP Request')).toBe(false);
 801 | 
 802 |       // New connection should exist
 803 |       expect(webhookConnections.some((c: any) => c.node === 'Slack')).toBe(true);
 804 |     });
 805 | 
 806 |     it('should rewire connection with specified sourceOutput', async () => {
 807 |       // Add IF node with connection on 'true' output
 808 |       const addNode: AddNodeOperation = {
 809 |         type: 'addNode',
 810 |         node: {
 811 |           name: 'IF',
 812 |           type: 'n8n-nodes-base.if',
 813 |           position: [600, 300]
 814 |         }
 815 |       };
 816 | 
 817 |       const addConn: AddConnectionOperation = {
 818 |         type: 'addConnection',
 819 |         source: 'IF',
 820 |         target: 'HTTP Request',
 821 |         sourceOutput: 'true'
 822 |       };
 823 | 
 824 |       const rewire: any = {
 825 |         type: 'rewireConnection',
 826 |         source: 'IF',
 827 |         from: 'HTTP Request',
 828 |         to: 'Slack',
 829 |         sourceOutput: 'true'
 830 |       };
 831 | 
 832 |       const request: WorkflowDiffRequest = {
 833 |         id: 'test-workflow',
 834 |         operations: [addNode, addConn, rewire]
 835 |       };
 836 | 
 837 |       const result = await diffEngine.applyDiff(baseWorkflow, request);
 838 | 
 839 |       expect(result.success).toBe(true);
 840 | 
 841 |       // Verify rewiring on 'true' output
 842 |       const trueConnections = result.workflow!.connections['IF']['true'][0];
 843 |       expect(trueConnections.some((c: any) => c.node === 'HTTP Request')).toBe(false);
 844 |       expect(trueConnections.some((c: any) => c.node === 'Slack')).toBe(true);
 845 |     });
 846 | 
 847 |     it('should preserve other parallel connections when rewiring', async () => {
 848 |       // Setup: Webhook connects to both HTTP Request (in baseWorkflow) and Slack (added here)
 849 |       // Add a Set node, then rewire HTTP Request → Set
 850 |       // Slack connection should remain unchanged
 851 | 
 852 |       // Add Slack connection in parallel
 853 |       const addSlackConn: AddConnectionOperation = {
 854 |         type: 'addConnection',
 855 |         source: 'Webhook',
 856 |         target: 'Slack'
 857 |       };
 858 | 
 859 |       // Add Set node to rewire to
 860 |       const addSetNode: AddNodeOperation = {
 861 |         type: 'addNode',
 862 |         node: {
 863 |           name: 'Set',
 864 |           type: 'n8n-nodes-base.set',
 865 |           position: [800, 300]
 866 |         }
 867 |       };
 868 | 
 869 |       // Rewire HTTP Request → Set
 870 |       const rewire: any = {
 871 |         type: 'rewireConnection',
 872 |         source: 'Webhook',
 873 |         from: 'HTTP Request',
 874 |         to: 'Set'
 875 |       };
 876 | 
 877 |       const request: WorkflowDiffRequest = {
 878 |         id: 'test-workflow',
 879 |         operations: [addSlackConn, addSetNode, rewire]
 880 |       };
 881 | 
 882 |       const result = await diffEngine.applyDiff(baseWorkflow, request);
 883 | 
 884 |       expect(result.success).toBe(true);
 885 | 
 886 |       const webhookConnections = result.workflow!.connections['Webhook']['main'][0];
 887 | 
 888 |       // HTTP Request should be removed
 889 |       expect(webhookConnections.some((c: any) => c.node === 'HTTP Request')).toBe(false);
 890 | 
 891 |       // Set should be added
 892 |       expect(webhookConnections.some((c: any) => c.node === 'Set')).toBe(true);
 893 | 
 894 |       // Slack should still be there (parallel connection preserved)
 895 |       expect(webhookConnections.some((c: any) => c.node === 'Slack')).toBe(true);
 896 |     });
 897 | 
 898 |     it('should reject rewireConnection when source node not found', async () => {
 899 |       const rewire: any = {
 900 |         type: 'rewireConnection',
 901 |         source: 'NonExistent',
 902 |         from: 'HTTP Request',
 903 |         to: 'Slack'
 904 |       };
 905 | 
 906 |       const request: WorkflowDiffRequest = {
 907 |         id: 'test-workflow',
 908 |         operations: [rewire]
 909 |       };
 910 | 
 911 |       const result = await diffEngine.applyDiff(baseWorkflow, request);
 912 | 
 913 |       expect(result.success).toBe(false);
 914 |       expect(result.errors).toBeDefined();
 915 |       expect(result.errors![0].message).toContain('Source node not found');
 916 |       expect(result.errors![0].message).toContain('NonExistent');
 917 |       expect(result.errors![0].message).toContain('Available nodes');
 918 |     });
 919 | 
 920 |     it('should reject rewireConnection when "from" node not found', async () => {
 921 |       const rewire: any = {
 922 |         type: 'rewireConnection',
 923 |         source: 'Webhook',
 924 |         from: 'NonExistent',
 925 |         to: 'Slack'
 926 |       };
 927 | 
 928 |       const request: WorkflowDiffRequest = {
 929 |         id: 'test-workflow',
 930 |         operations: [rewire]
 931 |       };
 932 | 
 933 |       const result = await diffEngine.applyDiff(baseWorkflow, request);
 934 | 
 935 |       expect(result.success).toBe(false);
 936 |       expect(result.errors).toBeDefined();
 937 |       expect(result.errors![0].message).toContain('"From" node not found');
 938 |       expect(result.errors![0].message).toContain('NonExistent');
 939 |     });
 940 | 
 941 |     it('should reject rewireConnection when "to" node not found', async () => {
 942 |       const rewire: any = {
 943 |         type: 'rewireConnection',
 944 |         source: 'Webhook',
 945 |         from: 'HTTP Request',
 946 |         to: 'NonExistent'
 947 |       };
 948 | 
 949 |       const request: WorkflowDiffRequest = {
 950 |         id: 'test-workflow',
 951 |         operations: [rewire]
 952 |       };
 953 | 
 954 |       const result = await diffEngine.applyDiff(baseWorkflow, request);
 955 | 
 956 |       expect(result.success).toBe(false);
 957 |       expect(result.errors).toBeDefined();
 958 |       expect(result.errors![0].message).toContain('"To" node not found');
 959 |       expect(result.errors![0].message).toContain('NonExistent');
 960 |     });
 961 | 
 962 |     it('should reject rewireConnection when connection does not exist', async () => {
 963 |       // Slack node exists but doesn't have any outgoing connections
 964 |       // So this should fail with "No connections found" error
 965 |       const rewire: any = {
 966 |         type: 'rewireConnection',
 967 |         source: 'Slack',  // Slack has no outgoing connections in baseWorkflow
 968 |         from: 'HTTP Request',
 969 |         to: 'Webhook'  // Use existing node
 970 |       };
 971 | 
 972 |       const request: WorkflowDiffRequest = {
 973 |         id: 'test-workflow',
 974 |         operations: [rewire]
 975 |       };
 976 | 
 977 |       const result = await diffEngine.applyDiff(baseWorkflow, request);
 978 | 
 979 |       expect(result.success).toBe(false);
 980 |       expect(result.errors).toBeDefined();
 981 |       expect(result.errors![0].message).toContain('No connections found from');
 982 |       expect(result.errors![0].message).toContain('Slack');
 983 |     });
 984 | 
 985 |     it('should handle rewiring IF node branches correctly', async () => {
 986 |       // Add IF node with true/false branches
 987 |       const addIF: AddNodeOperation = {
 988 |         type: 'addNode',
 989 |         node: {
 990 |           name: 'IF',
 991 |           type: 'n8n-nodes-base.if',
 992 |           position: [600, 300]
 993 |         }
 994 |       };
 995 | 
 996 |       const addSuccess: AddNodeOperation = {
 997 |         type: 'addNode',
 998 |         node: {
 999 |           name: 'SuccessHandler',
1000 |           type: 'n8n-nodes-base.set',
1001 |           position: [800, 200]
1002 |         }
1003 |       };
1004 | 
1005 |       const addError: AddNodeOperation = {
1006 |         type: 'addNode',
1007 |         node: {
1008 |           name: 'ErrorHandler',
1009 |           type: 'n8n-nodes-base.set',
1010 |           position: [800, 400]
1011 |         }
1012 |       };
1013 | 
1014 |       const connectTrue: AddConnectionOperation = {
1015 |         type: 'addConnection',
1016 |         source: 'IF',
1017 |         target: 'SuccessHandler',
1018 |         sourceOutput: 'true'
1019 |       };
1020 | 
1021 |       const connectFalse: AddConnectionOperation = {
1022 |         type: 'addConnection',
1023 |         source: 'IF',
1024 |         target: 'ErrorHandler',
1025 |         sourceOutput: 'false'
1026 |       };
1027 | 
1028 |       // Rewire the false branch to go to SuccessHandler instead
1029 |       const rewireFalse: any = {
1030 |         type: 'rewireConnection',
1031 |         source: 'IF',
1032 |         from: 'ErrorHandler',
1033 |         to: 'Slack',
1034 |         sourceOutput: 'false'
1035 |       };
1036 | 
1037 |       const request: WorkflowDiffRequest = {
1038 |         id: 'test-workflow',
1039 |         operations: [addIF, addSuccess, addError, connectTrue, connectFalse, rewireFalse]
1040 |       };
1041 | 
1042 |       const result = await diffEngine.applyDiff(baseWorkflow, request);
1043 | 
1044 |       expect(result.success).toBe(true);
1045 | 
1046 |       // True branch should still point to SuccessHandler
1047 |       expect(result.workflow!.connections['IF']['true'][0][0].node).toBe('SuccessHandler');
1048 | 
1049 |       // False branch should now point to Slack
1050 |       expect(result.workflow!.connections['IF']['false'][0][0].node).toBe('Slack');
1051 |     });
1052 |   });
1053 | 
1054 |   describe('Smart Parameters (Phase 1)', () => {
1055 |     it('should use branch="true" for IF node connections', async () => {
1056 |       // Add IF node
1057 |       const addIF: any = {
1058 |         type: 'addNode',
1059 |         node: {
1060 |           name: 'IF',
1061 |           type: 'n8n-nodes-base.if',
1062 |           position: [400, 300]
1063 |         }
1064 |       };
1065 | 
1066 |       // Add TrueHandler node (use unique name)
1067 |       const addTrueHandler: any = {
1068 |         type: 'addNode',
1069 |         node: {
1070 |           name: 'TrueHandler',
1071 |           type: 'n8n-nodes-base.set',
1072 |           position: [600, 300]
1073 |         }
1074 |       };
1075 | 
1076 |       // Connect IF to TrueHandler using smart branch parameter
1077 |       const connectWithBranch: any = {
1078 |         type: 'addConnection',
1079 |         source: 'IF',
1080 |         target: 'TrueHandler',
1081 |         branch: 'true'  // Smart parameter instead of sourceOutput: 'true'
1082 |       };
1083 | 
1084 |       const request: WorkflowDiffRequest = {
1085 |         id: 'test-workflow',
1086 |         operations: [addIF, addTrueHandler, connectWithBranch]
1087 |       };
1088 | 
1089 |       const result = await diffEngine.applyDiff(baseWorkflow, request);
1090 | 
1091 |       expect(result.success).toBe(true);
1092 |       expect(result.workflow).toBeDefined();
1093 | 
1094 |       // Should create connection on 'main' output, index 0 (true branch)
1095 |       expect(result.workflow!.connections['IF']['main']).toBeDefined();
1096 |       expect(result.workflow!.connections['IF']['main'][0]).toBeDefined();
1097 |       expect(result.workflow!.connections['IF']['main'][0][0].node).toBe('TrueHandler');
1098 |     });
1099 | 
1100 |     it('should use branch="false" for IF node connections', async () => {
1101 |       const addIF: any = {
1102 |         type: 'addNode',
1103 |         node: {
1104 |           name: 'IF',
1105 |           type: 'n8n-nodes-base.if',
1106 |           position: [400, 300]
1107 |         }
1108 |       };
1109 | 
1110 |       const addFalseHandler: any = {
1111 |         type: 'addNode',
1112 |         node: {
1113 |           name: 'FalseHandler',
1114 |           type: 'n8n-nodes-base.set',
1115 |           position: [600, 300]
1116 |         }
1117 |       };
1118 | 
1119 |       const connectWithBranch: any = {
1120 |         type: 'addConnection',
1121 |         source: 'IF',
1122 |         target: 'FalseHandler',
1123 |         branch: 'false'  // Smart parameter for false branch
1124 |       };
1125 | 
1126 |       const request: WorkflowDiffRequest = {
1127 |         id: 'test-workflow',
1128 |         operations: [addIF, addFalseHandler, connectWithBranch]
1129 |       };
1130 | 
1131 |       const result = await diffEngine.applyDiff(baseWorkflow, request);
1132 | 
1133 |       expect(result.success).toBe(true);
1134 | 
1135 |       // Should create connection on 'main' output, index 1 (false branch)
1136 |       expect(result.workflow!.connections['IF']['main']).toBeDefined();
1137 |       expect(result.workflow!.connections['IF']['main'][1]).toBeDefined();
1138 |       expect(result.workflow!.connections['IF']['main'][1][0].node).toBe('FalseHandler');
1139 |     });
1140 | 
1141 |     it('should use case parameter for Switch node connections', async () => {
1142 |       // Add Switch node
1143 |       const addSwitch: any = {
1144 |         type: 'addNode',
1145 |         node: {
1146 |           name: 'Switch',
1147 |           type: 'n8n-nodes-base.switch',
1148 |           position: [400, 300]
1149 |         }
1150 |       };
1151 | 
1152 |       // Add handler nodes
1153 |       const addCase0: any = {
1154 |         type: 'addNode',
1155 |         node: {
1156 |           name: 'Case0Handler',
1157 |           type: 'n8n-nodes-base.set',
1158 |           position: [600, 200]
1159 |         }
1160 |       };
1161 | 
1162 |       const addCase1: any = {
1163 |         type: 'addNode',
1164 |         node: {
1165 |           name: 'Case1Handler',
1166 |           type: 'n8n-nodes-base.set',
1167 |           position: [600, 300]
1168 |         }
1169 |       };
1170 | 
1171 |       const addCase2: any = {
1172 |         type: 'addNode',
1173 |         node: {
1174 |           name: 'Case2Handler',
1175 |           type: 'n8n-nodes-base.set',
1176 |           position: [600, 400]
1177 |         }
1178 |       };
1179 | 
1180 |       // Connect using case parameter
1181 |       const connectCase0: any = {
1182 |         type: 'addConnection',
1183 |         source: 'Switch',
1184 |         target: 'Case0Handler',
1185 |         case: 0  // Smart parameter instead of sourceIndex: 0
1186 |       };
1187 | 
1188 |       const connectCase1: any = {
1189 |         type: 'addConnection',
1190 |         source: 'Switch',
1191 |         target: 'Case1Handler',
1192 |         case: 1  // Smart parameter instead of sourceIndex: 1
1193 |       };
1194 | 
1195 |       const connectCase2: any = {
1196 |         type: 'addConnection',
1197 |         source: 'Switch',
1198 |         target: 'Case2Handler',
1199 |         case: 2  // Smart parameter instead of sourceIndex: 2
1200 |       };
1201 | 
1202 |       const request: WorkflowDiffRequest = {
1203 |         id: 'test-workflow',
1204 |         operations: [addSwitch, addCase0, addCase1, addCase2, connectCase0, connectCase1, connectCase2]
1205 |       };
1206 | 
1207 |       const result = await diffEngine.applyDiff(baseWorkflow, request);
1208 | 
1209 |       expect(result.success).toBe(true);
1210 | 
1211 |       // All cases should be routed correctly
1212 |       expect(result.workflow!.connections['Switch']['main'][0][0].node).toBe('Case0Handler');
1213 |       expect(result.workflow!.connections['Switch']['main'][1][0].node).toBe('Case1Handler');
1214 |       expect(result.workflow!.connections['Switch']['main'][2][0].node).toBe('Case2Handler');
1215 |     });
1216 | 
1217 |     it('should use branch parameter with rewireConnection', async () => {
1218 |       // Setup: Create IF node with true/false branches
1219 |       const addIF: any = {
1220 |         type: 'addNode',
1221 |         node: {
1222 |           name: 'IFRewire',
1223 |           type: 'n8n-nodes-base.if',
1224 |           position: [400, 300]
1225 |         }
1226 |       };
1227 | 
1228 |       const addSuccess: any = {
1229 |         type: 'addNode',
1230 |         node: {
1231 |           name: 'SuccessHandler',
1232 |           type: 'n8n-nodes-base.set',
1233 |           position: [600, 200]
1234 |         }
1235 |       };
1236 | 
1237 |       const addNewSuccess: any = {
1238 |         type: 'addNode',
1239 |         node: {
1240 |           name: 'NewSuccessHandler',
1241 |           type: 'n8n-nodes-base.set',
1242 |           position: [600, 250]
1243 |         }
1244 |       };
1245 | 
1246 |       // Initial connection
1247 |       const initialConn: any = {
1248 |         type: 'addConnection',
1249 |         source: 'IFRewire',
1250 |         target: 'SuccessHandler',
1251 |         branch: 'true'
1252 |       };
1253 | 
1254 |       // Rewire using branch parameter
1255 |       const rewire: any = {
1256 |         type: 'rewireConnection',
1257 |         source: 'IFRewire',
1258 |         from: 'SuccessHandler',
1259 |         to: 'NewSuccessHandler',
1260 |         branch: 'true'  // Smart parameter
1261 |       };
1262 | 
1263 |       const request: WorkflowDiffRequest = {
1264 |         id: 'test-workflow',
1265 |         operations: [addIF, addSuccess, addNewSuccess, initialConn, rewire]
1266 |       };
1267 | 
1268 |       const result = await diffEngine.applyDiff(baseWorkflow, request);
1269 | 
1270 |       expect(result.success).toBe(true);
1271 | 
1272 |       // Should rewire the true branch (main output, index 0)
1273 |       expect(result.workflow!.connections['IFRewire']['main']).toBeDefined();
1274 |       expect(result.workflow!.connections['IFRewire']['main'][0]).toBeDefined();
1275 |       expect(result.workflow!.connections['IFRewire']['main'][0][0].node).toBe('NewSuccessHandler');
1276 |     });
1277 | 
1278 |     it('should use case parameter with rewireConnection', async () => {
1279 |       const addSwitch: any = {
1280 |         type: 'addNode',
1281 |         node: {
1282 |           name: 'Switch',
1283 |           type: 'n8n-nodes-base.switch',
1284 |           position: [400, 300]
1285 |         }
1286 |       };
1287 | 
1288 |       const addCase1: any = {
1289 |         type: 'addNode',
1290 |         node: {
1291 |           name: 'Case1Handler',
1292 |           type: 'n8n-nodes-base.set',
1293 |           position: [600, 300]
1294 |         }
1295 |       };
1296 | 
1297 |       const addNewCase1: any = {
1298 |         type: 'addNode',
1299 |         node: {
1300 |           name: 'NewCase1Handler',
1301 |           type: 'n8n-nodes-base.slack',
1302 |           position: [600, 350]
1303 |         }
1304 |       };
1305 | 
1306 |       const initialConn: any = {
1307 |         type: 'addConnection',
1308 |         source: 'Switch',
1309 |         target: 'Case1Handler',
1310 |         case: 1
1311 |       };
1312 | 
1313 |       const rewire: any = {
1314 |         type: 'rewireConnection',
1315 |         source: 'Switch',
1316 |         from: 'Case1Handler',
1317 |         to: 'NewCase1Handler',
1318 |         case: 1  // Smart parameter
1319 |       };
1320 | 
1321 |       const request: WorkflowDiffRequest = {
1322 |         id: 'test-workflow',
1323 |         operations: [addSwitch, addCase1, addNewCase1, initialConn, rewire]
1324 |       };
1325 | 
1326 |       const result = await diffEngine.applyDiff(baseWorkflow, request);
1327 | 
1328 |       expect(result.success).toBe(true);
1329 | 
1330 |       // Should rewire case 1
1331 |       expect(result.workflow!.connections['Switch']['main'][1][0].node).toBe('NewCase1Handler');
1332 |     });
1333 | 
1334 |     it('should not override explicit sourceOutput with branch parameter', async () => {
1335 |       const addIF: any = {
1336 |         type: 'addNode',
1337 |         node: {
1338 |           name: 'IFOverride',
1339 |           type: 'n8n-nodes-base.if',
1340 |           position: [400, 300]
1341 |         }
1342 |       };
1343 | 
1344 |       const addHandler: any = {
1345 |         type: 'addNode',
1346 |         node: {
1347 |           name: 'OverrideHandler',
1348 |           type: 'n8n-nodes-base.set',
1349 |           position: [600, 300]
1350 |         }
1351 |       };
1352 | 
1353 |       // Both branch and sourceOutput provided - sourceOutput should win
1354 |       const connectWithBoth: any = {
1355 |         type: 'addConnection',
1356 |         source: 'IFOverride',
1357 |         target: 'OverrideHandler',
1358 |         branch: 'true',          // Smart parameter suggests 'true'
1359 |         sourceOutput: 'false'    // Explicit parameter should override
1360 |       };
1361 | 
1362 |       const request: WorkflowDiffRequest = {
1363 |         id: 'test-workflow',
1364 |         operations: [addIF, addHandler, connectWithBoth]
1365 |       };
1366 | 
1367 |       const result = await diffEngine.applyDiff(baseWorkflow, request);
1368 | 
1369 |       expect(result.success).toBe(true);
1370 | 
1371 |       // Should use explicit sourceOutput ('false'), not smart branch parameter
1372 |       // Note: explicit sourceOutput='false' creates connection on output named 'false'
1373 |       // This is different from branch parameter which maps to sourceIndex
1374 |       expect(result.workflow!.connections['IFOverride']['false']).toBeDefined();
1375 |       expect(result.workflow!.connections['IFOverride']['false'][0][0].node).toBe('OverrideHandler');
1376 |       expect(result.workflow!.connections['IFOverride']['main']).toBeUndefined();
1377 |     });
1378 | 
1379 |     it('should not override explicit sourceIndex with case parameter', async () => {
1380 |       const addSwitch: any = {
1381 |         type: 'addNode',
1382 |         node: {
1383 |           name: 'Switch',
1384 |           type: 'n8n-nodes-base.switch',
1385 |           position: [400, 300]
1386 |         }
1387 |       };
1388 | 
1389 |       const addHandler: any = {
1390 |         type: 'addNode',
1391 |         node: {
1392 |           name: 'Handler',
1393 |           type: 'n8n-nodes-base.set',
1394 |           position: [600, 300]
1395 |         }
1396 |       };
1397 | 
1398 |       // Both case and sourceIndex provided - sourceIndex should win
1399 |       const connectWithBoth: any = {
1400 |         type: 'addConnection',
1401 |         source: 'Switch',
1402 |         target: 'Handler',
1403 |         case: 1,           // Smart parameter suggests index 1
1404 |         sourceIndex: 2     // Explicit parameter should override
1405 |       };
1406 | 
1407 |       const request: WorkflowDiffRequest = {
1408 |         id: 'test-workflow',
1409 |         operations: [addSwitch, addHandler, connectWithBoth]
1410 |       };
1411 | 
1412 |       const result = await diffEngine.applyDiff(baseWorkflow, request);
1413 | 
1414 |       expect(result.success).toBe(true);
1415 | 
1416 |       // Should use explicit sourceIndex (2), not case (1)
1417 |       expect(result.workflow!.connections['Switch']['main'][2]).toBeDefined();
1418 |       expect(result.workflow!.connections['Switch']['main'][2][0].node).toBe('Handler');
1419 |       expect(result.workflow!.connections['Switch']['main'][1]).toEqual([]);
1420 |     });
1421 |   });
1422 | 
1423 |   describe('AddConnection with sourceIndex (Phase 0 Fix)', () => {
1424 |     it('should add connection to correct sourceIndex', async () => {
1425 |       // Add IF node
1426 |       const addNodeOp: AddNodeOperation = {
1427 |         type: 'addNode',
1428 |         node: {
1429 |           name: 'IF',
1430 |           type: 'n8n-nodes-base.if',
1431 |           position: [600, 300]
1432 |         }
1433 |       };
1434 | 
1435 |       // Add two different target nodes
1436 |       const addNode1: AddNodeOperation = {
1437 |         type: 'addNode',
1438 |         node: {
1439 |           name: 'SuccessHandler',
1440 |           type: 'n8n-nodes-base.set',
1441 |           position: [800, 200]
1442 |         }
1443 |       };
1444 | 
1445 |       const addNode2: AddNodeOperation = {
1446 |         type: 'addNode',
1447 |         node: {
1448 |           name: 'ErrorHandler',
1449 |           type: 'n8n-nodes-base.set',
1450 |           position: [800, 400]
1451 |         }
1452 |       };
1453 | 
1454 |       // Connect to 'true' output at index 0
1455 |       const addConnection1: AddConnectionOperation = {
1456 |         type: 'addConnection',
1457 |         source: 'IF',
1458 |         target: 'SuccessHandler',
1459 |         sourceOutput: 'true',
1460 |         sourceIndex: 0
1461 |       };
1462 | 
1463 |       // Connect to 'false' output at index 0
1464 |       const addConnection2: AddConnectionOperation = {
1465 |         type: 'addConnection',
1466 |         source: 'IF',
1467 |         target: 'ErrorHandler',
1468 |         sourceOutput: 'false',
1469 |         sourceIndex: 0
1470 |       };
1471 | 
1472 |       const request: WorkflowDiffRequest = {
1473 |         id: 'test-workflow',
1474 |         operations: [addNodeOp, addNode1, addNode2, addConnection1, addConnection2]
1475 |       };
1476 | 
1477 |       const result = await diffEngine.applyDiff(baseWorkflow, request);
1478 | 
1479 |       expect(result.success).toBe(true);
1480 |       // Verify connections are at correct indices
1481 |       expect(result.workflow!.connections['IF']['true']).toBeDefined();
1482 |       expect(result.workflow!.connections['IF']['true'][0]).toBeDefined();
1483 |       expect(result.workflow!.connections['IF']['true'][0][0].node).toBe('SuccessHandler');
1484 | 
1485 |       expect(result.workflow!.connections['IF']['false']).toBeDefined();
1486 |       expect(result.workflow!.connections['IF']['false'][0]).toBeDefined();
1487 |       expect(result.workflow!.connections['IF']['false'][0][0].node).toBe('ErrorHandler');
1488 |     });
1489 | 
1490 |     it('should support multiple connections at same sourceIndex (parallel execution)', async () => {
1491 |       // Use a fresh workflow to avoid interference
1492 |       const freshWorkflow = JSON.parse(JSON.stringify(baseWorkflow));
1493 | 
1494 |       // Add three target nodes
1495 |       const addNode1: AddNodeOperation = {
1496 |         type: 'addNode',
1497 |         node: {
1498 |           name: 'Processor1',
1499 |           type: 'n8n-nodes-base.set',
1500 |           position: [600, 200]
1501 |         }
1502 |       };
1503 | 
1504 |       const addNode2: AddNodeOperation = {
1505 |         type: 'addNode',
1506 |         node: {
1507 |           name: 'Processor2',
1508 |           type: 'n8n-nodes-base.set',
1509 |           position: [600, 300]
1510 |         }
1511 |       };
1512 | 
1513 |       const addNode3: AddNodeOperation = {
1514 |         type: 'addNode',
1515 |         node: {
1516 |           name: 'Processor3',
1517 |           type: 'n8n-nodes-base.set',
1518 |           position: [600, 400]
1519 |         }
1520 |       };
1521 | 
1522 |       // All connect from Webhook at sourceIndex 0 (parallel)
1523 |       const addConnection1: AddConnectionOperation = {
1524 |         type: 'addConnection',
1525 |         source: 'Webhook',
1526 |         target: 'Processor1',
1527 |         sourceIndex: 0
1528 |       };
1529 | 
1530 |       const addConnection2: AddConnectionOperation = {
1531 |         type: 'addConnection',
1532 |         source: 'Webhook',
1533 |         target: 'Processor2',
1534 |         sourceIndex: 0
1535 |       };
1536 | 
1537 |       const addConnection3: AddConnectionOperation = {
1538 |         type: 'addConnection',
1539 |         source: 'Webhook',
1540 |         target: 'Processor3',
1541 |         sourceIndex: 0
1542 |       };
1543 | 
1544 |       const request: WorkflowDiffRequest = {
1545 |         id: 'test-workflow',
1546 |         operations: [addNode1, addNode2, addNode3, addConnection1, addConnection2, addConnection3]
1547 |       };
1548 | 
1549 |       const result = await diffEngine.applyDiff(freshWorkflow, request);
1550 | 
1551 |       expect(result.success).toBe(true);
1552 |       // All three new processors plus the existing HTTP Request should be at index 0
1553 |       // So we expect 4 total connections
1554 |       const connectionsAtIndex0 = result.workflow!.connections['Webhook']['main'][0];
1555 |       expect(connectionsAtIndex0.length).toBeGreaterThanOrEqual(3);
1556 |       const targets = connectionsAtIndex0.map((c: any) => c.node);
1557 |       expect(targets).toContain('Processor1');
1558 |       expect(targets).toContain('Processor2');
1559 |       expect(targets).toContain('Processor3');
1560 |     });
1561 | 
1562 |     it('should support connections at different sourceIndices (Switch node pattern)', async () => {
1563 |       // Add Switch node
1564 |       const addSwitchNode: AddNodeOperation = {
1565 |         type: 'addNode',
1566 |         node: {
1567 |           name: 'Switch',
1568 |           type: 'n8n-nodes-base.switch',
1569 |           position: [400, 300]
1570 |         }
1571 |       };
1572 | 
1573 |       // Add handlers for different cases
1574 |       const addCase0: AddNodeOperation = {
1575 |         type: 'addNode',
1576 |         node: {
1577 |           name: 'Case0Handler',
1578 |           type: 'n8n-nodes-base.set',
1579 |           position: [600, 200]
1580 |         }
1581 |       };
1582 | 
1583 |       const addCase1: AddNodeOperation = {
1584 |         type: 'addNode',
1585 |         node: {
1586 |           name: 'Case1Handler',
1587 |           type: 'n8n-nodes-base.set',
1588 |           position: [600, 300]
1589 |         }
1590 |       };
1591 | 
1592 |       const addCase2: AddNodeOperation = {
1593 |         type: 'addNode',
1594 |         node: {
1595 |           name: 'Case2Handler',
1596 |           type: 'n8n-nodes-base.set',
1597 |           position: [600, 400]
1598 |         }
1599 |       };
1600 | 
1601 |       // Connect to different sourceIndices
1602 |       const conn0: AddConnectionOperation = {
1603 |         type: 'addConnection',
1604 |         source: 'Switch',
1605 |         target: 'Case0Handler',
1606 |         sourceIndex: 0
1607 |       };
1608 | 
1609 |       const conn1: AddConnectionOperation = {
1610 |         type: 'addConnection',
1611 |         source: 'Switch',
1612 |         target: 'Case1Handler',
1613 |         sourceIndex: 1
1614 |       };
1615 | 
1616 |       const conn2: AddConnectionOperation = {
1617 |         type: 'addConnection',
1618 |         source: 'Switch',
1619 |         target: 'Case2Handler',
1620 |         sourceIndex: 2
1621 |       };
1622 | 
1623 |       const request: WorkflowDiffRequest = {
1624 |         id: 'test-workflow',
1625 |         operations: [addSwitchNode, addCase0, addCase1, addCase2, conn0, conn1, conn2]
1626 |       };
1627 | 
1628 |       const result = await diffEngine.applyDiff(baseWorkflow, request);
1629 | 
1630 |       expect(result.success).toBe(true);
1631 |       // Verify each case routes to correct handler
1632 |       expect(result.workflow!.connections['Switch']['main'][0][0].node).toBe('Case0Handler');
1633 |       expect(result.workflow!.connections['Switch']['main'][1][0].node).toBe('Case1Handler');
1634 |       expect(result.workflow!.connections['Switch']['main'][2][0].node).toBe('Case2Handler');
1635 |     });
1636 | 
1637 |     it('should properly handle sourceIndex 0 as explicit value vs default', async () => {
1638 |       // Use a fresh workflow
1639 |       const freshWorkflow = JSON.parse(JSON.stringify(baseWorkflow));
1640 | 
1641 |       const addNode: AddNodeOperation = {
1642 |         type: 'addNode',
1643 |         node: {
1644 |           name: 'TestNode',
1645 |           type: 'n8n-nodes-base.set',
1646 |           position: [600, 300]
1647 |         }
1648 |       };
1649 | 
1650 |       // Explicit sourceIndex: 0
1651 |       const connection1: AddConnectionOperation = {
1652 |         type: 'addConnection',
1653 |         source: 'Webhook',
1654 |         target: 'TestNode',
1655 |         sourceIndex: 0
1656 |       };
1657 | 
1658 |       const request: WorkflowDiffRequest = {
1659 |         id: 'test-workflow',
1660 |         operations: [addNode, connection1]
1661 |       };
1662 | 
1663 |       const result = await diffEngine.applyDiff(freshWorkflow, request);
1664 | 
1665 |       expect(result.success).toBe(true);
1666 |       expect(result.workflow!.connections['Webhook']['main'][0]).toBeDefined();
1667 |       // TestNode should be in the connections (might not be first if HTTP Request already exists)
1668 |       const targets = result.workflow!.connections['Webhook']['main'][0].map((c: any) => c.node);
1669 |       expect(targets).toContain('TestNode');
1670 |     });
1671 |   });
1672 | 
1673 |   describe('UpdateSettings Operation', () => {
1674 |     it('should update workflow settings', async () => {
1675 |       const operation: UpdateSettingsOperation = {
1676 |         type: 'updateSettings',
1677 |         settings: {
1678 |           executionOrder: 'v0',
1679 |           timezone: 'America/New_York',
1680 |           executionTimeout: 300
1681 |         }
1682 |       };
1683 | 
1684 |       const request: WorkflowDiffRequest = {
1685 |         id: 'test-workflow',
1686 |         operations: [operation]
1687 |       };
1688 | 
1689 |       const result = await diffEngine.applyDiff(baseWorkflow, request);
1690 |       
1691 |       expect(result.success).toBe(true);
1692 |       expect(result.workflow!.settings!.executionOrder).toBe('v0');
1693 |       expect(result.workflow!.settings!.timezone).toBe('America/New_York');
1694 |       expect(result.workflow!.settings!.executionTimeout).toBe(300);
1695 |     });
1696 | 
1697 |     it('should create settings object if not exists', async () => {
1698 |       delete baseWorkflow.settings;
1699 | 
1700 |       const operation: UpdateSettingsOperation = {
1701 |         type: 'updateSettings',
1702 |         settings: {
1703 |           saveManualExecutions: false
1704 |         }
1705 |       };
1706 | 
1707 |       const request: WorkflowDiffRequest = {
1708 |         id: 'test-workflow',
1709 |         operations: [operation]
1710 |       };
1711 | 
1712 |       const result = await diffEngine.applyDiff(baseWorkflow, request);
1713 |       
1714 |       expect(result.success).toBe(true);
1715 |       expect(result.workflow!.settings).toBeDefined();
1716 |       expect(result.workflow!.settings!.saveManualExecutions).toBe(false);
1717 |     });
1718 |   });
1719 | 
1720 |   describe('UpdateName Operation', () => {
1721 |     it('should update workflow name', async () => {
1722 |       const operation: UpdateNameOperation = {
1723 |         type: 'updateName',
1724 |         name: 'Updated Workflow Name'
1725 |       };
1726 | 
1727 |       const request: WorkflowDiffRequest = {
1728 |         id: 'test-workflow',
1729 |         operations: [operation]
1730 |       };
1731 | 
1732 |       const result = await diffEngine.applyDiff(baseWorkflow, request);
1733 |       
1734 |       expect(result.success).toBe(true);
1735 |       expect(result.workflow!.name).toBe('Updated Workflow Name');
1736 |     });
1737 |   });
1738 | 
1739 |   describe('Tag Operations', () => {
1740 |     it('should add a new tag', async () => {
1741 |       const operation: AddTagOperation = {
1742 |         type: 'addTag',
1743 |         tag: 'production'
1744 |       };
1745 | 
1746 |       const request: WorkflowDiffRequest = {
1747 |         id: 'test-workflow',
1748 |         operations: [operation]
1749 |       };
1750 | 
1751 |       const result = await diffEngine.applyDiff(baseWorkflow, request);
1752 |       
1753 |       expect(result.success).toBe(true);
1754 |       expect(result.workflow!.tags).toContain('production');
1755 |       expect(result.workflow!.tags).toHaveLength(3);
1756 |     });
1757 | 
1758 |     it('should not add duplicate tags', async () => {
1759 |       const operation: AddTagOperation = {
1760 |         type: 'addTag',
1761 |         tag: 'test' // Already exists
1762 |       };
1763 | 
1764 |       const request: WorkflowDiffRequest = {
1765 |         id: 'test-workflow',
1766 |         operations: [operation]
1767 |       };
1768 | 
1769 |       const result = await diffEngine.applyDiff(baseWorkflow, request);
1770 |       
1771 |       expect(result.success).toBe(true);
1772 |       expect(result.workflow!.tags).toHaveLength(2); // No change
1773 |     });
1774 | 
1775 |     it('should create tags array if not exists', async () => {
1776 |       delete baseWorkflow.tags;
1777 | 
1778 |       const operation: AddTagOperation = {
1779 |         type: 'addTag',
1780 |         tag: 'new-tag'
1781 |       };
1782 | 
1783 |       const request: WorkflowDiffRequest = {
1784 |         id: 'test-workflow',
1785 |         operations: [operation]
1786 |       };
1787 | 
1788 |       const result = await diffEngine.applyDiff(baseWorkflow, request);
1789 |       
1790 |       expect(result.success).toBe(true);
1791 |       expect(result.workflow!.tags).toBeDefined();
1792 |       expect(result.workflow!.tags).toEqual(['new-tag']);
1793 |     });
1794 | 
1795 |     it('should remove an existing tag', async () => {
1796 |       const operation: RemoveTagOperation = {
1797 |         type: 'removeTag',
1798 |         tag: 'test'
1799 |       };
1800 | 
1801 |       const request: WorkflowDiffRequest = {
1802 |         id: 'test-workflow',
1803 |         operations: [operation]
1804 |       };
1805 | 
1806 |       const result = await diffEngine.applyDiff(baseWorkflow, request);
1807 |       
1808 |       expect(result.success).toBe(true);
1809 |       expect(result.workflow!.tags).not.toContain('test');
1810 |       expect(result.workflow!.tags).toHaveLength(1);
1811 |     });
1812 | 
1813 |     it('should handle removing non-existent tag gracefully', async () => {
1814 |       const operation: RemoveTagOperation = {
1815 |         type: 'removeTag',
1816 |         tag: 'non-existent'
1817 |       };
1818 | 
1819 |       const request: WorkflowDiffRequest = {
1820 |         id: 'test-workflow',
1821 |         operations: [operation]
1822 |       };
1823 | 
1824 |       const result = await diffEngine.applyDiff(baseWorkflow, request);
1825 |       
1826 |       expect(result.success).toBe(true);
1827 |       expect(result.workflow!.tags).toHaveLength(2); // No change
1828 |     });
1829 |   });
1830 | 
1831 |   describe('ValidateOnly Mode', () => {
1832 |     it('should validate without applying changes', async () => {
1833 |       const operation: UpdateNameOperation = {
1834 |         type: 'updateName',
1835 |         name: 'Validated But Not Applied'
1836 |       };
1837 | 
1838 |       const request: WorkflowDiffRequest = {
1839 |         id: 'test-workflow',
1840 |         operations: [operation],
1841 |         validateOnly: true
1842 |       };
1843 | 
1844 |       const result = await diffEngine.applyDiff(baseWorkflow, request);
1845 |       
1846 |       expect(result.success).toBe(true);
1847 |       expect(result.message).toContain('Validation successful');
1848 |       expect(result.workflow).toBeUndefined();
1849 |     });
1850 | 
1851 |     it('should return validation errors in validateOnly mode', async () => {
1852 |       const operation: RemoveNodeOperation = {
1853 |         type: 'removeNode',
1854 |         nodeId: 'non-existent'
1855 |       };
1856 | 
1857 |       const request: WorkflowDiffRequest = {
1858 |         id: 'test-workflow',
1859 |         operations: [operation],
1860 |         validateOnly: true
1861 |       };
1862 | 
1863 |       const result = await diffEngine.applyDiff(baseWorkflow, request);
1864 |       
1865 |       expect(result.success).toBe(false);
1866 |       expect(result.errors![0].message).toContain('Node not found');
1867 |     });
1868 |   });
1869 | 
1870 |   describe('Operation Ordering', () => {
1871 |     it('should process node operations before connection operations', async () => {
1872 |       // This tests the two-pass processing: nodes first, then connections
1873 |       const operations = [
1874 |         {
1875 |           type: 'addConnection',
1876 |           source: 'NewNode',
1877 |           target: 'slack-1'
1878 |         } as AddConnectionOperation,
1879 |         {
1880 |           type: 'addNode',
1881 |           node: {
1882 |             name: 'NewNode',
1883 |             type: 'n8n-nodes-base.code',
1884 |             position: [800, 300]
1885 |           }
1886 |         } as AddNodeOperation
1887 |       ];
1888 | 
1889 |       const request: WorkflowDiffRequest = {
1890 |         id: 'test-workflow',
1891 |         operations
1892 |       };
1893 | 
1894 |       const result = await diffEngine.applyDiff(baseWorkflow, request);
1895 |       
1896 |       expect(result.success).toBe(true);
1897 |       expect(result.workflow!.nodes).toHaveLength(4);
1898 |       expect(result.workflow!.connections['NewNode']).toBeDefined();
1899 |     });
1900 | 
1901 |     it('should handle dependent operations correctly', async () => {
1902 |       const operations = [
1903 |         {
1904 |           type: 'removeNode',
1905 |           nodeId: 'http-1'
1906 |         } as RemoveNodeOperation,
1907 |         {
1908 |           type: 'addNode',
1909 |           node: {
1910 |             name: 'HTTP Request', // Reuse the same name
1911 |             type: 'n8n-nodes-base.httpRequest',
1912 |             position: [600, 300]
1913 |           }
1914 |         } as AddNodeOperation,
1915 |         {
1916 |           type: 'addConnection',
1917 |           source: 'webhook-1',
1918 |           target: 'HTTP Request'
1919 |         } as AddConnectionOperation
1920 |       ];
1921 | 
1922 |       const request: WorkflowDiffRequest = {
1923 |         id: 'test-workflow',
1924 |         operations
1925 |       };
1926 | 
1927 |       const result = await diffEngine.applyDiff(baseWorkflow, request);
1928 |       
1929 |       expect(result.success).toBe(true);
1930 |       expect(result.workflow!.nodes).toHaveLength(3);
1931 |       expect(result.workflow!.connections['Webhook'].main[0][0].node).toBe('HTTP Request');
1932 |     });
1933 |   });
1934 | 
1935 |   describe('Error Handling', () => {
1936 |     it('should handle unknown operation type', async () => {
1937 |       const operation = {
1938 |         type: 'unknownOperation',
1939 |         someData: 'test'
1940 |       } as any;
1941 | 
1942 |       const request: WorkflowDiffRequest = {
1943 |         id: 'test-workflow',
1944 |         operations: [operation]
1945 |       };
1946 | 
1947 |       const result = await diffEngine.applyDiff(baseWorkflow, request);
1948 |       
1949 |       expect(result.success).toBe(false);
1950 |       expect(result.errors![0].message).toContain('Unknown operation type');
1951 |     });
1952 | 
1953 |     it('should stop on first validation error', async () => {
1954 |       const operations = [
1955 |         {
1956 |           type: 'removeNode',
1957 |           nodeId: 'non-existent'
1958 |         } as RemoveNodeOperation,
1959 |         {
1960 |           type: 'updateName',
1961 |           name: 'This should not be applied'
1962 |         } as UpdateNameOperation
1963 |       ];
1964 | 
1965 |       const request: WorkflowDiffRequest = {
1966 |         id: 'test-workflow',
1967 |         operations
1968 |       };
1969 | 
1970 |       const result = await diffEngine.applyDiff(baseWorkflow, request);
1971 |       
1972 |       expect(result.success).toBe(false);
1973 |       expect(result.errors).toHaveLength(1);
1974 |       expect(result.errors![0].operation).toBe(0);
1975 |     });
1976 | 
1977 |     it('should return operation details in error', async () => {
1978 |       const operation: RemoveNodeOperation = {
1979 |         type: 'removeNode',
1980 |         nodeId: 'non-existent',
1981 |         description: 'Test remove operation'
1982 |       };
1983 | 
1984 |       const request: WorkflowDiffRequest = {
1985 |         id: 'test-workflow',
1986 |         operations: [operation]
1987 |       };
1988 | 
1989 |       const result = await diffEngine.applyDiff(baseWorkflow, request);
1990 |       
1991 |       expect(result.success).toBe(false);
1992 |       expect(result.errors![0].details).toEqual(operation);
1993 |     });
1994 |   });
1995 | 
1996 |   describe('Complex Scenarios', () => {
1997 |     it('should handle multiple operations of different types', async () => {
1998 |       const operations = [
1999 |         {
2000 |           type: 'updateName',
2001 |           name: 'Complex Workflow'
2002 |         } as UpdateNameOperation,
2003 |         {
2004 |           type: 'addNode',
2005 |           node: {
2006 |             name: 'Filter',
2007 |             type: 'n8n-nodes-base.filter',
2008 |             position: [800, 200]
2009 |           }
2010 |         } as AddNodeOperation,
2011 |         {
2012 |           type: 'removeConnection',
2013 |           source: 'HTTP Request',  // Use node name
2014 |           target: 'Slack'  // Use node name
2015 |         } as RemoveConnectionOperation,
2016 |         {
2017 |           type: 'addConnection',
2018 |           source: 'HTTP Request',  // Use node name
2019 |           target: 'Filter'
2020 |         } as AddConnectionOperation,
2021 |         {
2022 |           type: 'addConnection',
2023 |           source: 'Filter',
2024 |           target: 'Slack'  // Use node name
2025 |         } as AddConnectionOperation
2026 |       ];
2027 | 
2028 |       const request: WorkflowDiffRequest = {
2029 |         id: 'test-workflow',
2030 |         operations
2031 |       };
2032 | 
2033 |       const result = await diffEngine.applyDiff(baseWorkflow, request);
2034 |       
2035 |       expect(result.success).toBe(true);
2036 |       expect(result.workflow!.name).toBe('Complex Workflow');
2037 |       expect(result.workflow!.nodes).toHaveLength(4);
2038 |       expect(result.workflow!.connections['HTTP Request'].main[0][0].node).toBe('Filter');
2039 |       expect(result.workflow!.connections['Filter'].main[0][0].node).toBe('Slack');
2040 |       expect(result.operationsApplied).toBe(5);
2041 |     });
2042 | 
2043 |     it('should preserve workflow immutability', async () => {
2044 |       const originalNodes = [...baseWorkflow.nodes];
2045 |       const originalConnections = JSON.stringify(baseWorkflow.connections);
2046 | 
2047 |       const operation: UpdateNameOperation = {
2048 |         type: 'updateName',
2049 |         name: 'Modified'
2050 |       };
2051 | 
2052 |       const request: WorkflowDiffRequest = {
2053 |         id: 'test-workflow',
2054 |         operations: [operation]
2055 |       };
2056 | 
2057 |       await diffEngine.applyDiff(baseWorkflow, request);
2058 |       
2059 |       // Original workflow should remain unchanged
2060 |       expect(baseWorkflow.name).toBe('Test Workflow');
2061 |       expect(baseWorkflow.nodes).toEqual(originalNodes);
2062 |       expect(JSON.stringify(baseWorkflow.connections)).toBe(originalConnections);
2063 |     });
2064 | 
2065 |     it('should handle node ID as name fallback', async () => {
2066 |       // Test the findNode helper's fallback behavior
2067 |       const operation: UpdateNodeOperation = {
2068 |         type: 'updateNode',
2069 |         nodeId: 'Webhook', // Using name as ID
2070 |         updates: {
2071 |           'parameters.path': 'new-webhook-path'
2072 |         }
2073 |       };
2074 | 
2075 |       const request: WorkflowDiffRequest = {
2076 |         id: 'test-workflow',
2077 |         operations: [operation]
2078 |       };
2079 | 
2080 |       const result = await diffEngine.applyDiff(baseWorkflow, request);
2081 |       
2082 |       expect(result.success).toBe(true);
2083 |       const updatedNode = result.workflow!.nodes.find((n: any) => n.name === 'Webhook');
2084 |       expect(updatedNode!.parameters.path).toBe('new-webhook-path');
2085 |     });
2086 |   });
2087 | 
2088 |   describe('Success Messages', () => {
2089 |     it('should provide informative success message', async () => {
2090 |       const operations = [
2091 |         {
2092 |           type: 'addNode',
2093 |           node: {
2094 |             name: 'Node1',
2095 |             type: 'n8n-nodes-base.code',
2096 |             position: [100, 100]
2097 |           }
2098 |         } as AddNodeOperation,
2099 |         {
2100 |           type: 'updateSettings',
2101 |           settings: { timezone: 'UTC' }
2102 |         } as UpdateSettingsOperation,
2103 |         {
2104 |           type: 'addTag',
2105 |           tag: 'v2'
2106 |         } as AddTagOperation
2107 |       ];
2108 | 
2109 |       const request: WorkflowDiffRequest = {
2110 |         id: 'test-workflow',
2111 |         operations
2112 |       };
2113 | 
2114 |       const result = await diffEngine.applyDiff(baseWorkflow, request);
2115 |       
2116 |       expect(result.success).toBe(true);
2117 |       expect(result.message).toContain('Successfully applied 3 operations');
2118 |       expect(result.message).toContain('1 node ops');
2119 |       expect(result.message).toContain('2 other ops');
2120 |     });
2121 |   });
2122 | 
2123 |   describe('New Features - v2.14.4', () => {
2124 |     describe('cleanStaleConnections operation', () => {
2125 |       it('should remove connections referencing non-existent nodes', async () => {
2126 |         // Create a workflow with a stale connection
2127 |         const workflow = builder.build() as Workflow;
2128 | 
2129 |         // Add a connection to a non-existent node manually
2130 |         if (!workflow.connections['Webhook']) {
2131 |           workflow.connections['Webhook'] = {};
2132 |         }
2133 |         workflow.connections['Webhook']['main'] = [[
2134 |           { node: 'HTTP Request', type: 'main', index: 0 },
2135 |           { node: 'NonExistentNode', type: 'main', index: 0 }
2136 |         ]];
2137 | 
2138 |         const operations: CleanStaleConnectionsOperation[] = [{
2139 |           type: 'cleanStaleConnections'
2140 |         }];
2141 | 
2142 |         const request: WorkflowDiffRequest = {
2143 |           id: 'test-workflow',
2144 |           operations
2145 |         };
2146 | 
2147 |         const result = await diffEngine.applyDiff(workflow, request);
2148 | 
2149 |         expect(result.success).toBe(true);
2150 |         expect(result.workflow.connections['Webhook']['main'][0]).toHaveLength(1);
2151 |         expect(result.workflow.connections['Webhook']['main'][0][0].node).toBe('HTTP Request');
2152 |       });
2153 | 
2154 |       it('should remove entire source connection if source node does not exist', async () => {
2155 |         const workflow = builder.build() as Workflow;
2156 | 
2157 |         // Add connections from non-existent node
2158 |         workflow.connections['GhostNode'] = {
2159 |           'main': [[
2160 |             { node: 'HTTP Request', type: 'main', index: 0 }
2161 |           ]]
2162 |         };
2163 | 
2164 |         const operations: CleanStaleConnectionsOperation[] = [{
2165 |           type: 'cleanStaleConnections'
2166 |         }];
2167 | 
2168 |         const request: WorkflowDiffRequest = {
2169 |           id: 'test-workflow',
2170 |           operations
2171 |         };
2172 | 
2173 |         const result = await diffEngine.applyDiff(workflow, request);
2174 | 
2175 |         expect(result.success).toBe(true);
2176 |         expect(result.workflow.connections['GhostNode']).toBeUndefined();
2177 |       });
2178 | 
2179 |       it('should support dryRun mode', async () => {
2180 |         const workflow = builder.build() as Workflow;
2181 | 
2182 |         // Add a stale connection
2183 |         if (!workflow.connections['Webhook']) {
2184 |           workflow.connections['Webhook'] = {};
2185 |         }
2186 |         workflow.connections['Webhook']['main'] = [[
2187 |           { node: 'HTTP Request', type: 'main', index: 0 },
2188 |           { node: 'NonExistentNode', type: 'main', index: 0 }
2189 |         ]];
2190 | 
2191 |         const operations: CleanStaleConnectionsOperation[] = [{
2192 |           type: 'cleanStaleConnections',
2193 |           dryRun: true
2194 |         }];
2195 | 
2196 |         const request: WorkflowDiffRequest = {
2197 |           id: 'test-workflow',
2198 |           operations
2199 |         };
2200 | 
2201 |         const result = await diffEngine.applyDiff(workflow, request);
2202 | 
2203 |         expect(result.success).toBe(true);
2204 |         // In dryRun, stale connection should still be present (not actually removed)
2205 |         expect(result.workflow.connections['Webhook']['main'][0]).toHaveLength(2);
2206 |       });
2207 |     });
2208 | 
2209 |     describe('replaceConnections operation', () => {
2210 |       it('should replace entire connections object', async () => {
2211 |         const workflow = builder.build() as Workflow;
2212 | 
2213 |         const newConnections = {
2214 |           'Webhook': {
2215 |             'main': [[
2216 |               { node: 'Slack', type: 'main', index: 0 }
2217 |             ]]
2218 |           }
2219 |         };
2220 | 
2221 |         const operations: ReplaceConnectionsOperation[] = [{
2222 |           type: 'replaceConnections',
2223 |           connections: newConnections
2224 |         }];
2225 | 
2226 |         const request: WorkflowDiffRequest = {
2227 |           id: 'test-workflow',
2228 |           operations
2229 |         };
2230 | 
2231 |         const result = await diffEngine.applyDiff(workflow, request);
2232 | 
2233 |         expect(result.success).toBe(true);
2234 |         expect(result.workflow.connections).toEqual(newConnections);
2235 |         expect(result.workflow.connections['HTTP Request']).toBeUndefined();
2236 |       });
2237 | 
2238 |       it('should fail if referenced nodes do not exist', async () => {
2239 |         const workflow = builder.build() as Workflow;
2240 | 
2241 |         const newConnections = {
2242 |           'Webhook': {
2243 |             'main': [[
2244 |               { node: 'NonExistentNode', type: 'main', index: 0 }
2245 |             ]]
2246 |           }
2247 |         };
2248 | 
2249 |         const operations: ReplaceConnectionsOperation[] = [{
2250 |           type: 'replaceConnections',
2251 |           connections: newConnections
2252 |         }];
2253 | 
2254 |         const request: WorkflowDiffRequest = {
2255 |           id: 'test-workflow',
2256 |           operations
2257 |         };
2258 | 
2259 |         const result = await diffEngine.applyDiff(workflow, request);
2260 | 
2261 |         expect(result.success).toBe(false);
2262 |         expect(result.errors).toBeDefined();
2263 |         expect(result.errors![0].message).toContain('Target node not found');
2264 |       });
2265 |     });
2266 | 
2267 |     describe('removeConnection with ignoreErrors flag', () => {
2268 |       it('should succeed when connection does not exist if ignoreErrors is true', async () => {
2269 |         const workflow = builder.build() as Workflow;
2270 | 
2271 |         const operations: RemoveConnectionOperation[] = [{
2272 |           type: 'removeConnection',
2273 |           source: 'Webhook',
2274 |           target: 'NonExistentNode',
2275 |           ignoreErrors: true
2276 |         }];
2277 | 
2278 |         const request: WorkflowDiffRequest = {
2279 |           id: 'test-workflow',
2280 |           operations
2281 |         };
2282 | 
2283 |         const result = await diffEngine.applyDiff(workflow, request);
2284 | 
2285 |         expect(result.success).toBe(true);
2286 |       });
2287 | 
2288 |       it('should fail when connection does not exist if ignoreErrors is false', async () => {
2289 |         const workflow = builder.build() as Workflow;
2290 | 
2291 |         const operations: RemoveConnectionOperation[] = [{
2292 |           type: 'removeConnection',
2293 |           source: 'Webhook',
2294 |           target: 'NonExistentNode',
2295 |           ignoreErrors: false
2296 |         }];
2297 | 
2298 |         const request: WorkflowDiffRequest = {
2299 |           id: 'test-workflow',
2300 |           operations
2301 |         };
2302 | 
2303 |         const result = await diffEngine.applyDiff(workflow, request);
2304 | 
2305 |         expect(result.success).toBe(false);
2306 |         expect(result.errors).toBeDefined();
2307 |       });
2308 | 
2309 |       it('should default to atomic behavior when ignoreErrors is not specified', async () => {
2310 |         const workflow = builder.build() as Workflow;
2311 | 
2312 |         const operations: RemoveConnectionOperation[] = [{
2313 |           type: 'removeConnection',
2314 |           source: 'Webhook',
2315 |           target: 'NonExistentNode'
2316 |         }];
2317 | 
2318 |         const request: WorkflowDiffRequest = {
2319 |           id: 'test-workflow',
2320 |           operations
2321 |         };
2322 | 
2323 |         const result = await diffEngine.applyDiff(workflow, request);
2324 | 
2325 |         expect(result.success).toBe(false);
2326 |         expect(result.errors).toBeDefined();
2327 |       });
2328 |     });
2329 | 
2330 |     describe('continueOnError mode', () => {
2331 |       it('should apply valid operations and report failed ones', async () => {
2332 |         const workflow = builder.build() as Workflow;
2333 | 
2334 |         const operations: WorkflowDiffOperation[] = [
2335 |           {
2336 |             type: 'updateName',
2337 |             name: 'New Workflow Name'
2338 |           } as UpdateNameOperation,
2339 |           {
2340 |             type: 'removeConnection',
2341 |             source: 'Webhook',
2342 |             target: 'NonExistentNode'
2343 |           } as RemoveConnectionOperation,
2344 |           {
2345 |             type: 'addTag',
2346 |             tag: 'production'
2347 |           } as AddTagOperation
2348 |         ];
2349 | 
2350 |         const request: WorkflowDiffRequest = {
2351 |           id: 'test-workflow',
2352 |           operations,
2353 |           continueOnError: true
2354 |         };
2355 | 
2356 |         const result = await diffEngine.applyDiff(workflow, request);
2357 | 
2358 |         expect(result.success).toBe(true);
2359 |         expect(result.applied).toEqual([0, 2]); // Operations 0 and 2 succeeded
2360 |         expect(result.failed).toEqual([1]); // Operation 1 failed
2361 |         expect(result.errors).toHaveLength(1);
2362 |         expect(result.workflow.name).toBe('New Workflow Name');
2363 |         expect(result.workflow.tags).toContain('production');
2364 |       });
2365 | 
2366 |       it('should return success false if all operations fail in continueOnError mode', async () => {
2367 |         const workflow = builder.build() as Workflow;
2368 | 
2369 |         const operations: WorkflowDiffOperation[] = [
2370 |           {
2371 |             type: 'removeConnection',
2372 |             source: 'Webhook',
2373 |             target: 'Node1'
2374 |           } as RemoveConnectionOperation,
2375 |           {
2376 |             type: 'removeConnection',
2377 |             source: 'Webhook',
2378 |             target: 'Node2'
2379 |           } as RemoveConnectionOperation
2380 |         ];
2381 | 
2382 |         const request: WorkflowDiffRequest = {
2383 |           id: 'test-workflow',
2384 |           operations,
2385 |           continueOnError: true
2386 |         };
2387 | 
2388 |         const result = await diffEngine.applyDiff(workflow, request);
2389 | 
2390 |         expect(result.success).toBe(false);
2391 |         expect(result.applied).toHaveLength(0);
2392 |         expect(result.failed).toEqual([0, 1]);
2393 |       });
2394 | 
2395 |       it('should use atomic mode by default when continueOnError is not specified', async () => {
2396 |         const workflow = builder.build() as Workflow;
2397 | 
2398 |         const operations: WorkflowDiffOperation[] = [
2399 |           {
2400 |             type: 'updateName',
2401 |             name: 'New Name'
2402 |           } as UpdateNameOperation,
2403 |           {
2404 |             type: 'removeConnection',
2405 |             source: 'Webhook',
2406 |             target: 'NonExistent'
2407 |           } as RemoveConnectionOperation
2408 |         ];
2409 | 
2410 |         const request: WorkflowDiffRequest = {
2411 |           id: 'test-workflow',
2412 |           operations
2413 |         };
2414 | 
2415 |         const result = await diffEngine.applyDiff(workflow, request);
2416 | 
2417 |         expect(result.success).toBe(false);
2418 |         expect(result.applied).toBeUndefined();
2419 |         expect(result.failed).toBeUndefined();
2420 |         // Name should not have been updated due to atomic behavior
2421 |         expect(result.workflow).toBeUndefined();
2422 |       });
2423 |     });
2424 | 
2425 |     describe('Backwards compatibility', () => {
2426 |       it('should maintain existing behavior for all previous operation types', async () => {
2427 |         const workflow = builder.build() as Workflow;
2428 | 
2429 |         const operations: WorkflowDiffOperation[] = [
2430 |           { type: 'updateName', name: 'Test' } as UpdateNameOperation,
2431 |           { type: 'addTag', tag: 'test' } as AddTagOperation,
2432 |           { type: 'removeTag', tag: 'automation' } as RemoveTagOperation,
2433 |           { type: 'updateSettings', settings: { timezone: 'UTC' } } as UpdateSettingsOperation
2434 |         ];
2435 | 
2436 |         const request: WorkflowDiffRequest = {
2437 |           id: 'test-workflow',
2438 |           operations
2439 |         };
2440 | 
2441 |         const result = await diffEngine.applyDiff(workflow, request);
2442 | 
2443 |         expect(result.success).toBe(true);
2444 |         expect(result.operationsApplied).toBe(4);
2445 |       });
2446 |     });
2447 |   });
2448 | 
2449 |   describe('v2.14.4 Coverage Improvements', () => {
2450 |     describe('cleanStaleConnections - Advanced Scenarios', () => {
2451 |       it('should clean up multiple stale connections across different output types', async () => {
2452 |         const workflow = builder.build() as Workflow;
2453 | 
2454 |         // Add an IF node with multiple outputs
2455 |         workflow.nodes.push({
2456 |           id: 'if-1',
2457 |           name: 'IF',
2458 |           type: 'n8n-nodes-base.if',
2459 |           typeVersion: 1,
2460 |           position: [600, 400],
2461 |           parameters: {}
2462 |         });
2463 | 
2464 |         // Add connections with both valid and stale targets on different outputs
2465 |         workflow.connections['IF'] = {
2466 |           'true': [[
2467 |             { node: 'Slack', type: 'main', index: 0 },
2468 |             { node: 'StaleNode1', type: 'main', index: 0 }
2469 |           ]],
2470 |           'false': [[
2471 |             { node: 'HTTP Request', type: 'main', index: 0 },
2472 |             { node: 'StaleNode2', type: 'main', index: 0 }
2473 |           ]]
2474 |         };
2475 | 
2476 |         const operations: CleanStaleConnectionsOperation[] = [{
2477 |           type: 'cleanStaleConnections'
2478 |         }];
2479 | 
2480 |         const request: WorkflowDiffRequest = {
2481 |           id: 'test-workflow',
2482 |           operations
2483 |         };
2484 | 
2485 |         const result = await diffEngine.applyDiff(workflow, request);
2486 | 
2487 |         expect(result.success).toBe(true);
2488 |         expect(result.workflow.connections['IF']['true'][0]).toHaveLength(1);
2489 |         expect(result.workflow.connections['IF']['true'][0][0].node).toBe('Slack');
2490 |         expect(result.workflow.connections['IF']['false'][0]).toHaveLength(1);
2491 |         expect(result.workflow.connections['IF']['false'][0][0].node).toBe('HTTP Request');
2492 |       });
2493 | 
2494 |       it('should remove empty output types after cleaning stale connections', async () => {
2495 |         const workflow = builder.build() as Workflow;
2496 | 
2497 |         // Add node with connections
2498 |         workflow.nodes.push({
2499 |           id: 'if-1',
2500 |           name: 'IF',
2501 |           type: 'n8n-nodes-base.if',
2502 |           typeVersion: 1,
2503 |           position: [600, 400],
2504 |           parameters: {}
2505 |         });
2506 | 
2507 |         // Add connections where all targets in one output are stale
2508 |         workflow.connections['IF'] = {
2509 |           'true': [[
2510 |             { node: 'StaleNode1', type: 'main', index: 0 },
2511 |             { node: 'StaleNode2', type: 'main', index: 0 }
2512 |           ]],
2513 |           'false': [[
2514 |             { node: 'Slack', type: 'main', index: 0 }
2515 |           ]]
2516 |         };
2517 | 
2518 |         const operations: CleanStaleConnectionsOperation[] = [{
2519 |           type: 'cleanStaleConnections'
2520 |         }];
2521 | 
2522 |         const request: WorkflowDiffRequest = {
2523 |           id: 'test-workflow',
2524 |           operations
2525 |         };
2526 | 
2527 |         const result = await diffEngine.applyDiff(workflow, request);
2528 | 
2529 |         expect(result.success).toBe(true);
2530 |         expect(result.workflow.connections['IF']['true']).toBeUndefined();
2531 |         expect(result.workflow.connections['IF']['false']).toBeDefined();
2532 |         expect(result.workflow.connections['IF']['false'][0][0].node).toBe('Slack');
2533 |       });
2534 | 
2535 |       it('should clean up entire node connections when all outputs become empty', async () => {
2536 |         const workflow = builder.build() as Workflow;
2537 | 
2538 |         // Add node
2539 |         workflow.nodes.push({
2540 |           id: 'if-1',
2541 |           name: 'IF',
2542 |           type: 'n8n-nodes-base.if',
2543 |           typeVersion: 1,
2544 |           position: [600, 400],
2545 |           parameters: {}
2546 |         });
2547 | 
2548 |         // Add connections where ALL targets are stale
2549 |         workflow.connections['IF'] = {
2550 |           'true': [[
2551 |             { node: 'StaleNode1', type: 'main', index: 0 }
2552 |           ]],
2553 |           'false': [[
2554 |             { node: 'StaleNode2', type: 'main', index: 0 }
2555 |           ]]
2556 |         };
2557 | 
2558 |         const operations: CleanStaleConnectionsOperation[] = [{
2559 |           type: 'cleanStaleConnections'
2560 |         }];
2561 | 
2562 |         const request: WorkflowDiffRequest = {
2563 |           id: 'test-workflow',
2564 |           operations
2565 |         };
2566 | 
2567 |         const result = await diffEngine.applyDiff(workflow, request);
2568 | 
2569 |         expect(result.success).toBe(true);
2570 |         expect(result.workflow.connections['IF']).toBeUndefined();
2571 |       });
2572 | 
2573 |       it('should handle dryRun with multiple stale connections', async () => {
2574 |         const workflow = JSON.parse(JSON.stringify(baseWorkflow));
2575 | 
2576 |         // Add stale connections from both valid and invalid source nodes
2577 |         workflow.connections['GhostNode'] = {
2578 |           'main': [[{ node: 'HTTP Request', type: 'main', index: 0 }]]
2579 |         };
2580 | 
2581 |         if (!workflow.connections['Webhook']) {
2582 |           workflow.connections['Webhook'] = {};
2583 |         }
2584 |         workflow.connections['Webhook']['main'] = [[
2585 |           { node: 'HTTP Request', type: 'main', index: 0 },
2586 |           { node: 'StaleNode1', type: 'main', index: 0 },
2587 |           { node: 'StaleNode2', type: 'main', index: 0 }
2588 |         ]];
2589 | 
2590 |         const originalConnections = JSON.parse(JSON.stringify(workflow.connections));
2591 | 
2592 |         const operations: CleanStaleConnectionsOperation[] = [{
2593 |           type: 'cleanStaleConnections',
2594 |           dryRun: true
2595 |         }];
2596 | 
2597 |         const request: WorkflowDiffRequest = {
2598 |           id: 'test-workflow',
2599 |           operations
2600 |         };
2601 | 
2602 |         const result = await diffEngine.applyDiff(workflow, request);
2603 | 
2604 |         expect(result.success).toBe(true);
2605 |         // Connections should remain unchanged in dryRun
2606 |         expect(JSON.stringify(result.workflow.connections)).toBe(JSON.stringify(originalConnections));
2607 |       });
2608 | 
2609 |       it('should handle workflow with no stale connections', async () => {
2610 |         // Use baseWorkflow which has name-based connections
2611 |         const workflow = JSON.parse(JSON.stringify(baseWorkflow));
2612 |         const originalConnectionsCount = Object.keys(workflow.connections).length;
2613 | 
2614 |         const operations: CleanStaleConnectionsOperation[] = [{
2615 |           type: 'cleanStaleConnections'
2616 |         }];
2617 | 
2618 |         const request: WorkflowDiffRequest = {
2619 |           id: 'test-workflow',
2620 |           operations
2621 |         };
2622 | 
2623 |         const result = await diffEngine.applyDiff(workflow, request);
2624 | 
2625 |         expect(result.success).toBe(true);
2626 |         // Connections should remain unchanged (no stale connections to remove)
2627 |         // Verify by checking connection count
2628 |         expect(Object.keys(result.workflow.connections).length).toBe(originalConnectionsCount);
2629 |         expect(result.workflow.connections['Webhook']).toBeDefined();
2630 |         expect(result.workflow.connections['HTTP Request']).toBeDefined();
2631 |       });
2632 |     });
2633 | 
2634 |     describe('replaceConnections - Advanced Scenarios', () => {
2635 |       it('should fail validation when source node does not exist', async () => {
2636 |         const workflow = builder.build() as Workflow;
2637 | 
2638 |         const newConnections = {
2639 |           'NonExistentSource': {
2640 |             'main': [[
2641 |               { node: 'Slack', type: 'main', index: 0 }
2642 |             ]]
2643 |           }
2644 |         };
2645 | 
2646 |         const operations: ReplaceConnectionsOperation[] = [{
2647 |           type: 'replaceConnections',
2648 |           connections: newConnections
2649 |         }];
2650 | 
2651 |         const request: WorkflowDiffRequest = {
2652 |           id: 'test-workflow',
2653 |           operations
2654 |         };
2655 | 
2656 |         const result = await diffEngine.applyDiff(workflow, request);
2657 | 
2658 |         expect(result.success).toBe(false);
2659 |         expect(result.errors).toBeDefined();
2660 |         expect(result.errors![0].message).toContain('Source node not found');
2661 |       });
2662 | 
2663 |       it('should successfully replace with empty connections object', async () => {
2664 |         const workflow = builder.build() as Workflow;
2665 | 
2666 |         const operations: ReplaceConnectionsOperation[] = [{
2667 |           type: 'replaceConnections',
2668 |           connections: {}
2669 |         }];
2670 | 
2671 |         const request: WorkflowDiffRequest = {
2672 |           id: 'test-workflow',
2673 |           operations
2674 |         };
2675 | 
2676 |         const result = await diffEngine.applyDiff(workflow, request);
2677 | 
2678 |         expect(result.success).toBe(true);
2679 |         expect(result.workflow.connections).toEqual({});
2680 |       });
2681 | 
2682 |       it('should handle complex connection structures with multiple outputs', async () => {
2683 |         const workflow = builder.build() as Workflow;
2684 | 
2685 |         // Add IF node
2686 |         workflow.nodes.push({
2687 |           id: 'if-1',
2688 |           name: 'IF',
2689 |           type: 'n8n-nodes-base.if',
2690 |           typeVersion: 1,
2691 |           position: [600, 400],
2692 |           parameters: {}
2693 |         });
2694 | 
2695 |         const newConnections = {
2696 |           'Webhook': {
2697 |             'main': [[
2698 |               { node: 'IF', type: 'main', index: 0 }
2699 |             ]]
2700 |           },
2701 |           'IF': {
2702 |             'true': [[
2703 |               { node: 'Slack', type: 'main', index: 0 }
2704 |             ]],
2705 |             'false': [[
2706 |               { node: 'HTTP Request', type: 'main', index: 0 }
2707 |             ]]
2708 |           }
2709 |         };
2710 | 
2711 |         const operations: ReplaceConnectionsOperation[] = [{
2712 |           type: 'replaceConnections',
2713 |           connections: newConnections
2714 |         }];
2715 | 
2716 |         const request: WorkflowDiffRequest = {
2717 |           id: 'test-workflow',
2718 |           operations
2719 |         };
2720 | 
2721 |         const result = await diffEngine.applyDiff(workflow, request);
2722 | 
2723 |         expect(result.success).toBe(true);
2724 |         expect(result.workflow.connections).toEqual(newConnections);
2725 |       });
2726 |     });
2727 | 
2728 |     describe('removeConnection with ignoreErrors - Advanced Scenarios', () => {
2729 |       it('should succeed when source node does not exist with ignoreErrors', async () => {
2730 |         const workflow = builder.build() as Workflow;
2731 | 
2732 |         const operations: RemoveConnectionOperation[] = [{
2733 |           type: 'removeConnection',
2734 |           source: 'NonExistentSource',
2735 |           target: 'Slack',
2736 |           ignoreErrors: true
2737 |         }];
2738 | 
2739 |         const request: WorkflowDiffRequest = {
2740 |           id: 'test-workflow',
2741 |           operations
2742 |         };
2743 | 
2744 |         const result = await diffEngine.applyDiff(workflow, request);
2745 | 
2746 |         expect(result.success).toBe(true);
2747 |         // Workflow should remain unchanged (verify by checking node count)
2748 |         expect(Object.keys(result.workflow.connections).length).toBe(Object.keys(baseWorkflow.connections).length);
2749 |       });
2750 | 
2751 |       it('should succeed when both source and target nodes do not exist with ignoreErrors', async () => {
2752 |         const workflow = builder.build() as Workflow;
2753 | 
2754 |         const operations: RemoveConnectionOperation[] = [{
2755 |           type: 'removeConnection',
2756 |           source: 'NonExistentSource',
2757 |           target: 'NonExistentTarget',
2758 |           ignoreErrors: true
2759 |         }];
2760 | 
2761 |         const request: WorkflowDiffRequest = {
2762 |           id: 'test-workflow',
2763 |           operations
2764 |         };
2765 | 
2766 |         const result = await diffEngine.applyDiff(workflow, request);
2767 | 
2768 |         expect(result.success).toBe(true);
2769 |       });
2770 | 
2771 |       it('should succeed when connection exists but target node does not with ignoreErrors', async () => {
2772 |         const workflow = builder.build() as Workflow;
2773 | 
2774 |         // This is an edge case where connection references a valid node but we're trying to remove to non-existent
2775 |         const operations: RemoveConnectionOperation[] = [{
2776 |           type: 'removeConnection',
2777 |           source: 'Webhook',
2778 |           target: 'NonExistentTarget',
2779 |           ignoreErrors: true
2780 |         }];
2781 | 
2782 |         const request: WorkflowDiffRequest = {
2783 |           id: 'test-workflow',
2784 |           operations
2785 |         };
2786 | 
2787 |         const result = await diffEngine.applyDiff(workflow, request);
2788 | 
2789 |         expect(result.success).toBe(true);
2790 |       });
2791 | 
2792 |       it('should fail when source node does not exist without ignoreErrors', async () => {
2793 |         const workflow = builder.build() as Workflow;
2794 | 
2795 |         const operations: RemoveConnectionOperation[] = [{
2796 |           type: 'removeConnection',
2797 |           source: 'NonExistentSource',
2798 |           target: 'Slack',
2799 |           ignoreErrors: false
2800 |         }];
2801 | 
2802 |         const request: WorkflowDiffRequest = {
2803 |           id: 'test-workflow',
2804 |           operations
2805 |         };
2806 | 
2807 |         const result = await diffEngine.applyDiff(workflow, request);
2808 | 
2809 |         expect(result.success).toBe(false);
2810 |         expect(result.errors![0].message).toContain('Source node not found');
2811 |       });
2812 |     });
2813 | 
2814 |     describe('continueOnError - Advanced Scenarios', () => {
2815 |       it('should catch runtime errors during operation application', async () => {
2816 |         const workflow = builder.build() as Workflow;
2817 | 
2818 |         // Create an operation that will pass validation but fail during application
2819 |         // This is simulated by causing an error in the apply phase
2820 |         const operations: WorkflowDiffOperation[] = [
2821 |           {
2822 |             type: 'updateName',
2823 |             name: 'Valid Operation'
2824 |           } as UpdateNameOperation,
2825 |           {
2826 |             type: 'updateNode',
2827 |             nodeId: 'webhook-1',
2828 |             updates: {
2829 |               // This will pass validation but could fail in complex scenarios
2830 |               'parameters.invalidDeepPath.nested.value': 'test'
2831 |             }
2832 |           } as UpdateNodeOperation,
2833 |           {
2834 |             type: 'addTag',
2835 |             tag: 'another-valid'
2836 |           } as AddTagOperation
2837 |         ];
2838 | 
2839 |         const request: WorkflowDiffRequest = {
2840 |           id: 'test-workflow',
2841 |           operations,
2842 |           continueOnError: true
2843 |         };
2844 | 
2845 |         const result = await diffEngine.applyDiff(workflow, request);
2846 | 
2847 |         // All operations should succeed in this case (no runtime errors expected)
2848 |         expect(result.success).toBe(true);
2849 |         expect(result.applied).toBeDefined();
2850 |         expect(result.applied!.length).toBeGreaterThan(0);
2851 |       });
2852 | 
2853 |       it('should handle mixed validation and runtime errors', async () => {
2854 |         const workflow = builder.build() as Workflow;
2855 | 
2856 |         const operations: WorkflowDiffOperation[] = [
2857 |           {
2858 |             type: 'updateName',
2859 |             name: 'Operation 0'
2860 |           } as UpdateNameOperation,
2861 |           {
2862 |             type: 'removeNode',
2863 |             nodeId: 'non-existent-1'
2864 |           } as RemoveNodeOperation,
2865 |           {
2866 |             type: 'addTag',
2867 |             tag: 'tag1'
2868 |           } as AddTagOperation,
2869 |           {
2870 |             type: 'removeConnection',
2871 |             source: 'Webhook',
2872 |             target: 'NonExistent'
2873 |           } as RemoveConnectionOperation,
2874 |           {
2875 |             type: 'addTag',
2876 |             tag: 'tag2'
2877 |           } as AddTagOperation
2878 |         ];
2879 | 
2880 |         const request: WorkflowDiffRequest = {
2881 |           id: 'test-workflow',
2882 |           operations,
2883 |           continueOnError: true
2884 |         };
2885 | 
2886 |         const result = await diffEngine.applyDiff(workflow, request);
2887 | 
2888 |         expect(result.success).toBe(true);
2889 |         expect(result.applied).toContain(0); // updateName
2890 |         expect(result.applied).toContain(2); // first addTag
2891 |         expect(result.applied).toContain(4); // second addTag
2892 |         expect(result.failed).toContain(1); // removeNode
2893 |         expect(result.failed).toContain(3); // removeConnection
2894 |         expect(result.errors).toHaveLength(2);
2895 |       });
2896 | 
2897 |       it('should support validateOnly with continueOnError mode', async () => {
2898 |         const workflow = builder.build() as Workflow;
2899 | 
2900 |         const operations: WorkflowDiffOperation[] = [
2901 |           {
2902 |             type: 'updateName',
2903 |             name: 'New Name'
2904 |           } as UpdateNameOperation,
2905 |           {
2906 |             type: 'removeNode',
2907 |             nodeId: 'non-existent'
2908 |           } as RemoveNodeOperation,
2909 |           {
2910 |             type: 'addTag',
2911 |             tag: 'test-tag'
2912 |           } as AddTagOperation
2913 |         ];
2914 | 
2915 |         const request: WorkflowDiffRequest = {
2916 |           id: 'test-workflow',
2917 |           operations,
2918 |           continueOnError: true,
2919 |           validateOnly: true
2920 |         };
2921 | 
2922 |         const result = await diffEngine.applyDiff(workflow, request);
2923 | 
2924 |         expect(result.workflow).toBeUndefined();
2925 |         expect(result.message).toContain('Validation completed');
2926 |         expect(result.applied).toEqual([0, 2]);
2927 |         expect(result.failed).toEqual([1]);
2928 |         expect(result.errors).toHaveLength(1);
2929 |       });
2930 | 
2931 |       it('should handle all operations failing with helpful message', async () => {
2932 |         const workflow = builder.build() as Workflow;
2933 | 
2934 |         const operations: WorkflowDiffOperation[] = [
2935 |           {
2936 |             type: 'removeNode',
2937 |             nodeId: 'non-existent-1'
2938 |           } as RemoveNodeOperation,
2939 |           {
2940 |             type: 'removeNode',
2941 |             nodeId: 'non-existent-2'
2942 |           } as RemoveNodeOperation,
2943 |           {
2944 |             type: 'removeConnection',
2945 |             source: 'Invalid',
2946 |             target: 'Invalid'
2947 |           } as RemoveConnectionOperation
2948 |         ];
2949 | 
2950 |         const request: WorkflowDiffRequest = {
2951 |           id: 'test-workflow',
2952 |           operations,
2953 |           continueOnError: true
2954 |         };
2955 | 
2956 |         const result = await diffEngine.applyDiff(workflow, request);
2957 | 
2958 |         expect(result.success).toBe(false);
2959 |         expect(result.applied).toHaveLength(0);
2960 |         expect(result.failed).toEqual([0, 1, 2]);
2961 |         expect(result.errors).toHaveLength(3);
2962 |         expect(result.message).toContain('0 operations');
2963 |         expect(result.message).toContain('3 failed');
2964 |       });
2965 | 
2966 |       it('should preserve operation order in applied and failed arrays', async () => {
2967 |         const workflow = builder.build() as Workflow;
2968 | 
2969 |         const operations: WorkflowDiffOperation[] = [
2970 |           { type: 'updateName', name: 'Name1' } as UpdateNameOperation, // 0 - success
2971 |           { type: 'removeNode', nodeId: 'invalid1' } as RemoveNodeOperation, // 1 - fail
2972 |           { type: 'addTag', tag: 'tag1' } as AddTagOperation, // 2 - success
2973 |           { type: 'removeNode', nodeId: 'invalid2' } as RemoveNodeOperation, // 3 - fail
2974 |           { type: 'addTag', tag: 'tag2' } as AddTagOperation, // 4 - success
2975 |           { type: 'removeNode', nodeId: 'invalid3' } as RemoveNodeOperation, // 5 - fail
2976 |         ];
2977 | 
2978 |         const request: WorkflowDiffRequest = {
2979 |           id: 'test-workflow',
2980 |           operations,
2981 |           continueOnError: true
2982 |         };
2983 | 
2984 |         const result = await diffEngine.applyDiff(workflow, request);
2985 | 
2986 |         expect(result.success).toBe(true);
2987 |         expect(result.applied).toEqual([0, 2, 4]);
2988 |         expect(result.failed).toEqual([1, 3, 5]);
2989 |       });
2990 |     });
2991 | 
2992 |     describe('Edge Cases and Error Paths', () => {
2993 |       it('should handle workflow with initialized but empty connections', async () => {
2994 |         const workflow = builder.build() as Workflow;
2995 |         // Start with empty connections
2996 |         workflow.connections = {};
2997 | 
2998 |         // Add some nodes but no connections
2999 |         workflow.nodes.push({
3000 |           id: 'orphan-1',
3001 |           name: 'Orphan Node',
3002 |           type: 'n8n-nodes-base.code',
3003 |           typeVersion: 1,
3004 |           position: [800, 400],
3005 |           parameters: {}
3006 |         });
3007 | 
3008 |         const operations: CleanStaleConnectionsOperation[] = [{
3009 |           type: 'cleanStaleConnections'
3010 |         }];
3011 | 
3012 |         const request: WorkflowDiffRequest = {
3013 |           id: 'test-workflow',
3014 |           operations
3015 |         };
3016 | 
3017 |         const result = await diffEngine.applyDiff(workflow, request);
3018 | 
3019 |         expect(result.success).toBe(true);
3020 |         expect(result.workflow.connections).toEqual({});
3021 |       });
3022 | 
3023 |       it('should handle empty connections in cleanStaleConnections', async () => {
3024 |         const workflow = builder.build() as Workflow;
3025 |         workflow.connections = {};
3026 | 
3027 |         const operations: CleanStaleConnectionsOperation[] = [{
3028 |           type: 'cleanStaleConnections'
3029 |         }];
3030 | 
3031 |         const request: WorkflowDiffRequest = {
3032 |           id: 'test-workflow',
3033 |           operations
3034 |         };
3035 | 
3036 |         const result = await diffEngine.applyDiff(workflow, request);
3037 | 
3038 |         expect(result.success).toBe(true);
3039 |         expect(result.workflow.connections).toEqual({});
3040 |       });
3041 | 
3042 |       it('should handle removeConnection with ignoreErrors on valid but non-connected nodes', async () => {
3043 |         const workflow = builder.build() as Workflow;
3044 | 
3045 |         // Both nodes exist but no connection between them
3046 |         const operations: RemoveConnectionOperation[] = [{
3047 |           type: 'removeConnection',
3048 |           source: 'Slack',
3049 |           target: 'Webhook',
3050 |           ignoreErrors: true
3051 |         }];
3052 | 
3053 |         const request: WorkflowDiffRequest = {
3054 |           id: 'test-workflow',
3055 |           operations
3056 |         };
3057 | 
3058 |         const result = await diffEngine.applyDiff(workflow, request);
3059 | 
3060 |         expect(result.success).toBe(true);
3061 |       });
3062 | 
3063 |       it('should handle replaceConnections with nested connection arrays', async () => {
3064 |         const workflow = builder.build() as Workflow;
3065 | 
3066 |         const newConnections = {
3067 |           'Webhook': {
3068 |             'main': [
3069 |               [
3070 |                 { node: 'HTTP Request', type: 'main', index: 0 },
3071 |                 { node: 'Slack', type: 'main', index: 0 }
3072 |               ],
3073 |               [
3074 |                 { node: 'HTTP Request', type: 'main', index: 1 }
3075 |               ]
3076 |             ]
3077 |           }
3078 |         };
3079 | 
3080 |         const operations: ReplaceConnectionsOperation[] = [{
3081 |           type: 'replaceConnections',
3082 |           connections: newConnections
3083 |         }];
3084 | 
3085 |         const request: WorkflowDiffRequest = {
3086 |           id: 'test-workflow',
3087 |           operations
3088 |         };
3089 | 
3090 |         const result = await diffEngine.applyDiff(workflow, request);
3091 | 
3092 |         expect(result.success).toBe(true);
3093 |         expect(result.workflow.connections['Webhook']['main']).toHaveLength(2);
3094 |         expect(result.workflow.connections['Webhook']['main'][0]).toHaveLength(2);
3095 |         expect(result.workflow.connections['Webhook']['main'][1]).toHaveLength(1);
3096 |       });
3097 | 
3098 |       it('should validate cleanStaleConnections always returns null', async () => {
3099 |         const workflow = builder.build() as Workflow;
3100 | 
3101 |         // This tests that validation for cleanStaleConnections always passes
3102 |         const operations: CleanStaleConnectionsOperation[] = [{
3103 |           type: 'cleanStaleConnections'
3104 |         }];
3105 | 
3106 |         const request: WorkflowDiffRequest = {
3107 |           id: 'test-workflow',
3108 |           operations,
3109 |           validateOnly: true
3110 |         };
3111 | 
3112 |         const result = await diffEngine.applyDiff(workflow, request);
3113 | 
3114 |         expect(result.success).toBe(true);
3115 |         expect(result.message).toContain('Validation successful');
3116 |       });
3117 | 
3118 |       it('should handle continueOnError with no operations', async () => {
3119 |         const workflow = builder.build() as Workflow;
3120 | 
3121 |         const request: WorkflowDiffRequest = {
3122 |           id: 'test-workflow',
3123 |           operations: [],
3124 |           continueOnError: true
3125 |         };
3126 | 
3127 |         const result = await diffEngine.applyDiff(workflow, request);
3128 | 
3129 |         expect(result.success).toBe(false);
3130 |         expect(result.applied).toEqual([]);
3131 |         expect(result.failed).toEqual([]);
3132 |       });
3133 |     });
3134 | 
3135 |     describe('Integration Tests - v2.14.4 Features Combined', () => {
3136 |       it('should combine cleanStaleConnections and replaceConnections', async () => {
3137 |         const workflow = builder.build() as Workflow;
3138 | 
3139 |         // Add stale connections
3140 |         workflow.connections['GhostNode'] = {
3141 |           'main': [[{ node: 'Slack', type: 'main', index: 0 }]]
3142 |         };
3143 | 
3144 |         const operations: WorkflowDiffOperation[] = [
3145 |           {
3146 |             type: 'cleanStaleConnections'
3147 |           } as CleanStaleConnectionsOperation,
3148 |           {
3149 |             type: 'replaceConnections',
3150 |             connections: {
3151 |               'Webhook': {
3152 |                 'main': [[{ node: 'Slack', type: 'main', index: 0 }]]
3153 |               }
3154 |             }
3155 |           } as ReplaceConnectionsOperation
3156 |         ];
3157 | 
3158 |         const request: WorkflowDiffRequest = {
3159 |           id: 'test-workflow',
3160 |           operations
3161 |         };
3162 | 
3163 |         const result = await diffEngine.applyDiff(workflow, request);
3164 | 
3165 |         expect(result.success).toBe(true);
3166 |         expect(result.workflow.connections['GhostNode']).toBeUndefined();
3167 |         expect(result.workflow.connections['Webhook']['main'][0][0].node).toBe('Slack');
3168 |       });
3169 | 
3170 |       it('should use continueOnError with new v2.14.4 operations', async () => {
3171 |         const workflow = builder.build() as Workflow;
3172 | 
3173 |         const operations: WorkflowDiffOperation[] = [
3174 |           {
3175 |             type: 'cleanStaleConnections'
3176 |           } as CleanStaleConnectionsOperation,
3177 |           {
3178 |             type: 'replaceConnections',
3179 |             connections: {
3180 |               'NonExistentNode': {
3181 |                 'main': [[{ node: 'Slack', type: 'main', index: 0 }]]
3182 |               }
3183 |             }
3184 |           } as ReplaceConnectionsOperation,
3185 |           {
3186 |             type: 'removeConnection',
3187 |             source: 'Webhook',
3188 |             target: 'NonExistent',
3189 |             ignoreErrors: true
3190 |           } as RemoveConnectionOperation,
3191 |           {
3192 |             type: 'addTag',
3193 |             tag: 'final-tag'
3194 |           } as AddTagOperation
3195 |         ];
3196 | 
3197 |         const request: WorkflowDiffRequest = {
3198 |           id: 'test-workflow',
3199 |           operations,
3200 |           continueOnError: true
3201 |         };
3202 | 
3203 |         const result = await diffEngine.applyDiff(workflow, request);
3204 | 
3205 |         expect(result.success).toBe(true);
3206 |         expect(result.applied).toContain(0); // cleanStaleConnections
3207 |         expect(result.failed).toContain(1); // replaceConnections with invalid node
3208 |         expect(result.applied).toContain(2); // removeConnection with ignoreErrors
3209 |         expect(result.applied).toContain(3); // addTag
3210 |         expect(result.workflow.tags).toContain('final-tag');
3211 |       });
3212 |     });
3213 | 
3214 |     describe('Additional Edge Cases for 90% Coverage', () => {
3215 |       it('should handle cleanStaleConnections with connections from valid node to itself', async () => {
3216 |         const workflow = JSON.parse(JSON.stringify(baseWorkflow));
3217 | 
3218 |         // Add self-referencing connection
3219 |         if (!workflow.connections['Webhook']) {
3220 |           workflow.connections['Webhook'] = {};
3221 |         }
3222 |         workflow.connections['Webhook']['main'] = [[
3223 |           { node: 'Webhook', type: 'main', index: 0 },
3224 |           { node: 'HTTP Request', type: 'main', index: 0 }
3225 |         ]];
3226 | 
3227 |         const operations: CleanStaleConnectionsOperation[] = [{
3228 |           type: 'cleanStaleConnections'
3229 |         }];
3230 | 
3231 |         const request: WorkflowDiffRequest = {
3232 |           id: 'test-workflow',
3233 |           operations
3234 |         };
3235 | 
3236 |         const result = await diffEngine.applyDiff(workflow, request);
3237 | 
3238 |         expect(result.success).toBe(true);
3239 |         // Self-referencing connection should remain (it's valid)
3240 |         expect(result.workflow.connections['Webhook']['main'][0].some((c: any) => c.node === 'Webhook')).toBe(true);
3241 |       });
3242 | 
3243 |       it('should handle removeTag when tags array does not exist', async () => {
3244 |         const workflow = JSON.parse(JSON.stringify(baseWorkflow));
3245 |         delete workflow.tags;
3246 | 
3247 |         const operations: RemoveTagOperation[] = [{
3248 |           type: 'removeTag',
3249 |           tag: 'non-existent'
3250 |         }];
3251 | 
3252 |         const request: WorkflowDiffRequest = {
3253 |           id: 'test-workflow',
3254 |           operations
3255 |         };
3256 | 
3257 |         const result = await diffEngine.applyDiff(workflow, request);
3258 | 
3259 |         expect(result.success).toBe(true);
3260 |       });
3261 | 
3262 |       it('should handle cleanStaleConnections with multiple connection indices', async () => {
3263 |         const workflow = JSON.parse(JSON.stringify(baseWorkflow));
3264 | 
3265 |         // Add connections with multiple indices
3266 |         workflow.connections['Webhook'] = {
3267 |           'main': [
3268 |             [
3269 |               { node: 'HTTP Request', type: 'main', index: 0 },
3270 |               { node: 'Slack', type: 'main', index: 0 }
3271 |             ],
3272 |             [
3273 |               { node: 'StaleNode', type: 'main', index: 0 }
3274 |             ]
3275 |           ]
3276 |         };
3277 | 
3278 |         const operations: CleanStaleConnectionsOperation[] = [{
3279 |           type: 'cleanStaleConnections'
3280 |         }];
3281 | 
3282 |         const request: WorkflowDiffRequest = {
3283 |           id: 'test-workflow',
3284 |           operations
3285 |         };
3286 | 
3287 |         const result = await diffEngine.applyDiff(workflow, request);
3288 | 
3289 |         expect(result.success).toBe(true);
3290 |         // First index should remain with both valid connections
3291 |         expect(result.workflow.connections['Webhook']['main'][0]).toHaveLength(2);
3292 |         // Second index with stale node should be removed, so only one index remains
3293 |         expect(result.workflow.connections['Webhook']['main'].length).toBe(1);
3294 |       });
3295 | 
3296 |       it('should handle continueOnError with runtime error during apply', async () => {
3297 |         const workflow = JSON.parse(JSON.stringify(baseWorkflow));
3298 | 
3299 |         // Create a scenario that might cause runtime errors
3300 |         const operations: WorkflowDiffOperation[] = [
3301 |           {
3302 |             type: 'updateNode',
3303 |             nodeId: 'webhook-1',
3304 |             updates: {
3305 |               'parameters.test': 'value1'
3306 |             }
3307 |           } as UpdateNodeOperation,
3308 |           {
3309 |             type: 'removeNode',
3310 |             nodeId: 'invalid-node'
3311 |           } as RemoveNodeOperation,
3312 |           {
3313 |             type: 'updateNode',
3314 |             nodeName: 'HTTP Request',
3315 |             updates: {
3316 |               'parameters.test': 'value2'
3317 |             }
3318 |           } as UpdateNodeOperation
3319 |         ];
3320 | 
3321 |         const request: WorkflowDiffRequest = {
3322 |           id: 'test-workflow',
3323 |           operations,
3324 |           continueOnError: true
3325 |         };
3326 | 
3327 |         const result = await diffEngine.applyDiff(workflow, request);
3328 | 
3329 |         expect(result.applied).toContain(0);
3330 |         expect(result.failed).toContain(1);
3331 |         expect(result.applied).toContain(2);
3332 |       });
3333 | 
3334 |       it('should handle atomic mode failure in node operations', async () => {
3335 |         const workflow = JSON.parse(JSON.stringify(baseWorkflow));
3336 | 
3337 |         const operations: WorkflowDiffOperation[] = [
3338 |           {
3339 |             type: 'updateNode',
3340 |             nodeId: 'webhook-1',
3341 |             updates: {
3342 |               'parameters.valid': 'update'
3343 |             }
3344 |           } as UpdateNodeOperation,
3345 |           {
3346 |             type: 'removeNode',
3347 |             nodeId: 'invalid-node'
3348 |           } as RemoveNodeOperation
3349 |         ];
3350 | 
3351 |         const request: WorkflowDiffRequest = {
3352 |           id: 'test-workflow',
3353 |           operations
3354 |         };
3355 | 
3356 |         const result = await diffEngine.applyDiff(workflow, request);
3357 | 
3358 |         expect(result.success).toBe(false);
3359 |         expect(result.errors).toHaveLength(1);
3360 |         expect(result.errors![0].operation).toBe(1);
3361 |       });
3362 | 
3363 |       it('should handle atomic mode failure in connection operations', async () => {
3364 |         const workflow = JSON.parse(JSON.stringify(baseWorkflow));
3365 | 
3366 |         const operations: WorkflowDiffOperation[] = [
3367 |           {
3368 |             type: 'addNode',
3369 |             node: {
3370 |               name: 'NewNode',
3371 |               type: 'n8n-nodes-base.code',
3372 |               position: [900, 300],
3373 |               parameters: {}
3374 |             }
3375 |           } as AddNodeOperation,
3376 |           {
3377 |             type: 'addConnection',
3378 |             source: 'NewNode',
3379 |             target: 'InvalidTarget'
3380 |           } as AddConnectionOperation
3381 |         ];
3382 | 
3383 |         const request: WorkflowDiffRequest = {
3384 |           id: 'test-workflow',
3385 |           operations
3386 |         };
3387 | 
3388 |         const result = await diffEngine.applyDiff(workflow, request);
3389 | 
3390 |         expect(result.success).toBe(false);
3391 |         expect(result.errors).toHaveLength(1);
3392 |         expect(result.errors![0].operation).toBe(1);
3393 |       });
3394 | 
3395 |       it('should handle cleanStaleConnections in dryRun with source node missing', async () => {
3396 |         const workflow = JSON.parse(JSON.stringify(baseWorkflow));
3397 | 
3398 |         // Add connections from non-existent source
3399 |         workflow.connections['GhostSource1'] = {
3400 |           'main': [[{ node: 'Slack', type: 'main', index: 0 }]]
3401 |         };
3402 | 
3403 |         workflow.connections['GhostSource2'] = {
3404 |           'main': [[{ node: 'HTTP Request', type: 'main', index: 0 }]],
3405 |           'error': [[{ node: 'Slack', type: 'main', index: 0 }]]
3406 |         };
3407 | 
3408 |         const operations: CleanStaleConnectionsOperation[] = [{
3409 |           type: 'cleanStaleConnections',
3410 |           dryRun: true
3411 |         }];
3412 | 
3413 |         const request: WorkflowDiffRequest = {
3414 |           id: 'test-workflow',
3415 |           operations
3416 |         };
3417 | 
3418 |         const result = await diffEngine.applyDiff(workflow, request);
3419 | 
3420 |         expect(result.success).toBe(true);
3421 |         // In dryRun, connections should remain
3422 |         expect(result.workflow.connections['GhostSource1']).toBeDefined();
3423 |         expect(result.workflow.connections['GhostSource2']).toBeDefined();
3424 |       });
3425 | 
3426 |       it('should handle validateOnly in atomic mode', async () => {
3427 |         const workflow = JSON.parse(JSON.stringify(baseWorkflow));
3428 | 
3429 |         const operations: WorkflowDiffOperation[] = [
3430 |           {
3431 |             type: 'updateName',
3432 |             name: 'Validated Name'
3433 |           } as UpdateNameOperation,
3434 |           {
3435 |             type: 'addNode',
3436 |             node: {
3437 |               name: 'ValidNode',
3438 |               type: 'n8n-nodes-base.code',
3439 |               position: [900, 300],
3440 |               parameters: {}
3441 |             }
3442 |           } as AddNodeOperation
3443 |         ];
3444 | 
3445 |         const request: WorkflowDiffRequest = {
3446 |           id: 'test-workflow',
3447 |           operations,
3448 |           validateOnly: true
3449 |         };
3450 | 
3451 |         const result = await diffEngine.applyDiff(workflow, request);
3452 | 
3453 |         expect(result.success).toBe(true);
3454 |         expect(result.workflow).toBeUndefined();
3455 |         expect(result.message).toContain('Validation successful');
3456 |         expect(result.message).toContain('not applied');
3457 |       });
3458 | 
3459 |       it('should handle malformed workflow object gracefully', async () => {
3460 |         // Create a malformed workflow that will cause JSON parsing errors
3461 |         const malformedWorkflow: any = {
3462 |           name: 'Test',
3463 |           nodes: [],
3464 |           connections: {}
3465 |         };
3466 | 
3467 |         // Create circular reference to cause JSON.stringify to fail
3468 |         malformedWorkflow.self = malformedWorkflow;
3469 | 
3470 |         const operations: WorkflowDiffOperation[] = [{
3471 |           type: 'updateName',
3472 |           name: 'New Name'
3473 |         } as UpdateNameOperation];
3474 | 
3475 |         const request: WorkflowDiffRequest = {
3476 |           id: 'test-workflow',
3477 |           operations
3478 |         };
3479 | 
3480 |         const result = await diffEngine.applyDiff(malformedWorkflow, request);
3481 | 
3482 |         // Should handle the error gracefully
3483 |         expect(result.success).toBe(false);
3484 |         expect(result.errors).toBeDefined();
3485 |       });
3486 | 
3487 |       it('should handle continueOnError with all operations causing errors', async () => {
3488 |         const workflow = JSON.parse(JSON.stringify(baseWorkflow));
3489 | 
3490 |         const operations: WorkflowDiffOperation[] = [
3491 |           {
3492 |             type: 'removeNode',
3493 |             nodeId: 'invalid1'
3494 |           } as RemoveNodeOperation,
3495 |           {
3496 |             type: 'removeNode',
3497 |             nodeId: 'invalid2'
3498 |           } as RemoveNodeOperation,
3499 |           {
3500 |             type: 'addConnection',
3501 |             source: 'Invalid1',
3502 |             target: 'Invalid2'
3503 |           } as AddConnectionOperation
3504 |         ];
3505 | 
3506 |         const request: WorkflowDiffRequest = {
3507 |           id: 'test-workflow',
3508 |           operations,
3509 |           continueOnError: true
3510 |         };
3511 | 
3512 |         const result = await diffEngine.applyDiff(workflow, request);
3513 | 
3514 |         expect(result.success).toBe(false);
3515 |         expect(result.applied).toEqual([]);
3516 |         expect(result.failed).toEqual([0, 1, 2]);
3517 |         expect(result.errors).toHaveLength(3);
3518 |       });
3519 | 
3520 |       it('should handle atomic mode with empty operations array', async () => {
3521 |         const workflow = JSON.parse(JSON.stringify(baseWorkflow));
3522 | 
3523 |         const request: WorkflowDiffRequest = {
3524 |           id: 'test-workflow',
3525 |           operations: []
3526 |         };
3527 | 
3528 |         const result = await diffEngine.applyDiff(workflow, request);
3529 | 
3530 |         expect(result.success).toBe(true);
3531 |         expect(result.operationsApplied).toBe(0);
3532 |       });
3533 | 
3534 |       it('should handle removeConnection without sourceOutput specified', async () => {
3535 |         const workflow = JSON.parse(JSON.stringify(baseWorkflow));
3536 | 
3537 |         const operations: RemoveConnectionOperation[] = [{
3538 |           type: 'removeConnection',
3539 |           source: 'Webhook',
3540 |           target: 'HTTP Request'
3541 |           // sourceOutput not specified, should default to 'main'
3542 |         }];
3543 | 
3544 |         const request: WorkflowDiffRequest = {
3545 |           id: 'test-workflow',
3546 |           operations
3547 |         };
3548 | 
3549 |         const result = await diffEngine.applyDiff(workflow, request);
3550 | 
3551 |         expect(result.success).toBe(true);
3552 |       });
3553 | 
3554 |       it('should handle continueOnError validateOnly with all errors', async () => {
3555 |         const workflow = JSON.parse(JSON.stringify(baseWorkflow));
3556 | 
3557 |         const operations: WorkflowDiffOperation[] = [
3558 |           {
3559 |             type: 'removeNode',
3560 |             nodeId: 'invalid1'
3561 |           } as RemoveNodeOperation,
3562 |           {
3563 |             type: 'removeNode',
3564 |             nodeId: 'invalid2'
3565 |           } as RemoveNodeOperation
3566 |         ];
3567 | 
3568 |         const request: WorkflowDiffRequest = {
3569 |           id: 'test-workflow',
3570 |           operations,
3571 |           continueOnError: true,
3572 |           validateOnly: true
3573 |         };
3574 | 
3575 |         const result = await diffEngine.applyDiff(workflow, request);
3576 | 
3577 |         expect(result.success).toBe(false);
3578 |         expect(result.message).toContain('Validation completed');
3579 |         expect(result.errors).toHaveLength(2);
3580 |         expect(result.workflow).toBeUndefined();
3581 |       });
3582 | 
3583 | 
3584 |       it('should handle addConnection with all optional parameters specified', async () => {
3585 |         const workflow = JSON.parse(JSON.stringify(baseWorkflow));
3586 | 
3587 |         // Add Code node
3588 |         workflow.nodes.push({
3589 |           id: 'code-1',
3590 |           name: 'Code',
3591 |           type: 'n8n-nodes-base.code',
3592 |           typeVersion: 1,
3593 |           position: [900, 300],
3594 |           parameters: {}
3595 |         });
3596 | 
3597 |         const operations: AddConnectionOperation[] = [{
3598 |           type: 'addConnection',
3599 |           source: 'Slack',
3600 |           target: 'Code',
3601 |           sourceOutput: 'main',
3602 |           targetInput: 'main',
3603 |           sourceIndex: 0,
3604 |           targetIndex: 0
3605 |         }];
3606 | 
3607 |         const request: WorkflowDiffRequest = {
3608 |           id: 'test-workflow',
3609 |           operations
3610 |         };
3611 | 
3612 |         const result = await diffEngine.applyDiff(workflow, request);
3613 | 
3614 |         expect(result.success).toBe(true);
3615 |         expect(result.workflow.connections['Slack']['main'][0][0].node).toBe('Code');
3616 |         expect(result.workflow.connections['Slack']['main'][0][0].type).toBe('main');
3617 |         expect(result.workflow.connections['Slack']['main'][0][0].index).toBe(0);
3618 |       });
3619 | 
3620 |       it('should handle cleanStaleConnections actually removing source node connections', async () => {
3621 |         const workflow = JSON.parse(JSON.stringify(baseWorkflow));
3622 | 
3623 |         // Add connections from non-existent source that should be deleted entirely
3624 |         workflow.connections['NonExistentSource1'] = {
3625 |           'main': [[
3626 |             { node: 'Slack', type: 'main', index: 0 }
3627 |           ]]
3628 |         };
3629 | 
3630 |         workflow.connections['NonExistentSource2'] = {
3631 |           'main': [[
3632 |             { node: 'HTTP Request', type: 'main', index: 0 }
3633 |           ]],
3634 |           'error': [[
3635 |             { node: 'Slack', type: 'main', index: 0 }
3636 |           ]]
3637 |         };
3638 | 
3639 |         const operations: CleanStaleConnectionsOperation[] = [{
3640 |           type: 'cleanStaleConnections'
3641 |         }];
3642 | 
3643 |         const request: WorkflowDiffRequest = {
3644 |           id: 'test-workflow',
3645 |           operations
3646 |         };
3647 | 
3648 |         const result = await diffEngine.applyDiff(workflow, request);
3649 | 
3650 |         expect(result.success).toBe(true);
3651 |         expect(result.workflow.connections['NonExistentSource1']).toBeUndefined();
3652 |         expect(result.workflow.connections['NonExistentSource2']).toBeUndefined();
3653 |       });
3654 | 
3655 |       it('should handle validateOnly with no errors in continueOnError mode', async () => {
3656 |         const workflow = JSON.parse(JSON.stringify(baseWorkflow));
3657 | 
3658 |         const operations: WorkflowDiffOperation[] = [
3659 |           {
3660 |             type: 'updateName',
3661 |             name: 'Valid Name'
3662 |           } as UpdateNameOperation,
3663 |           {
3664 |             type: 'addTag',
3665 |             tag: 'valid-tag'
3666 |           } as AddTagOperation
3667 |         ];
3668 | 
3669 |         const request: WorkflowDiffRequest = {
3670 |           id: 'test-workflow',
3671 |           operations,
3672 |           continueOnError: true,
3673 |           validateOnly: true
3674 |         };
3675 | 
3676 |         const result = await diffEngine.applyDiff(workflow, request);
3677 | 
3678 |         expect(result.success).toBe(true);
3679 |         expect(result.message).toContain('Validation successful');
3680 |         expect(result.errors).toBeUndefined();
3681 |         expect(result.applied).toEqual([0, 1]);
3682 |         expect(result.failed).toEqual([]);
3683 |       });
3684 | 
3685 |       it('should handle addConnection initializing missing connection structure', async () => {
3686 |         const workflow = JSON.parse(JSON.stringify(baseWorkflow));
3687 | 
3688 |         // Add node without any connections
3689 |         workflow.nodes.push({
3690 |           id: 'orphan-1',
3691 |           name: 'Orphan',
3692 |           type: 'n8n-nodes-base.code',
3693 |           typeVersion: 1,
3694 |           position: [900, 300],
3695 |           parameters: {}
3696 |         });
3697 | 
3698 |         // Ensure Orphan has no connections initially
3699 |         delete workflow.connections['Orphan'];
3700 | 
3701 |         const operations: AddConnectionOperation[] = [{
3702 |           type: 'addConnection',
3703 |           source: 'Orphan',
3704 |           target: 'Slack'
3705 |         }];
3706 | 
3707 |         const request: WorkflowDiffRequest = {
3708 |           id: 'test-workflow',
3709 |           operations
3710 |         };
3711 | 
3712 |         const result = await diffEngine.applyDiff(workflow, request);
3713 | 
3714 |         expect(result.success).toBe(true);
3715 |         expect(result.workflow.connections['Orphan']).toBeDefined();
3716 |         expect(result.workflow.connections['Orphan']['main']).toBeDefined();
3717 |         expect(result.workflow.connections['Orphan']['main'][0][0].node).toBe('Slack');
3718 |       });
3719 | 
3720 |       it('should handle addConnection with sourceIndex requiring array expansion', async () => {
3721 |         const workflow = JSON.parse(JSON.stringify(baseWorkflow));
3722 | 
3723 |         // Add Code node
3724 |         workflow.nodes.push({
3725 |           id: 'code-1',
3726 |           name: 'Code',
3727 |           type: 'n8n-nodes-base.code',
3728 |           typeVersion: 1,
3729 |           position: [900, 300],
3730 |           parameters: {}
3731 |         });
3732 | 
3733 |         const operations: AddConnectionOperation[] = [{
3734 |           type: 'addConnection',
3735 |           source: 'Slack',
3736 |           target: 'Code',
3737 |           sourceIndex: 5 // Force array expansion to index 5
3738 |         }];
3739 | 
3740 |         const request: WorkflowDiffRequest = {
3741 |           id: 'test-workflow',
3742 |           operations
3743 |         };
3744 | 
3745 |         const result = await diffEngine.applyDiff(workflow, request);
3746 | 
3747 |         expect(result.success).toBe(true);
3748 |         expect(result.workflow.connections['Slack']['main'].length).toBeGreaterThanOrEqual(6);
3749 |         expect(result.workflow.connections['Slack']['main'][5][0].node).toBe('Code');
3750 |       });
3751 | 
3752 |       it('should handle removeConnection cleaning up empty output structures', async () => {
3753 |         const workflow = JSON.parse(JSON.stringify(baseWorkflow));
3754 | 
3755 |         // Set up a connection that will leave empty structures after removal
3756 |         workflow.connections['HTTP Request'] = {
3757 |           'main': [[
3758 |             { node: 'Slack', type: 'main', index: 0 }
3759 |           ]]
3760 |         };
3761 | 
3762 |         const operations: RemoveConnectionOperation[] = [{
3763 |           type: 'removeConnection',
3764 |           source: 'HTTP Request',
3765 |           target: 'Slack'
3766 |         }];
3767 | 
3768 |         const request: WorkflowDiffRequest = {
3769 |           id: 'test-workflow',
3770 |           operations
3771 |         };
3772 | 
3773 |         const result = await diffEngine.applyDiff(workflow, request);
3774 | 
3775 |         expect(result.success).toBe(true);
3776 |         // Connection should be removed entirely (cleanup of empty structures)
3777 |         expect(result.workflow.connections['HTTP Request']).toBeUndefined();
3778 |       });
3779 | 
3780 |       it('should handle complex cleanStaleConnections scenario with mixed valid/invalid', async () => {
3781 |         const workflow = JSON.parse(JSON.stringify(baseWorkflow));
3782 | 
3783 |         // Create a complex scenario with multiple source nodes
3784 |         workflow.connections['Webhook'] = {
3785 |           'main': [[
3786 |             { node: 'HTTP Request', type: 'main', index: 0 },
3787 |             { node: 'Stale1', type: 'main', index: 0 },
3788 |             { node: 'Slack', type: 'main', index: 0 },
3789 |             { node: 'Stale2', type: 'main', index: 0 }
3790 |           ]],
3791 |           'error': [[
3792 |             { node: 'Stale3', type: 'main', index: 0 }
3793 |           ]]
3794 |         };
3795 | 
3796 |         const operations: CleanStaleConnectionsOperation[] = [{
3797 |           type: 'cleanStaleConnections'
3798 |         }];
3799 | 
3800 |         const request: WorkflowDiffRequest = {
3801 |           id: 'test-workflow',
3802 |           operations
3803 |         };
3804 | 
3805 |         const result = await diffEngine.applyDiff(workflow, request);
3806 | 
3807 |         expect(result.success).toBe(true);
3808 |         // Only valid connections should remain
3809 |         expect(result.workflow.connections['Webhook']['main'][0]).toHaveLength(2);
3810 |         expect(result.workflow.connections['Webhook']['main'][0].some((c: any) => c.node === 'HTTP Request')).toBe(true);
3811 |         expect(result.workflow.connections['Webhook']['main'][0].some((c: any) => c.node === 'Slack')).toBe(true);
3812 |         // Error output should be removed entirely (all stale)
3813 |         expect(result.workflow.connections['Webhook']['error']).toBeUndefined();
3814 |       });
3815 |     });
3816 |   });
3817 | 
3818 |   // Issue #270: Special characters in node names
3819 |   describe('Special Characters in Node Names', () => {
3820 |     it('should handle apostrophes in node names for addConnection', async () => {
3821 |       // Default n8n Manual Trigger node name contains apostrophes
3822 |       const workflowWithApostrophes = {
3823 |         ...baseWorkflow,
3824 |         nodes: [
3825 |           ...baseWorkflow.nodes,
3826 |           {
3827 |             id: 'manual-trigger-1',
3828 |             name: "When clicking 'Execute workflow'", // Contains apostrophes
3829 |             type: 'n8n-nodes-base.manualTrigger',
3830 |             typeVersion: 1,
3831 |             position: [100, 100] as [number, number],
3832 |             parameters: {}
3833 |           }
3834 |         ]
3835 |       };
3836 | 
3837 |       const operation: AddConnectionOperation = {
3838 |         type: 'addConnection',
3839 |         source: "When clicking 'Execute workflow'",  // Using node name with apostrophes
3840 |         target: 'HTTP Request'
3841 |       };
3842 | 
3843 |       const request: WorkflowDiffRequest = {
3844 |         id: 'test-workflow',
3845 |         operations: [operation]
3846 |       };
3847 | 
3848 |       const result = await diffEngine.applyDiff(workflowWithApostrophes as Workflow, request);
3849 | 
3850 |       expect(result.success).toBe(true);
3851 |       expect(result.workflow.connections["When clicking 'Execute workflow'"]).toBeDefined();
3852 |       expect(result.workflow.connections["When clicking 'Execute workflow'"].main).toBeDefined();
3853 |     });
3854 | 
3855 |     it('should handle double quotes in node names', async () => {
3856 |       const workflowWithQuotes = {
3857 |         ...baseWorkflow,
3858 |         nodes: [
3859 |           ...baseWorkflow.nodes,
3860 |           {
3861 |             id: 'quoted-node-1',
3862 |             name: 'Node with "quotes"',  // Contains double quotes
3863 |             type: 'n8n-nodes-base.set',
3864 |             typeVersion: 1,
3865 |             position: [100, 100] as [number, number],
3866 |             parameters: {}
3867 |           }
3868 |         ]
3869 |       };
3870 | 
3871 |       const operation: AddConnectionOperation = {
3872 |         type: 'addConnection',
3873 |         source: 'Node with "quotes"',
3874 |         target: 'HTTP Request'
3875 |       };
3876 | 
3877 |       const request: WorkflowDiffRequest = {
3878 |         id: 'test-workflow',
3879 |         operations: [operation]
3880 |       };
3881 | 
3882 |       const result = await diffEngine.applyDiff(workflowWithQuotes as Workflow, request);
3883 | 
3884 |       expect(result.success).toBe(true);
3885 |       expect(result.workflow.connections['Node with "quotes"']).toBeDefined();
3886 |     });
3887 | 
3888 |     it('should handle backslashes in node names', async () => {
3889 |       const workflowWithBackslashes = {
3890 |         ...baseWorkflow,
3891 |         nodes: [
3892 |           ...baseWorkflow.nodes,
3893 |           {
3894 |             id: 'backslash-node-1',
3895 |             name: 'Path\\with\\backslashes',  // Contains backslashes
3896 |             type: 'n8n-nodes-base.set',
3897 |             typeVersion: 1,
3898 |             position: [100, 100] as [number, number],
3899 |             parameters: {}
3900 |           }
3901 |         ]
3902 |       };
3903 | 
3904 |       const operation: AddConnectionOperation = {
3905 |         type: 'addConnection',
3906 |         source: 'Path\\with\\backslashes',
3907 |         target: 'HTTP Request'
3908 |       };
3909 | 
3910 |       const request: WorkflowDiffRequest = {
3911 |         id: 'test-workflow',
3912 |         operations: [operation]
3913 |       };
3914 | 
3915 |       const result = await diffEngine.applyDiff(workflowWithBackslashes as Workflow, request);
3916 | 
3917 |       expect(result.success).toBe(true);
3918 |       expect(result.workflow.connections['Path\\with\\backslashes']).toBeDefined();
3919 |     });
3920 | 
3921 |     it('should handle mixed special characters in node names', async () => {
3922 |       const workflowWithMixed = {
3923 |         ...baseWorkflow,
3924 |         nodes: [
3925 |           ...baseWorkflow.nodes,
3926 |           {
3927 |             id: 'complex-node-1',
3928 |             name: "Complex 'name' with \"quotes\" and \\backslash",
3929 |             type: 'n8n-nodes-base.set',
3930 |             typeVersion: 1,
3931 |             position: [100, 100] as [number, number],
3932 |             parameters: {}
3933 |           }
3934 |         ]
3935 |       };
3936 | 
3937 |       const operation: AddConnectionOperation = {
3938 |         type: 'addConnection',
3939 |         source: "Complex 'name' with \"quotes\" and \\backslash",
3940 |         target: 'HTTP Request'
3941 |       };
3942 | 
3943 |       const request: WorkflowDiffRequest = {
3944 |         id: 'test-workflow',
3945 |         operations: [operation]
3946 |       };
3947 | 
3948 |       const result = await diffEngine.applyDiff(workflowWithMixed as Workflow, request);
3949 | 
3950 |       expect(result.success).toBe(true);
3951 |       expect(result.workflow.connections["Complex 'name' with \"quotes\" and \\backslash"]).toBeDefined();
3952 |     });
3953 | 
3954 |     it('should handle special characters in removeConnection', async () => {
3955 |       const workflowWithConnections = {
3956 |         ...baseWorkflow,
3957 |         nodes: [
3958 |           ...baseWorkflow.nodes,
3959 |           {
3960 |             id: 'apostrophe-node-1',
3961 |             name: "Node with 'apostrophes'",
3962 |             type: 'n8n-nodes-base.set',
3963 |             typeVersion: 1,
3964 |             position: [100, 100] as [number, number],
3965 |             parameters: {}
3966 |           }
3967 |         ],
3968 |         connections: {
3969 |           ...baseWorkflow.connections,
3970 |           "Node with 'apostrophes'": {
3971 |             main: [[{ node: 'HTTP Request', type: 'main', index: 0 }]]
3972 |           }
3973 |         }
3974 |       };
3975 | 
3976 |       const operation: RemoveConnectionOperation = {
3977 |         type: 'removeConnection',
3978 |         source: "Node with 'apostrophes'",
3979 |         target: 'HTTP Request'
3980 |       };
3981 | 
3982 |       const request: WorkflowDiffRequest = {
3983 |         id: 'test-workflow',
3984 |         operations: [operation]
3985 |       };
3986 | 
3987 |       const result = await diffEngine.applyDiff(workflowWithConnections as any, request);
3988 | 
3989 |       expect(result.success).toBe(true);
3990 |       expect(result.workflow.connections["Node with 'apostrophes'"]).toBeUndefined();
3991 |     });
3992 | 
3993 |     it('should handle special characters in updateNode', async () => {
3994 |       const workflowWithSpecialNode = {
3995 |         ...baseWorkflow,
3996 |         nodes: [
3997 |           ...baseWorkflow.nodes,
3998 |           {
3999 |             id: 'special-node-1',
4000 |             name: "Update 'this' node",
4001 |             type: 'n8n-nodes-base.set',
4002 |             typeVersion: 1,
4003 |             position: [100, 100] as [number, number],
4004 |             parameters: { value: 'old' }
4005 |           }
4006 |         ]
4007 |       };
4008 | 
4009 |       const operation: UpdateNodeOperation = {
4010 |         type: 'updateNode',
4011 |         nodeName: "Update 'this' node",
4012 |         updates: {
4013 |           'parameters.value': 'new'
4014 |         }
4015 |       };
4016 | 
4017 |       const request: WorkflowDiffRequest = {
4018 |         id: 'test-workflow',
4019 |         operations: [operation]
4020 |       };
4021 | 
4022 |       const result = await diffEngine.applyDiff(workflowWithSpecialNode as Workflow, request);
4023 | 
4024 |       expect(result.success).toBe(true);
4025 |       const updatedNode = result.workflow.nodes.find((n: any) => n.name === "Update 'this' node");
4026 |       expect(updatedNode?.parameters.value).toBe('new');
4027 |     });
4028 | 
4029 |     // Code Review Fix: Test whitespace normalization
4030 |     it('should handle tabs in node names', async () => {
4031 |       const workflowWithTabs = {
4032 |         ...baseWorkflow,
4033 |         nodes: [
4034 |           ...baseWorkflow.nodes,
4035 |           {
4036 |             id: 'tab-node-1',
4037 |             name: "Node\twith\ttabs",  // Contains tabs
4038 |             type: 'n8n-nodes-base.set',
4039 |             typeVersion: 1,
4040 |             position: [100, 100] as [number, number],
4041 |             parameters: {}
4042 |           }
4043 |         ]
4044 |       };
4045 | 
4046 |       const operation: AddConnectionOperation = {
4047 |         type: 'addConnection',
4048 |         source: "Node\twith\ttabs",  // Tabs should normalize to single spaces
4049 |         target: 'HTTP Request'
4050 |       };
4051 | 
4052 |       const request: WorkflowDiffRequest = {
4053 |         id: 'test-workflow',
4054 |         operations: [operation]
4055 |       };
4056 | 
4057 |       const result = await diffEngine.applyDiff(workflowWithTabs as Workflow, request);
4058 | 
4059 |       expect(result.success).toBe(true);
4060 |       // After normalization, both "Node\twith\ttabs" and "Node with tabs" should match
4061 |       expect(result.workflow.connections["Node\twith\ttabs"]).toBeDefined();
4062 |     });
4063 | 
4064 |     it('should handle newlines in node names', async () => {
4065 |       const workflowWithNewlines = {
4066 |         ...baseWorkflow,
4067 |         nodes: [
4068 |           ...baseWorkflow.nodes,
4069 |           {
4070 |             id: 'newline-node-1',
4071 |             name: "Node\nwith\nnewlines",  // Contains newlines
4072 |             type: 'n8n-nodes-base.set',
4073 |             typeVersion: 1,
4074 |             position: [100, 100] as [number, number],
4075 |             parameters: {}
4076 |           }
4077 |         ]
4078 |       };
4079 | 
4080 |       const operation: AddConnectionOperation = {
4081 |         type: 'addConnection',
4082 |         source: "Node\nwith\nnewlines",  // Newlines should normalize to single spaces
4083 |         target: 'HTTP Request'
4084 |       };
4085 | 
4086 |       const request: WorkflowDiffRequest = {
4087 |         id: 'test-workflow',
4088 |         operations: [operation]
4089 |       };
4090 | 
4091 |       const result = await diffEngine.applyDiff(workflowWithNewlines as Workflow, request);
4092 | 
4093 |       expect(result.success).toBe(true);
4094 |       expect(result.workflow.connections["Node\nwith\nnewlines"]).toBeDefined();
4095 |     });
4096 | 
4097 |     it('should handle mixed whitespace (tabs, newlines, spaces)', async () => {
4098 |       const workflowWithMixed = {
4099 |         ...baseWorkflow,
4100 |         nodes: [
4101 |           ...baseWorkflow.nodes,
4102 |           {
4103 |             id: 'mixed-whitespace-node-1',
4104 |             name: "Node\t  \n  with  \r\nmixed",  // Mixed whitespace
4105 |             type: 'n8n-nodes-base.set',
4106 |             typeVersion: 1,
4107 |             position: [100, 100] as [number, number],
4108 |             parameters: {}
4109 |           }
4110 |         ]
4111 |       };
4112 | 
4113 |       const operation: AddConnectionOperation = {
4114 |         type: 'addConnection',
4115 |         source: "Node\t  \n  with  \r\nmixed",  // Should normalize all whitespace
4116 |         target: 'HTTP Request'
4117 |       };
4118 | 
4119 |       const request: WorkflowDiffRequest = {
4120 |         id: 'test-workflow',
4121 |         operations: [operation]
4122 |       };
4123 | 
4124 |       const result = await diffEngine.applyDiff(workflowWithMixed as Workflow, request);
4125 | 
4126 |       expect(result.success).toBe(true);
4127 |       expect(result.workflow.connections["Node\t  \n  with  \r\nmixed"]).toBeDefined();
4128 |     });
4129 | 
4130 |     // Code Review Fix: Test escaped vs unescaped matching (core issue #270 scenario)
4131 |     it('should match escaped input with unescaped stored names (Issue #270 core scenario)', async () => {
4132 |       // Scenario: AI/JSON-RPC sends escaped name, n8n workflow has unescaped name
4133 |       const workflowWithUnescaped = {
4134 |         ...baseWorkflow,
4135 |         nodes: [
4136 |           ...baseWorkflow.nodes,
4137 |           {
4138 |             id: 'test-node',
4139 |             name: "When clicking 'Execute workflow'",  // Unescaped (how n8n stores it)
4140 |             type: 'n8n-nodes-base.manualTrigger',
4141 |             typeVersion: 1,
4142 |             position: [100, 100] as [number, number],
4143 |             parameters: {}
4144 |           }
4145 |         ]
4146 |       };
4147 | 
4148 |       const operation: AddConnectionOperation = {
4149 |         type: 'addConnection',
4150 |         source: "When clicking \\'Execute workflow\\'",  // Escaped (how JSON-RPC might send it)
4151 |         target: 'HTTP Request'
4152 |       };
4153 | 
4154 |       const request: WorkflowDiffRequest = {
4155 |         id: 'test-workflow',
4156 |         operations: [operation]
4157 |       };
4158 | 
4159 |       const result = await diffEngine.applyDiff(workflowWithUnescaped as Workflow, request);
4160 | 
4161 |       expect(result.success).toBe(true);  // Should match despite different escaping
4162 |       expect(result.workflow.connections["When clicking 'Execute workflow'"]).toBeDefined();
4163 |     });
4164 |   });
4165 | });
```
Page 57/59FirstPrevNextLast