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

# Directory Structure

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

```typescript
  1 | import { describe, it, expect, beforeAll, afterAll } from 'vitest';
  2 | import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js';
  3 | import { Client } from '@modelcontextprotocol/sdk/client/index.js';
  4 | import { TestableN8NMCPServer } from './test-helpers';
  5 | 
  6 | describe('MCP Session Management', { timeout: 15000 }, () => {
  7 |   let originalMswEnabled: string | undefined;
  8 |   
  9 |   beforeAll(() => {
 10 |     // Save original value
 11 |     originalMswEnabled = process.env.MSW_ENABLED;
 12 |     // Disable MSW for these integration tests
 13 |     process.env.MSW_ENABLED = 'false';
 14 |   });
 15 | 
 16 |   afterAll(async () => {
 17 |     // Restore original value
 18 |     if (originalMswEnabled !== undefined) {
 19 |       process.env.MSW_ENABLED = originalMswEnabled;
 20 |     } else {
 21 |       delete process.env.MSW_ENABLED;
 22 |     }
 23 |     // Clean up any shared resources
 24 |     await TestableN8NMCPServer.shutdownShared();
 25 |   });
 26 | 
 27 |   describe('Session Lifecycle', () => {
 28 |     it('should establish a new session', async () => {
 29 |       const mcpServer = new TestableN8NMCPServer();
 30 |       await mcpServer.initialize();
 31 |       
 32 |       const [serverTransport, clientTransport] = InMemoryTransport.createLinkedPair();
 33 |       await mcpServer.connectToTransport(serverTransport);
 34 | 
 35 |       const client = new Client({
 36 |         name: 'test-client',
 37 |         version: '1.0.0'
 38 |       }, {
 39 |         capabilities: {}
 40 |       });
 41 | 
 42 |       await client.connect(clientTransport);
 43 | 
 44 |       // Session should be established
 45 |       const serverInfo = await client.getServerVersion();
 46 |       expect(serverInfo).toHaveProperty('name', 'n8n-documentation-mcp');
 47 |       
 48 |       // Clean up - ensure proper order
 49 |       await client.close();
 50 |       await new Promise(resolve => setTimeout(resolve, 50)); // Give time for client to fully close
 51 |       await mcpServer.close();
 52 |     });
 53 | 
 54 |     it('should handle session initialization with capabilities', async () => {
 55 |       const mcpServer = new TestableN8NMCPServer();
 56 |       await mcpServer.initialize();
 57 |       
 58 |       const [serverTransport, clientTransport] = InMemoryTransport.createLinkedPair();
 59 |       await mcpServer.connectToTransport(serverTransport);
 60 | 
 61 |       const client = new Client({
 62 |         name: 'test-client',
 63 |         version: '1.0.0'
 64 |       }, {
 65 |         capabilities: {
 66 |           // Client capabilities
 67 |           experimental: {}
 68 |         }
 69 |       });
 70 | 
 71 |       await client.connect(clientTransport);
 72 | 
 73 |       const serverInfo = await client.getServerVersion();
 74 |       expect(serverInfo).toBeDefined();
 75 |       expect(serverInfo?.name).toBe('n8n-documentation-mcp');
 76 |       
 77 |       // Check capabilities if they exist
 78 |       if (serverInfo?.capabilities) {
 79 |         expect(serverInfo.capabilities).toHaveProperty('tools');
 80 |       }
 81 |       
 82 |       // Clean up - ensure proper order
 83 |       await client.close();
 84 |       await new Promise(resolve => setTimeout(resolve, 50)); // Give time for client to fully close
 85 |       await mcpServer.close();
 86 |     });
 87 | 
 88 |     it('should handle clean session termination', async () => {
 89 |       const mcpServer = new TestableN8NMCPServer();
 90 |       await mcpServer.initialize();
 91 |       
 92 |       const [serverTransport, clientTransport] = InMemoryTransport.createLinkedPair();
 93 |       await mcpServer.connectToTransport(serverTransport);
 94 | 
 95 |       const client = new Client({
 96 |         name: 'test-client',
 97 |         version: '1.0.0'
 98 |       }, {});
 99 | 
100 |       await client.connect(clientTransport);
101 | 
102 |       // Make some requests
103 |       await client.callTool({ name: 'get_database_statistics', arguments: {} });
104 |       await client.callTool({ name: 'list_nodes', arguments: { limit: 5 } });
105 | 
106 |       // Clean termination
107 |       await client.close();
108 |       await new Promise(resolve => setTimeout(resolve, 50)); // Give time for client to fully close
109 | 
110 |       // Client should be closed
111 |       try {
112 |         await client.callTool({ name: 'get_database_statistics', arguments: {} });
113 |         expect.fail('Should not be able to make requests after close');
114 |       } catch (error) {
115 |         expect(error).toBeDefined();
116 |       }
117 |       
118 |       await mcpServer.close();
119 |     });
120 | 
121 |     it('should handle abrupt disconnection', async () => {
122 |       const mcpServer = new TestableN8NMCPServer();
123 |       await mcpServer.initialize();
124 |       
125 |       const [serverTransport, clientTransport] = InMemoryTransport.createLinkedPair();
126 |       await mcpServer.connectToTransport(serverTransport);
127 | 
128 |       const client = new Client({
129 |         name: 'test-client',
130 |         version: '1.0.0'
131 |       }, {});
132 | 
133 |       await client.connect(clientTransport);
134 | 
135 |       // Make a request to ensure connection is active
136 |       await client.callTool({ name: 'get_database_statistics', arguments: {} });
137 | 
138 |       // Simulate abrupt disconnection by closing transport
139 |       await clientTransport.close();
140 |       await new Promise(resolve => setTimeout(resolve, 50)); // Give time for transport to fully close
141 | 
142 |       // Further operations should fail
143 |       try {
144 |         await client.callTool({ name: 'list_nodes', arguments: {} });
145 |         expect.fail('Should not be able to make requests after transport close');
146 |       } catch (error) {
147 |         expect(error).toBeDefined();
148 |       }
149 |       
150 |       // Note: client is already disconnected, no need to close it
151 |       await mcpServer.close();
152 |     });
153 |   });
154 | 
155 |   describe('Multiple Sessions', () => {
156 |     it('should handle multiple concurrent sessions', async () => {
157 |       // Skip this test for now - it has concurrency issues
158 |       // TODO: Fix concurrent session handling in MCP server
159 |       console.log('Skipping concurrent sessions test - known timeout issue');
160 |       expect(true).toBe(true);
161 |     }, { skip: true });
162 | 
163 |     it('should isolate session state', async () => {
164 |       // Skip this test for now - it has concurrency issues
165 |       // TODO: Fix session isolation in MCP server
166 |       console.log('Skipping session isolation test - known timeout issue');
167 |       expect(true).toBe(true);
168 |     }, { skip: true });
169 | 
170 |     it('should handle sequential sessions without interference', async () => {
171 |       // Create first session
172 |       const mcpServer1 = new TestableN8NMCPServer();
173 |       await mcpServer1.initialize();
174 |       
175 |       const [st1, ct1] = InMemoryTransport.createLinkedPair();
176 |       await mcpServer1.connectToTransport(st1);
177 | 
178 |       const client1 = new Client({ name: 'seq-client1', version: '1.0.0' }, {});
179 |       await client1.connect(ct1);
180 | 
181 |       // First session operations
182 |       const response1 = await client1.callTool({ name: 'list_nodes', arguments: { limit: 3 } });
183 |       expect(response1).toBeDefined();
184 |       expect((response1 as any).content).toBeDefined();
185 |       expect((response1 as any).content[0]).toHaveProperty('type', 'text');
186 |       const data1 = JSON.parse(((response1 as any).content[0] as any).text);
187 |       // Handle both array response and object with nodes property
188 |       const nodes1 = Array.isArray(data1) ? data1 : data1.nodes;
189 |       expect(nodes1).toHaveLength(3);
190 | 
191 |       // Close first session completely
192 |       await client1.close();
193 |       await mcpServer1.close();
194 |       await new Promise(resolve => setTimeout(resolve, 100));
195 | 
196 |       // Create second session
197 |       const mcpServer2 = new TestableN8NMCPServer();
198 |       await mcpServer2.initialize();
199 |       
200 |       const [st2, ct2] = InMemoryTransport.createLinkedPair();
201 |       await mcpServer2.connectToTransport(st2);
202 | 
203 |       const client2 = new Client({ name: 'seq-client2', version: '1.0.0' }, {});
204 |       await client2.connect(ct2);
205 | 
206 |       // Second session operations
207 |       const response2 = await client2.callTool({ name: 'list_nodes', arguments: { limit: 5 } });
208 |       expect(response2).toBeDefined();
209 |       expect((response2 as any).content).toBeDefined();
210 |       expect((response2 as any).content[0]).toHaveProperty('type', 'text');
211 |       const data2 = JSON.parse(((response2 as any).content[0] as any).text);
212 |       // Handle both array response and object with nodes property
213 |       const nodes2 = Array.isArray(data2) ? data2 : data2.nodes;
214 |       expect(nodes2).toHaveLength(5);
215 | 
216 |       // Clean up
217 |       await client2.close();
218 |       await mcpServer2.close();
219 |     });
220 | 
221 |     it('should handle single server with multiple sequential connections', async () => {
222 |       const mcpServer = new TestableN8NMCPServer();
223 |       await mcpServer.initialize();
224 | 
225 |       // First connection
226 |       const [st1, ct1] = InMemoryTransport.createLinkedPair();
227 |       await mcpServer.connectToTransport(st1);
228 |       const client1 = new Client({ name: 'multi-seq-1', version: '1.0.0' }, {});
229 |       await client1.connect(ct1);
230 |       
231 |       const resp1 = await client1.callTool({ name: 'get_database_statistics', arguments: {} });
232 |       expect(resp1).toBeDefined();
233 |       
234 |       await client1.close();
235 |       await new Promise(resolve => setTimeout(resolve, 50));
236 | 
237 |       // Second connection to same server
238 |       const [st2, ct2] = InMemoryTransport.createLinkedPair();
239 |       await mcpServer.connectToTransport(st2);
240 |       const client2 = new Client({ name: 'multi-seq-2', version: '1.0.0' }, {});
241 |       await client2.connect(ct2);
242 |       
243 |       const resp2 = await client2.callTool({ name: 'get_database_statistics', arguments: {} });
244 |       expect(resp2).toBeDefined();
245 |       
246 |       await client2.close();
247 |       await mcpServer.close();
248 |     });
249 |   });
250 | 
251 |   describe('Session Recovery', () => {
252 |     it('should not persist state between sessions', async () => {
253 |       // First session
254 |       const mcpServer1 = new TestableN8NMCPServer();
255 |       await mcpServer1.initialize();
256 |       
257 |       const [st1, ct1] = InMemoryTransport.createLinkedPair();
258 |       await mcpServer1.connectToTransport(st1);
259 | 
260 |       const client1 = new Client({ name: 'client1', version: '1.0.0' }, {});
261 |       await client1.connect(ct1);
262 | 
263 |       // Make some requests
264 |       await client1.callTool({ name: 'list_nodes', arguments: { limit: 10 } });
265 |       await client1.close();
266 |       await mcpServer1.close();
267 | 
268 |       // Second session - should be fresh
269 |       const mcpServer2 = new TestableN8NMCPServer();
270 |       await mcpServer2.initialize();
271 |       
272 |       const [st2, ct2] = InMemoryTransport.createLinkedPair();
273 |       await mcpServer2.connectToTransport(st2);
274 | 
275 |       const client2 = new Client({ name: 'client2', version: '1.0.0' }, {});
276 |       await client2.connect(ct2);
277 | 
278 |       // Should work normally
279 |       const response = await client2.callTool({ name: 'get_database_statistics', arguments: {} });
280 |       expect(response).toBeDefined();
281 | 
282 |       await client2.close();
283 |       await mcpServer2.close();
284 |     });
285 | 
286 |     it('should handle rapid session cycling', async () => {
287 |       for (let i = 0; i < 10; i++) {
288 |         const mcpServer = new TestableN8NMCPServer();
289 |         await mcpServer.initialize();
290 |         
291 |         const [serverTransport, clientTransport] = InMemoryTransport.createLinkedPair();
292 |         await mcpServer.connectToTransport(serverTransport);
293 | 
294 |         const client = new Client({ 
295 |           name: `rapid-client-${i}`, 
296 |           version: '1.0.0' 
297 |         }, {});
298 | 
299 |         await client.connect(clientTransport);
300 |         
301 |         // Quick operation
302 |         const response = await client.callTool({ name: 'get_database_statistics', arguments: {} });
303 |         expect(response).toBeDefined();
304 | 
305 |         // Explicit cleanup for each iteration
306 |         await client.close();
307 |         await mcpServer.close();
308 |       }
309 |     });
310 |   });
311 | 
312 |   describe('Session Metadata', () => {
313 |     it('should track client information', async () => {
314 |       const mcpServer = new TestableN8NMCPServer();
315 |       await mcpServer.initialize();
316 |       
317 |       const [serverTransport, clientTransport] = InMemoryTransport.createLinkedPair();
318 |       await mcpServer.connectToTransport(serverTransport);
319 | 
320 |       const client = new Client({
321 |         name: 'test-client-with-metadata',
322 |         version: '2.0.0'
323 |       }, {
324 |         capabilities: {
325 |           experimental: {}
326 |         }
327 |       });
328 | 
329 |       await client.connect(clientTransport);
330 | 
331 |       // Server should be aware of client
332 |       const serverInfo = await client.getServerVersion();
333 |       expect(serverInfo).toBeDefined();
334 |       
335 |       await client.close();
336 |       await new Promise(resolve => setTimeout(resolve, 50)); // Give time for client to fully close
337 |       await mcpServer.close();
338 |     });
339 | 
340 |     it('should handle different client versions', async () => {
341 |       const mcpServer = new TestableN8NMCPServer();
342 |       await mcpServer.initialize();
343 |       
344 |       const clients = [];
345 | 
346 |       for (const version of ['1.0.0', '1.1.0', '2.0.0']) {
347 |         const [serverTransport, clientTransport] = InMemoryTransport.createLinkedPair();
348 |         await mcpServer.connectToTransport(serverTransport);
349 | 
350 |         const client = new Client({
351 |           name: 'version-test-client',
352 |           version
353 |         }, {});
354 | 
355 |         await client.connect(clientTransport);
356 |         clients.push(client);
357 |       }
358 | 
359 |       // All versions should work
360 |       const responses = await Promise.all(
361 |         clients.map(client => client.getServerVersion())
362 |       );
363 | 
364 |       responses.forEach(info => {
365 |         expect(info!.name).toBe('n8n-documentation-mcp');
366 |       });
367 |       
368 |       // Clean up
369 |       await Promise.all(clients.map(client => client.close()));
370 |       await new Promise(resolve => setTimeout(resolve, 100)); // Give time for all clients to fully close
371 |       await mcpServer.close();
372 |     });
373 |   });
374 | 
375 |   describe('Session Limits', () => {
376 |     it('should handle many sequential sessions', async () => {
377 |       const sessionCount = 20; // Reduced for faster tests
378 |       
379 |       for (let i = 0; i < sessionCount; i++) {
380 |         const mcpServer = new TestableN8NMCPServer();
381 |         await mcpServer.initialize();
382 |         
383 |         const [serverTransport, clientTransport] = InMemoryTransport.createLinkedPair();
384 |         await mcpServer.connectToTransport(serverTransport);
385 | 
386 |         const client = new Client({
387 |           name: `sequential-client-${i}`,
388 |           version: '1.0.0'
389 |         }, {});
390 | 
391 |         await client.connect(clientTransport);
392 |         
393 |         // Light operation
394 |         if (i % 10 === 0) {
395 |           await client.callTool({ name: 'get_database_statistics', arguments: {} });
396 |         }
397 | 
398 |         // Explicit cleanup
399 |         await client.close();
400 |         await mcpServer.close();
401 |       }
402 |     });
403 | 
404 |     it('should handle session with heavy usage', async () => {
405 |       const mcpServer = new TestableN8NMCPServer();
406 |       await mcpServer.initialize();
407 |       
408 |       const [serverTransport, clientTransport] = InMemoryTransport.createLinkedPair();
409 |       await mcpServer.connectToTransport(serverTransport);
410 | 
411 |       const client = new Client({
412 |         name: 'heavy-usage-client',
413 |         version: '1.0.0'
414 |       }, {});
415 | 
416 |       await client.connect(clientTransport);
417 | 
418 |       // Make many requests
419 |       const requestCount = 20; // Reduced for faster tests
420 |       const promises = [];
421 | 
422 |       for (let i = 0; i < requestCount; i++) {
423 |         const toolName = i % 2 === 0 ? 'list_nodes' : 'get_database_statistics';
424 |         const params = toolName === 'list_nodes' ? { limit: 1 } : {};
425 |         promises.push(client.callTool({ name: toolName as any, arguments: params }));
426 |       }
427 | 
428 |       const responses = await Promise.all(promises);
429 |       expect(responses).toHaveLength(requestCount);
430 |       
431 |       await client.close();
432 |       await new Promise(resolve => setTimeout(resolve, 50)); // Give time for client to fully close
433 |       await mcpServer.close();
434 |     });
435 |   });
436 | 
437 |   describe('Session Error Recovery', () => {
438 |     it('should handle errors without breaking session', async () => {
439 |       const mcpServer = new TestableN8NMCPServer();
440 |       await mcpServer.initialize();
441 |       
442 |       const [serverTransport, clientTransport] = InMemoryTransport.createLinkedPair();
443 |       await mcpServer.connectToTransport(serverTransport);
444 | 
445 |       const client = new Client({
446 |         name: 'error-recovery-client',
447 |         version: '1.0.0'
448 |       }, {});
449 | 
450 |       await client.connect(clientTransport);
451 | 
452 |       // Make an error-inducing request
453 |       try {
454 |         await client.callTool({ name: 'get_node_info', arguments: {
455 |           nodeType: 'invalid-node-type'
456 |         } });
457 |         expect.fail('Should have thrown an error');
458 |       } catch (error) {
459 |         expect(error).toBeDefined();
460 |       }
461 | 
462 |       // Session should still be active
463 |       const response = await client.callTool({ name: 'get_database_statistics', arguments: {} });
464 |       expect(response).toBeDefined();
465 |       
466 |       await client.close();
467 |       await new Promise(resolve => setTimeout(resolve, 50)); // Give time for client to fully close
468 |       await mcpServer.close();
469 |     });
470 | 
471 |     it('should handle multiple errors in sequence', async () => {
472 |       const mcpServer = new TestableN8NMCPServer();
473 |       await mcpServer.initialize();
474 |       
475 |       const [serverTransport, clientTransport] = InMemoryTransport.createLinkedPair();
476 |       await mcpServer.connectToTransport(serverTransport);
477 | 
478 |       const client = new Client({
479 |         name: 'multi-error-client',
480 |         version: '1.0.0'
481 |       }, {});
482 | 
483 |       await client.connect(clientTransport);
484 | 
485 |       // Multiple error-inducing requests
486 |       // Note: get_node_for_task was removed in v2.15.0
487 |       const errorPromises = [
488 |         client.callTool({ name: 'get_node_info', arguments: { nodeType: 'invalid1' } }).catch(e => e),
489 |         client.callTool({ name: 'get_node_info', arguments: { nodeType: 'invalid2' } }).catch(e => e),
490 |         client.callTool({ name: 'search_nodes', arguments: { query: '' } }).catch(e => e) // Empty query should error
491 |       ];
492 | 
493 |       const errors = await Promise.all(errorPromises);
494 |       errors.forEach(error => {
495 |         expect(error).toBeDefined();
496 |       });
497 | 
498 |       // Session should still work
499 |       const response = await client.callTool({ name: 'list_nodes', arguments: { limit: 1 } });
500 |       expect(response).toBeDefined();
501 |       
502 |       await client.close();
503 |       await new Promise(resolve => setTimeout(resolve, 50)); // Give time for client to fully close
504 |       await mcpServer.close();
505 |     });
506 |   });
507 | 
508 |   describe('Resource Cleanup', () => {
509 |     it('should properly close all resources on shutdown', async () => {
510 |       const testTimeout = setTimeout(() => {
511 |         console.error('Test timeout - possible deadlock in resource cleanup');
512 |         throw new Error('Test timeout after 10 seconds');
513 |       }, 10000);
514 | 
515 |       const resources = {
516 |         servers: [] as TestableN8NMCPServer[],
517 |         clients: [] as Client[],
518 |         transports: [] as any[]
519 |       };
520 | 
521 |       try {
522 |         // Create multiple servers and clients
523 |         for (let i = 0; i < 3; i++) {
524 |           const mcpServer = new TestableN8NMCPServer();
525 |           await mcpServer.initialize();
526 |           resources.servers.push(mcpServer);
527 | 
528 |           const [serverTransport, clientTransport] = InMemoryTransport.createLinkedPair();
529 |           resources.transports.push({ serverTransport, clientTransport });
530 |           
531 |           await mcpServer.connectToTransport(serverTransport);
532 | 
533 |           const client = new Client({
534 |             name: `cleanup-test-client-${i}`,
535 |             version: '1.0.0'
536 |           }, {});
537 | 
538 |           await client.connect(clientTransport);
539 |           resources.clients.push(client);
540 | 
541 |           // Make a request to ensure connection is active
542 |           await client.callTool({ name: 'get_database_statistics', arguments: {} });
543 |         }
544 | 
545 |         // Verify all resources are active
546 |         expect(resources.servers).toHaveLength(3);
547 |         expect(resources.clients).toHaveLength(3);
548 |         expect(resources.transports).toHaveLength(3);
549 | 
550 |         // Clean up all resources in proper order
551 |         // 1. Close all clients first
552 |         const clientClosePromises = resources.clients.map(async (client, index) => {
553 |           const timeout = setTimeout(() => {
554 |             console.warn(`Client ${index} close timeout`);
555 |           }, 1000);
556 |           
557 |           try {
558 |             await client.close();
559 |             clearTimeout(timeout);
560 |           } catch (error) {
561 |             clearTimeout(timeout);
562 |             console.warn(`Error closing client ${index}:`, error);
563 |           }
564 |         });
565 | 
566 |         await Promise.allSettled(clientClosePromises);
567 |         await new Promise(resolve => setTimeout(resolve, 100));
568 | 
569 |         // 2. Close all servers
570 |         const serverClosePromises = resources.servers.map(async (server, index) => {
571 |           const timeout = setTimeout(() => {
572 |             console.warn(`Server ${index} close timeout`);
573 |           }, 1000);
574 |           
575 |           try {
576 |             await server.close();
577 |             clearTimeout(timeout);
578 |           } catch (error) {
579 |             clearTimeout(timeout);
580 |             console.warn(`Error closing server ${index}:`, error);
581 |           }
582 |         });
583 | 
584 |         await Promise.allSettled(serverClosePromises);
585 | 
586 |         // 3. Verify cleanup by attempting operations (should fail)
587 |         for (let i = 0; i < resources.clients.length; i++) {
588 |           try {
589 |             await resources.clients[i].callTool({ name: 'get_database_statistics', arguments: {} });
590 |             expect.fail('Client should be closed');
591 |           } catch (error) {
592 |             // Expected - client is closed
593 |             expect(error).toBeDefined();
594 |           }
595 |         }
596 | 
597 |         // Test passed - all resources cleaned up properly
598 |         expect(true).toBe(true);
599 |       } finally {
600 |         clearTimeout(testTimeout);
601 | 
602 |         // Final cleanup attempt for any remaining resources
603 |         const finalCleanup = setTimeout(() => {
604 |           console.warn('Final cleanup timeout');
605 |         }, 2000);
606 | 
607 |         try {
608 |           await Promise.allSettled([
609 |             ...resources.clients.map(c => c.close().catch(() => {})),
610 |             ...resources.servers.map(s => s.close().catch(() => {}))
611 |           ]);
612 |           clearTimeout(finalCleanup);
613 |         } catch (error) {
614 |           clearTimeout(finalCleanup);
615 |           console.warn('Final cleanup error:', error);
616 |         }
617 |       }
618 |     });
619 |   });
620 | 
621 |   describe('Session Transport Events', () => {
622 |     it('should handle transport reconnection', async () => {
623 |       const testTimeout = setTimeout(() => {
624 |         console.error('Test timeout - possible deadlock in transport reconnection');
625 |         throw new Error('Test timeout after 10 seconds');
626 |       }, 10000);
627 | 
628 |       let mcpServer: TestableN8NMCPServer | null = null;
629 |       let client: Client | null = null;
630 |       let newClient: Client | null = null;
631 | 
632 |       try {
633 |         // Initial connection
634 |         mcpServer = new TestableN8NMCPServer();
635 |         await mcpServer.initialize();
636 |         
637 |         const [st1, ct1] = InMemoryTransport.createLinkedPair();
638 |         await mcpServer.connectToTransport(st1);
639 | 
640 |         client = new Client({
641 |           name: 'reconnect-client',
642 |           version: '1.0.0'
643 |         }, {});
644 | 
645 |         await client.connect(ct1);
646 |         
647 |         // Initial request
648 |         const response1 = await client.callTool({ name: 'get_database_statistics', arguments: {} });
649 |         expect(response1).toBeDefined();
650 | 
651 |         // Close first client
652 |         await client.close();
653 |         await new Promise(resolve => setTimeout(resolve, 100)); // Ensure full cleanup
654 | 
655 |         // New connection with same server
656 |         const [st2, ct2] = InMemoryTransport.createLinkedPair();
657 |         
658 |         const connectTimeout = setTimeout(() => {
659 |           throw new Error('Second connection timeout');
660 |         }, 3000);
661 | 
662 |         try {
663 |           await mcpServer.connectToTransport(st2);
664 |           clearTimeout(connectTimeout);
665 |         } catch (error) {
666 |           clearTimeout(connectTimeout);
667 |           throw error;
668 |         }
669 | 
670 |         newClient = new Client({
671 |           name: 'reconnect-client-2',
672 |           version: '1.0.0'
673 |         }, {});
674 | 
675 |         await newClient.connect(ct2);
676 |         
677 |         // Should work normally
678 |         const callTimeout = setTimeout(() => {
679 |           throw new Error('Second call timeout');
680 |         }, 3000);
681 | 
682 |         try {
683 |           const response2 = await newClient.callTool({ name: 'get_database_statistics', arguments: {} });
684 |           clearTimeout(callTimeout);
685 |           expect(response2).toBeDefined();
686 |         } catch (error) {
687 |           clearTimeout(callTimeout);
688 |           throw error;
689 |         }
690 |       } finally {
691 |         clearTimeout(testTimeout);
692 | 
693 |         // Cleanup with timeout protection
694 |         const cleanupTimeout = setTimeout(() => {
695 |           console.warn('Cleanup timeout - forcing exit');
696 |         }, 2000);
697 | 
698 |         try {
699 |           if (newClient) {
700 |             await newClient.close().catch(e => console.warn('Error closing new client:', e));
701 |           }
702 |           await new Promise(resolve => setTimeout(resolve, 100));
703 |           
704 |           if (mcpServer) {
705 |             await mcpServer.close().catch(e => console.warn('Error closing server:', e));
706 |           }
707 |           clearTimeout(cleanupTimeout);
708 |         } catch (error) {
709 |           clearTimeout(cleanupTimeout);
710 |           console.warn('Cleanup error:', error);
711 |         }
712 |       }
713 |     });
714 |   });
715 | });
```

--------------------------------------------------------------------------------
/src/utils/enhanced-documentation-fetcher.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { promises as fs } from 'fs';
  2 | import path from 'path';
  3 | import { logger } from './logger';
  4 | import { spawnSync } from 'child_process';
  5 | 
  6 | // Enhanced documentation structure with rich content
  7 | export interface EnhancedNodeDocumentation {
  8 |   markdown: string;
  9 |   url: string;
 10 |   title?: string;
 11 |   description?: string;
 12 |   operations?: OperationInfo[];
 13 |   apiMethods?: ApiMethodMapping[];
 14 |   examples?: CodeExample[];
 15 |   templates?: TemplateInfo[];
 16 |   relatedResources?: RelatedResource[];
 17 |   requiredScopes?: string[];
 18 |   metadata?: DocumentationMetadata;
 19 | }
 20 | 
 21 | export interface OperationInfo {
 22 |   resource: string;
 23 |   operation: string;
 24 |   description: string;
 25 |   subOperations?: string[];
 26 | }
 27 | 
 28 | export interface ApiMethodMapping {
 29 |   resource: string;
 30 |   operation: string;
 31 |   apiMethod: string;
 32 |   apiUrl: string;
 33 | }
 34 | 
 35 | export interface CodeExample {
 36 |   title?: string;
 37 |   description?: string;
 38 |   type: 'json' | 'javascript' | 'yaml' | 'text';
 39 |   code: string;
 40 |   language?: string;
 41 | }
 42 | 
 43 | export interface TemplateInfo {
 44 |   name: string;
 45 |   description?: string;
 46 |   url?: string;
 47 | }
 48 | 
 49 | export interface RelatedResource {
 50 |   title: string;
 51 |   url: string;
 52 |   type: 'documentation' | 'api' | 'tutorial' | 'external';
 53 | }
 54 | 
 55 | export interface DocumentationMetadata {
 56 |   contentType?: string[];
 57 |   priority?: string;
 58 |   tags?: string[];
 59 |   lastUpdated?: Date;
 60 | }
 61 | 
 62 | export class EnhancedDocumentationFetcher {
 63 |   private docsPath: string;
 64 |   private readonly docsRepoUrl = 'https://github.com/n8n-io/n8n-docs.git';
 65 |   private cloned = false;
 66 | 
 67 |   constructor(docsPath?: string) {
 68 |     // SECURITY: Validate and sanitize docsPath to prevent command injection
 69 |     // See: https://github.com/czlonkowski/n8n-mcp/issues/265 (CRITICAL-01 Part 2)
 70 |     const defaultPath = path.join(__dirname, '../../temp', 'n8n-docs');
 71 | 
 72 |     if (!docsPath) {
 73 |       this.docsPath = defaultPath;
 74 |     } else {
 75 |       // SECURITY: Block directory traversal and malicious paths
 76 |       const sanitized = this.sanitizePath(docsPath);
 77 | 
 78 |       if (!sanitized) {
 79 |         logger.error('Invalid docsPath rejected in constructor', { docsPath });
 80 |         throw new Error('Invalid docsPath: path contains disallowed characters or patterns');
 81 |       }
 82 | 
 83 |       // SECURITY: Verify path is absolute and within allowed boundaries
 84 |       const absolutePath = path.resolve(sanitized);
 85 | 
 86 |       // Block paths that could escape to sensitive directories
 87 |       if (absolutePath.startsWith('/etc') ||
 88 |           absolutePath.startsWith('/sys') ||
 89 |           absolutePath.startsWith('/proc') ||
 90 |           absolutePath.startsWith('/var/log')) {
 91 |         logger.error('docsPath points to system directory - blocked', { docsPath, absolutePath });
 92 |         throw new Error('Invalid docsPath: cannot use system directories');
 93 |       }
 94 | 
 95 |       this.docsPath = absolutePath;
 96 |       logger.info('docsPath validated and set', { docsPath: this.docsPath });
 97 |     }
 98 | 
 99 |     // SECURITY: Validate repository URL is HTTPS
100 |     if (!this.docsRepoUrl.startsWith('https://')) {
101 |       logger.error('docsRepoUrl must use HTTPS protocol', { url: this.docsRepoUrl });
102 |       throw new Error('Invalid repository URL: must use HTTPS protocol');
103 |     }
104 |   }
105 | 
106 |   /**
107 |    * Sanitize path input to prevent command injection and directory traversal
108 |    * SECURITY: Part of fix for command injection vulnerability
109 |    */
110 |   private sanitizePath(inputPath: string): string | null {
111 |     // SECURITY: Reject paths containing any shell metacharacters or control characters
112 |     // This prevents command injection even before attempting to sanitize
113 |     const dangerousChars = /[;&|`$(){}[\]<>'"\\#\n\r\t]/;
114 |     if (dangerousChars.test(inputPath)) {
115 |       logger.warn('Path contains shell metacharacters - rejected', { path: inputPath });
116 |       return null;
117 |     }
118 | 
119 |     // Block directory traversal attempts
120 |     if (inputPath.includes('..') || inputPath.startsWith('.')) {
121 |       logger.warn('Path traversal attempt blocked', { path: inputPath });
122 |       return null;
123 |     }
124 | 
125 |     return inputPath;
126 |   }
127 | 
128 |   /**
129 |    * Clone or update the n8n-docs repository
130 |    * SECURITY: Uses spawnSync with argument arrays to prevent command injection
131 |    * See: https://github.com/czlonkowski/n8n-mcp/issues/265 (CRITICAL-01 Part 2)
132 |    */
133 |   async ensureDocsRepository(): Promise<void> {
134 |     try {
135 |       const exists = await fs.access(this.docsPath).then(() => true).catch(() => false);
136 | 
137 |       if (!exists) {
138 |         logger.info('Cloning n8n-docs repository...', {
139 |           url: this.docsRepoUrl,
140 |           path: this.docsPath
141 |         });
142 |         await fs.mkdir(path.dirname(this.docsPath), { recursive: true });
143 | 
144 |         // SECURITY: Use spawnSync with argument array instead of string interpolation
145 |         // This prevents command injection even if docsPath or docsRepoUrl are compromised
146 |         const cloneResult = spawnSync('git', [
147 |           'clone',
148 |           '--depth', '1',
149 |           this.docsRepoUrl,
150 |           this.docsPath
151 |         ], {
152 |           stdio: 'pipe',
153 |           encoding: 'utf-8'
154 |         });
155 | 
156 |         if (cloneResult.status !== 0) {
157 |           const error = cloneResult.stderr || cloneResult.error?.message || 'Unknown error';
158 |           logger.error('Git clone failed', {
159 |             status: cloneResult.status,
160 |             stderr: error,
161 |             url: this.docsRepoUrl,
162 |             path: this.docsPath
163 |           });
164 |           throw new Error(`Git clone failed: ${error}`);
165 |         }
166 | 
167 |         logger.info('n8n-docs repository cloned successfully');
168 |       } else {
169 |         logger.info('Updating n8n-docs repository...', { path: this.docsPath });
170 | 
171 |         // SECURITY: Use spawnSync with argument array and cwd option
172 |         const pullResult = spawnSync('git', [
173 |           'pull',
174 |           '--ff-only'
175 |         ], {
176 |           cwd: this.docsPath,
177 |           stdio: 'pipe',
178 |           encoding: 'utf-8'
179 |         });
180 | 
181 |         if (pullResult.status !== 0) {
182 |           const error = pullResult.stderr || pullResult.error?.message || 'Unknown error';
183 |           logger.error('Git pull failed', {
184 |             status: pullResult.status,
185 |             stderr: error,
186 |             cwd: this.docsPath
187 |           });
188 |           throw new Error(`Git pull failed: ${error}`);
189 |         }
190 | 
191 |         logger.info('n8n-docs repository updated');
192 |       }
193 | 
194 |       this.cloned = true;
195 |     } catch (error) {
196 |       logger.error('Failed to clone/update n8n-docs repository:', error);
197 |       throw error;
198 |     }
199 |   }
200 | 
201 |   /**
202 |    * Get enhanced documentation for a specific node
203 |    */
204 |   async getEnhancedNodeDocumentation(nodeType: string): Promise<EnhancedNodeDocumentation | null> {
205 |     if (!this.cloned) {
206 |       await this.ensureDocsRepository();
207 |     }
208 | 
209 |     try {
210 |       const nodeName = this.extractNodeName(nodeType);
211 |       
212 |       // Common documentation paths to check
213 |       const possiblePaths = [
214 |         path.join(this.docsPath, 'docs', 'integrations', 'builtin', 'app-nodes', `${nodeType}.md`),
215 |         path.join(this.docsPath, 'docs', 'integrations', 'builtin', 'core-nodes', `${nodeType}.md`),
216 |         path.join(this.docsPath, 'docs', 'integrations', 'builtin', 'trigger-nodes', `${nodeType}.md`),
217 |         path.join(this.docsPath, 'docs', 'integrations', 'builtin', 'core-nodes', `${nodeName}.md`),
218 |         path.join(this.docsPath, 'docs', 'integrations', 'builtin', 'app-nodes', `${nodeName}.md`),
219 |         path.join(this.docsPath, 'docs', 'integrations', 'builtin', 'trigger-nodes', `${nodeName}.md`),
220 |       ];
221 | 
222 |       for (const docPath of possiblePaths) {
223 |         try {
224 |           const content = await fs.readFile(docPath, 'utf-8');
225 |           logger.debug(`Checking doc path: ${docPath}`);
226 |           
227 |           // Skip credential documentation files
228 |           if (this.isCredentialDoc(docPath, content)) {
229 |             logger.debug(`Skipping credential doc: ${docPath}`);
230 |             continue;
231 |           }
232 |           
233 |           logger.info(`Found documentation for ${nodeType} at: ${docPath}`);
234 |           return this.parseEnhancedDocumentation(content, docPath);
235 |         } catch (error) {
236 |           // File doesn't exist, continue
237 |           continue;
238 |         }
239 |       }
240 | 
241 |       // If no exact match, try to find by searching
242 |       logger.debug(`No exact match found, searching for ${nodeType}...`);
243 |       const foundPath = await this.searchForNodeDoc(nodeType);
244 |       if (foundPath) {
245 |         logger.info(`Found documentation via search at: ${foundPath}`);
246 |         const content = await fs.readFile(foundPath, 'utf-8');
247 |         
248 |         if (!this.isCredentialDoc(foundPath, content)) {
249 |           return this.parseEnhancedDocumentation(content, foundPath);
250 |         }
251 |       }
252 | 
253 |       logger.warn(`No documentation found for node: ${nodeType}`);
254 |       return null;
255 |     } catch (error) {
256 |       logger.error(`Failed to get documentation for ${nodeType}:`, error);
257 |       return null;
258 |     }
259 |   }
260 | 
261 |   /**
262 |    * Parse markdown content into enhanced documentation structure
263 |    */
264 |   private parseEnhancedDocumentation(markdown: string, filePath: string): EnhancedNodeDocumentation {
265 |     const doc: EnhancedNodeDocumentation = {
266 |       markdown,
267 |       url: this.generateDocUrl(filePath),
268 |     };
269 | 
270 |     // Extract frontmatter metadata
271 |     const metadata = this.extractFrontmatter(markdown);
272 |     if (metadata) {
273 |       doc.metadata = metadata;
274 |       doc.title = metadata.title;
275 |       doc.description = metadata.description;
276 |     }
277 | 
278 |     // Extract title and description from content if not in frontmatter
279 |     if (!doc.title) {
280 |       doc.title = this.extractTitle(markdown);
281 |     }
282 |     if (!doc.description) {
283 |       doc.description = this.extractDescription(markdown);
284 |     }
285 | 
286 |     // Extract operations
287 |     doc.operations = this.extractOperations(markdown);
288 | 
289 |     // Extract API method mappings
290 |     doc.apiMethods = this.extractApiMethods(markdown);
291 | 
292 |     // Extract code examples
293 |     doc.examples = this.extractCodeExamples(markdown);
294 | 
295 |     // Extract templates
296 |     doc.templates = this.extractTemplates(markdown);
297 | 
298 |     // Extract related resources
299 |     doc.relatedResources = this.extractRelatedResources(markdown);
300 | 
301 |     // Extract required scopes
302 |     doc.requiredScopes = this.extractRequiredScopes(markdown);
303 | 
304 |     return doc;
305 |   }
306 | 
307 |   /**
308 |    * Extract frontmatter metadata
309 |    */
310 |   private extractFrontmatter(markdown: string): any {
311 |     const frontmatterMatch = markdown.match(/^---\n([\s\S]*?)\n---/);
312 |     if (!frontmatterMatch) return null;
313 | 
314 |     const frontmatter: any = {};
315 |     const lines = frontmatterMatch[1].split('\n');
316 |     
317 |     for (const line of lines) {
318 |       if (line.includes(':')) {
319 |         const [key, ...valueParts] = line.split(':');
320 |         const value = valueParts.join(':').trim();
321 |         
322 |         // Parse arrays
323 |         if (value.startsWith('[') && value.endsWith(']')) {
324 |           frontmatter[key.trim()] = value
325 |             .slice(1, -1)
326 |             .split(',')
327 |             .map(v => v.trim());
328 |         } else {
329 |           frontmatter[key.trim()] = value;
330 |         }
331 |       }
332 |     }
333 | 
334 |     return frontmatter;
335 |   }
336 | 
337 |   /**
338 |    * Extract title from markdown
339 |    */
340 |   private extractTitle(markdown: string): string | undefined {
341 |     const match = markdown.match(/^#\s+(.+)$/m);
342 |     return match ? match[1].trim() : undefined;
343 |   }
344 | 
345 |   /**
346 |    * Extract description from markdown
347 |    */
348 |   private extractDescription(markdown: string): string | undefined {
349 |     // Remove frontmatter
350 |     const content = markdown.replace(/^---[\s\S]*?---\n/, '');
351 |     
352 |     // Find first paragraph after title
353 |     const lines = content.split('\n');
354 |     let foundTitle = false;
355 |     let description = '';
356 |     
357 |     for (const line of lines) {
358 |       if (line.startsWith('#')) {
359 |         foundTitle = true;
360 |         continue;
361 |       }
362 |       
363 |       if (foundTitle && line.trim() && !line.startsWith('#') && !line.startsWith('*') && !line.startsWith('-')) {
364 |         description = line.trim();
365 |         break;
366 |       }
367 |     }
368 |     
369 |     return description || undefined;
370 |   }
371 | 
372 |   /**
373 |    * Extract operations from markdown
374 |    */
375 |   private extractOperations(markdown: string): OperationInfo[] {
376 |     const operations: OperationInfo[] = [];
377 |     
378 |     // Find operations section
379 |     const operationsMatch = markdown.match(/##\s+Operations\s*\n([\s\S]*?)(?=\n##|\n#|$)/i);
380 |     if (!operationsMatch) return operations;
381 |     
382 |     const operationsText = operationsMatch[1];
383 |     
384 |     // Parse operation structure - handle nested bullet points
385 |     let currentResource: string | null = null;
386 |     const lines = operationsText.split('\n');
387 |     
388 |     for (const line of lines) {
389 |       const trimmedLine = line.trim();
390 |       
391 |       // Skip empty lines
392 |       if (!trimmedLine) continue;
393 |       
394 |       // Resource level - non-indented bullet with bold text (e.g., "* **Channel**")
395 |       if (line.match(/^\*\s+\*\*[^*]+\*\*\s*$/) && !line.match(/^\s+/)) {
396 |         const match = trimmedLine.match(/^\*\s+\*\*([^*]+)\*\*/);
397 |         if (match) {
398 |           currentResource = match[1].trim();
399 |         }
400 |         continue;
401 |       }
402 |       
403 |       // Skip if we don't have a current resource
404 |       if (!currentResource) continue;
405 |       
406 |       // Operation level - indented bullets (any whitespace + *)
407 |       if (line.match(/^\s+\*\s+/) && currentResource) {
408 |         // Extract operation name and description
409 |         const operationMatch = trimmedLine.match(/^\*\s+\*\*([^*]+)\*\*(.*)$/);
410 |         if (operationMatch) {
411 |           const operation = operationMatch[1].trim();
412 |           let description = operationMatch[2].trim();
413 |           
414 |           // Clean up description
415 |           description = description.replace(/^:\s*/, '').replace(/\.$/, '').trim();
416 |           
417 |           operations.push({
418 |             resource: currentResource,
419 |             operation,
420 |             description: description || operation,
421 |           });
422 |         } else {
423 |           // Handle operations without bold formatting or with different format
424 |           const simpleMatch = trimmedLine.match(/^\*\s+(.+)$/);
425 |           if (simpleMatch) {
426 |             const text = simpleMatch[1].trim();
427 |             // Split by colon to separate operation from description
428 |             const colonIndex = text.indexOf(':');
429 |             if (colonIndex > 0) {
430 |               operations.push({
431 |                 resource: currentResource,
432 |                 operation: text.substring(0, colonIndex).trim(),
433 |                 description: text.substring(colonIndex + 1).trim() || text,
434 |               });
435 |             } else {
436 |               operations.push({
437 |                 resource: currentResource,
438 |                 operation: text,
439 |                 description: text,
440 |               });
441 |             }
442 |           }
443 |         }
444 |       }
445 |     }
446 |     
447 |     return operations;
448 |   }
449 | 
450 |   /**
451 |    * Extract API method mappings from markdown tables
452 |    */
453 |   private extractApiMethods(markdown: string): ApiMethodMapping[] {
454 |     const apiMethods: ApiMethodMapping[] = [];
455 |     
456 |     // Find API method tables
457 |     const tableRegex = /\|.*Resource.*\|.*Operation.*\|.*(?:Slack API method|API method|Method).*\|[\s\S]*?\n(?=\n[^|]|$)/gi;
458 |     const tables = markdown.match(tableRegex);
459 |     
460 |     if (!tables) return apiMethods;
461 |     
462 |     for (const table of tables) {
463 |       const rows = table.split('\n').filter(row => row.trim() && !row.includes('---'));
464 |       
465 |       // Skip header row
466 |       for (let i = 1; i < rows.length; i++) {
467 |         const cells = rows[i].split('|').map(cell => cell.trim()).filter(Boolean);
468 |         
469 |         if (cells.length >= 3) {
470 |           const resource = cells[0];
471 |           const operation = cells[1];
472 |           const apiMethodCell = cells[2];
473 |           
474 |           // Extract API method and URL from markdown link
475 |           const linkMatch = apiMethodCell.match(/\[([^\]]+)\]\(([^)]+)\)/);
476 |           
477 |           if (linkMatch) {
478 |             apiMethods.push({
479 |               resource,
480 |               operation,
481 |               apiMethod: linkMatch[1],
482 |               apiUrl: linkMatch[2],
483 |             });
484 |           } else {
485 |             apiMethods.push({
486 |               resource,
487 |               operation,
488 |               apiMethod: apiMethodCell,
489 |               apiUrl: '',
490 |             });
491 |           }
492 |         }
493 |       }
494 |     }
495 |     
496 |     return apiMethods;
497 |   }
498 | 
499 |   /**
500 |    * Extract code examples from markdown
501 |    */
502 |   private extractCodeExamples(markdown: string): CodeExample[] {
503 |     const examples: CodeExample[] = [];
504 |     
505 |     // Extract all code blocks with language
506 |     const codeBlockRegex = /```(\w+)?\n([\s\S]*?)```/g;
507 |     let match;
508 |     
509 |     while ((match = codeBlockRegex.exec(markdown)) !== null) {
510 |       const language = match[1] || 'text';
511 |       const code = match[2].trim();
512 |       
513 |       // Look for title or description before the code block
514 |       const beforeCodeIndex = match.index;
515 |       const beforeText = markdown.substring(Math.max(0, beforeCodeIndex - 200), beforeCodeIndex);
516 |       const titleMatch = beforeText.match(/(?:###|####)\s+(.+)$/m);
517 |       
518 |       const example: CodeExample = {
519 |         type: this.mapLanguageToType(language),
520 |         language,
521 |         code,
522 |       };
523 |       
524 |       if (titleMatch) {
525 |         example.title = titleMatch[1].trim();
526 |       }
527 |       
528 |       // Try to parse JSON examples
529 |       if (language === 'json') {
530 |         try {
531 |           JSON.parse(code);
532 |           examples.push(example);
533 |         } catch (e) {
534 |           // Skip invalid JSON
535 |         }
536 |       } else {
537 |         examples.push(example);
538 |       }
539 |     }
540 |     
541 |     return examples;
542 |   }
543 | 
544 |   /**
545 |    * Extract template information
546 |    */
547 |   private extractTemplates(markdown: string): TemplateInfo[] {
548 |     const templates: TemplateInfo[] = [];
549 |     
550 |     // Look for template widget
551 |     const templateWidgetMatch = markdown.match(/\[\[\s*templatesWidget\s*\(\s*[^,]+,\s*'([^']+)'\s*\)\s*\]\]/);
552 |     if (templateWidgetMatch) {
553 |       templates.push({
554 |         name: templateWidgetMatch[1],
555 |         description: `Templates for ${templateWidgetMatch[1]}`,
556 |       });
557 |     }
558 |     
559 |     return templates;
560 |   }
561 | 
562 |   /**
563 |    * Extract related resources
564 |    */
565 |   private extractRelatedResources(markdown: string): RelatedResource[] {
566 |     const resources: RelatedResource[] = [];
567 |     
568 |     // Find related resources section
569 |     const relatedMatch = markdown.match(/##\s+(?:Related resources|Related|Resources)\s*\n([\s\S]*?)(?=\n##|\n#|$)/i);
570 |     if (!relatedMatch) return resources;
571 |     
572 |     const relatedText = relatedMatch[1];
573 |     
574 |     // Extract links
575 |     const linkRegex = /\[([^\]]+)\]\(([^)]+)\)/g;
576 |     let match;
577 |     
578 |     while ((match = linkRegex.exec(relatedText)) !== null) {
579 |       const title = match[1];
580 |       const url = match[2];
581 |       
582 |       // Determine resource type
583 |       let type: RelatedResource['type'] = 'external';
584 |       if (url.includes('docs.n8n.io') || url.startsWith('/')) {
585 |         type = 'documentation';
586 |       } else if (url.includes('api.')) {
587 |         type = 'api';
588 |       }
589 |       
590 |       resources.push({ title, url, type });
591 |     }
592 |     
593 |     return resources;
594 |   }
595 | 
596 |   /**
597 |    * Extract required scopes
598 |    */
599 |   private extractRequiredScopes(markdown: string): string[] {
600 |     const scopes: string[] = [];
601 |     
602 |     // Find required scopes section
603 |     const scopesMatch = markdown.match(/##\s+(?:Required scopes|Scopes)\s*\n([\s\S]*?)(?=\n##|\n#|$)/i);
604 |     if (!scopesMatch) return scopes;
605 |     
606 |     const scopesText = scopesMatch[1];
607 |     
608 |     // Extract scope patterns (common formats)
609 |     const scopeRegex = /`([a-z:._-]+)`/gi;
610 |     let match;
611 |     
612 |     while ((match = scopeRegex.exec(scopesText)) !== null) {
613 |       const scope = match[1];
614 |       if (scope.includes(':') || scope.includes('.')) {
615 |         scopes.push(scope);
616 |       }
617 |     }
618 |     
619 |     return [...new Set(scopes)]; // Remove duplicates
620 |   }
621 | 
622 |   /**
623 |    * Map language to code example type
624 |    */
625 |   private mapLanguageToType(language: string): CodeExample['type'] {
626 |     switch (language.toLowerCase()) {
627 |       case 'json':
628 |         return 'json';
629 |       case 'js':
630 |       case 'javascript':
631 |       case 'typescript':
632 |       case 'ts':
633 |         return 'javascript';
634 |       case 'yaml':
635 |       case 'yml':
636 |         return 'yaml';
637 |       default:
638 |         return 'text';
639 |     }
640 |   }
641 | 
642 |   /**
643 |    * Check if this is a credential documentation
644 |    */
645 |   private isCredentialDoc(filePath: string, content: string): boolean {
646 |     return filePath.includes('/credentials/') || 
647 |            (content.includes('title: ') && 
648 |             content.includes(' credentials') && 
649 |             !content.includes(' node documentation'));
650 |   }
651 | 
652 |   /**
653 |    * Extract node name from node type
654 |    */
655 |   private extractNodeName(nodeType: string): string {
656 |     const parts = nodeType.split('.');
657 |     const name = parts[parts.length - 1];
658 |     return name.toLowerCase();
659 |   }
660 | 
661 |   /**
662 |    * Search for node documentation file
663 |    * SECURITY: Uses Node.js fs APIs instead of shell commands to prevent command injection
664 |    * See: https://github.com/czlonkowski/n8n-mcp/issues/265 (CRITICAL-01)
665 |    */
666 |   private async searchForNodeDoc(nodeType: string): Promise<string | null> {
667 |     try {
668 |       // SECURITY: Sanitize input to prevent command injection and directory traversal
669 |       const sanitized = nodeType.replace(/[^a-zA-Z0-9._-]/g, '');
670 | 
671 |       if (!sanitized) {
672 |         logger.warn('Invalid nodeType after sanitization', { nodeType });
673 |         return null;
674 |       }
675 | 
676 |       // SECURITY: Block directory traversal attacks
677 |       if (sanitized.includes('..') || sanitized.startsWith('.') || sanitized.startsWith('/')) {
678 |         logger.warn('Path traversal attempt blocked', { nodeType, sanitized });
679 |         return null;
680 |       }
681 | 
682 |       // Log sanitization if it occurred
683 |       if (sanitized !== nodeType) {
684 |         logger.warn('nodeType was sanitized (potential injection attempt)', {
685 |           original: nodeType,
686 |           sanitized,
687 |         });
688 |       }
689 | 
690 |       // SECURITY: Use path.basename to strip any path components
691 |       const safeName = path.basename(sanitized);
692 |       const searchPath = path.join(this.docsPath, 'docs', 'integrations', 'builtin');
693 | 
694 |       // SECURITY: Read directory recursively using Node.js fs API (no shell execution!)
695 |       const files = await fs.readdir(searchPath, {
696 |         recursive: true,
697 |         encoding: 'utf-8'
698 |       }) as string[];
699 | 
700 |       // Try exact match first
701 |       let match = files.find(f =>
702 |         f.endsWith(`${safeName}.md`) &&
703 |         !f.includes('credentials') &&
704 |         !f.includes('trigger')
705 |       );
706 | 
707 |       if (match) {
708 |         const fullPath = path.join(searchPath, match);
709 | 
710 |         // SECURITY: Verify final path is within expected directory
711 |         if (!fullPath.startsWith(searchPath)) {
712 |           logger.error('Path traversal blocked in final path', { fullPath, searchPath });
713 |           return null;
714 |         }
715 | 
716 |         logger.info('Found documentation (exact match)', { path: fullPath });
717 |         return fullPath;
718 |       }
719 | 
720 |       // Try lowercase match
721 |       const lowerSafeName = safeName.toLowerCase();
722 |       match = files.find(f =>
723 |         f.endsWith(`${lowerSafeName}.md`) &&
724 |         !f.includes('credentials') &&
725 |         !f.includes('trigger')
726 |       );
727 | 
728 |       if (match) {
729 |         const fullPath = path.join(searchPath, match);
730 | 
731 |         // SECURITY: Verify final path is within expected directory
732 |         if (!fullPath.startsWith(searchPath)) {
733 |           logger.error('Path traversal blocked in final path', { fullPath, searchPath });
734 |           return null;
735 |         }
736 | 
737 |         logger.info('Found documentation (lowercase match)', { path: fullPath });
738 |         return fullPath;
739 |       }
740 | 
741 |       // Try partial match with node name
742 |       const nodeName = this.extractNodeName(safeName);
743 |       match = files.find(f =>
744 |         f.toLowerCase().includes(nodeName.toLowerCase()) &&
745 |         f.endsWith('.md') &&
746 |         !f.includes('credentials') &&
747 |         !f.includes('trigger')
748 |       );
749 | 
750 |       if (match) {
751 |         const fullPath = path.join(searchPath, match);
752 | 
753 |         // SECURITY: Verify final path is within expected directory
754 |         if (!fullPath.startsWith(searchPath)) {
755 |           logger.error('Path traversal blocked in final path', { fullPath, searchPath });
756 |           return null;
757 |         }
758 | 
759 |         logger.info('Found documentation (partial match)', { path: fullPath });
760 |         return fullPath;
761 |       }
762 | 
763 |       logger.debug('No documentation found', { nodeType: safeName });
764 |       return null;
765 |     } catch (error) {
766 |       logger.error('Error searching for node documentation:', {
767 |         error: error instanceof Error ? error.message : String(error),
768 |         nodeType,
769 |       });
770 |       return null;
771 |     }
772 |   }
773 | 
774 |   /**
775 |    * Generate documentation URL from file path
776 |    */
777 |   private generateDocUrl(filePath: string): string {
778 |     const relativePath = path.relative(this.docsPath, filePath);
779 |     const urlPath = relativePath
780 |       .replace(/^docs\//, '')
781 |       .replace(/\.md$/, '')
782 |       .replace(/\\/g, '/');
783 |     
784 |     return `https://docs.n8n.io/${urlPath}`;
785 |   }
786 | 
787 |   /**
788 |    * Clean up cloned repository
789 |    */
790 |   async cleanup(): Promise<void> {
791 |     try {
792 |       await fs.rm(this.docsPath, { recursive: true, force: true });
793 |       this.cloned = false;
794 |       logger.info('Cleaned up documentation repository');
795 |     } catch (error) {
796 |       logger.error('Failed to cleanup docs repository:', error);
797 |     }
798 |   }
799 | }
```

--------------------------------------------------------------------------------
/tests/unit/mcp/parameter-validation.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
  2 | import { N8NDocumentationMCPServer } from '../../../src/mcp/server';
  3 | 
  4 | // Mock the database and dependencies
  5 | vi.mock('../../../src/database/database-adapter');
  6 | vi.mock('../../../src/database/node-repository');
  7 | vi.mock('../../../src/templates/template-service');
  8 | vi.mock('../../../src/utils/logger');
  9 | 
 10 | class TestableN8NMCPServer extends N8NDocumentationMCPServer {
 11 |   // Expose the private validateToolParams method for testing
 12 |   public testValidateToolParams(toolName: string, args: any, requiredParams: string[]): void {
 13 |     return (this as any).validateToolParams(toolName, args, requiredParams);
 14 |   }
 15 | 
 16 |   // Expose the private executeTool method for testing
 17 |   public async testExecuteTool(name: string, args: any): Promise<any> {
 18 |     return (this as any).executeTool(name, args);
 19 |   }
 20 | }
 21 | 
 22 | describe('Parameter Validation', () => {
 23 |   let server: TestableN8NMCPServer;
 24 | 
 25 |   beforeEach(() => {
 26 |     // Set environment variable to use in-memory database
 27 |     process.env.NODE_DB_PATH = ':memory:';
 28 |     server = new TestableN8NMCPServer();
 29 |   });
 30 | 
 31 |   afterEach(() => {
 32 |     delete process.env.NODE_DB_PATH;
 33 |   });
 34 | 
 35 |   describe('validateToolParams', () => {
 36 |     describe('Basic Parameter Validation', () => {
 37 |       it('should pass validation when all required parameters are provided', () => {
 38 |         const args = { nodeType: 'nodes-base.httpRequest', config: {} };
 39 |         
 40 |         expect(() => {
 41 |           server.testValidateToolParams('test_tool', args, ['nodeType', 'config']);
 42 |         }).not.toThrow();
 43 |       });
 44 | 
 45 |       it('should throw error when required parameter is missing', () => {
 46 |         const args = { config: {} };
 47 |         
 48 |         expect(() => {
 49 |           server.testValidateToolParams('test_tool', args, ['nodeType', 'config']);
 50 |         }).toThrow('Missing required parameters for test_tool: nodeType');
 51 |       });
 52 | 
 53 |       it('should throw error when multiple required parameters are missing', () => {
 54 |         const args = {};
 55 |         
 56 |         expect(() => {
 57 |           server.testValidateToolParams('test_tool', args, ['nodeType', 'config', 'query']);
 58 |         }).toThrow('Missing required parameters for test_tool: nodeType, config, query');
 59 |       });
 60 | 
 61 |       it('should throw error when required parameter is undefined', () => {
 62 |         const args = { nodeType: undefined, config: {} };
 63 |         
 64 |         expect(() => {
 65 |           server.testValidateToolParams('test_tool', args, ['nodeType', 'config']);
 66 |         }).toThrow('Missing required parameters for test_tool: nodeType');
 67 |       });
 68 | 
 69 |       it('should throw error when required parameter is null', () => {
 70 |         const args = { nodeType: null, config: {} };
 71 |         
 72 |         expect(() => {
 73 |           server.testValidateToolParams('test_tool', args, ['nodeType', 'config']);
 74 |         }).toThrow('Missing required parameters for test_tool: nodeType');
 75 |       });
 76 | 
 77 |       it('should reject when required parameter is empty string (Issue #275 fix)', () => {
 78 |         const args = { query: '', limit: 10 };
 79 | 
 80 |         expect(() => {
 81 |           server.testValidateToolParams('test_tool', args, ['query']);
 82 |         }).toThrow('String parameters cannot be empty');
 83 |       });
 84 | 
 85 |       it('should pass when required parameter is zero', () => {
 86 |         const args = { limit: 0, query: 'test' };
 87 |         
 88 |         expect(() => {
 89 |           server.testValidateToolParams('test_tool', args, ['limit']);
 90 |         }).not.toThrow();
 91 |       });
 92 | 
 93 |       it('should pass when required parameter is false', () => {
 94 |         const args = { includeData: false, id: '123' };
 95 |         
 96 |         expect(() => {
 97 |           server.testValidateToolParams('test_tool', args, ['includeData']);
 98 |         }).not.toThrow();
 99 |       });
100 |     });
101 | 
102 |     describe('Edge Cases', () => {
103 |       it('should handle empty args object', () => {
104 |         expect(() => {
105 |           server.testValidateToolParams('test_tool', {}, ['param1']);
106 |         }).toThrow('Missing required parameters for test_tool: param1');
107 |       });
108 | 
109 |       it('should handle null args', () => {
110 |         expect(() => {
111 |           server.testValidateToolParams('test_tool', null, ['param1']);
112 |         }).toThrow();
113 |       });
114 | 
115 |       it('should handle undefined args', () => {
116 |         expect(() => {
117 |           server.testValidateToolParams('test_tool', undefined, ['param1']);
118 |         }).toThrow();
119 |       });
120 | 
121 |       it('should pass when no required parameters are specified', () => {
122 |         const args = { optionalParam: 'value' };
123 |         
124 |         expect(() => {
125 |           server.testValidateToolParams('test_tool', args, []);
126 |         }).not.toThrow();
127 |       });
128 | 
129 |       it('should handle special characters in parameter names', () => {
130 |         const args = { 'param-with-dash': 'value', 'param_with_underscore': 'value' };
131 |         
132 |         expect(() => {
133 |           server.testValidateToolParams('test_tool', args, ['param-with-dash', 'param_with_underscore']);
134 |         }).not.toThrow();
135 |       });
136 |     });
137 |   });
138 | 
139 |   describe('Tool-Specific Parameter Validation', () => {
140 |     // Mock the actual tool methods to avoid database calls
141 |     beforeEach(() => {
142 |       // Mock all the tool methods that would be called
143 |       vi.spyOn(server as any, 'getNodeInfo').mockResolvedValue({ mockResult: true });
144 |       vi.spyOn(server as any, 'searchNodes').mockResolvedValue({ results: [] });
145 |       vi.spyOn(server as any, 'getNodeDocumentation').mockResolvedValue({ docs: 'test' });
146 |       vi.spyOn(server as any, 'getNodeEssentials').mockResolvedValue({ essentials: true });
147 |       vi.spyOn(server as any, 'searchNodeProperties').mockResolvedValue({ properties: [] });
148 |       // Note: getNodeForTask removed in v2.15.0
149 |       vi.spyOn(server as any, 'validateNodeConfig').mockResolvedValue({ valid: true });
150 |       vi.spyOn(server as any, 'validateNodeMinimal').mockResolvedValue({ missing: [] });
151 |       vi.spyOn(server as any, 'getPropertyDependencies').mockResolvedValue({ dependencies: {} });
152 |       vi.spyOn(server as any, 'getNodeAsToolInfo').mockResolvedValue({ toolInfo: true });
153 |       vi.spyOn(server as any, 'listNodeTemplates').mockResolvedValue({ templates: [] });
154 |       vi.spyOn(server as any, 'getTemplate').mockResolvedValue({ template: {} });
155 |       vi.spyOn(server as any, 'searchTemplates').mockResolvedValue({ templates: [] });
156 |       vi.spyOn(server as any, 'getTemplatesForTask').mockResolvedValue({ templates: [] });
157 |       vi.spyOn(server as any, 'validateWorkflow').mockResolvedValue({ valid: true });
158 |       vi.spyOn(server as any, 'validateWorkflowConnections').mockResolvedValue({ valid: true });
159 |       vi.spyOn(server as any, 'validateWorkflowExpressions').mockResolvedValue({ valid: true });
160 |     });
161 | 
162 |     describe('get_node_info', () => {
163 |       it('should require nodeType parameter', async () => {
164 |         await expect(server.testExecuteTool('get_node_info', {}))
165 |           .rejects.toThrow('Missing required parameters for get_node_info: nodeType');
166 |       });
167 | 
168 |       it('should succeed with valid nodeType', async () => {
169 |         const result = await server.testExecuteTool('get_node_info', { 
170 |           nodeType: 'nodes-base.httpRequest' 
171 |         });
172 |         expect(result).toEqual({ mockResult: true });
173 |       });
174 |     });
175 | 
176 |     describe('search_nodes', () => {
177 |       it('should require query parameter', async () => {
178 |         await expect(server.testExecuteTool('search_nodes', {}))
179 |           .rejects.toThrow('search_nodes: Validation failed:\n  • query: query is required');
180 |       });
181 | 
182 |       it('should succeed with valid query', async () => {
183 |         const result = await server.testExecuteTool('search_nodes', { 
184 |           query: 'http' 
185 |         });
186 |         expect(result).toEqual({ results: [] });
187 |       });
188 | 
189 |       it('should handle optional limit parameter', async () => {
190 |         const result = await server.testExecuteTool('search_nodes', { 
191 |           query: 'http',
192 |           limit: 10
193 |         });
194 |         expect(result).toEqual({ results: [] });
195 |       });
196 | 
197 |       it('should reject invalid limit value', async () => {
198 |         await expect(server.testExecuteTool('search_nodes', { 
199 |           query: 'http',
200 |           limit: 'invalid'
201 |         })).rejects.toThrow('search_nodes: Validation failed:\n  • limit: limit must be a number, got string');
202 |       });
203 |     });
204 | 
205 |     describe('validate_node_operation', () => {
206 |       it('should require nodeType and config parameters', async () => {
207 |         await expect(server.testExecuteTool('validate_node_operation', {}))
208 |           .rejects.toThrow('validate_node_operation: Validation failed:\n  • nodeType: nodeType is required\n  • config: config is required');
209 |       });
210 | 
211 |       it('should require nodeType parameter when config is provided', async () => {
212 |         await expect(server.testExecuteTool('validate_node_operation', { config: {} }))
213 |           .rejects.toThrow('validate_node_operation: Validation failed:\n  • nodeType: nodeType is required');
214 |       });
215 | 
216 |       it('should require config parameter when nodeType is provided', async () => {
217 |         await expect(server.testExecuteTool('validate_node_operation', { nodeType: 'nodes-base.httpRequest' }))
218 |           .rejects.toThrow('validate_node_operation: Validation failed:\n  • config: config is required');
219 |       });
220 | 
221 |       it('should succeed with valid parameters', async () => {
222 |         const result = await server.testExecuteTool('validate_node_operation', { 
223 |           nodeType: 'nodes-base.httpRequest',
224 |           config: { method: 'GET', url: 'https://api.example.com' }
225 |         });
226 |         expect(result).toEqual({ valid: true });
227 |       });
228 |     });
229 | 
230 |     describe('search_node_properties', () => {
231 |       it('should require nodeType and query parameters', async () => {
232 |         await expect(server.testExecuteTool('search_node_properties', {}))
233 |           .rejects.toThrow('Missing required parameters for search_node_properties: nodeType, query');
234 |       });
235 | 
236 |       it('should succeed with valid parameters', async () => {
237 |         const result = await server.testExecuteTool('search_node_properties', { 
238 |           nodeType: 'nodes-base.httpRequest',
239 |           query: 'auth'
240 |         });
241 |         expect(result).toEqual({ properties: [] });
242 |       });
243 | 
244 |       it('should handle optional maxResults parameter', async () => {
245 |         const result = await server.testExecuteTool('search_node_properties', { 
246 |           nodeType: 'nodes-base.httpRequest',
247 |           query: 'auth',
248 |           maxResults: 5
249 |         });
250 |         expect(result).toEqual({ properties: [] });
251 |       });
252 |     });
253 | 
254 |     describe('list_node_templates', () => {
255 |       it('should require nodeTypes parameter', async () => {
256 |         await expect(server.testExecuteTool('list_node_templates', {}))
257 |           .rejects.toThrow('list_node_templates: Validation failed:\n  • nodeTypes: nodeTypes is required');
258 |       });
259 | 
260 |       it('should succeed with valid nodeTypes array', async () => {
261 |         const result = await server.testExecuteTool('list_node_templates', { 
262 |           nodeTypes: ['nodes-base.httpRequest', 'nodes-base.slack']
263 |         });
264 |         expect(result).toEqual({ templates: [] });
265 |       });
266 |     });
267 | 
268 |     describe('get_template', () => {
269 |       it('should require templateId parameter', async () => {
270 |         await expect(server.testExecuteTool('get_template', {}))
271 |           .rejects.toThrow('Missing required parameters for get_template: templateId');
272 |       });
273 | 
274 |       it('should succeed with valid templateId', async () => {
275 |         const result = await server.testExecuteTool('get_template', { 
276 |           templateId: 123
277 |         });
278 |         expect(result).toEqual({ template: {} });
279 |       });
280 |     });
281 |   });
282 | 
283 |   describe('Numeric Parameter Conversion', () => {
284 |     beforeEach(() => {
285 |       vi.spyOn(server as any, 'searchNodes').mockResolvedValue({ results: [] });
286 |       vi.spyOn(server as any, 'searchNodeProperties').mockResolvedValue({ properties: [] });
287 |       vi.spyOn(server as any, 'listNodeTemplates').mockResolvedValue({ templates: [] });
288 |       vi.spyOn(server as any, 'getTemplate').mockResolvedValue({ template: {} });
289 |     });
290 | 
291 |     describe('limit parameter conversion', () => {
292 |       it('should reject string limit values', async () => {
293 |         await expect(server.testExecuteTool('search_nodes', { 
294 |           query: 'test',
295 |           limit: '15'
296 |         })).rejects.toThrow('search_nodes: Validation failed:\n  • limit: limit must be a number, got string');
297 |       });
298 | 
299 |       it('should reject invalid string limit values', async () => {
300 |         await expect(server.testExecuteTool('search_nodes', { 
301 |           query: 'test',
302 |           limit: 'invalid'
303 |         })).rejects.toThrow('search_nodes: Validation failed:\n  • limit: limit must be a number, got string');
304 |       });
305 | 
306 |       it('should use default when limit is undefined', async () => {
307 |         const mockSearchNodes = vi.spyOn(server as any, 'searchNodes');
308 |         
309 |         await server.testExecuteTool('search_nodes', { 
310 |           query: 'test'
311 |         });
312 | 
313 |         expect(mockSearchNodes).toHaveBeenCalledWith('test', 20, { mode: undefined });
314 |       });
315 | 
316 |       it('should reject zero as limit due to minimum constraint', async () => {
317 |         await expect(server.testExecuteTool('search_nodes', { 
318 |           query: 'test',
319 |           limit: 0
320 |         })).rejects.toThrow('search_nodes: Validation failed:\n  • limit: limit must be at least 1, got 0');
321 |       });
322 |     });
323 | 
324 |     describe('maxResults parameter conversion', () => {
325 |       it('should convert string numbers to numbers', async () => {
326 |         const mockSearchNodeProperties = vi.spyOn(server as any, 'searchNodeProperties');
327 |         
328 |         await server.testExecuteTool('search_node_properties', { 
329 |           nodeType: 'nodes-base.httpRequest',
330 |           query: 'auth',
331 |           maxResults: '5'
332 |         });
333 | 
334 |         expect(mockSearchNodeProperties).toHaveBeenCalledWith('nodes-base.httpRequest', 'auth', 5);
335 |       });
336 | 
337 |       it('should use default when maxResults is invalid', async () => {
338 |         const mockSearchNodeProperties = vi.spyOn(server as any, 'searchNodeProperties');
339 |         
340 |         await server.testExecuteTool('search_node_properties', { 
341 |           nodeType: 'nodes-base.httpRequest',
342 |           query: 'auth',
343 |           maxResults: 'invalid'
344 |         });
345 | 
346 |         expect(mockSearchNodeProperties).toHaveBeenCalledWith('nodes-base.httpRequest', 'auth', 20);
347 |       });
348 |     });
349 | 
350 |     describe('templateLimit parameter conversion', () => {
351 |       it('should reject string limit values', async () => {
352 |         await expect(server.testExecuteTool('list_node_templates', { 
353 |           nodeTypes: ['nodes-base.httpRequest'],
354 |           limit: '5'
355 |         })).rejects.toThrow('list_node_templates: Validation failed:\n  • limit: limit must be a number, got string');
356 |       });
357 | 
358 |       it('should reject invalid string limit values', async () => {
359 |         await expect(server.testExecuteTool('list_node_templates', { 
360 |           nodeTypes: ['nodes-base.httpRequest'],
361 |           limit: 'invalid'
362 |         })).rejects.toThrow('list_node_templates: Validation failed:\n  • limit: limit must be a number, got string');
363 |       });
364 |     });
365 | 
366 |     describe('templateId parameter handling', () => {
367 |       it('should pass through numeric templateId', async () => {
368 |         const mockGetTemplate = vi.spyOn(server as any, 'getTemplate');
369 |         
370 |         await server.testExecuteTool('get_template', { 
371 |           templateId: 123
372 |         });
373 | 
374 |         expect(mockGetTemplate).toHaveBeenCalledWith(123, 'full');
375 |       });
376 | 
377 |       it('should convert string templateId to number', async () => {
378 |         const mockGetTemplate = vi.spyOn(server as any, 'getTemplate');
379 |         
380 |         await server.testExecuteTool('get_template', { 
381 |           templateId: '123'
382 |         });
383 | 
384 |         expect(mockGetTemplate).toHaveBeenCalledWith(123, 'full');
385 |       });
386 |     });
387 |   });
388 | 
389 |   describe('Tools with No Required Parameters', () => {
390 |     beforeEach(() => {
391 |       vi.spyOn(server as any, 'getToolsDocumentation').mockResolvedValue({ docs: 'test' });
392 |       vi.spyOn(server as any, 'listNodes').mockResolvedValue({ nodes: [] });
393 |       vi.spyOn(server as any, 'listAITools').mockResolvedValue({ tools: [] });
394 |       vi.spyOn(server as any, 'getDatabaseStatistics').mockResolvedValue({ stats: {} });
395 |       vi.spyOn(server as any, 'listTasks').mockResolvedValue({ tasks: [] });
396 |     });
397 | 
398 |     it('should allow tools_documentation with no parameters', async () => {
399 |       const result = await server.testExecuteTool('tools_documentation', {});
400 |       expect(result).toEqual({ docs: 'test' });
401 |     });
402 | 
403 |     it('should allow list_nodes with no parameters', async () => {
404 |       const result = await server.testExecuteTool('list_nodes', {});
405 |       expect(result).toEqual({ nodes: [] });
406 |     });
407 | 
408 |     it('should allow list_ai_tools with no parameters', async () => {
409 |       const result = await server.testExecuteTool('list_ai_tools', {});
410 |       expect(result).toEqual({ tools: [] });
411 |     });
412 | 
413 |     it('should allow get_database_statistics with no parameters', async () => {
414 |       const result = await server.testExecuteTool('get_database_statistics', {});
415 |       expect(result).toEqual({ stats: {} });
416 |     });
417 | 
418 |     it('should allow list_tasks with no parameters', async () => {
419 |       const result = await server.testExecuteTool('list_tasks', {});
420 |       expect(result).toEqual({ tasks: [] });
421 |     });
422 |   });
423 | 
424 |   describe('Error Message Quality', () => {
425 |     it('should provide clear error messages with tool name', () => {
426 |       expect(() => {
427 |         server.testValidateToolParams('get_node_info', {}, ['nodeType']);
428 |       }).toThrow('Missing required parameters for get_node_info: nodeType. Please provide the required parameters to use this tool.');
429 |     });
430 | 
431 |     it('should list all missing parameters', () => {
432 |       expect(() => {
433 |         server.testValidateToolParams('validate_node_operation', { profile: 'strict' }, ['nodeType', 'config']);
434 |       }).toThrow('validate_node_operation: Validation failed:\n  • nodeType: nodeType is required\n  • config: config is required');
435 |     });
436 | 
437 |     it('should include helpful guidance', () => {
438 |       try {
439 |         server.testValidateToolParams('test_tool', {}, ['param1', 'param2']);
440 |       } catch (error: any) {
441 |         expect(error.message).toContain('Please provide the required parameters to use this tool');
442 |       }
443 |     });
444 |   });
445 | 
446 |   describe('MCP Error Response Handling', () => {
447 |     it('should convert validation errors to MCP error responses rather than throwing exceptions', async () => {
448 |       // This test simulates what happens at the MCP level when a tool validation fails
449 |       // The server should catch the validation error and return it as an MCP error response
450 |       
451 |       // Directly test the executeTool method to ensure it throws appropriately
452 |       // The MCP server's request handler should catch these and convert to error responses
453 |       await expect(server.testExecuteTool('get_node_info', {}))
454 |         .rejects.toThrow('Missing required parameters for get_node_info: nodeType');
455 |       
456 |       await expect(server.testExecuteTool('search_nodes', {}))
457 |         .rejects.toThrow('search_nodes: Validation failed:\n  • query: query is required');
458 |       
459 |       await expect(server.testExecuteTool('validate_node_operation', { nodeType: 'test' }))
460 |         .rejects.toThrow('validate_node_operation: Validation failed:\n  • config: config is required');
461 |     });
462 | 
463 |     it('should handle edge cases in parameter validation gracefully', async () => {
464 |       // Test with null args (should be handled by args = args || {})
465 |       await expect(server.testExecuteTool('get_node_info', null))
466 |         .rejects.toThrow('Missing required parameters');
467 |       
468 |       // Test with undefined args
469 |       await expect(server.testExecuteTool('get_node_info', undefined))
470 |         .rejects.toThrow('Missing required parameters');
471 |     });
472 | 
473 |     it('should provide consistent error format across all tools', async () => {
474 |       // Tools using legacy validation
475 |       const legacyValidationTools = [
476 |         { name: 'get_node_info', args: {}, expected: 'Missing required parameters for get_node_info: nodeType' },
477 |         { name: 'get_node_documentation', args: {}, expected: 'Missing required parameters for get_node_documentation: nodeType' },
478 |         { name: 'get_node_essentials', args: {}, expected: 'Missing required parameters for get_node_essentials: nodeType' },
479 |         { name: 'search_node_properties', args: {}, expected: 'Missing required parameters for search_node_properties: nodeType, query' },
480 |         // Note: get_node_for_task removed in v2.15.0
481 |         { name: 'get_property_dependencies', args: {}, expected: 'Missing required parameters for get_property_dependencies: nodeType' },
482 |         { name: 'get_node_as_tool_info', args: {}, expected: 'Missing required parameters for get_node_as_tool_info: nodeType' },
483 |         { name: 'get_template', args: {}, expected: 'Missing required parameters for get_template: templateId' },
484 |       ];
485 | 
486 |       for (const tool of legacyValidationTools) {
487 |         await expect(server.testExecuteTool(tool.name, tool.args))
488 |           .rejects.toThrow(tool.expected);
489 |       }
490 | 
491 |       // Tools using new schema validation
492 |       const schemaValidationTools = [
493 |         { name: 'search_nodes', args: {}, expected: 'search_nodes: Validation failed:\n  • query: query is required' },
494 |         { name: 'validate_node_operation', args: {}, expected: 'validate_node_operation: Validation failed:\n  • nodeType: nodeType is required\n  • config: config is required' },
495 |         { name: 'validate_node_minimal', args: {}, expected: 'validate_node_minimal: Validation failed:\n  • nodeType: nodeType is required\n  • config: config is required' },
496 |         { name: 'list_node_templates', args: {}, expected: 'list_node_templates: Validation failed:\n  • nodeTypes: nodeTypes is required' },
497 |       ];
498 | 
499 |       for (const tool of schemaValidationTools) {
500 |         await expect(server.testExecuteTool(tool.name, tool.args))
501 |           .rejects.toThrow(tool.expected);
502 |       }
503 |     });
504 | 
505 |     it('should validate n8n management tools parameters', async () => {
506 |       // Mock the n8n handlers to avoid actual API calls
507 |       const mockHandlers = [
508 |         'handleCreateWorkflow',
509 |         'handleGetWorkflow', 
510 |         'handleGetWorkflowDetails',
511 |         'handleGetWorkflowStructure',
512 |         'handleGetWorkflowMinimal',
513 |         'handleUpdateWorkflow',
514 |         'handleDeleteWorkflow',
515 |         'handleValidateWorkflow',
516 |         'handleTriggerWebhookWorkflow',
517 |         'handleGetExecution',
518 |         'handleDeleteExecution'
519 |       ];
520 | 
521 |       for (const handler of mockHandlers) {
522 |         vi.doMock('../../../src/mcp/handlers-n8n-manager', () => ({
523 |           [handler]: vi.fn().mockResolvedValue({ success: true })
524 |         }));
525 |       }
526 | 
527 |       vi.doMock('../../../src/mcp/handlers-workflow-diff', () => ({
528 |         handleUpdatePartialWorkflow: vi.fn().mockResolvedValue({ success: true })
529 |       }));
530 | 
531 |       const n8nToolsWithRequiredParams = [
532 |         { name: 'n8n_create_workflow', args: {}, expected: 'n8n_create_workflow: Validation failed:\n  • name: name is required\n  • nodes: nodes is required\n  • connections: connections is required' },
533 |         { name: 'n8n_get_workflow', args: {}, expected: 'n8n_get_workflow: Validation failed:\n  • id: id is required' },
534 |         { name: 'n8n_get_workflow_details', args: {}, expected: 'n8n_get_workflow_details: Validation failed:\n  • id: id is required' },
535 |         { name: 'n8n_get_workflow_structure', args: {}, expected: 'n8n_get_workflow_structure: Validation failed:\n  • id: id is required' },
536 |         { name: 'n8n_get_workflow_minimal', args: {}, expected: 'n8n_get_workflow_minimal: Validation failed:\n  • id: id is required' },
537 |         { name: 'n8n_update_full_workflow', args: {}, expected: 'n8n_update_full_workflow: Validation failed:\n  • id: id is required' },
538 |         { name: 'n8n_delete_workflow', args: {}, expected: 'n8n_delete_workflow: Validation failed:\n  • id: id is required' },
539 |         { name: 'n8n_validate_workflow', args: {}, expected: 'n8n_validate_workflow: Validation failed:\n  • id: id is required' },
540 |         { name: 'n8n_get_execution', args: {}, expected: 'n8n_get_execution: Validation failed:\n  • id: id is required' },
541 |         { name: 'n8n_delete_execution', args: {}, expected: 'n8n_delete_execution: Validation failed:\n  • id: id is required' },
542 |       ];
543 | 
544 |       // n8n_update_partial_workflow and n8n_trigger_webhook_workflow use legacy validation
545 |       await expect(server.testExecuteTool('n8n_update_partial_workflow', {}))
546 |         .rejects.toThrow('Missing required parameters for n8n_update_partial_workflow: id, operations');
547 |       
548 |       await expect(server.testExecuteTool('n8n_trigger_webhook_workflow', {}))
549 |         .rejects.toThrow('Missing required parameters for n8n_trigger_webhook_workflow: webhookUrl');
550 | 
551 |       for (const tool of n8nToolsWithRequiredParams) {
552 |         await expect(server.testExecuteTool(tool.name, tool.args))
553 |           .rejects.toThrow(tool.expected);
554 |       }
555 |     });
556 |   });
557 | });
```

--------------------------------------------------------------------------------
/tests/unit/http-server/multi-tenant-support.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Comprehensive unit tests for multi-tenant support in http-server-single-session.ts
  3 |  *
  4 |  * Tests the new functions and logic:
  5 |  * - extractMultiTenantHeaders function
  6 |  * - Instance context creation and validation from headers
  7 |  * - Session ID generation with configuration hash
  8 |  * - Context switching with locking mechanism
  9 |  * - Security logging with sanitization
 10 |  */
 11 | 
 12 | import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
 13 | import express from 'express';
 14 | import { InstanceContext } from '../../../src/types/instance-context';
 15 | 
 16 | // Mock dependencies
 17 | vi.mock('../../../src/utils/logger', () => ({
 18 |   Logger: vi.fn().mockImplementation(() => ({
 19 |     debug: vi.fn(),
 20 |     info: vi.fn(),
 21 |     warn: vi.fn(),
 22 |     error: vi.fn()
 23 |   })),
 24 |   logger: {
 25 |     debug: vi.fn(),
 26 |     info: vi.fn(),
 27 |     warn: vi.fn(),
 28 |     error: vi.fn()
 29 |   }
 30 | }));
 31 | 
 32 | vi.mock('../../../src/utils/console-manager', () => ({
 33 |   ConsoleManager: {
 34 |     getInstance: vi.fn().mockReturnValue({
 35 |       isolate: vi.fn((fn) => fn())
 36 |     })
 37 |   }
 38 | }));
 39 | 
 40 | vi.mock('../../../src/mcp/server', () => ({
 41 |   N8NDocumentationMCPServer: vi.fn().mockImplementation(() => ({
 42 |     setInstanceContext: vi.fn(),
 43 |     handleMessage: vi.fn(),
 44 |     close: vi.fn()
 45 |   }))
 46 | }));
 47 | 
 48 | vi.mock('uuid', () => ({
 49 |   v4: vi.fn(() => 'test-uuid-1234-5678-9012')
 50 | }));
 51 | 
 52 | vi.mock('crypto', () => ({
 53 |   createHash: vi.fn(() => ({
 54 |     update: vi.fn().mockReturnThis(),
 55 |     digest: vi.fn(() => 'test-hash-abc123')
 56 |   }))
 57 | }));
 58 | 
 59 | // Since the functions are not exported, we'll test them through the HTTP server behavior
 60 | describe('HTTP Server Multi-Tenant Support', () => {
 61 |   let mockRequest: Partial<express.Request>;
 62 |   let mockResponse: Partial<express.Response>;
 63 |   let originalEnv: NodeJS.ProcessEnv;
 64 | 
 65 |   beforeEach(() => {
 66 |     originalEnv = { ...process.env };
 67 | 
 68 |     mockRequest = {
 69 |       headers: {},
 70 |       method: 'POST',
 71 |       url: '/mcp',
 72 |       body: {}
 73 |     };
 74 | 
 75 |     mockResponse = {
 76 |       status: vi.fn().mockReturnThis(),
 77 |       json: vi.fn().mockReturnThis(),
 78 |       send: vi.fn().mockReturnThis(),
 79 |       setHeader: vi.fn().mockReturnThis(),
 80 |       writeHead: vi.fn(),
 81 |       write: vi.fn(),
 82 |       end: vi.fn()
 83 |     };
 84 | 
 85 |     vi.clearAllMocks();
 86 |   });
 87 | 
 88 |   afterEach(() => {
 89 |     process.env = originalEnv;
 90 |   });
 91 | 
 92 |   describe('extractMultiTenantHeaders Function', () => {
 93 |     // Since extractMultiTenantHeaders is not exported, we'll test its behavior indirectly
 94 |     // by examining how the HTTP server processes headers
 95 | 
 96 |     it('should extract all multi-tenant headers when present', () => {
 97 |       // Arrange
 98 |       const headers: any = {
 99 |         'x-n8n-url': 'https://tenant1.n8n.cloud',
100 |         'x-n8n-key': 'tenant1-api-key',
101 |         'x-instance-id': 'tenant1-instance',
102 |         'x-session-id': 'tenant1-session-123'
103 |       };
104 | 
105 |       mockRequest.headers = headers;
106 | 
107 |       // The function would extract these headers in a type-safe manner
108 |       // We can verify this behavior by checking if the server processes them correctly
109 | 
110 |       // Assert that headers are properly typed and extracted
111 |       expect(headers['x-n8n-url']).toBe('https://tenant1.n8n.cloud');
112 |       expect(headers['x-n8n-key']).toBe('tenant1-api-key');
113 |       expect(headers['x-instance-id']).toBe('tenant1-instance');
114 |       expect(headers['x-session-id']).toBe('tenant1-session-123');
115 |     });
116 | 
117 |     it('should handle missing headers gracefully', () => {
118 |       // Arrange
119 |       const headers: any = {
120 |         'x-n8n-url': 'https://tenant1.n8n.cloud'
121 |         // Other headers missing
122 |       };
123 | 
124 |       mockRequest.headers = headers;
125 | 
126 |       // Extract function should handle undefined values
127 |       expect(headers['x-n8n-url']).toBe('https://tenant1.n8n.cloud');
128 |       expect(headers['x-n8n-key']).toBeUndefined();
129 |       expect(headers['x-instance-id']).toBeUndefined();
130 |       expect(headers['x-session-id']).toBeUndefined();
131 |     });
132 | 
133 |     it('should handle case-insensitive headers', () => {
134 |       // Arrange
135 |       const headers: any = {
136 |         'X-N8N-URL': 'https://tenant1.n8n.cloud',
137 |         'X-N8N-KEY': 'tenant1-api-key',
138 |         'X-INSTANCE-ID': 'tenant1-instance',
139 |         'X-SESSION-ID': 'tenant1-session-123'
140 |       };
141 | 
142 |       mockRequest.headers = headers;
143 | 
144 |       // Express normalizes headers to lowercase
145 |       expect(headers['X-N8N-URL']).toBe('https://tenant1.n8n.cloud');
146 |     });
147 | 
148 |     it('should handle array header values', () => {
149 |       // Arrange - Express can provide headers as arrays
150 |       const headers: any = {
151 |         'x-n8n-url': ['https://tenant1.n8n.cloud'],
152 |         'x-n8n-key': ['tenant1-api-key', 'duplicate-key'] // Multiple values
153 |       };
154 | 
155 |       mockRequest.headers = headers as any;
156 | 
157 |       // Function should handle array values appropriately
158 |       expect(Array.isArray(headers['x-n8n-url'])).toBe(true);
159 |       expect(Array.isArray(headers['x-n8n-key'])).toBe(true);
160 |     });
161 | 
162 |     it('should handle non-string header values', () => {
163 |       // Arrange
164 |       const headers: any = {
165 |         'x-n8n-url': undefined,
166 |         'x-n8n-key': null,
167 |         'x-instance-id': 123,  // Should be string
168 |         'x-session-id': ['value1', 'value2']
169 |       };
170 | 
171 |       mockRequest.headers = headers as any;
172 | 
173 |       // Function should handle type safety
174 |       expect(typeof headers['x-instance-id']).toBe('number');
175 |       expect(Array.isArray(headers['x-session-id'])).toBe(true);
176 |     });
177 |   });
178 | 
179 |   describe('Instance Context Creation and Validation', () => {
180 |     it('should create valid instance context from complete headers', () => {
181 |       // Arrange
182 |       const headers: any = {
183 |         'x-n8n-url': 'https://tenant1.n8n.cloud',
184 |         'x-n8n-key': 'valid-api-key-123',
185 |         'x-instance-id': 'tenant1-instance',
186 |         'x-session-id': 'tenant1-session-123'
187 |       };
188 | 
189 |       // Simulate instance context creation
190 |       const instanceContext: InstanceContext = {
191 |         n8nApiUrl: headers['x-n8n-url'],
192 |         n8nApiKey: headers['x-n8n-key'],
193 |         instanceId: headers['x-instance-id'],
194 |         sessionId: headers['x-session-id']
195 |       };
196 | 
197 |       // Assert valid context
198 |       expect(instanceContext.n8nApiUrl).toBe('https://tenant1.n8n.cloud');
199 |       expect(instanceContext.n8nApiKey).toBe('valid-api-key-123');
200 |       expect(instanceContext.instanceId).toBe('tenant1-instance');
201 |       expect(instanceContext.sessionId).toBe('tenant1-session-123');
202 |     });
203 | 
204 |     it('should create partial instance context when some headers missing', () => {
205 |       // Arrange
206 |       const headers: any = {
207 |         'x-n8n-url': 'https://tenant1.n8n.cloud'
208 |         // Other headers missing
209 |       };
210 | 
211 |       // Simulate partial context creation
212 |       const instanceContext: InstanceContext = {
213 |         n8nApiUrl: headers['x-n8n-url'],
214 |         n8nApiKey: headers['x-n8n-key'], // undefined
215 |         instanceId: headers['x-instance-id'], // undefined
216 |         sessionId: headers['x-session-id'] // undefined
217 |       };
218 | 
219 |       // Assert partial context
220 |       expect(instanceContext.n8nApiUrl).toBe('https://tenant1.n8n.cloud');
221 |       expect(instanceContext.n8nApiKey).toBeUndefined();
222 |       expect(instanceContext.instanceId).toBeUndefined();
223 |       expect(instanceContext.sessionId).toBeUndefined();
224 |     });
225 | 
226 |     it('should return undefined context when no relevant headers present', () => {
227 |       // Arrange
228 |       const headers: any = {
229 |         'authorization': 'Bearer token',
230 |         'content-type': 'application/json'
231 |         // No x-n8n-* headers
232 |       };
233 | 
234 |       // Simulate context creation logic
235 |       const hasUrl = headers['x-n8n-url'];
236 |       const hasKey = headers['x-n8n-key'];
237 |       const instanceContext = (!hasUrl && !hasKey) ? undefined : {};
238 | 
239 |       // Assert no context created
240 |       expect(instanceContext).toBeUndefined();
241 |     });
242 | 
243 |     it.skip('should validate instance context before use', () => {
244 |       // TODO: Fix import issue with validateInstanceContext
245 |       // Arrange
246 |       const invalidContext: InstanceContext = {
247 |         n8nApiUrl: 'invalid-url',
248 |         n8nApiKey: 'placeholder'
249 |       };
250 | 
251 |       // Import validation function to test
252 |       const { validateInstanceContext } = require('../../../src/types/instance-context');
253 | 
254 |       // Act
255 |       const result = validateInstanceContext(invalidContext);
256 | 
257 |       // Assert
258 |       expect(result.valid).toBe(false);
259 |       expect(result.errors).toBeDefined();
260 |       expect(result.errors?.length).toBeGreaterThan(0);
261 |     });
262 | 
263 |     it('should handle malformed URLs in headers', () => {
264 |       // Arrange
265 |       const headers: any = {
266 |         'x-n8n-url': 'not-a-valid-url',
267 |         'x-n8n-key': 'valid-key'
268 |       };
269 | 
270 |       const instanceContext: InstanceContext = {
271 |         n8nApiUrl: headers['x-n8n-url'],
272 |         n8nApiKey: headers['x-n8n-key']
273 |       };
274 | 
275 |       // Should not throw during creation
276 |       expect(() => instanceContext).not.toThrow();
277 |       expect(instanceContext.n8nApiUrl).toBe('not-a-valid-url');
278 |     });
279 | 
280 |     it('should handle special characters in headers', () => {
281 |       // Arrange
282 |       const headers: any = {
283 |         'x-n8n-url': 'https://[email protected]',
284 |         'x-n8n-key': 'key-with-special-chars!@#$%',
285 |         'x-instance-id': 'instance_with_underscores',
286 |         'x-session-id': 'session-with-hyphens-123'
287 |       };
288 | 
289 |       const instanceContext: InstanceContext = {
290 |         n8nApiUrl: headers['x-n8n-url'],
291 |         n8nApiKey: headers['x-n8n-key'],
292 |         instanceId: headers['x-instance-id'],
293 |         sessionId: headers['x-session-id']
294 |       };
295 | 
296 |       // Should handle special characters
297 |       expect(instanceContext.n8nApiUrl).toContain('@');
298 |       expect(instanceContext.n8nApiKey).toContain('!@#$%');
299 |       expect(instanceContext.instanceId).toContain('_');
300 |       expect(instanceContext.sessionId).toContain('-');
301 |     });
302 |   });
303 | 
304 |   describe('Session ID Generation with Configuration Hash', () => {
305 |     it.skip('should generate consistent session ID for same configuration', () => {
306 |       // TODO: Fix vi.mocked() issue
307 |       // Arrange
308 |       const crypto = require('crypto');
309 |       const uuid = require('uuid');
310 | 
311 |       const config1 = {
312 |         n8nApiUrl: 'https://tenant1.n8n.cloud',
313 |         n8nApiKey: 'api-key-123'
314 |       };
315 | 
316 |       const config2 = {
317 |         n8nApiUrl: 'https://tenant1.n8n.cloud',
318 |         n8nApiKey: 'api-key-123'
319 |       };
320 | 
321 |       // Mock hash generation to be deterministic
322 |       const mockHash = vi.mocked(crypto.createHash).mockReturnValue({
323 |         update: vi.fn().mockReturnThis(),
324 |         digest: vi.fn(() => 'same-hash-for-same-config')
325 |       });
326 | 
327 |       // Generate session IDs
328 |       const sessionId1 = `test-uuid-1234-5678-9012-same-hash-for-same-config`;
329 |       const sessionId2 = `test-uuid-1234-5678-9012-same-hash-for-same-config`;
330 | 
331 |       // Assert same session IDs for same config
332 |       expect(sessionId1).toBe(sessionId2);
333 |       expect(mockHash).toHaveBeenCalled();
334 |     });
335 | 
336 |     it.skip('should generate different session ID for different configuration', () => {
337 |       // TODO: Fix vi.mocked() issue
338 |       // Arrange
339 |       const crypto = require('crypto');
340 | 
341 |       const config1 = {
342 |         n8nApiUrl: 'https://tenant1.n8n.cloud',
343 |         n8nApiKey: 'api-key-123'
344 |       };
345 | 
346 |       const config2 = {
347 |         n8nApiUrl: 'https://tenant2.n8n.cloud',
348 |         n8nApiKey: 'different-api-key'
349 |       };
350 | 
351 |       // Mock different hashes for different configs
352 |       let callCount = 0;
353 |       const mockHash = vi.mocked(crypto.createHash).mockReturnValue({
354 |         update: vi.fn().mockReturnThis(),
355 |         digest: vi.fn(() => callCount++ === 0 ? 'hash-config-1' : 'hash-config-2')
356 |       });
357 | 
358 |       // Generate session IDs
359 |       const sessionId1 = `test-uuid-1234-5678-9012-hash-config-1`;
360 |       const sessionId2 = `test-uuid-1234-5678-9012-hash-config-2`;
361 | 
362 |       // Assert different session IDs for different configs
363 |       expect(sessionId1).not.toBe(sessionId2);
364 |       expect(sessionId1).toContain('hash-config-1');
365 |       expect(sessionId2).toContain('hash-config-2');
366 |     });
367 | 
368 |     it.skip('should include UUID in session ID for uniqueness', () => {
369 |       // TODO: Fix vi.mocked() issue
370 |       // Arrange
371 |       const uuid = require('uuid');
372 |       const crypto = require('crypto');
373 | 
374 |       vi.mocked(uuid.v4).mockReturnValue('unique-uuid-abcd-efgh');
375 |       vi.mocked(crypto.createHash).mockReturnValue({
376 |         update: vi.fn().mockReturnThis(),
377 |         digest: vi.fn(() => 'config-hash')
378 |       });
379 | 
380 |       // Generate session ID
381 |       const sessionId = `unique-uuid-abcd-efgh-config-hash`;
382 | 
383 |       // Assert UUID is included
384 |       expect(sessionId).toContain('unique-uuid-abcd-efgh');
385 |       expect(sessionId).toContain('config-hash');
386 |     });
387 | 
388 |     it.skip('should handle undefined configuration in hash generation', () => {
389 |       // TODO: Fix vi.mocked() issue
390 |       // Arrange
391 |       const crypto = require('crypto');
392 | 
393 |       const config = {
394 |         n8nApiUrl: undefined,
395 |         n8nApiKey: undefined
396 |       };
397 | 
398 |       // Mock hash for undefined config
399 |       const mockHashInstance = {
400 |         update: vi.fn().mockReturnThis(),
401 |         digest: vi.fn(() => 'undefined-config-hash')
402 |       };
403 | 
404 |       vi.mocked(crypto.createHash).mockReturnValue(mockHashInstance);
405 | 
406 |       // Should handle undefined values gracefully
407 |       expect(() => {
408 |         const configString = JSON.stringify(config);
409 |         mockHashInstance.update(configString);
410 |         const hash = mockHashInstance.digest();
411 |       }).not.toThrow();
412 | 
413 |       expect(mockHashInstance.update).toHaveBeenCalled();
414 |       expect(mockHashInstance.digest).toHaveBeenCalledWith('hex');
415 |     });
416 |   });
417 | 
418 |   describe('Security Logging with Sanitization', () => {
419 |     it.skip('should sanitize sensitive information in logs', () => {
420 |       // TODO: Fix import issue with logger
421 |       // Arrange
422 |       const { logger } = require('../../../src/utils/logger');
423 | 
424 |       const context = {
425 |         n8nApiUrl: 'https://tenant1.n8n.cloud',
426 |         n8nApiKey: 'super-secret-api-key-123',
427 |         instanceId: 'tenant1-instance'
428 |       };
429 | 
430 |       // Simulate security logging
431 |       const sanitizedContext = {
432 |         n8nApiUrl: context.n8nApiUrl,
433 |         n8nApiKey: '***REDACTED***',
434 |         instanceId: context.instanceId
435 |       };
436 | 
437 |       logger.info('Multi-tenant context created', sanitizedContext);
438 | 
439 |       // Assert
440 |       expect(logger.info).toHaveBeenCalledWith(
441 |         'Multi-tenant context created',
442 |         expect.objectContaining({
443 |           n8nApiKey: '***REDACTED***'
444 |         })
445 |       );
446 |     });
447 | 
448 |     it.skip('should log session creation events', () => {
449 |       // TODO: Fix logger import issues
450 |       // Arrange
451 |       const { logger } = require('../../../src/utils/logger');
452 | 
453 |       const sessionData = {
454 |         sessionId: 'session-123-abc',
455 |         instanceId: 'tenant1-instance',
456 |         hasValidConfig: true
457 |       };
458 | 
459 |       logger.debug('Session created for multi-tenant instance', sessionData);
460 | 
461 |       // Assert
462 |       expect(logger.debug).toHaveBeenCalledWith(
463 |         'Session created for multi-tenant instance',
464 |         sessionData
465 |       );
466 |     });
467 | 
468 |     it.skip('should log context switching events', () => {
469 |       // TODO: Fix logger import issues
470 |       // Arrange
471 |       const { logger } = require('../../../src/utils/logger');
472 | 
473 |       const switchingData = {
474 |         fromSession: 'session-old-123',
475 |         toSession: 'session-new-456',
476 |         instanceId: 'tenant2-instance'
477 |       };
478 | 
479 |       logger.debug('Context switching between instances', switchingData);
480 | 
481 |       // Assert
482 |       expect(logger.debug).toHaveBeenCalledWith(
483 |         'Context switching between instances',
484 |         switchingData
485 |       );
486 |     });
487 | 
488 |     it.skip('should log validation failures securely', () => {
489 |       // TODO: Fix logger import issues
490 |       // Arrange
491 |       const { logger } = require('../../../src/utils/logger');
492 | 
493 |       const validationError = {
494 |         field: 'n8nApiUrl',
495 |         error: 'Invalid URL format',
496 |         value: '***REDACTED***' // Sensitive value should be redacted
497 |       };
498 | 
499 |       logger.warn('Instance context validation failed', validationError);
500 | 
501 |       // Assert
502 |       expect(logger.warn).toHaveBeenCalledWith(
503 |         'Instance context validation failed',
504 |         expect.objectContaining({
505 |           value: '***REDACTED***'
506 |         })
507 |       );
508 |     });
509 | 
510 |     it.skip('should not log API keys or sensitive data in plain text', () => {
511 |       // TODO: Fix logger import issues
512 |       // Arrange
513 |       const { logger } = require('../../../src/utils/logger');
514 | 
515 |       // Simulate various log calls that might contain sensitive data
516 |       logger.debug('Processing request', {
517 |         headers: {
518 |           'x-n8n-key': '***REDACTED***'
519 |         }
520 |       });
521 | 
522 |       logger.info('Context validation', {
523 |         n8nApiKey: '***REDACTED***'
524 |       });
525 | 
526 |       // Assert no sensitive data is logged
527 |       const allCalls = [
528 |         ...vi.mocked(logger.debug).mock.calls,
529 |         ...vi.mocked(logger.info).mock.calls
530 |       ];
531 | 
532 |       allCalls.forEach(call => {
533 |         const callString = JSON.stringify(call);
534 |         expect(callString).not.toMatch(/api[_-]?key['":]?\s*['"][^*]/i);
535 |         expect(callString).not.toMatch(/secret/i);
536 |         expect(callString).not.toMatch(/password/i);
537 |       });
538 |     });
539 |   });
540 | 
541 |   describe('Context Switching and Session Management', () => {
542 |     it('should handle session creation for new instance context', () => {
543 |       // Arrange
544 |       const context1: InstanceContext = {
545 |         n8nApiUrl: 'https://tenant1.n8n.cloud',
546 |         n8nApiKey: 'tenant1-key',
547 |         instanceId: 'tenant1'
548 |       };
549 | 
550 |       // Simulate session creation
551 |       const sessionId = 'session-tenant1-123';
552 |       const sessions = new Map();
553 | 
554 |       sessions.set(sessionId, {
555 |         context: context1,
556 |         lastAccess: new Date(),
557 |         initialized: true
558 |       });
559 | 
560 |       // Assert
561 |       expect(sessions.has(sessionId)).toBe(true);
562 |       expect(sessions.get(sessionId).context).toEqual(context1);
563 |     });
564 | 
565 |     it('should handle session switching between different contexts', () => {
566 |       // Arrange
567 |       const context1: InstanceContext = {
568 |         n8nApiUrl: 'https://tenant1.n8n.cloud',
569 |         n8nApiKey: 'tenant1-key',
570 |         instanceId: 'tenant1'
571 |       };
572 | 
573 |       const context2: InstanceContext = {
574 |         n8nApiUrl: 'https://tenant2.n8n.cloud',
575 |         n8nApiKey: 'tenant2-key',
576 |         instanceId: 'tenant2'
577 |       };
578 | 
579 |       const sessions = new Map();
580 |       const session1Id = 'session-tenant1-123';
581 |       const session2Id = 'session-tenant2-456';
582 | 
583 |       // Create sessions
584 |       sessions.set(session1Id, { context: context1, lastAccess: new Date() });
585 |       sessions.set(session2Id, { context: context2, lastAccess: new Date() });
586 | 
587 |       // Simulate context switching
588 |       let currentSession = session1Id;
589 |       expect(sessions.get(currentSession).context.instanceId).toBe('tenant1');
590 | 
591 |       currentSession = session2Id;
592 |       expect(sessions.get(currentSession).context.instanceId).toBe('tenant2');
593 | 
594 |       // Assert successful switching
595 |       expect(sessions.size).toBe(2);
596 |       expect(sessions.has(session1Id)).toBe(true);
597 |       expect(sessions.has(session2Id)).toBe(true);
598 |     });
599 | 
600 |     it('should prevent race conditions in session management', async () => {
601 |       // Arrange
602 |       const sessions = new Map();
603 |       const locks = new Map();
604 |       const sessionId = 'session-123';
605 | 
606 |       // Simulate locking mechanism
607 |       const acquireLock = (id: string) => {
608 |         if (locks.has(id)) {
609 |           return false; // Lock already acquired
610 |         }
611 |         locks.set(id, true);
612 |         return true;
613 |       };
614 | 
615 |       const releaseLock = (id: string) => {
616 |         locks.delete(id);
617 |       };
618 | 
619 |       // Test concurrent access
620 |       const lock1 = acquireLock(sessionId);
621 |       const lock2 = acquireLock(sessionId);
622 | 
623 |       // Assert only one lock can be acquired
624 |       expect(lock1).toBe(true);
625 |       expect(lock2).toBe(false);
626 | 
627 |       // Release and reacquire
628 |       releaseLock(sessionId);
629 |       const lock3 = acquireLock(sessionId);
630 |       expect(lock3).toBe(true);
631 |     });
632 | 
633 |     it('should handle session cleanup for inactive sessions', () => {
634 |       // Arrange
635 |       const sessions = new Map();
636 |       const now = new Date();
637 |       const oldTime = new Date(now.getTime() - 10 * 60 * 1000); // 10 minutes ago
638 | 
639 |       sessions.set('active-session', {
640 |         lastAccess: now,
641 |         context: { instanceId: 'active' }
642 |       });
643 | 
644 |       sessions.set('inactive-session', {
645 |         lastAccess: oldTime,
646 |         context: { instanceId: 'inactive' }
647 |       });
648 | 
649 |       // Simulate cleanup (5 minute threshold)
650 |       const threshold = 5 * 60 * 1000;
651 |       const cutoff = new Date(now.getTime() - threshold);
652 | 
653 |       for (const [sessionId, session] of sessions.entries()) {
654 |         if (session.lastAccess < cutoff) {
655 |           sessions.delete(sessionId);
656 |         }
657 |       }
658 | 
659 |       // Assert cleanup
660 |       expect(sessions.has('active-session')).toBe(true);
661 |       expect(sessions.has('inactive-session')).toBe(false);
662 |       expect(sessions.size).toBe(1);
663 |     });
664 | 
665 |     it('should handle maximum session limit', () => {
666 |       // Arrange
667 |       const sessions = new Map();
668 |       const MAX_SESSIONS = 3;
669 | 
670 |       // Fill to capacity
671 |       for (let i = 0; i < MAX_SESSIONS; i++) {
672 |         sessions.set(`session-${i}`, {
673 |           lastAccess: new Date(),
674 |           context: { instanceId: `tenant-${i}` }
675 |         });
676 |       }
677 | 
678 |       // Try to add one more
679 |       const oldestSession = 'session-0';
680 |       const newSession = 'session-new';
681 | 
682 |       if (sessions.size >= MAX_SESSIONS) {
683 |         // Remove oldest session
684 |         sessions.delete(oldestSession);
685 |       }
686 | 
687 |       sessions.set(newSession, {
688 |         lastAccess: new Date(),
689 |         context: { instanceId: 'new-tenant' }
690 |       });
691 | 
692 |       // Assert limit maintained
693 |       expect(sessions.size).toBe(MAX_SESSIONS);
694 |       expect(sessions.has(oldestSession)).toBe(false);
695 |       expect(sessions.has(newSession)).toBe(true);
696 |     });
697 |   });
698 | 
699 |   describe('Error Handling and Edge Cases', () => {
700 |     it.skip('should handle invalid header types gracefully', () => {
701 |       // TODO: Fix require() import issues
702 |       // Arrange
703 |       const headers: any = {
704 |         'x-n8n-url': ['array', 'of', 'values'],
705 |         'x-n8n-key': 12345, // number instead of string
706 |         'x-instance-id': null,
707 |         'x-session-id': undefined
708 |       };
709 | 
710 |       // Should not throw when processing invalid types
711 |       expect(() => {
712 |         const extractedUrl = Array.isArray(headers['x-n8n-url'])
713 |           ? headers['x-n8n-url'][0]
714 |           : headers['x-n8n-url'];
715 |         const extractedKey = typeof headers['x-n8n-key'] === 'string'
716 |           ? headers['x-n8n-key']
717 |           : String(headers['x-n8n-key']);
718 |       }).not.toThrow();
719 |     });
720 | 
721 |     it('should handle missing or corrupt session data', () => {
722 |       // Arrange
723 |       const sessions = new Map();
724 |       sessions.set('corrupt-session', null);
725 |       sessions.set('incomplete-session', { lastAccess: new Date() }); // missing context
726 | 
727 |       // Should handle corrupt data gracefully
728 |       expect(() => {
729 |         for (const [sessionId, session] of sessions.entries()) {
730 |           if (!session || !session.context) {
731 |             sessions.delete(sessionId);
732 |           }
733 |         }
734 |       }).not.toThrow();
735 | 
736 |       // Assert cleanup of corrupt data
737 |       expect(sessions.has('corrupt-session')).toBe(false);
738 |       expect(sessions.has('incomplete-session')).toBe(false);
739 |     });
740 | 
741 |     it.skip('should handle context validation errors gracefully', () => {
742 |       // TODO: Fix require() import issues
743 |       // Arrange
744 |       const invalidContext: InstanceContext = {
745 |         n8nApiUrl: 'not-a-url',
746 |         n8nApiKey: '',
747 |         n8nApiTimeout: -1,
748 |         n8nApiMaxRetries: -5
749 |       };
750 | 
751 |       const { validateInstanceContext } = require('../../../src/types/instance-context');
752 | 
753 |       // Should not throw even with invalid context
754 |       expect(() => {
755 |         const result = validateInstanceContext(invalidContext);
756 |         if (!result.valid) {
757 |           // Handle validation errors gracefully
758 |           const errors = result.errors || [];
759 |           errors.forEach((error: any) => {
760 |             // Log error without throwing
761 |             console.warn('Validation error:', error);
762 |           });
763 |         }
764 |       }).not.toThrow();
765 |     });
766 | 
767 |     it('should handle memory pressure during session management', () => {
768 |       // Arrange
769 |       const sessions = new Map();
770 |       const MAX_MEMORY_SESSIONS = 50;
771 | 
772 |       // Simulate memory pressure
773 |       for (let i = 0; i < MAX_MEMORY_SESSIONS * 2; i++) {
774 |         sessions.set(`session-${i}`, {
775 |           lastAccess: new Date(),
776 |           context: { instanceId: `tenant-${i}` },
777 |           data: new Array(1000).fill('memory-pressure-test') // Simulate memory usage
778 |         });
779 | 
780 |         // Implement emergency cleanup when approaching limits
781 |         if (sessions.size > MAX_MEMORY_SESSIONS) {
782 |           const oldestEntries = Array.from(sessions.entries())
783 |             .sort(([,a], [,b]) => a.lastAccess.getTime() - b.lastAccess.getTime())
784 |             .slice(0, 10); // Remove 10 oldest
785 | 
786 |           oldestEntries.forEach(([sessionId]) => {
787 |             sessions.delete(sessionId);
788 |           });
789 |         }
790 |       }
791 | 
792 |       // Assert memory management
793 |       expect(sessions.size).toBeLessThanOrEqual(MAX_MEMORY_SESSIONS + 10);
794 |     });
795 |   });
796 | });
```

--------------------------------------------------------------------------------
/tests/unit/services/loop-output-edge-cases.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, it, expect, beforeEach, vi } from 'vitest';
  2 | import { WorkflowValidator } from '@/services/workflow-validator';
  3 | import { NodeRepository } from '@/database/node-repository';
  4 | import { EnhancedConfigValidator } from '@/services/enhanced-config-validator';
  5 | 
  6 | // Mock dependencies
  7 | vi.mock('@/database/node-repository');
  8 | vi.mock('@/services/enhanced-config-validator');
  9 | 
 10 | describe('Loop Output Fix - Edge Cases', () => {
 11 |   let validator: WorkflowValidator;
 12 |   let mockNodeRepository: any;
 13 |   let mockNodeValidator: any;
 14 | 
 15 |   beforeEach(() => {
 16 |     vi.clearAllMocks();
 17 | 
 18 |     mockNodeRepository = {
 19 |       getNode: vi.fn((nodeType: string) => {
 20 |         // Default return
 21 |         if (nodeType === 'nodes-base.splitInBatches') {
 22 |           return {
 23 |             nodeType: 'nodes-base.splitInBatches',
 24 |             outputs: [
 25 |               { displayName: 'Done', name: 'done' },
 26 |               { displayName: 'Loop', name: 'loop' }
 27 |             ],
 28 |             outputNames: ['done', 'loop'],
 29 |             properties: []
 30 |           };
 31 |         }
 32 |         return {
 33 |           nodeType,
 34 |           properties: []
 35 |         };
 36 |       })
 37 |     };
 38 | 
 39 |     mockNodeValidator = {
 40 |       validateWithMode: vi.fn().mockReturnValue({
 41 |         errors: [],
 42 |         warnings: []
 43 |       })
 44 |     };
 45 | 
 46 |     validator = new WorkflowValidator(mockNodeRepository, mockNodeValidator);
 47 |   });
 48 | 
 49 |   describe('Nodes without outputs', () => {
 50 |     it('should handle nodes with null outputs gracefully', async () => {
 51 |       mockNodeRepository.getNode.mockReturnValue({
 52 |         nodeType: 'nodes-base.httpRequest',
 53 |         outputs: null,
 54 |         outputNames: null,
 55 |         properties: []
 56 |       });
 57 | 
 58 |       const workflow = {
 59 |         name: 'No Outputs Workflow',
 60 |         nodes: [
 61 |           {
 62 |             id: '1',
 63 |             name: 'HTTP Request',
 64 |             type: 'n8n-nodes-base.httpRequest',
 65 |             position: [100, 100],
 66 |             parameters: { url: 'https://example.com' }
 67 |           },
 68 |           {
 69 |             id: '2',
 70 |             name: 'Set',
 71 |             type: 'n8n-nodes-base.set',
 72 |             position: [300, 100],
 73 |             parameters: {}
 74 |           }
 75 |         ],
 76 |         connections: {
 77 |           'HTTP Request': {
 78 |             main: [
 79 |               [{ node: 'Set', type: 'main', index: 0 }]
 80 |             ]
 81 |           }
 82 |         }
 83 |       };
 84 | 
 85 |       const result = await validator.validateWorkflow(workflow as any);
 86 | 
 87 |       // Should not crash or produce output-related errors
 88 |       expect(result).toBeDefined();
 89 |       const outputErrors = result.errors.filter(e => 
 90 |         e.message?.includes('output') && !e.message?.includes('Connection')
 91 |       );
 92 |       expect(outputErrors).toHaveLength(0);
 93 |     });
 94 | 
 95 |     it('should handle nodes with undefined outputs gracefully', async () => {
 96 |       mockNodeRepository.getNode.mockReturnValue({
 97 |         nodeType: 'nodes-base.webhook',
 98 |         // outputs and outputNames are undefined
 99 |         properties: []
100 |       });
101 | 
102 |       const workflow = {
103 |         name: 'Undefined Outputs Workflow',
104 |         nodes: [
105 |           {
106 |             id: '1',
107 |             name: 'Webhook',
108 |             type: 'n8n-nodes-base.webhook',
109 |             position: [100, 100],
110 |             parameters: {}
111 |           }
112 |         ],
113 |         connections: {}
114 |       };
115 | 
116 |       const result = await validator.validateWorkflow(workflow as any);
117 | 
118 |       expect(result).toBeDefined();
119 |       expect(result.valid).toBeTruthy(); // Empty workflow with webhook should be valid
120 |     });
121 | 
122 |     it('should handle nodes with empty outputs array', async () => {
123 |       mockNodeRepository.getNode.mockReturnValue({
124 |         nodeType: 'nodes-base.customNode',
125 |         outputs: [],
126 |         outputNames: [],
127 |         properties: []
128 |       });
129 | 
130 |       const workflow = {
131 |         name: 'Empty Outputs Workflow',
132 |         nodes: [
133 |           {
134 |             id: '1',
135 |             name: 'Custom Node',
136 |             type: 'n8n-nodes-base.customNode',
137 |             position: [100, 100],
138 |             parameters: {}
139 |           }
140 |         ],
141 |         connections: {
142 |           'Custom Node': {
143 |             main: [
144 |               [{ node: 'Custom Node', type: 'main', index: 0 }] // Self-reference
145 |             ]
146 |           }
147 |         }
148 |       };
149 | 
150 |       const result = await validator.validateWorkflow(workflow as any);
151 | 
152 |       // Should warn about self-reference but not crash
153 |       const selfRefWarnings = result.warnings.filter(w => 
154 |         w.message?.includes('self-referencing')
155 |       );
156 |       expect(selfRefWarnings).toHaveLength(1);
157 |     });
158 |   });
159 | 
160 |   describe('Invalid connection indices', () => {
161 |     it('should handle negative connection indices', async () => {
162 |       // Use default mock that includes outputs for SplitInBatches
163 | 
164 |       const workflow = {
165 |         name: 'Negative Index Workflow',
166 |         nodes: [
167 |           {
168 |             id: '1',
169 |             name: 'Split In Batches',
170 |             type: 'n8n-nodes-base.splitInBatches',
171 |             position: [100, 100],
172 |             parameters: {}
173 |           },
174 |           {
175 |             id: '2',
176 |             name: 'Set',
177 |             type: 'n8n-nodes-base.set',
178 |             position: [300, 100],
179 |             parameters: {}
180 |           }
181 |         ],
182 |         connections: {
183 |           'Split In Batches': {
184 |             main: [
185 |               [{ node: 'Set', type: 'main', index: -1 }] // Invalid negative index
186 |             ]
187 |           }
188 |         }
189 |       };
190 | 
191 |       const result = await validator.validateWorkflow(workflow as any);
192 | 
193 |       const negativeIndexErrors = result.errors.filter(e => 
194 |         e.message?.includes('Invalid connection index -1')
195 |       );
196 |       expect(negativeIndexErrors).toHaveLength(1);
197 |       expect(negativeIndexErrors[0].message).toContain('must be non-negative');
198 |     });
199 | 
200 |     it('should handle very large connection indices', async () => {
201 |       mockNodeRepository.getNode.mockReturnValue({
202 |         nodeType: 'nodes-base.switch',
203 |         outputs: [
204 |           { displayName: 'Output 1' },
205 |           { displayName: 'Output 2' }
206 |         ],
207 |         properties: []
208 |       });
209 | 
210 |       const workflow = {
211 |         name: 'Large Index Workflow',
212 |         nodes: [
213 |           {
214 |             id: '1',
215 |             name: 'Switch',
216 |             type: 'n8n-nodes-base.switch',
217 |             position: [100, 100],
218 |             parameters: {}
219 |           },
220 |           {
221 |             id: '2',
222 |             name: 'Set',
223 |             type: 'n8n-nodes-base.set',
224 |             position: [300, 100],
225 |             parameters: {}
226 |           }
227 |         ],
228 |         connections: {
229 |           'Switch': {
230 |             main: [
231 |               [{ node: 'Set', type: 'main', index: 999 }] // Very large index
232 |             ]
233 |           }
234 |         }
235 |       };
236 | 
237 |       const result = await validator.validateWorkflow(workflow as any);
238 | 
239 |       // Should validate without crashing (n8n allows large indices)
240 |       expect(result).toBeDefined();
241 |     });
242 |   });
243 | 
244 |   describe('Malformed connection structures', () => {
245 |     it('should handle null connection objects', async () => {
246 |       // Use default mock that includes outputs for SplitInBatches
247 | 
248 |       const workflow = {
249 |         name: 'Null Connections Workflow',
250 |         nodes: [
251 |           {
252 |             id: '1',
253 |             name: 'Split In Batches',
254 |             type: 'n8n-nodes-base.splitInBatches',
255 |             position: [100, 100],
256 |             parameters: {}
257 |           }
258 |         ],
259 |         connections: {
260 |           'Split In Batches': {
261 |             main: [
262 |               null, // Null output
263 |               [{ node: 'NonExistent', type: 'main', index: 0 }]
264 |             ] as any
265 |           }
266 |         }
267 |       };
268 | 
269 |       const result = await validator.validateWorkflow(workflow as any);
270 | 
271 |       // Should handle gracefully without crashing
272 |       expect(result).toBeDefined();
273 |     });
274 | 
275 |     it('should handle missing connection properties', async () => {
276 |       // Use default mock that includes outputs for SplitInBatches
277 | 
278 |       const workflow = {
279 |         name: 'Malformed Connections Workflow',
280 |         nodes: [
281 |           {
282 |             id: '1',
283 |             name: 'Split In Batches',
284 |             type: 'n8n-nodes-base.splitInBatches',
285 |             position: [100, 100],
286 |             parameters: {}
287 |           },
288 |           {
289 |             id: '2',
290 |             name: 'Set',
291 |             type: 'n8n-nodes-base.set',
292 |             position: [300, 100],
293 |             parameters: {}
294 |           }
295 |         ],
296 |         connections: {
297 |           'Split In Batches': {
298 |             main: [
299 |               [
300 |                 { node: 'Set' } as any, // Missing type and index
301 |                 { type: 'main', index: 0 } as any, // Missing node
302 |                 {} as any // Empty object
303 |               ]
304 |             ]
305 |           }
306 |         }
307 |       };
308 | 
309 |       const result = await validator.validateWorkflow(workflow as any);
310 | 
311 |       // Should handle malformed connections but report errors
312 |       expect(result).toBeDefined();
313 |       expect(result.errors.length).toBeGreaterThan(0);
314 |     });
315 |   });
316 | 
317 |   describe('Deep loop back detection limits', () => {
318 |     it('should respect maxDepth limit in checkForLoopBack', async () => {
319 |       // Use default mock that includes outputs for SplitInBatches
320 | 
321 |       // Create a very deep chain that exceeds maxDepth (50)
322 |       const nodes = [
323 |         {
324 |           id: '1',
325 |           name: 'Split In Batches',
326 |           type: 'n8n-nodes-base.splitInBatches',
327 |           position: [100, 100],
328 |           parameters: {}
329 |         }
330 |       ];
331 | 
332 |       const connections: any = {
333 |         'Split In Batches': {
334 |           main: [
335 |             [], // Done output
336 |             [{ node: 'Node1', type: 'main', index: 0 }] // Loop output
337 |           ]
338 |         }
339 |       };
340 | 
341 |       // Create chain of 60 nodes (exceeds maxDepth of 50)
342 |       for (let i = 1; i <= 60; i++) {
343 |         nodes.push({
344 |           id: (i + 1).toString(),
345 |           name: `Node${i}`,
346 |           type: 'n8n-nodes-base.set',
347 |           position: [100 + i * 50, 100],
348 |           parameters: {}
349 |         });
350 | 
351 |         if (i < 60) {
352 |           connections[`Node${i}`] = {
353 |             main: [[{ node: `Node${i + 1}`, type: 'main', index: 0 }]]
354 |           };
355 |         } else {
356 |           // Last node connects back to Split In Batches
357 |           connections[`Node${i}`] = {
358 |             main: [[{ node: 'Split In Batches', type: 'main', index: 0 }]]
359 |           };
360 |         }
361 |       }
362 | 
363 |       const workflow = {
364 |         name: 'Deep Chain Workflow',
365 |         nodes,
366 |         connections
367 |       };
368 | 
369 |       const result = await validator.validateWorkflow(workflow as any);
370 | 
371 |       // Should warn about missing loop back because depth limit prevents detection
372 |       const loopBackWarnings = result.warnings.filter(w => 
373 |         w.message?.includes('doesn\'t connect back')
374 |       );
375 |       expect(loopBackWarnings).toHaveLength(1);
376 |     });
377 | 
378 |     it('should handle circular references without infinite loops', async () => {
379 |       // Use default mock that includes outputs for SplitInBatches
380 | 
381 |       const workflow = {
382 |         name: 'Circular Reference Workflow',
383 |         nodes: [
384 |           {
385 |             id: '1',
386 |             name: 'Split In Batches',
387 |             type: 'n8n-nodes-base.splitInBatches',
388 |             position: [100, 100],
389 |             parameters: {}
390 |           },
391 |           {
392 |             id: '2',
393 |             name: 'NodeA',
394 |             type: 'n8n-nodes-base.set',
395 |             position: [300, 100],
396 |             parameters: {}
397 |           },
398 |           {
399 |             id: '3',
400 |             name: 'NodeB',
401 |             type: 'n8n-nodes-base.function',
402 |             position: [500, 100],
403 |             parameters: {}
404 |           }
405 |         ],
406 |         connections: {
407 |           'Split In Batches': {
408 |             main: [
409 |               [],
410 |               [{ node: 'NodeA', type: 'main', index: 0 }]
411 |             ]
412 |           },
413 |           'NodeA': {
414 |             main: [
415 |               [{ node: 'NodeB', type: 'main', index: 0 }]
416 |             ]
417 |           },
418 |           'NodeB': {
419 |             main: [
420 |               [{ node: 'NodeA', type: 'main', index: 0 }] // Circular: B -> A -> B -> A ...
421 |             ]
422 |           }
423 |         }
424 |       };
425 | 
426 |       const result = await validator.validateWorkflow(workflow as any);
427 | 
428 |       // Should complete without hanging and warn about missing loop back
429 |       expect(result).toBeDefined();
430 |       const loopBackWarnings = result.warnings.filter(w => 
431 |         w.message?.includes('doesn\'t connect back')
432 |       );
433 |       expect(loopBackWarnings).toHaveLength(1);
434 |     });
435 | 
436 |     it('should handle self-referencing nodes in loop back detection', async () => {
437 |       // Use default mock that includes outputs for SplitInBatches
438 | 
439 |       const workflow = {
440 |         name: 'Self Reference Workflow',
441 |         nodes: [
442 |           {
443 |             id: '1',
444 |             name: 'Split In Batches',
445 |             type: 'n8n-nodes-base.splitInBatches',
446 |             position: [100, 100],
447 |             parameters: {}
448 |           },
449 |           {
450 |             id: '2',
451 |             name: 'SelfRef',
452 |             type: 'n8n-nodes-base.set',
453 |             position: [300, 100],
454 |             parameters: {}
455 |           }
456 |         ],
457 |         connections: {
458 |           'Split In Batches': {
459 |             main: [
460 |               [],
461 |               [{ node: 'SelfRef', type: 'main', index: 0 }]
462 |             ]
463 |           },
464 |           'SelfRef': {
465 |             main: [
466 |               [{ node: 'SelfRef', type: 'main', index: 0 }] // Self-reference instead of loop back
467 |             ]
468 |           }
469 |         }
470 |       };
471 | 
472 |       const result = await validator.validateWorkflow(workflow as any);
473 | 
474 |       // Should warn about missing loop back and self-reference
475 |       const loopBackWarnings = result.warnings.filter(w => 
476 |         w.message?.includes('doesn\'t connect back')
477 |       );
478 |       const selfRefWarnings = result.warnings.filter(w => 
479 |         w.message?.includes('self-referencing')
480 |       );
481 | 
482 |       expect(loopBackWarnings).toHaveLength(1);
483 |       expect(selfRefWarnings).toHaveLength(1);
484 |     });
485 |   });
486 | 
487 |   describe('Complex output structures', () => {
488 |     it('should handle nodes with many outputs', async () => {
489 |       const manyOutputs = Array.from({ length: 20 }, (_, i) => ({
490 |         displayName: `Output ${i + 1}`,
491 |         name: `output${i + 1}`,
492 |         description: `Output number ${i + 1}`
493 |       }));
494 | 
495 |       mockNodeRepository.getNode.mockReturnValue({
496 |         nodeType: 'nodes-base.complexSwitch',
497 |         outputs: manyOutputs,
498 |         outputNames: manyOutputs.map(o => o.name),
499 |         properties: []
500 |       });
501 | 
502 |       const workflow = {
503 |         name: 'Many Outputs Workflow',
504 |         nodes: [
505 |           {
506 |             id: '1',
507 |             name: 'Complex Switch',
508 |             type: 'n8n-nodes-base.complexSwitch',
509 |             position: [100, 100],
510 |             parameters: {}
511 |           },
512 |           {
513 |             id: '2',
514 |             name: 'Set',
515 |             type: 'n8n-nodes-base.set',
516 |             position: [300, 100],
517 |             parameters: {}
518 |           }
519 |         ],
520 |         connections: {
521 |           'Complex Switch': {
522 |             main: Array.from({ length: 20 }, () => [
523 |               { node: 'Set', type: 'main', index: 0 }
524 |             ])
525 |           }
526 |         }
527 |       };
528 | 
529 |       const result = await validator.validateWorkflow(workflow as any);
530 | 
531 |       // Should handle without performance issues
532 |       expect(result).toBeDefined();
533 |     });
534 | 
535 |     it('should handle mixed output types (main, error, ai_tool)', async () => {
536 |       mockNodeRepository.getNode.mockReturnValue({
537 |         nodeType: 'nodes-base.complexNode',
538 |         outputs: [
539 |           { displayName: 'Main', type: 'main' },
540 |           { displayName: 'Error', type: 'error' }
541 |         ],
542 |         properties: []
543 |       });
544 | 
545 |       const workflow = {
546 |         name: 'Mixed Output Types Workflow',
547 |         nodes: [
548 |           {
549 |             id: '1',
550 |             name: 'Complex Node',
551 |             type: 'n8n-nodes-base.complexNode',
552 |             position: [100, 100],
553 |             parameters: {}
554 |           },
555 |           {
556 |             id: '2',
557 |             name: 'Main Handler',
558 |             type: 'n8n-nodes-base.set',
559 |             position: [300, 50],
560 |             parameters: {}
561 |           },
562 |           {
563 |             id: '3',
564 |             name: 'Error Handler',
565 |             type: 'n8n-nodes-base.set',
566 |             position: [300, 150],
567 |             parameters: {}
568 |           },
569 |           {
570 |             id: '4',
571 |             name: 'Tool',
572 |             type: 'n8n-nodes-base.httpRequest',
573 |             position: [500, 100],
574 |             parameters: {}
575 |           }
576 |         ],
577 |         connections: {
578 |           'Complex Node': {
579 |             main: [
580 |               [{ node: 'Main Handler', type: 'main', index: 0 }]
581 |             ],
582 |             error: [
583 |               [{ node: 'Error Handler', type: 'main', index: 0 }]
584 |             ],
585 |             ai_tool: [
586 |               [{ node: 'Tool', type: 'main', index: 0 }]
587 |             ]
588 |           }
589 |         }
590 |       };
591 | 
592 |       const result = await validator.validateWorkflow(workflow as any);
593 | 
594 |       // Should validate all connection types
595 |       expect(result).toBeDefined();
596 |       expect(result.statistics.validConnections).toBe(3);
597 |     });
598 |   });
599 | 
600 |   describe('SplitInBatches specific edge cases', () => {
601 |     it('should handle SplitInBatches with no connections', async () => {
602 |       // Use default mock that includes outputs for SplitInBatches
603 | 
604 |       const workflow = {
605 |         name: 'Isolated SplitInBatches',
606 |         nodes: [
607 |           {
608 |             id: '1',
609 |             name: 'Split In Batches',
610 |             type: 'n8n-nodes-base.splitInBatches',
611 |             position: [100, 100],
612 |             parameters: {}
613 |           }
614 |         ],
615 |         connections: {}
616 |       };
617 | 
618 |       const result = await validator.validateWorkflow(workflow as any);
619 | 
620 |       // Should not produce SplitInBatches-specific warnings for isolated node
621 |       const splitWarnings = result.warnings.filter(w => 
622 |         w.message?.includes('SplitInBatches') || 
623 |         w.message?.includes('loop') ||
624 |         w.message?.includes('done')
625 |       );
626 |       expect(splitWarnings).toHaveLength(0);
627 |     });
628 | 
629 |     it('should handle SplitInBatches with only one output connected', async () => {
630 |       // Use default mock that includes outputs for SplitInBatches
631 | 
632 |       const workflow = {
633 |         name: 'Single Output SplitInBatches',
634 |         nodes: [
635 |           {
636 |             id: '1',
637 |             name: 'Split In Batches',
638 |             type: 'n8n-nodes-base.splitInBatches',
639 |             position: [100, 100],
640 |             parameters: {}
641 |           },
642 |           {
643 |             id: '2',
644 |             name: 'Final Action',
645 |             type: 'n8n-nodes-base.emailSend',
646 |             position: [300, 100],
647 |             parameters: {}
648 |           }
649 |         ],
650 |         connections: {
651 |           'Split In Batches': {
652 |             main: [
653 |               [{ node: 'Final Action', type: 'main', index: 0 }], // Only done output connected
654 |               [] // Loop output empty
655 |             ]
656 |           }
657 |         }
658 |       };
659 | 
660 |       const result = await validator.validateWorkflow(workflow as any);
661 | 
662 |       // Should NOT warn about empty loop output (it's only a problem if loop connects to something but doesn't loop back)
663 |       // An empty loop output is valid - it just means no looping occurs
664 |       const loopWarnings = result.warnings.filter(w => 
665 |         w.message?.includes('loop') && w.message?.includes('connect back')
666 |       );
667 |       expect(loopWarnings).toHaveLength(0);
668 |     });
669 | 
670 |     it('should handle SplitInBatches with both outputs to same node', async () => {
671 |       // Use default mock that includes outputs for SplitInBatches
672 | 
673 |       const workflow = {
674 |         name: 'Same Target SplitInBatches',
675 |         nodes: [
676 |           {
677 |             id: '1',
678 |             name: 'Split In Batches',
679 |             type: 'n8n-nodes-base.splitInBatches',
680 |             position: [100, 100],
681 |             parameters: {}
682 |           },
683 |           {
684 |             id: '2',
685 |             name: 'Multi Purpose',
686 |             type: 'n8n-nodes-base.set',
687 |             position: [300, 100],
688 |             parameters: {}
689 |           }
690 |         ],
691 |         connections: {
692 |           'Split In Batches': {
693 |             main: [
694 |               [{ node: 'Multi Purpose', type: 'main', index: 0 }], // Done -> Multi Purpose
695 |               [{ node: 'Multi Purpose', type: 'main', index: 0 }]  // Loop -> Multi Purpose
696 |             ]
697 |           },
698 |           'Multi Purpose': {
699 |             main: [
700 |               [{ node: 'Split In Batches', type: 'main', index: 0 }] // Loop back
701 |             ]
702 |           }
703 |         }
704 |       };
705 | 
706 |       const result = await validator.validateWorkflow(workflow as any);
707 | 
708 |       // Both outputs go to same node which loops back - should be valid
709 |       // No warnings about loop back since it does connect back
710 |       const loopWarnings = result.warnings.filter(w => 
711 |         w.message?.includes('loop') && w.message?.includes('connect back')
712 |       );
713 |       expect(loopWarnings).toHaveLength(0);
714 |     });
715 | 
716 |     it('should detect reversed outputs with processing node on done output', async () => {
717 |       // Use default mock that includes outputs for SplitInBatches
718 | 
719 |       const workflow = {
720 |         name: 'Reversed SplitInBatches with Function Node',
721 |         nodes: [
722 |           {
723 |             id: '1',
724 |             name: 'Split In Batches',
725 |             type: 'n8n-nodes-base.splitInBatches',
726 |             position: [100, 100],
727 |             parameters: {}
728 |           },
729 |           {
730 |             id: '2',
731 |             name: 'Process Function',
732 |             type: 'n8n-nodes-base.function',
733 |             position: [300, 100],
734 |             parameters: {}
735 |           }
736 |         ],
737 |         connections: {
738 |           'Split In Batches': {
739 |             main: [
740 |               [{ node: 'Process Function', type: 'main', index: 0 }], // Done -> Function (this is wrong)
741 |               [] // Loop output empty
742 |             ]
743 |           },
744 |           'Process Function': {
745 |             main: [
746 |               [{ node: 'Split In Batches', type: 'main', index: 0 }] // Function connects back (indicates it should be on loop)
747 |             ]
748 |           }
749 |         }
750 |       };
751 | 
752 |       const result = await validator.validateWorkflow(workflow as any);
753 | 
754 |       // Should error about reversed outputs since function node on done output connects back
755 |       const reversedErrors = result.errors.filter(e => 
756 |         e.message?.includes('SplitInBatches outputs appear reversed')
757 |       );
758 |       expect(reversedErrors).toHaveLength(1);
759 |     });
760 | 
761 |     it('should handle non-existent node type gracefully', async () => {
762 |       // Node doesn't exist in repository
763 |       mockNodeRepository.getNode.mockReturnValue(null);
764 | 
765 |       const workflow = {
766 |         name: 'Unknown Node Type',
767 |         nodes: [
768 |           {
769 |             id: '1',
770 |             name: 'Unknown Node',
771 |             type: 'n8n-nodes-base.unknownNode',
772 |             position: [100, 100],
773 |             parameters: {}
774 |           }
775 |         ],
776 |         connections: {}
777 |       };
778 | 
779 |       const result = await validator.validateWorkflow(workflow as any);
780 | 
781 |       // Should report unknown node type error
782 |       const unknownNodeErrors = result.errors.filter(e => 
783 |         e.message?.includes('Unknown node type')
784 |       );
785 |       expect(unknownNodeErrors).toHaveLength(1);
786 |     });
787 |   });
788 | 
789 |   describe('Performance edge cases', () => {
790 |     it('should handle very large workflows efficiently', async () => {
791 |       mockNodeRepository.getNode.mockReturnValue({
792 |         nodeType: 'nodes-base.set',
793 |         properties: []
794 |       });
795 | 
796 |       // Create workflow with 1000 nodes
797 |       const nodes = Array.from({ length: 1000 }, (_, i) => ({
798 |         id: `node${i}`,
799 |         name: `Node ${i}`,
800 |         type: 'n8n-nodes-base.set',
801 |         position: [100 + (i % 50) * 50, 100 + Math.floor(i / 50) * 50],
802 |         parameters: {}
803 |       }));
804 | 
805 |       // Create simple linear connections
806 |       const connections: any = {};
807 |       for (let i = 0; i < 999; i++) {
808 |         connections[`Node ${i}`] = {
809 |           main: [[{ node: `Node ${i + 1}`, type: 'main', index: 0 }]]
810 |         };
811 |       }
812 | 
813 |       const workflow = {
814 |         name: 'Large Workflow',
815 |         nodes,
816 |         connections
817 |       };
818 | 
819 |       const startTime = Date.now();
820 |       const result = await validator.validateWorkflow(workflow as any);
821 |       const duration = Date.now() - startTime;
822 | 
823 |       // Should complete within reasonable time (< 5 seconds)
824 |       expect(duration).toBeLessThan(5000);
825 |       expect(result).toBeDefined();
826 |       expect(result.statistics.totalNodes).toBe(1000);
827 |     });
828 | 
829 |     it('should handle workflows with many SplitInBatches nodes', async () => {
830 |       // Use default mock that includes outputs for SplitInBatches
831 | 
832 |       // Create 100 SplitInBatches nodes
833 |       const nodes = Array.from({ length: 100 }, (_, i) => ({
834 |         id: `split${i}`,
835 |         name: `Split ${i}`,
836 |         type: 'n8n-nodes-base.splitInBatches',
837 |         position: [100 + (i % 10) * 100, 100 + Math.floor(i / 10) * 100],
838 |         parameters: {}
839 |       }));
840 | 
841 |       const connections: any = {};
842 |       // Each split connects to the next one
843 |       for (let i = 0; i < 99; i++) {
844 |         connections[`Split ${i}`] = {
845 |           main: [
846 |             [{ node: `Split ${i + 1}`, type: 'main', index: 0 }], // Done -> next split
847 |             [] // Empty loop
848 |           ]
849 |         };
850 |       }
851 | 
852 |       const workflow = {
853 |         name: 'Many SplitInBatches Workflow',
854 |         nodes,
855 |         connections
856 |       };
857 | 
858 |       const result = await validator.validateWorkflow(workflow as any);
859 | 
860 |       // Should validate all nodes without performance issues
861 |       expect(result).toBeDefined();
862 |       expect(result.statistics.totalNodes).toBe(100);
863 |     });
864 |   });
865 | });
```
Page 35/59FirstPrevNextLast