This is page 37 of 45. Use http://codebase.md/czlonkowski/n8n-mcp?lines=false&page={x} to view the full context. # Directory Structure ``` ├── _config.yml ├── .claude │ └── agents │ ├── code-reviewer.md │ ├── context-manager.md │ ├── debugger.md │ ├── deployment-engineer.md │ ├── mcp-backend-engineer.md │ ├── n8n-mcp-tester.md │ ├── technical-researcher.md │ └── test-automator.md ├── .dockerignore ├── .env.docker ├── .env.example ├── .env.n8n.example ├── .env.test ├── .env.test.example ├── .github │ ├── ABOUT.md │ ├── BENCHMARK_THRESHOLDS.md │ ├── FUNDING.yml │ ├── gh-pages.yml │ ├── secret_scanning.yml │ └── workflows │ ├── benchmark-pr.yml │ ├── benchmark.yml │ ├── docker-build-fast.yml │ ├── docker-build-n8n.yml │ ├── docker-build.yml │ ├── release.yml │ ├── test.yml │ └── update-n8n-deps.yml ├── .gitignore ├── .npmignore ├── ATTRIBUTION.md ├── CHANGELOG.md ├── CLAUDE.md ├── codecov.yml ├── coverage.json ├── data │ ├── .gitkeep │ ├── nodes.db │ ├── nodes.db-shm │ ├── nodes.db-wal │ └── templates.db ├── deploy │ └── quick-deploy-n8n.sh ├── docker │ ├── docker-entrypoint.sh │ ├── n8n-mcp │ ├── parse-config.js │ └── README.md ├── docker-compose.buildkit.yml ├── docker-compose.extract.yml ├── docker-compose.n8n.yml ├── docker-compose.override.yml.example ├── docker-compose.test-n8n.yml ├── docker-compose.yml ├── Dockerfile ├── Dockerfile.railway ├── Dockerfile.test ├── docs │ ├── AUTOMATED_RELEASES.md │ ├── BENCHMARKS.md │ ├── CHANGELOG.md │ ├── CLAUDE_CODE_SETUP.md │ ├── CLAUDE_INTERVIEW.md │ ├── CODECOV_SETUP.md │ ├── CODEX_SETUP.md │ ├── CURSOR_SETUP.md │ ├── DEPENDENCY_UPDATES.md │ ├── DOCKER_README.md │ ├── DOCKER_TROUBLESHOOTING.md │ ├── FINAL_AI_VALIDATION_SPEC.md │ ├── FLEXIBLE_INSTANCE_CONFIGURATION.md │ ├── HTTP_DEPLOYMENT.md │ ├── img │ │ ├── cc_command.png │ │ ├── cc_connected.png │ │ ├── codex_connected.png │ │ ├── cursor_tut.png │ │ ├── Railway_api.png │ │ ├── Railway_server_address.png │ │ ├── vsc_ghcp_chat_agent_mode.png │ │ ├── vsc_ghcp_chat_instruction_files.png │ │ ├── vsc_ghcp_chat_thinking_tool.png │ │ └── windsurf_tut.png │ ├── INSTALLATION.md │ ├── LIBRARY_USAGE.md │ ├── local │ │ ├── DEEP_DIVE_ANALYSIS_2025-10-02.md │ │ ├── DEEP_DIVE_ANALYSIS_README.md │ │ ├── Deep_dive_p1_p2.md │ │ ├── integration-testing-plan.md │ │ ├── integration-tests-phase1-summary.md │ │ ├── N8N_AI_WORKFLOW_BUILDER_ANALYSIS.md │ │ ├── P0_IMPLEMENTATION_PLAN.md │ │ └── TEMPLATE_MINING_ANALYSIS.md │ ├── MCP_ESSENTIALS_README.md │ ├── MCP_QUICK_START_GUIDE.md │ ├── N8N_DEPLOYMENT.md │ ├── RAILWAY_DEPLOYMENT.md │ ├── README_CLAUDE_SETUP.md │ ├── README.md │ ├── tools-documentation-usage.md │ ├── VS_CODE_PROJECT_SETUP.md │ ├── WINDSURF_SETUP.md │ └── workflow-diff-examples.md ├── examples │ └── enhanced-documentation-demo.js ├── fetch_log.txt ├── LICENSE ├── MEMORY_N8N_UPDATE.md ├── MEMORY_TEMPLATE_UPDATE.md ├── monitor_fetch.sh ├── N8N_HTTP_STREAMABLE_SETUP.md ├── n8n-nodes.db ├── P0-R3-TEST-PLAN.md ├── package-lock.json ├── package.json ├── package.runtime.json ├── PRIVACY.md ├── railway.json ├── README.md ├── renovate.json ├── scripts │ ├── analyze-optimization.sh │ ├── audit-schema-coverage.ts │ ├── build-optimized.sh │ ├── compare-benchmarks.js │ ├── demo-optimization.sh │ ├── deploy-http.sh │ ├── deploy-to-vm.sh │ ├── export-webhook-workflows.ts │ ├── extract-changelog.js │ ├── extract-from-docker.js │ ├── extract-nodes-docker.sh │ ├── extract-nodes-simple.sh │ ├── format-benchmark-results.js │ ├── generate-benchmark-stub.js │ ├── generate-detailed-reports.js │ ├── generate-test-summary.js │ ├── http-bridge.js │ ├── mcp-http-client.js │ ├── migrate-nodes-fts.ts │ ├── migrate-tool-docs.ts │ ├── n8n-docs-mcp.service │ ├── nginx-n8n-mcp.conf │ ├── prebuild-fts5.ts │ ├── prepare-release.js │ ├── publish-npm-quick.sh │ ├── publish-npm.sh │ ├── quick-test.ts │ ├── run-benchmarks-ci.js │ ├── sync-runtime-version.js │ ├── test-ai-validation-debug.ts │ ├── test-code-node-enhancements.ts │ ├── test-code-node-fixes.ts │ ├── test-docker-config.sh │ ├── test-docker-fingerprint.ts │ ├── test-docker-optimization.sh │ ├── test-docker.sh │ ├── test-empty-connection-validation.ts │ ├── test-error-message-tracking.ts │ ├── test-error-output-validation.ts │ ├── test-error-validation.js │ ├── test-essentials.ts │ ├── test-expression-code-validation.ts │ ├── test-expression-format-validation.js │ ├── test-fts5-search.ts │ ├── test-fuzzy-fix.ts │ ├── test-fuzzy-simple.ts │ ├── test-helpers-validation.ts │ ├── test-http-search.ts │ ├── test-http.sh │ ├── test-jmespath-validation.ts │ ├── test-multi-tenant-simple.ts │ ├── test-multi-tenant.ts │ ├── test-n8n-integration.sh │ ├── test-node-info.js │ ├── test-node-type-validation.ts │ ├── test-nodes-base-prefix.ts │ ├── test-operation-validation.ts │ ├── test-optimized-docker.sh │ ├── test-release-automation.js │ ├── test-search-improvements.ts │ ├── test-security.ts │ ├── test-single-session.sh │ ├── test-sqljs-triggers.ts │ ├── test-telemetry-debug.ts │ ├── test-telemetry-direct.ts │ ├── test-telemetry-env.ts │ ├── test-telemetry-integration.ts │ ├── test-telemetry-no-select.ts │ ├── test-telemetry-security.ts │ ├── test-telemetry-simple.ts │ ├── test-typeversion-validation.ts │ ├── test-url-configuration.ts │ ├── test-user-id-persistence.ts │ ├── test-webhook-validation.ts │ ├── test-workflow-insert.ts │ ├── test-workflow-sanitizer.ts │ ├── test-workflow-tracking-debug.ts │ ├── update-and-publish-prep.sh │ ├── update-n8n-deps.js │ ├── update-readme-version.js │ ├── vitest-benchmark-json-reporter.js │ └── vitest-benchmark-reporter.ts ├── SECURITY.md ├── src │ ├── config │ │ └── n8n-api.ts │ ├── data │ │ └── canonical-ai-tool-examples.json │ ├── database │ │ ├── database-adapter.ts │ │ ├── migrations │ │ │ └── add-template-node-configs.sql │ │ ├── node-repository.ts │ │ ├── nodes.db │ │ ├── schema-optimized.sql │ │ └── schema.sql │ ├── errors │ │ └── validation-service-error.ts │ ├── http-server-single-session.ts │ ├── http-server.ts │ ├── index.ts │ ├── loaders │ │ └── node-loader.ts │ ├── mappers │ │ └── docs-mapper.ts │ ├── mcp │ │ ├── handlers-n8n-manager.ts │ │ ├── handlers-workflow-diff.ts │ │ ├── index.ts │ │ ├── server.ts │ │ ├── stdio-wrapper.ts │ │ ├── tool-docs │ │ │ ├── configuration │ │ │ │ ├── get-node-as-tool-info.ts │ │ │ │ ├── get-node-documentation.ts │ │ │ │ ├── get-node-essentials.ts │ │ │ │ ├── get-node-info.ts │ │ │ │ ├── get-property-dependencies.ts │ │ │ │ ├── index.ts │ │ │ │ └── search-node-properties.ts │ │ │ ├── discovery │ │ │ │ ├── get-database-statistics.ts │ │ │ │ ├── index.ts │ │ │ │ ├── list-ai-tools.ts │ │ │ │ ├── list-nodes.ts │ │ │ │ └── search-nodes.ts │ │ │ ├── guides │ │ │ │ ├── ai-agents-guide.ts │ │ │ │ └── index.ts │ │ │ ├── index.ts │ │ │ ├── system │ │ │ │ ├── index.ts │ │ │ │ ├── n8n-diagnostic.ts │ │ │ │ ├── n8n-health-check.ts │ │ │ │ ├── n8n-list-available-tools.ts │ │ │ │ └── tools-documentation.ts │ │ │ ├── templates │ │ │ │ ├── get-template.ts │ │ │ │ ├── get-templates-for-task.ts │ │ │ │ ├── index.ts │ │ │ │ ├── list-node-templates.ts │ │ │ │ ├── list-tasks.ts │ │ │ │ ├── search-templates-by-metadata.ts │ │ │ │ └── search-templates.ts │ │ │ ├── types.ts │ │ │ ├── validation │ │ │ │ ├── index.ts │ │ │ │ ├── validate-node-minimal.ts │ │ │ │ ├── validate-node-operation.ts │ │ │ │ ├── validate-workflow-connections.ts │ │ │ │ ├── validate-workflow-expressions.ts │ │ │ │ └── validate-workflow.ts │ │ │ └── workflow_management │ │ │ ├── index.ts │ │ │ ├── n8n-autofix-workflow.ts │ │ │ ├── n8n-create-workflow.ts │ │ │ ├── n8n-delete-execution.ts │ │ │ ├── n8n-delete-workflow.ts │ │ │ ├── n8n-get-execution.ts │ │ │ ├── n8n-get-workflow-details.ts │ │ │ ├── n8n-get-workflow-minimal.ts │ │ │ ├── n8n-get-workflow-structure.ts │ │ │ ├── n8n-get-workflow.ts │ │ │ ├── n8n-list-executions.ts │ │ │ ├── n8n-list-workflows.ts │ │ │ ├── n8n-trigger-webhook-workflow.ts │ │ │ ├── n8n-update-full-workflow.ts │ │ │ ├── n8n-update-partial-workflow.ts │ │ │ └── n8n-validate-workflow.ts │ │ ├── tools-documentation.ts │ │ ├── tools-n8n-friendly.ts │ │ ├── tools-n8n-manager.ts │ │ ├── tools.ts │ │ └── workflow-examples.ts │ ├── mcp-engine.ts │ ├── mcp-tools-engine.ts │ ├── n8n │ │ ├── MCPApi.credentials.ts │ │ └── MCPNode.node.ts │ ├── parsers │ │ ├── node-parser.ts │ │ ├── property-extractor.ts │ │ └── simple-parser.ts │ ├── scripts │ │ ├── debug-http-search.ts │ │ ├── extract-from-docker.ts │ │ ├── fetch-templates-robust.ts │ │ ├── fetch-templates.ts │ │ ├── rebuild-database.ts │ │ ├── rebuild-optimized.ts │ │ ├── rebuild.ts │ │ ├── sanitize-templates.ts │ │ ├── seed-canonical-ai-examples.ts │ │ ├── test-autofix-documentation.ts │ │ ├── test-autofix-workflow.ts │ │ ├── test-execution-filtering.ts │ │ ├── test-node-suggestions.ts │ │ ├── test-protocol-negotiation.ts │ │ ├── test-summary.ts │ │ ├── test-webhook-autofix.ts │ │ ├── validate.ts │ │ └── validation-summary.ts │ ├── services │ │ ├── ai-node-validator.ts │ │ ├── ai-tool-validators.ts │ │ ├── confidence-scorer.ts │ │ ├── config-validator.ts │ │ ├── enhanced-config-validator.ts │ │ ├── example-generator.ts │ │ ├── execution-processor.ts │ │ ├── expression-format-validator.ts │ │ ├── expression-validator.ts │ │ ├── n8n-api-client.ts │ │ ├── n8n-validation.ts │ │ ├── node-documentation-service.ts │ │ ├── node-similarity-service.ts │ │ ├── node-specific-validators.ts │ │ ├── operation-similarity-service.ts │ │ ├── property-dependencies.ts │ │ ├── property-filter.ts │ │ ├── resource-similarity-service.ts │ │ ├── sqlite-storage-service.ts │ │ ├── task-templates.ts │ │ ├── universal-expression-validator.ts │ │ ├── workflow-auto-fixer.ts │ │ ├── workflow-diff-engine.ts │ │ └── workflow-validator.ts │ ├── telemetry │ │ ├── batch-processor.ts │ │ ├── config-manager.ts │ │ ├── early-error-logger.ts │ │ ├── error-sanitization-utils.ts │ │ ├── error-sanitizer.ts │ │ ├── event-tracker.ts │ │ ├── event-validator.ts │ │ ├── index.ts │ │ ├── performance-monitor.ts │ │ ├── rate-limiter.ts │ │ ├── startup-checkpoints.ts │ │ ├── telemetry-error.ts │ │ ├── telemetry-manager.ts │ │ ├── telemetry-types.ts │ │ └── workflow-sanitizer.ts │ ├── templates │ │ ├── batch-processor.ts │ │ ├── metadata-generator.ts │ │ ├── README.md │ │ ├── template-fetcher.ts │ │ ├── template-repository.ts │ │ └── template-service.ts │ ├── types │ │ ├── index.ts │ │ ├── instance-context.ts │ │ ├── n8n-api.ts │ │ ├── node-types.ts │ │ └── workflow-diff.ts │ └── utils │ ├── auth.ts │ ├── bridge.ts │ ├── cache-utils.ts │ ├── console-manager.ts │ ├── documentation-fetcher.ts │ ├── enhanced-documentation-fetcher.ts │ ├── error-handler.ts │ ├── example-generator.ts │ ├── fixed-collection-validator.ts │ ├── logger.ts │ ├── mcp-client.ts │ ├── n8n-errors.ts │ ├── node-source-extractor.ts │ ├── node-type-normalizer.ts │ ├── node-type-utils.ts │ ├── node-utils.ts │ ├── npm-version-checker.ts │ ├── protocol-version.ts │ ├── simple-cache.ts │ ├── ssrf-protection.ts │ ├── template-node-resolver.ts │ ├── template-sanitizer.ts │ ├── url-detector.ts │ ├── validation-schemas.ts │ └── version.ts ├── test-output.txt ├── test-reinit-fix.sh ├── tests │ ├── __snapshots__ │ │ └── .gitkeep │ ├── auth.test.ts │ ├── benchmarks │ │ ├── database-queries.bench.ts │ │ ├── index.ts │ │ ├── mcp-tools.bench.ts │ │ ├── mcp-tools.bench.ts.disabled │ │ ├── mcp-tools.bench.ts.skip │ │ ├── node-loading.bench.ts.disabled │ │ ├── README.md │ │ ├── search-operations.bench.ts.disabled │ │ └── validation-performance.bench.ts.disabled │ ├── bridge.test.ts │ ├── comprehensive-extraction-test.js │ ├── data │ │ └── .gitkeep │ ├── debug-slack-doc.js │ ├── demo-enhanced-documentation.js │ ├── docker-tests-README.md │ ├── error-handler.test.ts │ ├── examples │ │ └── using-database-utils.test.ts │ ├── extracted-nodes-db │ │ ├── database-import.json │ │ ├── extraction-report.json │ │ ├── insert-nodes.sql │ │ ├── n8n-nodes-base__Airtable.json │ │ ├── n8n-nodes-base__Discord.json │ │ ├── n8n-nodes-base__Function.json │ │ ├── n8n-nodes-base__HttpRequest.json │ │ ├── n8n-nodes-base__If.json │ │ ├── n8n-nodes-base__Slack.json │ │ ├── n8n-nodes-base__SplitInBatches.json │ │ └── n8n-nodes-base__Webhook.json │ ├── factories │ │ ├── node-factory.ts │ │ └── property-definition-factory.ts │ ├── fixtures │ │ ├── .gitkeep │ │ ├── database │ │ │ └── test-nodes.json │ │ ├── factories │ │ │ ├── node.factory.ts │ │ │ └── parser-node.factory.ts │ │ └── template-configs.ts │ ├── helpers │ │ └── env-helpers.ts │ ├── http-server-auth.test.ts │ ├── integration │ │ ├── ai-validation │ │ │ ├── ai-agent-validation.test.ts │ │ │ ├── ai-tool-validation.test.ts │ │ │ ├── chat-trigger-validation.test.ts │ │ │ ├── e2e-validation.test.ts │ │ │ ├── helpers.ts │ │ │ ├── llm-chain-validation.test.ts │ │ │ ├── README.md │ │ │ └── TEST_REPORT.md │ │ ├── ci │ │ │ └── database-population.test.ts │ │ ├── database │ │ │ ├── connection-management.test.ts │ │ │ ├── empty-database.test.ts │ │ │ ├── fts5-search.test.ts │ │ │ ├── node-fts5-search.test.ts │ │ │ ├── node-repository.test.ts │ │ │ ├── performance.test.ts │ │ │ ├── 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 │ │ │ │ └── list-tools.test.ts │ │ │ ├── test-connection.ts │ │ │ ├── types │ │ │ │ └── mcp-responses.ts │ │ │ ├── utils │ │ │ │ ├── cleanup-helpers.ts │ │ │ │ ├── credentials.ts │ │ │ │ ├── factories.ts │ │ │ │ ├── fixtures.ts │ │ │ │ ├── mcp-context.ts │ │ │ │ ├── n8n-client.ts │ │ │ │ ├── node-repository.ts │ │ │ │ ├── response-types.ts │ │ │ │ ├── test-context.ts │ │ │ │ └── webhook-workflows.ts │ │ │ └── workflows │ │ │ ├── autofix-workflow.test.ts │ │ │ ├── create-workflow.test.ts │ │ │ ├── delete-workflow.test.ts │ │ │ ├── get-workflow-details.test.ts │ │ │ ├── get-workflow-minimal.test.ts │ │ │ ├── get-workflow-structure.test.ts │ │ │ ├── get-workflow.test.ts │ │ │ ├── list-workflows.test.ts │ │ │ ├── smart-parameters.test.ts │ │ │ ├── update-partial-workflow.test.ts │ │ │ ├── update-workflow.test.ts │ │ │ └── validate-workflow.test.ts │ │ ├── security │ │ │ ├── command-injection-prevention.test.ts │ │ │ └── rate-limiting.test.ts │ │ ├── setup │ │ │ ├── integration-setup.ts │ │ │ └── msw-test-server.ts │ │ ├── telemetry │ │ │ ├── docker-user-id-stability.test.ts │ │ │ └── mcp-telemetry.test.ts │ │ ├── templates │ │ │ └── metadata-operations.test.ts │ │ └── workflow-creation-node-type-format.test.ts │ ├── logger.test.ts │ ├── MOCKING_STRATEGY.md │ ├── mocks │ │ ├── n8n-api │ │ │ ├── data │ │ │ │ ├── credentials.ts │ │ │ │ ├── executions.ts │ │ │ │ └── workflows.ts │ │ │ ├── handlers.ts │ │ │ └── index.ts │ │ └── README.md │ ├── node-storage-export.json │ ├── setup │ │ ├── global-setup.ts │ │ ├── msw-setup.ts │ │ ├── TEST_ENV_DOCUMENTATION.md │ │ └── test-env.ts │ ├── test-database-extraction.js │ ├── test-direct-extraction.js │ ├── test-enhanced-documentation.js │ ├── test-enhanced-integration.js │ ├── test-mcp-extraction.js │ ├── test-mcp-server-extraction.js │ ├── test-mcp-tools-integration.js │ ├── test-node-documentation-service.js │ ├── test-node-list.js │ ├── test-package-info.js │ ├── test-parsing-operations.js │ ├── test-slack-node-complete.js │ ├── test-small-rebuild.js │ ├── test-sqlite-search.js │ ├── test-storage-system.js │ ├── unit │ │ ├── __mocks__ │ │ │ ├── n8n-nodes-base.test.ts │ │ │ ├── n8n-nodes-base.ts │ │ │ └── README.md │ │ ├── database │ │ │ ├── __mocks__ │ │ │ │ └── better-sqlite3.ts │ │ │ ├── database-adapter-unit.test.ts │ │ │ ├── node-repository-core.test.ts │ │ │ ├── node-repository-operations.test.ts │ │ │ ├── node-repository-outputs.test.ts │ │ │ ├── README.md │ │ │ └── template-repository-core.test.ts │ │ ├── docker │ │ │ ├── config-security.test.ts │ │ │ ├── edge-cases.test.ts │ │ │ ├── parse-config.test.ts │ │ │ └── serve-command.test.ts │ │ ├── errors │ │ │ └── validation-service-error.test.ts │ │ ├── examples │ │ │ └── using-n8n-nodes-base-mock.test.ts │ │ ├── flexible-instance-security-advanced.test.ts │ │ ├── flexible-instance-security.test.ts │ │ ├── http-server │ │ │ └── multi-tenant-support.test.ts │ │ ├── http-server-n8n-mode.test.ts │ │ ├── http-server-n8n-reinit.test.ts │ │ ├── http-server-session-management.test.ts │ │ ├── loaders │ │ │ └── node-loader.test.ts │ │ ├── mappers │ │ │ └── docs-mapper.test.ts │ │ ├── mcp │ │ │ ├── get-node-essentials-examples.test.ts │ │ │ ├── handlers-n8n-manager-simple.test.ts │ │ │ ├── handlers-n8n-manager.test.ts │ │ │ ├── handlers-workflow-diff.test.ts │ │ │ ├── lru-cache-behavior.test.ts │ │ │ ├── multi-tenant-tool-listing.test.ts.disabled │ │ │ ├── parameter-validation.test.ts │ │ │ ├── search-nodes-examples.test.ts │ │ │ ├── tools-documentation.test.ts │ │ │ └── tools.test.ts │ │ ├── monitoring │ │ │ └── cache-metrics.test.ts │ │ ├── MULTI_TENANT_TEST_COVERAGE.md │ │ ├── multi-tenant-integration.test.ts │ │ ├── parsers │ │ │ ├── node-parser-outputs.test.ts │ │ │ ├── node-parser.test.ts │ │ │ ├── property-extractor.test.ts │ │ │ └── simple-parser.test.ts │ │ ├── scripts │ │ │ └── fetch-templates-extraction.test.ts │ │ ├── services │ │ │ ├── ai-node-validator.test.ts │ │ │ ├── ai-tool-validators.test.ts │ │ │ ├── confidence-scorer.test.ts │ │ │ ├── config-validator-basic.test.ts │ │ │ ├── config-validator-edge-cases.test.ts │ │ │ ├── config-validator-node-specific.test.ts │ │ │ ├── config-validator-security.test.ts │ │ │ ├── debug-validator.test.ts │ │ │ ├── enhanced-config-validator-integration.test.ts │ │ │ ├── enhanced-config-validator-operations.test.ts │ │ │ ├── enhanced-config-validator.test.ts │ │ │ ├── example-generator.test.ts │ │ │ ├── execution-processor.test.ts │ │ │ ├── expression-format-validator.test.ts │ │ │ ├── expression-validator-edge-cases.test.ts │ │ │ ├── expression-validator.test.ts │ │ │ ├── fixed-collection-validation.test.ts │ │ │ ├── loop-output-edge-cases.test.ts │ │ │ ├── n8n-api-client.test.ts │ │ │ ├── n8n-validation.test.ts │ │ │ ├── node-similarity-service.test.ts │ │ │ ├── node-specific-validators.test.ts │ │ │ ├── operation-similarity-service-comprehensive.test.ts │ │ │ ├── operation-similarity-service.test.ts │ │ │ ├── property-dependencies.test.ts │ │ │ ├── property-filter-edge-cases.test.ts │ │ │ ├── property-filter.test.ts │ │ │ ├── resource-similarity-service-comprehensive.test.ts │ │ │ ├── resource-similarity-service.test.ts │ │ │ ├── task-templates.test.ts │ │ │ ├── template-service.test.ts │ │ │ ├── universal-expression-validator.test.ts │ │ │ ├── validation-fixes.test.ts │ │ │ ├── workflow-auto-fixer.test.ts │ │ │ ├── workflow-diff-engine.test.ts │ │ │ ├── workflow-fixed-collection-validation.test.ts │ │ │ ├── workflow-validator-comprehensive.test.ts │ │ │ ├── workflow-validator-edge-cases.test.ts │ │ │ ├── workflow-validator-error-outputs.test.ts │ │ │ ├── workflow-validator-expression-format.test.ts │ │ │ ├── workflow-validator-loops-simple.test.ts │ │ │ ├── workflow-validator-loops.test.ts │ │ │ ├── workflow-validator-mocks.test.ts │ │ │ ├── workflow-validator-performance.test.ts │ │ │ ├── workflow-validator-with-mocks.test.ts │ │ │ └── workflow-validator.test.ts │ │ ├── telemetry │ │ │ ├── batch-processor.test.ts │ │ │ ├── config-manager.test.ts │ │ │ ├── event-tracker.test.ts │ │ │ ├── event-validator.test.ts │ │ │ ├── rate-limiter.test.ts │ │ │ ├── telemetry-error.test.ts │ │ │ ├── telemetry-manager.test.ts │ │ │ ├── v2.18.3-fixes-verification.test.ts │ │ │ └── workflow-sanitizer.test.ts │ │ ├── templates │ │ │ ├── batch-processor.test.ts │ │ │ ├── metadata-generator.test.ts │ │ │ ├── template-repository-metadata.test.ts │ │ │ └── template-repository-security.test.ts │ │ ├── test-env-example.test.ts │ │ ├── test-infrastructure.test.ts │ │ ├── types │ │ │ ├── instance-context-coverage.test.ts │ │ │ └── instance-context-multi-tenant.test.ts │ │ ├── utils │ │ │ ├── auth-timing-safe.test.ts │ │ │ ├── cache-utils.test.ts │ │ │ ├── console-manager.test.ts │ │ │ ├── database-utils.test.ts │ │ │ ├── fixed-collection-validator.test.ts │ │ │ ├── n8n-errors.test.ts │ │ │ ├── node-type-normalizer.test.ts │ │ │ ├── node-type-utils.test.ts │ │ │ ├── node-utils.test.ts │ │ │ ├── simple-cache-memory-leak-fix.test.ts │ │ │ ├── ssrf-protection.test.ts │ │ │ └── template-node-resolver.test.ts │ │ └── validation-fixes.test.ts │ └── utils │ ├── assertions.ts │ ├── builders │ │ └── workflow.builder.ts │ ├── data-generators.ts │ ├── database-utils.ts │ ├── README.md │ └── test-helpers.ts ├── thumbnail.png ├── tsconfig.build.json ├── tsconfig.json ├── types │ ├── mcp.d.ts │ └── test-env.d.ts ├── verify-telemetry-fix.js ├── versioned-nodes.md ├── vitest.config.benchmark.ts ├── vitest.config.integration.ts └── vitest.config.ts ``` # Files -------------------------------------------------------------------------------- /tests/integration/n8n-api/workflows/smart-parameters.test.ts: -------------------------------------------------------------------------------- ```typescript /** * Integration Tests: Smart Parameters with Real n8n API * * These tests verify that smart parameters (branch='true'/'false', case=N) * correctly map to n8n's actual connection structure when tested against * a real n8n instance. * * CRITICAL: These tests validate against REAL n8n connection structure: * ✅ workflow.connections.IF.main[0] (correct) * ❌ workflow.connections.IF.true (wrong - what unit tests did) * * These integration tests would have caught the bugs that unit tests missed: * - Bug 1: branch='true' mapping to sourceOutput instead of sourceIndex * - Bug 2: Zod schema stripping branch/case parameters */ import { describe, it, expect, beforeEach, afterEach, afterAll } from 'vitest'; import { createTestContext, TestContext, createTestWorkflowName } from '../utils/test-context'; import { getTestN8nClient } from '../utils/n8n-client'; import { N8nApiClient } from '../../../../src/services/n8n-api-client'; import { cleanupOrphanedWorkflows } from '../utils/cleanup-helpers'; import { createMcpContext } from '../utils/mcp-context'; import { InstanceContext } from '../../../../src/types/instance-context'; import { handleUpdatePartialWorkflow } from '../../../../src/mcp/handlers-workflow-diff'; import { Workflow } from '../../../../src/types/n8n-api'; describe('Integration: Smart Parameters with Real n8n API', () => { let context: TestContext; let client: N8nApiClient; let mcpContext: InstanceContext; beforeEach(() => { context = createTestContext(); client = getTestN8nClient(); mcpContext = createMcpContext(); }); afterEach(async () => { await context.cleanup(); }); afterAll(async () => { if (!process.env.CI) { await cleanupOrphanedWorkflows(); } }); // ====================================================================== // TEST 1: IF node with branch='true' // ====================================================================== describe('IF Node Smart Parameters', () => { it('should handle branch="true" with real n8n API', async () => { // Create minimal workflow with IF node const workflowName = createTestWorkflowName('Smart Params - IF True Branch'); const workflow = await client.createWorkflow({ name: workflowName, nodes: [ { id: 'manual-1', name: 'Manual', type: 'n8n-nodes-base.manualTrigger', typeVersion: 1, position: [250, 300], parameters: {} }, { id: 'if-1', name: 'IF', type: 'n8n-nodes-base.if', typeVersion: 2, position: [450, 300], parameters: { conditions: { conditions: [ { id: 'cond-1', leftValue: '={{ $json.value }}', rightValue: 'test', operation: 'equal' } ] } } } ], connections: { Manual: { main: [[{ node: 'IF', type: 'main', index: 0 }]] } }, tags: ['mcp-integration-test'] }); expect(workflow.id).toBeTruthy(); if (!workflow.id) throw new Error('Workflow ID is missing'); context.trackWorkflow(workflow.id); // Add TrueHandler node and connection using branch='true' smart parameter const result = await handleUpdatePartialWorkflow( { id: workflow.id, operations: [ { type: 'addNode', node: { name: 'TrueHandler', type: 'n8n-nodes-base.set', typeVersion: 3.4, position: [650, 200], parameters: { assignments: { assignments: [ { id: 'assign-1', name: 'handled', value: 'true', type: 'string' } ] } } } }, { type: 'addConnection', source: 'IF', target: 'TrueHandler', branch: 'true' // Smart parameter } ] }, mcpContext ); expect(result.success).toBe(true); // Fetch actual workflow from n8n API const fetchedWorkflow = await client.getWorkflow(workflow.id); // CRITICAL: Assert REAL n8n connection structure // The connection should be at index 0 of main output array (true branch) expect(fetchedWorkflow.connections.IF).toBeDefined(); expect(fetchedWorkflow.connections.IF.main).toBeDefined(); expect(fetchedWorkflow.connections.IF.main[0]).toBeDefined(); expect(fetchedWorkflow.connections.IF.main[0].length).toBe(1); expect(fetchedWorkflow.connections.IF.main[0][0].node).toBe('TrueHandler'); expect(fetchedWorkflow.connections.IF.main[0][0].type).toBe('main'); // Verify false branch (index 1) is empty or undefined expect(fetchedWorkflow.connections.IF.main[1] || []).toHaveLength(0); }); // ====================================================================== // TEST 2: IF node with branch='false' // ====================================================================== it('should handle branch="false" with real n8n API', async () => { // Create minimal workflow with IF node const workflowName = createTestWorkflowName('Smart Params - IF False Branch'); const workflow = await client.createWorkflow({ name: workflowName, nodes: [ { id: 'manual-1', name: 'Manual', type: 'n8n-nodes-base.manualTrigger', typeVersion: 1, position: [250, 300], parameters: {} }, { id: 'if-1', name: 'IF', type: 'n8n-nodes-base.if', typeVersion: 2, position: [450, 300], parameters: { conditions: { conditions: [ { id: 'cond-1', leftValue: '={{ $json.value }}', rightValue: 'test', operation: 'equal' } ] } } } ], connections: { Manual: { main: [[{ node: 'IF', type: 'main', index: 0 }]] } }, tags: ['mcp-integration-test'] }); expect(workflow.id).toBeTruthy(); if (!workflow.id) throw new Error('Workflow ID is missing'); context.trackWorkflow(workflow.id); // Add FalseHandler node and connection using branch='false' smart parameter const result = await handleUpdatePartialWorkflow( { id: workflow.id, operations: [ { type: 'addNode', node: { name: 'FalseHandler', type: 'n8n-nodes-base.set', typeVersion: 3.4, position: [650, 400], parameters: { assignments: { assignments: [ { id: 'assign-1', name: 'handled', value: 'false', type: 'string' } ] } } } }, { type: 'addConnection', source: 'IF', target: 'FalseHandler', branch: 'false' // Smart parameter } ] }, mcpContext ); expect(result.success).toBe(true); // Fetch actual workflow from n8n API const fetchedWorkflow = await client.getWorkflow(workflow.id); // CRITICAL: Assert REAL n8n connection structure // The connection should be at index 1 of main output array (false branch) expect(fetchedWorkflow.connections.IF).toBeDefined(); expect(fetchedWorkflow.connections.IF.main).toBeDefined(); expect(fetchedWorkflow.connections.IF.main[1]).toBeDefined(); expect(fetchedWorkflow.connections.IF.main[1].length).toBe(1); expect(fetchedWorkflow.connections.IF.main[1][0].node).toBe('FalseHandler'); expect(fetchedWorkflow.connections.IF.main[1][0].type).toBe('main'); // Verify true branch (index 0) is empty or undefined expect(fetchedWorkflow.connections.IF.main[0] || []).toHaveLength(0); }); // ====================================================================== // TEST 3: IF node with both branches // ====================================================================== it('should handle both branch="true" and branch="false" simultaneously', async () => { // Create minimal workflow with IF node const workflowName = createTestWorkflowName('Smart Params - IF Both Branches'); const workflow = await client.createWorkflow({ name: workflowName, nodes: [ { id: 'manual-1', name: 'Manual', type: 'n8n-nodes-base.manualTrigger', typeVersion: 1, position: [250, 300], parameters: {} }, { id: 'if-1', name: 'IF', type: 'n8n-nodes-base.if', typeVersion: 2, position: [450, 300], parameters: { conditions: { conditions: [ { id: 'cond-1', leftValue: '={{ $json.value }}', rightValue: 'test', operation: 'equal' } ] } } } ], connections: { Manual: { main: [[{ node: 'IF', type: 'main', index: 0 }]] } }, tags: ['mcp-integration-test'] }); expect(workflow.id).toBeTruthy(); if (!workflow.id) throw new Error('Workflow ID is missing'); context.trackWorkflow(workflow.id); // Add both handlers and connections in single operation batch const result = await handleUpdatePartialWorkflow( { id: workflow.id, operations: [ { type: 'addNode', node: { name: 'TrueHandler', type: 'n8n-nodes-base.set', typeVersion: 3.4, position: [650, 200], parameters: { assignments: { assignments: [ { id: 'assign-1', name: 'branch', value: 'true', type: 'string' } ] } } } }, { type: 'addNode', node: { name: 'FalseHandler', type: 'n8n-nodes-base.set', typeVersion: 3.4, position: [650, 400], parameters: { assignments: { assignments: [ { id: 'assign-2', name: 'branch', value: 'false', type: 'string' } ] } } } }, { type: 'addConnection', source: 'IF', target: 'TrueHandler', branch: 'true' }, { type: 'addConnection', source: 'IF', target: 'FalseHandler', branch: 'false' } ] }, mcpContext ); expect(result.success).toBe(true); // Fetch actual workflow from n8n API const fetchedWorkflow = await client.getWorkflow(workflow.id); // CRITICAL: Assert both branches exist at separate indices expect(fetchedWorkflow.connections.IF).toBeDefined(); expect(fetchedWorkflow.connections.IF.main).toBeDefined(); // True branch at index 0 expect(fetchedWorkflow.connections.IF.main[0]).toBeDefined(); expect(fetchedWorkflow.connections.IF.main[0].length).toBe(1); expect(fetchedWorkflow.connections.IF.main[0][0].node).toBe('TrueHandler'); // False branch at index 1 expect(fetchedWorkflow.connections.IF.main[1]).toBeDefined(); expect(fetchedWorkflow.connections.IF.main[1].length).toBe(1); expect(fetchedWorkflow.connections.IF.main[1][0].node).toBe('FalseHandler'); }); }); // ====================================================================== // TEST 4-6: Switch node with case parameter // ====================================================================== describe('Switch Node Smart Parameters', () => { it('should handle case=0, case=1, case=2 with real n8n API', async () => { // Create minimal workflow with Switch node const workflowName = createTestWorkflowName('Smart Params - Switch Cases'); const workflow = await client.createWorkflow({ name: workflowName, nodes: [ { id: 'manual-1', name: 'Manual', type: 'n8n-nodes-base.manualTrigger', typeVersion: 1, position: [250, 300], parameters: {} }, { id: 'switch-1', name: 'Switch', type: 'n8n-nodes-base.switch', typeVersion: 3, position: [450, 300], parameters: { mode: 'rules', rules: { rules: [ { id: 'rule-1', conditions: { conditions: [ { id: 'cond-1', leftValue: '={{ $json.case }}', rightValue: 'a', operation: 'equal' } ] }, output: 0 }, { id: 'rule-2', conditions: { conditions: [ { id: 'cond-2', leftValue: '={{ $json.case }}', rightValue: 'b', operation: 'equal' } ] }, output: 1 }, { id: 'rule-3', conditions: { conditions: [ { id: 'cond-3', leftValue: '={{ $json.case }}', rightValue: 'c', operation: 'equal' } ] }, output: 2 } ] } } } ], connections: { Manual: { main: [[{ node: 'Switch', type: 'main', index: 0 }]] } }, tags: ['mcp-integration-test'] }); expect(workflow.id).toBeTruthy(); if (!workflow.id) throw new Error('Workflow ID is missing'); context.trackWorkflow(workflow.id); // Add 3 handler nodes and connections using case smart parameter const result = await handleUpdatePartialWorkflow( { id: workflow.id, operations: [ // Add handlers { type: 'addNode', node: { name: 'Handler0', type: 'n8n-nodes-base.set', typeVersion: 3.4, position: [650, 100], parameters: { assignments: { assignments: [ { id: 'assign-1', name: 'case', value: '0', type: 'string' } ] } } } }, { type: 'addNode', node: { name: 'Handler1', type: 'n8n-nodes-base.set', typeVersion: 3.4, position: [650, 300], parameters: { assignments: { assignments: [ { id: 'assign-2', name: 'case', value: '1', type: 'string' } ] } } } }, { type: 'addNode', node: { name: 'Handler2', type: 'n8n-nodes-base.set', typeVersion: 3.4, position: [650, 500], parameters: { assignments: { assignments: [ { id: 'assign-3', name: 'case', value: '2', type: 'string' } ] } } } }, // Add connections with case parameter { type: 'addConnection', source: 'Switch', target: 'Handler0', case: 0 // Smart parameter }, { type: 'addConnection', source: 'Switch', target: 'Handler1', case: 1 // Smart parameter }, { type: 'addConnection', source: 'Switch', target: 'Handler2', case: 2 // Smart parameter } ] }, mcpContext ); expect(result.success).toBe(true); // Fetch actual workflow from n8n API const fetchedWorkflow = await client.getWorkflow(workflow.id); // CRITICAL: Assert connections at correct indices expect(fetchedWorkflow.connections.Switch).toBeDefined(); expect(fetchedWorkflow.connections.Switch.main).toBeDefined(); // Case 0 at index 0 expect(fetchedWorkflow.connections.Switch.main[0]).toBeDefined(); expect(fetchedWorkflow.connections.Switch.main[0].length).toBe(1); expect(fetchedWorkflow.connections.Switch.main[0][0].node).toBe('Handler0'); // Case 1 at index 1 expect(fetchedWorkflow.connections.Switch.main[1]).toBeDefined(); expect(fetchedWorkflow.connections.Switch.main[1].length).toBe(1); expect(fetchedWorkflow.connections.Switch.main[1][0].node).toBe('Handler1'); // Case 2 at index 2 expect(fetchedWorkflow.connections.Switch.main[2]).toBeDefined(); expect(fetchedWorkflow.connections.Switch.main[2].length).toBe(1); expect(fetchedWorkflow.connections.Switch.main[2][0].node).toBe('Handler2'); }); }); // ====================================================================== // TEST 5: rewireConnection with branch parameter // ====================================================================== describe('RewireConnection with Smart Parameters', () => { it('should rewire connection using branch="true" parameter', async () => { // Create workflow with IF node and initial connection const workflowName = createTestWorkflowName('Smart Params - Rewire IF True'); const workflow = await client.createWorkflow({ name: workflowName, nodes: [ { id: 'manual-1', name: 'Manual', type: 'n8n-nodes-base.manualTrigger', typeVersion: 1, position: [250, 300], parameters: {} }, { id: 'if-1', name: 'IF', type: 'n8n-nodes-base.if', typeVersion: 2, position: [450, 300], parameters: { conditions: { conditions: [ { id: 'cond-1', leftValue: '={{ $json.value }}', rightValue: 'test', operation: 'equal' } ] } } }, { id: 'handler1', name: 'Handler1', type: 'n8n-nodes-base.set', typeVersion: 3.4, position: [650, 200], parameters: { assignments: { assignments: [ { id: 'assign-1', name: 'handler', value: '1', type: 'string' } ] } } }, { id: 'handler2', name: 'Handler2', type: 'n8n-nodes-base.set', typeVersion: 3.4, position: [650, 400], parameters: { assignments: { assignments: [ { id: 'assign-2', name: 'handler', value: '2', type: 'string' } ] } } } ], connections: { Manual: { main: [[{ node: 'IF', type: 'main', index: 0 }]] }, IF: { // Initial connection: true branch to Handler1 main: [[{ node: 'Handler1', type: 'main', index: 0 }], []] } }, tags: ['mcp-integration-test'] }); expect(workflow.id).toBeTruthy(); if (!workflow.id) throw new Error('Workflow ID is missing'); context.trackWorkflow(workflow.id); // Rewire true branch from Handler1 to Handler2 const result = await handleUpdatePartialWorkflow( { id: workflow.id, operations: [ { type: 'rewireConnection', source: 'IF', from: 'Handler1', to: 'Handler2', branch: 'true' // Smart parameter } ] }, mcpContext ); expect(result.success).toBe(true); // Fetch actual workflow from n8n API const fetchedWorkflow = await client.getWorkflow(workflow.id); // CRITICAL: Assert connection rewired at index 0 (true branch) expect(fetchedWorkflow.connections.IF).toBeDefined(); expect(fetchedWorkflow.connections.IF.main).toBeDefined(); expect(fetchedWorkflow.connections.IF.main[0]).toBeDefined(); expect(fetchedWorkflow.connections.IF.main[0].length).toBe(1); expect(fetchedWorkflow.connections.IF.main[0][0].node).toBe('Handler2'); }); // ====================================================================== // TEST 6: rewireConnection with case parameter // ====================================================================== it('should rewire connection using case=1 parameter', async () => { // Create workflow with Switch node and initial connections const workflowName = createTestWorkflowName('Smart Params - Rewire Switch Case'); const workflow = await client.createWorkflow({ name: workflowName, nodes: [ { id: 'manual-1', name: 'Manual', type: 'n8n-nodes-base.manualTrigger', typeVersion: 1, position: [250, 300], parameters: {} }, { id: 'switch-1', name: 'Switch', type: 'n8n-nodes-base.switch', typeVersion: 3, position: [450, 300], parameters: { mode: 'rules', rules: { rules: [ { id: 'rule-1', conditions: { conditions: [ { id: 'cond-1', leftValue: '={{ $json.case }}', rightValue: 'a', operation: 'equal' } ] }, output: 0 }, { id: 'rule-2', conditions: { conditions: [ { id: 'cond-2', leftValue: '={{ $json.case }}', rightValue: 'b', operation: 'equal' } ] }, output: 1 } ] } } }, { id: 'handler1', name: 'OldHandler', type: 'n8n-nodes-base.set', typeVersion: 3.4, position: [650, 300], parameters: { assignments: { assignments: [ { id: 'assign-1', name: 'handler', value: 'old', type: 'string' } ] } } }, { id: 'handler2', name: 'NewHandler', type: 'n8n-nodes-base.set', typeVersion: 3.4, position: [650, 500], parameters: { assignments: { assignments: [ { id: 'assign-2', name: 'handler', value: 'new', type: 'string' } ] } } } ], connections: { Manual: { main: [[{ node: 'Switch', type: 'main', index: 0 }]] }, Switch: { // Initial connection: case 1 to OldHandler main: [[], [{ node: 'OldHandler', type: 'main', index: 0 }]] } }, tags: ['mcp-integration-test'] }); expect(workflow.id).toBeTruthy(); if (!workflow.id) throw new Error('Workflow ID is missing'); context.trackWorkflow(workflow.id); // Rewire case 1 from OldHandler to NewHandler const result = await handleUpdatePartialWorkflow( { id: workflow.id, operations: [ { type: 'rewireConnection', source: 'Switch', from: 'OldHandler', to: 'NewHandler', case: 1 // Smart parameter } ] }, mcpContext ); expect(result.success).toBe(true); // Fetch actual workflow from n8n API const fetchedWorkflow = await client.getWorkflow(workflow.id); // CRITICAL: Assert connection rewired at index 1 (case 1) expect(fetchedWorkflow.connections.Switch).toBeDefined(); expect(fetchedWorkflow.connections.Switch.main).toBeDefined(); expect(fetchedWorkflow.connections.Switch.main[1]).toBeDefined(); expect(fetchedWorkflow.connections.Switch.main[1].length).toBe(1); expect(fetchedWorkflow.connections.Switch.main[1][0].node).toBe('NewHandler'); }); }); // ====================================================================== // TEST 7: Explicit sourceIndex overrides branch // ====================================================================== describe('Parameter Priority', () => { it('should prioritize explicit sourceIndex over branch parameter', async () => { // Create minimal workflow with IF node const workflowName = createTestWorkflowName('Smart Params - Explicit Override Branch'); const workflow = await client.createWorkflow({ name: workflowName, nodes: [ { id: 'manual-1', name: 'Manual', type: 'n8n-nodes-base.manualTrigger', typeVersion: 1, position: [250, 300], parameters: {} }, { id: 'if-1', name: 'IF', type: 'n8n-nodes-base.if', typeVersion: 2, position: [450, 300], parameters: { conditions: { conditions: [ { id: 'cond-1', leftValue: '={{ $json.value }}', rightValue: 'test', operation: 'equal' } ] } } } ], connections: { Manual: { main: [[{ node: 'IF', type: 'main', index: 0 }]] } }, tags: ['mcp-integration-test'] }); expect(workflow.id).toBeTruthy(); if (!workflow.id) throw new Error('Workflow ID is missing'); context.trackWorkflow(workflow.id); // Add connection with BOTH branch='true' (would be index 0) and explicit sourceIndex=1 // Explicit sourceIndex should win const result = await handleUpdatePartialWorkflow( { id: workflow.id, operations: [ { type: 'addNode', node: { name: 'Handler', type: 'n8n-nodes-base.set', typeVersion: 3.4, position: [650, 400], parameters: { assignments: { assignments: [ { id: 'assign-1', name: 'test', value: 'value', type: 'string' } ] } } } }, { type: 'addConnection', source: 'IF', target: 'Handler', branch: 'true', // Would map to index 0 sourceIndex: 1 // Explicit index should override } ] }, mcpContext ); expect(result.success).toBe(true); // Fetch actual workflow from n8n API const fetchedWorkflow = await client.getWorkflow(workflow.id); // CRITICAL: Assert connection is at index 1 (explicit wins) expect(fetchedWorkflow.connections.IF).toBeDefined(); expect(fetchedWorkflow.connections.IF.main).toBeDefined(); expect(fetchedWorkflow.connections.IF.main[1]).toBeDefined(); expect(fetchedWorkflow.connections.IF.main[1].length).toBe(1); expect(fetchedWorkflow.connections.IF.main[1][0].node).toBe('Handler'); // Index 0 should be empty expect(fetchedWorkflow.connections.IF.main[0] || []).toHaveLength(0); }); // ====================================================================== // TEST 8: Explicit sourceIndex overrides case // ====================================================================== it('should prioritize explicit sourceIndex over case parameter', async () => { // Create minimal workflow with Switch node const workflowName = createTestWorkflowName('Smart Params - Explicit Override Case'); const workflow = await client.createWorkflow({ name: workflowName, nodes: [ { id: 'manual-1', name: 'Manual', type: 'n8n-nodes-base.manualTrigger', typeVersion: 1, position: [250, 300], parameters: {} }, { id: 'switch-1', name: 'Switch', type: 'n8n-nodes-base.switch', typeVersion: 3, position: [450, 300], parameters: { mode: 'rules', rules: { rules: [ { id: 'rule-1', conditions: { conditions: [ { id: 'cond-1', leftValue: '={{ $json.case }}', rightValue: 'a', operation: 'equal' } ] }, output: 0 }, { id: 'rule-2', conditions: { conditions: [ { id: 'cond-2', leftValue: '={{ $json.case }}', rightValue: 'b', operation: 'equal' } ] }, output: 1 } ] } } } ], connections: { Manual: { main: [[{ node: 'Switch', type: 'main', index: 0 }]] } }, tags: ['mcp-integration-test'] }); expect(workflow.id).toBeTruthy(); if (!workflow.id) throw new Error('Workflow ID is missing'); context.trackWorkflow(workflow.id); // Add connection with BOTH case=1 (would be index 1) and explicit sourceIndex=2 // Explicit sourceIndex should win const result = await handleUpdatePartialWorkflow( { id: workflow.id, operations: [ { type: 'addNode', node: { name: 'Handler', type: 'n8n-nodes-base.set', typeVersion: 3.4, position: [650, 500], parameters: { assignments: { assignments: [ { id: 'assign-1', name: 'test', value: 'value', type: 'string' } ] } } } }, { type: 'addConnection', source: 'Switch', target: 'Handler', case: 1, // Would map to index 1 sourceIndex: 2 // Explicit index should override } ] }, mcpContext ); expect(result.success).toBe(true); // Fetch actual workflow from n8n API const fetchedWorkflow = await client.getWorkflow(workflow.id); // CRITICAL: Assert connection is at index 2 (explicit wins) expect(fetchedWorkflow.connections.Switch).toBeDefined(); expect(fetchedWorkflow.connections.Switch.main).toBeDefined(); expect(fetchedWorkflow.connections.Switch.main[2]).toBeDefined(); expect(fetchedWorkflow.connections.Switch.main[2].length).toBe(1); expect(fetchedWorkflow.connections.Switch.main[2][0].node).toBe('Handler'); // Index 1 should be empty expect(fetchedWorkflow.connections.Switch.main[1] || []).toHaveLength(0); }); }); // ====================================================================== // ERROR CASES: Invalid smart parameter values // ====================================================================== describe('Error Cases', () => { it('should reject invalid branch value', async () => { // Create minimal workflow with IF node const workflowName = createTestWorkflowName('Smart Params - Invalid Branch'); const workflow = await client.createWorkflow({ name: workflowName, nodes: [ { id: 'manual-1', name: 'Manual', type: 'n8n-nodes-base.manualTrigger', typeVersion: 1, position: [250, 300], parameters: {} }, { id: 'if-1', name: 'IF', type: 'n8n-nodes-base.if', typeVersion: 2, position: [450, 300], parameters: { conditions: { conditions: [ { id: 'cond-1', leftValue: '={{ $json.value }}', rightValue: 'test', operation: 'equal' } ] } } } ], connections: { Manual: { main: [[{ node: 'IF', type: 'main', index: 0 }]] } }, tags: ['mcp-integration-test'] }); expect(workflow.id).toBeTruthy(); if (!workflow.id) throw new Error('Workflow ID is missing'); context.trackWorkflow(workflow.id); // Try to add connection with invalid branch value const result = await handleUpdatePartialWorkflow( { id: workflow.id, operations: [ { type: 'addNode', node: { name: 'Handler', type: 'n8n-nodes-base.set', typeVersion: 3.4, position: [650, 300], parameters: { assignments: { assignments: [] } } } }, { type: 'addConnection', source: 'IF', target: 'Handler', branch: 'invalid' as any // Invalid value } ] }, mcpContext ); // Should fail validation expect(result.success).toBe(false); expect(result.error).toBeDefined(); }); it('should handle negative case value gracefully', async () => { // NOTE: Currently negative case values are accepted and mapped to sourceIndex=-1 // which n8n API accepts (though it may not behave correctly at runtime). // TODO: Add validation to reject negative case values in future enhancement. // Create minimal workflow with Switch node const workflowName = createTestWorkflowName('Smart Params - Negative Case'); const workflow = await client.createWorkflow({ name: workflowName, nodes: [ { id: 'manual-1', name: 'Manual', type: 'n8n-nodes-base.manualTrigger', typeVersion: 1, position: [250, 300], parameters: {} }, { id: 'switch-1', name: 'Switch', type: 'n8n-nodes-base.switch', typeVersion: 3, position: [450, 300], parameters: { mode: 'rules', rules: { rules: [] } } } ], connections: { Manual: { main: [[{ node: 'Switch', type: 'main', index: 0 }]] } }, tags: ['mcp-integration-test'] }); expect(workflow.id).toBeTruthy(); if (!workflow.id) throw new Error('Workflow ID is missing'); context.trackWorkflow(workflow.id); // Add connection with negative case value // Currently accepted but should be validated in future const result = await handleUpdatePartialWorkflow( { id: workflow.id, operations: [ { type: 'addNode', node: { name: 'Handler', type: 'n8n-nodes-base.set', typeVersion: 3.4, position: [650, 300], parameters: { assignments: { assignments: [] } } } }, { type: 'addConnection', source: 'Switch', target: 'Handler', case: -1 // Negative value - should be validated but currently accepted } ] }, mcpContext ); // Currently succeeds (validation gap) // TODO: Should fail validation when enhanced validation is added expect(result.success).toBe(true); }); }); // ====================================================================== // EDGE CASES: Branch parameter on non-IF node // ====================================================================== describe('Edge Cases', () => { it('should ignore branch parameter on non-IF node (fallback to default)', async () => { // Create workflow with Set node (not an IF node) const workflowName = createTestWorkflowName('Smart Params - Branch on Non-IF'); const workflow = await client.createWorkflow({ name: workflowName, nodes: [ { id: 'manual-1', name: 'Manual', type: 'n8n-nodes-base.manualTrigger', typeVersion: 1, position: [250, 300], parameters: {} }, { id: 'set-1', name: 'Set', type: 'n8n-nodes-base.set', typeVersion: 3.4, position: [450, 300], parameters: { assignments: { assignments: [] } } } ], connections: { Manual: { main: [[{ node: 'Set', type: 'main', index: 0 }]] } }, tags: ['mcp-integration-test'] }); expect(workflow.id).toBeTruthy(); if (!workflow.id) throw new Error('Workflow ID is missing'); context.trackWorkflow(workflow.id); // Add connection with branch parameter on non-IF node // Should be ignored and use default index 0 const result = await handleUpdatePartialWorkflow( { id: workflow.id, operations: [ { type: 'addNode', node: { name: 'Handler', type: 'n8n-nodes-base.set', typeVersion: 3.4, position: [650, 300], parameters: { assignments: { assignments: [] } } } }, { type: 'addConnection', source: 'Set', target: 'Handler', branch: 'true' // Should be ignored on non-IF node } ] }, mcpContext ); expect(result.success).toBe(true); // Fetch actual workflow from n8n API const fetchedWorkflow = await client.getWorkflow(workflow.id); // Connection should be at default index 0 (branch parameter ignored) expect(fetchedWorkflow.connections.Set).toBeDefined(); expect(fetchedWorkflow.connections.Set.main).toBeDefined(); expect(fetchedWorkflow.connections.Set.main[0]).toBeDefined(); expect(fetchedWorkflow.connections.Set.main[0].length).toBe(1); expect(fetchedWorkflow.connections.Set.main[0][0].node).toBe('Handler'); }); }); // ====================================================================== // TEST 11: Array Index Preservation (Issue #272 - Critical Bug Fix) // ====================================================================== describe('Array Index Preservation for Multi-Output Nodes', () => { it('should preserve array indices when rewiring Switch node connections', async () => { // This test verifies the fix for the critical bug where filtering empty arrays // caused index shifting in multi-output nodes (Switch, IF with multiple handlers) // // Bug: workflow.connections[node][output].filter(conns => conns.length > 0) // Fix: Only remove trailing empty arrays, preserve intermediate ones const workflowName = createTestWorkflowName('Array Index Preservation - Switch'); // Create workflow with Switch node connected to 4 handlers const workflow: Workflow = await client.createWorkflow({ name: workflowName, nodes: [ { id: '1', name: 'Start', type: 'n8n-nodes-base.manualTrigger', typeVersion: 1, position: [0, 0], parameters: {} }, { id: '2', name: 'Switch', type: 'n8n-nodes-base.switch', typeVersion: 3, position: [200, 0], parameters: { options: {}, rules: { rules: [ { conditions: { conditions: [{ leftValue: '={{$json.value}}', rightValue: '1', operator: { type: 'string', operation: 'equals' } }] } }, { conditions: { conditions: [{ leftValue: '={{$json.value}}', rightValue: '2', operator: { type: 'string', operation: 'equals' } }] } }, { conditions: { conditions: [{ leftValue: '={{$json.value}}', rightValue: '3', operator: { type: 'string', operation: 'equals' } }] } } ] } } }, { id: '3', name: 'Handler0', type: 'n8n-nodes-base.noOp', typeVersion: 1, position: [400, -100], parameters: {} }, { id: '4', name: 'Handler1', type: 'n8n-nodes-base.noOp', typeVersion: 1, position: [400, 0], parameters: {} }, { id: '5', name: 'Handler2', type: 'n8n-nodes-base.noOp', typeVersion: 1, position: [400, 100], parameters: {} }, { id: '6', name: 'Handler3', type: 'n8n-nodes-base.noOp', typeVersion: 1, position: [400, 200], parameters: {} }, { id: '7', name: 'NewHandler1', type: 'n8n-nodes-base.noOp', typeVersion: 1, position: [400, 50], parameters: {} } ], connections: { Start: { main: [[{ node: 'Switch', type: 'main', index: 0 }]] }, Switch: { main: [ [{ node: 'Handler0', type: 'main', index: 0 }], // case 0 [{ node: 'Handler1', type: 'main', index: 0 }], // case 1 [{ node: 'Handler2', type: 'main', index: 0 }], // case 2 [{ node: 'Handler3', type: 'main', index: 0 }] // case 3 (fallback) ] } } }); expect(workflow.id).toBeTruthy(); if (!workflow.id) throw new Error('Workflow ID is missing'); context.trackWorkflow(workflow.id); // Rewire case 1 from Handler1 to NewHandler1 // CRITICAL: This should NOT shift indices of case 2 and case 3 await handleUpdatePartialWorkflow({ id: workflow.id, operations: [ { type: 'rewireConnection', source: 'Switch', from: 'Handler1', to: 'NewHandler1', case: 1 } ] }); const fetchedWorkflow = await client.getWorkflow(workflow.id); // Verify all indices are preserved correctly expect(fetchedWorkflow.connections.Switch).toBeDefined(); expect(fetchedWorkflow.connections.Switch.main).toBeDefined(); // case 0: Should still be Handler0 expect(fetchedWorkflow.connections.Switch.main[0]).toBeDefined(); expect(fetchedWorkflow.connections.Switch.main[0].length).toBe(1); expect(fetchedWorkflow.connections.Switch.main[0][0].node).toBe('Handler0'); // case 1: Should now be NewHandler1 (rewired) expect(fetchedWorkflow.connections.Switch.main[1]).toBeDefined(); expect(fetchedWorkflow.connections.Switch.main[1].length).toBe(1); expect(fetchedWorkflow.connections.Switch.main[1][0].node).toBe('NewHandler1'); // case 2: Should STILL be Handler2 (index NOT shifted!) expect(fetchedWorkflow.connections.Switch.main[2]).toBeDefined(); expect(fetchedWorkflow.connections.Switch.main[2].length).toBe(1); expect(fetchedWorkflow.connections.Switch.main[2][0].node).toBe('Handler2'); // case 3: Should STILL be Handler3 (index NOT shifted!) expect(fetchedWorkflow.connections.Switch.main[3]).toBeDefined(); expect(fetchedWorkflow.connections.Switch.main[3].length).toBe(1); expect(fetchedWorkflow.connections.Switch.main[3][0].node).toBe('Handler3'); }); it('should preserve empty arrays when removing IF node connections', async () => { // This test verifies that removing a connection creates an empty array // rather than shifting indices (which would break the true/false semantics) const workflowName = createTestWorkflowName('Array Preservation - IF Remove'); // Create workflow with IF node connected to both branches const workflow: Workflow = await client.createWorkflow({ name: workflowName, nodes: [ { id: '1', name: 'Start', type: 'n8n-nodes-base.manualTrigger', typeVersion: 1, position: [0, 0], parameters: {} }, { id: '2', name: 'IF', type: 'n8n-nodes-base.if', typeVersion: 2, position: [200, 0], parameters: { conditions: { conditions: [ { id: 'cond-1', leftValue: '={{ $json.value }}', rightValue: 'test', operation: 'equal' } ] } } }, { id: '3', name: 'TrueHandler', type: 'n8n-nodes-base.noOp', typeVersion: 1, position: [400, -50], parameters: {} }, { id: '4', name: 'FalseHandler', type: 'n8n-nodes-base.noOp', typeVersion: 1, position: [400, 50], parameters: {} } ], connections: { Start: { main: [[{ node: 'IF', type: 'main', index: 0 }]] }, IF: { main: [ [{ node: 'TrueHandler', type: 'main', index: 0 }], // true branch (index 0) [{ node: 'FalseHandler', type: 'main', index: 0 }] // false branch (index 1) ] } } }); expect(workflow.id).toBeTruthy(); if (!workflow.id) throw new Error('Workflow ID is missing'); context.trackWorkflow(workflow.id); // Remove connection from true branch (index 0) await handleUpdatePartialWorkflow({ id: workflow.id, operations: [ { type: 'removeConnection', source: 'IF', target: 'TrueHandler', branch: 'true' } ] }); const fetchedWorkflow = await client.getWorkflow(workflow.id); // Verify structure: empty array at index 0, FalseHandler still at index 1 expect(fetchedWorkflow.connections.IF).toBeDefined(); expect(fetchedWorkflow.connections.IF.main).toBeDefined(); expect(fetchedWorkflow.connections.IF.main.length).toBe(2); // Index 0 (true branch): Should be empty array (NOT removed!) expect(fetchedWorkflow.connections.IF.main[0]).toBeDefined(); expect(fetchedWorkflow.connections.IF.main[0].length).toBe(0); // Index 1 (false branch): Should STILL be FalseHandler (NOT shifted to index 0!) expect(fetchedWorkflow.connections.IF.main[1]).toBeDefined(); expect(fetchedWorkflow.connections.IF.main[1].length).toBe(1); expect(fetchedWorkflow.connections.IF.main[1][0].node).toBe('FalseHandler'); }); it('should preserve indices when removing first case from Switch node', async () => { // MOST CRITICAL TEST: Verifies removing first output doesn't shift all others // This is the exact bug scenario that was production-breaking const workflowName = createTestWorkflowName('Array Preservation - Switch Remove First'); const workflow: Workflow = await client.createWorkflow({ name: workflowName, nodes: [ { id: '1', name: 'Start', type: 'n8n-nodes-base.manualTrigger', typeVersion: 1, position: [0, 0], parameters: {} }, { id: '2', name: 'Switch', type: 'n8n-nodes-base.switch', typeVersion: 3, position: [200, 0], parameters: { options: {}, rules: { rules: [ { conditions: { conditions: [{ leftValue: '={{$json.case}}', rightValue: '0', operator: { type: 'string', operation: 'equals' } }] } }, { conditions: { conditions: [{ leftValue: '={{$json.case}}', rightValue: '1', operator: { type: 'string', operation: 'equals' } }] } }, { conditions: { conditions: [{ leftValue: '={{$json.case}}', rightValue: '2', operator: { type: 'string', operation: 'equals' } }] } } ] } } }, { id: '3', name: 'Handler0', type: 'n8n-nodes-base.noOp', typeVersion: 1, position: [400, -100], parameters: {} }, { id: '4', name: 'Handler1', type: 'n8n-nodes-base.noOp', typeVersion: 1, position: [400, 0], parameters: {} }, { id: '5', name: 'Handler2', type: 'n8n-nodes-base.noOp', typeVersion: 1, position: [400, 100], parameters: {} }, { id: '6', name: 'FallbackHandler', type: 'n8n-nodes-base.noOp', typeVersion: 1, position: [400, 200], parameters: {} } ], connections: { Start: { main: [[{ node: 'Switch', type: 'main', index: 0 }]] }, Switch: { main: [ [{ node: 'Handler0', type: 'main', index: 0 }], // case 0 [{ node: 'Handler1', type: 'main', index: 0 }], // case 1 [{ node: 'Handler2', type: 'main', index: 0 }], // case 2 [{ node: 'FallbackHandler', type: 'main', index: 0 }] // fallback (case 3) ] } } }); expect(workflow.id).toBeTruthy(); if (!workflow.id) throw new Error('Workflow ID is missing'); context.trackWorkflow(workflow.id); // Remove connection from case 0 (first output) await handleUpdatePartialWorkflow({ id: workflow.id, operations: [ { type: 'removeConnection', source: 'Switch', target: 'Handler0', case: 0 } ] }); const fetchedWorkflow = await client.getWorkflow(workflow.id); expect(fetchedWorkflow.connections.Switch).toBeDefined(); expect(fetchedWorkflow.connections.Switch.main).toBeDefined(); expect(fetchedWorkflow.connections.Switch.main.length).toBe(4); // case 0: Should be empty array (NOT removed!) expect(fetchedWorkflow.connections.Switch.main[0]).toBeDefined(); expect(fetchedWorkflow.connections.Switch.main[0].length).toBe(0); // case 1: Should STILL be Handler1 at index 1 (NOT shifted to 0!) expect(fetchedWorkflow.connections.Switch.main[1]).toBeDefined(); expect(fetchedWorkflow.connections.Switch.main[1].length).toBe(1); expect(fetchedWorkflow.connections.Switch.main[1][0].node).toBe('Handler1'); // case 2: Should STILL be Handler2 at index 2 (NOT shifted to 1!) expect(fetchedWorkflow.connections.Switch.main[2]).toBeDefined(); expect(fetchedWorkflow.connections.Switch.main[2].length).toBe(1); expect(fetchedWorkflow.connections.Switch.main[2][0].node).toBe('Handler2'); // case 3: Should STILL be FallbackHandler at index 3 (NOT shifted to 2!) expect(fetchedWorkflow.connections.Switch.main[3]).toBeDefined(); expect(fetchedWorkflow.connections.Switch.main[3].length).toBe(1); expect(fetchedWorkflow.connections.Switch.main[3][0].node).toBe('FallbackHandler'); }); it('should preserve indices through sequential operations on Switch node', async () => { // Complex scenario: Multiple operations in sequence on the same Switch node // This tests that our fix works correctly across multiple operations const workflowName = createTestWorkflowName('Array Preservation - Sequential Ops'); const workflow: Workflow = await client.createWorkflow({ name: workflowName, nodes: [ { id: '1', name: 'Start', type: 'n8n-nodes-base.manualTrigger', typeVersion: 1, position: [0, 0], parameters: {} }, { id: '2', name: 'Switch', type: 'n8n-nodes-base.switch', typeVersion: 3, position: [200, 0], parameters: { options: {}, rules: { rules: [ { conditions: { conditions: [{ leftValue: '={{$json.value}}', rightValue: '1', operator: { type: 'string', operation: 'equals' } }] } }, { conditions: { conditions: [{ leftValue: '={{$json.value}}', rightValue: '2', operator: { type: 'string', operation: 'equals' } }] } } ] } } }, { id: '3', name: 'Handler0', type: 'n8n-nodes-base.noOp', typeVersion: 1, position: [400, -50], parameters: {} }, { id: '4', name: 'Handler1', type: 'n8n-nodes-base.noOp', typeVersion: 1, position: [400, 50], parameters: {} }, { id: '5', name: 'NewHandler0', type: 'n8n-nodes-base.noOp', typeVersion: 1, position: [400, -100], parameters: {} }, { id: '6', name: 'NewHandler2', type: 'n8n-nodes-base.noOp', typeVersion: 1, position: [400, 150], parameters: {} } ], connections: { Start: { main: [[{ node: 'Switch', type: 'main', index: 0 }]] }, Switch: { main: [ [{ node: 'Handler0', type: 'main', index: 0 }], // case 0 [{ node: 'Handler1', type: 'main', index: 0 }] // case 1 ] } } }); expect(workflow.id).toBeTruthy(); if (!workflow.id) throw new Error('Workflow ID is missing'); context.trackWorkflow(workflow.id); // Sequential operations: // 1. Rewire case 0: Handler0 → NewHandler0 // 2. Add connection to case 2 (new handler) // 3. Remove connection from case 1 (Handler1) await handleUpdatePartialWorkflow({ id: workflow.id, operations: [ { type: 'rewireConnection', source: 'Switch', from: 'Handler0', to: 'NewHandler0', case: 0 }, { type: 'addConnection', source: 'Switch', target: 'NewHandler2', case: 2 }, { type: 'removeConnection', source: 'Switch', target: 'Handler1', case: 1 } ] }); const fetchedWorkflow = await client.getWorkflow(workflow.id); expect(fetchedWorkflow.connections.Switch).toBeDefined(); expect(fetchedWorkflow.connections.Switch.main).toBeDefined(); expect(fetchedWorkflow.connections.Switch.main.length).toBe(3); // case 0: Should be NewHandler0 (rewired) expect(fetchedWorkflow.connections.Switch.main[0]).toBeDefined(); expect(fetchedWorkflow.connections.Switch.main[0].length).toBe(1); expect(fetchedWorkflow.connections.Switch.main[0][0].node).toBe('NewHandler0'); // case 1: Should be empty array (removed) expect(fetchedWorkflow.connections.Switch.main[1]).toBeDefined(); expect(fetchedWorkflow.connections.Switch.main[1].length).toBe(0); // case 2: Should be NewHandler2 (added) expect(fetchedWorkflow.connections.Switch.main[2]).toBeDefined(); expect(fetchedWorkflow.connections.Switch.main[2].length).toBe(1); expect(fetchedWorkflow.connections.Switch.main[2][0].node).toBe('NewHandler2'); }); it('should preserve indices when rewiring Filter node connections', async () => { // Filter node has 2 outputs: kept items (index 0) and discarded items (index 1) // Test that rewiring one doesn't affect the other const workflowName = createTestWorkflowName('Array Preservation - Filter'); const workflow: Workflow = await client.createWorkflow({ name: workflowName, nodes: [ { id: '1', name: 'Start', type: 'n8n-nodes-base.manualTrigger', typeVersion: 1, position: [0, 0], parameters: {} }, { id: '2', name: 'Filter', type: 'n8n-nodes-base.filter', typeVersion: 2, position: [200, 0], parameters: { conditions: { conditions: [ { id: 'cond-1', leftValue: '={{ $json.value }}', rightValue: 10, operator: { type: 'number', operation: 'gt' } } ] } } }, { id: '3', name: 'KeptHandler', type: 'n8n-nodes-base.noOp', typeVersion: 1, position: [400, -50], parameters: {} }, { id: '4', name: 'DiscardedHandler', type: 'n8n-nodes-base.noOp', typeVersion: 1, position: [400, 50], parameters: {} }, { id: '5', name: 'NewKeptHandler', type: 'n8n-nodes-base.noOp', typeVersion: 1, position: [400, -100], parameters: {} } ], connections: { Start: { main: [[{ node: 'Filter', type: 'main', index: 0 }]] }, Filter: { main: [ [{ node: 'KeptHandler', type: 'main', index: 0 }], // kept items (index 0) [{ node: 'DiscardedHandler', type: 'main', index: 0 }] // discarded items (index 1) ] } } }); expect(workflow.id).toBeTruthy(); if (!workflow.id) throw new Error('Workflow ID is missing'); context.trackWorkflow(workflow.id); // Rewire kept items output (index 0) from KeptHandler to NewKeptHandler await handleUpdatePartialWorkflow({ id: workflow.id, operations: [ { type: 'rewireConnection', source: 'Filter', from: 'KeptHandler', to: 'NewKeptHandler', sourceIndex: 0 } ] }); const fetchedWorkflow = await client.getWorkflow(workflow.id); expect(fetchedWorkflow.connections.Filter).toBeDefined(); expect(fetchedWorkflow.connections.Filter.main).toBeDefined(); expect(fetchedWorkflow.connections.Filter.main.length).toBe(2); // Index 0 (kept items): Should now be NewKeptHandler expect(fetchedWorkflow.connections.Filter.main[0]).toBeDefined(); expect(fetchedWorkflow.connections.Filter.main[0].length).toBe(1); expect(fetchedWorkflow.connections.Filter.main[0][0].node).toBe('NewKeptHandler'); // Index 1 (discarded items): Should STILL be DiscardedHandler (unchanged) expect(fetchedWorkflow.connections.Filter.main[1]).toBeDefined(); expect(fetchedWorkflow.connections.Filter.main[1].length).toBe(1); expect(fetchedWorkflow.connections.Filter.main[1][0].node).toBe('DiscardedHandler'); }); }); // ====================================================================== // TEST 16-19: Merge Node - Multiple Inputs (targetIndex preservation) // ====================================================================== describe('Merge Node - Multiple Inputs (targetIndex Preservation)', () => { it('should preserve targetIndex when removing connection to Merge input 0', async () => { // CRITICAL: Merge has multiple INPUTS (unlike Switch which has multiple outputs) // This tests that targetIndex preservation works for incoming connections // Bug would cause: Remove input 0 → input 1 shifts to input 0 const workflowName = createTestWorkflowName('Merge - Remove Input 0'); const workflow: Workflow = await client.createWorkflow({ name: workflowName, nodes: [ { id: '1', name: 'Start', type: 'n8n-nodes-base.manualTrigger', typeVersion: 1, position: [0, 0], parameters: {} }, { id: '2', name: 'Source1', type: 'n8n-nodes-base.set', typeVersion: 3.4, position: [200, -50], parameters: { assignments: { assignments: [ { id: 'a1', name: 'source', value: '1', type: 'string' } ] } } }, { id: '3', name: 'Source2', type: 'n8n-nodes-base.set', typeVersion: 3.4, position: [200, 50], parameters: { assignments: { assignments: [ { id: 'a2', name: 'source', value: '2', type: 'string' } ] } } }, { id: '4', name: 'Merge', type: 'n8n-nodes-base.merge', typeVersion: 3, position: [400, 0], parameters: { mode: 'append' } }, { id: '5', name: 'Output', type: 'n8n-nodes-base.noOp', typeVersion: 1, position: [600, 0], parameters: {} } ], connections: { Start: { main: [[{ node: 'Source1', type: 'main', index: 0 }]] }, Source1: { main: [[{ node: 'Merge', type: 'main', index: 0 }]] // to Merge input 0 }, Source2: { main: [[{ node: 'Merge', type: 'main', index: 1 }]] // to Merge input 1 }, Merge: { main: [[{ node: 'Output', type: 'main', index: 0 }]] } } }); expect(workflow.id).toBeTruthy(); if (!workflow.id) throw new Error('Workflow ID is missing'); context.trackWorkflow(workflow.id); // Remove connection from Source1 to Merge (input 0) await handleUpdatePartialWorkflow({ id: workflow.id, operations: [ { type: 'removeConnection', source: 'Source1', target: 'Merge' } ] }); const fetchedWorkflow = await client.getWorkflow(workflow.id); // CRITICAL VERIFICATION: Source2 should STILL connect to Merge at targetIndex 1 // Bug would cause it to shift to targetIndex 0 expect(fetchedWorkflow.connections.Source2).toBeDefined(); expect(fetchedWorkflow.connections.Source2.main).toBeDefined(); expect(fetchedWorkflow.connections.Source2.main[0]).toBeDefined(); expect(fetchedWorkflow.connections.Source2.main[0].length).toBe(1); expect(fetchedWorkflow.connections.Source2.main[0][0].node).toBe('Merge'); expect(fetchedWorkflow.connections.Source2.main[0][0].index).toBe(1); // STILL index 1! // Source1 should no longer connect to Merge expect(fetchedWorkflow.connections.Source1).toBeUndefined(); }); it('should preserve targetIndex when removing middle connection to Merge', async () => { // MOST CRITICAL: Remove middle input, verify inputs 0 and 2 stay at their indices // This is the multi-input equivalent of the Switch node bug const workflowName = createTestWorkflowName('Merge - Remove Middle Input'); const workflow: Workflow = await client.createWorkflow({ name: workflowName, nodes: [ { id: '1', name: 'Source0', type: 'n8n-nodes-base.set', typeVersion: 3.4, position: [0, -100], parameters: { assignments: { assignments: [ { id: 'a0', name: 'source', value: '0', type: 'string' } ] } } }, { id: '2', name: 'Source1', type: 'n8n-nodes-base.set', typeVersion: 3.4, position: [0, 0], parameters: { assignments: { assignments: [ { id: 'a1', name: 'source', value: '1', type: 'string' } ] } } }, { id: '3', name: 'Source2', type: 'n8n-nodes-base.set', typeVersion: 3.4, position: [0, 100], parameters: { assignments: { assignments: [ { id: 'a2', name: 'source', value: '2', type: 'string' } ] } } }, { id: '4', name: 'Merge', type: 'n8n-nodes-base.merge', typeVersion: 3, position: [200, 0], parameters: { mode: 'append' } } ], connections: { Source0: { main: [[{ node: 'Merge', type: 'main', index: 0 }]] // input 0 }, Source1: { main: [[{ node: 'Merge', type: 'main', index: 1 }]] // input 1 }, Source2: { main: [[{ node: 'Merge', type: 'main', index: 2 }]] // input 2 } } }); expect(workflow.id).toBeTruthy(); if (!workflow.id) throw new Error('Workflow ID is missing'); context.trackWorkflow(workflow.id); // Remove connection from Source1 to Merge (middle input) await handleUpdatePartialWorkflow({ id: workflow.id, operations: [ { type: 'removeConnection', source: 'Source1', target: 'Merge' } ] }); const fetchedWorkflow = await client.getWorkflow(workflow.id); // Source0 should STILL connect to Merge at targetIndex 0 expect(fetchedWorkflow.connections.Source0).toBeDefined(); expect(fetchedWorkflow.connections.Source0.main[0][0].node).toBe('Merge'); expect(fetchedWorkflow.connections.Source0.main[0][0].index).toBe(0); // STILL 0! // Source2 should STILL connect to Merge at targetIndex 2 (NOT shifted to 1!) expect(fetchedWorkflow.connections.Source2).toBeDefined(); expect(fetchedWorkflow.connections.Source2.main[0][0].node).toBe('Merge'); expect(fetchedWorkflow.connections.Source2.main[0][0].index).toBe(2); // STILL 2! // Source1 should no longer connect to Merge expect(fetchedWorkflow.connections.Source1).toBeUndefined(); }); it('should handle replacing source connection to Merge input', async () => { // Test replacing which node connects to a Merge input // Use remove + add pattern (not rewireConnection which changes target, not source) const workflowName = createTestWorkflowName('Merge - Replace Source'); const workflow: Workflow = await client.createWorkflow({ name: workflowName, nodes: [ { id: '1', name: 'Source1', type: 'n8n-nodes-base.set', typeVersion: 3.4, position: [0, -50], parameters: { assignments: { assignments: [ { id: 'a1', name: 'source', value: '1', type: 'string' } ] } } }, { id: '2', name: 'Source2', type: 'n8n-nodes-base.set', typeVersion: 3.4, position: [0, 50], parameters: { assignments: { assignments: [ { id: 'a2', name: 'source', value: '2', type: 'string' } ] } } }, { id: '3', name: 'NewSource1', type: 'n8n-nodes-base.set', typeVersion: 3.4, position: [0, -100], parameters: { assignments: { assignments: [ { id: 'a3', name: 'source', value: 'new1', type: 'string' } ] } } }, { id: '4', name: 'Merge', type: 'n8n-nodes-base.merge', typeVersion: 3, position: [200, 0], parameters: { mode: 'append' } } ], connections: { Source1: { main: [[{ node: 'Merge', type: 'main', index: 0 }]] }, Source2: { main: [[{ node: 'Merge', type: 'main', index: 1 }]] } } }); expect(workflow.id).toBeTruthy(); if (!workflow.id) throw new Error('Workflow ID is missing'); context.trackWorkflow(workflow.id); // Replace Source1 with NewSource1 (both to Merge input 0) // Use remove + add pattern await handleUpdatePartialWorkflow({ id: workflow.id, operations: [ { type: 'removeConnection', source: 'Source1', target: 'Merge' }, { type: 'addConnection', source: 'NewSource1', target: 'Merge', targetInput: 'main', targetIndex: 0 } ] }); const fetchedWorkflow = await client.getWorkflow(workflow.id); // NewSource1 should now connect to Merge at input 0 expect(fetchedWorkflow.connections.NewSource1).toBeDefined(); expect(fetchedWorkflow.connections.NewSource1.main[0][0].node).toBe('Merge'); expect(fetchedWorkflow.connections.NewSource1.main[0][0].index).toBe(0); // Source2 should STILL connect to Merge at input 1 (unchanged) expect(fetchedWorkflow.connections.Source2).toBeDefined(); expect(fetchedWorkflow.connections.Source2.main[0][0].node).toBe('Merge'); expect(fetchedWorkflow.connections.Source2.main[0][0].index).toBe(1); // Source1 should no longer connect to Merge expect(fetchedWorkflow.connections.Source1).toBeUndefined(); }); it('should preserve indices through sequential operations on Merge inputs', async () => { // Complex scenario: Multiple operations on Merge inputs in sequence const workflowName = createTestWorkflowName('Merge - Sequential Ops'); const workflow: Workflow = await client.createWorkflow({ name: workflowName, nodes: [ { id: '1', name: 'Source1', type: 'n8n-nodes-base.set', typeVersion: 3.4, position: [0, -50], parameters: { assignments: { assignments: [ { id: 'a1', name: 'source', value: '1', type: 'string' } ] } } }, { id: '2', name: 'Source2', type: 'n8n-nodes-base.set', typeVersion: 3.4, position: [0, 50], parameters: { assignments: { assignments: [ { id: 'a2', name: 'source', value: '2', type: 'string' } ] } } }, { id: '3', name: 'NewSource1', type: 'n8n-nodes-base.set', typeVersion: 3.4, position: [0, -100], parameters: { assignments: { assignments: [ { id: 'a3', name: 'source', value: 'new1', type: 'string' } ] } } }, { id: '4', name: 'Source3', type: 'n8n-nodes-base.set', typeVersion: 3.4, position: [0, 150], parameters: { assignments: { assignments: [ { id: 'a4', name: 'source', value: '3', type: 'string' } ] } } }, { id: '5', name: 'Merge', type: 'n8n-nodes-base.merge', typeVersion: 3, position: [200, 0], parameters: { mode: 'append' } } ], connections: { Source1: { main: [[{ node: 'Merge', type: 'main', index: 0 }]] }, Source2: { main: [[{ node: 'Merge', type: 'main', index: 1 }]] } } }); expect(workflow.id).toBeTruthy(); if (!workflow.id) throw new Error('Workflow ID is missing'); context.trackWorkflow(workflow.id); // Sequential operations: // 1. Replace input 0: Source1 → NewSource1 (remove + add) // 2. Add Source3 → Merge input 2 // 3. Remove connection from Source2 (input 1) await handleUpdatePartialWorkflow({ id: workflow.id, operations: [ { type: 'removeConnection', source: 'Source1', target: 'Merge' }, { type: 'addConnection', source: 'NewSource1', target: 'Merge', targetInput: 'main', targetIndex: 0 }, { type: 'addConnection', source: 'Source3', target: 'Merge', targetInput: 'main', targetIndex: 2 }, { type: 'removeConnection', source: 'Source2', target: 'Merge' } ] }); const fetchedWorkflow = await client.getWorkflow(workflow.id); // NewSource1 should connect to Merge at input 0 (rewired) expect(fetchedWorkflow.connections.NewSource1).toBeDefined(); expect(fetchedWorkflow.connections.NewSource1.main[0][0].node).toBe('Merge'); expect(fetchedWorkflow.connections.NewSource1.main[0][0].index).toBe(0); // Source2 removed, should not exist expect(fetchedWorkflow.connections.Source2).toBeUndefined(); // Source3 should connect to Merge at input 2 (NOT shifted to 1!) expect(fetchedWorkflow.connections.Source3).toBeDefined(); expect(fetchedWorkflow.connections.Source3.main[0][0].node).toBe('Merge'); expect(fetchedWorkflow.connections.Source3.main[0][0].index).toBe(2); }); }); }); ```