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

# Directory Structure

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

--------------------------------------------------------------------------------
/tests/unit/services/expression-format-validator.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, it, expect } from 'vitest';
  2 | import { ExpressionFormatValidator } from '../../../src/services/expression-format-validator';
  3 | 
  4 | describe('ExpressionFormatValidator', () => {
  5 |   describe('validateAndFix', () => {
  6 |     const context = {
  7 |       nodeType: 'n8n-nodes-base.httpRequest',
  8 |       nodeName: 'HTTP Request',
  9 |       nodeId: 'test-id-1'
 10 |     };
 11 | 
 12 |     describe('Simple string expressions', () => {
 13 |       it('should detect missing = prefix for expression', () => {
 14 |         const value = '{{ $env.API_KEY }}';
 15 |         const issue = ExpressionFormatValidator.validateAndFix(value, 'apiKey', context);
 16 | 
 17 |         expect(issue).toBeTruthy();
 18 |         expect(issue?.issueType).toBe('missing-prefix');
 19 |         expect(issue?.correctedValue).toBe('={{ $env.API_KEY }}');
 20 |         expect(issue?.severity).toBe('error');
 21 |       });
 22 | 
 23 |       it('should accept expression with = prefix', () => {
 24 |         const value = '={{ $env.API_KEY }}';
 25 |         const issue = ExpressionFormatValidator.validateAndFix(value, 'apiKey', context);
 26 | 
 27 |         expect(issue).toBeNull();
 28 |       });
 29 | 
 30 |       it('should detect mixed content without prefix', () => {
 31 |         const value = 'Bearer {{ $env.TOKEN }}';
 32 |         const issue = ExpressionFormatValidator.validateAndFix(value, 'authorization', context);
 33 | 
 34 |         expect(issue).toBeTruthy();
 35 |         expect(issue?.issueType).toBe('missing-prefix');
 36 |         expect(issue?.correctedValue).toBe('=Bearer {{ $env.TOKEN }}');
 37 |       });
 38 | 
 39 |       it('should accept mixed content with prefix', () => {
 40 |         const value = '=Bearer {{ $env.TOKEN }}';
 41 |         const issue = ExpressionFormatValidator.validateAndFix(value, 'authorization', context);
 42 | 
 43 |         expect(issue).toBeNull();
 44 |       });
 45 | 
 46 |       it('should ignore plain strings without expressions', () => {
 47 |         const value = 'https://api.example.com';
 48 |         const issue = ExpressionFormatValidator.validateAndFix(value, 'url', context);
 49 | 
 50 |         expect(issue).toBeNull();
 51 |       });
 52 |     });
 53 | 
 54 |     describe('Resource Locator fields', () => {
 55 |       const githubContext = {
 56 |         nodeType: 'n8n-nodes-base.github',
 57 |         nodeName: 'GitHub',
 58 |         nodeId: 'github-1'
 59 |       };
 60 | 
 61 |       it('should detect expression in owner field needing resource locator', () => {
 62 |         const value = '{{ $vars.GITHUB_OWNER }}';
 63 |         const issue = ExpressionFormatValidator.validateAndFix(value, 'owner', githubContext);
 64 | 
 65 |         expect(issue).toBeTruthy();
 66 |         expect(issue?.issueType).toBe('needs-resource-locator');
 67 |         expect(issue?.correctedValue).toEqual({
 68 |           __rl: true,
 69 |           value: '={{ $vars.GITHUB_OWNER }}',
 70 |           mode: 'expression'
 71 |         });
 72 |         expect(issue?.severity).toBe('error');
 73 |       });
 74 | 
 75 |       it('should accept resource locator with expression', () => {
 76 |         const value = {
 77 |           __rl: true,
 78 |           value: '={{ $vars.GITHUB_OWNER }}',
 79 |           mode: 'expression'
 80 |         };
 81 |         const issue = ExpressionFormatValidator.validateAndFix(value, 'owner', githubContext);
 82 | 
 83 |         expect(issue).toBeNull();
 84 |       });
 85 | 
 86 |       it('should detect missing prefix in resource locator value', () => {
 87 |         const value = {
 88 |           __rl: true,
 89 |           value: '{{ $vars.GITHUB_OWNER }}',
 90 |           mode: 'expression'
 91 |         };
 92 |         const issue = ExpressionFormatValidator.validateAndFix(value, 'owner', githubContext);
 93 | 
 94 |         expect(issue).toBeTruthy();
 95 |         expect(issue?.issueType).toBe('missing-prefix');
 96 |         expect(issue?.correctedValue.value).toBe('={{ $vars.GITHUB_OWNER }}');
 97 |       });
 98 | 
 99 |       it('should warn if expression has prefix but should use RL format', () => {
100 |         const value = '={{ $vars.GITHUB_OWNER }}';
101 |         const issue = ExpressionFormatValidator.validateAndFix(value, 'owner', githubContext);
102 | 
103 |         expect(issue).toBeTruthy();
104 |         expect(issue?.issueType).toBe('needs-resource-locator');
105 |         expect(issue?.severity).toBe('warning');
106 |       });
107 |     });
108 | 
109 |     describe('Multiple expressions', () => {
110 |       it('should detect multiple expressions without prefix', () => {
111 |         const value = '{{ $json.first }} - {{ $json.last }}';
112 |         const issue = ExpressionFormatValidator.validateAndFix(value, 'fullName', context);
113 | 
114 |         expect(issue).toBeTruthy();
115 |         expect(issue?.issueType).toBe('missing-prefix');
116 |         expect(issue?.correctedValue).toBe('={{ $json.first }} - {{ $json.last }}');
117 |       });
118 | 
119 |       it('should accept multiple expressions with prefix', () => {
120 |         const value = '={{ $json.first }} - {{ $json.last }}';
121 |         const issue = ExpressionFormatValidator.validateAndFix(value, 'fullName', context);
122 | 
123 |         expect(issue).toBeNull();
124 |       });
125 |     });
126 | 
127 |     describe('Edge cases', () => {
128 |       it('should handle null values', () => {
129 |         const issue = ExpressionFormatValidator.validateAndFix(null, 'field', context);
130 |         expect(issue).toBeNull();
131 |       });
132 | 
133 |       it('should handle undefined values', () => {
134 |         const issue = ExpressionFormatValidator.validateAndFix(undefined, 'field', context);
135 |         expect(issue).toBeNull();
136 |       });
137 | 
138 |       it('should handle empty strings', () => {
139 |         const issue = ExpressionFormatValidator.validateAndFix('', 'field', context);
140 |         expect(issue).toBeNull();
141 |       });
142 | 
143 |       it('should handle numbers', () => {
144 |         const issue = ExpressionFormatValidator.validateAndFix(42, 'field', context);
145 |         expect(issue).toBeNull();
146 |       });
147 | 
148 |       it('should handle booleans', () => {
149 |         const issue = ExpressionFormatValidator.validateAndFix(true, 'field', context);
150 |         expect(issue).toBeNull();
151 |       });
152 | 
153 |       it('should handle arrays', () => {
154 |         const issue = ExpressionFormatValidator.validateAndFix(['item1', 'item2'], 'field', context);
155 |         expect(issue).toBeNull();
156 |       });
157 |     });
158 |   });
159 | 
160 |   describe('validateNodeParameters', () => {
161 |     const context = {
162 |       nodeType: 'n8n-nodes-base.emailSend',
163 |       nodeName: 'Send Email',
164 |       nodeId: 'email-1'
165 |     };
166 | 
167 |     it('should validate all parameters recursively', () => {
168 |       const parameters = {
169 |         fromEmail: '{{ $env.SENDER_EMAIL }}',
170 |         toEmail: '[email protected]',
171 |         subject: 'Test {{ $json.type }}',
172 |         body: {
173 |           html: '<p>Hello {{ $json.name }}</p>',
174 |           text: 'Hello {{ $json.name }}'
175 |         },
176 |         options: {
177 |           replyTo: '={{ $env.REPLY_EMAIL }}'
178 |         }
179 |       };
180 | 
181 |       const issues = ExpressionFormatValidator.validateNodeParameters(parameters, context);
182 | 
183 |       expect(issues).toHaveLength(4);
184 |       expect(issues.map(i => i.fieldPath)).toContain('fromEmail');
185 |       expect(issues.map(i => i.fieldPath)).toContain('subject');
186 |       expect(issues.map(i => i.fieldPath)).toContain('body.html');
187 |       expect(issues.map(i => i.fieldPath)).toContain('body.text');
188 |     });
189 | 
190 |     it('should handle arrays with expressions', () => {
191 |       const parameters = {
192 |         recipients: [
193 |           '{{ $json.email1 }}',
194 |           '[email protected]',
195 |           '={{ $json.email2 }}'
196 |         ]
197 |       };
198 | 
199 |       const issues = ExpressionFormatValidator.validateNodeParameters(parameters, context);
200 | 
201 |       expect(issues).toHaveLength(1);
202 |       expect(issues[0].fieldPath).toBe('recipients[0]');
203 |       expect(issues[0].correctedValue).toBe('={{ $json.email1 }}');
204 |     });
205 | 
206 |     it('should handle nested objects', () => {
207 |       const parameters = {
208 |         config: {
209 |           database: {
210 |             host: '{{ $env.DB_HOST }}',
211 |             port: 5432,
212 |             name: 'mydb'
213 |           }
214 |         }
215 |       };
216 | 
217 |       const issues = ExpressionFormatValidator.validateNodeParameters(parameters, context);
218 | 
219 |       expect(issues).toHaveLength(1);
220 |       expect(issues[0].fieldPath).toBe('config.database.host');
221 |     });
222 | 
223 |     it('should skip circular references', () => {
224 |       const circular: any = { a: 1 };
225 |       circular.self = circular;
226 | 
227 |       const parameters = {
228 |         normal: '{{ $json.value }}',
229 |         circular
230 |       };
231 | 
232 |       const issues = ExpressionFormatValidator.validateNodeParameters(parameters, context);
233 | 
234 |       // Should only find the issue in 'normal', not crash on circular
235 |       expect(issues).toHaveLength(1);
236 |       expect(issues[0].fieldPath).toBe('normal');
237 |     });
238 | 
239 |     it('should handle maximum recursion depth', () => {
240 |       // Create a deeply nested object (105 levels deep, exceeding the limit of 100)
241 |       let deepObject: any = { value: '{{ $json.data }}' };
242 |       let current = deepObject;
243 |       for (let i = 0; i < 105; i++) {
244 |         current.nested = { value: `{{ $json.level${i} }}` };
245 |         current = current.nested;
246 |       }
247 | 
248 |       const parameters = {
249 |         deep: deepObject
250 |       };
251 | 
252 |       const issues = ExpressionFormatValidator.validateNodeParameters(parameters, context);
253 | 
254 |       // Should find expression format issues up to the depth limit
255 |       const depthWarning = issues.find(i => i.explanation.includes('Maximum recursion depth'));
256 |       expect(depthWarning).toBeTruthy();
257 |       expect(depthWarning?.severity).toBe('warning');
258 | 
259 |       // Should still find some expression format errors before hitting the limit
260 |       const formatErrors = issues.filter(i => i.issueType === 'missing-prefix');
261 |       expect(formatErrors.length).toBeGreaterThan(0);
262 |       expect(formatErrors.length).toBeLessThanOrEqual(100); // Should not exceed the depth limit
263 |     });
264 |   });
265 | 
266 |   describe('formatErrorMessage', () => {
267 |     const context = {
268 |       nodeType: 'n8n-nodes-base.github',
269 |       nodeName: 'Create Issue',
270 |       nodeId: 'github-1'
271 |     };
272 | 
273 |     it('should format error message for missing prefix', () => {
274 |       const issue = {
275 |         fieldPath: 'title',
276 |         currentValue: '{{ $json.title }}',
277 |         correctedValue: '={{ $json.title }}',
278 |         issueType: 'missing-prefix' as const,
279 |         explanation: "Expression missing required '=' prefix.",
280 |         severity: 'error' as const
281 |       };
282 | 
283 |       const message = ExpressionFormatValidator.formatErrorMessage(issue, context);
284 | 
285 |       expect(message).toContain("Expression format error in node 'Create Issue'");
286 |       expect(message).toContain('Field \'title\'');
287 |       expect(message).toContain('Current (incorrect):');
288 |       expect(message).toContain('"title": "{{ $json.title }}"');
289 |       expect(message).toContain('Fixed (correct):');
290 |       expect(message).toContain('"title": "={{ $json.title }}"');
291 |     });
292 | 
293 |     it('should format error message for resource locator', () => {
294 |       const issue = {
295 |         fieldPath: 'owner',
296 |         currentValue: '{{ $vars.OWNER }}',
297 |         correctedValue: {
298 |           __rl: true,
299 |           value: '={{ $vars.OWNER }}',
300 |           mode: 'expression'
301 |         },
302 |         issueType: 'needs-resource-locator' as const,
303 |         explanation: 'Field needs resource locator format.',
304 |         severity: 'error' as const
305 |       };
306 | 
307 |       const message = ExpressionFormatValidator.formatErrorMessage(issue, context);
308 | 
309 |       expect(message).toContain("Expression format error in node 'Create Issue'");
310 |       expect(message).toContain('Current (incorrect):');
311 |       expect(message).toContain('"owner": "{{ $vars.OWNER }}"');
312 |       expect(message).toContain('Fixed (correct):');
313 |       expect(message).toContain('"__rl": true');
314 |       expect(message).toContain('"value": "={{ $vars.OWNER }}"');
315 |       expect(message).toContain('"mode": "expression"');
316 |     });
317 |   });
318 | 
319 |   describe('Real-world examples', () => {
320 |     it('should validate Email Send node example', () => {
321 |       const context = {
322 |         nodeType: 'n8n-nodes-base.emailSend',
323 |         nodeName: 'Error Handler',
324 |         nodeId: 'b9dd1cfd-ee66-4049-97e7-1af6d976a4e0'
325 |       };
326 | 
327 |       const parameters = {
328 |         fromEmail: '{{ $env.ADMIN_EMAIL }}',
329 |         toEmail: '[email protected]',
330 |         subject: 'GitHub Issue Workflow Error - HIGH PRIORITY',
331 |         options: {}
332 |       };
333 | 
334 |       const issues = ExpressionFormatValidator.validateNodeParameters(parameters, context);
335 | 
336 |       expect(issues).toHaveLength(1);
337 |       expect(issues[0].fieldPath).toBe('fromEmail');
338 |       expect(issues[0].correctedValue).toBe('={{ $env.ADMIN_EMAIL }}');
339 |     });
340 | 
341 |     it('should validate GitHub node example', () => {
342 |       const context = {
343 |         nodeType: 'n8n-nodes-base.github',
344 |         nodeName: 'Send Welcome Comment',
345 |         nodeId: '3c742ca1-af8f-4d80-a47e-e68fb1ced491'
346 |       };
347 | 
348 |       const parameters = {
349 |         operation: 'createComment',
350 |         owner: '{{ $vars.GITHUB_OWNER }}',
351 |         repository: '{{ $vars.GITHUB_REPO }}',
352 |         issueNumber: null,
353 |         body: '👋 Hi @{{ $(\'Extract Issue Data\').first().json.author }}!\n\nThank you for creating this issue.'
354 |       };
355 | 
356 |       const issues = ExpressionFormatValidator.validateNodeParameters(parameters, context);
357 | 
358 |       expect(issues.length).toBeGreaterThan(0);
359 |       expect(issues.some(i => i.fieldPath === 'owner')).toBe(true);
360 |       expect(issues.some(i => i.fieldPath === 'repository')).toBe(true);
361 |       expect(issues.some(i => i.fieldPath === 'body')).toBe(true);
362 |     });
363 |   });
364 | });
```

--------------------------------------------------------------------------------
/tests/unit/services/enhanced-config-validator-operations.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Tests for EnhancedConfigValidator operation and resource validation
  3 |  */
  4 | 
  5 | import { describe, it, expect, beforeEach, afterEach } from 'vitest';
  6 | import { EnhancedConfigValidator } from '../../../src/services/enhanced-config-validator';
  7 | import { NodeRepository } from '../../../src/database/node-repository';
  8 | import { createTestDatabase } from '../../utils/database-utils';
  9 | 
 10 | describe('EnhancedConfigValidator - Operation and Resource Validation', () => {
 11 |   let repository: NodeRepository;
 12 |   let testDb: any;
 13 | 
 14 |   beforeEach(async () => {
 15 |     testDb = await createTestDatabase();
 16 |     repository = testDb.nodeRepository;
 17 | 
 18 |     // Initialize similarity services
 19 |     EnhancedConfigValidator.initializeSimilarityServices(repository);
 20 | 
 21 |     // Add Google Drive test node
 22 |     const googleDriveNode = {
 23 |       nodeType: 'nodes-base.googleDrive',
 24 |       packageName: 'n8n-nodes-base',
 25 |       displayName: 'Google Drive',
 26 |       description: 'Access Google Drive',
 27 |       category: 'transform',
 28 |       style: 'declarative' as const,
 29 |       isAITool: false,
 30 |       isTrigger: false,
 31 |       isWebhook: false,
 32 |       isVersioned: true,
 33 |       version: '1',
 34 |       properties: [
 35 |         {
 36 |           name: 'resource',
 37 |           type: 'options',
 38 |           required: true,
 39 |           options: [
 40 |             { value: 'file', name: 'File' },
 41 |             { value: 'folder', name: 'Folder' },
 42 |             { value: 'fileFolder', name: 'File & Folder' }
 43 |           ]
 44 |         },
 45 |         {
 46 |           name: 'operation',
 47 |           type: 'options',
 48 |           required: true,
 49 |           displayOptions: {
 50 |             show: {
 51 |               resource: ['file']
 52 |             }
 53 |           },
 54 |           options: [
 55 |             { value: 'copy', name: 'Copy' },
 56 |             { value: 'delete', name: 'Delete' },
 57 |             { value: 'download', name: 'Download' },
 58 |             { value: 'list', name: 'List' },
 59 |             { value: 'share', name: 'Share' },
 60 |             { value: 'update', name: 'Update' },
 61 |             { value: 'upload', name: 'Upload' }
 62 |           ]
 63 |         },
 64 |         {
 65 |           name: 'operation',
 66 |           type: 'options',
 67 |           required: true,
 68 |           displayOptions: {
 69 |             show: {
 70 |               resource: ['folder']
 71 |             }
 72 |           },
 73 |           options: [
 74 |             { value: 'create', name: 'Create' },
 75 |             { value: 'delete', name: 'Delete' },
 76 |             { value: 'share', name: 'Share' }
 77 |           ]
 78 |         },
 79 |         {
 80 |           name: 'operation',
 81 |           type: 'options',
 82 |           required: true,
 83 |           displayOptions: {
 84 |             show: {
 85 |               resource: ['fileFolder']
 86 |             }
 87 |           },
 88 |           options: [
 89 |             { value: 'search', name: 'Search' }
 90 |           ]
 91 |         }
 92 |       ],
 93 |       operations: [],
 94 |       credentials: []
 95 |     };
 96 | 
 97 |     repository.saveNode(googleDriveNode);
 98 | 
 99 |     // Add Slack test node
100 |     const slackNode = {
101 |       nodeType: 'nodes-base.slack',
102 |       packageName: 'n8n-nodes-base',
103 |       displayName: 'Slack',
104 |       description: 'Send messages to Slack',
105 |       category: 'communication',
106 |       style: 'declarative' as const,
107 |       isAITool: false,
108 |       isTrigger: false,
109 |       isWebhook: false,
110 |       isVersioned: true,
111 |       version: '2',
112 |       properties: [
113 |         {
114 |           name: 'resource',
115 |           type: 'options',
116 |           required: true,
117 |           options: [
118 |             { value: 'channel', name: 'Channel' },
119 |             { value: 'message', name: 'Message' },
120 |             { value: 'user', name: 'User' }
121 |           ]
122 |         },
123 |         {
124 |           name: 'operation',
125 |           type: 'options',
126 |           required: true,
127 |           displayOptions: {
128 |             show: {
129 |               resource: ['message']
130 |             }
131 |           },
132 |           options: [
133 |             { value: 'send', name: 'Send' },
134 |             { value: 'update', name: 'Update' },
135 |             { value: 'delete', name: 'Delete' }
136 |           ]
137 |         }
138 |       ],
139 |       operations: [],
140 |       credentials: []
141 |     };
142 | 
143 |     repository.saveNode(slackNode);
144 |   });
145 | 
146 |   afterEach(async () => {
147 |     // Clean up database
148 |     if (testDb) {
149 |       await testDb.cleanup();
150 |     }
151 |   });
152 | 
153 |   describe('Invalid Operations', () => {
154 |     it('should detect invalid operation "listFiles" for Google Drive', () => {
155 |       const config = {
156 |         resource: 'fileFolder',
157 |         operation: 'listFiles'
158 |       };
159 | 
160 |       const node = repository.getNode('nodes-base.googleDrive');
161 |       const result = EnhancedConfigValidator.validateWithMode(
162 |         'nodes-base.googleDrive',
163 |         config,
164 |         node.properties,
165 |         'operation',
166 |         'ai-friendly'
167 |       );
168 | 
169 |       expect(result.valid).toBe(false);
170 | 
171 |       // Should have an error for invalid operation
172 |       const operationError = result.errors.find(e => e.property === 'operation');
173 |       expect(operationError).toBeDefined();
174 |       expect(operationError!.message).toContain('Invalid operation "listFiles"');
175 |       expect(operationError!.message).toContain('Did you mean');
176 |       expect(operationError!.fix).toContain('search'); // Should suggest 'search' for fileFolder resource
177 |     });
178 | 
179 |     it('should provide suggestions for typos in operations', () => {
180 |       const config = {
181 |         resource: 'file',
182 |         operation: 'downlod' // Typo: missing 'a'
183 |       };
184 | 
185 |       const node = repository.getNode('nodes-base.googleDrive');
186 |       const result = EnhancedConfigValidator.validateWithMode(
187 |         'nodes-base.googleDrive',
188 |         config,
189 |         node.properties,
190 |         'operation',
191 |         'ai-friendly'
192 |       );
193 | 
194 |       expect(result.valid).toBe(false);
195 | 
196 |       const operationError = result.errors.find(e => e.property === 'operation');
197 |       expect(operationError).toBeDefined();
198 |       expect(operationError!.message).toContain('Did you mean "download"');
199 |     });
200 | 
201 |     it('should list valid operations for the resource', () => {
202 |       const config = {
203 |         resource: 'folder',
204 |         operation: 'upload' // Invalid for folder resource
205 |       };
206 | 
207 |       const node = repository.getNode('nodes-base.googleDrive');
208 |       const result = EnhancedConfigValidator.validateWithMode(
209 |         'nodes-base.googleDrive',
210 |         config,
211 |         node.properties,
212 |         'operation',
213 |         'ai-friendly'
214 |       );
215 | 
216 |       expect(result.valid).toBe(false);
217 | 
218 |       const operationError = result.errors.find(e => e.property === 'operation');
219 |       expect(operationError).toBeDefined();
220 |       expect(operationError!.fix).toContain('Valid operations for resource "folder"');
221 |       expect(operationError!.fix).toContain('create');
222 |       expect(operationError!.fix).toContain('delete');
223 |       expect(operationError!.fix).toContain('share');
224 |     });
225 |   });
226 | 
227 |   describe('Invalid Resources', () => {
228 |     it('should detect plural resource "files" and suggest singular', () => {
229 |       const config = {
230 |         resource: 'files', // Should be 'file'
231 |         operation: 'list'
232 |       };
233 | 
234 |       const node = repository.getNode('nodes-base.googleDrive');
235 |       const result = EnhancedConfigValidator.validateWithMode(
236 |         'nodes-base.googleDrive',
237 |         config,
238 |         node.properties,
239 |         'operation',
240 |         'ai-friendly'
241 |       );
242 | 
243 |       expect(result.valid).toBe(false);
244 | 
245 |       const resourceError = result.errors.find(e => e.property === 'resource');
246 |       expect(resourceError).toBeDefined();
247 |       expect(resourceError!.message).toContain('Invalid resource "files"');
248 |       expect(resourceError!.message).toContain('Did you mean "file"');
249 |       expect(resourceError!.fix).toContain('Use singular');
250 |     });
251 | 
252 |     it('should suggest similar resources for typos', () => {
253 |       const config = {
254 |         resource: 'flie', // Typo
255 |         operation: 'download'
256 |       };
257 | 
258 |       const node = repository.getNode('nodes-base.googleDrive');
259 |       const result = EnhancedConfigValidator.validateWithMode(
260 |         'nodes-base.googleDrive',
261 |         config,
262 |         node.properties,
263 |         'operation',
264 |         'ai-friendly'
265 |       );
266 | 
267 |       expect(result.valid).toBe(false);
268 | 
269 |       const resourceError = result.errors.find(e => e.property === 'resource');
270 |       expect(resourceError).toBeDefined();
271 |       expect(resourceError!.message).toContain('Did you mean "file"');
272 |     });
273 | 
274 |     it('should list valid resources when no match found', () => {
275 |       const config = {
276 |         resource: 'document', // Not a valid resource
277 |         operation: 'create'
278 |       };
279 | 
280 |       const node = repository.getNode('nodes-base.googleDrive');
281 |       const result = EnhancedConfigValidator.validateWithMode(
282 |         'nodes-base.googleDrive',
283 |         config,
284 |         node.properties,
285 |         'operation',
286 |         'ai-friendly'
287 |       );
288 | 
289 |       expect(result.valid).toBe(false);
290 | 
291 |       const resourceError = result.errors.find(e => e.property === 'resource');
292 |       expect(resourceError).toBeDefined();
293 |       expect(resourceError!.fix).toContain('Valid resources:');
294 |       expect(resourceError!.fix).toContain('file');
295 |       expect(resourceError!.fix).toContain('folder');
296 |     });
297 |   });
298 | 
299 |   describe('Combined Resource and Operation Validation', () => {
300 |     it('should validate both resource and operation together', () => {
301 |       const config = {
302 |         resource: 'files', // Invalid: should be singular
303 |         operation: 'listFiles' // Invalid: should be 'list' or 'search'
304 |       };
305 | 
306 |       const node = repository.getNode('nodes-base.googleDrive');
307 |       const result = EnhancedConfigValidator.validateWithMode(
308 |         'nodes-base.googleDrive',
309 |         config,
310 |         node.properties,
311 |         'operation',
312 |         'ai-friendly'
313 |       );
314 | 
315 |       expect(result.valid).toBe(false);
316 |       expect(result.errors.length).toBeGreaterThanOrEqual(2);
317 | 
318 |       // Should have error for resource
319 |       const resourceError = result.errors.find(e => e.property === 'resource');
320 |       expect(resourceError).toBeDefined();
321 |       expect(resourceError!.message).toContain('files');
322 | 
323 |       // Should have error for operation
324 |       const operationError = result.errors.find(e => e.property === 'operation');
325 |       expect(operationError).toBeDefined();
326 |       expect(operationError!.message).toContain('listFiles');
327 |     });
328 |   });
329 | 
330 |   describe('Slack Node Validation', () => {
331 |     it('should suggest "send" instead of "sendMessage"', () => {
332 |       const config = {
333 |         resource: 'message',
334 |         operation: 'sendMessage' // Common mistake
335 |       };
336 | 
337 |       const node = repository.getNode('nodes-base.slack');
338 |       const result = EnhancedConfigValidator.validateWithMode(
339 |         'nodes-base.slack',
340 |         config,
341 |         node.properties,
342 |         'operation',
343 |         'ai-friendly'
344 |       );
345 | 
346 |       expect(result.valid).toBe(false);
347 | 
348 |       const operationError = result.errors.find(e => e.property === 'operation');
349 |       expect(operationError).toBeDefined();
350 |       expect(operationError!.message).toContain('Did you mean "send"');
351 |     });
352 | 
353 |     it('should suggest singular "channel" instead of "channels"', () => {
354 |       const config = {
355 |         resource: 'channels', // Should be singular
356 |         operation: 'create'
357 |       };
358 | 
359 |       const node = repository.getNode('nodes-base.slack');
360 |       const result = EnhancedConfigValidator.validateWithMode(
361 |         'nodes-base.slack',
362 |         config,
363 |         node.properties,
364 |         'operation',
365 |         'ai-friendly'
366 |       );
367 | 
368 |       expect(result.valid).toBe(false);
369 | 
370 |       const resourceError = result.errors.find(e => e.property === 'resource');
371 |       expect(resourceError).toBeDefined();
372 |       expect(resourceError!.message).toContain('Did you mean "channel"');
373 |     });
374 |   });
375 | 
376 |   describe('Valid Configurations', () => {
377 |     it('should accept valid Google Drive configuration', () => {
378 |       const config = {
379 |         resource: 'file',
380 |         operation: 'download'
381 |       };
382 | 
383 |       const node = repository.getNode('nodes-base.googleDrive');
384 |       const result = EnhancedConfigValidator.validateWithMode(
385 |         'nodes-base.googleDrive',
386 |         config,
387 |         node.properties,
388 |         'operation',
389 |         'ai-friendly'
390 |       );
391 | 
392 |       // Should not have errors for resource or operation
393 |       const resourceError = result.errors.find(e => e.property === 'resource');
394 |       const operationError = result.errors.find(e => e.property === 'operation');
395 |       expect(resourceError).toBeUndefined();
396 |       expect(operationError).toBeUndefined();
397 |     });
398 | 
399 |     it('should accept valid Slack configuration', () => {
400 |       const config = {
401 |         resource: 'message',
402 |         operation: 'send'
403 |       };
404 | 
405 |       const node = repository.getNode('nodes-base.slack');
406 |       const result = EnhancedConfigValidator.validateWithMode(
407 |         'nodes-base.slack',
408 |         config,
409 |         node.properties,
410 |         'operation',
411 |         'ai-friendly'
412 |       );
413 | 
414 |       // Should not have errors for resource or operation
415 |       const resourceError = result.errors.find(e => e.property === 'resource');
416 |       const operationError = result.errors.find(e => e.property === 'operation');
417 |       expect(resourceError).toBeUndefined();
418 |       expect(operationError).toBeUndefined();
419 |     });
420 |   });
421 | });
```

--------------------------------------------------------------------------------
/tests/unit/services/node-sanitizer.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Node Sanitizer Tests
  3 |  * Tests for auto-adding required metadata to filter-based nodes
  4 |  */
  5 | 
  6 | import { describe, it, expect } from 'vitest';
  7 | import { sanitizeNode, validateNodeMetadata } from '../../../src/services/node-sanitizer';
  8 | import { WorkflowNode } from '../../../src/types/n8n-api';
  9 | 
 10 | describe('Node Sanitizer', () => {
 11 |   describe('sanitizeNode', () => {
 12 |     it('should add complete filter options to IF v2.2 node', () => {
 13 |       const node: WorkflowNode = {
 14 |         id: 'test-if',
 15 |         name: 'IF Node',
 16 |         type: 'n8n-nodes-base.if',
 17 |         typeVersion: 2.2,
 18 |         position: [0, 0],
 19 |         parameters: {
 20 |           conditions: {
 21 |             conditions: [
 22 |               {
 23 |                 id: 'condition1',
 24 |                 leftValue: '={{ $json.value }}',
 25 |                 rightValue: '',
 26 |                 operator: {
 27 |                   type: 'string',
 28 |                   operation: 'isNotEmpty'
 29 |                 }
 30 |               }
 31 |             ]
 32 |           }
 33 |         }
 34 |       };
 35 | 
 36 |       const sanitized = sanitizeNode(node);
 37 | 
 38 |       // Check that options were added
 39 |       expect(sanitized.parameters.conditions).toHaveProperty('options');
 40 |       const options = (sanitized.parameters.conditions as any).options;
 41 | 
 42 |       expect(options).toEqual({
 43 |         version: 2,
 44 |         leftValue: '',
 45 |         caseSensitive: true,
 46 |         typeValidation: 'strict'
 47 |       });
 48 |     });
 49 | 
 50 |     it('should preserve existing options while adding missing fields', () => {
 51 |       const node: WorkflowNode = {
 52 |         id: 'test-if-partial',
 53 |         name: 'IF Node Partial',
 54 |         type: 'n8n-nodes-base.if',
 55 |         typeVersion: 2.2,
 56 |         position: [0, 0],
 57 |         parameters: {
 58 |           conditions: {
 59 |             options: {
 60 |               caseSensitive: false // User-provided value
 61 |             },
 62 |             conditions: []
 63 |           }
 64 |         }
 65 |       };
 66 | 
 67 |       const sanitized = sanitizeNode(node);
 68 |       const options = (sanitized.parameters.conditions as any).options;
 69 | 
 70 |       // Should preserve user value
 71 |       expect(options.caseSensitive).toBe(false);
 72 | 
 73 |       // Should add missing fields
 74 |       expect(options.version).toBe(2);
 75 |       expect(options.leftValue).toBe('');
 76 |       expect(options.typeValidation).toBe('strict');
 77 |     });
 78 | 
 79 |     it('should fix invalid operator structure (type field misuse)', () => {
 80 |       const node: WorkflowNode = {
 81 |         id: 'test-if-bad-operator',
 82 |         name: 'IF Bad Operator',
 83 |         type: 'n8n-nodes-base.if',
 84 |         typeVersion: 2.2,
 85 |         position: [0, 0],
 86 |         parameters: {
 87 |           conditions: {
 88 |             conditions: [
 89 |               {
 90 |                 id: 'condition1',
 91 |                 leftValue: '={{ $json.value }}',
 92 |                 rightValue: '',
 93 |                 operator: {
 94 |                   type: 'isNotEmpty' // WRONG: type should be data type, not operation
 95 |                 }
 96 |               }
 97 |             ]
 98 |           }
 99 |         }
100 |       };
101 | 
102 |       const sanitized = sanitizeNode(node);
103 |       const condition = (sanitized.parameters.conditions as any).conditions[0];
104 | 
105 |       // Should fix operator structure
106 |       expect(condition.operator.type).toBe('boolean'); // Inferred data type (isEmpty/isNotEmpty are boolean ops)
107 |       expect(condition.operator.operation).toBe('isNotEmpty'); // Moved to operation field
108 |     });
109 | 
110 |     it('should add singleValue for unary operators', () => {
111 |       const node: WorkflowNode = {
112 |         id: 'test-if-unary',
113 |         name: 'IF Unary',
114 |         type: 'n8n-nodes-base.if',
115 |         typeVersion: 2.2,
116 |         position: [0, 0],
117 |         parameters: {
118 |           conditions: {
119 |             conditions: [
120 |               {
121 |                 id: 'condition1',
122 |                 leftValue: '={{ $json.value }}',
123 |                 rightValue: '',
124 |                 operator: {
125 |                   type: 'string',
126 |                   operation: 'isNotEmpty'
127 |                   // Missing singleValue
128 |                 }
129 |               }
130 |             ]
131 |           }
132 |         }
133 |       };
134 | 
135 |       const sanitized = sanitizeNode(node);
136 |       const condition = (sanitized.parameters.conditions as any).conditions[0];
137 | 
138 |       expect(condition.operator.singleValue).toBe(true);
139 |     });
140 | 
141 |     it('should sanitize Switch v3.2 node rules', () => {
142 |       const node: WorkflowNode = {
143 |         id: 'test-switch',
144 |         name: 'Switch Node',
145 |         type: 'n8n-nodes-base.switch',
146 |         typeVersion: 3.2,
147 |         position: [0, 0],
148 |         parameters: {
149 |           mode: 'rules',
150 |           rules: {
151 |             rules: [
152 |               {
153 |                 outputKey: 'audio',
154 |                 conditions: {
155 |                   conditions: [
156 |                     {
157 |                       id: 'cond1',
158 |                       leftValue: '={{ $json.fileType }}',
159 |                       rightValue: 'audio',
160 |                       operator: {
161 |                         type: 'string',
162 |                         operation: 'equals'
163 |                       }
164 |                     }
165 |                   ]
166 |                 }
167 |               }
168 |             ]
169 |           }
170 |         }
171 |       };
172 | 
173 |       const sanitized = sanitizeNode(node);
174 |       const rule = (sanitized.parameters.rules as any).rules[0];
175 | 
176 |       // Check that options were added to rule conditions
177 |       expect(rule.conditions).toHaveProperty('options');
178 |       expect(rule.conditions.options).toEqual({
179 |         version: 2,
180 |         leftValue: '',
181 |         caseSensitive: true,
182 |         typeValidation: 'strict'
183 |       });
184 |     });
185 | 
186 |     it('should not modify non-filter nodes', () => {
187 |       const node: WorkflowNode = {
188 |         id: 'test-http',
189 |         name: 'HTTP Request',
190 |         type: 'n8n-nodes-base.httpRequest',
191 |         typeVersion: 4.2,
192 |         position: [0, 0],
193 |         parameters: {
194 |           method: 'GET',
195 |           url: 'https://example.com'
196 |         }
197 |       };
198 | 
199 |       const sanitized = sanitizeNode(node);
200 | 
201 |       // Should return unchanged
202 |       expect(sanitized).toEqual(node);
203 |     });
204 | 
205 |     it('should not modify old IF versions', () => {
206 |       const node: WorkflowNode = {
207 |         id: 'test-if-old',
208 |         name: 'Old IF',
209 |         type: 'n8n-nodes-base.if',
210 |         typeVersion: 2.0, // Pre-filter version
211 |         position: [0, 0],
212 |         parameters: {
213 |           conditions: []
214 |         }
215 |       };
216 | 
217 |       const sanitized = sanitizeNode(node);
218 | 
219 |       // Should return unchanged
220 |       expect(sanitized).toEqual(node);
221 |     });
222 | 
223 |     it('should remove singleValue from binary operators like "equals"', () => {
224 |       const node: WorkflowNode = {
225 |         id: 'test-if-binary',
226 |         name: 'IF Binary Operator',
227 |         type: 'n8n-nodes-base.if',
228 |         typeVersion: 2.2,
229 |         position: [0, 0],
230 |         parameters: {
231 |           conditions: {
232 |             conditions: [
233 |               {
234 |                 id: 'condition1',
235 |                 leftValue: '={{ $json.value }}',
236 |                 rightValue: 'test',
237 |                 operator: {
238 |                   type: 'string',
239 |                   operation: 'equals',
240 |                   singleValue: true // WRONG: equals is binary, not unary
241 |                 }
242 |               }
243 |             ]
244 |           }
245 |         }
246 |       };
247 | 
248 |       const sanitized = sanitizeNode(node);
249 |       const condition = (sanitized.parameters.conditions as any).conditions[0];
250 | 
251 |       // Should remove singleValue from binary operator
252 |       expect(condition.operator.singleValue).toBeUndefined();
253 |       expect(condition.operator.type).toBe('string');
254 |       expect(condition.operator.operation).toBe('equals');
255 |     });
256 |   });
257 | 
258 |   describe('validateNodeMetadata', () => {
259 |     it('should detect missing conditions.options', () => {
260 |       const node: WorkflowNode = {
261 |         id: 'test',
262 |         name: 'IF Missing Options',
263 |         type: 'n8n-nodes-base.if',
264 |         typeVersion: 2.2,
265 |         position: [0, 0],
266 |         parameters: {
267 |           conditions: {
268 |             conditions: []
269 |             // Missing options
270 |           }
271 |         }
272 |       };
273 | 
274 |       const issues = validateNodeMetadata(node);
275 | 
276 |       expect(issues.length).toBeGreaterThan(0);
277 |       expect(issues[0]).toBe('Missing conditions.options');
278 |     });
279 | 
280 |     it('should detect missing operator.type', () => {
281 |       const node: WorkflowNode = {
282 |         id: 'test',
283 |         name: 'IF Bad Operator',
284 |         type: 'n8n-nodes-base.if',
285 |         typeVersion: 2.2,
286 |         position: [0, 0],
287 |         parameters: {
288 |           conditions: {
289 |             options: {
290 |               version: 2,
291 |               leftValue: '',
292 |               caseSensitive: true,
293 |               typeValidation: 'strict'
294 |             },
295 |             conditions: [
296 |               {
297 |                 id: 'cond1',
298 |                 leftValue: '={{ $json.value }}',
299 |                 rightValue: '',
300 |                 operator: {
301 |                   operation: 'equals'
302 |                   // Missing type
303 |                 }
304 |               }
305 |             ]
306 |           }
307 |         }
308 |       };
309 | 
310 |       const issues = validateNodeMetadata(node);
311 | 
312 |       expect(issues.length).toBeGreaterThan(0);
313 |       expect(issues.some(issue => issue.includes("missing required field 'type'"))).toBe(true);
314 |     });
315 | 
316 |     it('should detect invalid operator.type value', () => {
317 |       const node: WorkflowNode = {
318 |         id: 'test',
319 |         name: 'IF Invalid Type',
320 |         type: 'n8n-nodes-base.if',
321 |         typeVersion: 2.2,
322 |         position: [0, 0],
323 |         parameters: {
324 |           conditions: {
325 |             options: {
326 |               version: 2,
327 |               leftValue: '',
328 |               caseSensitive: true,
329 |               typeValidation: 'strict'
330 |             },
331 |             conditions: [
332 |               {
333 |                 id: 'cond1',
334 |                 leftValue: '={{ $json.value }}',
335 |                 rightValue: '',
336 |                 operator: {
337 |                   type: 'isNotEmpty', // WRONG: operation name, not data type
338 |                   operation: 'isNotEmpty'
339 |                 }
340 |               }
341 |             ]
342 |           }
343 |         }
344 |       };
345 | 
346 |       const issues = validateNodeMetadata(node);
347 | 
348 |       expect(issues.some(issue => issue.includes('invalid type "isNotEmpty"'))).toBe(true);
349 |     });
350 | 
351 |     it('should detect missing singleValue for unary operators', () => {
352 |       const node: WorkflowNode = {
353 |         id: 'test',
354 |         name: 'IF Missing SingleValue',
355 |         type: 'n8n-nodes-base.if',
356 |         typeVersion: 2.2,
357 |         position: [0, 0],
358 |         parameters: {
359 |           conditions: {
360 |             options: {
361 |               version: 2,
362 |               leftValue: '',
363 |               caseSensitive: true,
364 |               typeValidation: 'strict'
365 |             },
366 |             conditions: [
367 |               {
368 |                 id: 'cond1',
369 |                 leftValue: '={{ $json.value }}',
370 |                 rightValue: '',
371 |                 operator: {
372 |                   type: 'string',
373 |                   operation: 'isNotEmpty'
374 |                   // Missing singleValue: true
375 |                 }
376 |               }
377 |             ]
378 |           }
379 |         }
380 |       };
381 | 
382 |       const issues = validateNodeMetadata(node);
383 | 
384 |       expect(issues.length).toBeGreaterThan(0);
385 |       expect(issues.some(issue => issue.includes('requires singleValue: true'))).toBe(true);
386 |     });
387 | 
388 |     it('should detect singleValue on binary operators', () => {
389 |       const node: WorkflowNode = {
390 |         id: 'test',
391 |         name: 'IF Binary with SingleValue',
392 |         type: 'n8n-nodes-base.if',
393 |         typeVersion: 2.2,
394 |         position: [0, 0],
395 |         parameters: {
396 |           conditions: {
397 |             options: {
398 |               version: 2,
399 |               leftValue: '',
400 |               caseSensitive: true,
401 |               typeValidation: 'strict'
402 |             },
403 |             conditions: [
404 |               {
405 |                 id: 'cond1',
406 |                 leftValue: '={{ $json.value }}',
407 |                 rightValue: 'test',
408 |                 operator: {
409 |                   type: 'string',
410 |                   operation: 'equals',
411 |                   singleValue: true  // WRONG: equals is binary
412 |                 }
413 |               }
414 |             ]
415 |           }
416 |         }
417 |       };
418 | 
419 |       const issues = validateNodeMetadata(node);
420 | 
421 |       expect(issues.length).toBeGreaterThan(0);
422 |       expect(issues.some(issue => issue.includes('should not have singleValue: true'))).toBe(true);
423 |     });
424 | 
425 |     it('should return empty array for valid node', () => {
426 |       const node: WorkflowNode = {
427 |         id: 'test',
428 |         name: 'Valid IF',
429 |         type: 'n8n-nodes-base.if',
430 |         typeVersion: 2.2,
431 |         position: [0, 0],
432 |         parameters: {
433 |           conditions: {
434 |             options: {
435 |               version: 2,
436 |               leftValue: '',
437 |               caseSensitive: true,
438 |               typeValidation: 'strict'
439 |             },
440 |             conditions: [
441 |               {
442 |                 id: 'cond1',
443 |                 leftValue: '={{ $json.value }}',
444 |                 rightValue: '',
445 |                 operator: {
446 |                   type: 'string',
447 |                   operation: 'isNotEmpty',
448 |                   singleValue: true
449 |                 }
450 |               }
451 |             ]
452 |           }
453 |         }
454 |       };
455 | 
456 |       const issues = validateNodeMetadata(node);
457 | 
458 |       expect(issues).toEqual([]);
459 |     });
460 |   });
461 | });
462 | 
```

--------------------------------------------------------------------------------
/P0-R3-TEST-PLAN.md:
--------------------------------------------------------------------------------

```markdown
  1 | # P0-R3 Feature Test Coverage Plan
  2 | 
  3 | ## Executive Summary
  4 | 
  5 | This document outlines comprehensive test coverage for the P0-R3 feature (Template-based Configuration Examples). The feature adds real-world configuration examples from popular templates to node search and essentials tools.
  6 | 
  7 | **Feature Overview:**
  8 | - New database table: `template_node_configs` (197 pre-extracted configurations)
  9 | - Enhanced tools: `search_nodes({includeExamples: true})` and `get_node_essentials({includeExamples: true})`
 10 | - Breaking changes: Removed `get_node_for_task` tool
 11 | 
 12 | ## Test Files Created
 13 | 
 14 | ### Unit Tests
 15 | 
 16 | #### 1. `/tests/unit/scripts/fetch-templates-extraction.test.ts` ✅
 17 | **Purpose:** Test template extraction logic from `fetch-templates.ts`
 18 | 
 19 | **Coverage:**
 20 | - `extractNodeConfigs()` - 90%+ coverage
 21 |   - Valid workflows with multiple nodes
 22 |   - Empty workflows
 23 |   - Malformed compressed data
 24 |   - Invalid JSON
 25 |   - Nodes without parameters
 26 |   - Sticky note filtering
 27 |   - Credential handling
 28 |   - Expression detection
 29 |   - Special characters
 30 |   - Large workflows (100 nodes)
 31 | 
 32 | - `detectExpressions()` - 100% coverage
 33 |   - `={{...}}` syntax detection
 34 |   - `$json` references
 35 |   - `$node` references
 36 |   - Nested objects
 37 |   - Arrays
 38 |   - Null/undefined handling
 39 |   - Multiple expression types
 40 | 
 41 | **Test Count:** 27 tests
 42 | **Expected Coverage:** 92%+
 43 | 
 44 | ---
 45 | 
 46 | #### 2. `/tests/unit/mcp/search-nodes-examples.test.ts` ✅
 47 | **Purpose:** Test `search_nodes` tool with includeExamples parameter
 48 | 
 49 | **Coverage:**
 50 | - includeExamples parameter behavior
 51 |   - false: no examples returned
 52 |   - undefined: no examples returned (default)
 53 |   - true: examples returned
 54 | - Example data structure validation
 55 | - Top 2 limit enforcement
 56 | - Backward compatibility
 57 | - Performance (<100ms)
 58 | - Error handling (malformed JSON, database errors)
 59 | - searchNodesLIKE integration
 60 | - searchNodesFTS integration
 61 | 
 62 | **Test Count:** 12 tests
 63 | **Expected Coverage:** 85%+
 64 | 
 65 | ---
 66 | 
 67 | #### 3. `/tests/unit/mcp/get-node-essentials-examples.test.ts` ✅
 68 | **Purpose:** Test `get_node_essentials` tool with includeExamples parameter
 69 | 
 70 | **Coverage:**
 71 | - includeExamples parameter behavior
 72 | - Full metadata structure
 73 |   - configuration object
 74 |   - source (template, views, complexity)
 75 |   - useCases (limited to 2)
 76 |   - metadata (hasCredentials, hasExpressions)
 77 | - Cache key differentiation
 78 | - Backward compatibility
 79 | - Performance (<100ms)
 80 | - Error handling
 81 | - Top 3 limit enforcement
 82 | 
 83 | **Test Count:** 13 tests
 84 | **Expected Coverage:** 88%+
 85 | 
 86 | ---
 87 | 
 88 | ### Integration Tests
 89 | 
 90 | #### 4. `/tests/integration/database/template-node-configs.test.ts` ✅
 91 | **Purpose:** Test database schema, migrations, and operations
 92 | 
 93 | **Coverage:**
 94 | - Schema validation
 95 |   - Table creation
 96 |   - All columns present
 97 |   - Correct types and constraints
 98 |   - CHECK constraint on complexity
 99 | - Indexes
100 |   - idx_config_node_type_rank
101 |   - idx_config_complexity
102 |   - idx_config_auth
103 | - View: ranked_node_configs
104 |   - Top 5 per node_type
105 |   - Correct ordering
106 | - Foreign key constraints
107 |   - CASCADE delete
108 |   - Referential integrity
109 | - Data operations
110 |   - INSERT with all fields
111 |   - Nullable fields
112 |   - Rank updates
113 |   - Delete rank > 10
114 | - Performance
115 |   - 1000 records < 10ms queries
116 | - Migration idempotency
117 | 
118 | **Test Count:** 19 tests
119 | **Expected Coverage:** 95%+
120 | 
121 | ---
122 | 
123 | #### 5. `/tests/integration/mcp/template-examples-e2e.test.ts` ✅
124 | **Purpose:** End-to-end integration testing
125 | 
126 | **Coverage:**
127 | - Direct SQL queries
128 |   - Top 2 examples for search_nodes
129 |   - Top 3 examples with metadata for get_node_essentials
130 | - Data structure validation
131 |   - Valid JSON in all fields
132 |   - Credentials when has_credentials=1
133 | - Ranked view functionality
134 | - Performance with 100+ configs
135 |   - Query performance < 5ms
136 |   - Complexity filtering
137 | - Edge cases
138 |   - Non-existent node types
139 |   - Long parameters_json (100 params)
140 |   - Special characters (Unicode, emojis, symbols)
141 | - Data integrity
142 |   - Foreign key constraints
143 |   - Cascade deletes
144 | 
145 | **Test Count:** 14 tests
146 | **Expected Coverage:** 90%+
147 | 
148 | ---
149 | 
150 | ### Test Fixtures
151 | 
152 | #### 6. `/tests/fixtures/template-configs.ts` ✅
153 | **Purpose:** Reusable test data
154 | 
155 | **Provides:**
156 | - `sampleConfigs`: 7 realistic node configurations
157 |   - simpleWebhook
158 |   - webhookWithAuth
159 |   - httpRequestBasic
160 |   - httpRequestWithExpressions
161 |   - slackMessage
162 |   - codeNodeTransform
163 |   - codeNodeWithExpressions
164 | 
165 | - `sampleWorkflows`: 3 complete workflows
166 |   - webhookToSlack
167 |   - apiWorkflow
168 |   - complexWorkflow
169 | 
170 | - **Helper Functions:**
171 |   - `compressWorkflow()` - Compress to base64
172 |   - `createTemplateMetadata()` - Generate metadata
173 |   - `createConfigBatch()` - Batch create configs
174 |   - `getConfigByComplexity()` - Filter by complexity
175 |   - `getConfigsWithExpressions()` - Filter with expressions
176 |   - `getConfigsWithCredentials()` - Filter with credentials
177 |   - `createInsertStatement()` - SQL insert helper
178 | 
179 | ---
180 | 
181 | ## Existing Tests Requiring Updates
182 | 
183 | ### High Priority
184 | 
185 | #### 1. `tests/unit/mcp/parameter-validation.test.ts`
186 | **Line 480:** Remove `get_node_for_task` from legacyValidationTools array
187 | 
188 | ```typescript
189 | // REMOVE THIS:
190 | { name: 'get_node_for_task', args: {}, expected: 'Missing required parameters for get_node_for_task: task' },
191 | ```
192 | 
193 | **Status:** ⚠️ BREAKING CHANGE - Tool removed
194 | 
195 | ---
196 | 
197 | #### 2. `tests/unit/mcp/tools.test.ts`
198 | **Update:** Remove `get_node_for_task` from templates category
199 | 
200 | ```typescript
201 | // BEFORE:
202 | templates: ['list_tasks', 'get_node_for_task', 'search_templates', ...]
203 | 
204 | // AFTER:
205 | templates: ['list_tasks', 'search_templates', ...]
206 | ```
207 | 
208 | **Add:** Tests for new includeExamples parameter in tool definitions
209 | 
210 | ```typescript
211 | it('should have includeExamples parameter in search_nodes', () => {
212 |   const searchNodesTool = tools.find(t => t.name === 'search_nodes');
213 |   expect(searchNodesTool.inputSchema.properties.includeExamples).toBeDefined();
214 |   expect(searchNodesTool.inputSchema.properties.includeExamples.type).toBe('boolean');
215 |   expect(searchNodesTool.inputSchema.properties.includeExamples.default).toBe(false);
216 | });
217 | 
218 | it('should have includeExamples parameter in get_node_essentials', () => {
219 |   const essentialsTool = tools.find(t => t.name === 'get_node_essentials');
220 |   expect(essentialsTool.inputSchema.properties.includeExamples).toBeDefined();
221 | });
222 | ```
223 | 
224 | **Status:** ⚠️ REQUIRED UPDATE
225 | 
226 | ---
227 | 
228 | #### 3. `tests/integration/mcp-protocol/session-management.test.ts`
229 | **Remove:** Test case calling `get_node_for_task` with invalid task
230 | 
231 | ```typescript
232 | // REMOVE THIS TEST:
233 | client.callTool({ name: 'get_node_for_task', arguments: { task: 'invalid_task' } }).catch(e => e)
234 | ```
235 | 
236 | **Status:** ⚠️ BREAKING CHANGE
237 | 
238 | ---
239 | 
240 | #### 4. `tests/integration/mcp-protocol/tool-invocation.test.ts`
241 | **Remove:** Entire `get_node_for_task` describe block
242 | 
243 | **Add:** Tests for new includeExamples functionality
244 | 
245 | ```typescript
246 | describe('search_nodes with includeExamples', () => {
247 |   it('should return examples when includeExamples is true', async () => {
248 |     const response = await client.callTool({
249 |       name: 'search_nodes',
250 |       arguments: { query: 'webhook', includeExamples: true }
251 |     });
252 | 
253 |     expect(response.results).toBeDefined();
254 |     // Examples may or may not be present depending on database
255 |   });
256 | 
257 |   it('should not return examples when includeExamples is false', async () => {
258 |     const response = await client.callTool({
259 |       name: 'search_nodes',
260 |       arguments: { query: 'webhook', includeExamples: false }
261 |     });
262 | 
263 |     expect(response.results).toBeDefined();
264 |     response.results.forEach(node => {
265 |       expect(node.examples).toBeUndefined();
266 |     });
267 |   });
268 | });
269 | 
270 | describe('get_node_essentials with includeExamples', () => {
271 |   it('should return examples with metadata when includeExamples is true', async () => {
272 |     const response = await client.callTool({
273 |       name: 'get_node_essentials',
274 |       arguments: { nodeType: 'nodes-base.webhook', includeExamples: true }
275 |     });
276 | 
277 |     expect(response.nodeType).toBeDefined();
278 |     // Examples may or may not be present depending on database
279 |   });
280 | });
281 | ```
282 | 
283 | **Status:** ⚠️ REQUIRED UPDATE
284 | 
285 | ---
286 | 
287 | ### Medium Priority
288 | 
289 | #### 5. `tests/unit/services/task-templates.test.ts`
290 | **Status:** ✅ No changes needed (TaskTemplates marked as deprecated but not removed)
291 | 
292 | **Note:** TaskTemplates remains for backward compatibility. Tests should continue to pass.
293 | 
294 | ---
295 | 
296 | ## Test Execution Plan
297 | 
298 | ### Phase 1: Unit Tests
299 | ```bash
300 | # Run new unit tests
301 | npm test tests/unit/scripts/fetch-templates-extraction.test.ts
302 | npm test tests/unit/mcp/search-nodes-examples.test.ts
303 | npm test tests/unit/mcp/get-node-essentials-examples.test.ts
304 | 
305 | # Expected: All pass, 52 tests
306 | ```
307 | 
308 | ### Phase 2: Integration Tests
309 | ```bash
310 | # Run new integration tests
311 | npm test tests/integration/database/template-node-configs.test.ts
312 | npm test tests/integration/mcp/template-examples-e2e.test.ts
313 | 
314 | # Expected: All pass, 33 tests
315 | ```
316 | 
317 | ### Phase 3: Update Existing Tests
318 | ```bash
319 | # Update files as outlined above, then run:
320 | npm test tests/unit/mcp/parameter-validation.test.ts
321 | npm test tests/unit/mcp/tools.test.ts
322 | npm test tests/integration/mcp-protocol/session-management.test.ts
323 | npm test tests/integration/mcp-protocol/tool-invocation.test.ts
324 | 
325 | # Expected: All pass after updates
326 | ```
327 | 
328 | ### Phase 4: Full Test Suite
329 | ```bash
330 | # Run all tests
331 | npm test
332 | 
333 | # Run with coverage
334 | npm run test:coverage
335 | 
336 | # Expected coverage improvements:
337 | # - src/scripts/fetch-templates.ts: +20% (60% → 80%)
338 | # - src/mcp/server.ts: +5% (75% → 80%)
339 | # - Overall project: +2% (current → current+2%)
340 | ```
341 | 
342 | ---
343 | 
344 | ## Coverage Expectations
345 | 
346 | ### New Code Coverage
347 | 
348 | | File | Function | Target | Tests |
349 | |------|----------|--------|-------|
350 | | fetch-templates.ts | extractNodeConfigs | 95% | 15 tests |
351 | | fetch-templates.ts | detectExpressions | 100% | 12 tests |
352 | | server.ts | searchNodes (with examples) | 90% | 8 tests |
353 | | server.ts | getNodeEssentials (with examples) | 90% | 10 tests |
354 | | Database migration | template_node_configs | 100% | 19 tests |
355 | 
356 | ### Overall Coverage Goals
357 | 
358 | - **Unit Tests:** 90%+ coverage for new code
359 | - **Integration Tests:** All happy paths + critical error paths
360 | - **E2E Tests:** Complete feature workflows
361 | - **Performance:** All queries <10ms (database), <100ms (MCP)
362 | 
363 | ---
364 | 
365 | ## Test Infrastructure
366 | 
367 | ### Dependencies Required
368 | All dependencies already present in `package.json`:
369 | - vitest (test runner)
370 | - better-sqlite3 (database)
371 | - @vitest/coverage-v8 (coverage)
372 | 
373 | ### Test Utilities Used
374 | - TestDatabase helper (from existing test utils)
375 | - createTestDatabaseAdapter (from existing test utils)
376 | - Standard vitest matchers
377 | 
378 | ### No New Dependencies Required ✅
379 | 
380 | ---
381 | 
382 | ## Regression Prevention
383 | 
384 | ### Critical Paths Protected
385 | 
386 | 1. **Backward Compatibility**
387 |    - Tools work without includeExamples parameter
388 |    - Existing workflows unchanged
389 |    - Cache keys differentiated
390 | 
391 | 2. **Performance**
392 |    - No degradation when includeExamples=false
393 |    - Indexed queries <10ms
394 |    - Example fetch errors don't break responses
395 | 
396 | 3. **Data Integrity**
397 |    - Foreign key constraints enforced
398 |    - JSON validation in all fields
399 |    - Rank calculations correct
400 | 
401 | ---
402 | 
403 | ## CI/CD Integration
404 | 
405 | ### GitHub Actions Updates
406 | No changes required. Existing test commands will run new tests:
407 | 
408 | ```yaml
409 | - run: npm test
410 | - run: npm run test:coverage
411 | ```
412 | 
413 | ### Coverage Thresholds
414 | Current thresholds maintained. Expected improvements:
415 | - Lines: +2%
416 | - Functions: +3%
417 | - Branches: +2%
418 | 
419 | ---
420 | 
421 | ## Manual Testing Checklist
422 | 
423 | ### Pre-Deployment Verification
424 | 
425 | - [ ] Run `npm run rebuild` - Verify migration applies cleanly
426 | - [ ] Run `npm run fetch:templates --extract-only` - Verify extraction works
427 | - [ ] Check database: `SELECT COUNT(*) FROM template_node_configs` - Should be ~197
428 | - [ ] Test MCP tool: `search_nodes({query: "webhook", includeExamples: true})`
429 | - [ ] Test MCP tool: `get_node_essentials({nodeType: "nodes-base.webhook", includeExamples: true})`
430 | - [ ] Verify backward compatibility: Tools work without includeExamples parameter
431 | - [ ] Performance test: Query 100 nodes with examples < 200ms
432 | 
433 | ---
434 | 
435 | ## Rollback Plan
436 | 
437 | If issues are detected:
438 | 
439 | 1. **Database Rollback:**
440 |    ```sql
441 |    DROP TABLE IF EXISTS template_node_configs;
442 |    DROP VIEW IF EXISTS ranked_node_configs;
443 |    ```
444 | 
445 | 2. **Code Rollback:**
446 |    - Revert server.ts changes
447 |    - Revert tools.ts changes
448 |    - Restore get_node_for_task tool (if critical)
449 | 
450 | 3. **Test Rollback:**
451 |    - Revert parameter-validation.test.ts
452 |    - Revert tools.test.ts
453 |    - Revert tool-invocation.test.ts
454 | 
455 | ---
456 | 
457 | ## Success Metrics
458 | 
459 | ### Test Metrics
460 | - ✅ 85+ new tests added
461 | - ✅ 0 tests failing after updates
462 | - ✅ Coverage increase 2%+
463 | - ✅ All performance tests pass
464 | 
465 | ### Feature Metrics
466 | - ✅ 197 template configs extracted
467 | - ✅ Top 2/3 examples returned correctly
468 | - ✅ Query performance <10ms
469 | - ✅ No backward compatibility breaks
470 | 
471 | ---
472 | 
473 | ## Conclusion
474 | 
475 | This test plan provides **comprehensive coverage** for the P0-R3 feature with:
476 | - **85+ new tests** across unit, integration, and E2E levels
477 | - **Complete coverage** of extraction, storage, and retrieval
478 | - **Backward compatibility** protection
479 | - **Performance validation** (<10ms queries)
480 | - **Clear migration path** for existing tests
481 | 
482 | **All test files are ready for execution.** Update the 4 existing test files as outlined, then run the full test suite.
483 | 
484 | **Estimated Total Implementation Time:** 2-3 hours for updating existing tests + validation
485 | 
```

--------------------------------------------------------------------------------
/tests/unit/services/expression-validator-edge-cases.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, it, expect, vi, beforeEach } from 'vitest';
  2 | import { ExpressionValidator } from '@/services/expression-validator';
  3 | 
  4 | // Mock the database
  5 | vi.mock('better-sqlite3');
  6 | 
  7 | describe('ExpressionValidator - Edge Cases', () => {
  8 |   beforeEach(() => {
  9 |     vi.clearAllMocks();
 10 |   });
 11 | 
 12 |   describe('Null and Undefined Handling', () => {
 13 |     it('should handle null expression gracefully', () => {
 14 |       const context = { availableNodes: ['Node1'] };
 15 |       const result = ExpressionValidator.validateExpression(null as any, context);
 16 |       expect(result.valid).toBe(true);
 17 |       expect(result.errors).toEqual([]);
 18 |     });
 19 | 
 20 |     it('should handle undefined expression gracefully', () => {
 21 |       const context = { availableNodes: ['Node1'] };
 22 |       const result = ExpressionValidator.validateExpression(undefined as any, context);
 23 |       expect(result.valid).toBe(true);
 24 |       expect(result.errors).toEqual([]);
 25 |     });
 26 | 
 27 |     it('should handle null context gracefully', () => {
 28 |       const result = ExpressionValidator.validateExpression('{{ $json.data }}', null as any);
 29 |       expect(result).toBeDefined();
 30 |       // With null context, it will likely have errors about missing context
 31 |       expect(result.valid).toBe(false);
 32 |     });
 33 | 
 34 |     it('should handle undefined context gracefully', () => {
 35 |       const result = ExpressionValidator.validateExpression('{{ $json.data }}', undefined as any);
 36 |       expect(result).toBeDefined();
 37 |       // With undefined context, it will likely have errors about missing context
 38 |       expect(result.valid).toBe(false);
 39 |     });
 40 |   });
 41 | 
 42 |   describe('Boundary Value Testing', () => {
 43 |     it('should handle empty string expression', () => {
 44 |       const context = { availableNodes: [] };
 45 |       const result = ExpressionValidator.validateExpression('', context);
 46 |       expect(result.valid).toBe(true);
 47 |       expect(result.errors).toEqual([]);
 48 |       expect(result.usedVariables.size).toBe(0);
 49 |     });
 50 | 
 51 |     it('should handle extremely long expressions', () => {
 52 |       const longExpression = '{{ ' + '$json.field'.repeat(1000) + ' }}';
 53 |       const context = { availableNodes: ['Node1'] };
 54 |       
 55 |       const start = Date.now();
 56 |       const result = ExpressionValidator.validateExpression(longExpression, context);
 57 |       const duration = Date.now() - start;
 58 |       
 59 |       expect(result).toBeDefined();
 60 |       expect(duration).toBeLessThan(1000); // Should process within 1 second
 61 |     });
 62 | 
 63 |     it('should handle deeply nested property access', () => {
 64 |       const deepExpression = '{{ $json' + '.property'.repeat(50) + ' }}';
 65 |       const context = { availableNodes: ['Node1'] };
 66 |       
 67 |       const result = ExpressionValidator.validateExpression(deepExpression, context);
 68 |       expect(result.valid).toBe(true);
 69 |       expect(result.usedVariables.has('$json')).toBe(true);
 70 |     });
 71 | 
 72 |     it('should handle many different variables in one expression', () => {
 73 |       const complexExpression = `{{
 74 |         $json.data + 
 75 |         $node["Node1"].json.value +
 76 |         $input.item.field +
 77 |         $items("Node2", 0)[0].data +
 78 |         $parameter["apiKey"] +
 79 |         $env.API_URL +
 80 |         $workflow.name +
 81 |         $execution.id +
 82 |         $itemIndex +
 83 |         $now
 84 |       }}`;
 85 |       
 86 |       const context = { 
 87 |         availableNodes: ['Node1', 'Node2'],
 88 |         hasInputData: true
 89 |       };
 90 |       
 91 |       const result = ExpressionValidator.validateExpression(complexExpression, context);
 92 |       expect(result.usedVariables.size).toBeGreaterThan(5);
 93 |       expect(result.usedNodes.has('Node1')).toBe(true);
 94 |       expect(result.usedNodes.has('Node2')).toBe(true);
 95 |     });
 96 |   });
 97 | 
 98 |   describe('Invalid Syntax Handling', () => {
 99 |     it('should detect unclosed expressions', () => {
100 |       const expressions = [
101 |         '{{ $json.field',
102 |         '$json.field }}',
103 |         '{{ $json.field }',
104 |         '{ $json.field }}'
105 |       ];
106 |       
107 |       const context = { availableNodes: [] };
108 |       
109 |       expressions.forEach(expr => {
110 |         const result = ExpressionValidator.validateExpression(expr, context);
111 |         expect(result.errors.some(e => e.includes('Unmatched'))).toBe(true);
112 |       });
113 |     });
114 | 
115 |     it('should detect nested expressions', () => {
116 |       const nestedExpression = '{{ $json.field + {{ $node["Node1"].json }} }}';
117 |       const context = { availableNodes: ['Node1'] };
118 |       
119 |       const result = ExpressionValidator.validateExpression(nestedExpression, context);
120 |       expect(result.errors.some(e => e.includes('Nested expressions'))).toBe(true);
121 |     });
122 | 
123 |     it('should detect empty expressions', () => {
124 |       const emptyExpression = 'Value: {{}}';
125 |       const context = { availableNodes: [] };
126 |       
127 |       const result = ExpressionValidator.validateExpression(emptyExpression, context);
128 |       expect(result.errors.some(e => e.includes('Empty expression'))).toBe(true);
129 |     });
130 | 
131 |     it('should handle malformed node references', () => {
132 |       const expressions = [
133 |         '{{ $node[].json }}',
134 |         '{{ $node[""].json }}',
135 |         '{{ $node[Node1].json }}', // Missing quotes
136 |         '{{ $node["Node1" ].json }}' // Extra space - this might actually be valid
137 |       ];
138 |       
139 |       const context = { availableNodes: ['Node1'] };
140 |       
141 |       expressions.forEach(expr => {
142 |         const result = ExpressionValidator.validateExpression(expr, context);
143 |         // Some of these might generate warnings or errors
144 |         expect(result).toBeDefined();
145 |       });
146 |     });
147 |   });
148 | 
149 |   describe('Special Characters and Unicode', () => {
150 |     it('should handle special characters in node names', () => {
151 |       const specialNodes = ['Node-123', 'Node_Test', 'Node@Special', 'Node 中文', 'Node😊'];
152 |       const context = { availableNodes: specialNodes };
153 |       
154 |       specialNodes.forEach(nodeName => {
155 |         const expression = `{{ $node["${nodeName}"].json.value }}`;
156 |         const result = ExpressionValidator.validateExpression(expression, context);
157 |         expect(result.usedNodes.has(nodeName)).toBe(true);
158 |         expect(result.errors.filter(e => e.includes(nodeName))).toHaveLength(0);
159 |       });
160 |     });
161 | 
162 |     it('should handle Unicode in property names', () => {
163 |       const expression = '{{ $json.名前 + $json.שם + $json.имя }}';
164 |       const context = { availableNodes: [] };
165 |       
166 |       const result = ExpressionValidator.validateExpression(expression, context);
167 |       expect(result.usedVariables.has('$json')).toBe(true);
168 |     });
169 |   });
170 | 
171 |   describe('Context Validation', () => {
172 |     it('should warn about $input when no input data available', () => {
173 |       const expression = '{{ $input.item.data }}';
174 |       const context = { 
175 |         availableNodes: [],
176 |         hasInputData: false
177 |       };
178 |       
179 |       const result = ExpressionValidator.validateExpression(expression, context);
180 |       expect(result.warnings.some(w => w.includes('$input'))).toBe(true);
181 |     });
182 | 
183 |     it('should handle references to non-existent nodes', () => {
184 |       const expression = '{{ $node["NonExistentNode"].json.value }}';
185 |       const context = { availableNodes: ['Node1', 'Node2'] };
186 |       
187 |       const result = ExpressionValidator.validateExpression(expression, context);
188 |       expect(result.errors.some(e => e.includes('NonExistentNode'))).toBe(true);
189 |     });
190 | 
191 |     it('should validate $items function references', () => {
192 |       const expression = '{{ $items("NonExistentNode", 0)[0].json }}';
193 |       const context = { availableNodes: ['Node1', 'Node2'] };
194 |       
195 |       const result = ExpressionValidator.validateExpression(expression, context);
196 |       expect(result.errors.some(e => e.includes('NonExistentNode'))).toBe(true);
197 |     });
198 |   });
199 | 
200 |   describe('Complex Expression Patterns', () => {
201 |     it('should handle JavaScript operations in expressions', () => {
202 |       const expressions = [
203 |         '{{ $json.count > 10 ? "high" : "low" }}',
204 |         '{{ Math.round($json.price * 1.2) }}',
205 |         '{{ $json.items.filter(item => item.active).length }}',
206 |         '{{ new Date($json.timestamp).toISOString() }}',
207 |         '{{ $json.name.toLowerCase().replace(" ", "-") }}'
208 |       ];
209 |       
210 |       const context = { availableNodes: [] };
211 |       
212 |       expressions.forEach(expr => {
213 |         const result = ExpressionValidator.validateExpression(expr, context);
214 |         expect(result.usedVariables.has('$json')).toBe(true);
215 |       });
216 |     });
217 | 
218 |     it('should handle array access patterns', () => {
219 |       const expressions = [
220 |         '{{ $json[0] }}',
221 |         '{{ $json.items[5].name }}',
222 |         '{{ $node["Node1"].json[0].data[1] }}',
223 |         '{{ $json["items"][0]["name"] }}'
224 |       ];
225 |       
226 |       const context = { availableNodes: ['Node1'] };
227 |       
228 |       expressions.forEach(expr => {
229 |         const result = ExpressionValidator.validateExpression(expr, context);
230 |         expect(result.usedVariables.size).toBeGreaterThan(0);
231 |       });
232 |     });
233 |   });
234 | 
235 |   describe('validateNodeExpressions', () => {
236 |     it('should validate all expressions in node parameters', () => {
237 |       const parameters = {
238 |         field1: '{{ $json.data }}',
239 |         field2: 'static value',
240 |         nested: {
241 |           field3: '{{ $node["Node1"].json.value }}',
242 |           array: [
243 |             '{{ $json.item1 }}',
244 |             'not an expression',
245 |             '{{ $json.item2 }}'
246 |           ]
247 |         }
248 |       };
249 |       
250 |       const context = { availableNodes: ['Node1'] };
251 |       const result = ExpressionValidator.validateNodeExpressions(parameters, context);
252 |       
253 |       expect(result.usedVariables.has('$json')).toBe(true);
254 |       expect(result.usedNodes.has('Node1')).toBe(true);
255 |       expect(result.valid).toBe(true);
256 |     });
257 | 
258 |     it('should handle null/undefined in parameters', () => {
259 |       const parameters = {
260 |         field1: null,
261 |         field2: undefined,
262 |         field3: '',
263 |         field4: '{{ $json.data }}'
264 |       };
265 |       
266 |       const context = { availableNodes: [] };
267 |       const result = ExpressionValidator.validateNodeExpressions(parameters, context);
268 |       
269 |       expect(result.usedVariables.has('$json')).toBe(true);
270 |       expect(result.errors.length).toBe(0);
271 |     });
272 | 
273 |     it('should handle circular references in parameters', () => {
274 |       const parameters: any = {
275 |         field1: '{{ $json.data }}'
276 |       };
277 |       parameters.circular = parameters;
278 |       
279 |       const context = { availableNodes: [] };
280 |       // Should not throw
281 |       expect(() => {
282 |         ExpressionValidator.validateNodeExpressions(parameters, context);
283 |       }).not.toThrow();
284 |     });
285 | 
286 |     it('should aggregate errors from multiple expressions', () => {
287 |       const parameters = {
288 |         field1: '{{ $node["Missing1"].json }}',
289 |         field2: '{{ $node["Missing2"].json }}',
290 |         field3: '{{ }}', // Empty expression
291 |         field4: '{{ $json.valid }}'
292 |       };
293 |       
294 |       const context = { availableNodes: ['ValidNode'] };
295 |       const result = ExpressionValidator.validateNodeExpressions(parameters, context);
296 |       
297 |       expect(result.valid).toBe(false);
298 |       // Should have at least 3 errors: 2 missing nodes + 1 empty expression
299 |       expect(result.errors.length).toBeGreaterThanOrEqual(3);
300 |       expect(result.usedVariables.has('$json')).toBe(true);
301 |     });
302 |   });
303 | 
304 |   describe('Performance Edge Cases', () => {
305 |     it('should handle recursive parameter structures efficiently', () => {
306 |       const createNestedObject = (depth: number): any => {
307 |         if (depth === 0) return '{{ $json.value }}';
308 |         return {
309 |           level: depth,
310 |           expression: `{{ $json.level${depth} }}`,
311 |           nested: createNestedObject(depth - 1)
312 |         };
313 |       };
314 |       
315 |       const deepParameters = createNestedObject(100);
316 |       const context = { availableNodes: [] };
317 |       
318 |       const start = Date.now();
319 |       const result = ExpressionValidator.validateNodeExpressions(deepParameters, context);
320 |       const duration = Date.now() - start;
321 |       
322 |       expect(result).toBeDefined();
323 |       expect(duration).toBeLessThan(1000); // Should complete within 1 second
324 |     });
325 | 
326 |     it('should handle large arrays of expressions', () => {
327 |       const parameters = {
328 |         items: Array(1000).fill(null).map((_, i) => `{{ $json.item${i} }}`)
329 |       };
330 |       
331 |       const context = { availableNodes: [] };
332 |       const result = ExpressionValidator.validateNodeExpressions(parameters, context);
333 |       
334 |       expect(result.usedVariables.has('$json')).toBe(true);
335 |       expect(result.valid).toBe(true);
336 |     });
337 |   });
338 | 
339 |   describe('Error Message Quality', () => {
340 |     it('should provide helpful error messages', () => {
341 |       const testCases = [
342 |         {
343 |           expression: '{{ $node["Node With Spaces"].json }}',
344 |           context: { availableNodes: ['NodeWithSpaces'] },
345 |           expectedError: 'Node With Spaces'
346 |         },
347 |         {
348 |           expression: '{{ $items("WrongNode", -1) }}',
349 |           context: { availableNodes: ['RightNode'] },
350 |           expectedError: 'WrongNode'
351 |         }
352 |       ];
353 |       
354 |       testCases.forEach(({ expression, context, expectedError }) => {
355 |         const result = ExpressionValidator.validateExpression(expression, context);
356 |         const hasRelevantError = result.errors.some(e => e.includes(expectedError));
357 |         expect(hasRelevantError).toBe(true);
358 |       });
359 |     });
360 |   });
361 | });
```

--------------------------------------------------------------------------------
/tests/unit/validation-fixes.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Test suite for validation system fixes
  3 |  * Covers issues #58, #68, #70, #73
  4 |  */
  5 | 
  6 | import { describe, test, expect, beforeAll, afterAll } from 'vitest';
  7 | import { WorkflowValidator } from '../../src/services/workflow-validator';
  8 | import { EnhancedConfigValidator } from '../../src/services/enhanced-config-validator';
  9 | import { ToolValidation, Validator, ValidationError } from '../../src/utils/validation-schemas';
 10 | 
 11 | describe('Validation System Fixes', () => {
 12 |   let workflowValidator: WorkflowValidator;
 13 |   let mockNodeRepository: any;
 14 | 
 15 |   beforeAll(async () => {
 16 |     // Initialize test environment
 17 |     process.env.NODE_ENV = 'test';
 18 |     
 19 |     // Mock repository for testing
 20 |     mockNodeRepository = {
 21 |       getNode: (nodeType: string) => {
 22 |         if (nodeType === 'nodes-base.webhook' || nodeType === 'n8n-nodes-base.webhook') {
 23 |           return {
 24 |             nodeType: 'nodes-base.webhook',
 25 |             displayName: 'Webhook',
 26 |             properties: [
 27 |               { name: 'path', required: true, displayName: 'Path' },
 28 |               { name: 'httpMethod', required: true, displayName: 'HTTP Method' }
 29 |             ]
 30 |           };
 31 |         }
 32 |         if (nodeType === 'nodes-base.set' || nodeType === 'n8n-nodes-base.set') {
 33 |           return {
 34 |             nodeType: 'nodes-base.set',
 35 |             displayName: 'Set',
 36 |             properties: [
 37 |               { name: 'values', required: false, displayName: 'Values' }
 38 |             ]
 39 |           };
 40 |         }
 41 |         return null;
 42 |       }
 43 |     } as any;
 44 | 
 45 |     workflowValidator = new WorkflowValidator(mockNodeRepository, EnhancedConfigValidator);
 46 |   });
 47 | 
 48 |   afterAll(() => {
 49 |     // Reset NODE_ENV instead of deleting it
 50 |     delete (process.env as any).NODE_ENV;
 51 |   });
 52 | 
 53 |   describe('Issue #73: validate_node_minimal crashes without input validation', () => {
 54 |     test('should handle empty config in validation schemas', () => {
 55 |       // Test the validation schema handles empty config
 56 |       const result = ToolValidation.validateNodeMinimal({
 57 |         nodeType: 'nodes-base.webhook',
 58 |         config: undefined
 59 |       });
 60 |       
 61 |       expect(result).toBeDefined();
 62 |       expect(result.valid).toBe(false);
 63 |       expect(result.errors.length).toBeGreaterThan(0);
 64 |       expect(result.errors[0].field).toBe('config');
 65 |     });
 66 | 
 67 |     test('should handle null config in validation schemas', () => {
 68 |       const result = ToolValidation.validateNodeMinimal({
 69 |         nodeType: 'nodes-base.webhook',
 70 |         config: null
 71 |       });
 72 |       
 73 |       expect(result).toBeDefined();
 74 |       expect(result.valid).toBe(false);
 75 |       expect(result.errors.length).toBeGreaterThan(0);
 76 |       expect(result.errors[0].field).toBe('config');
 77 |     });
 78 | 
 79 |     test('should accept valid config object', () => {
 80 |       const result = ToolValidation.validateNodeMinimal({
 81 |         nodeType: 'nodes-base.webhook',
 82 |         config: { path: '/webhook', httpMethod: 'POST' }
 83 |       });
 84 |       
 85 |       expect(result).toBeDefined();
 86 |       expect(result.valid).toBe(true);
 87 |       expect(result.errors).toHaveLength(0);
 88 |     });
 89 |   });
 90 | 
 91 |   describe('Issue #58: validate_node_operation crashes on nested input', () => {
 92 |     test('should handle invalid nodeType gracefully', () => {
 93 |       expect(() => {
 94 |         EnhancedConfigValidator.validateWithMode(
 95 |           undefined as any,
 96 |           { resource: 'channel', operation: 'create' },
 97 |           [],
 98 |           'operation',
 99 |           'ai-friendly'
100 |         );
101 |       }).toThrow(Error);
102 |     });
103 | 
104 |     test('should handle null nodeType gracefully', () => {
105 |       expect(() => {
106 |         EnhancedConfigValidator.validateWithMode(
107 |           null as any,
108 |           { resource: 'channel', operation: 'create' },
109 |           [],
110 |           'operation',
111 |           'ai-friendly'
112 |         );
113 |       }).toThrow(Error);
114 |     });
115 | 
116 |     test('should handle non-string nodeType gracefully', () => {
117 |       expect(() => {
118 |         EnhancedConfigValidator.validateWithMode(
119 |           { type: 'nodes-base.slack' } as any,
120 |           { resource: 'channel', operation: 'create' },
121 |           [],
122 |           'operation',
123 |           'ai-friendly'
124 |         );
125 |       }).toThrow(Error);
126 |     });
127 | 
128 |     test('should handle valid nodeType properly', () => {
129 |       const result = EnhancedConfigValidator.validateWithMode(
130 |         'nodes-base.set',
131 |         { values: {} },
132 |         [],
133 |         'operation',
134 |         'ai-friendly'
135 |       );
136 |       
137 |       expect(result).toBeDefined();
138 |       expect(typeof result.valid).toBe('boolean');
139 |     });
140 |   });
141 | 
142 |   describe('Issue #70: Profile settings not respected', () => {
143 |     test('should pass profile parameter to all validation phases', async () => {
144 |       const workflow = {
145 |         nodes: [
146 |           {
147 |             id: '1',
148 |             name: 'Webhook',
149 |             type: 'n8n-nodes-base.webhook',
150 |             position: [100, 200] as [number, number],
151 |             parameters: { path: '/test', httpMethod: 'POST' },
152 |             typeVersion: 1
153 |           },
154 |           {
155 |             id: '2',
156 |             name: 'Set',
157 |             type: 'n8n-nodes-base.set',
158 |             position: [300, 200] as [number, number],
159 |             parameters: { values: {} },
160 |             typeVersion: 1
161 |           }
162 |         ],
163 |         connections: {
164 |           'Webhook': {
165 |             main: [[{ node: 'Set', type: 'main', index: 0 }]]
166 |           }
167 |         }
168 |       };
169 | 
170 |       const result = await workflowValidator.validateWorkflow(workflow, {
171 |         validateNodes: true,
172 |         validateConnections: true,
173 |         validateExpressions: true,
174 |         profile: 'minimal'
175 |       });
176 | 
177 |       expect(result).toBeDefined();
178 |       expect(result.valid).toBe(true);
179 |       // In minimal profile, should have fewer warnings/errors - just check it's reasonable
180 |       expect(result.warnings.length).toBeLessThanOrEqual(5);
181 |     });
182 | 
183 |     test('should filter out sticky notes from validation', async () => {
184 |       const workflow = {
185 |         nodes: [
186 |           {
187 |             id: '1',
188 |             name: 'Webhook',
189 |             type: 'n8n-nodes-base.webhook',
190 |             position: [100, 200] as [number, number],
191 |             parameters: { path: '/test', httpMethod: 'POST' },
192 |             typeVersion: 1
193 |           },
194 |           {
195 |             id: '2',
196 |             name: 'Sticky Note',
197 |             type: 'n8n-nodes-base.stickyNote',
198 |             position: [300, 100] as [number, number],
199 |             parameters: { content: 'This is a note' },
200 |             typeVersion: 1
201 |           }
202 |         ],
203 |         connections: {}
204 |       };
205 | 
206 |       const result = await workflowValidator.validateWorkflow(workflow);
207 | 
208 |       expect(result).toBeDefined();
209 |       expect(result.statistics.totalNodes).toBe(1); // Only webhook, sticky note excluded
210 |       expect(result.statistics.enabledNodes).toBe(1);
211 |     });
212 | 
213 |     test('should allow legitimate loops in cycle detection', async () => {
214 |       const workflow = {
215 |         nodes: [
216 |           {
217 |             id: '1',
218 |             name: 'Manual Trigger',
219 |             type: 'n8n-nodes-base.manualTrigger',
220 |             position: [100, 200] as [number, number],
221 |             parameters: {},
222 |             typeVersion: 1
223 |           },
224 |           {
225 |             id: '2',
226 |             name: 'SplitInBatches',
227 |             type: 'n8n-nodes-base.splitInBatches',
228 |             position: [300, 200] as [number, number],
229 |             parameters: { batchSize: 1 },
230 |             typeVersion: 1
231 |           },
232 |           {
233 |             id: '3',
234 |             name: 'Set',
235 |             type: 'n8n-nodes-base.set',
236 |             position: [500, 200] as [number, number],
237 |             parameters: { values: {} },
238 |             typeVersion: 1
239 |           }
240 |         ],
241 |         connections: {
242 |           'Manual Trigger': {
243 |             main: [[{ node: 'SplitInBatches', type: 'main', index: 0 }]]
244 |           },
245 |           'SplitInBatches': {
246 |             main: [
247 |               [{ node: 'Set', type: 'main', index: 0 }], // Done output
248 |               [{ node: 'Set', type: 'main', index: 0 }]  // Loop output
249 |             ]
250 |           },
251 |           'Set': {
252 |             main: [[{ node: 'SplitInBatches', type: 'main', index: 0 }]] // Loop back
253 |           }
254 |         }
255 |       };
256 | 
257 |       const result = await workflowValidator.validateWorkflow(workflow);
258 | 
259 |       expect(result).toBeDefined();
260 |       // Should not report cycle error for legitimate SplitInBatches loop
261 |       const cycleErrors = result.errors.filter(e => e.message.includes('cycle'));
262 |       expect(cycleErrors).toHaveLength(0);
263 |     });
264 |   });
265 | 
266 |   describe('Issue #68: Better error recovery suggestions', () => {
267 |     test('should provide recovery suggestions for invalid node types', async () => {
268 |       const workflow = {
269 |         nodes: [
270 |           {
271 |             id: '1',
272 |             name: 'Invalid Node',
273 |             type: 'invalid-node-type',
274 |             position: [100, 200] as [number, number],
275 |             parameters: {},
276 |             typeVersion: 1
277 |           }
278 |         ],
279 |         connections: {}
280 |       };
281 | 
282 |       const result = await workflowValidator.validateWorkflow(workflow);
283 | 
284 |       expect(result).toBeDefined();
285 |       expect(result.valid).toBe(false);
286 |       expect(result.suggestions.length).toBeGreaterThan(0);
287 |       
288 |       // Should contain recovery suggestions
289 |       const recoveryStarted = result.suggestions.some(s => s.includes('🔧 RECOVERY'));
290 |       expect(recoveryStarted).toBe(true);
291 |     });
292 | 
293 |     test('should provide recovery suggestions for connection errors', async () => {
294 |       const workflow = {
295 |         nodes: [
296 |           {
297 |             id: '1',
298 |             name: 'Webhook',
299 |             type: 'n8n-nodes-base.webhook',
300 |             position: [100, 200] as [number, number],
301 |             parameters: { path: '/test', httpMethod: 'POST' },
302 |             typeVersion: 1
303 |           }
304 |         ],
305 |         connections: {
306 |           'Webhook': {
307 |             main: [[{ node: 'NonExistentNode', type: 'main', index: 0 }]]
308 |           }
309 |         }
310 |       };
311 | 
312 |       const result = await workflowValidator.validateWorkflow(workflow);
313 | 
314 |       expect(result).toBeDefined();
315 |       expect(result.valid).toBe(false);
316 |       expect(result.suggestions.length).toBeGreaterThan(0);
317 |       
318 |       // Should contain connection recovery suggestions
319 |       const connectionRecovery = result.suggestions.some(s => 
320 |         s.includes('Connection errors detected') || s.includes('connection')
321 |       );
322 |       expect(connectionRecovery).toBe(true);
323 |     });
324 | 
325 |     test('should provide workflow for multiple errors', async () => {
326 |       const workflow = {
327 |         nodes: [
328 |           {
329 |             id: '1',
330 |             name: 'Invalid Node 1',
331 |             type: 'invalid-type-1',
332 |             position: [100, 200] as [number, number],
333 |             parameters: {}
334 |             // Missing typeVersion
335 |           },
336 |           {
337 |             id: '2',
338 |             name: 'Invalid Node 2',
339 |             type: 'invalid-type-2',
340 |             position: [300, 200] as [number, number],
341 |             parameters: {}
342 |             // Missing typeVersion
343 |           },
344 |           {
345 |             id: '3',
346 |             name: 'Invalid Node 3',
347 |             type: 'invalid-type-3',
348 |             position: [500, 200] as [number, number],
349 |             parameters: {}
350 |             // Missing typeVersion
351 |           }
352 |         ],
353 |         connections: {
354 |           'Invalid Node 1': {
355 |             main: [[{ node: 'NonExistent', type: 'main', index: 0 }]]
356 |           }
357 |         }
358 |       };
359 | 
360 |       const result = await workflowValidator.validateWorkflow(workflow);
361 | 
362 |       expect(result).toBeDefined();
363 |       expect(result.valid).toBe(false);
364 |       expect(result.errors.length).toBeGreaterThan(3);
365 |       
366 |       // Should provide step-by-step recovery workflow
367 |       const workflowSuggestion = result.suggestions.some(s => 
368 |         s.includes('SUGGESTED WORKFLOW') && s.includes('Too many errors detected')
369 |       );
370 |       expect(workflowSuggestion).toBe(true);
371 |     });
372 |   });
373 | 
374 |   describe('Enhanced Input Validation', () => {
375 |     test('should validate tool parameters with schemas', () => {
376 |       // Test validate_node_operation parameters
377 |       const validationResult = ToolValidation.validateNodeOperation({
378 |         nodeType: 'nodes-base.webhook',
379 |         config: { path: '/test' },
380 |         profile: 'ai-friendly'
381 |       });
382 |       
383 |       expect(validationResult.valid).toBe(true);
384 |       expect(validationResult.errors).toHaveLength(0);
385 |     });
386 | 
387 |     test('should reject invalid parameters', () => {
388 |       const validationResult = ToolValidation.validateNodeOperation({
389 |         nodeType: 123, // Invalid type
390 |         config: 'not an object', // Invalid type
391 |         profile: 'invalid-profile' // Invalid enum value
392 |       });
393 |       
394 |       expect(validationResult.valid).toBe(false);
395 |       expect(validationResult.errors.length).toBeGreaterThan(0);
396 |     });
397 | 
398 |     test('should format validation errors properly', () => {
399 |       const validationResult = ToolValidation.validateNodeOperation({
400 |         nodeType: null,
401 |         config: null
402 |       });
403 |       
404 |       const errorMessage = Validator.formatErrors(validationResult, 'validate_node_operation');
405 |       
406 |       expect(errorMessage).toContain('validate_node_operation: Validation failed:');
407 |       expect(errorMessage).toContain('nodeType');
408 |       expect(errorMessage).toContain('config');
409 |     });
410 |   });
411 | });
```

--------------------------------------------------------------------------------
/tests/unit/services/config-validator-edge-cases.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, it, expect, vi, beforeEach } from 'vitest';
  2 | import { ConfigValidator } from '@/services/config-validator';
  3 | import type { ValidationResult, ValidationError, ValidationWarning } from '@/services/config-validator';
  4 | 
  5 | // Mock the database
  6 | vi.mock('better-sqlite3');
  7 | 
  8 | describe('ConfigValidator - Edge Cases', () => {
  9 |   beforeEach(() => {
 10 |     vi.clearAllMocks();
 11 |   });
 12 | 
 13 |   describe('Null and Undefined Handling', () => {
 14 |     it('should handle null config gracefully', () => {
 15 |       const nodeType = 'nodes-base.test';
 16 |       const config = null as any;
 17 |       const properties: any[] = [];
 18 | 
 19 |       expect(() => {
 20 |         ConfigValidator.validate(nodeType, config, properties);
 21 |       }).toThrow(TypeError);
 22 |     });
 23 | 
 24 |     it('should handle undefined config gracefully', () => {
 25 |       const nodeType = 'nodes-base.test';
 26 |       const config = undefined as any;
 27 |       const properties: any[] = [];
 28 | 
 29 |       expect(() => {
 30 |         ConfigValidator.validate(nodeType, config, properties);
 31 |       }).toThrow(TypeError);
 32 |     });
 33 | 
 34 |     it('should handle null properties array gracefully', () => {
 35 |       const nodeType = 'nodes-base.test';
 36 |       const config = {};
 37 |       const properties = null as any;
 38 | 
 39 |       expect(() => {
 40 |         ConfigValidator.validate(nodeType, config, properties);
 41 |       }).toThrow(TypeError);
 42 |     });
 43 | 
 44 |     it('should handle undefined properties array gracefully', () => {
 45 |       const nodeType = 'nodes-base.test';
 46 |       const config = {};
 47 |       const properties = undefined as any;
 48 | 
 49 |       expect(() => {
 50 |         ConfigValidator.validate(nodeType, config, properties);
 51 |       }).toThrow(TypeError);
 52 |     });
 53 | 
 54 |     it('should handle properties with null values in config', () => {
 55 |       const nodeType = 'nodes-base.test';
 56 |       const config = {
 57 |         nullField: null,
 58 |         undefinedField: undefined,
 59 |         validField: 'value'
 60 |       };
 61 |       const properties = [
 62 |         { name: 'nullField', type: 'string', required: true },
 63 |         { name: 'undefinedField', type: 'string', required: true },
 64 |         { name: 'validField', type: 'string' }
 65 |       ];
 66 | 
 67 |       const result = ConfigValidator.validate(nodeType, config, properties);
 68 | 
 69 |       // Check that we have errors for both null and undefined required fields
 70 |       expect(result.errors.some(e => e.property === 'nullField')).toBe(true);
 71 |       expect(result.errors.some(e => e.property === 'undefinedField')).toBe(true);
 72 |       
 73 |       // The actual error types might vary, so let's just ensure we caught the errors
 74 |       const nullFieldError = result.errors.find(e => e.property === 'nullField');
 75 |       const undefinedFieldError = result.errors.find(e => e.property === 'undefinedField');
 76 |       
 77 |       expect(nullFieldError).toBeDefined();
 78 |       expect(undefinedFieldError).toBeDefined();
 79 |     });
 80 |   });
 81 | 
 82 |   describe('Boundary Value Testing', () => {
 83 |     it('should handle empty arrays', () => {
 84 |       const nodeType = 'nodes-base.test';
 85 |       const config = {
 86 |         arrayField: []
 87 |       };
 88 |       const properties = [
 89 |         { name: 'arrayField', type: 'collection' }
 90 |       ];
 91 | 
 92 |       const result = ConfigValidator.validate(nodeType, config, properties);
 93 | 
 94 |       expect(result.valid).toBe(true);
 95 |     });
 96 | 
 97 |     it('should handle very large property arrays', () => {
 98 |       const nodeType = 'nodes-base.test';
 99 |       const config = { field1: 'value1' };
100 |       const properties = Array(1000).fill(null).map((_, i) => ({
101 |         name: `field${i}`,
102 |         type: 'string'
103 |       }));
104 | 
105 |       const result = ConfigValidator.validate(nodeType, config, properties);
106 | 
107 |       expect(result.valid).toBe(true);
108 |     });
109 | 
110 |     it('should handle deeply nested displayOptions', () => {
111 |       const nodeType = 'nodes-base.test';
112 |       const config = {
113 |         level1: 'a',
114 |         level2: 'b',
115 |         level3: 'c',
116 |         deepField: 'value'
117 |       };
118 |       const properties = [
119 |         { name: 'level1', type: 'options', options: ['a', 'b'] },
120 |         { name: 'level2', type: 'options', options: ['a', 'b'], displayOptions: { show: { level1: ['a'] } } },
121 |         { name: 'level3', type: 'options', options: ['a', 'b', 'c'], displayOptions: { show: { level1: ['a'], level2: ['b'] } } },
122 |         { name: 'deepField', type: 'string', displayOptions: { show: { level1: ['a'], level2: ['b'], level3: ['c'] } } }
123 |       ];
124 | 
125 |       const result = ConfigValidator.validate(nodeType, config, properties);
126 | 
127 |       expect(result.visibleProperties).toContain('deepField');
128 |     });
129 | 
130 |     it('should handle extremely long string values', () => {
131 |       const nodeType = 'nodes-base.test';
132 |       const longString = 'a'.repeat(10000);
133 |       const config = {
134 |         longField: longString
135 |       };
136 |       const properties = [
137 |         { name: 'longField', type: 'string' }
138 |       ];
139 | 
140 |       const result = ConfigValidator.validate(nodeType, config, properties);
141 | 
142 |       expect(result.valid).toBe(true);
143 |     });
144 |   });
145 | 
146 |   describe('Invalid Data Type Handling', () => {
147 |     it('should handle NaN values', () => {
148 |       const nodeType = 'nodes-base.test';
149 |       const config = {
150 |         numberField: NaN
151 |       };
152 |       const properties = [
153 |         { name: 'numberField', type: 'number' }
154 |       ];
155 | 
156 |       const result = ConfigValidator.validate(nodeType, config, properties);
157 | 
158 |       // NaN is technically type 'number' in JavaScript, so type validation passes
159 |       // The validator might not have specific NaN checking, so we check for warnings
160 |       // or just verify it doesn't crash
161 |       expect(result).toBeDefined();
162 |       expect(() => result).not.toThrow();
163 |     });
164 | 
165 |     it('should handle Infinity values', () => {
166 |       const nodeType = 'nodes-base.test';
167 |       const config = {
168 |         numberField: Infinity
169 |       };
170 |       const properties = [
171 |         { name: 'numberField', type: 'number' }
172 |       ];
173 | 
174 |       const result = ConfigValidator.validate(nodeType, config, properties);
175 | 
176 |       // Infinity is technically a valid number in JavaScript
177 |       // The validator might not flag it as an error, so just verify it handles it
178 |       expect(result).toBeDefined();
179 |       expect(() => result).not.toThrow();
180 |     });
181 | 
182 |     it('should handle objects when expecting primitives', () => {
183 |       const nodeType = 'nodes-base.test';
184 |       const config = {
185 |         stringField: { nested: 'object' },
186 |         numberField: { value: 123 }
187 |       };
188 |       const properties = [
189 |         { name: 'stringField', type: 'string' },
190 |         { name: 'numberField', type: 'number' }
191 |       ];
192 | 
193 |       const result = ConfigValidator.validate(nodeType, config, properties);
194 | 
195 |       expect(result.errors).toHaveLength(2);
196 |       expect(result.errors.every(e => e.type === 'invalid_type')).toBe(true);
197 |     });
198 | 
199 |     it('should handle circular references in config', () => {
200 |       const nodeType = 'nodes-base.test';
201 |       const config: any = { field: 'value' };
202 |       config.circular = config; // Create circular reference
203 |       const properties = [
204 |         { name: 'field', type: 'string' },
205 |         { name: 'circular', type: 'json' }
206 |       ];
207 | 
208 |       // Should not throw error
209 |       const result = ConfigValidator.validate(nodeType, config, properties);
210 | 
211 |       expect(result).toBeDefined();
212 |     });
213 |   });
214 | 
215 |   describe('Performance Boundaries', () => {
216 |     it('should validate large config objects within reasonable time', () => {
217 |       const nodeType = 'nodes-base.test';
218 |       const config: Record<string, any> = {};
219 |       const properties: any[] = [];
220 | 
221 |       // Create a large config with 1000 properties
222 |       for (let i = 0; i < 1000; i++) {
223 |         config[`field_${i}`] = `value_${i}`;
224 |         properties.push({
225 |           name: `field_${i}`,
226 |           type: 'string'
227 |         });
228 |       }
229 | 
230 |       const startTime = Date.now();
231 |       const result = ConfigValidator.validate(nodeType, config, properties);
232 |       const endTime = Date.now();
233 | 
234 |       expect(result.valid).toBe(true);
235 |       expect(endTime - startTime).toBeLessThan(1000); // Should complete within 1 second
236 |     });
237 |   });
238 | 
239 |   describe('Special Characters and Encoding', () => {
240 |     it('should handle special characters in property values', () => {
241 |       const nodeType = 'nodes-base.test';
242 |       const config = {
243 |         specialField: 'Value with special chars: <>&"\'`\n\r\t'
244 |       };
245 |       const properties = [
246 |         { name: 'specialField', type: 'string' }
247 |       ];
248 | 
249 |       const result = ConfigValidator.validate(nodeType, config, properties);
250 | 
251 |       expect(result.valid).toBe(true);
252 |     });
253 | 
254 |     it('should handle unicode characters', () => {
255 |       const nodeType = 'nodes-base.test';
256 |       const config = {
257 |         unicodeField: '🚀 Unicode: 你好世界 مرحبا بالعالم'
258 |       };
259 |       const properties = [
260 |         { name: 'unicodeField', type: 'string' }
261 |       ];
262 | 
263 |       const result = ConfigValidator.validate(nodeType, config, properties);
264 | 
265 |       expect(result.valid).toBe(true);
266 |     });
267 |   });
268 | 
269 |   describe('Complex Validation Scenarios', () => {
270 |     it('should handle conflicting displayOptions conditions', () => {
271 |       const nodeType = 'nodes-base.test';
272 |       const config = {
273 |         mode: 'both',
274 |         showField: true,
275 |         conflictField: 'value'
276 |       };
277 |       const properties = [
278 |         { name: 'mode', type: 'options', options: ['show', 'hide', 'both'] },
279 |         { name: 'showField', type: 'boolean' },
280 |         {
281 |           name: 'conflictField',
282 |           type: 'string',
283 |           displayOptions: {
284 |             show: { mode: ['show'], showField: [true] },
285 |             hide: { mode: ['hide'] }
286 |           }
287 |         }
288 |       ];
289 | 
290 |       const result = ConfigValidator.validate(nodeType, config, properties);
291 | 
292 |       // With mode='both', the field visibility depends on implementation
293 |       expect(result).toBeDefined();
294 |     });
295 | 
296 |     it('should handle multiple validation profiles correctly', () => {
297 |       const nodeType = 'nodes-base.code';
298 |       const config = {
299 |         language: 'javascript',
300 |         jsCode: 'const x = 1;'
301 |       };
302 |       const properties = [
303 |         { name: 'language', type: 'options' },
304 |         { name: 'jsCode', type: 'string' }
305 |       ];
306 | 
307 |       // Should perform node-specific validation for Code nodes
308 |       const result = ConfigValidator.validate(nodeType, config, properties);
309 | 
310 |       expect(result.warnings.some(w => 
311 |         w.message.includes('No return statement found')
312 |       )).toBe(true);
313 |     });
314 |   });
315 | 
316 |   describe('Error Recovery and Resilience', () => {
317 |     it('should continue validation after encountering errors', () => {
318 |       const nodeType = 'nodes-base.test';
319 |       const config = {
320 |         field1: 'invalid-for-number',
321 |         field2: null, // Required field missing
322 |         field3: 'valid'
323 |       };
324 |       const properties = [
325 |         { name: 'field1', type: 'number' },
326 |         { name: 'field2', type: 'string', required: true },
327 |         { name: 'field3', type: 'string' }
328 |       ];
329 | 
330 |       const result = ConfigValidator.validate(nodeType, config, properties);
331 | 
332 |       // Should have errors for field1 and field2, but field3 should be validated
333 |       expect(result.errors.length).toBeGreaterThanOrEqual(2);
334 |       
335 |       // Check that we have errors for field1 (type error) and field2 (required field)
336 |       const field1Error = result.errors.find(e => e.property === 'field1');
337 |       const field2Error = result.errors.find(e => e.property === 'field2');
338 |       
339 |       expect(field1Error).toBeDefined();
340 |       expect(field1Error?.type).toBe('invalid_type');
341 |       
342 |       expect(field2Error).toBeDefined();
343 |       // field2 is null, which might be treated as invalid_type rather than missing_required
344 |       expect(['missing_required', 'invalid_type']).toContain(field2Error?.type);
345 |       
346 |       expect(result.visibleProperties).toContain('field3');
347 |     });
348 | 
349 |     it('should handle malformed property definitions gracefully', () => {
350 |       const nodeType = 'nodes-base.test';
351 |       const config = { field: 'value' };
352 |       const properties = [
353 |         { name: 'field', type: 'string' },
354 |         { /* Malformed property without name */ type: 'string' } as any,
355 |         { name: 'field2', /* Missing type */ } as any
356 |       ];
357 | 
358 |       // Should handle malformed properties without crashing
359 |       // Note: null properties will cause errors in the current implementation
360 |       const result = ConfigValidator.validate(nodeType, config, properties);
361 |       expect(result).toBeDefined();
362 |       expect(result.valid).toBeDefined();
363 |     });
364 |   });
365 | 
366 |   describe('validateBatch method implementation', () => {
367 |     it('should validate multiple configs in batch if method exists', () => {
368 |       // This test is for future implementation
369 |       const configs = [
370 |         { nodeType: 'nodes-base.test', config: { field: 'value1' }, properties: [] },
371 |         { nodeType: 'nodes-base.test', config: { field: 'value2' }, properties: [] }
372 |       ];
373 | 
374 |       // If validateBatch method is implemented in the future
375 |       if ('validateBatch' in ConfigValidator) {
376 |         const results = (ConfigValidator as any).validateBatch(configs);
377 |         expect(results).toHaveLength(2);
378 |       } else {
379 |         // For now, just validate individually
380 |         const results = configs.map(c => 
381 |           ConfigValidator.validate(c.nodeType, c.config, c.properties)
382 |         );
383 |         expect(results).toHaveLength(2);
384 |       }
385 |     });
386 |   });
387 | });
```

--------------------------------------------------------------------------------
/src/templates/template-service.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { DatabaseAdapter } from '../database/database-adapter';
  2 | import { TemplateRepository, StoredTemplate } from './template-repository';
  3 | import { logger } from '../utils/logger';
  4 | 
  5 | export interface TemplateInfo {
  6 |   id: number;
  7 |   name: string;
  8 |   description: string;
  9 |   author: {
 10 |     name: string;
 11 |     username: string;
 12 |     verified: boolean;
 13 |   };
 14 |   nodes: string[];
 15 |   views: number;
 16 |   created: string;
 17 |   url: string;
 18 |   metadata?: {
 19 |     categories: string[];
 20 |     complexity: 'simple' | 'medium' | 'complex';
 21 |     use_cases: string[];
 22 |     estimated_setup_minutes: number;
 23 |     required_services: string[];
 24 |     key_features: string[];
 25 |     target_audience: string[];
 26 |   };
 27 | }
 28 | 
 29 | export interface TemplateWithWorkflow extends TemplateInfo {
 30 |   workflow: any;
 31 | }
 32 | 
 33 | export interface PaginatedResponse<T> {
 34 |   items: T[];
 35 |   total: number;
 36 |   limit: number;
 37 |   offset: number;
 38 |   hasMore: boolean;
 39 | }
 40 | 
 41 | export interface TemplateMinimal {
 42 |   id: number;
 43 |   name: string;
 44 |   description: string;
 45 |   views: number;
 46 |   nodeCount: number;
 47 |   metadata?: {
 48 |     categories: string[];
 49 |     complexity: 'simple' | 'medium' | 'complex';
 50 |     use_cases: string[];
 51 |     estimated_setup_minutes: number;
 52 |     required_services: string[];
 53 |     key_features: string[];
 54 |     target_audience: string[];
 55 |   };
 56 | }
 57 | 
 58 | export type TemplateField = 'id' | 'name' | 'description' | 'author' | 'nodes' | 'views' | 'created' | 'url' | 'metadata';
 59 | export type PartialTemplateInfo = Partial<TemplateInfo>;
 60 | 
 61 | export class TemplateService {
 62 |   private repository: TemplateRepository;
 63 |   
 64 |   constructor(db: DatabaseAdapter) {
 65 |     this.repository = new TemplateRepository(db);
 66 |   }
 67 |   
 68 |   /**
 69 |    * List templates that use specific node types
 70 |    */
 71 |   async listNodeTemplates(nodeTypes: string[], limit: number = 10, offset: number = 0): Promise<PaginatedResponse<TemplateInfo>> {
 72 |     const templates = this.repository.getTemplatesByNodes(nodeTypes, limit, offset);
 73 |     const total = this.repository.getNodeTemplatesCount(nodeTypes);
 74 |     
 75 |     return {
 76 |       items: templates.map(this.formatTemplateInfo),
 77 |       total,
 78 |       limit,
 79 |       offset,
 80 |       hasMore: offset + limit < total
 81 |     };
 82 |   }
 83 |   
 84 |   /**
 85 |    * Get a specific template with different detail levels
 86 |    */
 87 |   async getTemplate(templateId: number, mode: 'nodes_only' | 'structure' | 'full' = 'full'): Promise<any> {
 88 |     const template = this.repository.getTemplate(templateId);
 89 |     if (!template) {
 90 |       return null;
 91 |     }
 92 |     
 93 |     const workflow = JSON.parse(template.workflow_json || '{}');
 94 |     
 95 |     if (mode === 'nodes_only') {
 96 |       return {
 97 |         id: template.id,
 98 |         name: template.name,
 99 |         nodes: workflow.nodes?.map((n: any) => ({
100 |           type: n.type,
101 |           name: n.name
102 |         })) || []
103 |       };
104 |     }
105 |     
106 |     if (mode === 'structure') {
107 |       return {
108 |         id: template.id,
109 |         name: template.name,
110 |         nodes: workflow.nodes?.map((n: any) => ({
111 |           id: n.id,
112 |           type: n.type,
113 |           name: n.name,
114 |           position: n.position
115 |         })) || [],
116 |         connections: workflow.connections || {}
117 |       };
118 |     }
119 |     
120 |     // Full mode
121 |     return {
122 |       ...this.formatTemplateInfo(template),
123 |       workflow
124 |     };
125 |   }
126 |   
127 |   /**
128 |    * Search templates by query
129 |    */
130 |   async searchTemplates(query: string, limit: number = 20, offset: number = 0, fields?: string[]): Promise<PaginatedResponse<PartialTemplateInfo>> {
131 |     const templates = this.repository.searchTemplates(query, limit, offset);
132 |     const total = this.repository.getSearchCount(query);
133 |     
134 |     // If fields are specified, filter the template info
135 |     const items = fields 
136 |       ? templates.map(t => this.formatTemplateWithFields(t, fields))
137 |       : templates.map(t => this.formatTemplateInfo(t));
138 |     
139 |     return {
140 |       items,
141 |       total,
142 |       limit,
143 |       offset,
144 |       hasMore: offset + limit < total
145 |     };
146 |   }
147 |   
148 |   /**
149 |    * Get templates for a specific task
150 |    */
151 |   async getTemplatesForTask(task: string, limit: number = 10, offset: number = 0): Promise<PaginatedResponse<TemplateInfo>> {
152 |     const templates = this.repository.getTemplatesForTask(task, limit, offset);
153 |     const total = this.repository.getTaskTemplatesCount(task);
154 |     
155 |     return {
156 |       items: templates.map(this.formatTemplateInfo),
157 |       total,
158 |       limit,
159 |       offset,
160 |       hasMore: offset + limit < total
161 |     };
162 |   }
163 |   
164 |   /**
165 |    * List all templates with minimal data
166 |    */
167 |   async listTemplates(limit: number = 10, offset: number = 0, sortBy: 'views' | 'created_at' | 'name' = 'views', includeMetadata: boolean = false): Promise<PaginatedResponse<TemplateMinimal>> {
168 |     const templates = this.repository.getAllTemplates(limit, offset, sortBy);
169 |     const total = this.repository.getTemplateCount();
170 |     
171 |     const items = templates.map(t => {
172 |       const item: TemplateMinimal = {
173 |         id: t.id,
174 |         name: t.name,
175 |         description: t.description, // Always include description
176 |         views: t.views,
177 |         nodeCount: JSON.parse(t.nodes_used).length
178 |       };
179 |       
180 |       // Optionally include metadata
181 |       if (includeMetadata && t.metadata_json) {
182 |         try {
183 |           item.metadata = JSON.parse(t.metadata_json);
184 |         } catch (error) {
185 |           logger.warn(`Failed to parse metadata for template ${t.id}:`, error);
186 |         }
187 |       }
188 |       
189 |       return item;
190 |     });
191 |     
192 |     return {
193 |       items,
194 |       total,
195 |       limit,
196 |       offset,
197 |       hasMore: offset + limit < total
198 |     };
199 |   }
200 |   
201 |   /**
202 |    * List available tasks
203 |    */
204 |   listAvailableTasks(): string[] {
205 |     return [
206 |       'ai_automation',
207 |       'data_sync',
208 |       'webhook_processing',
209 |       'email_automation',
210 |       'slack_integration',
211 |       'data_transformation',
212 |       'file_processing',
213 |       'scheduling',
214 |       'api_integration',
215 |       'database_operations'
216 |     ];
217 |   }
218 |   
219 |   /**
220 |    * Search templates by metadata filters
221 |    */
222 |   async searchTemplatesByMetadata(
223 |     filters: {
224 |       category?: string;
225 |       complexity?: 'simple' | 'medium' | 'complex';
226 |       maxSetupMinutes?: number;
227 |       minSetupMinutes?: number;
228 |       requiredService?: string;
229 |       targetAudience?: string;
230 |     },
231 |     limit: number = 20,
232 |     offset: number = 0
233 |   ): Promise<PaginatedResponse<TemplateInfo>> {
234 |     const templates = this.repository.searchTemplatesByMetadata(filters, limit, offset);
235 |     const total = this.repository.getMetadataSearchCount(filters);
236 |     
237 |     return {
238 |       items: templates.map(this.formatTemplateInfo.bind(this)),
239 |       total,
240 |       limit,
241 |       offset,
242 |       hasMore: offset + limit < total
243 |     };
244 |   }
245 |   
246 |   /**
247 |    * Get available categories from template metadata
248 |    */
249 |   async getAvailableCategories(): Promise<string[]> {
250 |     return this.repository.getAvailableCategories();
251 |   }
252 |   
253 |   /**
254 |    * Get available target audiences from template metadata
255 |    */
256 |   async getAvailableTargetAudiences(): Promise<string[]> {
257 |     return this.repository.getAvailableTargetAudiences();
258 |   }
259 |   
260 |   /**
261 |    * Get templates by category
262 |    */
263 |   async getTemplatesByCategory(
264 |     category: string,
265 |     limit: number = 10,
266 |     offset: number = 0
267 |   ): Promise<PaginatedResponse<TemplateInfo>> {
268 |     const templates = this.repository.getTemplatesByCategory(category, limit, offset);
269 |     const total = this.repository.getMetadataSearchCount({ category });
270 |     
271 |     return {
272 |       items: templates.map(this.formatTemplateInfo.bind(this)),
273 |       total,
274 |       limit,
275 |       offset,
276 |       hasMore: offset + limit < total
277 |     };
278 |   }
279 |   
280 |   /**
281 |    * Get templates by complexity level
282 |    */
283 |   async getTemplatesByComplexity(
284 |     complexity: 'simple' | 'medium' | 'complex',
285 |     limit: number = 10,
286 |     offset: number = 0
287 |   ): Promise<PaginatedResponse<TemplateInfo>> {
288 |     const templates = this.repository.getTemplatesByComplexity(complexity, limit, offset);
289 |     const total = this.repository.getMetadataSearchCount({ complexity });
290 |     
291 |     return {
292 |       items: templates.map(this.formatTemplateInfo.bind(this)),
293 |       total,
294 |       limit,
295 |       offset,
296 |       hasMore: offset + limit < total
297 |     };
298 |   }
299 |   
300 |   /**
301 |    * Get template statistics
302 |    */
303 |   async getTemplateStats(): Promise<Record<string, any>> {
304 |     return this.repository.getTemplateStats();
305 |   }
306 |   
307 |   /**
308 |    * Fetch and update templates from n8n.io
309 |    * @param mode - 'rebuild' to clear and rebuild, 'update' to add only new templates
310 |    */
311 |   async fetchAndUpdateTemplates(
312 |     progressCallback?: (message: string, current: number, total: number) => void,
313 |     mode: 'rebuild' | 'update' = 'rebuild'
314 |   ): Promise<void> {
315 |     try {
316 |       // Dynamically import fetcher only when needed (requires axios)
317 |       const { TemplateFetcher } = await import('./template-fetcher');
318 |       const fetcher = new TemplateFetcher();
319 |       
320 |       // Get existing template IDs if in update mode
321 |       let existingIds: Set<number> = new Set();
322 |       let sinceDate: Date | undefined;
323 | 
324 |       if (mode === 'update') {
325 |         existingIds = this.repository.getExistingTemplateIds();
326 |         logger.info(`Update mode: Found ${existingIds.size} existing templates in database`);
327 | 
328 |         // Get most recent template date and fetch only templates from last 2 weeks
329 |         const mostRecentDate = this.repository.getMostRecentTemplateDate();
330 |         if (mostRecentDate) {
331 |           // Fetch templates from 2 weeks before the most recent template
332 |           sinceDate = new Date(mostRecentDate);
333 |           sinceDate.setDate(sinceDate.getDate() - 14);
334 |           logger.info(`Update mode: Fetching templates since ${sinceDate.toISOString().split('T')[0]} (2 weeks before most recent)`);
335 |         } else {
336 |           // No templates yet, fetch from last 2 weeks
337 |           sinceDate = new Date();
338 |           sinceDate.setDate(sinceDate.getDate() - 14);
339 |           logger.info(`Update mode: No existing templates, fetching from last 2 weeks`);
340 |         }
341 |       } else {
342 |         // Clear existing templates in rebuild mode
343 |         this.repository.clearTemplates();
344 |         logger.info('Rebuild mode: Cleared existing templates');
345 |       }
346 | 
347 |       // Fetch template list
348 |       logger.info(`Fetching template list from n8n.io (mode: ${mode})`);
349 |       const templates = await fetcher.fetchTemplates((current, total) => {
350 |         progressCallback?.('Fetching template list', current, total);
351 |       }, sinceDate);
352 |       
353 |       logger.info(`Found ${templates.length} templates matching date criteria`);
354 |       
355 |       // Filter to only new templates if in update mode
356 |       let templatesToFetch = templates;
357 |       if (mode === 'update') {
358 |         templatesToFetch = templates.filter(t => !existingIds.has(t.id));
359 |         logger.info(`Update mode: ${templatesToFetch.length} new templates to fetch (skipping ${templates.length - templatesToFetch.length} existing)`);
360 |         
361 |         if (templatesToFetch.length === 0) {
362 |           logger.info('No new templates to fetch');
363 |           progressCallback?.('No new templates', 0, 0);
364 |           return;
365 |         }
366 |       }
367 |       
368 |       // Fetch details for each template
369 |       logger.info(`Fetching details for ${templatesToFetch.length} templates`);
370 |       const details = await fetcher.fetchAllTemplateDetails(templatesToFetch, (current, total) => {
371 |         progressCallback?.('Fetching template details', current, total);
372 |       });
373 |       
374 |       // Save to database
375 |       logger.info('Saving templates to database');
376 |       let saved = 0;
377 |       for (const template of templatesToFetch) {
378 |         const detail = details.get(template.id);
379 |         if (detail) {
380 |           this.repository.saveTemplate(template, detail);
381 |           saved++;
382 |         }
383 |       }
384 |       
385 |       logger.info(`Successfully saved ${saved} templates to database`);
386 |       
387 |       // Rebuild FTS5 index after bulk import
388 |       if (saved > 0) {
389 |         logger.info('Rebuilding FTS5 index for templates');
390 |         this.repository.rebuildTemplateFTS();
391 |       }
392 |       
393 |       progressCallback?.('Complete', saved, saved);
394 |     } catch (error) {
395 |       logger.error('Error fetching templates:', error);
396 |       throw error;
397 |     }
398 |   }
399 |   
400 |   /**
401 |    * Format stored template for API response
402 |    */
403 |   private formatTemplateInfo(template: StoredTemplate): TemplateInfo {
404 |     const info: TemplateInfo = {
405 |       id: template.id,
406 |       name: template.name,
407 |       description: template.description,
408 |       author: {
409 |         name: template.author_name,
410 |         username: template.author_username,
411 |         verified: template.author_verified === 1
412 |       },
413 |       nodes: JSON.parse(template.nodes_used),
414 |       views: template.views,
415 |       created: template.created_at,
416 |       url: template.url
417 |     };
418 |     
419 |     // Include metadata if available
420 |     if (template.metadata_json) {
421 |       try {
422 |         info.metadata = JSON.parse(template.metadata_json);
423 |       } catch (error) {
424 |         logger.warn(`Failed to parse metadata for template ${template.id}:`, error);
425 |       }
426 |     }
427 |     
428 |     return info;
429 |   }
430 |   
431 |   /**
432 |    * Format template with only specified fields
433 |    */
434 |   private formatTemplateWithFields(template: StoredTemplate, fields: string[]): PartialTemplateInfo {
435 |     const fullInfo = this.formatTemplateInfo(template);
436 |     const result: PartialTemplateInfo = {};
437 |     
438 |     // Only include requested fields
439 |     for (const field of fields) {
440 |       if (field in fullInfo) {
441 |         (result as any)[field] = (fullInfo as any)[field];
442 |       }
443 |     }
444 |     
445 |     return result;
446 |   }
447 | }
```

--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------

```yaml
  1 | name: Test Suite
  2 | on:
  3 |   push:
  4 |     branches: [main, feat/comprehensive-testing-suite]
  5 |     paths-ignore:
  6 |       - '**.md'
  7 |       - '**.txt'
  8 |       - 'docs/**'
  9 |       - 'examples/**'
 10 |       - '.github/FUNDING.yml'
 11 |       - '.github/ISSUE_TEMPLATE/**'
 12 |       - '.github/pull_request_template.md'
 13 |       - '.gitignore'
 14 |       - 'LICENSE*'
 15 |       - 'ATTRIBUTION.md'
 16 |       - 'SECURITY.md'
 17 |       - 'CODE_OF_CONDUCT.md'
 18 |   pull_request:
 19 |     branches: [main]
 20 |     paths-ignore:
 21 |       - '**.md'
 22 |       - '**.txt'
 23 |       - 'docs/**'
 24 |       - 'examples/**'
 25 |       - '.github/FUNDING.yml'
 26 |       - '.github/ISSUE_TEMPLATE/**'
 27 |       - '.github/pull_request_template.md'
 28 |       - '.gitignore'
 29 |       - 'LICENSE*'
 30 |       - 'ATTRIBUTION.md'
 31 |       - 'SECURITY.md'
 32 |       - 'CODE_OF_CONDUCT.md'
 33 | 
 34 | permissions:
 35 |   contents: read
 36 |   issues: write
 37 |   pull-requests: write
 38 |   checks: write
 39 | 
 40 | jobs:
 41 |   test:
 42 |     runs-on: ubuntu-latest
 43 |     timeout-minutes: 10  # Add a 10-minute timeout to prevent hanging
 44 |     steps:
 45 |       - uses: actions/checkout@v4
 46 |       
 47 |       - uses: actions/setup-node@v4
 48 |         with:
 49 |           node-version: 20
 50 |           cache: 'npm'
 51 |       
 52 |       - name: Install dependencies
 53 |         run: npm ci
 54 |       
 55 |       # Verify test environment setup
 56 |       - name: Verify test environment
 57 |         run: |
 58 |           echo "Current directory: $(pwd)"
 59 |           echo "Checking for .env.test file:"
 60 |           ls -la .env.test || echo ".env.test not found!"
 61 |           echo "First few lines of .env.test:"
 62 |           head -5 .env.test || echo "Cannot read .env.test"
 63 |       
 64 |       # Run unit tests first (without MSW)
 65 |       - name: Run unit tests with coverage
 66 |         run: npm run test:unit -- --coverage --coverage.thresholds.lines=0 --coverage.thresholds.functions=0 --coverage.thresholds.branches=0 --coverage.thresholds.statements=0 --reporter=default --reporter=junit
 67 |         env:
 68 |           CI: true
 69 |       
 70 |       # Run integration tests separately (with MSW setup)
 71 |       - name: Run integration tests
 72 |         run: npm run test:integration -- --reporter=default --reporter=junit
 73 |         env:
 74 |           CI: true
 75 |           N8N_API_URL: ${{ secrets.N8N_API_URL }}
 76 |           N8N_API_KEY: ${{ secrets.N8N_API_KEY }}
 77 |           N8N_TEST_WEBHOOK_GET_URL: ${{ secrets.N8N_TEST_WEBHOOK_GET_URL }}
 78 |           N8N_TEST_WEBHOOK_POST_URL: ${{ secrets.N8N_TEST_WEBHOOK_POST_URL }}
 79 |           N8N_TEST_WEBHOOK_PUT_URL: ${{ secrets.N8N_TEST_WEBHOOK_PUT_URL }}
 80 |           N8N_TEST_WEBHOOK_DELETE_URL: ${{ secrets.N8N_TEST_WEBHOOK_DELETE_URL }}
 81 |       
 82 |       # Generate test summary
 83 |       - name: Generate test summary
 84 |         if: always()
 85 |         run: node scripts/generate-test-summary.js
 86 |       
 87 |       # Generate detailed reports
 88 |       - name: Generate detailed reports
 89 |         if: always()
 90 |         run: node scripts/generate-detailed-reports.js
 91 |       
 92 |       # Upload test results artifacts
 93 |       - name: Upload test results
 94 |         if: always()
 95 |         uses: actions/upload-artifact@v4
 96 |         with:
 97 |           name: test-results-${{ github.run_number }}-${{ github.run_attempt }}
 98 |           path: |
 99 |             test-results/
100 |             test-summary.md
101 |             test-reports/
102 |           retention-days: 30
103 |           if-no-files-found: warn
104 |       
105 |       # Upload coverage artifacts
106 |       - name: Upload coverage reports
107 |         if: always()
108 |         uses: actions/upload-artifact@v4
109 |         with:
110 |           name: coverage-${{ github.run_number }}-${{ github.run_attempt }}
111 |           path: |
112 |             coverage/
113 |           retention-days: 30
114 |           if-no-files-found: warn
115 |       
116 |       # Upload coverage to Codecov
117 |       - name: Upload coverage to Codecov
118 |         if: always()
119 |         uses: codecov/codecov-action@v4
120 |         with:
121 |           token: ${{ secrets.CODECOV_TOKEN }}
122 |           files: ./coverage/lcov.info
123 |           flags: unittests
124 |           name: codecov-umbrella
125 |           fail_ci_if_error: false
126 |           verbose: true
127 |       
128 |       # Run linting
129 |       - name: Run linting
130 |         run: npm run lint
131 |       
132 |       # Run type checking
133 |       - name: Run type checking
134 |         run: npm run typecheck
135 |       
136 |       # Run benchmarks
137 |       - name: Run benchmarks
138 |         id: benchmarks
139 |         run: npm run benchmark:ci
140 |         continue-on-error: true
141 |       
142 |       # Upload benchmark results
143 |       - name: Upload benchmark results
144 |         if: always() && steps.benchmarks.outcome != 'skipped'
145 |         uses: actions/upload-artifact@v4
146 |         with:
147 |           name: benchmark-results-${{ github.run_number }}-${{ github.run_attempt }}
148 |           path: |
149 |             benchmark-results.json
150 |           retention-days: 30
151 |           if-no-files-found: warn
152 |       
153 |       # Create test report comment for PRs
154 |       - name: Create test report comment
155 |         if: github.event_name == 'pull_request' && always()
156 |         uses: actions/github-script@v7
157 |         continue-on-error: true
158 |         with:
159 |           script: |
160 |             const fs = require('fs');
161 |             let summary = '## Test Results\n\nTest summary generation failed.';
162 |             
163 |             try {
164 |               if (fs.existsSync('test-summary.md')) {
165 |                 summary = fs.readFileSync('test-summary.md', 'utf8');
166 |               }
167 |             } catch (error) {
168 |               console.error('Error reading test summary:', error);
169 |             }
170 |             
171 |             try {
172 |               // Find existing comment
173 |               const { data: comments } = await github.rest.issues.listComments({
174 |                 owner: context.repo.owner,
175 |                 repo: context.repo.repo,
176 |                 issue_number: context.issue.number,
177 |               });
178 |               
179 |               const botComment = comments.find(comment => 
180 |                 comment.user.type === 'Bot' && 
181 |                 comment.body.includes('## Test Results')
182 |               );
183 |               
184 |               if (botComment) {
185 |                 // Update existing comment
186 |                 await github.rest.issues.updateComment({
187 |                   owner: context.repo.owner,
188 |                   repo: context.repo.repo,
189 |                   comment_id: botComment.id,
190 |                   body: summary
191 |                 });
192 |               } else {
193 |                 // Create new comment
194 |                 await github.rest.issues.createComment({
195 |                   owner: context.repo.owner,
196 |                   repo: context.repo.repo,
197 |                   issue_number: context.issue.number,
198 |                   body: summary
199 |                 });
200 |               }
201 |             } catch (error) {
202 |               console.error('Failed to create/update PR comment:', error.message);
203 |               console.log('This is likely due to insufficient permissions for external PRs.');
204 |               console.log('Test results have been saved to the job summary instead.');
205 |             }
206 |       
207 |       # Generate job summary
208 |       - name: Generate job summary
209 |         if: always()
210 |         run: |
211 |           echo "# Test Run Summary" >> $GITHUB_STEP_SUMMARY
212 |           echo "" >> $GITHUB_STEP_SUMMARY
213 |           
214 |           if [ -f test-summary.md ]; then
215 |             cat test-summary.md >> $GITHUB_STEP_SUMMARY
216 |           else
217 |             echo "Test summary generation failed." >> $GITHUB_STEP_SUMMARY
218 |           fi
219 |           
220 |           echo "" >> $GITHUB_STEP_SUMMARY
221 |           echo "## 📥 Download Artifacts" >> $GITHUB_STEP_SUMMARY
222 |           echo "" >> $GITHUB_STEP_SUMMARY
223 |           echo "- [Test Results](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})" >> $GITHUB_STEP_SUMMARY
224 |           echo "- [Coverage Report](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})" >> $GITHUB_STEP_SUMMARY
225 |           echo "- [Benchmark Results](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})" >> $GITHUB_STEP_SUMMARY
226 |       
227 |       # Store test metadata
228 |       - name: Store test metadata
229 |         if: always()
230 |         run: |
231 |           cat > test-metadata.json << EOF
232 |           {
233 |             "run_id": "${{ github.run_id }}",
234 |             "run_number": "${{ github.run_number }}",
235 |             "run_attempt": "${{ github.run_attempt }}",
236 |             "sha": "${{ github.sha }}",
237 |             "ref": "${{ github.ref }}",
238 |             "event_name": "${{ github.event_name }}",
239 |             "repository": "${{ github.repository }}",
240 |             "actor": "${{ github.actor }}",
241 |             "timestamp": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
242 |             "node_version": "$(node --version)",
243 |             "npm_version": "$(npm --version)"
244 |           }
245 |           EOF
246 |       
247 |       - name: Upload test metadata
248 |         if: always()
249 |         uses: actions/upload-artifact@v4
250 |         with:
251 |           name: test-metadata-${{ github.run_number }}-${{ github.run_attempt }}
252 |           path: test-metadata.json
253 |           retention-days: 30
254 | 
255 |   # Separate job to process and publish test results
256 |   publish-results:
257 |     needs: test
258 |     runs-on: ubuntu-latest
259 |     if: always()
260 |     permissions:
261 |       checks: write
262 |       pull-requests: write
263 |     steps:
264 |       - uses: actions/checkout@v4
265 |       
266 |       # Download all artifacts
267 |       - name: Download all artifacts
268 |         uses: actions/download-artifact@v4
269 |         with:
270 |           path: artifacts
271 |       
272 |       # Publish test results as checks
273 |       - name: Publish test results
274 |         uses: dorny/test-reporter@v1
275 |         if: always()
276 |         continue-on-error: true
277 |         with:
278 |           name: Test Results
279 |           path: 'artifacts/test-results-*/test-results/junit.xml'
280 |           reporter: java-junit
281 |           fail-on-error: false
282 |           fail-on-empty: false
283 |       
284 |       # Create a combined artifact with all results
285 |       - name: Create combined results artifact
286 |         if: always()
287 |         run: |
288 |           mkdir -p combined-results
289 |           cp -r artifacts/* combined-results/ 2>/dev/null || true
290 |           
291 |           # Create index file
292 |           cat > combined-results/index.html << 'EOF'
293 |           <!DOCTYPE html>
294 |           <html>
295 |           <head>
296 |               <title>n8n-mcp Test Results</title>
297 |               <style>
298 |                   body { font-family: Arial, sans-serif; margin: 40px; }
299 |                   h1 { color: #333; }
300 |                   .section { margin: 20px 0; padding: 20px; border: 1px solid #ddd; border-radius: 5px; }
301 |                   a { color: #0066cc; text-decoration: none; }
302 |                   a:hover { text-decoration: underline; }
303 |               </style>
304 |           </head>
305 |           <body>
306 |               <h1>n8n-mcp Test Results</h1>
307 |               <div class="section">
308 |                   <h2>Test Reports</h2>
309 |                   <ul>
310 |                       <li><a href="test-results-${{ github.run_number }}-${{ github.run_attempt }}/test-reports/report.html">📊 Detailed HTML Report</a></li>
311 |                       <li><a href="test-results-${{ github.run_number }}-${{ github.run_attempt }}/test-results/html/index.html">📈 Vitest HTML Report</a></li>
312 |                       <li><a href="test-results-${{ github.run_number }}-${{ github.run_attempt }}/test-reports/report.md">📄 Markdown Report</a></li>
313 |                       <li><a href="test-results-${{ github.run_number }}-${{ github.run_attempt }}/test-summary.md">📝 PR Summary</a></li>
314 |                       <li><a href="test-results-${{ github.run_number }}-${{ github.run_attempt }}/test-results/junit.xml">🔧 JUnit XML</a></li>
315 |                       <li><a href="test-results-${{ github.run_number }}-${{ github.run_attempt }}/test-results/results.json">🔢 JSON Results</a></li>
316 |                       <li><a href="test-results-${{ github.run_number }}-${{ github.run_attempt }}/test-reports/report.json">📊 Full JSON Report</a></li>
317 |                   </ul>
318 |               </div>
319 |               <div class="section">
320 |                   <h2>Coverage Reports</h2>
321 |                   <ul>
322 |                       <li><a href="coverage-${{ github.run_number }}-${{ github.run_attempt }}/html/index.html">HTML Coverage Report</a></li>
323 |                       <li><a href="coverage-${{ github.run_number }}-${{ github.run_attempt }}/lcov.info">LCOV Report</a></li>
324 |                       <li><a href="coverage-${{ github.run_number }}-${{ github.run_attempt }}/coverage-summary.json">Coverage Summary JSON</a></li>
325 |                   </ul>
326 |               </div>
327 |               <div class="section">
328 |                   <h2>Benchmark Results</h2>
329 |                   <ul>
330 |                       <li><a href="benchmark-results-${{ github.run_number }}-${{ github.run_attempt }}/benchmark-results.json">Benchmark Results JSON</a></li>
331 |                   </ul>
332 |               </div>
333 |               <div class="section">
334 |                   <h2>Metadata</h2>
335 |                   <ul>
336 |                       <li><a href="test-metadata-${{ github.run_number }}-${{ github.run_attempt }}/test-metadata.json">Test Run Metadata</a></li>
337 |                   </ul>
338 |               </div>
339 |               <div class="section">
340 |                   <p><em>Generated at $(date -u +%Y-%m-%dT%H:%M:%SZ)</em></p>
341 |                   <p><em>Run: #${{ github.run_number }} | SHA: ${{ github.sha }}</em></p>
342 |               </div>
343 |           </body>
344 |           </html>
345 |           EOF
346 |       
347 |       - name: Upload combined results
348 |         if: always()
349 |         uses: actions/upload-artifact@v4
350 |         with:
351 |           name: all-test-results-${{ github.run_number }}
352 |           path: combined-results/
353 |           retention-days: 90
```

--------------------------------------------------------------------------------
/docs/DOCKER_README.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Docker Deployment Guide for n8n-MCP
  2 | 
  3 | This guide provides comprehensive instructions for deploying n8n-MCP using Docker.
  4 | 
  5 | ## 🚀 Quick Start
  6 | 
  7 | ### Prerequisites
  8 | - Docker Engine 20.10+ (Docker Desktop on Windows/macOS, or Docker Engine on Linux)
  9 | - Docker Compose V2
 10 | - (Optional) openssl for generating auth tokens
 11 | 
 12 | ### 1. HTTP Server Mode (Recommended)
 13 | 
 14 | The simplest way to deploy n8n-MCP is using Docker Compose with HTTP mode:
 15 | 
 16 | ```bash
 17 | # Clone the repository
 18 | git clone https://github.com/czlonkowski/n8n-mcp.git
 19 | cd n8n-mcp
 20 | 
 21 | # Create .env file with auth token
 22 | cat > .env << EOF
 23 | AUTH_TOKEN=$(openssl rand -base64 32)
 24 | USE_FIXED_HTTP=true
 25 | EOF
 26 | 
 27 | # Start the server
 28 | docker compose up -d
 29 | 
 30 | # Check logs
 31 | docker compose logs -f
 32 | 
 33 | # Test the health endpoint
 34 | curl http://localhost:3000/health
 35 | ```
 36 | 
 37 | ### 2. Using Pre-built Images
 38 | 
 39 | Pre-built images are available on GitHub Container Registry:
 40 | 
 41 | ```bash
 42 | # Pull the latest image (~280MB optimized)
 43 | docker pull ghcr.io/czlonkowski/n8n-mcp:latest
 44 | 
 45 | # Run with HTTP mode
 46 | docker run -d \
 47 |   --name n8n-mcp \
 48 |   -e MCP_MODE=http \
 49 |   -e USE_FIXED_HTTP=true \
 50 |   -e AUTH_TOKEN=your-secure-token \
 51 |   -p 3000:3000 \
 52 |   ghcr.io/czlonkowski/n8n-mcp:latest
 53 | ```
 54 | 
 55 | ## 📋 Configuration Options
 56 | 
 57 | ### Environment Variables
 58 | 
 59 | | Variable | Description | Default | Required |
 60 | |----------|-------------|---------|----------|
 61 | | `MCP_MODE` | Server mode: `stdio` or `http` | `stdio` | No |
 62 | | `AUTH_TOKEN` | Bearer token for HTTP authentication | - | Yes (HTTP mode)* |
 63 | | `AUTH_TOKEN_FILE` | Path to file containing auth token (v2.7.5+) | - | Yes (HTTP mode)* |
 64 | | `PORT` | HTTP server port | `3000` | No |
 65 | | `NODE_ENV` | Environment: `development` or `production` | `production` | No |
 66 | | `LOG_LEVEL` | Logging level: `debug`, `info`, `warn`, `error` | `info` | No |
 67 | | `NODE_DB_PATH` | Custom database path (v2.7.16+) | `/app/data/nodes.db` | No |
 68 | | `AUTH_RATE_LIMIT_WINDOW` | Rate limit window in ms (v2.16.3+) | `900000` (15 min) | No |
 69 | | `AUTH_RATE_LIMIT_MAX` | Max auth attempts per window (v2.16.3+) | `20` | No |
 70 | | `WEBHOOK_SECURITY_MODE` | SSRF protection: `strict`/`moderate`/`permissive` (v2.16.3+) | `strict` | No |
 71 | 
 72 | *Either `AUTH_TOKEN` or `AUTH_TOKEN_FILE` must be set for HTTP mode. If both are set, `AUTH_TOKEN` takes precedence.
 73 | 
 74 | ### Configuration File Support (v2.8.2+)
 75 | 
 76 | You can mount a JSON configuration file to set environment variables:
 77 | 
 78 | ```bash
 79 | # Create config file
 80 | cat > config.json << EOF
 81 | {
 82 |   "MCP_MODE": "http",
 83 |   "AUTH_TOKEN": "your-secure-token",
 84 |   "LOG_LEVEL": "info",
 85 |   "N8N_API_URL": "https://your-n8n-instance.com",
 86 |   "N8N_API_KEY": "your-api-key"
 87 | }
 88 | EOF
 89 | 
 90 | # Run with config file
 91 | docker run -d \
 92 |   --name n8n-mcp \
 93 |   -v $(pwd)/config.json:/app/config.json:ro \
 94 |   -p 3000:3000 \
 95 |   ghcr.io/czlonkowski/n8n-mcp:latest
 96 | ```
 97 | 
 98 | The config file supports:
 99 | - All standard environment variables
100 | - Nested objects (flattened with underscore separators)
101 | - Arrays, booleans, numbers, and strings
102 | - Secure handling with command injection prevention
103 | - Dangerous variable blocking for security
104 | 
105 | ### Docker Compose Configuration
106 | 
107 | The default `docker-compose.yml` provides:
108 | - Automatic restart on failure
109 | - Named volume for data persistence
110 | - Memory limits (512MB max, 256MB reserved)
111 | - Health checks every 30 seconds
112 | - Container labels for organization
113 | 
114 | ### Custom Configuration
115 | 
116 | Create a `docker-compose.override.yml` for local customizations:
117 | 
118 | ```yaml
119 | # docker-compose.override.yml
120 | services:
121 |   n8n-mcp:
122 |     ports:
123 |       - "8080:3000"  # Use different port
124 |     environment:
125 |       LOG_LEVEL: debug
126 |       NODE_ENV: development
127 |     volumes:
128 |       - ./custom-data:/app/data  # Use local directory
129 | ```
130 | 
131 | ## 🔧 Usage Modes
132 | 
133 | ### HTTP Mode (Remote Access)
134 | 
135 | Perfect for cloud deployments and remote access:
136 | 
137 | ```bash
138 | # Start in HTTP mode
139 | docker run -d \
140 |   --name n8n-mcp-http \
141 |   -e MCP_MODE=http \
142 |   -e AUTH_TOKEN=your-secure-token \
143 |   -p 3000:3000 \
144 |   ghcr.io/czlonkowski/n8n-mcp:latest
145 | ```
146 | 
147 | Configure Claude Desktop with mcp-remote:
148 | ```json
149 | {
150 |   "mcpServers": {
151 |     "n8n-remote": {
152 |       "command": "npx",
153 |       "args": [
154 |         "-y",
155 |         "@modelcontextprotocol/mcp-remote@latest",
156 |         "connect",
157 |         "http://your-server:3000/mcp"
158 |       ],
159 |       "env": {
160 |         "MCP_AUTH_TOKEN": "your-secure-token"
161 |       }
162 |     }
163 |   }
164 | }
165 | ```
166 | 
167 | ### Stdio Mode (Local Direct Access)
168 | 
169 | For local Claude Desktop integration without HTTP:
170 | 
171 | ```bash
172 | # Run in stdio mode (interactive)
173 | docker run --rm -i --init \
174 |   -e MCP_MODE=stdio \
175 |   -v n8n-mcp-data:/app/data \
176 |   ghcr.io/czlonkowski/n8n-mcp:latest
177 | ```
178 | 
179 | ### Server Mode (Command Line)
180 | 
181 | You can also use the `serve` command to start in HTTP mode:
182 | 
183 | ```bash
184 | # Using the serve command (v2.8.2+)
185 | docker run -d \
186 |   --name n8n-mcp \
187 |   -e AUTH_TOKEN=your-secure-token \
188 |   -p 3000:3000 \
189 |   ghcr.io/czlonkowski/n8n-mcp:latest serve
190 | ```
191 | 
192 | Configure Claude Desktop:
193 | ```json
194 | {
195 |   "mcpServers": {
196 |     "n8n-docker": {
197 |       "command": "docker",
198 |       "args": [
199 |         "run",
200 |         "--rm",
201 |         "-i",
202 |         "--init",
203 |         "-e", "MCP_MODE=stdio",
204 |         "-v", "n8n-mcp-data:/app/data",
205 |         "ghcr.io/czlonkowski/n8n-mcp:latest"
206 |       ]
207 |     }
208 |   }
209 | }
210 | ```
211 | 
212 | ## 🏗️ Building from Source
213 | 
214 | ### Build Locally
215 | 
216 | ```bash
217 | # Clone repository
218 | git clone https://github.com/czlonkowski/n8n-mcp.git
219 | cd n8n-mcp
220 | 
221 | # Build image
222 | docker build -t n8n-mcp:local .
223 | 
224 | # Run your local build
225 | docker run -d \
226 |   --name n8n-mcp-local \
227 |   -e MCP_MODE=http \
228 |   -e AUTH_TOKEN=test-token \
229 |   -p 3000:3000 \
230 |   n8n-mcp:local
231 | ```
232 | 
233 | ### Multi-architecture Build
234 | 
235 | Build for multiple platforms:
236 | 
237 | ```bash
238 | # Enable buildx
239 | docker buildx create --use
240 | 
241 | # Build for amd64 and arm64
242 | docker buildx build \
243 |   --platform linux/amd64,linux/arm64 \
244 |   -t n8n-mcp:multiarch \
245 |   --load \
246 |   .
247 | ```
248 | 
249 | ## 🔍 Health Monitoring
250 | 
251 | ### Health Check Endpoint
252 | 
253 | The container includes a health check that runs every 30 seconds:
254 | 
255 | ```bash
256 | # Check health status
257 | curl http://localhost:3000/health
258 | ```
259 | 
260 | Response example:
261 | ```json
262 | {
263 |   "status": "healthy",
264 |   "uptime": 120.5,
265 |   "memory": {
266 |     "used": "8.5 MB",
267 |     "rss": "45.2 MB",
268 |     "external": "1.2 MB"
269 |   },
270 |   "version": "2.3.0",
271 |   "mode": "http",
272 |   "database": {
273 |     "adapter": "better-sqlite3",
274 |     "ready": true
275 |   }
276 | }
277 | ```
278 | 
279 | ### Docker Health Status
280 | 
281 | ```bash
282 | # Check container health
283 | docker ps --format "table {{.Names}}\t{{.Status}}"
284 | 
285 | # View health check logs
286 | docker inspect n8n-mcp | jq '.[0].State.Health'
287 | ```
288 | 
289 | ## 🔒 Security Features (v2.16.3+)
290 | 
291 | ### Rate Limiting
292 | 
293 | Protects against brute force authentication attacks:
294 | 
295 | ```bash
296 | # Configure in .env or docker-compose.yml
297 | AUTH_RATE_LIMIT_WINDOW=900000  # 15 minutes in milliseconds
298 | AUTH_RATE_LIMIT_MAX=20         # 20 attempts per IP per window
299 | ```
300 | 
301 | ### SSRF Protection
302 | 
303 | Prevents Server-Side Request Forgery when using webhook triggers:
304 | 
305 | ```bash
306 | # For production (blocks localhost + private IPs + cloud metadata)
307 | WEBHOOK_SECURITY_MODE=strict
308 | 
309 | # For local development with local n8n instance
310 | WEBHOOK_SECURITY_MODE=moderate
311 | 
312 | # For internal testing only (allows private IPs)
313 | WEBHOOK_SECURITY_MODE=permissive
314 | ```
315 | 
316 | **Note:** Cloud metadata endpoints (169.254.169.254, metadata.google.internal, etc.) are ALWAYS blocked in all modes.
317 | 
318 | ## 🔒 Authentication
319 | 
320 | ### Authentication
321 | 
322 | n8n-MCP supports two authentication methods for HTTP mode:
323 | 
324 | #### Method 1: AUTH_TOKEN (Environment Variable)
325 | - Set the token directly as an environment variable
326 | - Simple and straightforward for basic deployments
327 | - Always use a strong token (minimum 32 characters)
328 | 
329 | ```bash
330 | # Generate secure token
331 | openssl rand -base64 32
332 | 
333 | # Use in Docker
334 | docker run -e AUTH_TOKEN=your-secure-token ...
335 | ```
336 | 
337 | #### Method 2: AUTH_TOKEN_FILE (File Path) - NEW in v2.7.5
338 | - Read token from a file (Docker secrets compatible)
339 | - More secure for production deployments
340 | - Prevents token exposure in process lists
341 | 
342 | ```bash
343 | # Create token file
344 | echo "your-secure-token" > /path/to/token.txt
345 | 
346 | # Use with Docker secrets
347 | docker run -e AUTH_TOKEN_FILE=/run/secrets/auth_token ...
348 | ```
349 | 
350 | #### Best Practices
351 | - Never commit tokens to version control
352 | - Rotate tokens regularly
353 | - Use AUTH_TOKEN_FILE with Docker secrets for production
354 | - Ensure token files have restricted permissions (600)
355 | 
356 | ### Network Security
357 | 
358 | For production deployments:
359 | 
360 | 1. **Use HTTPS** - Put a reverse proxy (nginx, Caddy) in front
361 | 2. **Firewall** - Restrict access to trusted IPs only
362 | 3. **VPN** - Consider VPN access for internal use
363 | 
364 | Example with Caddy:
365 | ```
366 | your-domain.com {
367 |   reverse_proxy n8n-mcp:3000
368 |   basicauth * {
369 |     admin $2a$14$... # bcrypt hash
370 |   }
371 | }
372 | ```
373 | 
374 | ### Container Security
375 | 
376 | - Runs as non-root user (uid 1001)
377 | - Read-only root filesystem compatible
378 | - No unnecessary packages installed
379 | - Regular security updates via GitHub Actions
380 | 
381 | ## 📊 Resource Management
382 | 
383 | ### Memory Limits
384 | 
385 | Default limits in docker-compose.yml:
386 | - Maximum: 512MB
387 | - Reserved: 256MB
388 | 
389 | Adjust based on your needs:
390 | ```yaml
391 | services:
392 |   n8n-mcp:
393 |     deploy:
394 |       resources:
395 |         limits:
396 |           memory: 1G
397 |         reservations:
398 |           memory: 512M
399 | ```
400 | 
401 | ### Volume Management
402 | 
403 | ```bash
404 | # List volumes
405 | docker volume ls | grep n8n-mcp
406 | 
407 | # Inspect volume
408 | docker volume inspect n8n-mcp-data
409 | 
410 | # Backup data
411 | docker run --rm \
412 |   -v n8n-mcp-data:/source:ro \
413 |   -v $(pwd):/backup \
414 |   alpine tar czf /backup/n8n-mcp-backup.tar.gz -C /source .
415 | 
416 | # Restore data
417 | docker run --rm \
418 |   -v n8n-mcp-data:/target \
419 |   -v $(pwd):/backup:ro \
420 |   alpine tar xzf /backup/n8n-mcp-backup.tar.gz -C /target
421 | ```
422 | 
423 | ### Custom Database Path (v2.7.16+)
424 | 
425 | You can specify a custom database location using `NODE_DB_PATH`:
426 | 
427 | ```bash
428 | # Use custom path within mounted volume
429 | docker run -d \
430 |   --name n8n-mcp \
431 |   -e MCP_MODE=http \
432 |   -e AUTH_TOKEN=your-token \
433 |   -e NODE_DB_PATH=/app/data/custom/my-nodes.db \
434 |   -v n8n-mcp-data:/app/data \
435 |   -p 3000:3000 \
436 |   ghcr.io/czlonkowski/n8n-mcp:latest
437 | ```
438 | 
439 | **Important Notes:**
440 | - The path must end with `.db`
441 | - For data persistence, ensure the path is within a mounted volume
442 | - Paths outside mounted volumes will be lost on container restart
443 | - The directory will be created automatically if it doesn't exist
444 | 
445 | ## 🐛 Troubleshooting
446 | 
447 | ### Common Issues
448 | 
449 | #### Container Exits Immediately
450 | ```bash
451 | # Check logs
452 | docker logs n8n-mcp
453 | 
454 | # Common causes:
455 | # - Missing AUTH_TOKEN in HTTP mode
456 | # - Database initialization failure
457 | # - Port already in use
458 | ```
459 | 
460 | #### Database Not Initialized
461 | ```bash
462 | # Manually initialize database
463 | docker exec n8n-mcp node dist/scripts/rebuild.js
464 | 
465 | # Or recreate container with fresh volume
466 | docker compose down -v
467 | docker compose up -d
468 | ```
469 | 
470 | #### Permission Errors
471 | ```bash
472 | # Fix volume permissions
473 | docker exec n8n-mcp chown -R nodejs:nodejs /app/data
474 | ```
475 | 
476 | ### Debug Mode
477 | 
478 | Enable debug logging:
479 | ```bash
480 | docker run -d \
481 |   --name n8n-mcp-debug \
482 |   -e MCP_MODE=http \
483 |   -e AUTH_TOKEN=test \
484 |   -e LOG_LEVEL=debug \
485 |   -p 3000:3000 \
486 |   ghcr.io/czlonkowski/n8n-mcp:latest
487 | ```
488 | 
489 | ### Container Shell Access
490 | 
491 | ```bash
492 | # Access running container
493 | docker exec -it n8n-mcp sh
494 | 
495 | # Run as root for debugging
496 | docker exec -it -u root n8n-mcp sh
497 | ```
498 | 
499 | ## 🚀 Production Deployment
500 | 
501 | ### Recommended Setup
502 | 
503 | 1. **Use Docker Compose** for easier management
504 | 2. **Enable HTTPS** with reverse proxy
505 | 3. **Set up monitoring** (Prometheus, Grafana)
506 | 4. **Configure backups** for the data volume
507 | 5. **Use secrets management** for AUTH_TOKEN
508 | 
509 | ### Example Production Stack
510 | 
511 | ```yaml
512 | # docker-compose.prod.yml
513 | services:
514 |   n8n-mcp:
515 |     image: ghcr.io/czlonkowski/n8n-mcp:latest
516 |     restart: always
517 |     environment:
518 |       MCP_MODE: http
519 |       AUTH_TOKEN_FILE: /run/secrets/auth_token
520 |       NODE_ENV: production
521 |     secrets:
522 |       - auth_token
523 |     networks:
524 |       - internal
525 |     deploy:
526 |       resources:
527 |         limits:
528 |           memory: 1G
529 |         reservations:
530 |           memory: 512M
531 |   
532 |   nginx:
533 |     image: nginx:alpine
534 |     restart: always
535 |     ports:
536 |       - "443:443"
537 |     volumes:
538 |       - ./nginx.conf:/etc/nginx/nginx.conf:ro
539 |       - ./certs:/etc/nginx/certs:ro
540 |     networks:
541 |       - internal
542 |       - external
543 | 
544 | networks:
545 |   internal:
546 |   external:
547 | 
548 | secrets:
549 |   auth_token:
550 |     file: ./secrets/auth_token.txt
551 | ```
552 | 
553 | ## 📦 Available Images
554 | 
555 | - `ghcr.io/czlonkowski/n8n-mcp:latest` - Latest stable release
556 | - `ghcr.io/czlonkowski/n8n-mcp:2.3.0` - Specific version
557 | - `ghcr.io/czlonkowski/n8n-mcp:main-abc123` - Development builds
558 | 
559 | ### Image Details
560 | 
561 | - Base: `node:22-alpine`
562 | - Size: ~280MB compressed
563 | - Features: Pre-built database with all node information
564 | - Database: Complete SQLite with 525+ nodes
565 | - Architectures: `linux/amd64`, `linux/arm64`
566 | - Updated: Automatically via GitHub Actions
567 | 
568 | ## 🔄 Updates and Maintenance
569 | 
570 | ### Updating
571 | 
572 | ```bash
573 | # Pull latest image
574 | docker compose pull
575 | 
576 | # Recreate container
577 | docker compose up -d
578 | 
579 | # View update logs
580 | docker compose logs -f
581 | ```
582 | 
583 | ### Automatic Updates (Watchtower)
584 | 
585 | ```yaml
586 | # Add to docker-compose.yml
587 | services:
588 |   watchtower:
589 |     image: containrrr/watchtower
590 |     volumes:
591 |       - /var/run/docker.sock:/var/run/docker.sock
592 |     command: --interval 86400 n8n-mcp
593 | ```
594 | 
595 | ## 📚 Additional Resources
596 | 
597 | - [Main Documentation](./docs/README.md)
598 | - [HTTP Deployment Guide](./docs/HTTP_DEPLOYMENT.md)
599 | - [Troubleshooting Guide](./docs/TROUBLESHOOTING.md)
600 | - [Installation Guide](./docs/INSTALLATION.md)
601 | 
602 | ## 🤝 Support
603 | 
604 | - Issues: [GitHub Issues](https://github.com/czlonkowski/n8n-mcp/issues)
605 | - Discussions: [GitHub Discussions](https://github.com/czlonkowski/n8n-mcp/discussions)
606 | 
607 | ---
608 | 
609 | *Last updated: July 2025 - Docker implementation v1.1*
```
Page 19/59FirstPrevNextLast