#
tokens: 48758/50000 10/617 files (page 19/46)
lines: off (toggle) GitHub
raw markdown copy
This is page 19 of 46. Use http://codebase.md/czlonkowski/n8n-mcp?lines=false&page={x} to view the full context.

# Directory Structure

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

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

```typescript
import { toolsDocumentation } from './tool-docs';

export function getToolDocumentation(toolName: string, depth: 'essentials' | 'full' = 'essentials'): string {
  // Check for special documentation topics
  if (toolName === 'javascript_code_node_guide') {
    return getJavaScriptCodeNodeGuide(depth);
  }
  if (toolName === 'python_code_node_guide') {
    return getPythonCodeNodeGuide(depth);
  }
  
  const tool = toolsDocumentation[toolName];
  if (!tool) {
    return `Tool '${toolName}' not found. Use tools_documentation() to see available tools.`;
  }

  if (depth === 'essentials') {
    const { essentials } = tool;
    return `# ${tool.name}

${essentials.description}

**Example**: ${essentials.example}

**Key parameters**: ${essentials.keyParameters.join(', ')}

**Performance**: ${essentials.performance}

**Tips**:
${essentials.tips.map(tip => `- ${tip}`).join('\n')}

For full documentation, use: tools_documentation({topic: "${toolName}", depth: "full"})`;
  }

  // Full documentation
  const { full } = tool;
  return `# ${tool.name}

${full.description}

## Parameters
${Object.entries(full.parameters).map(([param, info]) => 
  `- **${param}** (${info.type}${info.required ? ', required' : ''}): ${info.description}`
).join('\n')}

## Returns
${full.returns}

## Examples
${full.examples.map(ex => `\`\`\`javascript\n${ex}\n\`\`\``).join('\n\n')}

## Common Use Cases
${full.useCases.map(uc => `- ${uc}`).join('\n')}

## Performance
${full.performance}

## Best Practices
${full.bestPractices.map(bp => `- ${bp}`).join('\n')}

## Common Pitfalls
${full.pitfalls.map(p => `- ${p}`).join('\n')}

## Related Tools
${full.relatedTools.map(t => `- ${t}`).join('\n')}`;
}

export function getToolsOverview(depth: 'essentials' | 'full' = 'essentials'): string {
  // Get version info from package.json
  const packageJson = require('../../package.json');
  const supportedN8nVersion = packageJson.dependencies?.n8n?.replace(/[^0-9.]/g, '') || 'latest';
  
  if (depth === 'essentials') {
    return `# n8n MCP Tools Reference

## Important: Compatibility Notice
⚠️ This MCP server is tested with n8n version ${supportedN8nVersion}. 
Inform the user to check their n8n version matches or is compatible with the supported version listed above.

## Code Node Configuration
When working with Code nodes, always start by calling the relevant guide:
- tools_documentation({topic: "javascript_code_node_guide"}) for JavaScript Code nodes
- tools_documentation({topic: "python_code_node_guide"}) for Python Code nodes

## Standard Workflow Pattern

1. **Find** the node you need:
   - search_nodes({query: "slack"}) - Search by keyword
   - list_nodes({category: "communication"}) - List by category
   - list_ai_tools() - List AI-capable nodes

2. **Configure** the node:
   - get_node_essentials("nodes-base.slack") - Get essential properties only (5KB)
   - get_node_info("nodes-base.slack") - Get complete schema (100KB+)
   - search_node_properties("nodes-base.slack", "auth") - Find specific properties

3. **Validate** before deployment:
   - validate_node_minimal("nodes-base.slack", config) - Check required fields
   - validate_node_operation("nodes-base.slack", config) - Full validation with fixes
   - validate_workflow(workflow) - Validate entire workflow

## Tool Categories

**Discovery Tools**
- search_nodes - Full-text search across all nodes
- list_nodes - List nodes with filtering by category, package, or type
- list_ai_tools - List all AI-capable nodes with usage guidance

**Configuration Tools**
- get_node_essentials - Returns 10-20 key properties with examples
- get_node_info - Returns complete node schema with all properties
- search_node_properties - Search for specific properties within a node
- get_property_dependencies - Analyze property visibility dependencies

**Validation Tools**
- validate_node_minimal - Quick validation of required fields only
- validate_node_operation - Full validation with operation awareness
- validate_workflow - Complete workflow validation including connections

**Template Tools**
- list_tasks - List common task templates
- get_node_for_task - Get pre-configured node for specific tasks
- search_templates - Search workflow templates by keyword
- get_template - Get complete workflow JSON by ID

**n8n API Tools** (requires N8N_API_URL configuration)
- n8n_create_workflow - Create new workflows
- n8n_update_partial_workflow - Update workflows using diff operations
- n8n_validate_workflow - Validate workflow from n8n instance
- n8n_trigger_webhook_workflow - Trigger workflow execution

## Performance Characteristics
- Instant (<10ms): search_nodes, list_nodes, get_node_essentials
- Fast (<100ms): validate_node_minimal, get_node_for_task
- Moderate (100-500ms): validate_workflow, get_node_info
- Network-dependent: All n8n_* tools

For comprehensive documentation on any tool:
tools_documentation({topic: "tool_name", depth: "full"})`;
  }

  const categories = getAllCategories();
  return `# n8n MCP Tools - Complete Reference

## Important: Compatibility Notice
⚠️ This MCP server is tested with n8n version ${supportedN8nVersion}. 
Run n8n_health_check() to verify your n8n instance compatibility and API connectivity.

## Code Node Guides
For Code node configuration, use these comprehensive guides:
- tools_documentation({topic: "javascript_code_node_guide", depth: "full"}) - JavaScript patterns, n8n variables, error handling
- tools_documentation({topic: "python_code_node_guide", depth: "full"}) - Python patterns, data access, debugging

## All Available Tools by Category

${categories.map(cat => {
  const tools = getToolsByCategory(cat);
  const categoryName = cat.charAt(0).toUpperCase() + cat.slice(1).replace('_', ' ');
  return `### ${categoryName}
${tools.map(toolName => {
  const tool = toolsDocumentation[toolName];
  return `- **${toolName}**: ${tool.essentials.description}`;
}).join('\n')}`;
}).join('\n\n')}

## Usage Notes
- All node types require the "nodes-base." or "nodes-langchain." prefix
- Use get_node_essentials() first for most tasks (95% smaller than get_node_info)
- Validation profiles: minimal (editing), runtime (default), strict (deployment)
- n8n API tools only available when N8N_API_URL and N8N_API_KEY are configured

For detailed documentation on any tool:
tools_documentation({topic: "tool_name", depth: "full"})`;
}

export function searchToolDocumentation(keyword: string): string[] {
  const results: string[] = [];
  
  for (const [toolName, tool] of Object.entries(toolsDocumentation)) {
    const searchText = `${toolName} ${tool.essentials.description} ${tool.full.description}`.toLowerCase();
    if (searchText.includes(keyword.toLowerCase())) {
      results.push(toolName);
    }
  }
  
  return results;
}

export function getToolsByCategory(category: string): string[] {
  return Object.entries(toolsDocumentation)
    .filter(([_, tool]) => tool.category === category)
    .map(([name, _]) => name);
}

export function getAllCategories(): string[] {
  const categories = new Set<string>();
  Object.values(toolsDocumentation).forEach(tool => {
    categories.add(tool.category);
  });
  return Array.from(categories);
}

// Special documentation topics
function getJavaScriptCodeNodeGuide(depth: 'essentials' | 'full' = 'essentials'): string {
  if (depth === 'essentials') {
    return `# JavaScript Code Node Guide

Essential patterns for JavaScript in n8n Code nodes.

**Key Concepts**:
- Access all items: \`$input.all()\` (not items[0])
- Current item data: \`$json\`
- Return format: \`[{json: {...}}]\` (array of objects)

**Available Helpers**:
- \`$helpers.httpRequest()\` - Make HTTP requests
- \`$jmespath()\` - Query JSON data
- \`DateTime\` - Luxon for date handling

**Common Patterns**:
\`\`\`javascript
// Process all items
const allItems = $input.all();
return allItems.map(item => ({
  json: {
    processed: true,
    original: item.json,
    timestamp: DateTime.now().toISO()
  }
}));
\`\`\`

**Tips**:
- Webhook data is under \`.body\` property
- Use async/await for HTTP requests
- Always return array format

For full guide: tools_documentation({topic: "javascript_code_node_guide", depth: "full"})`;
  }

  // Full documentation
  return `# JavaScript Code Node Complete Guide

Comprehensive guide for using JavaScript in n8n Code nodes.

## Data Access Patterns

### Accessing Input Data
\`\`\`javascript
// Get all items from previous node
const allItems = $input.all();

// Get specific node's output
const webhookData = $node["Webhook"].json;

// Current item in loop
const currentItem = $json;

// First item only
const firstItem = $input.first().json;
\`\`\`

### Webhook Data Structure
**CRITICAL**: Webhook data is nested under \`.body\`:
\`\`\`javascript
// WRONG - Won't work
const data = $json.name;

// CORRECT - Webhook data is under body
const data = $json.body.name;
\`\`\`

## Available Built-in Functions

### HTTP Requests
\`\`\`javascript
// Make HTTP request
const response = await $helpers.httpRequest({
  method: 'GET',
  url: 'https://api.example.com/data',
  headers: {
    'Authorization': 'Bearer token'
  }
});
\`\`\`

### Date/Time Handling
\`\`\`javascript
// Using Luxon DateTime
const now = DateTime.now();
const formatted = now.toFormat('yyyy-MM-dd');
const iso = now.toISO();
const plus5Days = now.plus({ days: 5 });
\`\`\`

### JSON Querying
\`\`\`javascript
// JMESPath queries
const result = $jmespath($json, "users[?age > 30].name");
\`\`\`

## Return Format Requirements

### Correct Format
\`\`\`javascript
// MUST return array of objects with json property
return [{
  json: {
    result: "success",
    data: processedData
  }
}];

// Multiple items
return items.map(item => ({
  json: {
    id: item.id,
    processed: true
  }
}));
\`\`\`

### Binary Data
\`\`\`javascript
// Return with binary data
return [{
  json: { filename: "report.pdf" },
  binary: {
    data: Buffer.from(pdfContent).toString('base64')
  }
}];
\`\`\`

## Common Patterns

### Processing Webhook Data
\`\`\`javascript
// Extract webhook payload
const webhookBody = $json.body;
const { username, email, items } = webhookBody;

// Process and return
return [{
  json: {
    username,
    email,
    itemCount: items.length,
    processedAt: DateTime.now().toISO()
  }
}];
\`\`\`

### Aggregating Data
\`\`\`javascript
// Sum values across all items
const allItems = $input.all();
const total = allItems.reduce((sum, item) => {
  return sum + (item.json.amount || 0);
}, 0);

return [{
  json: { 
    total,
    itemCount: allItems.length,
    average: total / allItems.length
  }
}];
\`\`\`

### Error Handling
\`\`\`javascript
try {
  const response = await $helpers.httpRequest({
    url: 'https://api.example.com/data'
  });
  
  return [{
    json: {
      success: true,
      data: response
    }
  }];
} catch (error) {
  return [{
    json: {
      success: false,
      error: error.message
    }
  }];
}
\`\`\`

## Available Node.js Modules
- crypto (built-in)
- Buffer
- URL/URLSearchParams
- Basic Node.js globals

## Common Pitfalls
1. Using \`items[0]\` instead of \`$input.all()\`
2. Forgetting webhook data is under \`.body\`
3. Returning plain objects instead of \`[{json: {...}}]\`
4. Using \`require()\` for external modules (not allowed)
5. Trying to use expression syntax \`{{}}\` inside code

## Best Practices
1. Always validate input data exists before accessing
2. Use try-catch for HTTP requests
3. Return early on validation failures
4. Keep code simple and readable
5. Use descriptive variable names

## Related Tools
- get_node_essentials("nodes-base.code")
- validate_node_operation()
- python_code_node_guide (for Python syntax)`;
}

function getPythonCodeNodeGuide(depth: 'essentials' | 'full' = 'essentials'): string {
  if (depth === 'essentials') {
    return `# Python Code Node Guide

Essential patterns for Python in n8n Code nodes.

**Key Concepts**:
- Access all items: \`_input.all()\` (not items[0])
- Current item data: \`_json\`
- Return format: \`[{"json": {...}}]\` (list of dicts)

**Limitations**:
- No external libraries (no requests, pandas, numpy)
- Use built-in functions only
- No pip install available

**Common Patterns**:
\`\`\`python
# Process all items
all_items = _input.all()
return [{
    "json": {
        "processed": True,
        "count": len(all_items),
        "first_item": all_items[0]["json"] if all_items else None
    }
}]
\`\`\`

**Tips**:
- Webhook data is under ["body"] key
- Use json module for parsing
- datetime for date handling

For full guide: tools_documentation({topic: "python_code_node_guide", depth: "full"})`;
  }

  // Full documentation
  return `# Python Code Node Complete Guide

Comprehensive guide for using Python in n8n Code nodes.

## Data Access Patterns

### Accessing Input Data
\`\`\`python
# Get all items from previous node
all_items = _input.all()

# Get specific node's output (use _node)
webhook_data = _node["Webhook"]["json"]

# Current item in loop
current_item = _json

# First item only
first_item = _input.first()["json"]
\`\`\`

### Webhook Data Structure
**CRITICAL**: Webhook data is nested under ["body"]:
\`\`\`python
# WRONG - Won't work
data = _json["name"]

# CORRECT - Webhook data is under body
data = _json["body"]["name"]
\`\`\`

## Available Built-in Modules

### Standard Library Only
\`\`\`python
import json
import datetime
import base64
import hashlib
import urllib.parse
import re
import math
import random
\`\`\`

### Date/Time Handling
\`\`\`python
from datetime import datetime, timedelta

# Current time
now = datetime.now()
iso_format = now.isoformat()

# Date arithmetic
future = now + timedelta(days=5)
formatted = now.strftime("%Y-%m-%d")
\`\`\`

### JSON Operations
\`\`\`python
# Parse JSON string
data = json.loads(json_string)

# Convert to JSON
json_output = json.dumps({"key": "value"})
\`\`\`

## Return Format Requirements

### Correct Format
\`\`\`python
# MUST return list of dictionaries with "json" key
return [{
    "json": {
        "result": "success",
        "data": processed_data
    }
}]

# Multiple items
return [
    {"json": {"id": item["json"]["id"], "processed": True}}
    for item in all_items
]
\`\`\`

### Binary Data
\`\`\`python
# Return with binary data
import base64

return [{
    "json": {"filename": "report.pdf"},
    "binary": {
        "data": base64.b64encode(pdf_content).decode()
    }
}]
\`\`\`

## Common Patterns

### Processing Webhook Data
\`\`\`python
# Extract webhook payload
webhook_body = _json["body"]
username = webhook_body.get("username")
email = webhook_body.get("email")
items = webhook_body.get("items", [])

# Process and return
return [{
    "json": {
        "username": username,
        "email": email,
        "item_count": len(items),
        "processed_at": datetime.now().isoformat()
    }
}]
\`\`\`

### Aggregating Data
\`\`\`python
# Sum values across all items
all_items = _input.all()
total = sum(item["json"].get("amount", 0) for item in all_items)

return [{
    "json": {
        "total": total,
        "item_count": len(all_items),
        "average": total / len(all_items) if all_items else 0
    }
}]
\`\`\`

### Error Handling
\`\`\`python
try:
    # Process data
    webhook_data = _json["body"]
    result = process_data(webhook_data)
    
    return [{
        "json": {
            "success": True,
            "data": result
        }
    }]
except Exception as e:
    return [{
        "json": {
            "success": False,
            "error": str(e)
        }
    }]
\`\`\`

### Data Transformation
\`\`\`python
# Transform all items
all_items = _input.all()
transformed = []

for item in all_items:
    data = item["json"]
    transformed.append({
        "json": {
            "id": data.get("id"),
            "name": data.get("name", "").upper(),
            "timestamp": datetime.now().isoformat(),
            "valid": bool(data.get("email"))
        }
    })

return transformed
\`\`\`

## Limitations & Workarounds

### No External Libraries
\`\`\`python
# CANNOT USE:
# import requests  # Not available
# import pandas   # Not available
# import numpy    # Not available

# WORKAROUND: Use JavaScript Code node for HTTP requests
# Or use HTTP Request node before Code node
\`\`\`

### HTTP Requests Alternative
Since Python requests library is not available, use:
1. JavaScript Code node with $helpers.httpRequest()
2. HTTP Request node before your Python Code node
3. Webhook node to receive data

## Common Pitfalls
1. Trying to import external libraries (requests, pandas)
2. Using items[0] instead of _input.all()
3. Forgetting webhook data is under ["body"]
4. Returning dictionaries instead of [{"json": {...}}]
5. Not handling missing keys with .get()

## Best Practices
1. Always use .get() for dictionary access
2. Validate data before processing
3. Handle empty input arrays
4. Use list comprehensions for transformations
5. Return meaningful error messages

## Type Conversions
\`\`\`python
# String to number
value = float(_json.get("amount", "0"))

# Boolean conversion
is_active = str(_json.get("active", "")).lower() == "true"

# Safe JSON parsing
try:
    data = json.loads(_json.get("json_string", "{}"))
except json.JSONDecodeError:
    data = {}
\`\`\`

## Related Tools
- get_node_essentials("nodes-base.code")
- validate_node_operation()
- javascript_code_node_guide (for JavaScript syntax)`;
}
```

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

```typescript
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { NodeParser } from '@/parsers/node-parser';
import { PropertyExtractor } from '@/parsers/property-extractor';
import {
  programmaticNodeFactory,
  declarativeNodeFactory,
  triggerNodeFactory,
  webhookNodeFactory,
  aiToolNodeFactory,
  versionedNodeClassFactory,
  versionedNodeTypeClassFactory,
  malformedNodeFactory,
  nodeClassFactory,
  propertyFactory,
  stringPropertyFactory,
  optionsPropertyFactory
} from '@tests/fixtures/factories/parser-node.factory';

// Mock PropertyExtractor
vi.mock('@/parsers/property-extractor');

describe('NodeParser', () => {
  let parser: NodeParser;
  let mockPropertyExtractor: any;

  beforeEach(() => {
    vi.clearAllMocks();
    
    // Setup mock property extractor
    mockPropertyExtractor = {
      extractProperties: vi.fn().mockReturnValue([]),
      extractCredentials: vi.fn().mockReturnValue([]),
      detectAIToolCapability: vi.fn().mockReturnValue(false),
      extractOperations: vi.fn().mockReturnValue([])
    };
    
    (PropertyExtractor as any).mockImplementation(() => mockPropertyExtractor);
    
    parser = new NodeParser();
  });

  describe('parse method', () => {
    it('should parse correctly when node is programmatic', () => {
      const nodeDefinition = programmaticNodeFactory.build();
      const NodeClass = nodeClassFactory.build({ description: nodeDefinition });
      
      mockPropertyExtractor.extractProperties.mockReturnValue(nodeDefinition.properties);
      mockPropertyExtractor.extractCredentials.mockReturnValue(nodeDefinition.credentials);
      
      const result = parser.parse(NodeClass as any, 'n8n-nodes-base');
      
      expect(result).toMatchObject({
        style: 'programmatic',
        nodeType: `nodes-base.${nodeDefinition.name}`,
        displayName: nodeDefinition.displayName,
        description: nodeDefinition.description,
        category: nodeDefinition.group?.[0] || 'misc',
        packageName: 'n8n-nodes-base'
      });
      
      // Check specific properties separately to avoid strict matching
      expect(result.isVersioned).toBe(false);
      expect(result.version).toBe(nodeDefinition.version?.toString() || '1');
      
      expect(mockPropertyExtractor.extractProperties).toHaveBeenCalledWith(NodeClass);
      expect(mockPropertyExtractor.extractCredentials).toHaveBeenCalledWith(NodeClass);
    });

    it('should parse correctly when node is declarative', () => {
      const nodeDefinition = declarativeNodeFactory.build();
      const NodeClass = nodeClassFactory.build({ description: nodeDefinition });
      
      const result = parser.parse(NodeClass as any, 'n8n-nodes-base');
      
      expect(result.style).toBe('declarative');
      expect(result.nodeType).toBe(`nodes-base.${nodeDefinition.name}`);
    });

    it('should preserve type when package prefix is already included', () => {
      const nodeDefinition = programmaticNodeFactory.build({
        name: 'nodes-base.slack'
      });
      const NodeClass = nodeClassFactory.build({ description: nodeDefinition });
      
      const result = parser.parse(NodeClass as any, 'n8n-nodes-base');
      
      expect(result.nodeType).toBe('nodes-base.slack');
    });

    it('should set isTrigger flag when node is a trigger', () => {
      const nodeDefinition = triggerNodeFactory.build();
      const NodeClass = nodeClassFactory.build({ description: nodeDefinition });
      
      const result = parser.parse(NodeClass as any, 'n8n-nodes-base');
      
      expect(result.isTrigger).toBe(true);
    });

    it('should set isWebhook flag when node is a webhook', () => {
      const nodeDefinition = webhookNodeFactory.build();
      const NodeClass = nodeClassFactory.build({ description: nodeDefinition });
      
      const result = parser.parse(NodeClass as any, 'n8n-nodes-base');
      
      expect(result.isWebhook).toBe(true);
    });

    it('should set isAITool flag when node has AI capability', () => {
      const nodeDefinition = aiToolNodeFactory.build();
      const NodeClass = nodeClassFactory.build({ description: nodeDefinition });
      
      mockPropertyExtractor.detectAIToolCapability.mockReturnValue(true);
      
      const result = parser.parse(NodeClass as any, 'n8n-nodes-base');
      
      expect(result.isAITool).toBe(true);
    });

    it('should parse correctly when node uses VersionedNodeType class', () => {
      // Create a simple versioned node class without modifying function properties
      const VersionedNodeClass = class VersionedNodeType {
        baseDescription = {
          name: 'versionedNode',
          displayName: 'Versioned Node',
          description: 'A versioned node',
          defaultVersion: 2
        };
        nodeVersions = {
          1: { description: { properties: [] } },
          2: { description: { properties: [] } }
        };
        currentVersion = 2;
      };
      
      mockPropertyExtractor.extractProperties.mockReturnValue([
        propertyFactory.build(),
        propertyFactory.build()
      ]);
      
      const result = parser.parse(VersionedNodeClass as any, 'n8n-nodes-base');
      
      expect(result.isVersioned).toBe(true);
      expect(result.version).toBe('2');
      expect(result.nodeType).toBe('nodes-base.versionedNode');
    });

    it('should parse correctly when node has nodeVersions property', () => {
      const versionedDef = versionedNodeClassFactory.build();
      const NodeClass = class {
        nodeVersions = versionedDef.nodeVersions;
        baseDescription = versionedDef.baseDescription;
      };
      
      const result = parser.parse(NodeClass as any, 'n8n-nodes-base');
      
      expect(result.isVersioned).toBe(true);
      expect(result.version).toBe('2');
    });

    it('should use max version when version is an array', () => {
      const nodeDefinition = programmaticNodeFactory.build({
        version: [1, 1.1, 1.2, 2]
      });
      const NodeClass = nodeClassFactory.build({ description: nodeDefinition });
      
      const result = parser.parse(NodeClass as any, 'n8n-nodes-base');
      
      expect(result.isVersioned).toBe(true);
      expect(result.version).toBe('2'); // Should return max version
    });

    it('should throw error when node is missing name property', () => {
      const nodeDefinition = malformedNodeFactory.build();
      const NodeClass = nodeClassFactory.build({ description: nodeDefinition });
      
      expect(() => parser.parse(NodeClass as any, 'n8n-nodes-base')).toThrow('Node is missing name property');
    });

    it('should use static description when instantiation fails', () => {
      const NodeClass = class {
        static description = programmaticNodeFactory.build();
        constructor() {
          throw new Error('Cannot instantiate');
        }
      };
      
      const result = parser.parse(NodeClass as any, 'n8n-nodes-base');
      
      expect(result.displayName).toBe(NodeClass.description.displayName);
    });

    it('should extract category when using different property names', () => {
      const testCases = [
        { group: ['transform'], expected: 'transform' },
        { categories: ['output'], expected: 'output' },
        { category: 'trigger', expected: 'trigger' },
        { /* no category */ expected: 'misc' }
      ];
      
      testCases.forEach(({ group, categories, category, expected }) => {
        const nodeDefinition = programmaticNodeFactory.build({
          group,
          categories,
          category
        } as any);
        const NodeClass = nodeClassFactory.build({ description: nodeDefinition });
        
        const result = parser.parse(NodeClass as any, 'n8n-nodes-base');
        
        expect(result.category).toBe(expected);
      });
    });

    it('should set isTrigger flag when node has polling property', () => {
      const nodeDefinition = programmaticNodeFactory.build({
        polling: true
      });
      const NodeClass = nodeClassFactory.build({ description: nodeDefinition });
      
      const result = parser.parse(NodeClass as any, 'n8n-nodes-base');
      
      expect(result.isTrigger).toBe(true);
    });

    it('should set isTrigger flag when node has eventTrigger property', () => {
      const nodeDefinition = programmaticNodeFactory.build({
        eventTrigger: true
      });
      const NodeClass = nodeClassFactory.build({ description: nodeDefinition });
      
      const result = parser.parse(NodeClass as any, 'n8n-nodes-base');
      
      expect(result.isTrigger).toBe(true);
    });

    it('should set isTrigger flag when node name contains trigger', () => {
      const nodeDefinition = programmaticNodeFactory.build({
        name: 'myTrigger'
      });
      const NodeClass = nodeClassFactory.build({ description: nodeDefinition });
      
      const result = parser.parse(NodeClass as any, 'n8n-nodes-base');
      
      expect(result.isTrigger).toBe(true);
    });

    it('should set isWebhook flag when node name contains webhook', () => {
      const nodeDefinition = programmaticNodeFactory.build({
        name: 'customWebhook'
      });
      const NodeClass = nodeClassFactory.build({ description: nodeDefinition });
      
      const result = parser.parse(NodeClass as any, 'n8n-nodes-base');
      
      expect(result.isWebhook).toBe(true);
    });

    it('should parse correctly when node is an instance object', () => {
      const nodeDefinition = programmaticNodeFactory.build();
      const nodeInstance = {
        description: nodeDefinition
      };
      
      mockPropertyExtractor.extractProperties.mockReturnValue(nodeDefinition.properties);

      const result = parser.parse(nodeInstance as any, 'n8n-nodes-base');
      
      expect(result.displayName).toBe(nodeDefinition.displayName);
    });

    it('should handle different package name formats', () => {
      const nodeDefinition = programmaticNodeFactory.build();
      const NodeClass = nodeClassFactory.build({ description: nodeDefinition });
      
      const testCases = [
        { packageName: '@n8n/n8n-nodes-langchain', expectedPrefix: 'nodes-langchain' },
        { packageName: 'n8n-nodes-custom', expectedPrefix: 'nodes-custom' },
        { packageName: 'custom-package', expectedPrefix: 'custom-package' }
      ];
      
      testCases.forEach(({ packageName, expectedPrefix }) => {
        const result = parser.parse(NodeClass as any, packageName);
        expect(result.nodeType).toBe(`${expectedPrefix}.${nodeDefinition.name}`);
      });
    });
  });

  describe('version extraction', () => {
    it('should prioritize currentVersion over description.defaultVersion', () => {
      const NodeClass = class {
        currentVersion = 2.2;  // Should be returned
        description = {
          name: 'AI Agent',
          displayName: 'AI Agent',
          defaultVersion: 3  // Should be ignored when currentVersion exists
        };
      };

      const result = parser.parse(NodeClass as any, 'n8n-nodes-base');

      expect(result.version).toBe('2.2');
    });

    it('should extract version from description.defaultVersion', () => {
      const NodeClass = class {
        description = {
          name: 'test',
          displayName: 'Test',
          defaultVersion: 3
        };
      };

      const result = parser.parse(NodeClass as any, 'n8n-nodes-base');

      expect(result.version).toBe('3');
    });

    it('should handle currentVersion = 0 correctly', () => {
      const NodeClass = class {
        currentVersion = 0;  // Edge case: version 0 should be valid
        description = {
          name: 'test',
          displayName: 'Test',
          defaultVersion: 5  // Should be ignored
        };
      };

      const result = parser.parse(NodeClass as any, 'n8n-nodes-base');

      expect(result.version).toBe('0');
    });

    it('should NOT extract version from non-existent baseDescription (legacy bug)', () => {
      const NodeClass = class {
        baseDescription = {  // This property doesn't exist on VersionedNodeType!
          name: 'test',
          displayName: 'Test',
          defaultVersion: 3
        };
      };

      const result = parser.parse(NodeClass as any, 'n8n-nodes-base');

      expect(result.version).toBe('1');  // Should fallback to default
    });

    it('should extract version from nodeVersions keys', () => {
      const NodeClass = class {
        description = { name: 'test', displayName: 'Test' };
        nodeVersions = {
          1: { description: {} },
          2: { description: {} },
          3: { description: {} }
        };
      };
      
      const result = parser.parse(NodeClass as any, 'n8n-nodes-base');
      
      expect(result.version).toBe('3');
    });

    it('should extract version from instance nodeVersions', () => {
      const NodeClass = class {
        description = { name: 'test', displayName: 'Test' };
        
        constructor() {
          (this as any).nodeVersions = {
            1: { description: {} },
            2: { description: {} },
            4: { description: {} }
          };
        }
      };
      
      const result = parser.parse(NodeClass as any, 'n8n-nodes-base');
      
      expect(result.version).toBe('4');
    });

    it('should handle version as number in description', () => {
      const nodeDefinition = programmaticNodeFactory.build({
        version: 2
      });
      const NodeClass = nodeClassFactory.build({ description: nodeDefinition });
      
      const result = parser.parse(NodeClass as any, 'n8n-nodes-base');
      
      expect(result.version).toBe('2');
    });

    it('should handle version as string in description', () => {
      const nodeDefinition = programmaticNodeFactory.build({
        version: '1.5' as any
      });
      const NodeClass = nodeClassFactory.build({ description: nodeDefinition });
      
      const result = parser.parse(NodeClass as any, 'n8n-nodes-base');
      
      expect(result.version).toBe('1.5');
    });

    it('should default to version 1 when no version found', () => {
      const nodeDefinition = programmaticNodeFactory.build();
      delete (nodeDefinition as any).version;
      const NodeClass = nodeClassFactory.build({ description: nodeDefinition });
      
      const result = parser.parse(NodeClass as any, 'n8n-nodes-base');
      
      expect(result.version).toBe('1');
    });
  });

  describe('versioned node detection', () => {
    it('should detect versioned nodes with nodeVersions', () => {
      const NodeClass = class {
        description = { name: 'test', displayName: 'Test' };
        nodeVersions = { 1: {}, 2: {} };
      };
      
      const result = parser.parse(NodeClass as any, 'n8n-nodes-base');
      
      expect(result.isVersioned).toBe(true);
    });

    it('should detect versioned nodes with defaultVersion', () => {
      const NodeClass = class {
        baseDescription = {
          name: 'test',
          displayName: 'Test',
          defaultVersion: 2
        };
      };
      
      const result = parser.parse(NodeClass as any, 'n8n-nodes-base');
      
      expect(result.isVersioned).toBe(true);
    });

    it('should detect versioned nodes with version array in instance', () => {
      const NodeClass = class {
        description = {
          name: 'test',
          displayName: 'Test',
          version: [1, 1.1, 2]
        };
      };
      
      const result = parser.parse(NodeClass as any, 'n8n-nodes-base');
      
      expect(result.isVersioned).toBe(true);
    });

    it('should not detect non-versioned nodes as versioned', () => {
      const nodeDefinition = programmaticNodeFactory.build({
        version: 1
      });
      const NodeClass = nodeClassFactory.build({ description: nodeDefinition });
      
      const result = parser.parse(NodeClass as any, 'n8n-nodes-base');
      
      expect(result.isVersioned).toBe(false);
    });
  });

  describe('edge cases', () => {
    it('should handle null/undefined description gracefully', () => {
      const NodeClass = class {
        description = null;
      };
      
      expect(() => parser.parse(NodeClass as any, 'n8n-nodes-base')).toThrow();
    });

    it('should handle empty routing object for declarative nodes', () => {
      const nodeDefinition = declarativeNodeFactory.build({
        routing: {} as any
      });
      const NodeClass = nodeClassFactory.build({ description: nodeDefinition });
      
      const result = parser.parse(NodeClass as any, 'n8n-nodes-base');
      
      expect(result.style).toBe('declarative');
    });

    it('should handle complex nested versioned structure', () => {
      const NodeClass = class VersionedNodeType {
        constructor() {
          (this as any).baseDescription = {
            name: 'complex',
            displayName: 'Complex Node',
            defaultVersion: 3
          };
          (this as any).nodeVersions = {
            1: { description: { properties: [] } },
            2: { description: { properties: [] } },
            3: { description: { properties: [] } }
          };
        }
      };
      
      // Override constructor name check
      Object.defineProperty(NodeClass.prototype.constructor, 'name', {
        value: 'VersionedNodeType'
      });
      
      const result = parser.parse(NodeClass as any, 'n8n-nodes-base');
      
      expect(result.isVersioned).toBe(true);
      expect(result.version).toBe('3');
    });
  });
});
```

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

```typescript
import { NodeRepository } from '../database/node-repository';
import { logger } from '../utils/logger';

export interface NodeSuggestion {
  nodeType: string;
  displayName: string;
  confidence: number;
  reason: string;
  category?: string;
  description?: string;
}

export interface SimilarityScore {
  nameSimilarity: number;
  categoryMatch: number;
  packageMatch: number;
  patternMatch: number;
  totalScore: number;
}

export interface CommonMistakePattern {
  pattern: string;
  suggestion: string;
  confidence: number;
  reason: string;
}

export class NodeSimilarityService {
  // Constants to avoid magic numbers
  private static readonly SCORING_THRESHOLD = 50; // Minimum 50% confidence to suggest
  private static readonly TYPO_EDIT_DISTANCE = 2; // Max 2 character differences for typo detection
  private static readonly SHORT_SEARCH_LENGTH = 5; // Searches ≤5 chars need special handling
  private static readonly CACHE_DURATION_MS = 5 * 60 * 1000; // 5 minutes
  private static readonly AUTO_FIX_CONFIDENCE = 0.9; // 90% confidence for auto-fix

  private repository: NodeRepository;
  private commonMistakes: Map<string, CommonMistakePattern[]>;
  private nodeCache: any[] | null = null;
  private cacheExpiry: number = 0;
  private cacheVersion: number = 0; // Track cache version for invalidation

  constructor(repository: NodeRepository) {
    this.repository = repository;
    this.commonMistakes = this.initializeCommonMistakes();
  }

  /**
   * Initialize common mistake patterns
   * Using safer string-based patterns instead of complex regex to avoid ReDoS
   */
  private initializeCommonMistakes(): Map<string, CommonMistakePattern[]> {
    const patterns = new Map<string, CommonMistakePattern[]>();

    // Case variations - using exact string matching (case-insensitive)
    patterns.set('case_variations', [
      { pattern: 'httprequest', suggestion: 'nodes-base.httpRequest', confidence: 0.95, reason: 'Incorrect capitalization' },
      { pattern: 'webhook', suggestion: 'nodes-base.webhook', confidence: 0.95, reason: 'Incorrect capitalization' },
      { pattern: 'slack', suggestion: 'nodes-base.slack', confidence: 0.9, reason: 'Missing package prefix' },
      { pattern: 'gmail', suggestion: 'nodes-base.gmail', confidence: 0.9, reason: 'Missing package prefix' },
      { pattern: 'googlesheets', suggestion: 'nodes-base.googleSheets', confidence: 0.9, reason: 'Missing package prefix' },
      { pattern: 'telegram', suggestion: 'nodes-base.telegram', confidence: 0.9, reason: 'Missing package prefix' },
    ]);

    // Specific case variations that are common
    patterns.set('specific_variations', [
      { pattern: 'HttpRequest', suggestion: 'nodes-base.httpRequest', confidence: 0.95, reason: 'Incorrect capitalization' },
      { pattern: 'HTTPRequest', suggestion: 'nodes-base.httpRequest', confidence: 0.95, reason: 'Common capitalization mistake' },
      { pattern: 'Webhook', suggestion: 'nodes-base.webhook', confidence: 0.95, reason: 'Incorrect capitalization' },
      { pattern: 'WebHook', suggestion: 'nodes-base.webhook', confidence: 0.95, reason: 'Common capitalization mistake' },
    ]);

    // Deprecated package prefixes
    patterns.set('deprecated_prefixes', [
      { pattern: 'n8n-nodes-base.', suggestion: 'nodes-base.', confidence: 0.95, reason: 'Full package name used instead of short form' },
      { pattern: '@n8n/n8n-nodes-langchain.', suggestion: 'nodes-langchain.', confidence: 0.95, reason: 'Full package name used instead of short form' },
    ]);

    // Common typos - exact matches
    patterns.set('typos', [
      { pattern: 'htprequest', suggestion: 'nodes-base.httpRequest', confidence: 0.8, reason: 'Likely typo' },
      { pattern: 'httpreqest', suggestion: 'nodes-base.httpRequest', confidence: 0.8, reason: 'Likely typo' },
      { pattern: 'webook', suggestion: 'nodes-base.webhook', confidence: 0.8, reason: 'Likely typo' },
      { pattern: 'slak', suggestion: 'nodes-base.slack', confidence: 0.8, reason: 'Likely typo' },
      { pattern: 'googlesheets', suggestion: 'nodes-base.googleSheets', confidence: 0.8, reason: 'Likely typo' },
    ]);

    // AI/LangChain specific
    patterns.set('ai_nodes', [
      { pattern: 'openai', suggestion: 'nodes-langchain.openAi', confidence: 0.85, reason: 'AI node - incorrect package' },
      { pattern: 'nodes-base.openai', suggestion: 'nodes-langchain.openAi', confidence: 0.9, reason: 'Wrong package - OpenAI is in LangChain package' },
      { pattern: 'chatopenai', suggestion: 'nodes-langchain.lmChatOpenAi', confidence: 0.85, reason: 'LangChain node naming convention' },
      { pattern: 'vectorstore', suggestion: 'nodes-langchain.vectorStoreInMemory', confidence: 0.7, reason: 'Generic vector store reference' },
    ]);

    return patterns;
  }

  /**
   * Check if a type is a common node name without prefix
   */
  private isCommonNodeWithoutPrefix(type: string): string | null {
    const commonNodes: Record<string, string> = {
      'httprequest': 'nodes-base.httpRequest',
      'webhook': 'nodes-base.webhook',
      'slack': 'nodes-base.slack',
      'gmail': 'nodes-base.gmail',
      'googlesheets': 'nodes-base.googleSheets',
      'telegram': 'nodes-base.telegram',
      'discord': 'nodes-base.discord',
      'notion': 'nodes-base.notion',
      'airtable': 'nodes-base.airtable',
      'postgres': 'nodes-base.postgres',
      'mysql': 'nodes-base.mySql',
      'mongodb': 'nodes-base.mongoDb',
    };

    const normalized = type.toLowerCase();
    return commonNodes[normalized] || null;
  }

  /**
   * Find similar nodes for an invalid type
   */
  async findSimilarNodes(invalidType: string, limit: number = 5): Promise<NodeSuggestion[]> {
    if (!invalidType || invalidType.trim() === '') {
      return [];
    }

    const suggestions: NodeSuggestion[] = [];

    // First, check for exact common mistakes
    const mistakeSuggestion = this.checkCommonMistakes(invalidType);
    if (mistakeSuggestion) {
      suggestions.push(mistakeSuggestion);
    }

    // Get all nodes (with caching)
    const allNodes = await this.getCachedNodes();

    // Calculate similarity scores for all nodes
    const scores = allNodes.map(node => ({
      node,
      score: this.calculateSimilarityScore(invalidType, node)
    }));

    // Sort by total score and filter high scores
    scores.sort((a, b) => b.score.totalScore - a.score.totalScore);

    // Add top suggestions (excluding already added exact matches)
    for (const { node, score } of scores) {
      if (suggestions.some(s => s.nodeType === node.nodeType)) {
        continue;
      }

      if (score.totalScore >= NodeSimilarityService.SCORING_THRESHOLD) {
        suggestions.push(this.createSuggestion(node, score));
      }

      if (suggestions.length >= limit) {
        break;
      }
    }

    return suggestions;
  }

  /**
   * Check for common mistake patterns (ReDoS-safe implementation)
   */
  private checkCommonMistakes(invalidType: string): NodeSuggestion | null {
    const cleanType = invalidType.trim();
    const lowerType = cleanType.toLowerCase();

    // First check for common nodes without prefix
    const commonNodeSuggestion = this.isCommonNodeWithoutPrefix(cleanType);
    if (commonNodeSuggestion) {
      const node = this.repository.getNode(commonNodeSuggestion);
      if (node) {
        return {
          nodeType: commonNodeSuggestion,
          displayName: node.displayName,
          confidence: 0.9,
          reason: 'Missing package prefix',
          category: node.category,
          description: node.description
        };
      }
    }

    // Check deprecated prefixes (string-based, no regex)
    for (const [category, patterns] of this.commonMistakes) {
      if (category === 'deprecated_prefixes') {
        for (const pattern of patterns) {
          if (cleanType.startsWith(pattern.pattern)) {
            const actualSuggestion = cleanType.replace(pattern.pattern, pattern.suggestion);
            const node = this.repository.getNode(actualSuggestion);
            if (node) {
              return {
                nodeType: actualSuggestion,
                displayName: node.displayName,
                confidence: pattern.confidence,
                reason: pattern.reason,
                category: node.category,
                description: node.description
              };
            }
          }
        }
      }
    }

    // Check exact matches for typos and variations
    for (const [category, patterns] of this.commonMistakes) {
      if (category === 'deprecated_prefixes') continue; // Already handled

      for (const pattern of patterns) {
        // Simple string comparison (case-sensitive for specific_variations)
        const match = category === 'specific_variations'
          ? cleanType === pattern.pattern
          : lowerType === pattern.pattern.toLowerCase();

        if (match && pattern.suggestion) {
          const node = this.repository.getNode(pattern.suggestion);
          if (node) {
            return {
              nodeType: pattern.suggestion,
              displayName: node.displayName,
              confidence: pattern.confidence,
              reason: pattern.reason,
              category: node.category,
              description: node.description
            };
          }
        }
      }
    }

    return null;
  }

  /**
   * Calculate multi-factor similarity score
   */
  private calculateSimilarityScore(invalidType: string, node: any): SimilarityScore {
    const cleanInvalid = this.normalizeNodeType(invalidType);
    const cleanValid = this.normalizeNodeType(node.nodeType);
    const displayNameClean = this.normalizeNodeType(node.displayName);

    // Special handling for very short search terms (e.g., "http", "sheet")
    const isShortSearch = invalidType.length <= NodeSimilarityService.SHORT_SEARCH_LENGTH;

    // Name similarity (40% weight)
    let nameSimilarity = Math.max(
      this.getStringSimilarity(cleanInvalid, cleanValid),
      this.getStringSimilarity(cleanInvalid, displayNameClean)
    ) * 40;

    // For short searches that are substrings, give a small name similarity boost
    if (isShortSearch && (cleanValid.includes(cleanInvalid) || displayNameClean.includes(cleanInvalid))) {
      nameSimilarity = Math.max(nameSimilarity, 10);
    }

    // Category match (20% weight)
    let categoryMatch = 0;
    if (node.category) {
      const categoryClean = this.normalizeNodeType(node.category);
      if (cleanInvalid.includes(categoryClean) || categoryClean.includes(cleanInvalid)) {
        categoryMatch = 20;
      }
    }

    // Package match (15% weight)
    let packageMatch = 0;
    const invalidParts = cleanInvalid.split(/[.-]/);
    const validParts = cleanValid.split(/[.-]/);

    if (invalidParts[0] === validParts[0]) {
      packageMatch = 15;
    }

    // Pattern match (25% weight)
    let patternMatch = 0;

    // Check if it's a substring match
    if (cleanValid.includes(cleanInvalid) || displayNameClean.includes(cleanInvalid)) {
      // Boost score significantly for short searches that are exact substring matches
      // Short searches need more boost to reach the 50 threshold
      patternMatch = isShortSearch ? 45 : 25;
    } else if (this.getEditDistance(cleanInvalid, cleanValid) <= NodeSimilarityService.TYPO_EDIT_DISTANCE) {
      // Small edit distance indicates likely typo
      patternMatch = 20;
    } else if (this.getEditDistance(cleanInvalid, displayNameClean) <= NodeSimilarityService.TYPO_EDIT_DISTANCE) {
      patternMatch = 18;
    }

    // For very short searches, also check if the search term appears at the start
    if (isShortSearch && (cleanValid.startsWith(cleanInvalid) || displayNameClean.startsWith(cleanInvalid))) {
      patternMatch = Math.max(patternMatch, 40);
    }

    const totalScore = nameSimilarity + categoryMatch + packageMatch + patternMatch;

    return {
      nameSimilarity,
      categoryMatch,
      packageMatch,
      patternMatch,
      totalScore
    };
  }

  /**
   * Create a suggestion object from node and score
   */
  private createSuggestion(node: any, score: SimilarityScore): NodeSuggestion {
    let reason = 'Similar node';

    if (score.patternMatch >= 20) {
      reason = 'Name similarity';
    } else if (score.categoryMatch >= 15) {
      reason = 'Same category';
    } else if (score.packageMatch >= 10) {
      reason = 'Same package';
    }

    // Calculate confidence (0-1 scale)
    const confidence = Math.min(score.totalScore / 100, 1);

    return {
      nodeType: node.nodeType,
      displayName: node.displayName,
      confidence,
      reason,
      category: node.category,
      description: node.description
    };
  }

  /**
   * Normalize node type for comparison
   */
  private normalizeNodeType(type: string): string {
    return type
      .toLowerCase()
      .replace(/[^a-z0-9]/g, '')
      .trim();
  }

  /**
   * Calculate string similarity (0-1)
   */
  private getStringSimilarity(s1: string, s2: string): number {
    if (s1 === s2) return 1;
    if (!s1 || !s2) return 0;

    const distance = this.getEditDistance(s1, s2);
    const maxLen = Math.max(s1.length, s2.length);

    return 1 - (distance / maxLen);
  }

  /**
   * Calculate Levenshtein distance with optimizations
   * - Early termination when difference exceeds threshold
   * - Space-optimized to use only two rows instead of full matrix
   * - Fast path for identical or vastly different strings
   */
  private getEditDistance(s1: string, s2: string, maxDistance: number = 5): number {
    // Fast path: identical strings
    if (s1 === s2) return 0;

    const m = s1.length;
    const n = s2.length;

    // Fast path: length difference exceeds threshold
    const lengthDiff = Math.abs(m - n);
    if (lengthDiff > maxDistance) return maxDistance + 1;

    // Fast path: empty strings
    if (m === 0) return n;
    if (n === 0) return m;

    // Space optimization: only need previous and current row
    let prev = Array(n + 1).fill(0).map((_, i) => i);

    for (let i = 1; i <= m; i++) {
      const curr = [i];
      let minInRow = i;

      for (let j = 1; j <= n; j++) {
        const cost = s1[i - 1] === s2[j - 1] ? 0 : 1;
        const val = Math.min(
          curr[j - 1] + 1,      // deletion
          prev[j] + 1,          // insertion
          prev[j - 1] + cost    // substitution
        );
        curr.push(val);
        minInRow = Math.min(minInRow, val);
      }

      // Early termination: if minimum in this row exceeds threshold
      if (minInRow > maxDistance) {
        return maxDistance + 1;
      }

      prev = curr;
    }

    return prev[n];
  }

  /**
   * Get cached nodes or fetch from repository
   * Implements proper cache invalidation with version tracking
   */
  private async getCachedNodes(): Promise<any[]> {
    const now = Date.now();

    if (!this.nodeCache || now > this.cacheExpiry) {
      try {
        const newNodes = this.repository.getAllNodes();

        // Only update cache if we got valid data
        if (newNodes && newNodes.length > 0) {
          this.nodeCache = newNodes;
          this.cacheExpiry = now + NodeSimilarityService.CACHE_DURATION_MS;
          this.cacheVersion++;
          logger.debug('Node cache refreshed', {
            count: newNodes.length,
            version: this.cacheVersion
          });
        } else if (this.nodeCache) {
          // Return stale cache if new fetch returned empty
          logger.warn('Node fetch returned empty, using stale cache');
        }
      } catch (error) {
        logger.error('Failed to fetch nodes for similarity service', error);
        // Return stale cache on error if available
        if (this.nodeCache) {
          logger.info('Using stale cache due to fetch error');
          return this.nodeCache;
        }
        return [];
      }
    }

    return this.nodeCache || [];
  }

  /**
   * Invalidate the cache (e.g., after database updates)
   */
  public invalidateCache(): void {
    this.nodeCache = null;
    this.cacheExpiry = 0;
    this.cacheVersion++;
    logger.debug('Node cache invalidated', { version: this.cacheVersion });
  }

  /**
   * Clear and refresh cache immediately
   */
  public async refreshCache(): Promise<void> {
    this.invalidateCache();
    await this.getCachedNodes();
  }

  /**
   * Format suggestions into a user-friendly message
   */
  formatSuggestionMessage(suggestions: NodeSuggestion[], invalidType: string): string {
    if (suggestions.length === 0) {
      return `Unknown node type: "${invalidType}". No similar nodes found.`;
    }

    let message = `Unknown node type: "${invalidType}"\n\nDid you mean one of these?\n`;

    for (const suggestion of suggestions) {
      const confidence = Math.round(suggestion.confidence * 100);
      message += `• ${suggestion.nodeType} (${confidence}% match)`;

      if (suggestion.displayName) {
        message += ` - ${suggestion.displayName}`;
      }

      message += `\n  → ${suggestion.reason}`;

      if (suggestion.confidence >= 0.9) {
        message += ' (can be auto-fixed)';
      }

      message += '\n';
    }

    return message;
  }

  /**
   * Check if a suggestion is high confidence for auto-fixing
   */
  isAutoFixable(suggestion: NodeSuggestion): boolean {
    return suggestion.confidence >= NodeSimilarityService.AUTO_FIX_CONFIDENCE;
  }

  /**
   * Clear the node cache (useful after database updates)
   * @deprecated Use invalidateCache() instead for proper version tracking
   */
  clearCache(): void {
    this.invalidateCache();
  }
}
```

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

```typescript
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { z } from 'zod';
import { TelemetryEventValidator, telemetryEventSchema, workflowTelemetrySchema } from '../../../src/telemetry/event-validator';
import { TelemetryEvent, WorkflowTelemetry } from '../../../src/telemetry/telemetry-types';

// Mock logger to avoid console output in tests
vi.mock('../../../src/utils/logger', () => ({
  logger: {
    debug: vi.fn(),
    info: vi.fn(),
    warn: vi.fn(),
    error: vi.fn(),
  }
}));

describe('TelemetryEventValidator', () => {
  let validator: TelemetryEventValidator;

  beforeEach(() => {
    validator = new TelemetryEventValidator();
    vi.clearAllMocks();
  });

  describe('validateEvent()', () => {
    it('should validate a basic valid event', () => {
      const event: TelemetryEvent = {
        user_id: 'user123',
        event: 'tool_used',
        properties: { tool: 'httpRequest', success: true, duration: 500 }
      };

      const result = validator.validateEvent(event);
      expect(result).toEqual(event);
    });

    it('should validate event with specific schema for tool_used', () => {
      const event: TelemetryEvent = {
        user_id: 'user123',
        event: 'tool_used',
        properties: { tool: 'httpRequest', success: true, duration: 500 }
      };

      const result = validator.validateEvent(event);
      expect(result).not.toBeNull();
      expect(result?.properties.tool).toBe('httpRequest');
      expect(result?.properties.success).toBe(true);
      expect(result?.properties.duration).toBe(500);
    });

    it('should validate search_query event with specific schema', () => {
      const event: TelemetryEvent = {
        user_id: 'user123',
        event: 'search_query',
        properties: {
          query: 'test query',
          resultsFound: 5,
          searchType: 'nodes',
          hasResults: true,
          isZeroResults: false
        }
      };

      const result = validator.validateEvent(event);
      expect(result).not.toBeNull();
      expect(result?.properties.query).toBe('test query');
      expect(result?.properties.resultsFound).toBe(5);
      expect(result?.properties.hasResults).toBe(true);
    });

    it('should validate performance_metric event with specific schema', () => {
      const event: TelemetryEvent = {
        user_id: 'user123',
        event: 'performance_metric',
        properties: {
          operation: 'database_query',
          duration: 1500,
          isSlow: true,
          isVerySlow: false,
          metadata: { table: 'nodes' }
        }
      };

      const result = validator.validateEvent(event);
      expect(result).not.toBeNull();
      expect(result?.properties.operation).toBe('database_query');
      expect(result?.properties.duration).toBe(1500);
      expect(result?.properties.isSlow).toBe(true);
    });

    it('should sanitize sensitive data from properties', () => {
      const event: TelemetryEvent = {
        user_id: 'user123',
        event: 'generic_event',
        properties: {
          description: 'Visit https://example.com/secret and [email protected] with key abcdef123456789012345678901234567890',
          apiKey: 'super-secret-key-12345678901234567890',
          normalProp: 'normal value'
        }
      };

      const result = validator.validateEvent(event);
      expect(result).not.toBeNull();
      expect(result?.properties.description).toBe('Visit [URL] and [EMAIL] with key [KEY]');
      expect(result?.properties.normalProp).toBe('normal value');
      expect(result?.properties).not.toHaveProperty('apiKey'); // Should be filtered out
    });

    it('should handle nested object sanitization with depth limit', () => {
      const event: TelemetryEvent = {
        user_id: 'user123',
        event: 'nested_event',
        properties: {
          nested: {
            level1: {
              level2: {
                level3: {
                  level4: 'should be truncated',
                  apiKey: 'secret123',
                  description: 'Visit https://example.com'
                },
                description: 'Visit https://another.com'
              }
            }
          }
        }
      };

      const result = validator.validateEvent(event);
      expect(result).not.toBeNull();
      expect(result?.properties.nested.level1.level2.level3).toBe('[NESTED]');
      expect(result?.properties.nested.level1.level2.description).toBe('Visit [URL]');
    });

    it('should handle array sanitization with size limit', () => {
      const event: TelemetryEvent = {
        user_id: 'user123',
        event: 'array_event',
        properties: {
          items: Array.from({ length: 15 }, (_, i) => ({
            id: i,
            description: 'Visit https://example.com',
            value: `item-${i}`
          }))
        }
      };

      const result = validator.validateEvent(event);
      expect(result).not.toBeNull();
      expect(Array.isArray(result?.properties.items)).toBe(true);
      expect(result?.properties.items.length).toBe(10); // Should be limited to 10
    });

    it('should reject events with invalid user_id', () => {
      const event: TelemetryEvent = {
        user_id: '', // Empty string
        event: 'test_event',
        properties: {}
      };

      const result = validator.validateEvent(event);
      expect(result).toBeNull();
    });

    it('should reject events with invalid event name', () => {
      const event: TelemetryEvent = {
        user_id: 'user123',
        event: 'invalid-event-name!@#', // Invalid characters
        properties: {}
      };

      const result = validator.validateEvent(event);
      expect(result).toBeNull();
    });

    it('should reject tool_used event with invalid properties', () => {
      const event: TelemetryEvent = {
        user_id: 'user123',
        event: 'tool_used',
        properties: {
          tool: 'test',
          success: 'not-a-boolean', // Should be boolean
          duration: -1 // Should be positive
        }
      };

      const result = validator.validateEvent(event);
      expect(result).toBeNull();
    });

    it('should filter out sensitive keys from properties', () => {
      const event: TelemetryEvent = {
        user_id: 'user123',
        event: 'sensitive_event',
        properties: {
          password: 'secret123',
          token: 'bearer-token',
          apikey: 'api-key-value',
          secret: 'secret-value',
          credential: 'cred-value',
          auth: 'auth-header',
          url: 'https://example.com',
          endpoint: 'api.example.com',
          host: 'localhost',
          database: 'prod-db',
          normalProp: 'safe-value',
          count: 42,
          enabled: true
        }
      };

      const result = validator.validateEvent(event);
      expect(result).not.toBeNull();
      expect(result?.properties).not.toHaveProperty('password');
      expect(result?.properties).not.toHaveProperty('token');
      expect(result?.properties).not.toHaveProperty('apikey');
      expect(result?.properties).not.toHaveProperty('secret');
      expect(result?.properties).not.toHaveProperty('credential');
      expect(result?.properties).not.toHaveProperty('auth');
      expect(result?.properties).not.toHaveProperty('url');
      expect(result?.properties).not.toHaveProperty('endpoint');
      expect(result?.properties).not.toHaveProperty('host');
      expect(result?.properties).not.toHaveProperty('database');
      expect(result?.properties.normalProp).toBe('safe-value');
      expect(result?.properties.count).toBe(42);
      expect(result?.properties.enabled).toBe(true);
    });

    it('should handle validation_details event schema', () => {
      const event: TelemetryEvent = {
        user_id: 'user123',
        event: 'validation_details',
        properties: {
          nodeType: 'nodes-base.httpRequest',
          errorType: 'required_field_missing',
          errorCategory: 'validation_error',
          details: { field: 'url' }
        }
      };

      const result = validator.validateEvent(event);
      expect(result).not.toBeNull();
      expect(result?.properties.nodeType).toBe('nodes-base.httpRequest');
      expect(result?.properties.errorType).toBe('required_field_missing');
    });

    it('should handle null and undefined values', () => {
      const event: TelemetryEvent = {
        user_id: 'user123',
        event: 'null_event',
        properties: {
          nullValue: null,
          undefinedValue: undefined,
          normalValue: 'test'
        }
      };

      const result = validator.validateEvent(event);
      expect(result).not.toBeNull();
      expect(result?.properties.nullValue).toBeNull();
      expect(result?.properties.undefinedValue).toBeNull();
      expect(result?.properties.normalValue).toBe('test');
    });
  });

  describe('validateWorkflow()', () => {
    it('should validate a valid workflow', () => {
      const workflow: WorkflowTelemetry = {
        user_id: 'user123',
        workflow_hash: 'hash123',
        node_count: 3,
        node_types: ['webhook', 'httpRequest', 'set'],
        has_trigger: true,
        has_webhook: true,
        complexity: 'medium',
        sanitized_workflow: {
          nodes: [
            { id: '1', type: 'webhook' },
            { id: '2', type: 'httpRequest' },
            { id: '3', type: 'set' }
          ],
          connections: { '1': { main: [[{ node: '2', type: 'main', index: 0 }]] } }
        }
      };

      const result = validator.validateWorkflow(workflow);
      expect(result).toEqual(workflow);
    });

    it('should reject workflow with too many nodes', () => {
      const workflow: WorkflowTelemetry = {
        user_id: 'user123',
        workflow_hash: 'hash123',
        node_count: 1001, // Over limit
        node_types: ['webhook'],
        has_trigger: true,
        has_webhook: true,
        complexity: 'complex',
        sanitized_workflow: {
          nodes: [],
          connections: {}
        }
      };

      const result = validator.validateWorkflow(workflow);
      expect(result).toBeNull();
    });

    it('should reject workflow with invalid complexity', () => {
      const workflow = {
        user_id: 'user123',
        workflow_hash: 'hash123',
        node_count: 3,
        node_types: ['webhook'],
        has_trigger: true,
        has_webhook: true,
        complexity: 'invalid' as any, // Invalid complexity
        sanitized_workflow: {
          nodes: [],
          connections: {}
        }
      };

      const result = validator.validateWorkflow(workflow);
      expect(result).toBeNull();
    });

    it('should reject workflow with too many node types', () => {
      const workflow: WorkflowTelemetry = {
        user_id: 'user123',
        workflow_hash: 'hash123',
        node_count: 3,
        node_types: Array.from({ length: 101 }, (_, i) => `node-${i}`), // Over limit
        has_trigger: true,
        has_webhook: true,
        complexity: 'complex',
        sanitized_workflow: {
          nodes: [],
          connections: {}
        }
      };

      const result = validator.validateWorkflow(workflow);
      expect(result).toBeNull();
    });
  });

  describe('getStats()', () => {
    it('should track validation statistics', () => {
      const validEvent: TelemetryEvent = {
        user_id: 'user123',
        event: 'valid_event',
        properties: {}
      };

      const invalidEvent: TelemetryEvent = {
        user_id: '', // Invalid
        event: 'invalid_event',
        properties: {}
      };

      validator.validateEvent(validEvent);
      validator.validateEvent(validEvent);
      validator.validateEvent(invalidEvent);

      const stats = validator.getStats();
      expect(stats.successes).toBe(2);
      expect(stats.errors).toBe(1);
      expect(stats.total).toBe(3);
      expect(stats.errorRate).toBeCloseTo(0.333, 3);
    });

    it('should handle division by zero in error rate', () => {
      const stats = validator.getStats();
      expect(stats.errorRate).toBe(0);
    });
  });

  describe('resetStats()', () => {
    it('should reset validation statistics', () => {
      const validEvent: TelemetryEvent = {
        user_id: 'user123',
        event: 'valid_event',
        properties: {}
      };

      validator.validateEvent(validEvent);
      validator.resetStats();

      const stats = validator.getStats();
      expect(stats.successes).toBe(0);
      expect(stats.errors).toBe(0);
      expect(stats.total).toBe(0);
      expect(stats.errorRate).toBe(0);
    });
  });

  describe('Schema validation', () => {
    describe('telemetryEventSchema', () => {
      it('should validate with created_at timestamp', () => {
        const event = {
          user_id: 'user123',
          event: 'test_event',
          properties: {},
          created_at: '2024-01-01T00:00:00Z'
        };

        const result = telemetryEventSchema.safeParse(event);
        expect(result.success).toBe(true);
      });

      it('should reject invalid datetime format', () => {
        const event = {
          user_id: 'user123',
          event: 'test_event',
          properties: {},
          created_at: 'invalid-date'
        };

        const result = telemetryEventSchema.safeParse(event);
        expect(result.success).toBe(false);
      });

      it('should enforce user_id length limits', () => {
        const longUserId = 'a'.repeat(65);
        const event = {
          user_id: longUserId,
          event: 'test_event',
          properties: {}
        };

        const result = telemetryEventSchema.safeParse(event);
        expect(result.success).toBe(false);
      });

      it('should enforce event name regex pattern', () => {
        const event = {
          user_id: 'user123',
          event: 'invalid event name with spaces!',
          properties: {}
        };

        const result = telemetryEventSchema.safeParse(event);
        expect(result.success).toBe(false);
      });
    });

    describe('workflowTelemetrySchema', () => {
      it('should enforce node array size limits', () => {
        const workflow = {
          user_id: 'user123',
          workflow_hash: 'hash123',
          node_count: 3,
          node_types: ['test'],
          has_trigger: true,
          has_webhook: false,
          complexity: 'simple',
          sanitized_workflow: {
            nodes: Array.from({ length: 1001 }, (_, i) => ({ id: i })), // Over limit
            connections: {}
          }
        };

        const result = workflowTelemetrySchema.safeParse(workflow);
        expect(result.success).toBe(false);
      });

      it('should validate with optional created_at', () => {
        const workflow = {
          user_id: 'user123',
          workflow_hash: 'hash123',
          node_count: 1,
          node_types: ['webhook'],
          has_trigger: true,
          has_webhook: true,
          complexity: 'simple',
          sanitized_workflow: {
            nodes: [{ id: '1' }],
            connections: {}
          },
          created_at: '2024-01-01T00:00:00Z'
        };

        const result = workflowTelemetrySchema.safeParse(workflow);
        expect(result.success).toBe(true);
      });
    });
  });

  describe('String sanitization edge cases', () => {
    it('should handle multiple URLs in same string', () => {
      const event: TelemetryEvent = {
        user_id: 'user123',
        event: 'test_event',
        properties: {
          description: 'Visit https://example.com or http://test.com for more info'
        }
      };

      const result = validator.validateEvent(event);
      expect(result?.properties.description).toBe('Visit [URL] or [URL] for more info');
    });

    it('should handle mixed sensitive content', () => {
      const event: TelemetryEvent = {
        user_id: 'user123',
        event: 'test_event',
        properties: {
          message: 'Contact [email protected] at https://secure.com with key abc123def456ghi789jkl012mno345pqr'
        }
      };

      const result = validator.validateEvent(event);
      expect(result?.properties.message).toBe('Contact [EMAIL] at [URL] with key [KEY]');
    });

    it('should preserve non-sensitive content', () => {
      const event: TelemetryEvent = {
        user_id: 'user123',
        event: 'test_event',
        properties: {
          status: 'success',
          count: 42,
          enabled: true,
          short_id: 'abc123' // Too short to be considered a key
        }
      };

      const result = validator.validateEvent(event);
      expect(result?.properties.status).toBe('success');
      expect(result?.properties.count).toBe(42);
      expect(result?.properties.enabled).toBe(true);
      expect(result?.properties.short_id).toBe('abc123');
    });
  });

  describe('Error handling', () => {
    it('should handle Zod parsing errors gracefully', () => {
      const invalidEvent = {
        user_id: 123, // Should be string
        event: 'test_event',
        properties: {}
      };

      const result = validator.validateEvent(invalidEvent as any);
      expect(result).toBeNull();
    });

    it('should handle unexpected errors during validation', () => {
      const eventWithCircularRef: any = {
        user_id: 'user123',
        event: 'test_event',
        properties: {}
      };
      // Create circular reference
      eventWithCircularRef.properties.self = eventWithCircularRef;

      const result = validator.validateEvent(eventWithCircularRef);
      // Should handle gracefully and not throw
      expect(result).not.toThrow;
    });
  });
});
```

--------------------------------------------------------------------------------
/scripts/test-release-automation.js:
--------------------------------------------------------------------------------

```javascript
#!/usr/bin/env node

/**
 * Test script for release automation
 * Validates the release workflow components locally
 */

const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');

// Color codes for output
const colors = {
  reset: '\x1b[0m',
  red: '\x1b[31m',
  green: '\x1b[32m',
  yellow: '\x1b[33m',
  blue: '\x1b[34m',
  magenta: '\x1b[35m',
  cyan: '\x1b[36m'
};

function log(message, color = 'reset') {
  console.log(`${colors[color]}${message}${colors.reset}`);
}

function header(title) {
  log(`\n${'='.repeat(60)}`, 'cyan');
  log(`🧪 ${title}`, 'cyan');
  log(`${'='.repeat(60)}`, 'cyan');
}

function section(title) {
  log(`\n📋 ${title}`, 'blue');
  log(`${'-'.repeat(40)}`, 'blue');
}

function success(message) {
  log(`✅ ${message}`, 'green');
}

function warning(message) {
  log(`⚠️  ${message}`, 'yellow');
}

function error(message) {
  log(`❌ ${message}`, 'red');
}

function info(message) {
  log(`ℹ️  ${message}`, 'blue');
}

class ReleaseAutomationTester {
  constructor() {
    this.rootDir = path.resolve(__dirname, '..');
    this.errors = [];
    this.warnings = [];
  }

  /**
   * Test if required files exist
   */
  testFileExistence() {
    section('Testing File Existence');
    
    const requiredFiles = [
      'package.json',
      'package.runtime.json',
      'docs/CHANGELOG.md',
      '.github/workflows/release.yml',
      'scripts/sync-runtime-version.js',
      'scripts/publish-npm.sh'
    ];

    for (const file of requiredFiles) {
      const filePath = path.join(this.rootDir, file);
      if (fs.existsSync(filePath)) {
        success(`Found: ${file}`);
      } else {
        error(`Missing: ${file}`);
        this.errors.push(`Missing required file: ${file}`);
      }
    }
  }

  /**
   * Test version detection logic
   */
  testVersionDetection() {
    section('Testing Version Detection');
    
    try {
      const packageJson = require(path.join(this.rootDir, 'package.json'));
      const runtimeJson = require(path.join(this.rootDir, 'package.runtime.json'));
      
      success(`Package.json version: ${packageJson.version}`);
      success(`Runtime package version: ${runtimeJson.version}`);
      
      if (packageJson.version === runtimeJson.version) {
        success('Version sync: Both versions match');
      } else {
        warning('Version sync: Versions do not match - run sync:runtime-version');
        this.warnings.push('Package versions are not synchronized');
      }
      
      // Test semantic version format
      const semverRegex = /^\d+\.\d+\.\d+(?:-[\w\.-]+)?(?:\+[\w\.-]+)?$/;
      if (semverRegex.test(packageJson.version)) {
        success(`Version format: Valid semantic version (${packageJson.version})`);
      } else {
        error(`Version format: Invalid semantic version (${packageJson.version})`);
        this.errors.push('Invalid semantic version format');
      }
      
    } catch (err) {
      error(`Version detection failed: ${err.message}`);
      this.errors.push(`Version detection error: ${err.message}`);
    }
  }

  /**
   * Test changelog parsing
   */
  testChangelogParsing() {
    section('Testing Changelog Parsing');
    
    try {
      const changelogPath = path.join(this.rootDir, 'docs/CHANGELOG.md');
      
      if (!fs.existsSync(changelogPath)) {
        error('Changelog file not found');
        this.errors.push('Missing changelog file');
        return;
      }
      
      const changelogContent = fs.readFileSync(changelogPath, 'utf8');
      const packageJson = require(path.join(this.rootDir, 'package.json'));
      const currentVersion = packageJson.version;
      
      // Check if current version exists in changelog
      const versionRegex = new RegExp(`^## \\[${currentVersion.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\]`, 'm');
      
      if (versionRegex.test(changelogContent)) {
        success(`Changelog entry found for version ${currentVersion}`);
        
        // Test extraction logic (simplified version of the GitHub Actions script)
        const lines = changelogContent.split('\n');
        let startIndex = -1;
        let endIndex = -1;
        
        for (let i = 0; i < lines.length; i++) {
          if (versionRegex.test(lines[i])) {
            startIndex = i;
            break;
          }
        }
        
        if (startIndex !== -1) {
          // Find the end of this version's section
          for (let i = startIndex + 1; i < lines.length; i++) {
            if (lines[i].startsWith('## [') && !lines[i].includes('Unreleased')) {
              endIndex = i;
              break;
            }
          }
          
          if (endIndex === -1) {
            endIndex = lines.length;
          }
          
          const sectionLines = lines.slice(startIndex + 1, endIndex);
          const contentLines = sectionLines.filter(line => line.trim() !== '');
          
          if (contentLines.length > 0) {
            success(`Changelog content extracted: ${contentLines.length} lines`);
            info(`Preview: ${contentLines[0].substring(0, 100)}...`);
          } else {
            warning('Changelog section appears to be empty');
            this.warnings.push(`Empty changelog section for version ${currentVersion}`);
          }
        }
        
      } else {
        warning(`No changelog entry found for current version ${currentVersion}`);
        this.warnings.push(`Missing changelog entry for version ${currentVersion}`);
      }
      
      // Check changelog format
      if (changelogContent.includes('## [Unreleased]')) {
        success('Changelog format: Contains Unreleased section');
      } else {
        warning('Changelog format: Missing Unreleased section');
      }
      
      if (changelogContent.includes('Keep a Changelog')) {
        success('Changelog format: Follows Keep a Changelog format');
      } else {
        warning('Changelog format: Does not reference Keep a Changelog');
      }
      
    } catch (err) {
      error(`Changelog parsing failed: ${err.message}`);
      this.errors.push(`Changelog parsing error: ${err.message}`);
    }
  }

  /**
   * Test build process
   */
  testBuildProcess() {
    section('Testing Build Process');
    
    try {
      // Check if dist directory exists
      const distPath = path.join(this.rootDir, 'dist');
      if (fs.existsSync(distPath)) {
        success('Build output: dist directory exists');
        
        // Check for key build files
        const keyFiles = [
          'dist/index.js',
          'dist/mcp/index.js',
          'dist/mcp/server.js'
        ];
        
        for (const file of keyFiles) {
          const filePath = path.join(this.rootDir, file);
          if (fs.existsSync(filePath)) {
            success(`Build file: ${file} exists`);
          } else {
            warning(`Build file: ${file} missing - run 'npm run build'`);
            this.warnings.push(`Missing build file: ${file}`);
          }
        }
        
      } else {
        warning('Build output: dist directory missing - run "npm run build"');
        this.warnings.push('Missing build output');
      }
      
      // Check database
      const dbPath = path.join(this.rootDir, 'data/nodes.db');
      if (fs.existsSync(dbPath)) {
        const stats = fs.statSync(dbPath);
        success(`Database: nodes.db exists (${Math.round(stats.size / 1024 / 1024)}MB)`);
      } else {
        warning('Database: nodes.db missing - run "npm run rebuild"');
        this.warnings.push('Missing database file');
      }
      
    } catch (err) {
      error(`Build process test failed: ${err.message}`);
      this.errors.push(`Build process error: ${err.message}`);
    }
  }

  /**
   * Test npm publish preparation
   */
  testNpmPublishPrep() {
    section('Testing NPM Publish Preparation');
    
    try {
      const packageJson = require(path.join(this.rootDir, 'package.json'));
      const runtimeJson = require(path.join(this.rootDir, 'package.runtime.json'));
      
      // Check package.json fields
      const requiredFields = ['name', 'version', 'description', 'main', 'bin'];
      for (const field of requiredFields) {
        if (packageJson[field]) {
          success(`Package field: ${field} is present`);
        } else {
          error(`Package field: ${field} is missing`);
          this.errors.push(`Missing package.json field: ${field}`);
        }
      }
      
      // Check runtime dependencies
      if (runtimeJson.dependencies) {
        const depCount = Object.keys(runtimeJson.dependencies).length;
        success(`Runtime dependencies: ${depCount} packages`);
        
        // List key dependencies
        const keyDeps = ['@modelcontextprotocol/sdk', 'express', 'sql.js'];
        for (const dep of keyDeps) {
          if (runtimeJson.dependencies[dep]) {
            success(`Key dependency: ${dep} (${runtimeJson.dependencies[dep]})`);
          } else {
            warning(`Key dependency: ${dep} is missing`);
            this.warnings.push(`Missing key dependency: ${dep}`);
          }
        }
        
      } else {
        error('Runtime package has no dependencies');
        this.errors.push('Missing runtime dependencies');
      }
      
      // Check files array
      if (packageJson.files && Array.isArray(packageJson.files)) {
        success(`Package files: ${packageJson.files.length} patterns specified`);
        info(`Files: ${packageJson.files.join(', ')}`);
      } else {
        warning('Package files: No files array specified');
        this.warnings.push('No files array in package.json');
      }
      
    } catch (err) {
      error(`NPM publish prep test failed: ${err.message}`);
      this.errors.push(`NPM publish prep error: ${err.message}`);
    }
  }

  /**
   * Test Docker configuration
   */
  testDockerConfig() {
    section('Testing Docker Configuration');
    
    try {
      const dockerfiles = ['Dockerfile', 'Dockerfile.railway'];
      
      for (const dockerfile of dockerfiles) {
        const dockerfilePath = path.join(this.rootDir, dockerfile);
        if (fs.existsSync(dockerfilePath)) {
          success(`Dockerfile: ${dockerfile} exists`);
          
          const content = fs.readFileSync(dockerfilePath, 'utf8');
          
          // Check for key instructions
          if (content.includes('FROM node:')) {
            success(`${dockerfile}: Uses Node.js base image`);
          } else {
            warning(`${dockerfile}: Does not use standard Node.js base image`);
          }
          
          if (content.includes('COPY dist')) {
            success(`${dockerfile}: Copies build output`);
          } else {
            warning(`${dockerfile}: May not copy build output correctly`);
          }
          
        } else {
          warning(`Dockerfile: ${dockerfile} not found`);
          this.warnings.push(`Missing Dockerfile: ${dockerfile}`);
        }
      }
      
      // Check docker-compose files
      const composeFiles = ['docker-compose.yml', 'docker-compose.n8n.yml'];
      for (const composeFile of composeFiles) {
        const composePath = path.join(this.rootDir, composeFile);
        if (fs.existsSync(composePath)) {
          success(`Docker Compose: ${composeFile} exists`);
        } else {
          info(`Docker Compose: ${composeFile} not found (optional)`);
        }
      }
      
    } catch (err) {
      error(`Docker config test failed: ${err.message}`);
      this.errors.push(`Docker config error: ${err.message}`);
    }
  }

  /**
   * Test workflow file syntax
   */
  testWorkflowSyntax() {
    section('Testing Workflow Syntax');
    
    try {
      const workflowPath = path.join(this.rootDir, '.github/workflows/release.yml');
      
      if (!fs.existsSync(workflowPath)) {
        error('Release workflow file not found');
        this.errors.push('Missing release workflow file');
        return;
      }
      
      const workflowContent = fs.readFileSync(workflowPath, 'utf8');
      
      // Basic YAML structure checks
      if (workflowContent.includes('name: Automated Release')) {
        success('Workflow: Has correct name');
      } else {
        warning('Workflow: Name may be incorrect');
      }
      
      if (workflowContent.includes('on:') && workflowContent.includes('push:')) {
        success('Workflow: Has push trigger');
      } else {
        error('Workflow: Missing push trigger');
        this.errors.push('Workflow missing push trigger');
      }
      
      if (workflowContent.includes('branches: [main]')) {
        success('Workflow: Configured for main branch');
      } else {
        warning('Workflow: May not be configured for main branch');
      }
      
      // Check for required jobs
      const requiredJobs = [
        'detect-version-change',
        'extract-changelog',
        'create-release',
        'publish-npm',
        'build-docker'
      ];
      
      for (const job of requiredJobs) {
        if (workflowContent.includes(`${job}:`)) {
          success(`Workflow job: ${job} defined`);
        } else {
          error(`Workflow job: ${job} missing`);
          this.errors.push(`Missing workflow job: ${job}`);
        }
      }
      
      // Check for secrets usage
      if (workflowContent.includes('${{ secrets.NPM_TOKEN }}')) {
        success('Workflow: NPM_TOKEN secret configured');
      } else {
        warning('Workflow: NPM_TOKEN secret may be missing');
        this.warnings.push('NPM_TOKEN secret may need to be configured');
      }
      
      if (workflowContent.includes('${{ secrets.GITHUB_TOKEN }}')) {
        success('Workflow: GITHUB_TOKEN secret configured');
      } else {
        warning('Workflow: GITHUB_TOKEN secret may be missing');
      }
      
    } catch (err) {
      error(`Workflow syntax test failed: ${err.message}`);
      this.errors.push(`Workflow syntax error: ${err.message}`);
    }
  }

  /**
   * Test environment and dependencies
   */
  testEnvironment() {
    section('Testing Environment');
    
    try {
      // Check Node.js version
      const nodeVersion = process.version;
      success(`Node.js version: ${nodeVersion}`);
      
      // Check if npm is available
      try {
        const npmVersion = execSync('npm --version', { encoding: 'utf8', stdio: 'pipe' }).trim();
        success(`NPM version: ${npmVersion}`);
      } catch (err) {
        error('NPM not available');
        this.errors.push('NPM not available');
      }
      
      // Check if git is available
      try {
        const gitVersion = execSync('git --version', { encoding: 'utf8', stdio: 'pipe' }).trim();
        success(`Git available: ${gitVersion}`);
      } catch (err) {
        error('Git not available');
        this.errors.push('Git not available');
      }
      
      // Check if we're in a git repository
      try {
        execSync('git rev-parse --git-dir', { stdio: 'pipe' });
        success('Git repository: Detected');
        
        // Check current branch
        try {
          const branch = execSync('git branch --show-current', { encoding: 'utf8', stdio: 'pipe' }).trim();
          info(`Current branch: ${branch}`);
        } catch (err) {
          info('Could not determine current branch');
        }
        
      } catch (err) {
        warning('Not in a git repository');
        this.warnings.push('Not in a git repository');
      }
      
    } catch (err) {
      error(`Environment test failed: ${err.message}`);
      this.errors.push(`Environment error: ${err.message}`);
    }
  }

  /**
   * Run all tests
   */
  async runAllTests() {
    header('Release Automation Test Suite');
    
    info('Testing release automation components...');
    
    this.testFileExistence();
    this.testVersionDetection();
    this.testChangelogParsing();
    this.testBuildProcess();
    this.testNpmPublishPrep();
    this.testDockerConfig();
    this.testWorkflowSyntax();
    this.testEnvironment();
    
    // Summary
    header('Test Summary');
    
    if (this.errors.length === 0 && this.warnings.length === 0) {
      log('🎉 All tests passed! Release automation is ready.', 'green');
    } else {
      if (this.errors.length > 0) {
        log(`\n❌ ${this.errors.length} Error(s):`, 'red');
        this.errors.forEach(err => log(`   • ${err}`, 'red'));
      }
      
      if (this.warnings.length > 0) {
        log(`\n⚠️  ${this.warnings.length} Warning(s):`, 'yellow');
        this.warnings.forEach(warn => log(`   • ${warn}`, 'yellow'));
      }
      
      if (this.errors.length > 0) {
        log('\n🔧 Please fix the errors before running the release workflow.', 'red');
        process.exit(1);
      } else {
        log('\n✅ No critical errors found. Warnings should be reviewed but won\'t prevent releases.', 'yellow');
      }
    }
    
    // Next steps
    log('\n📋 Next Steps:', 'cyan');
    log('1. Ensure all secrets are configured in GitHub repository settings:', 'cyan');
    log('   • NPM_TOKEN (required for npm publishing)', 'cyan');
    log('   • GITHUB_TOKEN (automatically available)', 'cyan');
    log('\n2. To trigger a release:', 'cyan');
    log('   • Update version in package.json', 'cyan');
    log('   • Update changelog in docs/CHANGELOG.md', 'cyan');
    log('   • Commit and push to main branch', 'cyan');
    log('\n3. Monitor the release workflow in GitHub Actions', 'cyan');
    
    return this.errors.length === 0;
  }
}

// Run the tests
if (require.main === module) {
  const tester = new ReleaseAutomationTester();
  tester.runAllTests().catch(err => {
    console.error('Test suite failed:', err);
    process.exit(1);
  });
}

module.exports = ReleaseAutomationTester;
```

--------------------------------------------------------------------------------
/tests/integration/mcp-protocol/error-handling.test.ts:
--------------------------------------------------------------------------------

```typescript
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js';
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { TestableN8NMCPServer } from './test-helpers';

describe('MCP Error Handling', () => {
  let mcpServer: TestableN8NMCPServer;
  let client: Client;

  beforeEach(async () => {
    mcpServer = new TestableN8NMCPServer();
    await mcpServer.initialize();
    
    const [serverTransport, clientTransport] = InMemoryTransport.createLinkedPair();
    await mcpServer.connectToTransport(serverTransport);
    
    client = new Client({
      name: 'test-client',
      version: '1.0.0'
    }, {
      capabilities: {}
    });
    
    await client.connect(clientTransport);
  });

  afterEach(async () => {
    await client.close();
    await mcpServer.close();
  });

  describe('JSON-RPC Error Codes', () => {
    it('should handle invalid request (parse error)', async () => {
      // The MCP SDK handles parsing, so we test with invalid method instead
      try {
        await (client as any).request({
          method: '',  // Empty method
          params: {}
        });
        expect.fail('Should have thrown an error');
      } catch (error: any) {
        expect(error).toBeDefined();
      }
    });

    it('should handle method not found', async () => {
      try {
        await (client as any).request({
          method: 'nonexistent/method',
          params: {}
        });
        expect.fail('Should have thrown an error');
      } catch (error: any) {
        expect(error).toBeDefined();
        expect(error.message).toContain('not found');
      }
    });

    it('should handle invalid params', async () => {
      try {
        // Missing required parameter
        await client.callTool({ name: 'get_node_info', arguments: {} });
        expect.fail('Should have thrown an error');
      } catch (error: any) {
        expect(error).toBeDefined();
        // The error now properly validates required parameters
        expect(error.message).toContain("Missing required parameters");
      }
    });

    it('should handle internal errors gracefully', async () => {
      try {
        // Invalid node type format should cause internal processing error
        await client.callTool({ name: 'get_node_info', arguments: {
          nodeType: 'completely-invalid-format-$$$$'
        } });
        expect.fail('Should have thrown an error');
      } catch (error: any) {
        expect(error).toBeDefined();
        expect(error.message).toContain('not found');
      }
    });
  });

  describe('Tool-Specific Errors', () => {
    describe('Node Discovery Errors', () => {
      it('should handle invalid category filter', async () => {
        const response = await client.callTool({ name: 'list_nodes', arguments: {
          category: 'invalid_category'
        } });

        // Should return empty array, not error
        const result = JSON.parse((response as any).content[0].text);
        expect(result).toHaveProperty('nodes');
        expect(Array.isArray(result.nodes)).toBe(true);
        expect(result.nodes).toHaveLength(0);
      });

      it('should handle invalid search mode', async () => {
        try {
          await client.callTool({ name: 'search_nodes', arguments: {
            query: 'test',
            mode: 'INVALID_MODE' as any
          } });
          expect.fail('Should have thrown an error');
        } catch (error: any) {
          expect(error).toBeDefined();
        }
      });

      it('should handle empty search query', async () => {
        try {
          await client.callTool({ name: 'search_nodes', arguments: {
            query: ''
          } });
          expect.fail('Should have thrown an error');
        } catch (error: any) {
          expect(error).toBeDefined();
          expect(error.message).toContain("search_nodes: Validation failed:");
          expect(error.message).toContain("query: query cannot be empty");
        }
      });

      it('should handle non-existent node types', async () => {
        try {
          await client.callTool({ name: 'get_node_info', arguments: {
            nodeType: 'nodes-base.thisDoesNotExist'
          } });
          expect.fail('Should have thrown an error');
        } catch (error: any) {
          expect(error).toBeDefined();
          expect(error.message).toContain('not found');
        }
      });
    });

    describe('Validation Errors', () => {
      it('should handle invalid validation profile', async () => {
        try {
          await client.callTool({ name: 'validate_node_operation', arguments: {
            nodeType: 'nodes-base.httpRequest',
            config: { method: 'GET', url: 'https://api.example.com' },
            profile: 'invalid_profile' as any
          } });
          expect.fail('Should have thrown an error');
        } catch (error: any) {
          expect(error).toBeDefined();
        }
      });

      it('should handle malformed workflow structure', async () => {
        try {
          await client.callTool({ name: 'validate_workflow', arguments: {
            workflow: {
              // Missing required 'nodes' array
              connections: {}
            }
          } });
          expect.fail('Should have thrown an error');
        } catch (error: any) {
          expect(error).toBeDefined();
          expect(error.message).toContain("validate_workflow: Validation failed:");
          expect(error.message).toContain("workflow.nodes: workflow.nodes is required");
        }
      });

      it('should handle circular workflow references', async () => {
        const workflow = {
          nodes: [
            {
              id: '1',
              name: 'Node1',
              type: 'nodes-base.noOp',
              typeVersion: 1,
              position: [0, 0],
              parameters: {}
            },
            {
              id: '2',
              name: 'Node2',
              type: 'nodes-base.noOp',
              typeVersion: 1,
              position: [250, 0],
              parameters: {}
            }
          ],
          connections: {
            'Node1': {
              'main': [[{ node: 'Node2', type: 'main', index: 0 }]]
            },
            'Node2': {
              'main': [[{ node: 'Node1', type: 'main', index: 0 }]]
            }
          }
        };

        const response = await client.callTool({ name: 'validate_workflow', arguments: {
          workflow
        } });

        const validation = JSON.parse((response as any).content[0].text);
        expect(validation.warnings).toBeDefined();
      });
    });

    describe('Documentation Errors', () => {
      it('should handle non-existent documentation topics', async () => {
        const response = await client.callTool({ name: 'tools_documentation', arguments: {
          topic: 'completely_fake_tool'
        } });

        expect((response as any).content[0].text).toContain('not found');
      });

      it('should handle invalid depth parameter', async () => {
        try {
          await client.callTool({ name: 'tools_documentation', arguments: {
            depth: 'invalid_depth' as any
          } });
          expect.fail('Should have thrown an error');
        } catch (error: any) {
          expect(error).toBeDefined();
        }
      });
    });
  });

  describe('Large Payload Handling', () => {
    it('should handle large node info requests', async () => {
      // HTTP Request node has extensive properties
      const response = await client.callTool({ name: 'get_node_info', arguments: {
        nodeType: 'nodes-base.httpRequest'
      } });

      expect((response as any).content[0].text.length).toBeGreaterThan(10000);
      
      // Should be valid JSON
      const nodeInfo = JSON.parse((response as any).content[0].text);
      expect(nodeInfo).toHaveProperty('properties');
    });

    it('should handle large workflow validation', async () => {
      // Create a large workflow
      const nodes = [];
      const connections: any = {};

      for (let i = 0; i < 50; i++) {
        const nodeName = `Node${i}`;
        nodes.push({
          id: String(i),
          name: nodeName,
          type: 'nodes-base.noOp',
          typeVersion: 1,
          position: [i * 100, 0],
          parameters: {}
        });

        if (i > 0) {
          const prevNode = `Node${i - 1}`;
          connections[prevNode] = {
            'main': [[{ node: nodeName, type: 'main', index: 0 }]]
          };
        }
      }

      const response = await client.callTool({ name: 'validate_workflow', arguments: {
        workflow: { nodes, connections }
      } });

      const validation = JSON.parse((response as any).content[0].text);
      expect(validation).toHaveProperty('valid');
    });

    it('should handle many concurrent requests', async () => {
      const requestCount = 50;
      const promises = [];

      for (let i = 0; i < requestCount; i++) {
        promises.push(
          client.callTool({ name: 'list_nodes', arguments: {
            limit: 1,
            category: i % 2 === 0 ? 'trigger' : 'transform'
          } })
        );
      }

      const responses = await Promise.all(promises);
      expect(responses).toHaveLength(requestCount);
    });
  });

  describe('Invalid JSON Handling', () => {
    it('should handle invalid JSON in tool parameters', async () => {
      try {
        // Config should be an object, not a string
        await client.callTool({ name: 'validate_node_operation', arguments: {
          nodeType: 'nodes-base.httpRequest',
          config: 'invalid json string' as any
        } });
        expect.fail('Should have thrown an error');
      } catch (error: any) {
        expect(error).toBeDefined();
      }
    });

    it('should handle malformed workflow JSON', async () => {
      try {
        await client.callTool({ name: 'validate_workflow', arguments: {
          workflow: 'not a valid workflow object' as any
        } });
        expect.fail('Should have thrown an error');
      } catch (error: any) {
        expect(error).toBeDefined();
      }
    });
  });

  describe('Timeout Scenarios', () => {
    it('should handle rapid sequential requests', async () => {
      const start = Date.now();
      
      for (let i = 0; i < 20; i++) {
        await client.callTool({ name: 'get_database_statistics', arguments: {} });
      }

      const duration = Date.now() - start;
      
      // Should complete reasonably quickly (under 5 seconds)
      expect(duration).toBeLessThan(5000);
    });

    it('should handle long-running operations', async () => {
      // Search with complex query that requires more processing
      const response = await client.callTool({ name: 'search_nodes', arguments: {
        query: 'a b c d e f g h i j k l m n o p q r s t u v w x y z',
        mode: 'AND'
      } });

      expect(response).toBeDefined();
    });
  });

  describe('Memory Pressure', () => {
    it('should handle multiple large responses', async () => {
      const promises = [];

      // Request multiple large node infos
      const largeNodes = [
        'nodes-base.httpRequest',
        'nodes-base.postgres',
        'nodes-base.googleSheets',
        'nodes-base.slack',
        'nodes-base.gmail'
      ];

      for (const nodeType of largeNodes) {
        promises.push(
          client.callTool({ name: 'get_node_info', arguments: { nodeType } })
            .catch(() => null) // Some might not exist
        );
      }

      const responses = await Promise.all(promises);
      const validResponses = responses.filter(r => r !== null);
      
      expect(validResponses.length).toBeGreaterThan(0);
    });

    it('should handle workflow with many nodes', async () => {
      const nodeCount = 100;
      const nodes = [];

      for (let i = 0; i < nodeCount; i++) {
        nodes.push({
          id: String(i),
          name: `Node${i}`,
          type: 'nodes-base.noOp',
          typeVersion: 1,
          position: [i * 50, Math.floor(i / 10) * 100],
          parameters: {
            // Add some data to increase memory usage
            data: `This is some test data for node ${i}`.repeat(10)
          }
        });
      }

      const response = await client.callTool({ name: 'validate_workflow', arguments: {
        workflow: {
          nodes,
          connections: {}
        }
      } });

      const validation = JSON.parse((response as any).content[0].text);
      expect(validation).toHaveProperty('valid');
    });
  });

  describe('Error Recovery', () => {
    it('should continue working after errors', async () => {
      // Cause an error
      try {
        await client.callTool({ name: 'get_node_info', arguments: {
          nodeType: 'invalid'
        } });
      } catch (error) {
        // Expected
      }

      // Should still work
      const response = await client.callTool({ name: 'list_nodes', arguments: { limit: 1 } });
      expect(response).toBeDefined();
    });

    it('should handle mixed success and failure', async () => {
      const promises = [
        client.callTool({ name: 'list_nodes', arguments: { limit: 5 } }),
        client.callTool({ name: 'get_node_info', arguments: { nodeType: 'invalid' } }).catch(e => ({ error: e })),
        client.callTool({ name: 'get_database_statistics', arguments: {} }),
        client.callTool({ name: 'search_nodes', arguments: { query: '' } }).catch(e => ({ error: e })),
        client.callTool({ name: 'list_ai_tools', arguments: {} })
      ];

      const results = await Promise.all(promises);
      
      // Some should succeed, some should fail
      const successes = results.filter(r => !('error' in r));
      const failures = results.filter(r => 'error' in r);
      
      expect(successes.length).toBeGreaterThan(0);
      expect(failures.length).toBeGreaterThan(0);
    });
  });

  describe('Edge Cases', () => {
    it('should handle empty responses gracefully', async () => {
      const response = await client.callTool({ name: 'list_nodes', arguments: {
        category: 'nonexistent_category'
      } });

      const result = JSON.parse((response as any).content[0].text);
      expect(result).toHaveProperty('nodes');
      expect(Array.isArray(result.nodes)).toBe(true);
      expect(result.nodes).toHaveLength(0);
    });

    it('should handle special characters in parameters', async () => {
      const response = await client.callTool({ name: 'search_nodes', arguments: {
        query: 'test!@#$%^&*()_+-=[]{}|;\':",./<>?'
      } });

      // Should return results or empty array, not error
      const result = JSON.parse((response as any).content[0].text);
      expect(result).toHaveProperty('results');
      expect(Array.isArray(result.results)).toBe(true);
    });

    it('should handle unicode in parameters', async () => {
      const response = await client.callTool({ name: 'search_nodes', arguments: {
        query: 'test 测试 тест परीक्षण'
      } });

      const result = JSON.parse((response as any).content[0].text);
      expect(result).toHaveProperty('results');
      expect(Array.isArray(result.results)).toBe(true);
    });

    it('should handle null and undefined gracefully', async () => {
      // Most tools should handle missing optional params
      const response = await client.callTool({ name: 'list_nodes', arguments: {
        limit: undefined as any,
        category: null as any
      } });

      const result = JSON.parse((response as any).content[0].text);
      expect(result).toHaveProperty('nodes');
      expect(Array.isArray(result.nodes)).toBe(true);
    });
  });

  describe('Error Message Quality', () => {
    it('should provide helpful error messages', async () => {
      try {
        // Use a truly invalid node type
        await client.callTool({ name: 'get_node_info', arguments: {
          nodeType: 'invalid-node-type-that-does-not-exist'
        } });
        expect.fail('Should have thrown an error');
      } catch (error: any) {
        expect(error.message).toBeDefined();
        expect(error.message.length).toBeGreaterThan(10);
        // Should mention the issue
        expect(error.message.toLowerCase()).toMatch(/not found|invalid|missing/);
      }
    });

    it('should indicate missing required parameters', async () => {
      try {
        await client.callTool({ name: 'search_nodes', arguments: {} });
        expect.fail('Should have thrown an error');
      } catch (error: any) {
        expect(error).toBeDefined();
        // The error now properly validates required parameters
        expect(error.message).toContain("search_nodes: Validation failed:");
        expect(error.message).toContain("query: query is required");
      }
    });

    it('should provide context for validation errors', async () => {
      const response = await client.callTool({ name: 'validate_node_operation', arguments: {
        nodeType: 'nodes-base.httpRequest',
        config: {
          // Missing required fields
          method: 'INVALID_METHOD'
        }
      } });

      const validation = JSON.parse((response as any).content[0].text);
      expect(validation.valid).toBe(false);
      expect(validation.errors).toBeDefined();
      expect(Array.isArray(validation.errors)).toBe(true);
      expect(validation.errors.length).toBeGreaterThan(0);
      if (validation.errors.length > 0) {
        expect(validation.errors[0].message).toBeDefined();
        // Field property might not exist on all error types
        if (validation.errors[0].field !== undefined) {
          expect(validation.errors[0].field).toBeDefined();
        }
      }
    });
  });
});
```

--------------------------------------------------------------------------------
/src/scripts/fetch-templates.ts:
--------------------------------------------------------------------------------

```typescript
#!/usr/bin/env node
import { createDatabaseAdapter } from '../database/database-adapter';
import { TemplateService } from '../templates/template-service';
import * as fs from 'fs';
import * as path from 'path';
import * as zlib from 'zlib';
import * as dotenv from 'dotenv';
import type { MetadataRequest } from '../templates/metadata-generator';

// Load environment variables
dotenv.config();

/**
 * Extract node configurations from a template workflow
 */
function extractNodeConfigs(
  templateId: number,
  templateName: string,
  templateViews: number,
  workflowCompressed: string,
  metadata: any
): Array<{
  node_type: string;
  template_id: number;
  template_name: string;
  template_views: number;
  node_name: string;
  parameters_json: string;
  credentials_json: string | null;
  has_credentials: number;
  has_expressions: number;
  complexity: string;
  use_cases: string;
}> {
  try {
    // Decompress workflow
    const decompressed = zlib.gunzipSync(Buffer.from(workflowCompressed, 'base64'));
    const workflow = JSON.parse(decompressed.toString('utf-8'));

    const configs: any[] = [];

    for (const node of workflow.nodes || []) {
      // Skip UI-only nodes (sticky notes, etc.)
      if (node.type.includes('stickyNote') || !node.parameters) {
        continue;
      }

      configs.push({
        node_type: node.type,
        template_id: templateId,
        template_name: templateName,
        template_views: templateViews,
        node_name: node.name,
        parameters_json: JSON.stringify(node.parameters),
        credentials_json: node.credentials ? JSON.stringify(node.credentials) : null,
        has_credentials: node.credentials ? 1 : 0,
        has_expressions: detectExpressions(node.parameters) ? 1 : 0,
        complexity: metadata?.complexity || 'medium',
        use_cases: JSON.stringify(metadata?.use_cases || [])
      });
    }

    return configs;
  } catch (error) {
    console.error(`Error extracting configs from template ${templateId}:`, error);
    return [];
  }
}

/**
 * Detect n8n expressions in parameters
 */
function detectExpressions(params: any): boolean {
  if (!params) return false;
  const json = JSON.stringify(params);
  return json.includes('={{') || json.includes('$json') || json.includes('$node');
}

/**
 * Insert extracted configs into database and rank them
 */
function insertAndRankConfigs(db: any, configs: any[]) {
  if (configs.length === 0) {
    console.log('No configs to insert');
    return;
  }

  // Clear old configs for these templates
  const templateIds = [...new Set(configs.map(c => c.template_id))];
  const placeholders = templateIds.map(() => '?').join(',');
  db.prepare(`DELETE FROM template_node_configs WHERE template_id IN (${placeholders})`).run(...templateIds);

  // Insert new configs
  const insertStmt = db.prepare(`
    INSERT INTO template_node_configs (
      node_type, template_id, template_name, template_views,
      node_name, parameters_json, credentials_json,
      has_credentials, has_expressions, complexity, use_cases
    ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
  `);

  for (const config of configs) {
    insertStmt.run(
      config.node_type,
      config.template_id,
      config.template_name,
      config.template_views,
      config.node_name,
      config.parameters_json,
      config.credentials_json,
      config.has_credentials,
      config.has_expressions,
      config.complexity,
      config.use_cases
    );
  }

  // Rank configs per node_type by template popularity
  db.exec(`
    UPDATE template_node_configs
    SET rank = (
      SELECT COUNT(*) + 1
      FROM template_node_configs AS t2
      WHERE t2.node_type = template_node_configs.node_type
        AND t2.template_views > template_node_configs.template_views
    )
  `);

  // Keep only top 10 per node_type
  db.exec(`
    DELETE FROM template_node_configs
    WHERE id NOT IN (
      SELECT id FROM template_node_configs
      WHERE rank <= 10
      ORDER BY node_type, rank
    )
  `);

  console.log(`✅ Extracted and ranked ${configs.length} node configurations`);
}

/**
 * Extract node configurations from existing templates
 */
async function extractTemplateConfigs(db: any, service: TemplateService) {
  console.log('📦 Extracting node configurations from templates...');
  const repository = (service as any).repository;
  const allTemplates = repository.getAllTemplates();

  const allConfigs: any[] = [];
  let configsExtracted = 0;

  for (const template of allTemplates) {
    if (template.workflow_json_compressed) {
      const metadata = template.metadata_json ? JSON.parse(template.metadata_json) : null;
      const configs = extractNodeConfigs(
        template.id,
        template.name,
        template.views,
        template.workflow_json_compressed,
        metadata
      );
      allConfigs.push(...configs);
      configsExtracted += configs.length;
    }
  }

  if (allConfigs.length > 0) {
    insertAndRankConfigs(db, allConfigs);

    // Show stats
    const configStats = db.prepare(`
      SELECT
        COUNT(DISTINCT node_type) as node_types,
        COUNT(*) as total_configs,
        AVG(rank) as avg_rank
      FROM template_node_configs
    `).get() as any;

    console.log(`📊 Node config stats:`);
    console.log(`   - Unique node types: ${configStats.node_types}`);
    console.log(`   - Total configs stored: ${configStats.total_configs}`);
    console.log(`   - Average rank: ${configStats.avg_rank?.toFixed(1) || 'N/A'}`);
  } else {
    console.log('⚠️  No node configurations extracted');
  }
}

async function fetchTemplates(
  mode: 'rebuild' | 'update' = 'rebuild',
  generateMetadata: boolean = false,
  metadataOnly: boolean = false,
  extractOnly: boolean = false
) {
  // If extract-only mode, skip template fetching and only extract configs
  if (extractOnly) {
    console.log('📦 Extract-only mode: Extracting node configurations from existing templates...\n');

    const db = await createDatabaseAdapter('./data/nodes.db');

    // Ensure template_node_configs table exists
    try {
      const tableExists = db.prepare(`
        SELECT name FROM sqlite_master
        WHERE type='table' AND name='template_node_configs'
      `).get();

      if (!tableExists) {
        console.log('📋 Creating template_node_configs table...');
        const migrationPath = path.join(__dirname, '../../src/database/migrations/add-template-node-configs.sql');
        const migration = fs.readFileSync(migrationPath, 'utf8');
        db.exec(migration);
        console.log('✅ Table created successfully\n');
      }
    } catch (error) {
      console.error('❌ Error checking/creating template_node_configs table:', error);
      if ('close' in db && typeof db.close === 'function') {
        db.close();
      }
      process.exit(1);
    }

    const service = new TemplateService(db);

    await extractTemplateConfigs(db, service);

    if ('close' in db && typeof db.close === 'function') {
      db.close();
    }
    return;
  }

  // If metadata-only mode, skip template fetching entirely
  if (metadataOnly) {
    console.log('🤖 Metadata-only mode: Generating metadata for existing templates...\n');

    if (!process.env.OPENAI_API_KEY) {
      console.error('❌ OPENAI_API_KEY not set in environment');
      process.exit(1);
    }

    const db = await createDatabaseAdapter('./data/nodes.db');
    const service = new TemplateService(db);

    await generateTemplateMetadata(db, service);

    if ('close' in db && typeof db.close === 'function') {
      db.close();
    }
    return;
  }
  
  const modeEmoji = mode === 'rebuild' ? '🔄' : '⬆️';
  const modeText = mode === 'rebuild' ? 'Rebuilding' : 'Updating';
  console.log(`${modeEmoji} ${modeText} n8n workflow templates...\n`);
  
  if (generateMetadata) {
    console.log('🤖 Metadata generation enabled (using OpenAI)\n');
  }
  
  // Ensure data directory exists
  const dataDir = './data';
  if (!fs.existsSync(dataDir)) {
    fs.mkdirSync(dataDir, { recursive: true });
  }
  
  // Initialize database
  const db = await createDatabaseAdapter('./data/nodes.db');
  
  // Handle database schema based on mode
  if (mode === 'rebuild') {
    try {
      // Drop existing tables in rebuild mode
      db.exec('DROP TABLE IF EXISTS templates');
      db.exec('DROP TABLE IF EXISTS templates_fts');
      console.log('🗑️  Dropped existing templates tables (rebuild mode)\n');
      
      // Apply fresh schema
      const schema = fs.readFileSync(path.join(__dirname, '../../src/database/schema.sql'), 'utf8');
      db.exec(schema);
      console.log('📋 Applied database schema\n');
    } catch (error) {
      console.error('❌ Error setting up database schema:', error);
      throw error;
    }
  } else {
    console.log('📊 Update mode: Keeping existing templates and schema\n');
    
    // In update mode, only ensure new columns exist (for migration)
    try {
      // Check if metadata columns exist, add them if not (migration support)
      const columns = db.prepare("PRAGMA table_info(templates)").all() as any[];
      const hasMetadataColumn = columns.some((col: any) => col.name === 'metadata_json');
      
      if (!hasMetadataColumn) {
        console.log('📋 Adding metadata columns to existing schema...');
        db.exec(`
          ALTER TABLE templates ADD COLUMN metadata_json TEXT;
          ALTER TABLE templates ADD COLUMN metadata_generated_at DATETIME;
        `);
        console.log('✅ Metadata columns added\n');
      }
    } catch (error) {
      // Columns might already exist, that's fine
      console.log('📋 Schema is up to date\n');
    }
  }
  
  // FTS5 initialization is handled by TemplateRepository
  // No need to duplicate the logic here
  
  // Create service
  const service = new TemplateService(db);
  
  // Progress tracking
  let lastMessage = '';
  const startTime = Date.now();
  
  try {
    await service.fetchAndUpdateTemplates((message, current, total) => {
      // Clear previous line
      if (lastMessage) {
        process.stdout.write('\r' + ' '.repeat(lastMessage.length) + '\r');
      }
      
      const progress = total > 0 ? Math.round((current / total) * 100) : 0;
      lastMessage = `📊 ${message}: ${current}/${total} (${progress}%)`;
      process.stdout.write(lastMessage);
    }, mode);  // Pass the mode parameter!
    
    console.log('\n'); // New line after progress
    
    // Get stats
    const stats = await service.getTemplateStats();
    const elapsed = Math.round((Date.now() - startTime) / 1000);
    
    console.log('✅ Template fetch complete!\n');
    console.log('📈 Statistics:');
    console.log(`   - Total templates: ${stats.totalTemplates}`);
    console.log(`   - Average views: ${stats.averageViews}`);
    console.log(`   - Time elapsed: ${elapsed} seconds`);
    console.log('\n🔝 Top used nodes:');
    
    stats.topUsedNodes.forEach((node: any, index: number) => {
      console.log(`   ${index + 1}. ${node.node} (${node.count} templates)`);
    });

    // Extract node configurations from templates
    console.log('');
    await extractTemplateConfigs(db, service);

    // Generate metadata if requested
    if (generateMetadata && process.env.OPENAI_API_KEY) {
      console.log('\n🤖 Generating metadata for templates...');
      await generateTemplateMetadata(db, service);
    } else if (generateMetadata && !process.env.OPENAI_API_KEY) {
      console.log('\n⚠️  Metadata generation requested but OPENAI_API_KEY not set');
    }

  } catch (error) {
    console.error('\n❌ Error fetching templates:', error);
    process.exit(1);
  }
  
  // Close database
  if ('close' in db && typeof db.close === 'function') {
    db.close();
  }
}

// Generate metadata for templates using OpenAI
async function generateTemplateMetadata(db: any, service: TemplateService) {
  try {
    const { BatchProcessor } = await import('../templates/batch-processor');
    const repository = (service as any).repository;
    
    // Get templates without metadata (0 = no limit)
    const limit = parseInt(process.env.METADATA_LIMIT || '0');
    const templatesWithoutMetadata = limit > 0 
      ? repository.getTemplatesWithoutMetadata(limit)
      : repository.getTemplatesWithoutMetadata(999999); // Get all
    
    if (templatesWithoutMetadata.length === 0) {
      console.log('✅ All templates already have metadata');
      return;
    }
    
    console.log(`Found ${templatesWithoutMetadata.length} templates without metadata`);
    
    // Create batch processor
    const batchSize = parseInt(process.env.OPENAI_BATCH_SIZE || '50');
    console.log(`Processing in batches of ${batchSize} templates each`);
    
    // Warn if batch size is very large
    if (batchSize > 100) {
      console.log(`⚠️  Large batch size (${batchSize}) may take longer to process`);
      console.log(`   Consider using OPENAI_BATCH_SIZE=50 for faster results`);
    }
    
    const processor = new BatchProcessor({
      apiKey: process.env.OPENAI_API_KEY!,
      model: process.env.OPENAI_MODEL || 'gpt-4o-mini',
      batchSize: batchSize,
      outputDir: './temp/batch'
    });
    
    // Prepare metadata requests
    const requests: MetadataRequest[] = templatesWithoutMetadata.map((t: any) => {
      let workflow = undefined;
      try {
        if (t.workflow_json_compressed) {
          const decompressed = zlib.gunzipSync(Buffer.from(t.workflow_json_compressed, 'base64'));
          workflow = JSON.parse(decompressed.toString());
        } else if (t.workflow_json) {
          workflow = JSON.parse(t.workflow_json);
        }
      } catch (error) {
        console.warn(`Failed to parse workflow for template ${t.id}:`, error);
      }

      // Parse nodes_used safely
      let nodes: string[] = [];
      try {
        if (t.nodes_used) {
          nodes = JSON.parse(t.nodes_used);
          // Ensure it's an array
          if (!Array.isArray(nodes)) {
            console.warn(`Template ${t.id} has invalid nodes_used (not an array), using empty array`);
            nodes = [];
          }
        }
      } catch (error) {
        console.warn(`Failed to parse nodes_used for template ${t.id}:`, error);
        nodes = [];
      }

      return {
        templateId: t.id,
        name: t.name,
        description: t.description,
        nodes: nodes,
        workflow
      };
    });
    
    // Process in batches
    const results = await processor.processTemplates(requests, (message, current, total) => {
      process.stdout.write(`\r📊 ${message}: ${current}/${total}`);
    });
    
    console.log('\n');
    
    // Update database with metadata
    const metadataMap = new Map();
    for (const [templateId, result] of results) {
      if (!result.error) {
        metadataMap.set(templateId, result.metadata);
      }
    }
    
    if (metadataMap.size > 0) {
      repository.batchUpdateMetadata(metadataMap);
      console.log(`✅ Updated metadata for ${metadataMap.size} templates`);
    }
    
    // Show stats
    const stats = repository.getMetadataStats();
    console.log('\n📈 Metadata Statistics:');
    console.log(`   - Total templates: ${stats.total}`);
    console.log(`   - With metadata: ${stats.withMetadata}`);
    console.log(`   - Without metadata: ${stats.withoutMetadata}`);
    console.log(`   - Outdated (>30 days): ${stats.outdated}`);
  } catch (error) {
    console.error('\n❌ Error generating metadata:', error);
  }
}

// Parse command line arguments
function parseArgs(): { mode: 'rebuild' | 'update', generateMetadata: boolean, metadataOnly: boolean, extractOnly: boolean } {
  const args = process.argv.slice(2);

  let mode: 'rebuild' | 'update' = 'rebuild';
  let generateMetadata = false;
  let metadataOnly = false;
  let extractOnly = false;

  // Check for --mode flag
  const modeIndex = args.findIndex(arg => arg.startsWith('--mode'));
  if (modeIndex !== -1) {
    const modeArg = args[modeIndex];
    const modeValue = modeArg.includes('=') ? modeArg.split('=')[1] : args[modeIndex + 1];

    if (modeValue === 'update') {
      mode = 'update';
    }
  }

  // Check for --update flag as shorthand
  if (args.includes('--update')) {
    mode = 'update';
  }

  // Check for --generate-metadata flag
  if (args.includes('--generate-metadata') || args.includes('--metadata')) {
    generateMetadata = true;
  }

  // Check for --metadata-only flag
  if (args.includes('--metadata-only')) {
    metadataOnly = true;
  }

  // Check for --extract-only flag
  if (args.includes('--extract-only') || args.includes('--extract')) {
    extractOnly = true;
  }

  // Show help if requested
  if (args.includes('--help') || args.includes('-h')) {
    console.log('Usage: npm run fetch:templates [options]\n');
    console.log('Options:');
    console.log('  --mode=rebuild|update  Rebuild from scratch or update existing (default: rebuild)');
    console.log('  --update               Shorthand for --mode=update');
    console.log('  --generate-metadata    Generate AI metadata after fetching templates');
    console.log('  --metadata             Shorthand for --generate-metadata');
    console.log('  --metadata-only        Only generate metadata, skip template fetching');
    console.log('  --extract-only         Only extract node configs, skip template fetching');
    console.log('  --extract              Shorthand for --extract-only');
    console.log('  --help, -h             Show this help message');
    process.exit(0);
  }

  return { mode, generateMetadata, metadataOnly, extractOnly };
}

// Run if called directly
if (require.main === module) {
  const { mode, generateMetadata, metadataOnly, extractOnly } = parseArgs();
  fetchTemplates(mode, generateMetadata, metadataOnly, extractOnly).catch(console.error);
}

export { fetchTemplates };
```

--------------------------------------------------------------------------------
/tests/unit/database/node-repository-outputs.test.ts:
--------------------------------------------------------------------------------

```typescript
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { NodeRepository } from '@/database/node-repository';
import { DatabaseAdapter } from '@/database/database-adapter';
import { ParsedNode } from '@/parsers/node-parser';

describe('NodeRepository - Outputs Handling', () => {
  let repository: NodeRepository;
  let mockDb: DatabaseAdapter;
  let mockStatement: any;

  beforeEach(() => {
    mockStatement = {
      run: vi.fn(),
      get: vi.fn(),
      all: vi.fn()
    };

    mockDb = {
      prepare: vi.fn().mockReturnValue(mockStatement),
      transaction: vi.fn(),
      exec: vi.fn(),
      close: vi.fn(),
      pragma: vi.fn()
    } as any;

    repository = new NodeRepository(mockDb);
  });

  describe('saveNode with outputs', () => {
    it('should save node with outputs and outputNames correctly', () => {
      const outputs = [
        { displayName: 'Done', description: 'Final results when loop completes' },
        { displayName: 'Loop', description: 'Current batch data during iteration' }
      ];
      const outputNames = ['done', 'loop'];

      const node: ParsedNode = {
        style: 'programmatic',
        nodeType: 'nodes-base.splitInBatches',
        displayName: 'Split In Batches',
        description: 'Split data into batches',
        category: 'transform',
        properties: [],
        credentials: [],
        isAITool: false,
        isTrigger: false,
        isWebhook: false,
        operations: [],
        version: '3',
        isVersioned: false,
        packageName: 'n8n-nodes-base',
        outputs,
        outputNames
      };

      repository.saveNode(node);

      expect(mockDb.prepare).toHaveBeenCalledWith(`
      INSERT OR REPLACE INTO nodes (
        node_type, package_name, display_name, description,
        category, development_style, is_ai_tool, is_trigger,
        is_webhook, is_versioned, version, documentation,
        properties_schema, operations, credentials_required,
        outputs, output_names
      ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
    `);

      expect(mockStatement.run).toHaveBeenCalledWith(
        'nodes-base.splitInBatches',
        'n8n-nodes-base',
        'Split In Batches',
        'Split data into batches',
        'transform',
        'programmatic',
        0, // false
        0, // false
        0, // false
        0, // false
        '3',
        null, // documentation
        JSON.stringify([], null, 2), // properties
        JSON.stringify([], null, 2), // operations
        JSON.stringify([], null, 2), // credentials
        JSON.stringify(outputs, null, 2), // outputs
        JSON.stringify(outputNames, null, 2) // output_names
      );
    });

    it('should save node with only outputs (no outputNames)', () => {
      const outputs = [
        { displayName: 'True', description: 'Items that match condition' },
        { displayName: 'False', description: 'Items that do not match condition' }
      ];

      const node: ParsedNode = {
        style: 'programmatic',
        nodeType: 'nodes-base.if',
        displayName: 'IF',
        description: 'Route items based on conditions',
        category: 'transform',
        properties: [],
        credentials: [],
        isAITool: false,
        isTrigger: false,
        isWebhook: false,
        operations: [],
        version: '2',
        isVersioned: false,
        packageName: 'n8n-nodes-base',
        outputs
        // no outputNames
      };

      repository.saveNode(node);

      const callArgs = mockStatement.run.mock.calls[0];
      expect(callArgs[15]).toBe(JSON.stringify(outputs, null, 2)); // outputs
      expect(callArgs[16]).toBe(null); // output_names should be null
    });

    it('should save node with only outputNames (no outputs)', () => {
      const outputNames = ['main', 'error'];

      const node: ParsedNode = {
        style: 'programmatic',
        nodeType: 'nodes-base.customNode',
        displayName: 'Custom Node',
        description: 'Custom node with output names only',
        category: 'transform',
        properties: [],
        credentials: [],
        isAITool: false,
        isTrigger: false,
        isWebhook: false,
        operations: [],
        version: '1',
        isVersioned: false,
        packageName: 'n8n-nodes-base',
        outputNames
        // no outputs
      };

      repository.saveNode(node);

      const callArgs = mockStatement.run.mock.calls[0];
      expect(callArgs[15]).toBe(null); // outputs should be null
      expect(callArgs[16]).toBe(JSON.stringify(outputNames, null, 2)); // output_names
    });

    it('should save node without outputs or outputNames', () => {
      const node: ParsedNode = {
        style: 'programmatic',
        nodeType: 'nodes-base.httpRequest',
        displayName: 'HTTP Request',
        description: 'Make HTTP requests',
        category: 'input',
        properties: [],
        credentials: [],
        isAITool: false,
        isTrigger: false,
        isWebhook: false,
        operations: [],
        version: '4',
        isVersioned: false,
        packageName: 'n8n-nodes-base'
        // no outputs or outputNames
      };

      repository.saveNode(node);

      const callArgs = mockStatement.run.mock.calls[0];
      expect(callArgs[15]).toBe(null); // outputs should be null
      expect(callArgs[16]).toBe(null); // output_names should be null
    });

    it('should handle empty outputs and outputNames arrays', () => {
      const node: ParsedNode = {
        style: 'programmatic',
        nodeType: 'nodes-base.emptyNode',
        displayName: 'Empty Node',
        description: 'Node with empty outputs',
        category: 'misc',
        properties: [],
        credentials: [],
        isAITool: false,
        isTrigger: false,
        isWebhook: false,
        operations: [],
        version: '1',
        isVersioned: false,
        packageName: 'n8n-nodes-base',
        outputs: [],
        outputNames: []
      };

      repository.saveNode(node);

      const callArgs = mockStatement.run.mock.calls[0];
      expect(callArgs[15]).toBe(JSON.stringify([], null, 2)); // outputs
      expect(callArgs[16]).toBe(JSON.stringify([], null, 2)); // output_names
    });
  });

  describe('getNode with outputs', () => {
    it('should retrieve node with outputs and outputNames correctly', () => {
      const outputs = [
        { displayName: 'Done', description: 'Final results when loop completes' },
        { displayName: 'Loop', description: 'Current batch data during iteration' }
      ];
      const outputNames = ['done', 'loop'];

      const mockRow = {
        node_type: 'nodes-base.splitInBatches',
        display_name: 'Split In Batches',
        description: 'Split data into batches',
        category: 'transform',
        development_style: 'programmatic',
        package_name: 'n8n-nodes-base',
        is_ai_tool: 0,
        is_trigger: 0,
        is_webhook: 0,
        is_versioned: 0,
        version: '3',
        properties_schema: JSON.stringify([]),
        operations: JSON.stringify([]),
        credentials_required: JSON.stringify([]),
        documentation: null,
        outputs: JSON.stringify(outputs),
        output_names: JSON.stringify(outputNames)
      };

      mockStatement.get.mockReturnValue(mockRow);

      const result = repository.getNode('nodes-base.splitInBatches');

      expect(result).toEqual({
        nodeType: 'nodes-base.splitInBatches',
        displayName: 'Split In Batches',
        description: 'Split data into batches',
        category: 'transform',
        developmentStyle: 'programmatic',
        package: 'n8n-nodes-base',
        isAITool: false,
        isTrigger: false,
        isWebhook: false,
        isVersioned: false,
        version: '3',
        properties: [],
        operations: [],
        credentials: [],
        hasDocumentation: false,
        outputs,
        outputNames
      });
    });

    it('should retrieve node with only outputs (null outputNames)', () => {
      const outputs = [
        { displayName: 'True', description: 'Items that match condition' }
      ];

      const mockRow = {
        node_type: 'nodes-base.if',
        display_name: 'IF',
        description: 'Route items',
        category: 'transform',
        development_style: 'programmatic',
        package_name: 'n8n-nodes-base',
        is_ai_tool: 0,
        is_trigger: 0,
        is_webhook: 0,
        is_versioned: 0,
        version: '2',
        properties_schema: JSON.stringify([]),
        operations: JSON.stringify([]),
        credentials_required: JSON.stringify([]),
        documentation: null,
        outputs: JSON.stringify(outputs),
        output_names: null
      };

      mockStatement.get.mockReturnValue(mockRow);

      const result = repository.getNode('nodes-base.if');

      expect(result.outputs).toEqual(outputs);
      expect(result.outputNames).toBe(null);
    });

    it('should retrieve node with only outputNames (null outputs)', () => {
      const outputNames = ['main'];

      const mockRow = {
        node_type: 'nodes-base.customNode',
        display_name: 'Custom Node',
        description: 'Custom node',
        category: 'misc',
        development_style: 'programmatic',
        package_name: 'n8n-nodes-base',
        is_ai_tool: 0,
        is_trigger: 0,
        is_webhook: 0,
        is_versioned: 0,
        version: '1',
        properties_schema: JSON.stringify([]),
        operations: JSON.stringify([]),
        credentials_required: JSON.stringify([]),
        documentation: null,
        outputs: null,
        output_names: JSON.stringify(outputNames)
      };

      mockStatement.get.mockReturnValue(mockRow);

      const result = repository.getNode('nodes-base.customNode');

      expect(result.outputs).toBe(null);
      expect(result.outputNames).toEqual(outputNames);
    });

    it('should retrieve node without outputs or outputNames', () => {
      const mockRow = {
        node_type: 'nodes-base.httpRequest',
        display_name: 'HTTP Request',
        description: 'Make HTTP requests',
        category: 'input',
        development_style: 'programmatic',
        package_name: 'n8n-nodes-base',
        is_ai_tool: 0,
        is_trigger: 0,
        is_webhook: 0,
        is_versioned: 0,
        version: '4',
        properties_schema: JSON.stringify([]),
        operations: JSON.stringify([]),
        credentials_required: JSON.stringify([]),
        documentation: null,
        outputs: null,
        output_names: null
      };

      mockStatement.get.mockReturnValue(mockRow);

      const result = repository.getNode('nodes-base.httpRequest');

      expect(result.outputs).toBe(null);
      expect(result.outputNames).toBe(null);
    });

    it('should handle malformed JSON gracefully', () => {
      const mockRow = {
        node_type: 'nodes-base.malformed',
        display_name: 'Malformed Node',
        description: 'Node with malformed JSON',
        category: 'misc',
        development_style: 'programmatic',
        package_name: 'n8n-nodes-base',
        is_ai_tool: 0,
        is_trigger: 0,
        is_webhook: 0,
        is_versioned: 0,
        version: '1',
        properties_schema: JSON.stringify([]),
        operations: JSON.stringify([]),
        credentials_required: JSON.stringify([]),
        documentation: null,
        outputs: '{invalid json}',
        output_names: '[invalid, json'
      };

      mockStatement.get.mockReturnValue(mockRow);

      const result = repository.getNode('nodes-base.malformed');

      // Should use default values when JSON parsing fails
      expect(result.outputs).toBe(null);
      expect(result.outputNames).toBe(null);
    });

    it('should return null for non-existent node', () => {
      mockStatement.get.mockReturnValue(null);

      const result = repository.getNode('nodes-base.nonExistent');

      expect(result).toBe(null);
    });

    it('should handle SplitInBatches counterintuitive output order correctly', () => {
      // Test that the output order is preserved: done=0, loop=1
      const outputs = [
        { displayName: 'Done', description: 'Final results when loop completes', index: 0 },
        { displayName: 'Loop', description: 'Current batch data during iteration', index: 1 }
      ];
      const outputNames = ['done', 'loop'];

      const mockRow = {
        node_type: 'nodes-base.splitInBatches',
        display_name: 'Split In Batches',
        description: 'Split data into batches',
        category: 'transform',
        development_style: 'programmatic',
        package_name: 'n8n-nodes-base',
        is_ai_tool: 0,
        is_trigger: 0,
        is_webhook: 0,
        is_versioned: 0,
        version: '3',
        properties_schema: JSON.stringify([]),
        operations: JSON.stringify([]),
        credentials_required: JSON.stringify([]),
        documentation: null,
        outputs: JSON.stringify(outputs),
        output_names: JSON.stringify(outputNames)
      };

      mockStatement.get.mockReturnValue(mockRow);

      const result = repository.getNode('nodes-base.splitInBatches');

      // Verify order is preserved
      expect(result.outputs[0].displayName).toBe('Done');
      expect(result.outputs[1].displayName).toBe('Loop');
      expect(result.outputNames[0]).toBe('done');
      expect(result.outputNames[1]).toBe('loop');
    });
  });

  describe('parseNodeRow with outputs', () => {
    it('should parse node row with outputs correctly using parseNodeRow', () => {
      const outputs = [{ displayName: 'Output' }];
      const outputNames = ['main'];

      const mockRow = {
        node_type: 'nodes-base.test',
        display_name: 'Test',
        description: 'Test node',
        category: 'misc',
        development_style: 'programmatic',
        package_name: 'n8n-nodes-base',
        is_ai_tool: 0,
        is_trigger: 0,
        is_webhook: 0,
        is_versioned: 0,
        version: '1',
        properties_schema: JSON.stringify([]),
        operations: JSON.stringify([]),
        credentials_required: JSON.stringify([]),
        documentation: null,
        outputs: JSON.stringify(outputs),
        output_names: JSON.stringify(outputNames)
      };

      mockStatement.all.mockReturnValue([mockRow]);

      const results = repository.getAllNodes(1);

      expect(results[0].outputs).toEqual(outputs);
      expect(results[0].outputNames).toEqual(outputNames);
    });

    it('should handle empty string as null for outputs', () => {
      const mockRow = {
        node_type: 'nodes-base.empty',
        display_name: 'Empty',
        description: 'Empty node',
        category: 'misc',
        development_style: 'programmatic',
        package_name: 'n8n-nodes-base',
        is_ai_tool: 0,
        is_trigger: 0,
        is_webhook: 0,
        is_versioned: 0,
        version: '1',
        properties_schema: JSON.stringify([]),
        operations: JSON.stringify([]),
        credentials_required: JSON.stringify([]),
        documentation: null,
        outputs: '', // empty string
        output_names: '' // empty string
      };

      mockStatement.all.mockReturnValue([mockRow]);

      const results = repository.getAllNodes(1);

      // Empty strings should be treated as null since they fail JSON parsing
      expect(results[0].outputs).toBe(null);
      expect(results[0].outputNames).toBe(null);
    });
  });

  describe('complex output structures', () => {
    it('should handle complex output objects with metadata', () => {
      const complexOutputs = [
        {
          displayName: 'Done',
          name: 'done',
          type: 'main',
          hint: 'Receives the final data after all batches have been processed',
          description: 'Final results when loop completes',
          index: 0
        },
        {
          displayName: 'Loop',
          name: 'loop',
          type: 'main', 
          hint: 'Receives the current batch data during each iteration',
          description: 'Current batch data during iteration',
          index: 1
        }
      ];

      const node: ParsedNode = {
        style: 'programmatic',
        nodeType: 'nodes-base.splitInBatches',
        displayName: 'Split In Batches',
        description: 'Split data into batches',
        category: 'transform',
        properties: [],
        credentials: [],
        isAITool: false,
        isTrigger: false,
        isWebhook: false,
        operations: [],
        version: '3',
        isVersioned: false,
        packageName: 'n8n-nodes-base',
        outputs: complexOutputs,
        outputNames: ['done', 'loop']
      };

      repository.saveNode(node);

      // Simulate retrieval
      const mockRow = {
        node_type: 'nodes-base.splitInBatches',
        display_name: 'Split In Batches',
        description: 'Split data into batches',
        category: 'transform',
        development_style: 'programmatic',
        package_name: 'n8n-nodes-base',
        is_ai_tool: 0,
        is_trigger: 0,
        is_webhook: 0,
        is_versioned: 0,
        version: '3',
        properties_schema: JSON.stringify([]),
        operations: JSON.stringify([]),
        credentials_required: JSON.stringify([]),
        documentation: null,
        outputs: JSON.stringify(complexOutputs),
        output_names: JSON.stringify(['done', 'loop'])
      };

      mockStatement.get.mockReturnValue(mockRow);

      const result = repository.getNode('nodes-base.splitInBatches');

      expect(result.outputs).toEqual(complexOutputs);
      expect(result.outputs[0]).toMatchObject({
        displayName: 'Done',
        name: 'done',
        type: 'main',
        hint: 'Receives the final data after all batches have been processed'
      });
    });
  });
});
```

--------------------------------------------------------------------------------
/tests/unit/services/execution-processor.test.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Execution Processor Service Tests
 *
 * Comprehensive test coverage for execution filtering and processing
 */

import { describe, it, expect } from 'vitest';
import {
  generatePreview,
  filterExecutionData,
  processExecution,
} from '../../../src/services/execution-processor';
import {
  Execution,
  ExecutionStatus,
  ExecutionFilterOptions,
} from '../../../src/types/n8n-api';

/**
 * Test data factories
 */

function createMockExecution(options: {
  id?: string;
  status?: ExecutionStatus;
  nodeData?: Record<string, any>;
  hasError?: boolean;
}): Execution {
  const { id = 'test-exec-1', status = ExecutionStatus.SUCCESS, nodeData = {}, hasError = false } = options;

  return {
    id,
    workflowId: 'workflow-1',
    status,
    mode: 'manual',
    finished: true,
    startedAt: '2024-01-01T10:00:00.000Z',
    stoppedAt: '2024-01-01T10:00:05.000Z',
    data: {
      resultData: {
        runData: nodeData,
        error: hasError ? { message: 'Test error' } : undefined,
      },
    },
  };
}

function createNodeData(itemCount: number, includeError = false) {
  const items = Array.from({ length: itemCount }, (_, i) => ({
    json: {
      id: i + 1,
      name: `Item ${i + 1}`,
      value: Math.random() * 100,
      nested: {
        field1: `value${i}`,
        field2: true,
      },
    },
  }));

  return [
    {
      startTime: Date.now(),
      executionTime: 123,
      data: {
        main: [items],
      },
      error: includeError ? { message: 'Node error' } : undefined,
    },
  ];
}

/**
 * Preview Mode Tests
 */
describe('ExecutionProcessor - Preview Mode', () => {
  it('should generate preview for empty execution', () => {
    const execution = createMockExecution({ nodeData: {} });
    const { preview, recommendation } = generatePreview(execution);

    expect(preview.totalNodes).toBe(0);
    expect(preview.executedNodes).toBe(0);
    expect(preview.estimatedSizeKB).toBe(0);
    expect(recommendation.canFetchFull).toBe(true);
    expect(recommendation.suggestedMode).toBe('full'); // Empty execution is safe to fetch in full
  });

  it('should generate preview with accurate item counts', () => {
    const execution = createMockExecution({
      nodeData: {
        'HTTP Request': createNodeData(50),
        'Filter': createNodeData(12),
      },
    });

    const { preview } = generatePreview(execution);

    expect(preview.totalNodes).toBe(2);
    expect(preview.executedNodes).toBe(2);
    expect(preview.nodes['HTTP Request'].itemCounts.output).toBe(50);
    expect(preview.nodes['Filter'].itemCounts.output).toBe(12);
  });

  it('should extract data structure from nodes', () => {
    const execution = createMockExecution({
      nodeData: {
        'HTTP Request': createNodeData(5),
      },
    });

    const { preview } = generatePreview(execution);
    const structure = preview.nodes['HTTP Request'].dataStructure;

    expect(structure).toHaveProperty('json');
    expect(structure.json).toHaveProperty('id');
    expect(structure.json).toHaveProperty('name');
    expect(structure.json).toHaveProperty('nested');
    expect(structure.json.id).toBe('number');
    expect(structure.json.name).toBe('string');
    expect(typeof structure.json.nested).toBe('object');
  });

  it('should estimate data size', () => {
    const execution = createMockExecution({
      nodeData: {
        'HTTP Request': createNodeData(50),
      },
    });

    const { preview } = generatePreview(execution);

    expect(preview.estimatedSizeKB).toBeGreaterThan(0);
    expect(preview.nodes['HTTP Request'].estimatedSizeKB).toBeGreaterThan(0);
  });

  it('should detect error status in nodes', () => {
    const execution = createMockExecution({
      nodeData: {
        'HTTP Request': createNodeData(5, true),
      },
    });

    const { preview } = generatePreview(execution);

    expect(preview.nodes['HTTP Request'].status).toBe('error');
    expect(preview.nodes['HTTP Request'].error).toBeDefined();
  });

  it('should recommend full mode for small datasets', () => {
    const execution = createMockExecution({
      nodeData: {
        'HTTP Request': createNodeData(5),
      },
    });

    const { recommendation } = generatePreview(execution);

    expect(recommendation.canFetchFull).toBe(true);
    expect(recommendation.suggestedMode).toBe('full');
  });

  it('should recommend filtered mode for large datasets', () => {
    const execution = createMockExecution({
      nodeData: {
        'HTTP Request': createNodeData(100),
      },
    });

    const { recommendation } = generatePreview(execution);

    expect(recommendation.canFetchFull).toBe(false);
    expect(recommendation.suggestedMode).toBe('filtered');
    expect(recommendation.suggestedItemsLimit).toBeGreaterThan(0);
  });

  it('should recommend summary mode for moderate datasets', () => {
    const execution = createMockExecution({
      nodeData: {
        'HTTP Request': createNodeData(30),
      },
    });

    const { recommendation } = generatePreview(execution);

    expect(recommendation.canFetchFull).toBe(false);
    expect(recommendation.suggestedMode).toBe('summary');
  });
});

/**
 * Filtering Mode Tests
 */
describe('ExecutionProcessor - Filtering', () => {
  it('should filter by node names', () => {
    const execution = createMockExecution({
      nodeData: {
        'HTTP Request': createNodeData(10),
        'Filter': createNodeData(5),
        'Set': createNodeData(3),
      },
    });

    const options: ExecutionFilterOptions = {
      mode: 'filtered',
      nodeNames: ['HTTP Request', 'Filter'],
    };

    const result = filterExecutionData(execution, options);

    expect(result.nodes).toHaveProperty('HTTP Request');
    expect(result.nodes).toHaveProperty('Filter');
    expect(result.nodes).not.toHaveProperty('Set');
    expect(result.summary?.executedNodes).toBe(2);
  });

  it('should handle non-existent node names gracefully', () => {
    const execution = createMockExecution({
      nodeData: {
        'HTTP Request': createNodeData(10),
      },
    });

    const options: ExecutionFilterOptions = {
      mode: 'filtered',
      nodeNames: ['NonExistent'],
    };

    const result = filterExecutionData(execution, options);

    expect(Object.keys(result.nodes || {})).toHaveLength(0);
    expect(result.summary?.executedNodes).toBe(0);
  });

  it('should limit items to 0 (structure only)', () => {
    const execution = createMockExecution({
      nodeData: {
        'HTTP Request': createNodeData(50),
      },
    });

    const options: ExecutionFilterOptions = {
      mode: 'filtered',
      itemsLimit: 0,
    };

    const result = filterExecutionData(execution, options);
    const nodeData = result.nodes?.['HTTP Request'];

    expect(nodeData?.data?.metadata.itemsShown).toBe(0);
    expect(nodeData?.data?.metadata.truncated).toBe(true);
    expect(nodeData?.data?.metadata.totalItems).toBe(50);

    // Check that we have structure but no actual values
    const output = nodeData?.data?.output?.[0]?.[0];
    expect(output).toBeDefined();
    expect(typeof output).toBe('object');
  });

  it('should limit items to 2 (default)', () => {
    const execution = createMockExecution({
      nodeData: {
        'HTTP Request': createNodeData(50),
      },
    });

    const options: ExecutionFilterOptions = {
      mode: 'summary',
    };

    const result = filterExecutionData(execution, options);
    const nodeData = result.nodes?.['HTTP Request'];

    expect(nodeData?.data?.metadata.itemsShown).toBe(2);
    expect(nodeData?.data?.metadata.totalItems).toBe(50);
    expect(nodeData?.data?.metadata.truncated).toBe(true);
    expect(nodeData?.data?.output?.[0]).toHaveLength(2);
  });

  it('should limit items to custom value', () => {
    const execution = createMockExecution({
      nodeData: {
        'HTTP Request': createNodeData(50),
      },
    });

    const options: ExecutionFilterOptions = {
      mode: 'filtered',
      itemsLimit: 5,
    };

    const result = filterExecutionData(execution, options);
    const nodeData = result.nodes?.['HTTP Request'];

    expect(nodeData?.data?.metadata.itemsShown).toBe(5);
    expect(nodeData?.data?.metadata.truncated).toBe(true);
    expect(nodeData?.data?.output?.[0]).toHaveLength(5);
  });

  it('should not truncate when itemsLimit is -1 (unlimited)', () => {
    const execution = createMockExecution({
      nodeData: {
        'HTTP Request': createNodeData(50),
      },
    });

    const options: ExecutionFilterOptions = {
      mode: 'filtered',
      itemsLimit: -1,
    };

    const result = filterExecutionData(execution, options);
    const nodeData = result.nodes?.['HTTP Request'];

    expect(nodeData?.data?.metadata.itemsShown).toBe(50);
    expect(nodeData?.data?.metadata.totalItems).toBe(50);
    expect(nodeData?.data?.metadata.truncated).toBe(false);
  });

  it('should not truncate when items are less than limit', () => {
    const execution = createMockExecution({
      nodeData: {
        'HTTP Request': createNodeData(3),
      },
    });

    const options: ExecutionFilterOptions = {
      mode: 'filtered',
      itemsLimit: 5,
    };

    const result = filterExecutionData(execution, options);
    const nodeData = result.nodes?.['HTTP Request'];

    expect(nodeData?.data?.metadata.itemsShown).toBe(3);
    expect(nodeData?.data?.metadata.truncated).toBe(false);
  });

  it('should include input data when requested', () => {
    const execution = createMockExecution({
      nodeData: {
        'HTTP Request': [
          {
            startTime: Date.now(),
            executionTime: 100,
            inputData: [[{ json: { input: 'test' } }]],
            data: {
              main: [[{ json: { output: 'result' } }]],
            },
          },
        ],
      },
    });

    const options: ExecutionFilterOptions = {
      mode: 'filtered',
      includeInputData: true,
    };

    const result = filterExecutionData(execution, options);
    const nodeData = result.nodes?.['HTTP Request'];

    expect(nodeData?.data?.input).toBeDefined();
    expect(nodeData?.data?.input?.[0]?.[0]?.json?.input).toBe('test');
  });

  it('should not include input data by default', () => {
    const execution = createMockExecution({
      nodeData: {
        'HTTP Request': [
          {
            startTime: Date.now(),
            executionTime: 100,
            inputData: [[{ json: { input: 'test' } }]],
            data: {
              main: [[{ json: { output: 'result' } }]],
            },
          },
        ],
      },
    });

    const options: ExecutionFilterOptions = {
      mode: 'filtered',
    };

    const result = filterExecutionData(execution, options);
    const nodeData = result.nodes?.['HTTP Request'];

    expect(nodeData?.data?.input).toBeUndefined();
  });
});

/**
 * Mode Tests
 */
describe('ExecutionProcessor - Modes', () => {
  it('should handle preview mode', () => {
    const execution = createMockExecution({
      nodeData: {
        'HTTP Request': createNodeData(50),
      },
    });

    const result = filterExecutionData(execution, { mode: 'preview' });

    expect(result.mode).toBe('preview');
    expect(result.preview).toBeDefined();
    expect(result.recommendation).toBeDefined();
    expect(result.nodes).toBeUndefined();
  });

  it('should handle summary mode', () => {
    const execution = createMockExecution({
      nodeData: {
        'HTTP Request': createNodeData(50),
      },
    });

    const result = filterExecutionData(execution, { mode: 'summary' });

    expect(result.mode).toBe('summary');
    expect(result.summary).toBeDefined();
    expect(result.nodes).toBeDefined();
    expect(result.nodes?.['HTTP Request']?.data?.metadata.itemsShown).toBe(2);
  });

  it('should handle filtered mode', () => {
    const execution = createMockExecution({
      nodeData: {
        'HTTP Request': createNodeData(50),
      },
    });

    const result = filterExecutionData(execution, {
      mode: 'filtered',
      itemsLimit: 5,
    });

    expect(result.mode).toBe('filtered');
    expect(result.summary).toBeDefined();
    expect(result.nodes?.['HTTP Request']?.data?.metadata.itemsShown).toBe(5);
  });

  it('should handle full mode', () => {
    const execution = createMockExecution({
      nodeData: {
        'HTTP Request': createNodeData(50),
      },
    });

    const result = filterExecutionData(execution, { mode: 'full' });

    expect(result.mode).toBe('full');
    expect(result.nodes?.['HTTP Request']?.data?.metadata.itemsShown).toBe(50);
    expect(result.nodes?.['HTTP Request']?.data?.metadata.truncated).toBe(false);
  });
});

/**
 * Edge Cases
 */
describe('ExecutionProcessor - Edge Cases', () => {
  it('should handle execution with no data', () => {
    const execution: Execution = {
      id: 'test-1',
      workflowId: 'workflow-1',
      status: ExecutionStatus.SUCCESS,
      mode: 'manual',
      finished: true,
      startedAt: '2024-01-01T10:00:00.000Z',
      stoppedAt: '2024-01-01T10:00:05.000Z',
    };

    const result = filterExecutionData(execution, { mode: 'summary' });

    expect(result.summary?.totalNodes).toBe(0);
    expect(result.summary?.executedNodes).toBe(0);
  });

  it('should handle execution with error', () => {
    const execution = createMockExecution({
      nodeData: {
        'HTTP Request': createNodeData(5),
      },
      hasError: true,
    });

    const result = filterExecutionData(execution, { mode: 'summary' });

    expect(result.error).toBeDefined();
  });

  it('should handle empty node data arrays', () => {
    const execution = createMockExecution({
      nodeData: {
        'HTTP Request': [],
      },
    });

    const result = filterExecutionData(execution, { mode: 'summary' });

    expect(result.nodes?.['HTTP Request']).toBeDefined();
    expect(result.nodes?.['HTTP Request'].itemsOutput).toBe(0);
  });

  it('should handle nested data structures', () => {
    const execution = createMockExecution({
      nodeData: {
        'HTTP Request': [
          {
            startTime: Date.now(),
            executionTime: 100,
            data: {
              main: [[{
                json: {
                  deeply: {
                    nested: {
                      structure: {
                        value: 'test',
                        array: [1, 2, 3],
                      },
                    },
                  },
                },
              }]],
            },
          },
        ],
      },
    });

    const { preview } = generatePreview(execution);
    const structure = preview.nodes['HTTP Request'].dataStructure;

    expect(structure.json.deeply).toBeDefined();
    expect(typeof structure.json.deeply).toBe('object');
  });

  it('should calculate duration correctly', () => {
    const execution = createMockExecution({
      nodeData: {
        'HTTP Request': createNodeData(5),
      },
    });

    const result = filterExecutionData(execution, { mode: 'summary' });

    expect(result.duration).toBe(5000); // 5 seconds
  });

  it('should handle execution without stop time', () => {
    const execution: Execution = {
      id: 'test-1',
      workflowId: 'workflow-1',
      status: ExecutionStatus.WAITING,
      mode: 'manual',
      finished: false,
      startedAt: '2024-01-01T10:00:00.000Z',
      data: {
        resultData: {
          runData: {},
        },
      },
    };

    const result = filterExecutionData(execution, { mode: 'summary' });

    expect(result.duration).toBeUndefined();
    expect(result.finished).toBe(false);
  });
});

/**
 * processExecution Tests
 */
describe('ExecutionProcessor - processExecution', () => {
  it('should return original execution when no options provided', () => {
    const execution = createMockExecution({
      nodeData: {
        'HTTP Request': createNodeData(5),
      },
    });

    const result = processExecution(execution, {});

    expect(result).toBe(execution);
  });

  it('should process when mode is specified', () => {
    const execution = createMockExecution({
      nodeData: {
        'HTTP Request': createNodeData(5),
      },
    });

    const result = processExecution(execution, { mode: 'preview' });

    expect(result).not.toBe(execution);
    expect((result as any).mode).toBe('preview');
  });

  it('should process when filtering options are provided', () => {
    const execution = createMockExecution({
      nodeData: {
        'HTTP Request': createNodeData(5),
        'Filter': createNodeData(3),
      },
    });

    const result = processExecution(execution, { nodeNames: ['HTTP Request'] });

    expect(result).not.toBe(execution);
    expect((result as any).nodes).toHaveProperty('HTTP Request');
    expect((result as any).nodes).not.toHaveProperty('Filter');
  });
});

/**
 * Summary Statistics Tests
 */
describe('ExecutionProcessor - Summary Statistics', () => {
  it('should calculate hasMoreData correctly', () => {
    const execution = createMockExecution({
      nodeData: {
        'HTTP Request': createNodeData(50),
      },
    });

    const result = filterExecutionData(execution, {
      mode: 'summary',
      itemsLimit: 2,
    });

    expect(result.summary?.hasMoreData).toBe(true);
  });

  it('should set hasMoreData to false when all data is included', () => {
    const execution = createMockExecution({
      nodeData: {
        'HTTP Request': createNodeData(2),
      },
    });

    const result = filterExecutionData(execution, {
      mode: 'summary',
      itemsLimit: 5,
    });

    expect(result.summary?.hasMoreData).toBe(false);
  });

  it('should count total items correctly across multiple nodes', () => {
    const execution = createMockExecution({
      nodeData: {
        'HTTP Request': createNodeData(10),
        'Filter': createNodeData(5),
        'Set': createNodeData(3),
      },
    });

    const result = filterExecutionData(execution, { mode: 'summary' });

    expect(result.summary?.totalItems).toBe(18);
  });
});

```

--------------------------------------------------------------------------------
/tests/integration/database/template-node-configs.test.ts:
--------------------------------------------------------------------------------

```typescript
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import Database from 'better-sqlite3';
import { DatabaseAdapter, createDatabaseAdapter } from '../../../src/database/database-adapter';
import fs from 'fs';
import path from 'path';

/**
 * Integration tests for template_node_configs table
 * Testing database schema, migrations, and data operations
 */

describe('Template Node Configs Database Integration', () => {
  let db: DatabaseAdapter;
  let dbPath: string;

  beforeEach(async () => {
    // Create temporary database
    dbPath = ':memory:';
    db = await createDatabaseAdapter(dbPath);

    // Apply schema
    const schemaPath = path.join(__dirname, '../../../src/database/schema.sql');
    const schema = fs.readFileSync(schemaPath, 'utf-8');
    db.exec(schema);

    // Apply migration
    const migrationPath = path.join(__dirname, '../../../src/database/migrations/add-template-node-configs.sql');
    const migration = fs.readFileSync(migrationPath, 'utf-8');
    db.exec(migration);

    // Insert test templates with id 1-1000 to satisfy foreign key constraints
    // Tests insert configs with various template_id values, so we pre-create many templates
    const stmt = db.prepare(`
      INSERT INTO templates (
        id, workflow_id, name, description, views,
        nodes_used, created_at, updated_at
      ) VALUES (?, ?, ?, ?, ?, ?, datetime('now'), datetime('now'))
    `);
    for (let i = 1; i <= 1000; i++) {
      stmt.run(i, i, `Test Template ${i}`, 'Test template for node configs', 100, '[]');
    }
  });

  afterEach(() => {
    if ('close' in db && typeof db.close === 'function') {
      db.close();
    }
  });

  describe('Schema Validation', () => {
    it('should create template_node_configs table', () => {
      const tableExists = db.prepare(`
        SELECT name FROM sqlite_master
        WHERE type='table' AND name='template_node_configs'
      `).get();

      expect(tableExists).toBeDefined();
      expect(tableExists).toHaveProperty('name', 'template_node_configs');
    });

    it('should have all required columns', () => {
      const columns = db.prepare(`PRAGMA table_info(template_node_configs)`).all() as any[];

      const columnNames = columns.map(col => col.name);
      expect(columnNames).toContain('id');
      expect(columnNames).toContain('node_type');
      expect(columnNames).toContain('template_id');
      expect(columnNames).toContain('template_name');
      expect(columnNames).toContain('template_views');
      expect(columnNames).toContain('node_name');
      expect(columnNames).toContain('parameters_json');
      expect(columnNames).toContain('credentials_json');
      expect(columnNames).toContain('has_credentials');
      expect(columnNames).toContain('has_expressions');
      expect(columnNames).toContain('complexity');
      expect(columnNames).toContain('use_cases');
      expect(columnNames).toContain('rank');
      expect(columnNames).toContain('created_at');
    });

    it('should have correct column types and constraints', () => {
      const columns = db.prepare(`PRAGMA table_info(template_node_configs)`).all() as any[];

      const idColumn = columns.find(col => col.name === 'id');
      expect(idColumn.pk).toBe(1); // Primary key

      const nodeTypeColumn = columns.find(col => col.name === 'node_type');
      expect(nodeTypeColumn.notnull).toBe(1); // NOT NULL

      const parametersJsonColumn = columns.find(col => col.name === 'parameters_json');
      expect(parametersJsonColumn.notnull).toBe(1); // NOT NULL
    });

    it('should have complexity CHECK constraint', () => {
      // Try to insert invalid complexity
      expect(() => {
        db.prepare(`
          INSERT INTO template_node_configs (
            node_type, template_id, template_name, template_views,
            node_name, parameters_json, complexity
          ) VALUES (?, ?, ?, ?, ?, ?, ?)
        `).run(
          'n8n-nodes-base.test',
          1,
          'Test Template',
          100,
          'Test Node',
          '{}',
          'invalid' // Should fail CHECK constraint
        );
      }).toThrow();
    });

    it('should accept valid complexity values', () => {
      const validComplexities = ['simple', 'medium', 'complex'];

      validComplexities.forEach((complexity, index) => {
        expect(() => {
          db.prepare(`
            INSERT INTO template_node_configs (
              node_type, template_id, template_name, template_views,
              node_name, parameters_json, complexity
            ) VALUES (?, ?, ?, ?, ?, ?, ?)
          `).run(
            'n8n-nodes-base.test',
            index + 1,
            'Test Template',
            100,
            'Test Node',
            '{}',
            complexity
          );
        }).not.toThrow();
      });

      const count = db.prepare('SELECT COUNT(*) as count FROM template_node_configs').get() as any;
      expect(count.count).toBe(3);
    });
  });

  describe('Indexes', () => {
    it('should create idx_config_node_type_rank index', () => {
      const indexes = db.prepare(`
        SELECT name FROM sqlite_master
        WHERE type='index' AND tbl_name='template_node_configs'
      `).all() as any[];

      const indexNames = indexes.map(idx => idx.name);
      expect(indexNames).toContain('idx_config_node_type_rank');
    });

    it('should create idx_config_complexity index', () => {
      const indexes = db.prepare(`
        SELECT name FROM sqlite_master
        WHERE type='index' AND tbl_name='template_node_configs'
      `).all() as any[];

      const indexNames = indexes.map(idx => idx.name);
      expect(indexNames).toContain('idx_config_complexity');
    });

    it('should create idx_config_auth index', () => {
      const indexes = db.prepare(`
        SELECT name FROM sqlite_master
        WHERE type='index' AND tbl_name='template_node_configs'
      `).all() as any[];

      const indexNames = indexes.map(idx => idx.name);
      expect(indexNames).toContain('idx_config_auth');
    });
  });

  describe('View: ranked_node_configs', () => {
    it('should create ranked_node_configs view', () => {
      const viewExists = db.prepare(`
        SELECT name FROM sqlite_master
        WHERE type='view' AND name='ranked_node_configs'
      `).get();

      expect(viewExists).toBeDefined();
      expect(viewExists).toHaveProperty('name', 'ranked_node_configs');
    });

    it('should return only top 5 ranked configs per node type', () => {
      // Insert 10 configs for same node type with different ranks
      for (let i = 1; i <= 10; i++) {
        db.prepare(`
          INSERT INTO template_node_configs (
            node_type, template_id, template_name, template_views,
            node_name, parameters_json, rank
          ) VALUES (?, ?, ?, ?, ?, ?, ?)
        `).run(
          'n8n-nodes-base.httpRequest',
          i,
          `Template ${i}`,
          1000 - (i * 50), // Decreasing views
          'HTTP Request',
          '{}',
          i // Rank 1-10
        );
      }

      const rankedConfigs = db.prepare('SELECT * FROM ranked_node_configs').all() as any[];

      // Should only return rank 1-5
      expect(rankedConfigs).toHaveLength(5);
      expect(Math.max(...rankedConfigs.map(c => c.rank))).toBe(5);
      expect(Math.min(...rankedConfigs.map(c => c.rank))).toBe(1);
    });

    it('should order by node_type and rank', () => {
      // Insert configs for multiple node types
      const configs = [
        { nodeType: 'n8n-nodes-base.webhook', rank: 2 },
        { nodeType: 'n8n-nodes-base.webhook', rank: 1 },
        { nodeType: 'n8n-nodes-base.httpRequest', rank: 2 },
        { nodeType: 'n8n-nodes-base.httpRequest', rank: 1 },
      ];

      configs.forEach((config, index) => {
        db.prepare(`
          INSERT INTO template_node_configs (
            node_type, template_id, template_name, template_views,
            node_name, parameters_json, rank
          ) VALUES (?, ?, ?, ?, ?, ?, ?)
        `).run(
          config.nodeType,
          index + 1,
          `Template ${index}`,
          100,
          'Node',
          '{}',
          config.rank
        );
      });

      const rankedConfigs = db.prepare('SELECT * FROM ranked_node_configs ORDER BY node_type, rank').all() as any[];

      // First two should be httpRequest rank 1, 2
      expect(rankedConfigs[0].node_type).toBe('n8n-nodes-base.httpRequest');
      expect(rankedConfigs[0].rank).toBe(1);
      expect(rankedConfigs[1].node_type).toBe('n8n-nodes-base.httpRequest');
      expect(rankedConfigs[1].rank).toBe(2);

      // Last two should be webhook rank 1, 2
      expect(rankedConfigs[2].node_type).toBe('n8n-nodes-base.webhook');
      expect(rankedConfigs[2].rank).toBe(1);
      expect(rankedConfigs[3].node_type).toBe('n8n-nodes-base.webhook');
      expect(rankedConfigs[3].rank).toBe(2);
    });
  });

  describe('Foreign Key Constraints', () => {
    beforeEach(() => {
      // Enable foreign keys
      db.exec('PRAGMA foreign_keys = ON');
      // Note: Templates are already created in the main beforeEach
    });

    it('should allow inserting config with valid template_id', () => {
      expect(() => {
        db.prepare(`
          INSERT INTO template_node_configs (
            node_type, template_id, template_name, template_views,
            node_name, parameters_json
          ) VALUES (?, ?, ?, ?, ?, ?)
        `).run(
          'n8n-nodes-base.test',
          1, // Valid template_id
          'Test Template',
          100,
          'Test Node',
          '{}'
        );
      }).not.toThrow();
    });

    it('should cascade delete configs when template is deleted', () => {
      // Insert config
      db.prepare(`
        INSERT INTO template_node_configs (
          node_type, template_id, template_name, template_views,
          node_name, parameters_json
        ) VALUES (?, ?, ?, ?, ?, ?)
      `).run(
        'n8n-nodes-base.test',
        1,
        'Test Template',
        100,
        'Test Node',
        '{}'
      );

      // Verify config exists
      let configs = db.prepare('SELECT * FROM template_node_configs WHERE template_id = ?').all(1) as any[];
      expect(configs).toHaveLength(1);

      // Delete template
      db.prepare('DELETE FROM templates WHERE id = ?').run(1);

      // Verify config is deleted (CASCADE)
      configs = db.prepare('SELECT * FROM template_node_configs WHERE template_id = ?').all(1) as any[];
      expect(configs).toHaveLength(0);
    });
  });

  describe('Data Operations', () => {
    it('should insert and retrieve config with all fields', () => {
      const testConfig = {
        node_type: 'n8n-nodes-base.webhook',
        template_id: 1,
        template_name: 'Webhook Template',
        template_views: 2000,
        node_name: 'Webhook Trigger',
        parameters_json: JSON.stringify({
          httpMethod: 'POST',
          path: 'webhook-test',
          responseMode: 'lastNode'
        }),
        credentials_json: JSON.stringify({
          webhookAuth: { id: '1', name: 'Webhook Auth' }
        }),
        has_credentials: 1,
        has_expressions: 1,
        complexity: 'medium',
        use_cases: JSON.stringify(['webhook processing', 'automation triggers']),
        rank: 1
      };

      db.prepare(`
        INSERT INTO template_node_configs (
          node_type, template_id, template_name, template_views,
          node_name, parameters_json, credentials_json,
          has_credentials, has_expressions, complexity, use_cases, rank
        ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
      `).run(...Object.values(testConfig));

      const retrieved = db.prepare('SELECT * FROM template_node_configs WHERE id = 1').get() as any;

      expect(retrieved.node_type).toBe(testConfig.node_type);
      expect(retrieved.template_id).toBe(testConfig.template_id);
      expect(retrieved.template_name).toBe(testConfig.template_name);
      expect(retrieved.template_views).toBe(testConfig.template_views);
      expect(retrieved.node_name).toBe(testConfig.node_name);
      expect(retrieved.parameters_json).toBe(testConfig.parameters_json);
      expect(retrieved.credentials_json).toBe(testConfig.credentials_json);
      expect(retrieved.has_credentials).toBe(testConfig.has_credentials);
      expect(retrieved.has_expressions).toBe(testConfig.has_expressions);
      expect(retrieved.complexity).toBe(testConfig.complexity);
      expect(retrieved.use_cases).toBe(testConfig.use_cases);
      expect(retrieved.rank).toBe(testConfig.rank);
      expect(retrieved.created_at).toBeDefined();
    });

    it('should handle nullable fields correctly', () => {
      db.prepare(`
        INSERT INTO template_node_configs (
          node_type, template_id, template_name, template_views,
          node_name, parameters_json
        ) VALUES (?, ?, ?, ?, ?, ?)
      `).run(
        'n8n-nodes-base.test',
        1,
        'Test',
        100,
        'Node',
        '{}'
      );

      const retrieved = db.prepare('SELECT * FROM template_node_configs WHERE id = 1').get() as any;

      expect(retrieved.credentials_json).toBeNull();
      expect(retrieved.has_credentials).toBe(0); // Default value
      expect(retrieved.has_expressions).toBe(0); // Default value
      expect(retrieved.rank).toBe(0); // Default value
    });

    it('should update rank values', () => {
      // Insert multiple configs
      for (let i = 1; i <= 3; i++) {
        db.prepare(`
          INSERT INTO template_node_configs (
            node_type, template_id, template_name, template_views,
            node_name, parameters_json, rank
          ) VALUES (?, ?, ?, ?, ?, ?, ?)
        `).run(
          'n8n-nodes-base.test',
          i,
          'Template',
          100,
          'Node',
          '{}',
          0 // Initial rank
        );
      }

      // Update ranks
      db.exec(`
        UPDATE template_node_configs
        SET rank = (
          SELECT COUNT(*) + 1
          FROM template_node_configs AS t2
          WHERE t2.node_type = template_node_configs.node_type
            AND t2.template_views > template_node_configs.template_views
        )
      `);

      const configs = db.prepare('SELECT * FROM template_node_configs ORDER BY rank').all() as any[];

      // All should have same rank (same views)
      expect(configs.every(c => c.rank === 1)).toBe(true);
    });

    it('should delete configs with rank > 10', () => {
      // Insert 15 configs with different ranks
      for (let i = 1; i <= 15; i++) {
        db.prepare(`
          INSERT INTO template_node_configs (
            node_type, template_id, template_name, template_views,
            node_name, parameters_json, rank
          ) VALUES (?, ?, ?, ?, ?, ?, ?)
        `).run(
          'n8n-nodes-base.test',
          i,
          'Template',
          100,
          'Node',
          '{}',
          i // Rank 1-15
        );
      }

      // Delete configs with rank > 10
      db.exec(`
        DELETE FROM template_node_configs
        WHERE id NOT IN (
          SELECT id FROM template_node_configs
          WHERE rank <= 10
          ORDER BY node_type, rank
        )
      `);

      const remaining = db.prepare('SELECT * FROM template_node_configs').all() as any[];

      expect(remaining).toHaveLength(10);
      expect(Math.max(...remaining.map(c => c.rank))).toBe(10);
    });
  });

  describe('Query Performance', () => {
    beforeEach(() => {
      // Insert 1000 configs for performance testing
      const stmt = db.prepare(`
        INSERT INTO template_node_configs (
          node_type, template_id, template_name, template_views,
          node_name, parameters_json, rank
        ) VALUES (?, ?, ?, ?, ?, ?, ?)
      `);

      const nodeTypes = [
        'n8n-nodes-base.httpRequest',
        'n8n-nodes-base.webhook',
        'n8n-nodes-base.slack',
        'n8n-nodes-base.googleSheets',
        'n8n-nodes-base.code'
      ];

      for (let i = 1; i <= 1000; i++) {
        const nodeType = nodeTypes[i % nodeTypes.length];
        stmt.run(
          nodeType,
          i,
          `Template ${i}`,
          Math.floor(Math.random() * 10000),
          'Node',
          '{}',
          (i % 10) + 1 // Rank 1-10
        );
      }
    });

    it('should query by node_type and rank efficiently', () => {
      const start = Date.now();
      const results = db.prepare(`
        SELECT * FROM template_node_configs
        WHERE node_type = ?
        ORDER BY rank
        LIMIT 3
      `).all('n8n-nodes-base.httpRequest') as any[];
      const duration = Date.now() - start;

      expect(results.length).toBeGreaterThan(0);
      expect(duration).toBeLessThan(10); // Should be very fast with index
    });

    it('should filter by complexity efficiently', () => {
      // First set some complexity values
      db.exec(`UPDATE template_node_configs SET complexity = 'simple' WHERE id % 3 = 0`);
      db.exec(`UPDATE template_node_configs SET complexity = 'medium' WHERE id % 3 = 1`);
      db.exec(`UPDATE template_node_configs SET complexity = 'complex' WHERE id % 3 = 2`);

      const start = Date.now();
      const results = db.prepare(`
        SELECT * FROM template_node_configs
        WHERE node_type = ? AND complexity = ?
        ORDER BY rank
        LIMIT 5
      `).all('n8n-nodes-base.webhook', 'simple') as any[];
      const duration = Date.now() - start;

      expect(duration).toBeLessThan(10); // Should be fast with index
    });
  });

  describe('Migration Idempotency', () => {
    it('should be safe to run migration multiple times', () => {
      const migrationPath = path.join(__dirname, '../../../src/database/migrations/add-template-node-configs.sql');
      const migration = fs.readFileSync(migrationPath, 'utf-8');

      // Run migration again
      expect(() => {
        db.exec(migration);
      }).not.toThrow();

      // Table should still exist
      const tableExists = db.prepare(`
        SELECT name FROM sqlite_master
        WHERE type='table' AND name='template_node_configs'
      `).get();

      expect(tableExists).toBeDefined();
    });
  });
});

```
Page 19/46FirstPrevNextLast