#
tokens: 45376/50000 7/620 files (page 25/60)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 25 of 60. 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
│   ├── CI_TEST_INFRASTRUCTURE.md
│   ├── CLAUDE_CODE_SETUP.md
│   ├── CLAUDE_INTERVIEW.md
│   ├── CODECOV_SETUP.md
│   ├── CODEX_SETUP.md
│   ├── CURSOR_SETUP.md
│   ├── DEPENDENCY_UPDATES.md
│   ├── DOCKER_README.md
│   ├── DOCKER_TROUBLESHOOTING.md
│   ├── FINAL_AI_VALIDATION_SPEC.md
│   ├── FLEXIBLE_INSTANCE_CONFIGURATION.md
│   ├── HTTP_DEPLOYMENT.md
│   ├── img
│   │   ├── cc_command.png
│   │   ├── cc_connected.png
│   │   ├── codex_connected.png
│   │   ├── cursor_tut.png
│   │   ├── Railway_api.png
│   │   ├── Railway_server_address.png
│   │   ├── skills.png
│   │   ├── vsc_ghcp_chat_agent_mode.png
│   │   ├── vsc_ghcp_chat_instruction_files.png
│   │   ├── vsc_ghcp_chat_thinking_tool.png
│   │   └── windsurf_tut.png
│   ├── INSTALLATION.md
│   ├── LIBRARY_USAGE.md
│   ├── local
│   │   ├── DEEP_DIVE_ANALYSIS_2025-10-02.md
│   │   ├── DEEP_DIVE_ANALYSIS_README.md
│   │   ├── Deep_dive_p1_p2.md
│   │   ├── integration-testing-plan.md
│   │   ├── integration-tests-phase1-summary.md
│   │   ├── N8N_AI_WORKFLOW_BUILDER_ANALYSIS.md
│   │   ├── P0_IMPLEMENTATION_PLAN.md
│   │   └── TEMPLATE_MINING_ANALYSIS.md
│   ├── MCP_ESSENTIALS_README.md
│   ├── MCP_QUICK_START_GUIDE.md
│   ├── N8N_DEPLOYMENT.md
│   ├── RAILWAY_DEPLOYMENT.md
│   ├── README_CLAUDE_SETUP.md
│   ├── README.md
│   ├── 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-sanitizer.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
│       ├── expression-utils.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-sanitizer.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
│   │   │   ├── expression-utils.test.ts
│   │   │   ├── fixed-collection-validator.test.ts
│   │   │   ├── n8n-errors.test.ts
│   │   │   ├── node-type-normalizer.test.ts
│   │   │   ├── node-type-utils.test.ts
│   │   │   ├── node-utils.test.ts
│   │   │   ├── simple-cache-memory-leak-fix.test.ts
│   │   │   ├── ssrf-protection.test.ts
│   │   │   └── template-node-resolver.test.ts
│   │   └── validation-fixes.test.ts
│   └── utils
│       ├── assertions.ts
│       ├── builders
│       │   └── workflow.builder.ts
│       ├── data-generators.ts
│       ├── database-utils.ts
│       ├── README.md
│       └── test-helpers.ts
├── thumbnail.png
├── tsconfig.build.json
├── tsconfig.json
├── types
│   ├── mcp.d.ts
│   └── test-env.d.ts
├── verify-telemetry-fix.js
├── versioned-nodes.md
├── vitest.config.benchmark.ts
├── vitest.config.integration.ts
└── vitest.config.ts
```

# Files

--------------------------------------------------------------------------------
/tests/unit/__mocks__/n8n-nodes-base.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { vi } from 'vitest';
  2 | 
  3 | // Mock types that match n8n-workflow
  4 | interface INodeExecutionData {
  5 |   json: any;
  6 |   binary?: any;
  7 |   pairedItem?: any;
  8 | }
  9 | 
 10 | interface IExecuteFunctions {
 11 |   getInputData(): INodeExecutionData[];
 12 |   getNodeParameter(parameterName: string, itemIndex: number, fallbackValue?: any): any;
 13 |   getCredentials(type: string): Promise<any>;
 14 |   helpers: {
 15 |     returnJsonArray(data: any): INodeExecutionData[];
 16 |     httpRequest(options: any): Promise<any>;
 17 |     webhook(): any;
 18 |   };
 19 | }
 20 | 
 21 | interface IWebhookFunctions {
 22 |   getWebhookName(): string;
 23 |   getBodyData(): any;
 24 |   getHeaderData(): any;
 25 |   getQueryData(): any;
 26 |   getRequestObject(): any;
 27 |   getResponseObject(): any;
 28 |   helpers: {
 29 |     returnJsonArray(data: any): INodeExecutionData[];
 30 |   };
 31 | }
 32 | 
 33 | interface INodeTypeDescription {
 34 |   displayName: string;
 35 |   name: string;
 36 |   group: string[];
 37 |   version: number;
 38 |   description: string;
 39 |   defaults: { name: string };
 40 |   inputs: string[];
 41 |   outputs: string[];
 42 |   credentials?: any[];
 43 |   webhooks?: any[];
 44 |   properties: any[];
 45 |   icon?: string;
 46 |   subtitle?: string;
 47 | }
 48 | 
 49 | interface INodeType {
 50 |   description: INodeTypeDescription;
 51 |   execute?(this: IExecuteFunctions): Promise<INodeExecutionData[][]>;
 52 |   webhook?(this: IWebhookFunctions): Promise<any>;
 53 |   trigger?(this: any): Promise<void>;
 54 |   poll?(this: any): Promise<INodeExecutionData[][] | null>;
 55 | }
 56 | 
 57 | // Base mock node implementation
 58 | class BaseMockNode implements INodeType {
 59 |   description: INodeTypeDescription;
 60 |   execute: any;
 61 |   webhook: any;
 62 |   
 63 |   constructor(description: INodeTypeDescription, execute?: any, webhook?: any) {
 64 |     this.description = description;
 65 |     this.execute = execute ? vi.fn(execute) : undefined;
 66 |     this.webhook = webhook ? vi.fn(webhook) : undefined;
 67 |   }
 68 | }
 69 | 
 70 | // Mock implementations for each node type
 71 | const mockWebhookNode = new BaseMockNode(
 72 |   {
 73 |     displayName: 'Webhook',
 74 |     name: 'webhook',
 75 |     group: ['trigger'],
 76 |     version: 1,
 77 |     description: 'Starts the workflow when a webhook is called',
 78 |     defaults: { name: 'Webhook' },
 79 |     inputs: [],
 80 |     outputs: ['main'],
 81 |     webhooks: [
 82 |       {
 83 |         name: 'default',
 84 |         httpMethod: '={{$parameter["httpMethod"]}}',
 85 |         path: '={{$parameter["path"]}}',
 86 |         responseMode: '={{$parameter["responseMode"]}}',
 87 |       }
 88 |     ],
 89 |     properties: [
 90 |       {
 91 |         displayName: 'Path',
 92 |         name: 'path',
 93 |         type: 'string',
 94 |         default: 'webhook',
 95 |         required: true,
 96 |         description: 'The path to listen on',
 97 |       },
 98 |       {
 99 |         displayName: 'HTTP Method',
100 |         name: 'httpMethod',
101 |         type: 'options',
102 |         default: 'GET',
103 |         options: [
104 |           { name: 'GET', value: 'GET' },
105 |           { name: 'POST', value: 'POST' },
106 |           { name: 'PUT', value: 'PUT' },
107 |           { name: 'DELETE', value: 'DELETE' },
108 |           { name: 'HEAD', value: 'HEAD' },
109 |           { name: 'PATCH', value: 'PATCH' },
110 |         ],
111 |       },
112 |       {
113 |         displayName: 'Response Mode',
114 |         name: 'responseMode',
115 |         type: 'options',
116 |         default: 'onReceived',
117 |         options: [
118 |           { name: 'On Received', value: 'onReceived' },
119 |           { name: 'Last Node', value: 'lastNode' },
120 |         ],
121 |       },
122 |     ],
123 |   },
124 |   undefined,
125 |   async function webhook(this: IWebhookFunctions) {
126 |     const returnData: INodeExecutionData[] = [];
127 |     returnData.push({
128 |       json: {
129 |         headers: this.getHeaderData(),
130 |         params: this.getQueryData(),
131 |         body: this.getBodyData(),
132 |       }
133 |     });
134 |     return {
135 |       workflowData: [returnData],
136 |     };
137 |   }
138 | );
139 | 
140 | const mockHttpRequestNode = new BaseMockNode(
141 |   {
142 |     displayName: 'HTTP Request',
143 |     name: 'httpRequest',
144 |     group: ['transform'],
145 |     version: 3,
146 |     description: 'Makes an HTTP request and returns the response',
147 |     defaults: { name: 'HTTP Request' },
148 |     inputs: ['main'],
149 |     outputs: ['main'],
150 |     properties: [
151 |       {
152 |         displayName: 'Method',
153 |         name: 'method',
154 |         type: 'options',
155 |         default: 'GET',
156 |         options: [
157 |           { name: 'GET', value: 'GET' },
158 |           { name: 'POST', value: 'POST' },
159 |           { name: 'PUT', value: 'PUT' },
160 |           { name: 'DELETE', value: 'DELETE' },
161 |           { name: 'HEAD', value: 'HEAD' },
162 |           { name: 'PATCH', value: 'PATCH' },
163 |         ],
164 |       },
165 |       {
166 |         displayName: 'URL',
167 |         name: 'url',
168 |         type: 'string',
169 |         default: '',
170 |         required: true,
171 |         placeholder: 'https://example.com',
172 |       },
173 |       {
174 |         displayName: 'Authentication',
175 |         name: 'authentication',
176 |         type: 'options',
177 |         default: 'none',
178 |         options: [
179 |           { name: 'None', value: 'none' },
180 |           { name: 'Basic Auth', value: 'basicAuth' },
181 |           { name: 'Digest Auth', value: 'digestAuth' },
182 |           { name: 'Header Auth', value: 'headerAuth' },
183 |           { name: 'OAuth1', value: 'oAuth1' },
184 |           { name: 'OAuth2', value: 'oAuth2' },
185 |         ],
186 |       },
187 |       {
188 |         displayName: 'Response Format',
189 |         name: 'responseFormat',
190 |         type: 'options',
191 |         default: 'json',
192 |         options: [
193 |           { name: 'JSON', value: 'json' },
194 |           { name: 'String', value: 'string' },
195 |           { name: 'File', value: 'file' },
196 |         ],
197 |       },
198 |       {
199 |         displayName: 'Options',
200 |         name: 'options',
201 |         type: 'collection',
202 |         placeholder: 'Add Option',
203 |         default: {},
204 |         options: [
205 |           {
206 |             displayName: 'Body Content Type',
207 |             name: 'bodyContentType',
208 |             type: 'options',
209 |             default: 'json',
210 |             options: [
211 |               { name: 'JSON', value: 'json' },
212 |               { name: 'Form Data', value: 'formData' },
213 |               { name: 'Form URL Encoded', value: 'form-urlencoded' },
214 |               { name: 'Raw', value: 'raw' },
215 |             ],
216 |           },
217 |           {
218 |             displayName: 'Headers',
219 |             name: 'headers',
220 |             type: 'fixedCollection',
221 |             default: {},
222 |             typeOptions: {
223 |               multipleValues: true,
224 |             },
225 |           },
226 |           {
227 |             displayName: 'Query Parameters',
228 |             name: 'queryParameters',
229 |             type: 'fixedCollection',
230 |             default: {},
231 |             typeOptions: {
232 |               multipleValues: true,
233 |             },
234 |           },
235 |         ],
236 |       },
237 |     ],
238 |   },
239 |   async function execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
240 |     const items = this.getInputData();
241 |     const returnData: INodeExecutionData[] = [];
242 |     
243 |     for (let i = 0; i < items.length; i++) {
244 |       const method = this.getNodeParameter('method', i) as string;
245 |       const url = this.getNodeParameter('url', i) as string;
246 |       
247 |       // Mock response
248 |       const response = {
249 |         statusCode: 200,
250 |         headers: {},
251 |         body: { success: true, method, url },
252 |       };
253 |       
254 |       returnData.push({
255 |         json: response,
256 |       });
257 |     }
258 |     
259 |     return [returnData];
260 |   }
261 | );
262 | 
263 | const mockSlackNode = new BaseMockNode(
264 |   {
265 |     displayName: 'Slack',
266 |     name: 'slack',
267 |     group: ['output'],
268 |     version: 2,
269 |     description: 'Send messages to Slack',
270 |     defaults: { name: 'Slack' },
271 |     inputs: ['main'],
272 |     outputs: ['main'],
273 |     credentials: [
274 |       {
275 |         name: 'slackApi',
276 |         required: true,
277 |       },
278 |     ],
279 |     properties: [
280 |       {
281 |         displayName: 'Resource',
282 |         name: 'resource',
283 |         type: 'options',
284 |         default: 'message',
285 |         options: [
286 |           { name: 'Channel', value: 'channel' },
287 |           { name: 'Message', value: 'message' },
288 |           { name: 'User', value: 'user' },
289 |           { name: 'File', value: 'file' },
290 |         ],
291 |       },
292 |       {
293 |         displayName: 'Operation',
294 |         name: 'operation',
295 |         type: 'options',
296 |         displayOptions: {
297 |           show: {
298 |             resource: ['message'],
299 |           },
300 |         },
301 |         default: 'post',
302 |         options: [
303 |           { name: 'Post', value: 'post' },
304 |           { name: 'Update', value: 'update' },
305 |           { name: 'Delete', value: 'delete' },
306 |         ],
307 |       },
308 |       {
309 |         displayName: 'Channel',
310 |         name: 'channel',
311 |         type: 'options',
312 |         typeOptions: {
313 |           loadOptionsMethod: 'getChannels',
314 |         },
315 |         displayOptions: {
316 |           show: {
317 |             resource: ['message'],
318 |             operation: ['post'],
319 |           },
320 |         },
321 |         default: '',
322 |         required: true,
323 |       },
324 |       {
325 |         displayName: 'Text',
326 |         name: 'text',
327 |         type: 'string',
328 |         typeOptions: {
329 |           alwaysOpenEditWindow: true,
330 |         },
331 |         displayOptions: {
332 |           show: {
333 |             resource: ['message'],
334 |             operation: ['post'],
335 |           },
336 |         },
337 |         default: '',
338 |         required: true,
339 |       },
340 |     ],
341 |   },
342 |   async function execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
343 |     const items = this.getInputData();
344 |     const returnData: INodeExecutionData[] = [];
345 |     
346 |     for (let i = 0; i < items.length; i++) {
347 |       const resource = this.getNodeParameter('resource', i) as string;
348 |       const operation = this.getNodeParameter('operation', i) as string;
349 |       
350 |       // Mock response
351 |       const response = {
352 |         ok: true,
353 |         channel: this.getNodeParameter('channel', i, '') as string,
354 |         ts: Date.now().toString(),
355 |         message: {
356 |           text: this.getNodeParameter('text', i, '') as string,
357 |         },
358 |       };
359 |       
360 |       returnData.push({
361 |         json: response,
362 |       });
363 |     }
364 |     
365 |     return [returnData];
366 |   }
367 | );
368 | 
369 | const mockFunctionNode = new BaseMockNode(
370 |   {
371 |     displayName: 'Function',
372 |     name: 'function',
373 |     group: ['transform'],
374 |     version: 1,
375 |     description: 'Execute custom JavaScript code',
376 |     defaults: { name: 'Function' },
377 |     inputs: ['main'],
378 |     outputs: ['main'],
379 |     properties: [
380 |       {
381 |         displayName: 'JavaScript Code',
382 |         name: 'functionCode',
383 |         type: 'string',
384 |         typeOptions: {
385 |           alwaysOpenEditWindow: true,
386 |           codeAutocomplete: 'function',
387 |           editor: 'code',
388 |           rows: 10,
389 |         },
390 |         default: 'return items;',
391 |         description: 'JavaScript code to execute',
392 |       },
393 |     ],
394 |   },
395 |   async function execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
396 |     const items = this.getInputData();
397 |     const functionCode = this.getNodeParameter('functionCode', 0) as string;
398 |     
399 |     // Simple mock - just return items
400 |     return [items];
401 |   }
402 | );
403 | 
404 | const mockNoOpNode = new BaseMockNode(
405 |   {
406 |     displayName: 'No Operation',
407 |     name: 'noOp',
408 |     group: ['utility'],
409 |     version: 1,
410 |     description: 'Does nothing',
411 |     defaults: { name: 'No Op' },
412 |     inputs: ['main'],
413 |     outputs: ['main'],
414 |     properties: [],
415 |   },
416 |   async function execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
417 |     return [this.getInputData()];
418 |   }
419 | );
420 | 
421 | const mockMergeNode = new BaseMockNode(
422 |   {
423 |     displayName: 'Merge',
424 |     name: 'merge',
425 |     group: ['transform'],
426 |     version: 2,
427 |     description: 'Merge multiple data streams',
428 |     defaults: { name: 'Merge' },
429 |     inputs: ['main', 'main'],
430 |     outputs: ['main'],
431 |     properties: [
432 |       {
433 |         displayName: 'Mode',
434 |         name: 'mode',
435 |         type: 'options',
436 |         default: 'append',
437 |         options: [
438 |           { name: 'Append', value: 'append' },
439 |           { name: 'Merge By Index', value: 'mergeByIndex' },
440 |           { name: 'Merge By Key', value: 'mergeByKey' },
441 |           { name: 'Multiplex', value: 'multiplex' },
442 |         ],
443 |       },
444 |     ],
445 |   },
446 |   async function execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
447 |     const mode = this.getNodeParameter('mode', 0) as string;
448 |     
449 |     // Mock merge - just return first input
450 |     return [this.getInputData()];
451 |   }
452 | );
453 | 
454 | const mockIfNode = new BaseMockNode(
455 |   {
456 |     displayName: 'IF',
457 |     name: 'if',
458 |     group: ['transform'],
459 |     version: 1,
460 |     description: 'Conditional logic',
461 |     defaults: { name: 'IF' },
462 |     inputs: ['main'],
463 |     outputs: ['main', 'main'],
464 |     // outputNames: ['true', 'false'], // Not a valid property in INodeTypeDescription
465 |     properties: [
466 |       {
467 |         displayName: 'Conditions',
468 |         name: 'conditions',
469 |         type: 'fixedCollection',
470 |         typeOptions: {
471 |           multipleValues: true,
472 |         },
473 |         default: {},
474 |         options: [
475 |           {
476 |             name: 'string',
477 |             displayName: 'String',
478 |             values: [
479 |               {
480 |                 displayName: 'Value 1',
481 |                 name: 'value1',
482 |                 type: 'string',
483 |                 default: '',
484 |               },
485 |               {
486 |                 displayName: 'Operation',
487 |                 name: 'operation',
488 |                 type: 'options',
489 |                 default: 'equals',
490 |                 options: [
491 |                   { name: 'Equals', value: 'equals' },
492 |                   { name: 'Not Equals', value: 'notEquals' },
493 |                   { name: 'Contains', value: 'contains' },
494 |                   { name: 'Not Contains', value: 'notContains' },
495 |                 ],
496 |               },
497 |               {
498 |                 displayName: 'Value 2',
499 |                 name: 'value2',
500 |                 type: 'string',
501 |                 default: '',
502 |               },
503 |             ],
504 |           },
505 |         ],
506 |       },
507 |     ],
508 |   },
509 |   async function execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
510 |     const items = this.getInputData();
511 |     const trueItems: INodeExecutionData[] = [];
512 |     const falseItems: INodeExecutionData[] = [];
513 |     
514 |     // Mock condition - split 50/50
515 |     items.forEach((item, index) => {
516 |       if (index % 2 === 0) {
517 |         trueItems.push(item);
518 |       } else {
519 |         falseItems.push(item);
520 |       }
521 |     });
522 |     
523 |     return [trueItems, falseItems];
524 |   }
525 | );
526 | 
527 | const mockSwitchNode = new BaseMockNode(
528 |   {
529 |     displayName: 'Switch',
530 |     name: 'switch',
531 |     group: ['transform'],
532 |     version: 1,
533 |     description: 'Route items based on conditions',
534 |     defaults: { name: 'Switch' },
535 |     inputs: ['main'],
536 |     outputs: ['main', 'main', 'main', 'main'],
537 |     properties: [
538 |       {
539 |         displayName: 'Mode',
540 |         name: 'mode',
541 |         type: 'options',
542 |         default: 'expression',
543 |         options: [
544 |           { name: 'Expression', value: 'expression' },
545 |           { name: 'Rules', value: 'rules' },
546 |         ],
547 |       },
548 |       {
549 |         displayName: 'Output',
550 |         name: 'output',
551 |         type: 'options',
552 |         displayOptions: {
553 |           show: {
554 |             mode: ['expression'],
555 |           },
556 |         },
557 |         default: 'all',
558 |         options: [
559 |           { name: 'All', value: 'all' },
560 |           { name: 'First Match', value: 'firstMatch' },
561 |         ],
562 |       },
563 |     ],
564 |   },
565 |   async function execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
566 |     const items = this.getInputData();
567 |     
568 |     // Mock routing - distribute evenly across outputs
569 |     const outputs: INodeExecutionData[][] = [[], [], [], []];
570 |     items.forEach((item, index) => {
571 |       outputs[index % 4].push(item);
572 |     });
573 |     
574 |     return outputs;
575 |   }
576 | );
577 | 
578 | // Node registry
579 | const nodeRegistry = new Map<string, INodeType>([
580 |   ['webhook', mockWebhookNode],
581 |   ['httpRequest', mockHttpRequestNode],
582 |   ['slack', mockSlackNode],
583 |   ['function', mockFunctionNode],
584 |   ['noOp', mockNoOpNode],
585 |   ['merge', mockMergeNode],
586 |   ['if', mockIfNode],
587 |   ['switch', mockSwitchNode],
588 | ]);
589 | 
590 | // Export mock functions
591 | export const getNodeTypes = vi.fn(() => ({
592 |   getByName: vi.fn((name: string) => nodeRegistry.get(name)),
593 |   getByNameAndVersion: vi.fn((name: string, version: number) => nodeRegistry.get(name)),
594 | }));
595 | 
596 | // Export individual node classes for direct import
597 | export const Webhook = mockWebhookNode;
598 | export const HttpRequest = mockHttpRequestNode;
599 | export const Slack = mockSlackNode;
600 | export const Function = mockFunctionNode;
601 | export const NoOp = mockNoOpNode;
602 | export const Merge = mockMergeNode;
603 | export const If = mockIfNode;
604 | export const Switch = mockSwitchNode;
605 | 
606 | // Test utility to override node behavior
607 | export const mockNodeBehavior = (nodeName: string, overrides: Partial<INodeType>) => {
608 |   const existingNode = nodeRegistry.get(nodeName);
609 |   if (!existingNode) {
610 |     throw new Error(`Node ${nodeName} not found in registry`);
611 |   }
612 |   
613 |   const updatedNode = new BaseMockNode(
614 |     { ...existingNode.description, ...overrides.description },
615 |     overrides.execute || existingNode.execute,
616 |     overrides.webhook || existingNode.webhook
617 |   );
618 |   
619 |   nodeRegistry.set(nodeName, updatedNode);
620 |   return updatedNode;
621 | };
622 | 
623 | // Test utility to reset all mocks
624 | export const resetAllMocks = () => {
625 |   getNodeTypes.mockClear();
626 |   nodeRegistry.forEach((node) => {
627 |     if (node.execute && vi.isMockFunction(node.execute)) {
628 |       node.execute.mockClear();
629 |     }
630 |     if (node.webhook && vi.isMockFunction(node.webhook)) {
631 |       node.webhook.mockClear();
632 |     }
633 |   });
634 | };
635 | 
636 | // Test utility to add custom nodes
637 | export const registerMockNode = (name: string, node: INodeType) => {
638 |   nodeRegistry.set(name, node);
639 | };
640 | 
641 | // Export default for require() compatibility
642 | export default {
643 |   getNodeTypes,
644 |   Webhook,
645 |   HttpRequest,
646 |   Slack,
647 |   Function,
648 |   NoOp,
649 |   Merge,
650 |   If,
651 |   Switch,
652 |   mockNodeBehavior,
653 |   resetAllMocks,
654 |   registerMockNode,
655 | };
```

--------------------------------------------------------------------------------
/tests/integration/mcp-protocol/workflow-error-validation.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, it, expect, beforeEach, afterEach } 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 Workflow Error Output Validation Integration', () => {
  7 |   let mcpServer: TestableN8NMCPServer;
  8 |   let client: Client;
  9 | 
 10 |   beforeEach(async () => {
 11 |     mcpServer = new TestableN8NMCPServer();
 12 |     await mcpServer.initialize();
 13 | 
 14 |     const [serverTransport, clientTransport] = InMemoryTransport.createLinkedPair();
 15 |     await mcpServer.connectToTransport(serverTransport);
 16 | 
 17 |     client = new Client({
 18 |       name: 'test-client',
 19 |       version: '1.0.0'
 20 |     }, {
 21 |       capabilities: {}
 22 |     });
 23 | 
 24 |     await client.connect(clientTransport);
 25 |   });
 26 | 
 27 |   afterEach(async () => {
 28 |     await client.close();
 29 |     await mcpServer.close();
 30 |   });
 31 | 
 32 |   describe('validate_workflow tool - Error Output Configuration', () => {
 33 |     it('should detect incorrect error output configuration via MCP', async () => {
 34 |       const workflow = {
 35 |         nodes: [
 36 |           {
 37 |             id: '1',
 38 |             name: 'Validate Input',
 39 |             type: 'n8n-nodes-base.set',
 40 |             typeVersion: 3.4,
 41 |             position: [-400, 64],
 42 |             parameters: {}
 43 |           },
 44 |           {
 45 |             id: '2',
 46 |             name: 'Filter URLs',
 47 |             type: 'n8n-nodes-base.filter',
 48 |             typeVersion: 2.2,
 49 |             position: [-176, 64],
 50 |             parameters: {}
 51 |           },
 52 |           {
 53 |             id: '3',
 54 |             name: 'Error Response1',
 55 |             type: 'n8n-nodes-base.respondToWebhook',
 56 |             typeVersion: 1.5,
 57 |             position: [-160, 240],
 58 |             parameters: {}
 59 |           }
 60 |         ],
 61 |         connections: {
 62 |           'Validate Input': {
 63 |             main: [
 64 |               [
 65 |                 { node: 'Filter URLs', type: 'main', index: 0 },
 66 |                 { node: 'Error Response1', type: 'main', index: 0 }  // WRONG! Both in main[0]
 67 |               ]
 68 |             ]
 69 |           }
 70 |         }
 71 |       };
 72 | 
 73 |       const response = await client.callTool({
 74 |         name: 'validate_workflow',
 75 |         arguments: { workflow }
 76 |       });
 77 | 
 78 |       expect((response as any).content).toHaveLength(1);
 79 |       expect((response as any).content[0].type).toBe('text');
 80 | 
 81 |       const result = JSON.parse(((response as any).content[0]).text);
 82 | 
 83 |       expect(result.valid).toBe(false);
 84 |       expect(Array.isArray(result.errors)).toBe(true);
 85 | 
 86 |       // Check for the specific error message about incorrect configuration
 87 |       const hasIncorrectConfigError = result.errors.some((e: any) =>
 88 |         e.message.includes('Incorrect error output configuration') &&
 89 |         e.message.includes('Error Response1') &&
 90 |         e.message.includes('appear to be error handlers but are in main[0]')
 91 |       );
 92 |       expect(hasIncorrectConfigError).toBe(true);
 93 | 
 94 |       // Verify the error message includes the JSON examples
 95 |       const errorMsg = result.errors.find((e: any) =>
 96 |         e.message.includes('Incorrect error output configuration')
 97 |       );
 98 |       expect(errorMsg?.message).toContain('INCORRECT (current)');
 99 |       expect(errorMsg?.message).toContain('CORRECT (should be)');
100 |       expect(errorMsg?.message).toContain('main[1] = error output');
101 |     });
102 | 
103 |     it('should validate correct error output configuration via MCP', async () => {
104 |       const workflow = {
105 |         nodes: [
106 |           {
107 |             id: '1',
108 |             name: 'Validate Input',
109 |             type: 'n8n-nodes-base.set',
110 |             typeVersion: 3.4,
111 |             position: [-400, 64],
112 |             parameters: {},
113 |             onError: 'continueErrorOutput'
114 |           },
115 |           {
116 |             id: '2',
117 |             name: 'Filter URLs',
118 |             type: 'n8n-nodes-base.filter',
119 |             typeVersion: 2.2,
120 |             position: [-176, 64],
121 |             parameters: {}
122 |           },
123 |           {
124 |             id: '3',
125 |             name: 'Error Response1',
126 |             type: 'n8n-nodes-base.respondToWebhook',
127 |             typeVersion: 1.5,
128 |             position: [-160, 240],
129 |             parameters: {}
130 |           }
131 |         ],
132 |         connections: {
133 |           'Validate Input': {
134 |             main: [
135 |               [
136 |                 { node: 'Filter URLs', type: 'main', index: 0 }
137 |               ],
138 |               [
139 |                 { node: 'Error Response1', type: 'main', index: 0 }  // Correctly in main[1]
140 |               ]
141 |             ]
142 |           }
143 |         }
144 |       };
145 | 
146 |       const response = await client.callTool({
147 |         name: 'validate_workflow',
148 |         arguments: { workflow }
149 |       });
150 | 
151 |       expect((response as any).content).toHaveLength(1);
152 |       expect((response as any).content[0].type).toBe('text');
153 | 
154 |       const result = JSON.parse(((response as any).content[0]).text);
155 | 
156 |       // Should not have the specific error about incorrect configuration
157 |       const hasIncorrectConfigError = result.errors?.some((e: any) =>
158 |         e.message.includes('Incorrect error output configuration')
159 |       ) ?? false;
160 |       expect(hasIncorrectConfigError).toBe(false);
161 |     });
162 | 
163 |     it('should detect onError and connection mismatches via MCP', async () => {
164 |       // Test case 1: onError set but no error connections
165 |       const workflow1 = {
166 |         nodes: [
167 |           {
168 |             id: '1',
169 |             name: 'HTTP Request',
170 |             type: 'n8n-nodes-base.httpRequest',
171 |             typeVersion: 4,
172 |             position: [100, 100],
173 |             parameters: {},
174 |             onError: 'continueErrorOutput'
175 |           },
176 |           {
177 |             id: '2',
178 |             name: 'Process Data',
179 |             type: 'n8n-nodes-base.set',
180 |             position: [300, 100],
181 |             parameters: {}
182 |           }
183 |         ],
184 |         connections: {
185 |           'HTTP Request': {
186 |             main: [
187 |               [
188 |                 { node: 'Process Data', type: 'main', index: 0 }
189 |               ]
190 |             ]
191 |           }
192 |         }
193 |       };
194 | 
195 |       // Test case 2: error connections but no onError
196 |       const workflow2 = {
197 |         nodes: [
198 |           {
199 |             id: '1',
200 |             name: 'HTTP Request',
201 |             type: 'n8n-nodes-base.httpRequest',
202 |             typeVersion: 4,
203 |             position: [100, 100],
204 |             parameters: {}
205 |             // No onError property
206 |           },
207 |           {
208 |             id: '2',
209 |             name: 'Process Data',
210 |             type: 'n8n-nodes-base.set',
211 |             position: [300, 100],
212 |             parameters: {}
213 |           },
214 |           {
215 |             id: '3',
216 |             name: 'Error Handler',
217 |             type: 'n8n-nodes-base.set',
218 |             position: [300, 200],
219 |             parameters: {}
220 |           }
221 |         ],
222 |         connections: {
223 |           'HTTP Request': {
224 |             main: [
225 |               [
226 |                 { node: 'Process Data', type: 'main', index: 0 }
227 |               ],
228 |               [
229 |                 { node: 'Error Handler', type: 'main', index: 0 }
230 |               ]
231 |             ]
232 |           }
233 |         }
234 |       };
235 | 
236 |       // Test both scenarios
237 |       const workflows = [workflow1, workflow2];
238 | 
239 |       for (const workflow of workflows) {
240 |         const response = await client.callTool({
241 |           name: 'validate_workflow',
242 |           arguments: { workflow }
243 |         });
244 | 
245 |         const result = JSON.parse(((response as any).content[0]).text);
246 | 
247 |         // Should detect some kind of validation issue
248 |         expect(result).toHaveProperty('valid');
249 |         expect(Array.isArray(result.errors || [])).toBe(true);
250 |         expect(Array.isArray(result.warnings || [])).toBe(true);
251 |       }
252 |     });
253 | 
254 |     it('should handle large workflows with complex error patterns via MCP', async () => {
255 |       // Create a large workflow with multiple error handling scenarios
256 |       const nodes = [];
257 |       const connections: any = {};
258 | 
259 |       // Create 50 nodes with various error handling patterns
260 |       for (let i = 1; i <= 50; i++) {
261 |         nodes.push({
262 |           id: i.toString(),
263 |           name: `Node${i}`,
264 |           type: i % 5 === 0 ? 'n8n-nodes-base.httpRequest' : 'n8n-nodes-base.set',
265 |           typeVersion: 1,
266 |           position: [i * 100, 100],
267 |           parameters: {},
268 |           ...(i % 3 === 0 ? { onError: 'continueErrorOutput' } : {})
269 |         });
270 |       }
271 | 
272 |       // Create connections with mixed correct and incorrect error handling
273 |       for (let i = 1; i < 50; i++) {
274 |         const hasErrorHandling = i % 3 === 0;
275 |         const nextNode = `Node${i + 1}`;
276 | 
277 |         if (hasErrorHandling && i % 6 === 0) {
278 |           // Incorrect: error handler in main[0] with success node
279 |           connections[`Node${i}`] = {
280 |             main: [
281 |               [
282 |                 { node: nextNode, type: 'main', index: 0 },
283 |                 { node: 'Error Handler', type: 'main', index: 0 }  // Wrong placement
284 |               ]
285 |             ]
286 |           };
287 |         } else if (hasErrorHandling) {
288 |           // Correct: separate success and error outputs
289 |           connections[`Node${i}`] = {
290 |             main: [
291 |               [
292 |                 { node: nextNode, type: 'main', index: 0 }
293 |               ],
294 |               [
295 |                 { node: 'Error Handler', type: 'main', index: 0 }
296 |               ]
297 |             ]
298 |           };
299 |         } else {
300 |           // Normal connection
301 |           connections[`Node${i}`] = {
302 |             main: [
303 |               [
304 |                 { node: nextNode, type: 'main', index: 0 }
305 |               ]
306 |             ]
307 |           };
308 |         }
309 |       }
310 | 
311 |       // Add error handler node
312 |       nodes.push({
313 |         id: '51',
314 |         name: 'Error Handler',
315 |         type: 'n8n-nodes-base.set',
316 |         typeVersion: 1,
317 |         position: [2600, 200],
318 |         parameters: {}
319 |       });
320 | 
321 |       const workflow = { nodes, connections };
322 | 
323 |       const startTime = Date.now();
324 |       const response = await client.callTool({
325 |         name: 'validate_workflow',
326 |         arguments: { workflow }
327 |       });
328 |       const endTime = Date.now();
329 | 
330 |       // Validation should complete quickly even for large workflows
331 |       expect(endTime - startTime).toBeLessThan(5000); // Less than 5 seconds
332 | 
333 |       const result = JSON.parse(((response as any).content[0]).text);
334 | 
335 |       // Should detect the incorrect error configurations
336 |       const hasErrors = result.errors && result.errors.length > 0;
337 |       expect(hasErrors).toBe(true);
338 | 
339 |       // Specifically check for incorrect error output configuration errors
340 |       const incorrectConfigErrors = result.errors.filter((e: any) =>
341 |         e.message.includes('Incorrect error output configuration')
342 |       );
343 |       expect(incorrectConfigErrors.length).toBeGreaterThan(0);
344 |     });
345 | 
346 |     it('should handle edge cases gracefully via MCP', async () => {
347 |       const edgeCaseWorkflows = [
348 |         // Empty workflow
349 |         { nodes: [], connections: {} },
350 | 
351 |         // Single isolated node
352 |         {
353 |           nodes: [{
354 |             id: '1',
355 |             name: 'Isolated',
356 |             type: 'n8n-nodes-base.set',
357 |             position: [100, 100],
358 |             parameters: {}
359 |           }],
360 |           connections: {}
361 |         },
362 | 
363 |         // Node with null/undefined connections
364 |         {
365 |           nodes: [{
366 |             id: '1',
367 |             name: 'Source',
368 |             type: 'n8n-nodes-base.httpRequest',
369 |             position: [100, 100],
370 |             parameters: {}
371 |           }],
372 |           connections: {
373 |             'Source': {
374 |               main: [null, undefined]
375 |             }
376 |           }
377 |         }
378 |       ];
379 | 
380 |       for (const workflow of edgeCaseWorkflows) {
381 |         const response = await client.callTool({
382 |           name: 'validate_workflow',
383 |           arguments: { workflow }
384 |         });
385 | 
386 |         expect((response as any).content).toHaveLength(1);
387 |         const result = JSON.parse(((response as any).content[0]).text);
388 | 
389 |         // Should not crash and should return a valid validation result
390 |         expect(result).toHaveProperty('valid');
391 |         expect(typeof result.valid).toBe('boolean');
392 |         expect(Array.isArray(result.errors || [])).toBe(true);
393 |         expect(Array.isArray(result.warnings || [])).toBe(true);
394 |       }
395 |     });
396 | 
397 |     it('should validate with different validation profiles via MCP', async () => {
398 |       const workflow = {
399 |         nodes: [
400 |           {
401 |             id: '1',
402 |             name: 'API Call',
403 |             type: 'n8n-nodes-base.httpRequest',
404 |             position: [100, 100],
405 |             parameters: {}
406 |           },
407 |           {
408 |             id: '2',
409 |             name: 'Success Handler',
410 |             type: 'n8n-nodes-base.set',
411 |             position: [300, 100],
412 |             parameters: {}
413 |           },
414 |           {
415 |             id: '3',
416 |             name: 'Error Response',
417 |             type: 'n8n-nodes-base.respondToWebhook',
418 |             position: [300, 200],
419 |             parameters: {}
420 |           }
421 |         ],
422 |         connections: {
423 |           'API Call': {
424 |             main: [
425 |               [
426 |                 { node: 'Success Handler', type: 'main', index: 0 },
427 |                 { node: 'Error Response', type: 'main', index: 0 }  // Incorrect placement
428 |               ]
429 |             ]
430 |           }
431 |         }
432 |       };
433 | 
434 |       const profiles = ['minimal', 'runtime', 'ai-friendly', 'strict'];
435 | 
436 |       for (const profile of profiles) {
437 |         const response = await client.callTool({
438 |           name: 'validate_workflow',
439 |           arguments: {
440 |             workflow,
441 |             options: { profile }
442 |           }
443 |         });
444 | 
445 |         const result = JSON.parse(((response as any).content[0]).text);
446 | 
447 |         // All profiles should detect this error output configuration issue
448 |         const hasIncorrectConfigError = result.errors?.some((e: any) =>
449 |           e.message.includes('Incorrect error output configuration')
450 |         );
451 |         expect(hasIncorrectConfigError).toBe(true);
452 |       }
453 |     });
454 |   });
455 | 
456 |   describe('Error Message Format Consistency', () => {
457 |     it('should format error messages consistently across different scenarios', async () => {
458 |       const scenarios = [
459 |         {
460 |           name: 'Single error handler in wrong place',
461 |           workflow: {
462 |             nodes: [
463 |               { id: '1', name: 'Source', type: 'n8n-nodes-base.httpRequest', position: [0, 0], parameters: {} },
464 |               { id: '2', name: 'Success', type: 'n8n-nodes-base.set', position: [200, 0], parameters: {} },
465 |               { id: '3', name: 'Error Handler', type: 'n8n-nodes-base.set', position: [200, 100], parameters: {} }
466 |             ],
467 |             connections: {
468 |               'Source': {
469 |                 main: [[
470 |                   { node: 'Success', type: 'main', index: 0 },
471 |                   { node: 'Error Handler', type: 'main', index: 0 }
472 |                 ]]
473 |               }
474 |             }
475 |           }
476 |         },
477 |         {
478 |           name: 'Multiple error handlers in wrong place',
479 |           workflow: {
480 |             nodes: [
481 |               { id: '1', name: 'Source', type: 'n8n-nodes-base.httpRequest', position: [0, 0], parameters: {} },
482 |               { id: '2', name: 'Success', type: 'n8n-nodes-base.set', position: [200, 0], parameters: {} },
483 |               { id: '3', name: 'Error Handler 1', type: 'n8n-nodes-base.set', position: [200, 100], parameters: {} },
484 |               { id: '4', name: 'Error Handler 2', type: 'n8n-nodes-base.emailSend', position: [200, 200], parameters: {} }
485 |             ],
486 |             connections: {
487 |               'Source': {
488 |                 main: [[
489 |                   { node: 'Success', type: 'main', index: 0 },
490 |                   { node: 'Error Handler 1', type: 'main', index: 0 },
491 |                   { node: 'Error Handler 2', type: 'main', index: 0 }
492 |                 ]]
493 |               }
494 |             }
495 |           }
496 |         }
497 |       ];
498 | 
499 |       for (const scenario of scenarios) {
500 |         const response = await client.callTool({
501 |           name: 'validate_workflow',
502 |           arguments: { workflow: scenario.workflow }
503 |         });
504 | 
505 |         const result = JSON.parse(((response as any).content[0]).text);
506 | 
507 |         const errorConfigError = result.errors.find((e: any) =>
508 |           e.message.includes('Incorrect error output configuration')
509 |         );
510 | 
511 |         expect(errorConfigError).toBeDefined();
512 | 
513 |         // Check that error message follows consistent format
514 |         expect(errorConfigError.message).toContain('INCORRECT (current):');
515 |         expect(errorConfigError.message).toContain('CORRECT (should be):');
516 |         expect(errorConfigError.message).toContain('main[0] = success output');
517 |         expect(errorConfigError.message).toContain('main[1] = error output');
518 |         expect(errorConfigError.message).toContain('Also add: "onError": "continueErrorOutput"');
519 | 
520 |         // Check JSON format is valid
521 |         const incorrectSection = errorConfigError.message.match(/INCORRECT \(current\):\n([\s\S]*?)\n\nCORRECT/);
522 |         const correctSection = errorConfigError.message.match(/CORRECT \(should be\):\n([\s\S]*?)\n\nAlso add/);
523 | 
524 |         expect(incorrectSection).toBeDefined();
525 |         expect(correctSection).toBeDefined();
526 | 
527 |         // Verify JSON structure is present (but don't parse due to comments)
528 |         expect(incorrectSection).toBeDefined();
529 |         expect(correctSection).toBeDefined();
530 |         expect(incorrectSection![1]).toContain('main');
531 |         expect(correctSection![1]).toContain('main');
532 |       }
533 |     });
534 |   });
535 | });
```

--------------------------------------------------------------------------------
/tests/unit/services/workflow-validator-performance.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 | vi.mock('@/utils/logger');
  7 | 
  8 | describe('WorkflowValidator - Performance Tests', () => {
  9 |   let validator: WorkflowValidator;
 10 |   let mockNodeRepository: any;
 11 | 
 12 |   beforeEach(() => {
 13 |     vi.clearAllMocks();
 14 | 
 15 |     // Create mock repository with performance optimizations
 16 |     mockNodeRepository = {
 17 |       getNode: vi.fn((type: string) => {
 18 |         // Return mock node info for any node type to avoid database calls
 19 |         return {
 20 |           node_type: type,
 21 |           display_name: 'Mock Node',
 22 |           isVersioned: true,
 23 |           version: 1
 24 |         };
 25 |       })
 26 |     };
 27 | 
 28 |     validator = new WorkflowValidator(mockNodeRepository, EnhancedConfigValidator);
 29 |   });
 30 | 
 31 |   describe('Large Workflow Performance', () => {
 32 |     it('should validate large workflows with many error paths efficiently', async () => {
 33 |       // Generate a large workflow with 500 nodes
 34 |       const nodeCount = 500;
 35 |       const nodes = [];
 36 |       const connections: any = {};
 37 | 
 38 |       // Create nodes with various error handling patterns
 39 |       for (let i = 1; i <= nodeCount; i++) {
 40 |         nodes.push({
 41 |           id: i.toString(),
 42 |           name: `Node${i}`,
 43 |           type: i % 5 === 0 ? 'n8n-nodes-base.httpRequest' : 'n8n-nodes-base.set',
 44 |           typeVersion: 1,
 45 |           position: [i * 10, (i % 10) * 100],
 46 |           parameters: {},
 47 |           ...(i % 3 === 0 ? { onError: 'continueErrorOutput' } : {})
 48 |         });
 49 |       }
 50 | 
 51 |       // Create connections with multiple error handling scenarios
 52 |       for (let i = 1; i < nodeCount; i++) {
 53 |         const hasErrorHandling = i % 3 === 0;
 54 |         const hasMultipleConnections = i % 7 === 0;
 55 | 
 56 |         if (hasErrorHandling && hasMultipleConnections) {
 57 |           // Mix correct and incorrect error handling patterns
 58 |           const isIncorrect = i % 14 === 0;
 59 | 
 60 |           if (isIncorrect) {
 61 |             // Incorrect: error handlers mixed with success nodes in main[0]
 62 |             connections[`Node${i}`] = {
 63 |               main: [
 64 |                 [
 65 |                   { node: `Node${i + 1}`, type: 'main', index: 0 },
 66 |                   { node: `Error Handler ${i}`, type: 'main', index: 0 } // Wrong!
 67 |                 ]
 68 |               ]
 69 |             };
 70 |           } else {
 71 |             // Correct: separate success and error outputs
 72 |             connections[`Node${i}`] = {
 73 |               main: [
 74 |                 [
 75 |                   { node: `Node${i + 1}`, type: 'main', index: 0 }
 76 |                 ],
 77 |                 [
 78 |                   { node: `Error Handler ${i}`, type: 'main', index: 0 }
 79 |                 ]
 80 |               ]
 81 |             };
 82 |           }
 83 | 
 84 |           // Add error handler node
 85 |           nodes.push({
 86 |             id: `error-${i}`,
 87 |             name: `Error Handler ${i}`,
 88 |             type: 'n8n-nodes-base.respondToWebhook',
 89 |             typeVersion: 1,
 90 |             position: [(i + nodeCount) * 10, 500],
 91 |             parameters: {}
 92 |           });
 93 |         } else {
 94 |           // Simple connection
 95 |           connections[`Node${i}`] = {
 96 |             main: [
 97 |               [
 98 |                 { node: `Node${i + 1}`, type: 'main', index: 0 }
 99 |               ]
100 |             ]
101 |           };
102 |         }
103 |       }
104 | 
105 |       const workflow = { nodes, connections };
106 | 
107 |       const startTime = performance.now();
108 |       const result = await validator.validateWorkflow(workflow as any);
109 |       const endTime = performance.now();
110 | 
111 |       const executionTime = endTime - startTime;
112 | 
113 |       // Validation should complete within reasonable time
114 |       expect(executionTime).toBeLessThan(10000); // Less than 10 seconds
115 | 
116 |       // Should still catch validation errors
117 |       expect(Array.isArray(result.errors)).toBe(true);
118 |       expect(Array.isArray(result.warnings)).toBe(true);
119 | 
120 |       // Should detect incorrect error configurations
121 |       const incorrectConfigErrors = result.errors.filter(e =>
122 |         e.message.includes('Incorrect error output configuration')
123 |       );
124 |       expect(incorrectConfigErrors.length).toBeGreaterThan(0);
125 | 
126 |       console.log(`Validated ${nodes.length} nodes in ${executionTime.toFixed(2)}ms`);
127 |       console.log(`Found ${result.errors.length} errors and ${result.warnings.length} warnings`);
128 |     });
129 | 
130 |     it('should handle deeply nested error handling chains efficiently', async () => {
131 |       // Create a chain of error handlers, each with their own error handling
132 |       const chainLength = 100;
133 |       const nodes = [];
134 |       const connections: any = {};
135 | 
136 |       for (let i = 1; i <= chainLength; i++) {
137 |         // Main processing node
138 |         nodes.push({
139 |           id: `main-${i}`,
140 |           name: `Main ${i}`,
141 |           type: 'n8n-nodes-base.httpRequest',
142 |           typeVersion: 1,
143 |           position: [i * 150, 100],
144 |           parameters: {},
145 |           onError: 'continueErrorOutput'
146 |         });
147 | 
148 |         // Error handler node
149 |         nodes.push({
150 |           id: `error-${i}`,
151 |           name: `Error Handler ${i}`,
152 |           type: 'n8n-nodes-base.httpRequest',
153 |           typeVersion: 1,
154 |           position: [i * 150, 300],
155 |           parameters: {},
156 |           onError: 'continueErrorOutput'
157 |         });
158 | 
159 |         // Fallback error node
160 |         nodes.push({
161 |           id: `fallback-${i}`,
162 |           name: `Fallback ${i}`,
163 |           type: 'n8n-nodes-base.set',
164 |           typeVersion: 1,
165 |           position: [i * 150, 500],
166 |           parameters: {}
167 |         });
168 | 
169 |         // Connections
170 |         connections[`Main ${i}`] = {
171 |           main: [
172 |             // Success path
173 |             i < chainLength ? [{ node: `Main ${i + 1}`, type: 'main', index: 0 }] : [],
174 |             // Error path
175 |             [{ node: `Error Handler ${i}`, type: 'main', index: 0 }]
176 |           ]
177 |         };
178 | 
179 |         connections[`Error Handler ${i}`] = {
180 |           main: [
181 |             // Success path (continue to next error handler or end)
182 |             [],
183 |             // Error path (go to fallback)
184 |             [{ node: `Fallback ${i}`, type: 'main', index: 0 }]
185 |           ]
186 |         };
187 |       }
188 | 
189 |       const workflow = { nodes, connections };
190 | 
191 |       const startTime = performance.now();
192 |       const result = await validator.validateWorkflow(workflow as any);
193 |       const endTime = performance.now();
194 | 
195 |       const executionTime = endTime - startTime;
196 | 
197 |       // Should complete quickly even with complex nested error handling
198 |       expect(executionTime).toBeLessThan(5000); // Less than 5 seconds
199 | 
200 |       // Should not have errors about incorrect configuration (this is correct)
201 |       const incorrectConfigErrors = result.errors.filter(e =>
202 |         e.message.includes('Incorrect error output configuration')
203 |       );
204 |       expect(incorrectConfigErrors.length).toBe(0);
205 | 
206 |       console.log(`Validated ${nodes.length} nodes with nested error handling in ${executionTime.toFixed(2)}ms`);
207 |     });
208 | 
209 |     it('should efficiently validate workflows with many parallel error paths', async () => {
210 |       // Create a workflow with one source node that fans out to many parallel paths,
211 |       // each with their own error handling
212 |       const parallelPathCount = 200;
213 |       const nodes = [
214 |         {
215 |           id: 'source',
216 |           name: 'Source',
217 |           type: 'n8n-nodes-base.webhook',
218 |           typeVersion: 1,
219 |           position: [0, 0],
220 |           parameters: {}
221 |         }
222 |       ];
223 |       const connections: any = {
224 |         'Source': {
225 |           main: [[]]
226 |         }
227 |       };
228 | 
229 |       // Create parallel paths
230 |       for (let i = 1; i <= parallelPathCount; i++) {
231 |         // Processing node
232 |         nodes.push({
233 |           id: `process-${i}`,
234 |           name: `Process ${i}`,
235 |           type: 'n8n-nodes-base.httpRequest',
236 |           typeVersion: 1,
237 |           position: [200, i * 20],
238 |           parameters: {},
239 |           onError: 'continueErrorOutput'
240 |         } as any);
241 | 
242 |         // Success handler
243 |         nodes.push({
244 |           id: `success-${i}`,
245 |           name: `Success ${i}`,
246 |           type: 'n8n-nodes-base.set',
247 |           typeVersion: 1,
248 |           position: [400, i * 20],
249 |           parameters: {}
250 |         });
251 | 
252 |         // Error handler
253 |         nodes.push({
254 |           id: `error-${i}`,
255 |           name: `Error Handler ${i}`,
256 |           type: 'n8n-nodes-base.respondToWebhook',
257 |           typeVersion: 1,
258 |           position: [400, i * 20 + 10],
259 |           parameters: {}
260 |         });
261 | 
262 |         // Connect source to processing node
263 |         connections['Source'].main[0].push({
264 |           node: `Process ${i}`,
265 |           type: 'main',
266 |           index: 0
267 |         });
268 | 
269 |         // Connect processing node to success and error handlers
270 |         connections[`Process ${i}`] = {
271 |           main: [
272 |             [{ node: `Success ${i}`, type: 'main', index: 0 }],
273 |             [{ node: `Error Handler ${i}`, type: 'main', index: 0 }]
274 |           ]
275 |         };
276 |       }
277 | 
278 |       const workflow = { nodes, connections };
279 | 
280 |       const startTime = performance.now();
281 |       const result = await validator.validateWorkflow(workflow as any);
282 |       const endTime = performance.now();
283 | 
284 |       const executionTime = endTime - startTime;
285 | 
286 |       // Should validate efficiently despite many parallel paths
287 |       expect(executionTime).toBeLessThan(8000); // Less than 8 seconds
288 | 
289 |       // Should not have errors about incorrect configuration
290 |       const incorrectConfigErrors = result.errors.filter(e =>
291 |         e.message.includes('Incorrect error output configuration')
292 |       );
293 |       expect(incorrectConfigErrors.length).toBe(0);
294 | 
295 |       console.log(`Validated ${nodes.length} nodes with ${parallelPathCount} parallel error paths in ${executionTime.toFixed(2)}ms`);
296 |     });
297 | 
298 |     it('should handle worst-case scenario with many incorrect configurations efficiently', async () => {
299 |       // Create a workflow where many nodes have the incorrect error configuration
300 |       // This tests the performance of the error detection algorithm
301 |       const nodeCount = 300;
302 |       const nodes = [];
303 |       const connections: any = {};
304 | 
305 |       for (let i = 1; i <= nodeCount; i++) {
306 |         // Main node
307 |         nodes.push({
308 |           id: `main-${i}`,
309 |           name: `Main ${i}`,
310 |           type: 'n8n-nodes-base.httpRequest',
311 |           typeVersion: 1,
312 |           position: [i * 20, 100],
313 |           parameters: {}
314 |         });
315 | 
316 |         // Success handler
317 |         nodes.push({
318 |           id: `success-${i}`,
319 |           name: `Success ${i}`,
320 |           type: 'n8n-nodes-base.set',
321 |           typeVersion: 1,
322 |           position: [i * 20, 200],
323 |           parameters: {}
324 |         });
325 | 
326 |         // Error handler (with error-indicating name)
327 |         nodes.push({
328 |           id: `error-${i}`,
329 |           name: `Error Handler ${i}`,
330 |           type: 'n8n-nodes-base.respondToWebhook',
331 |           typeVersion: 1,
332 |           position: [i * 20, 300],
333 |           parameters: {}
334 |         });
335 | 
336 |         // INCORRECT configuration: both success and error handlers in main[0]
337 |         connections[`Main ${i}`] = {
338 |           main: [
339 |             [
340 |               { node: `Success ${i}`, type: 'main', index: 0 },
341 |               { node: `Error Handler ${i}`, type: 'main', index: 0 } // Wrong!
342 |             ]
343 |           ]
344 |         };
345 |       }
346 | 
347 |       const workflow = { nodes, connections };
348 | 
349 |       const startTime = performance.now();
350 |       const result = await validator.validateWorkflow(workflow as any);
351 |       const endTime = performance.now();
352 | 
353 |       const executionTime = endTime - startTime;
354 | 
355 |       // Should complete within reasonable time even when generating many errors
356 |       expect(executionTime).toBeLessThan(15000); // Less than 15 seconds
357 | 
358 |       // Should detect ALL incorrect configurations
359 |       const incorrectConfigErrors = result.errors.filter(e =>
360 |         e.message.includes('Incorrect error output configuration')
361 |       );
362 |       expect(incorrectConfigErrors.length).toBe(nodeCount); // One error per node
363 | 
364 |       console.log(`Detected ${incorrectConfigErrors.length} incorrect configurations in ${nodes.length} nodes in ${executionTime.toFixed(2)}ms`);
365 |     });
366 |   });
367 | 
368 |   describe('Memory Usage and Optimization', () => {
369 |     it('should not leak memory during large workflow validation', async () => {
370 |       // Get initial memory usage
371 |       const initialMemory = process.memoryUsage().heapUsed;
372 | 
373 |       // Validate multiple large workflows
374 |       for (let run = 0; run < 5; run++) {
375 |         const nodeCount = 200;
376 |         const nodes = [];
377 |         const connections: any = {};
378 | 
379 |         for (let i = 1; i <= nodeCount; i++) {
380 |           nodes.push({
381 |             id: i.toString(),
382 |             name: `Node${i}`,
383 |             type: 'n8n-nodes-base.httpRequest',
384 |             typeVersion: 1,
385 |             position: [i * 10, 100],
386 |             parameters: {},
387 |             onError: 'continueErrorOutput'
388 |           });
389 | 
390 |           if (i > 1) {
391 |             connections[`Node${i - 1}`] = {
392 |               main: [
393 |                 [{ node: `Node${i}`, type: 'main', index: 0 }],
394 |                 [{ node: `Error${i}`, type: 'main', index: 0 }]
395 |               ]
396 |             };
397 | 
398 |             nodes.push({
399 |               id: `error-${i}`,
400 |               name: `Error${i}`,
401 |               type: 'n8n-nodes-base.set',
402 |               typeVersion: 1,
403 |               position: [i * 10, 200],
404 |               parameters: {}
405 |             });
406 |           }
407 |         }
408 | 
409 |         const workflow = { nodes, connections };
410 |         await validator.validateWorkflow(workflow as any);
411 | 
412 |         // Force garbage collection if available
413 |         if (global.gc) {
414 |           global.gc();
415 |         }
416 |       }
417 | 
418 |       const finalMemory = process.memoryUsage().heapUsed;
419 |       const memoryIncrease = finalMemory - initialMemory;
420 |       const memoryIncreaseMB = memoryIncrease / (1024 * 1024);
421 | 
422 |       // Memory increase should be reasonable (less than 50MB)
423 |       expect(memoryIncreaseMB).toBeLessThan(50);
424 | 
425 |       console.log(`Memory increase after 5 large workflow validations: ${memoryIncreaseMB.toFixed(2)}MB`);
426 |     });
427 | 
428 |     it('should handle concurrent validation requests efficiently', async () => {
429 |       // Create multiple validation requests that run concurrently
430 |       const concurrentRequests = 10;
431 |       const workflows = [];
432 | 
433 |       // Prepare workflows
434 |       for (let r = 0; r < concurrentRequests; r++) {
435 |         const nodeCount = 50;
436 |         const nodes = [];
437 |         const connections: any = {};
438 | 
439 |         for (let i = 1; i <= nodeCount; i++) {
440 |           nodes.push({
441 |             id: `${r}-${i}`,
442 |             name: `R${r}Node${i}`,
443 |             type: i % 2 === 0 ? 'n8n-nodes-base.httpRequest' : 'n8n-nodes-base.set',
444 |             typeVersion: 1,
445 |             position: [i * 20, r * 100],
446 |             parameters: {},
447 |             ...(i % 3 === 0 ? { onError: 'continueErrorOutput' } : {})
448 |           });
449 | 
450 |           if (i > 1) {
451 |             const hasError = i % 3 === 0;
452 |             const isIncorrect = i % 6 === 0;
453 | 
454 |             if (hasError && isIncorrect) {
455 |               // Incorrect configuration
456 |               connections[`R${r}Node${i - 1}`] = {
457 |                 main: [
458 |                   [
459 |                     { node: `R${r}Node${i}`, type: 'main', index: 0 },
460 |                     { node: `R${r}Error${i}`, type: 'main', index: 0 } // Wrong!
461 |                   ]
462 |                 ]
463 |               };
464 | 
465 |               nodes.push({
466 |                 id: `${r}-error-${i}`,
467 |                 name: `R${r}Error${i}`,
468 |                 type: 'n8n-nodes-base.respondToWebhook',
469 |                 typeVersion: 1,
470 |                 position: [i * 20, r * 100 + 50],
471 |                 parameters: {}
472 |               });
473 |             } else if (hasError) {
474 |               // Correct configuration
475 |               connections[`R${r}Node${i - 1}`] = {
476 |                 main: [
477 |                   [{ node: `R${r}Node${i}`, type: 'main', index: 0 }],
478 |                   [{ node: `R${r}Error${i}`, type: 'main', index: 0 }]
479 |                 ]
480 |               };
481 | 
482 |               nodes.push({
483 |                 id: `${r}-error-${i}`,
484 |                 name: `R${r}Error${i}`,
485 |                 type: 'n8n-nodes-base.set',
486 |                 typeVersion: 1,
487 |                 position: [i * 20, r * 100 + 50],
488 |                 parameters: {}
489 |               });
490 |             } else {
491 |               // Normal connection
492 |               connections[`R${r}Node${i - 1}`] = {
493 |                 main: [
494 |                   [{ node: `R${r}Node${i}`, type: 'main', index: 0 }]
495 |                 ]
496 |               };
497 |             }
498 |           }
499 |         }
500 | 
501 |         workflows.push({ nodes, connections });
502 |       }
503 | 
504 |       // Run concurrent validations
505 |       const startTime = performance.now();
506 |       const results = await Promise.all(
507 |         workflows.map(workflow => validator.validateWorkflow(workflow as any))
508 |       );
509 |       const endTime = performance.now();
510 | 
511 |       const totalTime = endTime - startTime;
512 | 
513 |       // All validations should complete
514 |       expect(results).toHaveLength(concurrentRequests);
515 | 
516 |       // Each result should be valid
517 |       results.forEach(result => {
518 |         expect(Array.isArray(result.errors)).toBe(true);
519 |         expect(Array.isArray(result.warnings)).toBe(true);
520 |       });
521 | 
522 |       // Concurrent execution should be efficient
523 |       expect(totalTime).toBeLessThan(20000); // Less than 20 seconds total
524 | 
525 |       console.log(`Completed ${concurrentRequests} concurrent validations in ${totalTime.toFixed(2)}ms`);
526 |     });
527 |   });
528 | });
```

--------------------------------------------------------------------------------
/src/mcp/tools-documentation.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { toolsDocumentation } from './tool-docs';
  2 | 
  3 | export function getToolDocumentation(toolName: string, depth: 'essentials' | 'full' = 'essentials'): string {
  4 |   // Check for special documentation topics
  5 |   if (toolName === 'javascript_code_node_guide') {
  6 |     return getJavaScriptCodeNodeGuide(depth);
  7 |   }
  8 |   if (toolName === 'python_code_node_guide') {
  9 |     return getPythonCodeNodeGuide(depth);
 10 |   }
 11 |   
 12 |   const tool = toolsDocumentation[toolName];
 13 |   if (!tool) {
 14 |     return `Tool '${toolName}' not found. Use tools_documentation() to see available tools.`;
 15 |   }
 16 | 
 17 |   if (depth === 'essentials') {
 18 |     const { essentials } = tool;
 19 |     return `# ${tool.name}
 20 | 
 21 | ${essentials.description}
 22 | 
 23 | **Example**: ${essentials.example}
 24 | 
 25 | **Key parameters**: ${essentials.keyParameters.join(', ')}
 26 | 
 27 | **Performance**: ${essentials.performance}
 28 | 
 29 | **Tips**:
 30 | ${essentials.tips.map(tip => `- ${tip}`).join('\n')}
 31 | 
 32 | For full documentation, use: tools_documentation({topic: "${toolName}", depth: "full"})`;
 33 |   }
 34 | 
 35 |   // Full documentation
 36 |   const { full } = tool;
 37 |   return `# ${tool.name}
 38 | 
 39 | ${full.description}
 40 | 
 41 | ## Parameters
 42 | ${Object.entries(full.parameters).map(([param, info]) => 
 43 |   `- **${param}** (${info.type}${info.required ? ', required' : ''}): ${info.description}`
 44 | ).join('\n')}
 45 | 
 46 | ## Returns
 47 | ${full.returns}
 48 | 
 49 | ## Examples
 50 | ${full.examples.map(ex => `\`\`\`javascript\n${ex}\n\`\`\``).join('\n\n')}
 51 | 
 52 | ## Common Use Cases
 53 | ${full.useCases.map(uc => `- ${uc}`).join('\n')}
 54 | 
 55 | ## Performance
 56 | ${full.performance}
 57 | 
 58 | ## Best Practices
 59 | ${full.bestPractices.map(bp => `- ${bp}`).join('\n')}
 60 | 
 61 | ## Common Pitfalls
 62 | ${full.pitfalls.map(p => `- ${p}`).join('\n')}
 63 | 
 64 | ## Related Tools
 65 | ${full.relatedTools.map(t => `- ${t}`).join('\n')}`;
 66 | }
 67 | 
 68 | export function getToolsOverview(depth: 'essentials' | 'full' = 'essentials'): string {
 69 |   // Get version info from package.json
 70 |   const packageJson = require('../../package.json');
 71 |   const supportedN8nVersion = packageJson.dependencies?.n8n?.replace(/[^0-9.]/g, '') || 'latest';
 72 |   
 73 |   if (depth === 'essentials') {
 74 |     return `# n8n MCP Tools Reference
 75 | 
 76 | ## Important: Compatibility Notice
 77 | ⚠️ This MCP server is tested with n8n version ${supportedN8nVersion}. 
 78 | Inform the user to check their n8n version matches or is compatible with the supported version listed above.
 79 | 
 80 | ## Code Node Configuration
 81 | When working with Code nodes, always start by calling the relevant guide:
 82 | - tools_documentation({topic: "javascript_code_node_guide"}) for JavaScript Code nodes
 83 | - tools_documentation({topic: "python_code_node_guide"}) for Python Code nodes
 84 | 
 85 | ## Standard Workflow Pattern
 86 | 
 87 | 1. **Find** the node you need:
 88 |    - search_nodes({query: "slack"}) - Search by keyword
 89 |    - list_nodes({category: "communication"}) - List by category
 90 |    - list_ai_tools() - List AI-capable nodes
 91 | 
 92 | 2. **Configure** the node:
 93 |    - get_node_essentials("nodes-base.slack") - Get essential properties only (5KB)
 94 |    - get_node_info("nodes-base.slack") - Get complete schema (100KB+)
 95 |    - search_node_properties("nodes-base.slack", "auth") - Find specific properties
 96 | 
 97 | 3. **Validate** before deployment:
 98 |    - validate_node_minimal("nodes-base.slack", config) - Check required fields
 99 |    - validate_node_operation("nodes-base.slack", config) - Full validation with fixes
100 |    - validate_workflow(workflow) - Validate entire workflow
101 | 
102 | ## Tool Categories
103 | 
104 | **Discovery Tools**
105 | - search_nodes - Full-text search across all nodes
106 | - list_nodes - List nodes with filtering by category, package, or type
107 | - list_ai_tools - List all AI-capable nodes with usage guidance
108 | 
109 | **Configuration Tools**
110 | - get_node_essentials - Returns 10-20 key properties with examples
111 | - get_node_info - Returns complete node schema with all properties
112 | - search_node_properties - Search for specific properties within a node
113 | - get_property_dependencies - Analyze property visibility dependencies
114 | 
115 | **Validation Tools**
116 | - validate_node_minimal - Quick validation of required fields only
117 | - validate_node_operation - Full validation with operation awareness
118 | - validate_workflow - Complete workflow validation including connections
119 | 
120 | **Template Tools**
121 | - list_tasks - List common task templates
122 | - get_node_for_task - Get pre-configured node for specific tasks
123 | - search_templates - Search workflow templates by keyword
124 | - get_template - Get complete workflow JSON by ID
125 | 
126 | **n8n API Tools** (requires N8N_API_URL configuration)
127 | - n8n_create_workflow - Create new workflows
128 | - n8n_update_partial_workflow - Update workflows using diff operations
129 | - n8n_validate_workflow - Validate workflow from n8n instance
130 | - n8n_trigger_webhook_workflow - Trigger workflow execution
131 | 
132 | ## Performance Characteristics
133 | - Instant (<10ms): search_nodes, list_nodes, get_node_essentials
134 | - Fast (<100ms): validate_node_minimal, get_node_for_task
135 | - Moderate (100-500ms): validate_workflow, get_node_info
136 | - Network-dependent: All n8n_* tools
137 | 
138 | For comprehensive documentation on any tool:
139 | tools_documentation({topic: "tool_name", depth: "full"})`;
140 |   }
141 | 
142 |   const categories = getAllCategories();
143 |   return `# n8n MCP Tools - Complete Reference
144 | 
145 | ## Important: Compatibility Notice
146 | ⚠️ This MCP server is tested with n8n version ${supportedN8nVersion}. 
147 | Run n8n_health_check() to verify your n8n instance compatibility and API connectivity.
148 | 
149 | ## Code Node Guides
150 | For Code node configuration, use these comprehensive guides:
151 | - tools_documentation({topic: "javascript_code_node_guide", depth: "full"}) - JavaScript patterns, n8n variables, error handling
152 | - tools_documentation({topic: "python_code_node_guide", depth: "full"}) - Python patterns, data access, debugging
153 | 
154 | ## All Available Tools by Category
155 | 
156 | ${categories.map(cat => {
157 |   const tools = getToolsByCategory(cat);
158 |   const categoryName = cat.charAt(0).toUpperCase() + cat.slice(1).replace('_', ' ');
159 |   return `### ${categoryName}
160 | ${tools.map(toolName => {
161 |   const tool = toolsDocumentation[toolName];
162 |   return `- **${toolName}**: ${tool.essentials.description}`;
163 | }).join('\n')}`;
164 | }).join('\n\n')}
165 | 
166 | ## Usage Notes
167 | - All node types require the "nodes-base." or "nodes-langchain." prefix
168 | - Use get_node_essentials() first for most tasks (95% smaller than get_node_info)
169 | - Validation profiles: minimal (editing), runtime (default), strict (deployment)
170 | - n8n API tools only available when N8N_API_URL and N8N_API_KEY are configured
171 | 
172 | For detailed documentation on any tool:
173 | tools_documentation({topic: "tool_name", depth: "full"})`;
174 | }
175 | 
176 | export function searchToolDocumentation(keyword: string): string[] {
177 |   const results: string[] = [];
178 |   
179 |   for (const [toolName, tool] of Object.entries(toolsDocumentation)) {
180 |     const searchText = `${toolName} ${tool.essentials.description} ${tool.full.description}`.toLowerCase();
181 |     if (searchText.includes(keyword.toLowerCase())) {
182 |       results.push(toolName);
183 |     }
184 |   }
185 |   
186 |   return results;
187 | }
188 | 
189 | export function getToolsByCategory(category: string): string[] {
190 |   return Object.entries(toolsDocumentation)
191 |     .filter(([_, tool]) => tool.category === category)
192 |     .map(([name, _]) => name);
193 | }
194 | 
195 | export function getAllCategories(): string[] {
196 |   const categories = new Set<string>();
197 |   Object.values(toolsDocumentation).forEach(tool => {
198 |     categories.add(tool.category);
199 |   });
200 |   return Array.from(categories);
201 | }
202 | 
203 | // Special documentation topics
204 | function getJavaScriptCodeNodeGuide(depth: 'essentials' | 'full' = 'essentials'): string {
205 |   if (depth === 'essentials') {
206 |     return `# JavaScript Code Node Guide
207 | 
208 | Essential patterns for JavaScript in n8n Code nodes.
209 | 
210 | **Key Concepts**:
211 | - Access all items: \`$input.all()\` (not items[0])
212 | - Current item data: \`$json\`
213 | - Return format: \`[{json: {...}}]\` (array of objects)
214 | 
215 | **Available Helpers**:
216 | - \`$helpers.httpRequest()\` - Make HTTP requests
217 | - \`$jmespath()\` - Query JSON data
218 | - \`DateTime\` - Luxon for date handling
219 | 
220 | **Common Patterns**:
221 | \`\`\`javascript
222 | // Process all items
223 | const allItems = $input.all();
224 | return allItems.map(item => ({
225 |   json: {
226 |     processed: true,
227 |     original: item.json,
228 |     timestamp: DateTime.now().toISO()
229 |   }
230 | }));
231 | \`\`\`
232 | 
233 | **Tips**:
234 | - Webhook data is under \`.body\` property
235 | - Use async/await for HTTP requests
236 | - Always return array format
237 | 
238 | For full guide: tools_documentation({topic: "javascript_code_node_guide", depth: "full"})`;
239 |   }
240 | 
241 |   // Full documentation
242 |   return `# JavaScript Code Node Complete Guide
243 | 
244 | Comprehensive guide for using JavaScript in n8n Code nodes.
245 | 
246 | ## Data Access Patterns
247 | 
248 | ### Accessing Input Data
249 | \`\`\`javascript
250 | // Get all items from previous node
251 | const allItems = $input.all();
252 | 
253 | // Get specific node's output
254 | const webhookData = $node["Webhook"].json;
255 | 
256 | // Current item in loop
257 | const currentItem = $json;
258 | 
259 | // First item only
260 | const firstItem = $input.first().json;
261 | \`\`\`
262 | 
263 | ### Webhook Data Structure
264 | **CRITICAL**: Webhook data is nested under \`.body\`:
265 | \`\`\`javascript
266 | // WRONG - Won't work
267 | const data = $json.name;
268 | 
269 | // CORRECT - Webhook data is under body
270 | const data = $json.body.name;
271 | \`\`\`
272 | 
273 | ## Available Built-in Functions
274 | 
275 | ### HTTP Requests
276 | \`\`\`javascript
277 | // Make HTTP request
278 | const response = await $helpers.httpRequest({
279 |   method: 'GET',
280 |   url: 'https://api.example.com/data',
281 |   headers: {
282 |     'Authorization': 'Bearer token'
283 |   }
284 | });
285 | \`\`\`
286 | 
287 | ### Date/Time Handling
288 | \`\`\`javascript
289 | // Using Luxon DateTime
290 | const now = DateTime.now();
291 | const formatted = now.toFormat('yyyy-MM-dd');
292 | const iso = now.toISO();
293 | const plus5Days = now.plus({ days: 5 });
294 | \`\`\`
295 | 
296 | ### JSON Querying
297 | \`\`\`javascript
298 | // JMESPath queries
299 | const result = $jmespath($json, "users[?age > 30].name");
300 | \`\`\`
301 | 
302 | ## Return Format Requirements
303 | 
304 | ### Correct Format
305 | \`\`\`javascript
306 | // MUST return array of objects with json property
307 | return [{
308 |   json: {
309 |     result: "success",
310 |     data: processedData
311 |   }
312 | }];
313 | 
314 | // Multiple items
315 | return items.map(item => ({
316 |   json: {
317 |     id: item.id,
318 |     processed: true
319 |   }
320 | }));
321 | \`\`\`
322 | 
323 | ### Binary Data
324 | \`\`\`javascript
325 | // Return with binary data
326 | return [{
327 |   json: { filename: "report.pdf" },
328 |   binary: {
329 |     data: Buffer.from(pdfContent).toString('base64')
330 |   }
331 | }];
332 | \`\`\`
333 | 
334 | ## Common Patterns
335 | 
336 | ### Processing Webhook Data
337 | \`\`\`javascript
338 | // Extract webhook payload
339 | const webhookBody = $json.body;
340 | const { username, email, items } = webhookBody;
341 | 
342 | // Process and return
343 | return [{
344 |   json: {
345 |     username,
346 |     email,
347 |     itemCount: items.length,
348 |     processedAt: DateTime.now().toISO()
349 |   }
350 | }];
351 | \`\`\`
352 | 
353 | ### Aggregating Data
354 | \`\`\`javascript
355 | // Sum values across all items
356 | const allItems = $input.all();
357 | const total = allItems.reduce((sum, item) => {
358 |   return sum + (item.json.amount || 0);
359 | }, 0);
360 | 
361 | return [{
362 |   json: { 
363 |     total,
364 |     itemCount: allItems.length,
365 |     average: total / allItems.length
366 |   }
367 | }];
368 | \`\`\`
369 | 
370 | ### Error Handling
371 | \`\`\`javascript
372 | try {
373 |   const response = await $helpers.httpRequest({
374 |     url: 'https://api.example.com/data'
375 |   });
376 |   
377 |   return [{
378 |     json: {
379 |       success: true,
380 |       data: response
381 |     }
382 |   }];
383 | } catch (error) {
384 |   return [{
385 |     json: {
386 |       success: false,
387 |       error: error.message
388 |     }
389 |   }];
390 | }
391 | \`\`\`
392 | 
393 | ## Available Node.js Modules
394 | - crypto (built-in)
395 | - Buffer
396 | - URL/URLSearchParams
397 | - Basic Node.js globals
398 | 
399 | ## Common Pitfalls
400 | 1. Using \`items[0]\` instead of \`$input.all()\`
401 | 2. Forgetting webhook data is under \`.body\`
402 | 3. Returning plain objects instead of \`[{json: {...}}]\`
403 | 4. Using \`require()\` for external modules (not allowed)
404 | 5. Trying to use expression syntax \`{{}}\` inside code
405 | 
406 | ## Best Practices
407 | 1. Always validate input data exists before accessing
408 | 2. Use try-catch for HTTP requests
409 | 3. Return early on validation failures
410 | 4. Keep code simple and readable
411 | 5. Use descriptive variable names
412 | 
413 | ## Related Tools
414 | - get_node_essentials("nodes-base.code")
415 | - validate_node_operation()
416 | - python_code_node_guide (for Python syntax)`;
417 | }
418 | 
419 | function getPythonCodeNodeGuide(depth: 'essentials' | 'full' = 'essentials'): string {
420 |   if (depth === 'essentials') {
421 |     return `# Python Code Node Guide
422 | 
423 | Essential patterns for Python in n8n Code nodes.
424 | 
425 | **Key Concepts**:
426 | - Access all items: \`_input.all()\` (not items[0])
427 | - Current item data: \`_json\`
428 | - Return format: \`[{"json": {...}}]\` (list of dicts)
429 | 
430 | **Limitations**:
431 | - No external libraries (no requests, pandas, numpy)
432 | - Use built-in functions only
433 | - No pip install available
434 | 
435 | **Common Patterns**:
436 | \`\`\`python
437 | # Process all items
438 | all_items = _input.all()
439 | return [{
440 |     "json": {
441 |         "processed": True,
442 |         "count": len(all_items),
443 |         "first_item": all_items[0]["json"] if all_items else None
444 |     }
445 | }]
446 | \`\`\`
447 | 
448 | **Tips**:
449 | - Webhook data is under ["body"] key
450 | - Use json module for parsing
451 | - datetime for date handling
452 | 
453 | For full guide: tools_documentation({topic: "python_code_node_guide", depth: "full"})`;
454 |   }
455 | 
456 |   // Full documentation
457 |   return `# Python Code Node Complete Guide
458 | 
459 | Comprehensive guide for using Python in n8n Code nodes.
460 | 
461 | ## Data Access Patterns
462 | 
463 | ### Accessing Input Data
464 | \`\`\`python
465 | # Get all items from previous node
466 | all_items = _input.all()
467 | 
468 | # Get specific node's output (use _node)
469 | webhook_data = _node["Webhook"]["json"]
470 | 
471 | # Current item in loop
472 | current_item = _json
473 | 
474 | # First item only
475 | first_item = _input.first()["json"]
476 | \`\`\`
477 | 
478 | ### Webhook Data Structure
479 | **CRITICAL**: Webhook data is nested under ["body"]:
480 | \`\`\`python
481 | # WRONG - Won't work
482 | data = _json["name"]
483 | 
484 | # CORRECT - Webhook data is under body
485 | data = _json["body"]["name"]
486 | \`\`\`
487 | 
488 | ## Available Built-in Modules
489 | 
490 | ### Standard Library Only
491 | \`\`\`python
492 | import json
493 | import datetime
494 | import base64
495 | import hashlib
496 | import urllib.parse
497 | import re
498 | import math
499 | import random
500 | \`\`\`
501 | 
502 | ### Date/Time Handling
503 | \`\`\`python
504 | from datetime import datetime, timedelta
505 | 
506 | # Current time
507 | now = datetime.now()
508 | iso_format = now.isoformat()
509 | 
510 | # Date arithmetic
511 | future = now + timedelta(days=5)
512 | formatted = now.strftime("%Y-%m-%d")
513 | \`\`\`
514 | 
515 | ### JSON Operations
516 | \`\`\`python
517 | # Parse JSON string
518 | data = json.loads(json_string)
519 | 
520 | # Convert to JSON
521 | json_output = json.dumps({"key": "value"})
522 | \`\`\`
523 | 
524 | ## Return Format Requirements
525 | 
526 | ### Correct Format
527 | \`\`\`python
528 | # MUST return list of dictionaries with "json" key
529 | return [{
530 |     "json": {
531 |         "result": "success",
532 |         "data": processed_data
533 |     }
534 | }]
535 | 
536 | # Multiple items
537 | return [
538 |     {"json": {"id": item["json"]["id"], "processed": True}}
539 |     for item in all_items
540 | ]
541 | \`\`\`
542 | 
543 | ### Binary Data
544 | \`\`\`python
545 | # Return with binary data
546 | import base64
547 | 
548 | return [{
549 |     "json": {"filename": "report.pdf"},
550 |     "binary": {
551 |         "data": base64.b64encode(pdf_content).decode()
552 |     }
553 | }]
554 | \`\`\`
555 | 
556 | ## Common Patterns
557 | 
558 | ### Processing Webhook Data
559 | \`\`\`python
560 | # Extract webhook payload
561 | webhook_body = _json["body"]
562 | username = webhook_body.get("username")
563 | email = webhook_body.get("email")
564 | items = webhook_body.get("items", [])
565 | 
566 | # Process and return
567 | return [{
568 |     "json": {
569 |         "username": username,
570 |         "email": email,
571 |         "item_count": len(items),
572 |         "processed_at": datetime.now().isoformat()
573 |     }
574 | }]
575 | \`\`\`
576 | 
577 | ### Aggregating Data
578 | \`\`\`python
579 | # Sum values across all items
580 | all_items = _input.all()
581 | total = sum(item["json"].get("amount", 0) for item in all_items)
582 | 
583 | return [{
584 |     "json": {
585 |         "total": total,
586 |         "item_count": len(all_items),
587 |         "average": total / len(all_items) if all_items else 0
588 |     }
589 | }]
590 | \`\`\`
591 | 
592 | ### Error Handling
593 | \`\`\`python
594 | try:
595 |     # Process data
596 |     webhook_data = _json["body"]
597 |     result = process_data(webhook_data)
598 |     
599 |     return [{
600 |         "json": {
601 |             "success": True,
602 |             "data": result
603 |         }
604 |     }]
605 | except Exception as e:
606 |     return [{
607 |         "json": {
608 |             "success": False,
609 |             "error": str(e)
610 |         }
611 |     }]
612 | \`\`\`
613 | 
614 | ### Data Transformation
615 | \`\`\`python
616 | # Transform all items
617 | all_items = _input.all()
618 | transformed = []
619 | 
620 | for item in all_items:
621 |     data = item["json"]
622 |     transformed.append({
623 |         "json": {
624 |             "id": data.get("id"),
625 |             "name": data.get("name", "").upper(),
626 |             "timestamp": datetime.now().isoformat(),
627 |             "valid": bool(data.get("email"))
628 |         }
629 |     })
630 | 
631 | return transformed
632 | \`\`\`
633 | 
634 | ## Limitations & Workarounds
635 | 
636 | ### No External Libraries
637 | \`\`\`python
638 | # CANNOT USE:
639 | # import requests  # Not available
640 | # import pandas   # Not available
641 | # import numpy    # Not available
642 | 
643 | # WORKAROUND: Use JavaScript Code node for HTTP requests
644 | # Or use HTTP Request node before Code node
645 | \`\`\`
646 | 
647 | ### HTTP Requests Alternative
648 | Since Python requests library is not available, use:
649 | 1. JavaScript Code node with $helpers.httpRequest()
650 | 2. HTTP Request node before your Python Code node
651 | 3. Webhook node to receive data
652 | 
653 | ## Common Pitfalls
654 | 1. Trying to import external libraries (requests, pandas)
655 | 2. Using items[0] instead of _input.all()
656 | 3. Forgetting webhook data is under ["body"]
657 | 4. Returning dictionaries instead of [{"json": {...}}]
658 | 5. Not handling missing keys with .get()
659 | 
660 | ## Best Practices
661 | 1. Always use .get() for dictionary access
662 | 2. Validate data before processing
663 | 3. Handle empty input arrays
664 | 4. Use list comprehensions for transformations
665 | 5. Return meaningful error messages
666 | 
667 | ## Type Conversions
668 | \`\`\`python
669 | # String to number
670 | value = float(_json.get("amount", "0"))
671 | 
672 | # Boolean conversion
673 | is_active = str(_json.get("active", "")).lower() == "true"
674 | 
675 | # Safe JSON parsing
676 | try:
677 |     data = json.loads(_json.get("json_string", "{}"))
678 | except json.JSONDecodeError:
679 |     data = {}
680 | \`\`\`
681 | 
682 | ## Related Tools
683 | - get_node_essentials("nodes-base.code")
684 | - validate_node_operation()
685 | - javascript_code_node_guide (for JavaScript syntax)`;
686 | }
```

--------------------------------------------------------------------------------
/tests/unit/parsers/node-parser.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, it, expect, vi, beforeEach } from 'vitest';
  2 | import { NodeParser } from '@/parsers/node-parser';
  3 | import { PropertyExtractor } from '@/parsers/property-extractor';
  4 | import {
  5 |   programmaticNodeFactory,
  6 |   declarativeNodeFactory,
  7 |   triggerNodeFactory,
  8 |   webhookNodeFactory,
  9 |   aiToolNodeFactory,
 10 |   versionedNodeClassFactory,
 11 |   versionedNodeTypeClassFactory,
 12 |   malformedNodeFactory,
 13 |   nodeClassFactory,
 14 |   propertyFactory,
 15 |   stringPropertyFactory,
 16 |   optionsPropertyFactory
 17 | } from '@tests/fixtures/factories/parser-node.factory';
 18 | 
 19 | // Mock PropertyExtractor
 20 | vi.mock('@/parsers/property-extractor');
 21 | 
 22 | describe('NodeParser', () => {
 23 |   let parser: NodeParser;
 24 |   let mockPropertyExtractor: any;
 25 | 
 26 |   beforeEach(() => {
 27 |     vi.clearAllMocks();
 28 |     
 29 |     // Setup mock property extractor
 30 |     mockPropertyExtractor = {
 31 |       extractProperties: vi.fn().mockReturnValue([]),
 32 |       extractCredentials: vi.fn().mockReturnValue([]),
 33 |       detectAIToolCapability: vi.fn().mockReturnValue(false),
 34 |       extractOperations: vi.fn().mockReturnValue([])
 35 |     };
 36 |     
 37 |     (PropertyExtractor as any).mockImplementation(() => mockPropertyExtractor);
 38 |     
 39 |     parser = new NodeParser();
 40 |   });
 41 | 
 42 |   describe('parse method', () => {
 43 |     it('should parse correctly when node is programmatic', () => {
 44 |       const nodeDefinition = programmaticNodeFactory.build();
 45 |       const NodeClass = nodeClassFactory.build({ description: nodeDefinition });
 46 |       
 47 |       mockPropertyExtractor.extractProperties.mockReturnValue(nodeDefinition.properties);
 48 |       mockPropertyExtractor.extractCredentials.mockReturnValue(nodeDefinition.credentials);
 49 |       
 50 |       const result = parser.parse(NodeClass as any, 'n8n-nodes-base');
 51 |       
 52 |       expect(result).toMatchObject({
 53 |         style: 'programmatic',
 54 |         nodeType: `nodes-base.${nodeDefinition.name}`,
 55 |         displayName: nodeDefinition.displayName,
 56 |         description: nodeDefinition.description,
 57 |         category: nodeDefinition.group?.[0] || 'misc',
 58 |         packageName: 'n8n-nodes-base'
 59 |       });
 60 |       
 61 |       // Check specific properties separately to avoid strict matching
 62 |       expect(result.isVersioned).toBe(false);
 63 |       expect(result.version).toBe(nodeDefinition.version?.toString() || '1');
 64 |       
 65 |       expect(mockPropertyExtractor.extractProperties).toHaveBeenCalledWith(NodeClass);
 66 |       expect(mockPropertyExtractor.extractCredentials).toHaveBeenCalledWith(NodeClass);
 67 |     });
 68 | 
 69 |     it('should parse correctly when node is declarative', () => {
 70 |       const nodeDefinition = declarativeNodeFactory.build();
 71 |       const NodeClass = nodeClassFactory.build({ description: nodeDefinition });
 72 |       
 73 |       const result = parser.parse(NodeClass as any, 'n8n-nodes-base');
 74 |       
 75 |       expect(result.style).toBe('declarative');
 76 |       expect(result.nodeType).toBe(`nodes-base.${nodeDefinition.name}`);
 77 |     });
 78 | 
 79 |     it('should preserve type when package prefix is already included', () => {
 80 |       const nodeDefinition = programmaticNodeFactory.build({
 81 |         name: 'nodes-base.slack'
 82 |       });
 83 |       const NodeClass = nodeClassFactory.build({ description: nodeDefinition });
 84 |       
 85 |       const result = parser.parse(NodeClass as any, 'n8n-nodes-base');
 86 |       
 87 |       expect(result.nodeType).toBe('nodes-base.slack');
 88 |     });
 89 | 
 90 |     it('should set isTrigger flag when node is a trigger', () => {
 91 |       const nodeDefinition = triggerNodeFactory.build();
 92 |       const NodeClass = nodeClassFactory.build({ description: nodeDefinition });
 93 |       
 94 |       const result = parser.parse(NodeClass as any, 'n8n-nodes-base');
 95 |       
 96 |       expect(result.isTrigger).toBe(true);
 97 |     });
 98 | 
 99 |     it('should set isWebhook flag when node is a webhook', () => {
100 |       const nodeDefinition = webhookNodeFactory.build();
101 |       const NodeClass = nodeClassFactory.build({ description: nodeDefinition });
102 |       
103 |       const result = parser.parse(NodeClass as any, 'n8n-nodes-base');
104 |       
105 |       expect(result.isWebhook).toBe(true);
106 |     });
107 | 
108 |     it('should set isAITool flag when node has AI capability', () => {
109 |       const nodeDefinition = aiToolNodeFactory.build();
110 |       const NodeClass = nodeClassFactory.build({ description: nodeDefinition });
111 |       
112 |       mockPropertyExtractor.detectAIToolCapability.mockReturnValue(true);
113 |       
114 |       const result = parser.parse(NodeClass as any, 'n8n-nodes-base');
115 |       
116 |       expect(result.isAITool).toBe(true);
117 |     });
118 | 
119 |     it('should parse correctly when node uses VersionedNodeType class', () => {
120 |       // Create a simple versioned node class without modifying function properties
121 |       const VersionedNodeClass = class VersionedNodeType {
122 |         baseDescription = {
123 |           name: 'versionedNode',
124 |           displayName: 'Versioned Node',
125 |           description: 'A versioned node',
126 |           defaultVersion: 2
127 |         };
128 |         nodeVersions = {
129 |           1: { description: { properties: [] } },
130 |           2: { description: { properties: [] } }
131 |         };
132 |         currentVersion = 2;
133 |       };
134 |       
135 |       mockPropertyExtractor.extractProperties.mockReturnValue([
136 |         propertyFactory.build(),
137 |         propertyFactory.build()
138 |       ]);
139 |       
140 |       const result = parser.parse(VersionedNodeClass as any, 'n8n-nodes-base');
141 |       
142 |       expect(result.isVersioned).toBe(true);
143 |       expect(result.version).toBe('2');
144 |       expect(result.nodeType).toBe('nodes-base.versionedNode');
145 |     });
146 | 
147 |     it('should parse correctly when node has nodeVersions property', () => {
148 |       const versionedDef = versionedNodeClassFactory.build();
149 |       const NodeClass = class {
150 |         nodeVersions = versionedDef.nodeVersions;
151 |         baseDescription = versionedDef.baseDescription;
152 |       };
153 |       
154 |       const result = parser.parse(NodeClass as any, 'n8n-nodes-base');
155 |       
156 |       expect(result.isVersioned).toBe(true);
157 |       expect(result.version).toBe('2');
158 |     });
159 | 
160 |     it('should use max version when version is an array', () => {
161 |       const nodeDefinition = programmaticNodeFactory.build({
162 |         version: [1, 1.1, 1.2, 2]
163 |       });
164 |       const NodeClass = nodeClassFactory.build({ description: nodeDefinition });
165 |       
166 |       const result = parser.parse(NodeClass as any, 'n8n-nodes-base');
167 |       
168 |       expect(result.isVersioned).toBe(true);
169 |       expect(result.version).toBe('2'); // Should return max version
170 |     });
171 | 
172 |     it('should throw error when node is missing name property', () => {
173 |       const nodeDefinition = malformedNodeFactory.build();
174 |       const NodeClass = nodeClassFactory.build({ description: nodeDefinition });
175 |       
176 |       expect(() => parser.parse(NodeClass as any, 'n8n-nodes-base')).toThrow('Node is missing name property');
177 |     });
178 | 
179 |     it('should use static description when instantiation fails', () => {
180 |       const NodeClass = class {
181 |         static description = programmaticNodeFactory.build();
182 |         constructor() {
183 |           throw new Error('Cannot instantiate');
184 |         }
185 |       };
186 |       
187 |       const result = parser.parse(NodeClass as any, 'n8n-nodes-base');
188 |       
189 |       expect(result.displayName).toBe(NodeClass.description.displayName);
190 |     });
191 | 
192 |     it('should extract category when using different property names', () => {
193 |       const testCases = [
194 |         { group: ['transform'], expected: 'transform' },
195 |         { categories: ['output'], expected: 'output' },
196 |         { category: 'trigger', expected: 'trigger' },
197 |         { /* no category */ expected: 'misc' }
198 |       ];
199 |       
200 |       testCases.forEach(({ group, categories, category, expected }) => {
201 |         const nodeDefinition = programmaticNodeFactory.build({
202 |           group,
203 |           categories,
204 |           category
205 |         } as any);
206 |         const NodeClass = nodeClassFactory.build({ description: nodeDefinition });
207 |         
208 |         const result = parser.parse(NodeClass as any, 'n8n-nodes-base');
209 |         
210 |         expect(result.category).toBe(expected);
211 |       });
212 |     });
213 | 
214 |     it('should set isTrigger flag when node has polling property', () => {
215 |       const nodeDefinition = programmaticNodeFactory.build({
216 |         polling: true
217 |       });
218 |       const NodeClass = nodeClassFactory.build({ description: nodeDefinition });
219 |       
220 |       const result = parser.parse(NodeClass as any, 'n8n-nodes-base');
221 |       
222 |       expect(result.isTrigger).toBe(true);
223 |     });
224 | 
225 |     it('should set isTrigger flag when node has eventTrigger property', () => {
226 |       const nodeDefinition = programmaticNodeFactory.build({
227 |         eventTrigger: true
228 |       });
229 |       const NodeClass = nodeClassFactory.build({ description: nodeDefinition });
230 |       
231 |       const result = parser.parse(NodeClass as any, 'n8n-nodes-base');
232 |       
233 |       expect(result.isTrigger).toBe(true);
234 |     });
235 | 
236 |     it('should set isTrigger flag when node name contains trigger', () => {
237 |       const nodeDefinition = programmaticNodeFactory.build({
238 |         name: 'myTrigger'
239 |       });
240 |       const NodeClass = nodeClassFactory.build({ description: nodeDefinition });
241 |       
242 |       const result = parser.parse(NodeClass as any, 'n8n-nodes-base');
243 |       
244 |       expect(result.isTrigger).toBe(true);
245 |     });
246 | 
247 |     it('should set isWebhook flag when node name contains webhook', () => {
248 |       const nodeDefinition = programmaticNodeFactory.build({
249 |         name: 'customWebhook'
250 |       });
251 |       const NodeClass = nodeClassFactory.build({ description: nodeDefinition });
252 |       
253 |       const result = parser.parse(NodeClass as any, 'n8n-nodes-base');
254 |       
255 |       expect(result.isWebhook).toBe(true);
256 |     });
257 | 
258 |     it('should parse correctly when node is an instance object', () => {
259 |       const nodeDefinition = programmaticNodeFactory.build();
260 |       const nodeInstance = {
261 |         description: nodeDefinition
262 |       };
263 |       
264 |       mockPropertyExtractor.extractProperties.mockReturnValue(nodeDefinition.properties);
265 | 
266 |       const result = parser.parse(nodeInstance as any, 'n8n-nodes-base');
267 |       
268 |       expect(result.displayName).toBe(nodeDefinition.displayName);
269 |     });
270 | 
271 |     it('should handle different package name formats', () => {
272 |       const nodeDefinition = programmaticNodeFactory.build();
273 |       const NodeClass = nodeClassFactory.build({ description: nodeDefinition });
274 |       
275 |       const testCases = [
276 |         { packageName: '@n8n/n8n-nodes-langchain', expectedPrefix: 'nodes-langchain' },
277 |         { packageName: 'n8n-nodes-custom', expectedPrefix: 'nodes-custom' },
278 |         { packageName: 'custom-package', expectedPrefix: 'custom-package' }
279 |       ];
280 |       
281 |       testCases.forEach(({ packageName, expectedPrefix }) => {
282 |         const result = parser.parse(NodeClass as any, packageName);
283 |         expect(result.nodeType).toBe(`${expectedPrefix}.${nodeDefinition.name}`);
284 |       });
285 |     });
286 |   });
287 | 
288 |   describe('version extraction', () => {
289 |     it('should prioritize currentVersion over description.defaultVersion', () => {
290 |       const NodeClass = class {
291 |         currentVersion = 2.2;  // Should be returned
292 |         description = {
293 |           name: 'AI Agent',
294 |           displayName: 'AI Agent',
295 |           defaultVersion: 3  // Should be ignored when currentVersion exists
296 |         };
297 |       };
298 | 
299 |       const result = parser.parse(NodeClass as any, 'n8n-nodes-base');
300 | 
301 |       expect(result.version).toBe('2.2');
302 |     });
303 | 
304 |     it('should extract version from description.defaultVersion', () => {
305 |       const NodeClass = class {
306 |         description = {
307 |           name: 'test',
308 |           displayName: 'Test',
309 |           defaultVersion: 3
310 |         };
311 |       };
312 | 
313 |       const result = parser.parse(NodeClass as any, 'n8n-nodes-base');
314 | 
315 |       expect(result.version).toBe('3');
316 |     });
317 | 
318 |     it('should handle currentVersion = 0 correctly', () => {
319 |       const NodeClass = class {
320 |         currentVersion = 0;  // Edge case: version 0 should be valid
321 |         description = {
322 |           name: 'test',
323 |           displayName: 'Test',
324 |           defaultVersion: 5  // Should be ignored
325 |         };
326 |       };
327 | 
328 |       const result = parser.parse(NodeClass as any, 'n8n-nodes-base');
329 | 
330 |       expect(result.version).toBe('0');
331 |     });
332 | 
333 |     it('should NOT extract version from non-existent baseDescription (legacy bug)', () => {
334 |       const NodeClass = class {
335 |         baseDescription = {  // This property doesn't exist on VersionedNodeType!
336 |           name: 'test',
337 |           displayName: 'Test',
338 |           defaultVersion: 3
339 |         };
340 |       };
341 | 
342 |       const result = parser.parse(NodeClass as any, 'n8n-nodes-base');
343 | 
344 |       expect(result.version).toBe('1');  // Should fallback to default
345 |     });
346 | 
347 |     it('should extract version from nodeVersions keys', () => {
348 |       const NodeClass = class {
349 |         description = { name: 'test', displayName: 'Test' };
350 |         nodeVersions = {
351 |           1: { description: {} },
352 |           2: { description: {} },
353 |           3: { description: {} }
354 |         };
355 |       };
356 |       
357 |       const result = parser.parse(NodeClass as any, 'n8n-nodes-base');
358 |       
359 |       expect(result.version).toBe('3');
360 |     });
361 | 
362 |     it('should extract version from instance nodeVersions', () => {
363 |       const NodeClass = class {
364 |         description = { name: 'test', displayName: 'Test' };
365 |         
366 |         constructor() {
367 |           (this as any).nodeVersions = {
368 |             1: { description: {} },
369 |             2: { description: {} },
370 |             4: { description: {} }
371 |           };
372 |         }
373 |       };
374 |       
375 |       const result = parser.parse(NodeClass as any, 'n8n-nodes-base');
376 |       
377 |       expect(result.version).toBe('4');
378 |     });
379 | 
380 |     it('should handle version as number in description', () => {
381 |       const nodeDefinition = programmaticNodeFactory.build({
382 |         version: 2
383 |       });
384 |       const NodeClass = nodeClassFactory.build({ description: nodeDefinition });
385 |       
386 |       const result = parser.parse(NodeClass as any, 'n8n-nodes-base');
387 |       
388 |       expect(result.version).toBe('2');
389 |     });
390 | 
391 |     it('should handle version as string in description', () => {
392 |       const nodeDefinition = programmaticNodeFactory.build({
393 |         version: '1.5' as any
394 |       });
395 |       const NodeClass = nodeClassFactory.build({ description: nodeDefinition });
396 |       
397 |       const result = parser.parse(NodeClass as any, 'n8n-nodes-base');
398 |       
399 |       expect(result.version).toBe('1.5');
400 |     });
401 | 
402 |     it('should default to version 1 when no version found', () => {
403 |       const nodeDefinition = programmaticNodeFactory.build();
404 |       delete (nodeDefinition as any).version;
405 |       const NodeClass = nodeClassFactory.build({ description: nodeDefinition });
406 |       
407 |       const result = parser.parse(NodeClass as any, 'n8n-nodes-base');
408 |       
409 |       expect(result.version).toBe('1');
410 |     });
411 |   });
412 | 
413 |   describe('versioned node detection', () => {
414 |     it('should detect versioned nodes with nodeVersions', () => {
415 |       const NodeClass = class {
416 |         description = { name: 'test', displayName: 'Test' };
417 |         nodeVersions = { 1: {}, 2: {} };
418 |       };
419 |       
420 |       const result = parser.parse(NodeClass as any, 'n8n-nodes-base');
421 |       
422 |       expect(result.isVersioned).toBe(true);
423 |     });
424 | 
425 |     it('should detect versioned nodes with defaultVersion', () => {
426 |       const NodeClass = class {
427 |         baseDescription = {
428 |           name: 'test',
429 |           displayName: 'Test',
430 |           defaultVersion: 2
431 |         };
432 |       };
433 |       
434 |       const result = parser.parse(NodeClass as any, 'n8n-nodes-base');
435 |       
436 |       expect(result.isVersioned).toBe(true);
437 |     });
438 | 
439 |     it('should detect versioned nodes with version array in instance', () => {
440 |       const NodeClass = class {
441 |         description = {
442 |           name: 'test',
443 |           displayName: 'Test',
444 |           version: [1, 1.1, 2]
445 |         };
446 |       };
447 |       
448 |       const result = parser.parse(NodeClass as any, 'n8n-nodes-base');
449 |       
450 |       expect(result.isVersioned).toBe(true);
451 |     });
452 | 
453 |     it('should not detect non-versioned nodes as versioned', () => {
454 |       const nodeDefinition = programmaticNodeFactory.build({
455 |         version: 1
456 |       });
457 |       const NodeClass = nodeClassFactory.build({ description: nodeDefinition });
458 |       
459 |       const result = parser.parse(NodeClass as any, 'n8n-nodes-base');
460 |       
461 |       expect(result.isVersioned).toBe(false);
462 |     });
463 |   });
464 | 
465 |   describe('edge cases', () => {
466 |     it('should handle null/undefined description gracefully', () => {
467 |       const NodeClass = class {
468 |         description = null;
469 |       };
470 |       
471 |       expect(() => parser.parse(NodeClass as any, 'n8n-nodes-base')).toThrow();
472 |     });
473 | 
474 |     it('should handle empty routing object for declarative nodes', () => {
475 |       const nodeDefinition = declarativeNodeFactory.build({
476 |         routing: {} as any
477 |       });
478 |       const NodeClass = nodeClassFactory.build({ description: nodeDefinition });
479 |       
480 |       const result = parser.parse(NodeClass as any, 'n8n-nodes-base');
481 |       
482 |       expect(result.style).toBe('declarative');
483 |     });
484 | 
485 |     it('should handle complex nested versioned structure', () => {
486 |       const NodeClass = class VersionedNodeType {
487 |         constructor() {
488 |           (this as any).baseDescription = {
489 |             name: 'complex',
490 |             displayName: 'Complex Node',
491 |             defaultVersion: 3
492 |           };
493 |           (this as any).nodeVersions = {
494 |             1: { description: { properties: [] } },
495 |             2: { description: { properties: [] } },
496 |             3: { description: { properties: [] } }
497 |           };
498 |         }
499 |       };
500 |       
501 |       // Override constructor name check
502 |       Object.defineProperty(NodeClass.prototype.constructor, 'name', {
503 |         value: 'VersionedNodeType'
504 |       });
505 |       
506 |       const result = parser.parse(NodeClass as any, 'n8n-nodes-base');
507 |       
508 |       expect(result.isVersioned).toBe(true);
509 |       expect(result.version).toBe('3');
510 |     });
511 |   });
512 | });
```

--------------------------------------------------------------------------------
/src/services/node-similarity-service.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { NodeRepository } from '../database/node-repository';
  2 | import { logger } from '../utils/logger';
  3 | 
  4 | export interface NodeSuggestion {
  5 |   nodeType: string;
  6 |   displayName: string;
  7 |   confidence: number;
  8 |   reason: string;
  9 |   category?: string;
 10 |   description?: string;
 11 | }
 12 | 
 13 | export interface SimilarityScore {
 14 |   nameSimilarity: number;
 15 |   categoryMatch: number;
 16 |   packageMatch: number;
 17 |   patternMatch: number;
 18 |   totalScore: number;
 19 | }
 20 | 
 21 | export interface CommonMistakePattern {
 22 |   pattern: string;
 23 |   suggestion: string;
 24 |   confidence: number;
 25 |   reason: string;
 26 | }
 27 | 
 28 | export class NodeSimilarityService {
 29 |   // Constants to avoid magic numbers
 30 |   private static readonly SCORING_THRESHOLD = 50; // Minimum 50% confidence to suggest
 31 |   private static readonly TYPO_EDIT_DISTANCE = 2; // Max 2 character differences for typo detection
 32 |   private static readonly SHORT_SEARCH_LENGTH = 5; // Searches ≤5 chars need special handling
 33 |   private static readonly CACHE_DURATION_MS = 5 * 60 * 1000; // 5 minutes
 34 |   private static readonly AUTO_FIX_CONFIDENCE = 0.9; // 90% confidence for auto-fix
 35 | 
 36 |   private repository: NodeRepository;
 37 |   private commonMistakes: Map<string, CommonMistakePattern[]>;
 38 |   private nodeCache: any[] | null = null;
 39 |   private cacheExpiry: number = 0;
 40 |   private cacheVersion: number = 0; // Track cache version for invalidation
 41 | 
 42 |   constructor(repository: NodeRepository) {
 43 |     this.repository = repository;
 44 |     this.commonMistakes = this.initializeCommonMistakes();
 45 |   }
 46 | 
 47 |   /**
 48 |    * Initialize common mistake patterns
 49 |    * Using safer string-based patterns instead of complex regex to avoid ReDoS
 50 |    */
 51 |   private initializeCommonMistakes(): Map<string, CommonMistakePattern[]> {
 52 |     const patterns = new Map<string, CommonMistakePattern[]>();
 53 | 
 54 |     // Case variations - using exact string matching (case-insensitive)
 55 |     patterns.set('case_variations', [
 56 |       { pattern: 'httprequest', suggestion: 'nodes-base.httpRequest', confidence: 0.95, reason: 'Incorrect capitalization' },
 57 |       { pattern: 'webhook', suggestion: 'nodes-base.webhook', confidence: 0.95, reason: 'Incorrect capitalization' },
 58 |       { pattern: 'slack', suggestion: 'nodes-base.slack', confidence: 0.9, reason: 'Missing package prefix' },
 59 |       { pattern: 'gmail', suggestion: 'nodes-base.gmail', confidence: 0.9, reason: 'Missing package prefix' },
 60 |       { pattern: 'googlesheets', suggestion: 'nodes-base.googleSheets', confidence: 0.9, reason: 'Missing package prefix' },
 61 |       { pattern: 'telegram', suggestion: 'nodes-base.telegram', confidence: 0.9, reason: 'Missing package prefix' },
 62 |     ]);
 63 | 
 64 |     // Specific case variations that are common
 65 |     patterns.set('specific_variations', [
 66 |       { pattern: 'HttpRequest', suggestion: 'nodes-base.httpRequest', confidence: 0.95, reason: 'Incorrect capitalization' },
 67 |       { pattern: 'HTTPRequest', suggestion: 'nodes-base.httpRequest', confidence: 0.95, reason: 'Common capitalization mistake' },
 68 |       { pattern: 'Webhook', suggestion: 'nodes-base.webhook', confidence: 0.95, reason: 'Incorrect capitalization' },
 69 |       { pattern: 'WebHook', suggestion: 'nodes-base.webhook', confidence: 0.95, reason: 'Common capitalization mistake' },
 70 |     ]);
 71 | 
 72 |     // Deprecated package prefixes
 73 |     patterns.set('deprecated_prefixes', [
 74 |       { pattern: 'n8n-nodes-base.', suggestion: 'nodes-base.', confidence: 0.95, reason: 'Full package name used instead of short form' },
 75 |       { pattern: '@n8n/n8n-nodes-langchain.', suggestion: 'nodes-langchain.', confidence: 0.95, reason: 'Full package name used instead of short form' },
 76 |     ]);
 77 | 
 78 |     // Common typos - exact matches
 79 |     patterns.set('typos', [
 80 |       { pattern: 'htprequest', suggestion: 'nodes-base.httpRequest', confidence: 0.8, reason: 'Likely typo' },
 81 |       { pattern: 'httpreqest', suggestion: 'nodes-base.httpRequest', confidence: 0.8, reason: 'Likely typo' },
 82 |       { pattern: 'webook', suggestion: 'nodes-base.webhook', confidence: 0.8, reason: 'Likely typo' },
 83 |       { pattern: 'slak', suggestion: 'nodes-base.slack', confidence: 0.8, reason: 'Likely typo' },
 84 |       { pattern: 'googlesheets', suggestion: 'nodes-base.googleSheets', confidence: 0.8, reason: 'Likely typo' },
 85 |     ]);
 86 | 
 87 |     // AI/LangChain specific
 88 |     patterns.set('ai_nodes', [
 89 |       { pattern: 'openai', suggestion: 'nodes-langchain.openAi', confidence: 0.85, reason: 'AI node - incorrect package' },
 90 |       { pattern: 'nodes-base.openai', suggestion: 'nodes-langchain.openAi', confidence: 0.9, reason: 'Wrong package - OpenAI is in LangChain package' },
 91 |       { pattern: 'chatopenai', suggestion: 'nodes-langchain.lmChatOpenAi', confidence: 0.85, reason: 'LangChain node naming convention' },
 92 |       { pattern: 'vectorstore', suggestion: 'nodes-langchain.vectorStoreInMemory', confidence: 0.7, reason: 'Generic vector store reference' },
 93 |     ]);
 94 | 
 95 |     return patterns;
 96 |   }
 97 | 
 98 |   /**
 99 |    * Check if a type is a common node name without prefix
100 |    */
101 |   private isCommonNodeWithoutPrefix(type: string): string | null {
102 |     const commonNodes: Record<string, string> = {
103 |       'httprequest': 'nodes-base.httpRequest',
104 |       'webhook': 'nodes-base.webhook',
105 |       'slack': 'nodes-base.slack',
106 |       'gmail': 'nodes-base.gmail',
107 |       'googlesheets': 'nodes-base.googleSheets',
108 |       'telegram': 'nodes-base.telegram',
109 |       'discord': 'nodes-base.discord',
110 |       'notion': 'nodes-base.notion',
111 |       'airtable': 'nodes-base.airtable',
112 |       'postgres': 'nodes-base.postgres',
113 |       'mysql': 'nodes-base.mySql',
114 |       'mongodb': 'nodes-base.mongoDb',
115 |     };
116 | 
117 |     const normalized = type.toLowerCase();
118 |     return commonNodes[normalized] || null;
119 |   }
120 | 
121 |   /**
122 |    * Find similar nodes for an invalid type
123 |    */
124 |   async findSimilarNodes(invalidType: string, limit: number = 5): Promise<NodeSuggestion[]> {
125 |     if (!invalidType || invalidType.trim() === '') {
126 |       return [];
127 |     }
128 | 
129 |     const suggestions: NodeSuggestion[] = [];
130 | 
131 |     // First, check for exact common mistakes
132 |     const mistakeSuggestion = this.checkCommonMistakes(invalidType);
133 |     if (mistakeSuggestion) {
134 |       suggestions.push(mistakeSuggestion);
135 |     }
136 | 
137 |     // Get all nodes (with caching)
138 |     const allNodes = await this.getCachedNodes();
139 | 
140 |     // Calculate similarity scores for all nodes
141 |     const scores = allNodes.map(node => ({
142 |       node,
143 |       score: this.calculateSimilarityScore(invalidType, node)
144 |     }));
145 | 
146 |     // Sort by total score and filter high scores
147 |     scores.sort((a, b) => b.score.totalScore - a.score.totalScore);
148 | 
149 |     // Add top suggestions (excluding already added exact matches)
150 |     for (const { node, score } of scores) {
151 |       if (suggestions.some(s => s.nodeType === node.nodeType)) {
152 |         continue;
153 |       }
154 | 
155 |       if (score.totalScore >= NodeSimilarityService.SCORING_THRESHOLD) {
156 |         suggestions.push(this.createSuggestion(node, score));
157 |       }
158 | 
159 |       if (suggestions.length >= limit) {
160 |         break;
161 |       }
162 |     }
163 | 
164 |     return suggestions;
165 |   }
166 | 
167 |   /**
168 |    * Check for common mistake patterns (ReDoS-safe implementation)
169 |    */
170 |   private checkCommonMistakes(invalidType: string): NodeSuggestion | null {
171 |     const cleanType = invalidType.trim();
172 |     const lowerType = cleanType.toLowerCase();
173 | 
174 |     // First check for common nodes without prefix
175 |     const commonNodeSuggestion = this.isCommonNodeWithoutPrefix(cleanType);
176 |     if (commonNodeSuggestion) {
177 |       const node = this.repository.getNode(commonNodeSuggestion);
178 |       if (node) {
179 |         return {
180 |           nodeType: commonNodeSuggestion,
181 |           displayName: node.displayName,
182 |           confidence: 0.9,
183 |           reason: 'Missing package prefix',
184 |           category: node.category,
185 |           description: node.description
186 |         };
187 |       }
188 |     }
189 | 
190 |     // Check deprecated prefixes (string-based, no regex)
191 |     for (const [category, patterns] of this.commonMistakes) {
192 |       if (category === 'deprecated_prefixes') {
193 |         for (const pattern of patterns) {
194 |           if (cleanType.startsWith(pattern.pattern)) {
195 |             const actualSuggestion = cleanType.replace(pattern.pattern, pattern.suggestion);
196 |             const node = this.repository.getNode(actualSuggestion);
197 |             if (node) {
198 |               return {
199 |                 nodeType: actualSuggestion,
200 |                 displayName: node.displayName,
201 |                 confidence: pattern.confidence,
202 |                 reason: pattern.reason,
203 |                 category: node.category,
204 |                 description: node.description
205 |               };
206 |             }
207 |           }
208 |         }
209 |       }
210 |     }
211 | 
212 |     // Check exact matches for typos and variations
213 |     for (const [category, patterns] of this.commonMistakes) {
214 |       if (category === 'deprecated_prefixes') continue; // Already handled
215 | 
216 |       for (const pattern of patterns) {
217 |         // Simple string comparison (case-sensitive for specific_variations)
218 |         const match = category === 'specific_variations'
219 |           ? cleanType === pattern.pattern
220 |           : lowerType === pattern.pattern.toLowerCase();
221 | 
222 |         if (match && pattern.suggestion) {
223 |           const node = this.repository.getNode(pattern.suggestion);
224 |           if (node) {
225 |             return {
226 |               nodeType: pattern.suggestion,
227 |               displayName: node.displayName,
228 |               confidence: pattern.confidence,
229 |               reason: pattern.reason,
230 |               category: node.category,
231 |               description: node.description
232 |             };
233 |           }
234 |         }
235 |       }
236 |     }
237 | 
238 |     return null;
239 |   }
240 | 
241 |   /**
242 |    * Calculate multi-factor similarity score
243 |    */
244 |   private calculateSimilarityScore(invalidType: string, node: any): SimilarityScore {
245 |     const cleanInvalid = this.normalizeNodeType(invalidType);
246 |     const cleanValid = this.normalizeNodeType(node.nodeType);
247 |     const displayNameClean = this.normalizeNodeType(node.displayName);
248 | 
249 |     // Special handling for very short search terms (e.g., "http", "sheet")
250 |     const isShortSearch = invalidType.length <= NodeSimilarityService.SHORT_SEARCH_LENGTH;
251 | 
252 |     // Name similarity (40% weight)
253 |     let nameSimilarity = Math.max(
254 |       this.getStringSimilarity(cleanInvalid, cleanValid),
255 |       this.getStringSimilarity(cleanInvalid, displayNameClean)
256 |     ) * 40;
257 | 
258 |     // For short searches that are substrings, give a small name similarity boost
259 |     if (isShortSearch && (cleanValid.includes(cleanInvalid) || displayNameClean.includes(cleanInvalid))) {
260 |       nameSimilarity = Math.max(nameSimilarity, 10);
261 |     }
262 | 
263 |     // Category match (20% weight)
264 |     let categoryMatch = 0;
265 |     if (node.category) {
266 |       const categoryClean = this.normalizeNodeType(node.category);
267 |       if (cleanInvalid.includes(categoryClean) || categoryClean.includes(cleanInvalid)) {
268 |         categoryMatch = 20;
269 |       }
270 |     }
271 | 
272 |     // Package match (15% weight)
273 |     let packageMatch = 0;
274 |     const invalidParts = cleanInvalid.split(/[.-]/);
275 |     const validParts = cleanValid.split(/[.-]/);
276 | 
277 |     if (invalidParts[0] === validParts[0]) {
278 |       packageMatch = 15;
279 |     }
280 | 
281 |     // Pattern match (25% weight)
282 |     let patternMatch = 0;
283 | 
284 |     // Check if it's a substring match
285 |     if (cleanValid.includes(cleanInvalid) || displayNameClean.includes(cleanInvalid)) {
286 |       // Boost score significantly for short searches that are exact substring matches
287 |       // Short searches need more boost to reach the 50 threshold
288 |       patternMatch = isShortSearch ? 45 : 25;
289 |     } else if (this.getEditDistance(cleanInvalid, cleanValid) <= NodeSimilarityService.TYPO_EDIT_DISTANCE) {
290 |       // Small edit distance indicates likely typo
291 |       patternMatch = 20;
292 |     } else if (this.getEditDistance(cleanInvalid, displayNameClean) <= NodeSimilarityService.TYPO_EDIT_DISTANCE) {
293 |       patternMatch = 18;
294 |     }
295 | 
296 |     // For very short searches, also check if the search term appears at the start
297 |     if (isShortSearch && (cleanValid.startsWith(cleanInvalid) || displayNameClean.startsWith(cleanInvalid))) {
298 |       patternMatch = Math.max(patternMatch, 40);
299 |     }
300 | 
301 |     const totalScore = nameSimilarity + categoryMatch + packageMatch + patternMatch;
302 | 
303 |     return {
304 |       nameSimilarity,
305 |       categoryMatch,
306 |       packageMatch,
307 |       patternMatch,
308 |       totalScore
309 |     };
310 |   }
311 | 
312 |   /**
313 |    * Create a suggestion object from node and score
314 |    */
315 |   private createSuggestion(node: any, score: SimilarityScore): NodeSuggestion {
316 |     let reason = 'Similar node';
317 | 
318 |     if (score.patternMatch >= 20) {
319 |       reason = 'Name similarity';
320 |     } else if (score.categoryMatch >= 15) {
321 |       reason = 'Same category';
322 |     } else if (score.packageMatch >= 10) {
323 |       reason = 'Same package';
324 |     }
325 | 
326 |     // Calculate confidence (0-1 scale)
327 |     const confidence = Math.min(score.totalScore / 100, 1);
328 | 
329 |     return {
330 |       nodeType: node.nodeType,
331 |       displayName: node.displayName,
332 |       confidence,
333 |       reason,
334 |       category: node.category,
335 |       description: node.description
336 |     };
337 |   }
338 | 
339 |   /**
340 |    * Normalize node type for comparison
341 |    */
342 |   private normalizeNodeType(type: string): string {
343 |     return type
344 |       .toLowerCase()
345 |       .replace(/[^a-z0-9]/g, '')
346 |       .trim();
347 |   }
348 | 
349 |   /**
350 |    * Calculate string similarity (0-1)
351 |    */
352 |   private getStringSimilarity(s1: string, s2: string): number {
353 |     if (s1 === s2) return 1;
354 |     if (!s1 || !s2) return 0;
355 | 
356 |     const distance = this.getEditDistance(s1, s2);
357 |     const maxLen = Math.max(s1.length, s2.length);
358 | 
359 |     return 1 - (distance / maxLen);
360 |   }
361 | 
362 |   /**
363 |    * Calculate Levenshtein distance with optimizations
364 |    * - Early termination when difference exceeds threshold
365 |    * - Space-optimized to use only two rows instead of full matrix
366 |    * - Fast path for identical or vastly different strings
367 |    */
368 |   private getEditDistance(s1: string, s2: string, maxDistance: number = 5): number {
369 |     // Fast path: identical strings
370 |     if (s1 === s2) return 0;
371 | 
372 |     const m = s1.length;
373 |     const n = s2.length;
374 | 
375 |     // Fast path: length difference exceeds threshold
376 |     const lengthDiff = Math.abs(m - n);
377 |     if (lengthDiff > maxDistance) return maxDistance + 1;
378 | 
379 |     // Fast path: empty strings
380 |     if (m === 0) return n;
381 |     if (n === 0) return m;
382 | 
383 |     // Space optimization: only need previous and current row
384 |     let prev = Array(n + 1).fill(0).map((_, i) => i);
385 | 
386 |     for (let i = 1; i <= m; i++) {
387 |       const curr = [i];
388 |       let minInRow = i;
389 | 
390 |       for (let j = 1; j <= n; j++) {
391 |         const cost = s1[i - 1] === s2[j - 1] ? 0 : 1;
392 |         const val = Math.min(
393 |           curr[j - 1] + 1,      // deletion
394 |           prev[j] + 1,          // insertion
395 |           prev[j - 1] + cost    // substitution
396 |         );
397 |         curr.push(val);
398 |         minInRow = Math.min(minInRow, val);
399 |       }
400 | 
401 |       // Early termination: if minimum in this row exceeds threshold
402 |       if (minInRow > maxDistance) {
403 |         return maxDistance + 1;
404 |       }
405 | 
406 |       prev = curr;
407 |     }
408 | 
409 |     return prev[n];
410 |   }
411 | 
412 |   /**
413 |    * Get cached nodes or fetch from repository
414 |    * Implements proper cache invalidation with version tracking
415 |    */
416 |   private async getCachedNodes(): Promise<any[]> {
417 |     const now = Date.now();
418 | 
419 |     if (!this.nodeCache || now > this.cacheExpiry) {
420 |       try {
421 |         const newNodes = this.repository.getAllNodes();
422 | 
423 |         // Only update cache if we got valid data
424 |         if (newNodes && newNodes.length > 0) {
425 |           this.nodeCache = newNodes;
426 |           this.cacheExpiry = now + NodeSimilarityService.CACHE_DURATION_MS;
427 |           this.cacheVersion++;
428 |           logger.debug('Node cache refreshed', {
429 |             count: newNodes.length,
430 |             version: this.cacheVersion
431 |           });
432 |         } else if (this.nodeCache) {
433 |           // Return stale cache if new fetch returned empty
434 |           logger.warn('Node fetch returned empty, using stale cache');
435 |         }
436 |       } catch (error) {
437 |         logger.error('Failed to fetch nodes for similarity service', error);
438 |         // Return stale cache on error if available
439 |         if (this.nodeCache) {
440 |           logger.info('Using stale cache due to fetch error');
441 |           return this.nodeCache;
442 |         }
443 |         return [];
444 |       }
445 |     }
446 | 
447 |     return this.nodeCache || [];
448 |   }
449 | 
450 |   /**
451 |    * Invalidate the cache (e.g., after database updates)
452 |    */
453 |   public invalidateCache(): void {
454 |     this.nodeCache = null;
455 |     this.cacheExpiry = 0;
456 |     this.cacheVersion++;
457 |     logger.debug('Node cache invalidated', { version: this.cacheVersion });
458 |   }
459 | 
460 |   /**
461 |    * Clear and refresh cache immediately
462 |    */
463 |   public async refreshCache(): Promise<void> {
464 |     this.invalidateCache();
465 |     await this.getCachedNodes();
466 |   }
467 | 
468 |   /**
469 |    * Format suggestions into a user-friendly message
470 |    */
471 |   formatSuggestionMessage(suggestions: NodeSuggestion[], invalidType: string): string {
472 |     if (suggestions.length === 0) {
473 |       return `Unknown node type: "${invalidType}". No similar nodes found.`;
474 |     }
475 | 
476 |     let message = `Unknown node type: "${invalidType}"\n\nDid you mean one of these?\n`;
477 | 
478 |     for (const suggestion of suggestions) {
479 |       const confidence = Math.round(suggestion.confidence * 100);
480 |       message += `• ${suggestion.nodeType} (${confidence}% match)`;
481 | 
482 |       if (suggestion.displayName) {
483 |         message += ` - ${suggestion.displayName}`;
484 |       }
485 | 
486 |       message += `\n  → ${suggestion.reason}`;
487 | 
488 |       if (suggestion.confidence >= 0.9) {
489 |         message += ' (can be auto-fixed)';
490 |       }
491 | 
492 |       message += '\n';
493 |     }
494 | 
495 |     return message;
496 |   }
497 | 
498 |   /**
499 |    * Check if a suggestion is high confidence for auto-fixing
500 |    */
501 |   isAutoFixable(suggestion: NodeSuggestion): boolean {
502 |     return suggestion.confidence >= NodeSimilarityService.AUTO_FIX_CONFIDENCE;
503 |   }
504 | 
505 |   /**
506 |    * Clear the node cache (useful after database updates)
507 |    * @deprecated Use invalidateCache() instead for proper version tracking
508 |    */
509 |   clearCache(): void {
510 |     this.invalidateCache();
511 |   }
512 | }
```

--------------------------------------------------------------------------------
/tests/unit/telemetry/event-validator.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, it, expect, beforeEach, vi } from 'vitest';
  2 | import { z } from 'zod';
  3 | import { TelemetryEventValidator, telemetryEventSchema, workflowTelemetrySchema } from '../../../src/telemetry/event-validator';
  4 | import { TelemetryEvent, WorkflowTelemetry } from '../../../src/telemetry/telemetry-types';
  5 | 
  6 | // Mock logger to avoid console output in tests
  7 | vi.mock('../../../src/utils/logger', () => ({
  8 |   logger: {
  9 |     debug: vi.fn(),
 10 |     info: vi.fn(),
 11 |     warn: vi.fn(),
 12 |     error: vi.fn(),
 13 |   }
 14 | }));
 15 | 
 16 | describe('TelemetryEventValidator', () => {
 17 |   let validator: TelemetryEventValidator;
 18 | 
 19 |   beforeEach(() => {
 20 |     validator = new TelemetryEventValidator();
 21 |     vi.clearAllMocks();
 22 |   });
 23 | 
 24 |   describe('validateEvent()', () => {
 25 |     it('should validate a basic valid event', () => {
 26 |       const event: TelemetryEvent = {
 27 |         user_id: 'user123',
 28 |         event: 'tool_used',
 29 |         properties: { tool: 'httpRequest', success: true, duration: 500 }
 30 |       };
 31 | 
 32 |       const result = validator.validateEvent(event);
 33 |       expect(result).toEqual(event);
 34 |     });
 35 | 
 36 |     it('should validate event with specific schema for tool_used', () => {
 37 |       const event: TelemetryEvent = {
 38 |         user_id: 'user123',
 39 |         event: 'tool_used',
 40 |         properties: { tool: 'httpRequest', success: true, duration: 500 }
 41 |       };
 42 | 
 43 |       const result = validator.validateEvent(event);
 44 |       expect(result).not.toBeNull();
 45 |       expect(result?.properties.tool).toBe('httpRequest');
 46 |       expect(result?.properties.success).toBe(true);
 47 |       expect(result?.properties.duration).toBe(500);
 48 |     });
 49 | 
 50 |     it('should validate search_query event with specific schema', () => {
 51 |       const event: TelemetryEvent = {
 52 |         user_id: 'user123',
 53 |         event: 'search_query',
 54 |         properties: {
 55 |           query: 'test query',
 56 |           resultsFound: 5,
 57 |           searchType: 'nodes',
 58 |           hasResults: true,
 59 |           isZeroResults: false
 60 |         }
 61 |       };
 62 | 
 63 |       const result = validator.validateEvent(event);
 64 |       expect(result).not.toBeNull();
 65 |       expect(result?.properties.query).toBe('test query');
 66 |       expect(result?.properties.resultsFound).toBe(5);
 67 |       expect(result?.properties.hasResults).toBe(true);
 68 |     });
 69 | 
 70 |     it('should validate performance_metric event with specific schema', () => {
 71 |       const event: TelemetryEvent = {
 72 |         user_id: 'user123',
 73 |         event: 'performance_metric',
 74 |         properties: {
 75 |           operation: 'database_query',
 76 |           duration: 1500,
 77 |           isSlow: true,
 78 |           isVerySlow: false,
 79 |           metadata: { table: 'nodes' }
 80 |         }
 81 |       };
 82 | 
 83 |       const result = validator.validateEvent(event);
 84 |       expect(result).not.toBeNull();
 85 |       expect(result?.properties.operation).toBe('database_query');
 86 |       expect(result?.properties.duration).toBe(1500);
 87 |       expect(result?.properties.isSlow).toBe(true);
 88 |     });
 89 | 
 90 |     it('should sanitize sensitive data from properties', () => {
 91 |       const event: TelemetryEvent = {
 92 |         user_id: 'user123',
 93 |         event: 'generic_event',
 94 |         properties: {
 95 |           description: 'Visit https://example.com/secret and [email protected] with key abcdef123456789012345678901234567890',
 96 |           apiKey: 'super-secret-key-12345678901234567890',
 97 |           normalProp: 'normal value'
 98 |         }
 99 |       };
100 | 
101 |       const result = validator.validateEvent(event);
102 |       expect(result).not.toBeNull();
103 |       expect(result?.properties.description).toBe('Visit [URL] and [EMAIL] with key [KEY]');
104 |       expect(result?.properties.normalProp).toBe('normal value');
105 |       expect(result?.properties).not.toHaveProperty('apiKey'); // Should be filtered out
106 |     });
107 | 
108 |     it('should handle nested object sanitization with depth limit', () => {
109 |       const event: TelemetryEvent = {
110 |         user_id: 'user123',
111 |         event: 'nested_event',
112 |         properties: {
113 |           nested: {
114 |             level1: {
115 |               level2: {
116 |                 level3: {
117 |                   level4: 'should be truncated',
118 |                   apiKey: 'secret123',
119 |                   description: 'Visit https://example.com'
120 |                 },
121 |                 description: 'Visit https://another.com'
122 |               }
123 |             }
124 |           }
125 |         }
126 |       };
127 | 
128 |       const result = validator.validateEvent(event);
129 |       expect(result).not.toBeNull();
130 |       expect(result?.properties.nested.level1.level2.level3).toBe('[NESTED]');
131 |       expect(result?.properties.nested.level1.level2.description).toBe('Visit [URL]');
132 |     });
133 | 
134 |     it('should handle array sanitization with size limit', () => {
135 |       const event: TelemetryEvent = {
136 |         user_id: 'user123',
137 |         event: 'array_event',
138 |         properties: {
139 |           items: Array.from({ length: 15 }, (_, i) => ({
140 |             id: i,
141 |             description: 'Visit https://example.com',
142 |             value: `item-${i}`
143 |           }))
144 |         }
145 |       };
146 | 
147 |       const result = validator.validateEvent(event);
148 |       expect(result).not.toBeNull();
149 |       expect(Array.isArray(result?.properties.items)).toBe(true);
150 |       expect(result?.properties.items.length).toBe(10); // Should be limited to 10
151 |     });
152 | 
153 |     it('should reject events with invalid user_id', () => {
154 |       const event: TelemetryEvent = {
155 |         user_id: '', // Empty string
156 |         event: 'test_event',
157 |         properties: {}
158 |       };
159 | 
160 |       const result = validator.validateEvent(event);
161 |       expect(result).toBeNull();
162 |     });
163 | 
164 |     it('should reject events with invalid event name', () => {
165 |       const event: TelemetryEvent = {
166 |         user_id: 'user123',
167 |         event: 'invalid-event-name!@#', // Invalid characters
168 |         properties: {}
169 |       };
170 | 
171 |       const result = validator.validateEvent(event);
172 |       expect(result).toBeNull();
173 |     });
174 | 
175 |     it('should reject tool_used event with invalid properties', () => {
176 |       const event: TelemetryEvent = {
177 |         user_id: 'user123',
178 |         event: 'tool_used',
179 |         properties: {
180 |           tool: 'test',
181 |           success: 'not-a-boolean', // Should be boolean
182 |           duration: -1 // Should be positive
183 |         }
184 |       };
185 | 
186 |       const result = validator.validateEvent(event);
187 |       expect(result).toBeNull();
188 |     });
189 | 
190 |     it('should filter out sensitive keys from properties', () => {
191 |       const event: TelemetryEvent = {
192 |         user_id: 'user123',
193 |         event: 'sensitive_event',
194 |         properties: {
195 |           password: 'secret123',
196 |           token: 'bearer-token',
197 |           apikey: 'api-key-value',
198 |           secret: 'secret-value',
199 |           credential: 'cred-value',
200 |           auth: 'auth-header',
201 |           url: 'https://example.com',
202 |           endpoint: 'api.example.com',
203 |           host: 'localhost',
204 |           database: 'prod-db',
205 |           normalProp: 'safe-value',
206 |           count: 42,
207 |           enabled: true
208 |         }
209 |       };
210 | 
211 |       const result = validator.validateEvent(event);
212 |       expect(result).not.toBeNull();
213 |       expect(result?.properties).not.toHaveProperty('password');
214 |       expect(result?.properties).not.toHaveProperty('token');
215 |       expect(result?.properties).not.toHaveProperty('apikey');
216 |       expect(result?.properties).not.toHaveProperty('secret');
217 |       expect(result?.properties).not.toHaveProperty('credential');
218 |       expect(result?.properties).not.toHaveProperty('auth');
219 |       expect(result?.properties).not.toHaveProperty('url');
220 |       expect(result?.properties).not.toHaveProperty('endpoint');
221 |       expect(result?.properties).not.toHaveProperty('host');
222 |       expect(result?.properties).not.toHaveProperty('database');
223 |       expect(result?.properties.normalProp).toBe('safe-value');
224 |       expect(result?.properties.count).toBe(42);
225 |       expect(result?.properties.enabled).toBe(true);
226 |     });
227 | 
228 |     it('should handle validation_details event schema', () => {
229 |       const event: TelemetryEvent = {
230 |         user_id: 'user123',
231 |         event: 'validation_details',
232 |         properties: {
233 |           nodeType: 'nodes-base.httpRequest',
234 |           errorType: 'required_field_missing',
235 |           errorCategory: 'validation_error',
236 |           details: { field: 'url' }
237 |         }
238 |       };
239 | 
240 |       const result = validator.validateEvent(event);
241 |       expect(result).not.toBeNull();
242 |       expect(result?.properties.nodeType).toBe('nodes-base.httpRequest');
243 |       expect(result?.properties.errorType).toBe('required_field_missing');
244 |     });
245 | 
246 |     it('should handle null and undefined values', () => {
247 |       const event: TelemetryEvent = {
248 |         user_id: 'user123',
249 |         event: 'null_event',
250 |         properties: {
251 |           nullValue: null,
252 |           undefinedValue: undefined,
253 |           normalValue: 'test'
254 |         }
255 |       };
256 | 
257 |       const result = validator.validateEvent(event);
258 |       expect(result).not.toBeNull();
259 |       expect(result?.properties.nullValue).toBeNull();
260 |       expect(result?.properties.undefinedValue).toBeNull();
261 |       expect(result?.properties.normalValue).toBe('test');
262 |     });
263 |   });
264 | 
265 |   describe('validateWorkflow()', () => {
266 |     it('should validate a valid workflow', () => {
267 |       const workflow: WorkflowTelemetry = {
268 |         user_id: 'user123',
269 |         workflow_hash: 'hash123',
270 |         node_count: 3,
271 |         node_types: ['webhook', 'httpRequest', 'set'],
272 |         has_trigger: true,
273 |         has_webhook: true,
274 |         complexity: 'medium',
275 |         sanitized_workflow: {
276 |           nodes: [
277 |             { id: '1', type: 'webhook' },
278 |             { id: '2', type: 'httpRequest' },
279 |             { id: '3', type: 'set' }
280 |           ],
281 |           connections: { '1': { main: [[{ node: '2', type: 'main', index: 0 }]] } }
282 |         }
283 |       };
284 | 
285 |       const result = validator.validateWorkflow(workflow);
286 |       expect(result).toEqual(workflow);
287 |     });
288 | 
289 |     it('should reject workflow with too many nodes', () => {
290 |       const workflow: WorkflowTelemetry = {
291 |         user_id: 'user123',
292 |         workflow_hash: 'hash123',
293 |         node_count: 1001, // Over limit
294 |         node_types: ['webhook'],
295 |         has_trigger: true,
296 |         has_webhook: true,
297 |         complexity: 'complex',
298 |         sanitized_workflow: {
299 |           nodes: [],
300 |           connections: {}
301 |         }
302 |       };
303 | 
304 |       const result = validator.validateWorkflow(workflow);
305 |       expect(result).toBeNull();
306 |     });
307 | 
308 |     it('should reject workflow with invalid complexity', () => {
309 |       const workflow = {
310 |         user_id: 'user123',
311 |         workflow_hash: 'hash123',
312 |         node_count: 3,
313 |         node_types: ['webhook'],
314 |         has_trigger: true,
315 |         has_webhook: true,
316 |         complexity: 'invalid' as any, // Invalid complexity
317 |         sanitized_workflow: {
318 |           nodes: [],
319 |           connections: {}
320 |         }
321 |       };
322 | 
323 |       const result = validator.validateWorkflow(workflow);
324 |       expect(result).toBeNull();
325 |     });
326 | 
327 |     it('should reject workflow with too many node types', () => {
328 |       const workflow: WorkflowTelemetry = {
329 |         user_id: 'user123',
330 |         workflow_hash: 'hash123',
331 |         node_count: 3,
332 |         node_types: Array.from({ length: 101 }, (_, i) => `node-${i}`), // Over limit
333 |         has_trigger: true,
334 |         has_webhook: true,
335 |         complexity: 'complex',
336 |         sanitized_workflow: {
337 |           nodes: [],
338 |           connections: {}
339 |         }
340 |       };
341 | 
342 |       const result = validator.validateWorkflow(workflow);
343 |       expect(result).toBeNull();
344 |     });
345 |   });
346 | 
347 |   describe('getStats()', () => {
348 |     it('should track validation statistics', () => {
349 |       const validEvent: TelemetryEvent = {
350 |         user_id: 'user123',
351 |         event: 'valid_event',
352 |         properties: {}
353 |       };
354 | 
355 |       const invalidEvent: TelemetryEvent = {
356 |         user_id: '', // Invalid
357 |         event: 'invalid_event',
358 |         properties: {}
359 |       };
360 | 
361 |       validator.validateEvent(validEvent);
362 |       validator.validateEvent(validEvent);
363 |       validator.validateEvent(invalidEvent);
364 | 
365 |       const stats = validator.getStats();
366 |       expect(stats.successes).toBe(2);
367 |       expect(stats.errors).toBe(1);
368 |       expect(stats.total).toBe(3);
369 |       expect(stats.errorRate).toBeCloseTo(0.333, 3);
370 |     });
371 | 
372 |     it('should handle division by zero in error rate', () => {
373 |       const stats = validator.getStats();
374 |       expect(stats.errorRate).toBe(0);
375 |     });
376 |   });
377 | 
378 |   describe('resetStats()', () => {
379 |     it('should reset validation statistics', () => {
380 |       const validEvent: TelemetryEvent = {
381 |         user_id: 'user123',
382 |         event: 'valid_event',
383 |         properties: {}
384 |       };
385 | 
386 |       validator.validateEvent(validEvent);
387 |       validator.resetStats();
388 | 
389 |       const stats = validator.getStats();
390 |       expect(stats.successes).toBe(0);
391 |       expect(stats.errors).toBe(0);
392 |       expect(stats.total).toBe(0);
393 |       expect(stats.errorRate).toBe(0);
394 |     });
395 |   });
396 | 
397 |   describe('Schema validation', () => {
398 |     describe('telemetryEventSchema', () => {
399 |       it('should validate with created_at timestamp', () => {
400 |         const event = {
401 |           user_id: 'user123',
402 |           event: 'test_event',
403 |           properties: {},
404 |           created_at: '2024-01-01T00:00:00Z'
405 |         };
406 | 
407 |         const result = telemetryEventSchema.safeParse(event);
408 |         expect(result.success).toBe(true);
409 |       });
410 | 
411 |       it('should reject invalid datetime format', () => {
412 |         const event = {
413 |           user_id: 'user123',
414 |           event: 'test_event',
415 |           properties: {},
416 |           created_at: 'invalid-date'
417 |         };
418 | 
419 |         const result = telemetryEventSchema.safeParse(event);
420 |         expect(result.success).toBe(false);
421 |       });
422 | 
423 |       it('should enforce user_id length limits', () => {
424 |         const longUserId = 'a'.repeat(65);
425 |         const event = {
426 |           user_id: longUserId,
427 |           event: 'test_event',
428 |           properties: {}
429 |         };
430 | 
431 |         const result = telemetryEventSchema.safeParse(event);
432 |         expect(result.success).toBe(false);
433 |       });
434 | 
435 |       it('should enforce event name regex pattern', () => {
436 |         const event = {
437 |           user_id: 'user123',
438 |           event: 'invalid event name with spaces!',
439 |           properties: {}
440 |         };
441 | 
442 |         const result = telemetryEventSchema.safeParse(event);
443 |         expect(result.success).toBe(false);
444 |       });
445 |     });
446 | 
447 |     describe('workflowTelemetrySchema', () => {
448 |       it('should enforce node array size limits', () => {
449 |         const workflow = {
450 |           user_id: 'user123',
451 |           workflow_hash: 'hash123',
452 |           node_count: 3,
453 |           node_types: ['test'],
454 |           has_trigger: true,
455 |           has_webhook: false,
456 |           complexity: 'simple',
457 |           sanitized_workflow: {
458 |             nodes: Array.from({ length: 1001 }, (_, i) => ({ id: i })), // Over limit
459 |             connections: {}
460 |           }
461 |         };
462 | 
463 |         const result = workflowTelemetrySchema.safeParse(workflow);
464 |         expect(result.success).toBe(false);
465 |       });
466 | 
467 |       it('should validate with optional created_at', () => {
468 |         const workflow = {
469 |           user_id: 'user123',
470 |           workflow_hash: 'hash123',
471 |           node_count: 1,
472 |           node_types: ['webhook'],
473 |           has_trigger: true,
474 |           has_webhook: true,
475 |           complexity: 'simple',
476 |           sanitized_workflow: {
477 |             nodes: [{ id: '1' }],
478 |             connections: {}
479 |           },
480 |           created_at: '2024-01-01T00:00:00Z'
481 |         };
482 | 
483 |         const result = workflowTelemetrySchema.safeParse(workflow);
484 |         expect(result.success).toBe(true);
485 |       });
486 |     });
487 |   });
488 | 
489 |   describe('String sanitization edge cases', () => {
490 |     it('should handle multiple URLs in same string', () => {
491 |       const event: TelemetryEvent = {
492 |         user_id: 'user123',
493 |         event: 'test_event',
494 |         properties: {
495 |           description: 'Visit https://example.com or http://test.com for more info'
496 |         }
497 |       };
498 | 
499 |       const result = validator.validateEvent(event);
500 |       expect(result?.properties.description).toBe('Visit [URL] or [URL] for more info');
501 |     });
502 | 
503 |     it('should handle mixed sensitive content', () => {
504 |       const event: TelemetryEvent = {
505 |         user_id: 'user123',
506 |         event: 'test_event',
507 |         properties: {
508 |           message: 'Contact [email protected] at https://secure.com with key abc123def456ghi789jkl012mno345pqr'
509 |         }
510 |       };
511 | 
512 |       const result = validator.validateEvent(event);
513 |       expect(result?.properties.message).toBe('Contact [EMAIL] at [URL] with key [KEY]');
514 |     });
515 | 
516 |     it('should preserve non-sensitive content', () => {
517 |       const event: TelemetryEvent = {
518 |         user_id: 'user123',
519 |         event: 'test_event',
520 |         properties: {
521 |           status: 'success',
522 |           count: 42,
523 |           enabled: true,
524 |           short_id: 'abc123' // Too short to be considered a key
525 |         }
526 |       };
527 | 
528 |       const result = validator.validateEvent(event);
529 |       expect(result?.properties.status).toBe('success');
530 |       expect(result?.properties.count).toBe(42);
531 |       expect(result?.properties.enabled).toBe(true);
532 |       expect(result?.properties.short_id).toBe('abc123');
533 |     });
534 |   });
535 | 
536 |   describe('Error handling', () => {
537 |     it('should handle Zod parsing errors gracefully', () => {
538 |       const invalidEvent = {
539 |         user_id: 123, // Should be string
540 |         event: 'test_event',
541 |         properties: {}
542 |       };
543 | 
544 |       const result = validator.validateEvent(invalidEvent as any);
545 |       expect(result).toBeNull();
546 |     });
547 | 
548 |     it('should handle unexpected errors during validation', () => {
549 |       const eventWithCircularRef: any = {
550 |         user_id: 'user123',
551 |         event: 'test_event',
552 |         properties: {}
553 |       };
554 |       // Create circular reference
555 |       eventWithCircularRef.properties.self = eventWithCircularRef;
556 | 
557 |       const result = validator.validateEvent(eventWithCircularRef);
558 |       // Should handle gracefully and not throw
559 |       expect(result).not.toThrow;
560 |     });
561 |   });
562 | });
```
Page 25/60FirstPrevNextLast