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