#
tokens: 46482/50000 10/617 files (page 18/59)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 18 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/monitoring/cache-metrics.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Unit tests for cache metrics monitoring functionality
  3 |  */
  4 | 
  5 | import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
  6 | import {
  7 |   getInstanceCacheMetrics,
  8 |   getN8nApiClient,
  9 |   clearInstanceCache
 10 | } from '../../../src/mcp/handlers-n8n-manager';
 11 | import {
 12 |   cacheMetrics,
 13 |   getCacheStatistics
 14 | } from '../../../src/utils/cache-utils';
 15 | import { InstanceContext } from '../../../src/types/instance-context';
 16 | 
 17 | // Mock the N8nApiClient
 18 | vi.mock('../../../src/clients/n8n-api-client', () => ({
 19 |   N8nApiClient: vi.fn().mockImplementation((config) => ({
 20 |     config,
 21 |     getWorkflows: vi.fn().mockResolvedValue([]),
 22 |     getWorkflow: vi.fn().mockResolvedValue({}),
 23 |     isConnected: vi.fn().mockReturnValue(true)
 24 |   }))
 25 | }));
 26 | 
 27 | // Mock logger to reduce noise in tests
 28 | vi.mock('../../../src/utils/logger', () => {
 29 |   const mockLogger = {
 30 |     debug: vi.fn(),
 31 |     info: vi.fn(),
 32 |     warn: vi.fn(),
 33 |     error: vi.fn()
 34 |   };
 35 | 
 36 |   return {
 37 |     Logger: vi.fn().mockImplementation(() => mockLogger),
 38 |     logger: mockLogger
 39 |   };
 40 | });
 41 | 
 42 | describe('Cache Metrics Monitoring', () => {
 43 |   beforeEach(() => {
 44 |     // Clear cache before each test
 45 |     clearInstanceCache();
 46 |     cacheMetrics.reset();
 47 | 
 48 |     // Reset environment variables
 49 |     delete process.env.N8N_API_URL;
 50 |     delete process.env.N8N_API_KEY;
 51 |     delete process.env.INSTANCE_CACHE_MAX;
 52 |     delete process.env.INSTANCE_CACHE_TTL_MINUTES;
 53 |   });
 54 | 
 55 |   afterEach(() => {
 56 |     vi.clearAllMocks();
 57 |   });
 58 | 
 59 |   describe('getInstanceCacheStatistics', () => {
 60 |     it('should return initial statistics', () => {
 61 |       const stats = getInstanceCacheMetrics();
 62 | 
 63 |       expect(stats).toBeDefined();
 64 |       expect(stats.hits).toBe(0);
 65 |       expect(stats.misses).toBe(0);
 66 |       expect(stats.size).toBe(0);
 67 |       expect(stats.avgHitRate).toBe(0);
 68 |     });
 69 | 
 70 |     it('should track cache hits and misses', () => {
 71 |       const context1: InstanceContext = {
 72 |         n8nApiUrl: 'https://api1.n8n.cloud',
 73 |         n8nApiKey: 'key1',
 74 |         instanceId: 'instance1'
 75 |       };
 76 | 
 77 |       const context2: InstanceContext = {
 78 |         n8nApiUrl: 'https://api2.n8n.cloud',
 79 |         n8nApiKey: 'key2',
 80 |         instanceId: 'instance2'
 81 |       };
 82 | 
 83 |       // First access - cache miss
 84 |       getN8nApiClient(context1);
 85 |       let stats = getInstanceCacheMetrics();
 86 |       expect(stats.misses).toBe(1);
 87 |       expect(stats.hits).toBe(0);
 88 |       expect(stats.size).toBe(1);
 89 | 
 90 |       // Second access same context - cache hit
 91 |       getN8nApiClient(context1);
 92 |       stats = getInstanceCacheMetrics();
 93 |       expect(stats.hits).toBe(1);
 94 |       expect(stats.misses).toBe(1);
 95 |       expect(stats.avgHitRate).toBe(0.5); // 1 hit / 2 total
 96 | 
 97 |       // Third access different context - cache miss
 98 |       getN8nApiClient(context2);
 99 |       stats = getInstanceCacheMetrics();
100 |       expect(stats.hits).toBe(1);
101 |       expect(stats.misses).toBe(2);
102 |       expect(stats.size).toBe(2);
103 |       expect(stats.avgHitRate).toBeCloseTo(0.333, 2); // 1 hit / 3 total
104 |     });
105 | 
106 |     it('should track evictions when cache is full', () => {
107 |       // Note: Cache is created with default size (100), so we need many items to trigger evictions
108 |       // This test verifies that eviction tracking works, even if we don't hit the limit in practice
109 |       const initialStats = getInstanceCacheMetrics();
110 | 
111 |       // The cache dispose callback should track evictions when items are removed
112 |       // For this test, we'll verify the eviction tracking mechanism exists
113 |       expect(initialStats.evictions).toBeGreaterThanOrEqual(0);
114 | 
115 |       // Add a few items to cache
116 |       const contexts = [
117 |         { n8nApiUrl: 'https://api1.n8n.cloud', n8nApiKey: 'key1' },
118 |         { n8nApiUrl: 'https://api2.n8n.cloud', n8nApiKey: 'key2' },
119 |         { n8nApiUrl: 'https://api3.n8n.cloud', n8nApiKey: 'key3' }
120 |       ];
121 | 
122 |       contexts.forEach(ctx => getN8nApiClient(ctx));
123 | 
124 |       const stats = getInstanceCacheMetrics();
125 |       expect(stats.size).toBe(3); // All items should fit in default cache (max: 100)
126 |     });
127 | 
128 |     it('should track cache operations over time', () => {
129 |       const context: InstanceContext = {
130 |         n8nApiUrl: 'https://api.n8n.cloud',
131 |         n8nApiKey: 'test-key'
132 |       };
133 | 
134 |       // Simulate multiple operations
135 |       for (let i = 0; i < 10; i++) {
136 |         getN8nApiClient(context);
137 |       }
138 | 
139 |       const stats = getInstanceCacheMetrics();
140 |       expect(stats.hits).toBe(9); // First is miss, rest are hits
141 |       expect(stats.misses).toBe(1);
142 |       expect(stats.avgHitRate).toBe(0.9); // 9/10
143 |       expect(stats.sets).toBeGreaterThanOrEqual(1);
144 |     });
145 | 
146 |     it('should include timestamp information', () => {
147 |       const stats = getInstanceCacheMetrics();
148 | 
149 |       expect(stats.createdAt).toBeInstanceOf(Date);
150 |       expect(stats.lastResetAt).toBeInstanceOf(Date);
151 |       expect(stats.createdAt.getTime()).toBeLessThanOrEqual(Date.now());
152 |     });
153 | 
154 |     it('should track cache clear operations', () => {
155 |       const context: InstanceContext = {
156 |         n8nApiUrl: 'https://api.n8n.cloud',
157 |         n8nApiKey: 'test-key'
158 |       };
159 | 
160 |       // Add some clients
161 |       getN8nApiClient(context);
162 | 
163 |       // Clear cache
164 |       clearInstanceCache();
165 | 
166 |       const stats = getInstanceCacheMetrics();
167 |       expect(stats.clears).toBe(1);
168 |       expect(stats.size).toBe(0);
169 |     });
170 |   });
171 | 
172 |   describe('Cache Metrics with Different Scenarios', () => {
173 |     it('should handle rapid successive requests', () => {
174 |       const context: InstanceContext = {
175 |         n8nApiUrl: 'https://api.n8n.cloud',
176 |         n8nApiKey: 'rapid-test'
177 |       };
178 | 
179 |       // Simulate rapid requests
180 |       const promises = [];
181 |       for (let i = 0; i < 50; i++) {
182 |         promises.push(Promise.resolve(getN8nApiClient(context)));
183 |       }
184 | 
185 |       return Promise.all(promises).then(() => {
186 |         const stats = getInstanceCacheMetrics();
187 |         expect(stats.hits).toBe(49); // First is miss
188 |         expect(stats.misses).toBe(1);
189 |         expect(stats.avgHitRate).toBe(0.98); // 49/50
190 |       });
191 |     });
192 | 
193 |     it('should track metrics for fallback to environment variables', () => {
194 |       // Note: Singleton mode (no context) doesn't use the instance cache
195 |       // This test verifies that cache metrics are not affected by singleton usage
196 |       const initialStats = getInstanceCacheMetrics();
197 | 
198 |       process.env.N8N_API_URL = 'https://env.n8n.cloud';
199 |       process.env.N8N_API_KEY = 'env-key';
200 | 
201 |       // Calls without context use singleton mode (no cache metrics)
202 |       getN8nApiClient();
203 |       getN8nApiClient();
204 | 
205 |       const stats = getInstanceCacheMetrics();
206 |       expect(stats.hits).toBe(initialStats.hits);
207 |       expect(stats.misses).toBe(initialStats.misses);
208 |     });
209 | 
210 |     it('should maintain separate metrics for different instances', () => {
211 |       const contexts = Array.from({ length: 5 }, (_, i) => ({
212 |         n8nApiUrl: `https://api${i}.n8n.cloud`,
213 |         n8nApiKey: `key${i}`,
214 |         instanceId: `instance${i}`
215 |       }));
216 | 
217 |       // Access each instance twice
218 |       contexts.forEach(ctx => {
219 |         getN8nApiClient(ctx); // Miss
220 |         getN8nApiClient(ctx); // Hit
221 |       });
222 | 
223 |       const stats = getInstanceCacheMetrics();
224 |       expect(stats.hits).toBe(5);
225 |       expect(stats.misses).toBe(5);
226 |       expect(stats.size).toBe(5);
227 |       expect(stats.avgHitRate).toBe(0.5);
228 |     });
229 | 
230 |     it('should handle cache with TTL expiration', () => {
231 |       // Note: TTL configuration is set when cache is created, not dynamically
232 |       // This test verifies that TTL-related cache behavior can be tracked
233 |       const context: InstanceContext = {
234 |         n8nApiUrl: 'https://ttl-test.n8n.cloud',
235 |         n8nApiKey: 'ttl-key'
236 |       };
237 | 
238 |       // First access - miss
239 |       getN8nApiClient(context);
240 | 
241 |       // Second access - hit (within TTL)
242 |       getN8nApiClient(context);
243 | 
244 |       const stats = getInstanceCacheMetrics();
245 |       expect(stats.hits).toBe(1);
246 |       expect(stats.misses).toBe(1);
247 |     });
248 |   });
249 | 
250 |   describe('getCacheStatistics (formatted)', () => {
251 |     it('should return human-readable statistics', () => {
252 |       const context: InstanceContext = {
253 |         n8nApiUrl: 'https://api.n8n.cloud',
254 |         n8nApiKey: 'test-key'
255 |       };
256 | 
257 |       // Generate some activity
258 |       getN8nApiClient(context);
259 |       getN8nApiClient(context);
260 |       getN8nApiClient({ ...context, instanceId: 'different' });
261 | 
262 |       const formattedStats = getCacheStatistics();
263 | 
264 |       expect(formattedStats).toContain('Cache Statistics:');
265 |       expect(formattedStats).toContain('Runtime:');
266 |       expect(formattedStats).toContain('Total Operations:');
267 |       expect(formattedStats).toContain('Hit Rate:');
268 |       expect(formattedStats).toContain('Current Size:');
269 |       expect(formattedStats).toContain('Total Evictions:');
270 |     });
271 | 
272 |     it('should show runtime in minutes', () => {
273 |       const stats = getCacheStatistics();
274 |       expect(stats).toMatch(/Runtime: \d+ minutes/);
275 |     });
276 | 
277 |     it('should show operation counts', () => {
278 |       const context: InstanceContext = {
279 |         n8nApiUrl: 'https://api.n8n.cloud',
280 |         n8nApiKey: 'test-key'
281 |       };
282 | 
283 |       // Generate operations
284 |       getN8nApiClient(context); // Set
285 |       getN8nApiClient(context); // Hit
286 |       clearInstanceCache(); // Clear
287 | 
288 |       const stats = getCacheStatistics();
289 |       expect(stats).toContain('Sets: 1');
290 |       expect(stats).toContain('Clears: 1');
291 |     });
292 |   });
293 | 
294 |   describe('Monitoring Performance Impact', () => {
295 |     it('should have minimal performance overhead', () => {
296 |       const context: InstanceContext = {
297 |         n8nApiUrl: 'https://perf-test.n8n.cloud',
298 |         n8nApiKey: 'perf-key'
299 |       };
300 | 
301 |       const startTime = performance.now();
302 | 
303 |       // Perform many operations
304 |       for (let i = 0; i < 1000; i++) {
305 |         getN8nApiClient(context);
306 |       }
307 | 
308 |       const endTime = performance.now();
309 |       const totalTime = endTime - startTime;
310 | 
311 |       // Should complete quickly (< 100ms for 1000 operations)
312 |       expect(totalTime).toBeLessThan(100);
313 | 
314 |       // Verify metrics were tracked
315 |       const stats = getInstanceCacheMetrics();
316 |       expect(stats.hits).toBe(999);
317 |       expect(stats.misses).toBe(1);
318 |     });
319 | 
320 |     it('should handle concurrent metric updates', async () => {
321 |       const contexts = Array.from({ length: 10 }, (_, i) => ({
322 |         n8nApiUrl: `https://concurrent${i}.n8n.cloud`,
323 |         n8nApiKey: `key${i}`
324 |       }));
325 | 
326 |       // Concurrent requests
327 |       const promises = contexts.map(ctx =>
328 |         Promise.resolve(getN8nApiClient(ctx))
329 |       );
330 | 
331 |       await Promise.all(promises);
332 | 
333 |       const stats = getInstanceCacheMetrics();
334 |       expect(stats.misses).toBe(10);
335 |       expect(stats.size).toBe(10);
336 |     });
337 |   });
338 | 
339 |   describe('Edge Cases and Error Conditions', () => {
340 |     it('should handle metrics when cache operations fail', () => {
341 |       const invalidContext = {
342 |         n8nApiUrl: '',
343 |         n8nApiKey: ''
344 |       } as InstanceContext;
345 | 
346 |       // This should fail validation but metrics should still work
347 |       const client = getN8nApiClient(invalidContext);
348 |       expect(client).toBeNull();
349 | 
350 |       // Metrics should not be affected by validation failures
351 |       const stats = getInstanceCacheMetrics();
352 |       expect(stats).toBeDefined();
353 |     });
354 | 
355 |     it('should maintain metrics integrity after reset', () => {
356 |       const context: InstanceContext = {
357 |         n8nApiUrl: 'https://reset-test.n8n.cloud',
358 |         n8nApiKey: 'reset-key'
359 |       };
360 | 
361 |       // Generate some metrics
362 |       getN8nApiClient(context);
363 |       getN8nApiClient(context);
364 | 
365 |       // Reset metrics
366 |       cacheMetrics.reset();
367 | 
368 |       // New operations should start fresh
369 |       getN8nApiClient(context);
370 |       const stats = getInstanceCacheMetrics();
371 | 
372 |       expect(stats.hits).toBe(1); // Cache still has item from before reset
373 |       expect(stats.misses).toBe(0);
374 |       expect(stats.lastResetAt.getTime()).toBeGreaterThan(stats.createdAt.getTime());
375 |     });
376 | 
377 |     it('should handle maximum cache size correctly', () => {
378 |       // Note: Cache uses default configuration (max: 100) since it's created at module load
379 |       const contexts = Array.from({ length: 5 }, (_, i) => ({
380 |         n8nApiUrl: `https://max${i}.n8n.cloud`,
381 |         n8nApiKey: `key${i}`
382 |       }));
383 | 
384 |       // Add items within default cache size
385 |       contexts.forEach(ctx => getN8nApiClient(ctx));
386 | 
387 |       const stats = getInstanceCacheMetrics();
388 |       expect(stats.size).toBe(5); // Should fit in default cache
389 |       expect(stats.maxSize).toBe(100); // Default max size
390 |     });
391 |   });
392 | });
```

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

```typescript
  1 | import { describe, it, expect, beforeEach, vi } from 'vitest';
  2 | import { NodeRepository } from '../../../src/database/node-repository';
  3 | import { DatabaseAdapter, PreparedStatement, RunResult } from '../../../src/database/database-adapter';
  4 | import { ParsedNode } from '../../../src/parsers/node-parser';
  5 | 
  6 | // Create a complete mock for DatabaseAdapter
  7 | class MockDatabaseAdapter implements DatabaseAdapter {
  8 |   private statements = new Map<string, MockPreparedStatement>();
  9 |   private mockData = new Map<string, any>();
 10 |   
 11 |   prepare = vi.fn((sql: string) => {
 12 |     if (!this.statements.has(sql)) {
 13 |       this.statements.set(sql, new MockPreparedStatement(sql, this.mockData));
 14 |     }
 15 |     return this.statements.get(sql)!;
 16 |   });
 17 |   
 18 |   exec = vi.fn();
 19 |   close = vi.fn();
 20 |   pragma = vi.fn();
 21 |   transaction = vi.fn((fn: () => any) => fn());
 22 |   checkFTS5Support = vi.fn(() => true);
 23 |   inTransaction = false;
 24 |   
 25 |   // Test helper to set mock data
 26 |   _setMockData(key: string, value: any) {
 27 |     this.mockData.set(key, value);
 28 |   }
 29 |   
 30 |   // Test helper to get statement by SQL
 31 |   _getStatement(sql: string) {
 32 |     return this.statements.get(sql);
 33 |   }
 34 | }
 35 | 
 36 | class MockPreparedStatement implements PreparedStatement {
 37 |   run = vi.fn((...params: any[]): RunResult => ({ changes: 1, lastInsertRowid: 1 }));
 38 |   get = vi.fn();
 39 |   all = vi.fn(() => []);
 40 |   iterate = vi.fn();
 41 |   pluck = vi.fn(() => this);
 42 |   expand = vi.fn(() => this);
 43 |   raw = vi.fn(() => this);
 44 |   columns = vi.fn(() => []);
 45 |   bind = vi.fn(() => this);
 46 |   
 47 |   constructor(private sql: string, private mockData: Map<string, any>) {
 48 |     // Configure get() based on SQL pattern
 49 |     if (sql.includes('SELECT * FROM nodes WHERE node_type = ?')) {
 50 |       this.get = vi.fn((nodeType: string) => this.mockData.get(`node:${nodeType}`));
 51 |     }
 52 |     
 53 |     // Configure all() for getAITools
 54 |     if (sql.includes('WHERE is_ai_tool = 1')) {
 55 |       this.all = vi.fn(() => this.mockData.get('ai_tools') || []);
 56 |     }
 57 |   }
 58 | }
 59 | 
 60 | describe('NodeRepository - Core Functionality', () => {
 61 |   let repository: NodeRepository;
 62 |   let mockAdapter: MockDatabaseAdapter;
 63 |   
 64 |   beforeEach(() => {
 65 |     mockAdapter = new MockDatabaseAdapter();
 66 |     repository = new NodeRepository(mockAdapter);
 67 |   });
 68 |   
 69 |   describe('saveNode', () => {
 70 |     it('should save a node with proper JSON serialization', () => {
 71 |       const parsedNode: ParsedNode = {
 72 |         nodeType: 'nodes-base.httpRequest',
 73 |         displayName: 'HTTP Request',
 74 |         description: 'Makes HTTP requests',
 75 |         category: 'transform',
 76 |         style: 'declarative',
 77 |         packageName: 'n8n-nodes-base',
 78 |         properties: [{ name: 'url', type: 'string' }],
 79 |         operations: [{ name: 'execute', displayName: 'Execute' }],
 80 |         credentials: [{ name: 'httpBasicAuth' }],
 81 |         isAITool: false,
 82 |         isTrigger: false,
 83 |         isWebhook: false,
 84 |         isVersioned: true,
 85 |         version: '1.0',
 86 |         documentation: 'HTTP Request documentation',
 87 |         outputs: undefined,
 88 |         outputNames: undefined
 89 |       };
 90 |       
 91 |       repository.saveNode(parsedNode);
 92 |       
 93 |       // Verify prepare was called with correct SQL
 94 |       expect(mockAdapter.prepare).toHaveBeenCalledWith(expect.stringContaining('INSERT OR REPLACE INTO nodes'));
 95 |       
 96 |       // Get the prepared statement and verify run was called
 97 |       const stmt = mockAdapter._getStatement(mockAdapter.prepare.mock.lastCall?.[0] || '');
 98 |       expect(stmt?.run).toHaveBeenCalledWith(
 99 |         'nodes-base.httpRequest',
100 |         'n8n-nodes-base',
101 |         'HTTP Request',
102 |         'Makes HTTP requests',
103 |         'transform',
104 |         'declarative',
105 |         0, // isAITool
106 |         0, // isTrigger
107 |         0, // isWebhook
108 |         1, // isVersioned
109 |         '1.0',
110 |         'HTTP Request documentation',
111 |         JSON.stringify([{ name: 'url', type: 'string' }], null, 2),
112 |         JSON.stringify([{ name: 'execute', displayName: 'Execute' }], null, 2),
113 |         JSON.stringify([{ name: 'httpBasicAuth' }], null, 2),
114 |         null, // outputs
115 |         null  // outputNames
116 |       );
117 |     });
118 |     
119 |     it('should handle nodes without optional fields', () => {
120 |       const minimalNode: ParsedNode = {
121 |         nodeType: 'nodes-base.simple',
122 |         displayName: 'Simple Node',
123 |         category: 'core',
124 |         style: 'programmatic',
125 |         packageName: 'n8n-nodes-base',
126 |         properties: [],
127 |         operations: [],
128 |         credentials: [],
129 |         isAITool: true,
130 |         isTrigger: true,
131 |         isWebhook: true,
132 |         isVersioned: false,
133 |         outputs: undefined,
134 |         outputNames: undefined
135 |       };
136 |       
137 |       repository.saveNode(minimalNode);
138 |       
139 |       const stmt = mockAdapter._getStatement(mockAdapter.prepare.mock.lastCall?.[0] || '');
140 |       const runCall = stmt?.run.mock.lastCall;
141 |       
142 |       expect(runCall?.[2]).toBe('Simple Node'); // displayName
143 |       expect(runCall?.[3]).toBeUndefined(); // description
144 |       expect(runCall?.[10]).toBeUndefined(); // version
145 |       expect(runCall?.[11]).toBeNull(); // documentation
146 |     });
147 |   });
148 |   
149 |   describe('getNode', () => {
150 |     it('should retrieve and deserialize a node correctly', () => {
151 |       const mockRow = {
152 |         node_type: 'nodes-base.httpRequest',
153 |         display_name: 'HTTP Request',
154 |         description: 'Makes HTTP requests',
155 |         category: 'transform',
156 |         development_style: 'declarative',
157 |         package_name: 'n8n-nodes-base',
158 |         is_ai_tool: 0,
159 |         is_trigger: 0,
160 |         is_webhook: 0,
161 |         is_versioned: 1,
162 |         version: '1.0',
163 |         properties_schema: JSON.stringify([{ name: 'url', type: 'string' }]),
164 |         operations: JSON.stringify([{ name: 'execute' }]),
165 |         credentials_required: JSON.stringify([{ name: 'httpBasicAuth' }]),
166 |         documentation: 'HTTP docs',
167 |         outputs: null,
168 |         output_names: null
169 |       };
170 |       
171 |       mockAdapter._setMockData('node:nodes-base.httpRequest', mockRow);
172 |       
173 |       const result = repository.getNode('nodes-base.httpRequest');
174 |       
175 |       expect(result).toEqual({
176 |         nodeType: 'nodes-base.httpRequest',
177 |         displayName: 'HTTP Request',
178 |         description: 'Makes HTTP requests',
179 |         category: 'transform',
180 |         developmentStyle: 'declarative',
181 |         package: 'n8n-nodes-base',
182 |         isAITool: false,
183 |         isTrigger: false,
184 |         isWebhook: false,
185 |         isVersioned: true,
186 |         version: '1.0',
187 |         properties: [{ name: 'url', type: 'string' }],
188 |         operations: [{ name: 'execute' }],
189 |         credentials: [{ name: 'httpBasicAuth' }],
190 |         hasDocumentation: true,
191 |         outputs: null,
192 |         outputNames: null
193 |       });
194 |     });
195 |     
196 |     it('should return null for non-existent nodes', () => {
197 |       const result = repository.getNode('non-existent');
198 |       expect(result).toBeNull();
199 |     });
200 |     
201 |     it('should handle invalid JSON gracefully', () => {
202 |       const mockRow = {
203 |         node_type: 'nodes-base.broken',
204 |         display_name: 'Broken Node',
205 |         description: 'Node with broken JSON',
206 |         category: 'transform',
207 |         development_style: 'declarative',
208 |         package_name: 'n8n-nodes-base',
209 |         is_ai_tool: 0,
210 |         is_trigger: 0,
211 |         is_webhook: 0,
212 |         is_versioned: 0,
213 |         version: null,
214 |         properties_schema: '{invalid json',
215 |         operations: 'not json at all',
216 |         credentials_required: '{"valid": "json"}',
217 |         documentation: null,
218 |         outputs: null,
219 |         output_names: null
220 |       };
221 |       
222 |       mockAdapter._setMockData('node:nodes-base.broken', mockRow);
223 |       
224 |       const result = repository.getNode('nodes-base.broken');
225 |       
226 |       expect(result?.properties).toEqual([]); // defaultValue from safeJsonParse
227 |       expect(result?.operations).toEqual([]); // defaultValue from safeJsonParse
228 |       expect(result?.credentials).toEqual({ valid: 'json' }); // successfully parsed
229 |     });
230 |   });
231 |   
232 |   describe('getAITools', () => {
233 |     it('should retrieve all AI tools sorted by display name', () => {
234 |       const mockAITools = [
235 |         {
236 |           node_type: 'nodes-base.openai',
237 |           display_name: 'OpenAI',
238 |           description: 'OpenAI integration',
239 |           package_name: 'n8n-nodes-base'
240 |         },
241 |         {
242 |           node_type: 'nodes-base.agent',
243 |           display_name: 'AI Agent',
244 |           description: 'AI Agent node',
245 |           package_name: '@n8n/n8n-nodes-langchain'
246 |         }
247 |       ];
248 |       
249 |       mockAdapter._setMockData('ai_tools', mockAITools);
250 |       
251 |       const result = repository.getAITools();
252 |       
253 |       expect(result).toEqual([
254 |         {
255 |           nodeType: 'nodes-base.openai',
256 |           displayName: 'OpenAI',
257 |           description: 'OpenAI integration',
258 |           package: 'n8n-nodes-base'
259 |         },
260 |         {
261 |           nodeType: 'nodes-base.agent',
262 |           displayName: 'AI Agent',
263 |           description: 'AI Agent node',
264 |           package: '@n8n/n8n-nodes-langchain'
265 |         }
266 |       ]);
267 |     });
268 |     
269 |     it('should return empty array when no AI tools exist', () => {
270 |       mockAdapter._setMockData('ai_tools', []);
271 |       
272 |       const result = repository.getAITools();
273 |       
274 |       expect(result).toEqual([]);
275 |     });
276 |   });
277 |   
278 |   describe('safeJsonParse', () => {
279 |     it('should parse valid JSON', () => {
280 |       // Access private method through the class
281 |       const parseMethod = (repository as any).safeJsonParse.bind(repository);
282 |       
283 |       const validJson = '{"key": "value", "number": 42}';
284 |       const result = parseMethod(validJson, {});
285 |       
286 |       expect(result).toEqual({ key: 'value', number: 42 });
287 |     });
288 |     
289 |     it('should return default value for invalid JSON', () => {
290 |       const parseMethod = (repository as any).safeJsonParse.bind(repository);
291 |       
292 |       const invalidJson = '{invalid json}';
293 |       const defaultValue = { default: true };
294 |       const result = parseMethod(invalidJson, defaultValue);
295 |       
296 |       expect(result).toEqual(defaultValue);
297 |     });
298 |     
299 |     it('should handle empty strings', () => {
300 |       const parseMethod = (repository as any).safeJsonParse.bind(repository);
301 |       
302 |       const result = parseMethod('', []);
303 |       expect(result).toEqual([]);
304 |     });
305 |     
306 |     it('should handle null and undefined', () => {
307 |       const parseMethod = (repository as any).safeJsonParse.bind(repository);
308 |       
309 |       // JSON.parse(null) returns null, not an error
310 |       expect(parseMethod(null, 'default')).toBe(null);
311 |       expect(parseMethod(undefined, 'default')).toBe('default');
312 |     });
313 |   });
314 |   
315 |   describe('Edge Cases', () => {
316 |     it('should handle very large JSON properties', () => {
317 |       const largeProperties = Array(1000).fill(null).map((_, i) => ({
318 |         name: `prop${i}`,
319 |         type: 'string',
320 |         description: 'A'.repeat(100)
321 |       }));
322 |       
323 |       const node: ParsedNode = {
324 |         nodeType: 'nodes-base.large',
325 |         displayName: 'Large Node',
326 |         category: 'test',
327 |         style: 'declarative',
328 |         packageName: 'test',
329 |         properties: largeProperties,
330 |         operations: [],
331 |         credentials: [],
332 |         isAITool: false,
333 |         isTrigger: false,
334 |         isWebhook: false,
335 |         isVersioned: false,
336 |         outputs: undefined,
337 |         outputNames: undefined
338 |       };
339 |       
340 |       repository.saveNode(node);
341 |       
342 |       const stmt = mockAdapter._getStatement(mockAdapter.prepare.mock.lastCall?.[0] || '');
343 |       const runCall = stmt?.run.mock.lastCall;
344 |       const savedProperties = runCall?.[12];
345 |       
346 |       expect(savedProperties).toBe(JSON.stringify(largeProperties, null, 2));
347 |     });
348 |     
349 |     it('should handle boolean conversion for integer fields', () => {
350 |       const mockRow = {
351 |         node_type: 'nodes-base.bool-test',
352 |         display_name: 'Bool Test',
353 |         description: 'Testing boolean conversion',
354 |         category: 'test',
355 |         development_style: 'declarative',
356 |         package_name: 'test',
357 |         is_ai_tool: 1,
358 |         is_trigger: 0,
359 |         is_webhook: '1', // String that should be converted
360 |         is_versioned: '0', // String that should be converted
361 |         version: null,
362 |         properties_schema: '[]',
363 |         operations: '[]',
364 |         credentials_required: '[]',
365 |         documentation: null,
366 |         outputs: null,
367 |         output_names: null
368 |       };
369 |       
370 |       mockAdapter._setMockData('node:nodes-base.bool-test', mockRow);
371 |       
372 |       const result = repository.getNode('nodes-base.bool-test');
373 |       
374 |       expect(result?.isAITool).toBe(true);
375 |       expect(result?.isTrigger).toBe(false);
376 |       expect(result?.isWebhook).toBe(true);
377 |       expect(result?.isVersioned).toBe(false);
378 |     });
379 |   });
380 | });
```

--------------------------------------------------------------------------------
/tests/integration/ai-validation/ai-tool-validation.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Integration Tests: AI Tool Validation
  3 |  *
  4 |  * Tests AI tool node validation against real n8n instance.
  5 |  * Covers HTTP Request Tool, Code Tool, Vector Store Tool, Workflow Tool, Calculator Tool.
  6 |  */
  7 | 
  8 | import { describe, it, expect, beforeEach, afterEach, afterAll } from 'vitest';
  9 | import { createTestContext, TestContext, createTestWorkflowName } from '../n8n-api/utils/test-context';
 10 | import { getTestN8nClient } from '../n8n-api/utils/n8n-client';
 11 | import { N8nApiClient } from '../../../src/services/n8n-api-client';
 12 | import { cleanupOrphanedWorkflows } from '../n8n-api/utils/cleanup-helpers';
 13 | import { createMcpContext } from '../n8n-api/utils/mcp-context';
 14 | import { InstanceContext } from '../../../src/types/instance-context';
 15 | import { handleValidateWorkflow } from '../../../src/mcp/handlers-n8n-manager';
 16 | import { getNodeRepository, closeNodeRepository } from '../n8n-api/utils/node-repository';
 17 | import { NodeRepository } from '../../../src/database/node-repository';
 18 | import { ValidationResponse } from '../n8n-api/types/mcp-responses';
 19 | import {
 20 |   createHTTPRequestToolNode,
 21 |   createCodeToolNode,
 22 |   createVectorStoreToolNode,
 23 |   createWorkflowToolNode,
 24 |   createCalculatorToolNode,
 25 |   createAIWorkflow
 26 | } from './helpers';
 27 | 
 28 | describe('Integration: AI Tool Validation', () => {
 29 |   let context: TestContext;
 30 |   let client: N8nApiClient;
 31 |   let mcpContext: InstanceContext;
 32 |   let repository: NodeRepository;
 33 | 
 34 |   beforeEach(async () => {
 35 |     context = createTestContext();
 36 |     client = getTestN8nClient();
 37 |     mcpContext = createMcpContext();
 38 |     repository = await getNodeRepository();
 39 |   });
 40 | 
 41 |   afterEach(async () => {
 42 |     await context.cleanup();
 43 |   });
 44 | 
 45 |   afterAll(async () => {
 46 |     await closeNodeRepository();
 47 |     if (!process.env.CI) {
 48 |       await cleanupOrphanedWorkflows();
 49 |     }
 50 |   });
 51 | 
 52 |   // ======================================================================
 53 |   // HTTP Request Tool Tests
 54 |   // ======================================================================
 55 | 
 56 |   describe('HTTP Request Tool', () => {
 57 |     it('should detect missing toolDescription', async () => {
 58 |       const httpTool = createHTTPRequestToolNode({
 59 |         name: 'HTTP Request Tool',
 60 |         toolDescription: '', // Missing
 61 |         url: 'https://api.example.com/data',
 62 |         method: 'GET'
 63 |       });
 64 | 
 65 |       const workflow = createAIWorkflow(
 66 |         [httpTool],
 67 |         {},
 68 |         {
 69 |           name: createTestWorkflowName('HTTP Tool - No Description'),
 70 |           tags: ['mcp-integration-test', 'ai-validation']
 71 |         }
 72 |       );
 73 | 
 74 |       const created = await client.createWorkflow(workflow);
 75 |       context.trackWorkflow(created.id!);
 76 | 
 77 |       const response = await handleValidateWorkflow(
 78 |         { id: created.id },
 79 |         repository,
 80 |         mcpContext
 81 |       );
 82 | 
 83 |       expect(response.success).toBe(true);
 84 |       const data = response.data as ValidationResponse;
 85 | 
 86 |       expect(data.valid).toBe(false);
 87 |       expect(data.errors).toBeDefined();
 88 | 
 89 |       const errorCodes = data.errors!.map(e => e.details?.code || e.code);
 90 |       expect(errorCodes).toContain('MISSING_TOOL_DESCRIPTION');
 91 |     });
 92 | 
 93 |     it('should detect missing URL', async () => {
 94 |       const httpTool = createHTTPRequestToolNode({
 95 |         name: 'HTTP Request Tool',
 96 |         toolDescription: 'Fetches data from API',
 97 |         url: '', // Missing
 98 |         method: 'GET'
 99 |       });
100 | 
101 |       const workflow = createAIWorkflow(
102 |         [httpTool],
103 |         {},
104 |         {
105 |           name: createTestWorkflowName('HTTP Tool - No URL'),
106 |           tags: ['mcp-integration-test', 'ai-validation']
107 |         }
108 |       );
109 | 
110 |       const created = await client.createWorkflow(workflow);
111 |       context.trackWorkflow(created.id!);
112 | 
113 |       const response = await handleValidateWorkflow(
114 |         { id: created.id },
115 |         repository,
116 |         mcpContext
117 |       );
118 | 
119 |       expect(response.success).toBe(true);
120 |       const data = response.data as ValidationResponse;
121 | 
122 |       expect(data.valid).toBe(false);
123 |       expect(data.errors).toBeDefined();
124 | 
125 |       const errorCodes = data.errors!.map(e => e.details?.code || e.code);
126 |       expect(errorCodes).toContain('MISSING_URL');
127 |     });
128 | 
129 |     it('should validate valid HTTP Request Tool', async () => {
130 |       const httpTool = createHTTPRequestToolNode({
131 |         name: 'HTTP Request Tool',
132 |         toolDescription: 'Fetches weather data from the weather API',
133 |         url: 'https://api.weather.com/current',
134 |         method: 'GET'
135 |       });
136 | 
137 |       const workflow = createAIWorkflow(
138 |         [httpTool],
139 |         {},
140 |         {
141 |           name: createTestWorkflowName('HTTP Tool - Valid'),
142 |           tags: ['mcp-integration-test', 'ai-validation']
143 |         }
144 |       );
145 | 
146 |       const created = await client.createWorkflow(workflow);
147 |       context.trackWorkflow(created.id!);
148 | 
149 |       const response = await handleValidateWorkflow(
150 |         { id: created.id },
151 |         repository,
152 |         mcpContext
153 |       );
154 | 
155 |       expect(response.success).toBe(true);
156 |       const data = response.data as ValidationResponse;
157 | 
158 |       expect(data.valid).toBe(true);
159 |       expect(data.errors).toBeUndefined();
160 |     });
161 |   });
162 | 
163 |   // ======================================================================
164 |   // Code Tool Tests
165 |   // ======================================================================
166 | 
167 |   describe('Code Tool', () => {
168 |     it('should detect missing code', async () => {
169 |       const codeTool = createCodeToolNode({
170 |         name: 'Code Tool',
171 |         toolDescription: 'Processes data with custom logic',
172 |         code: '' // Missing
173 |       });
174 | 
175 |       const workflow = createAIWorkflow(
176 |         [codeTool],
177 |         {},
178 |         {
179 |           name: createTestWorkflowName('Code Tool - No Code'),
180 |           tags: ['mcp-integration-test', 'ai-validation']
181 |         }
182 |       );
183 | 
184 |       const created = await client.createWorkflow(workflow);
185 |       context.trackWorkflow(created.id!);
186 | 
187 |       const response = await handleValidateWorkflow(
188 |         { id: created.id },
189 |         repository,
190 |         mcpContext
191 |       );
192 | 
193 |       expect(response.success).toBe(true);
194 |       const data = response.data as ValidationResponse;
195 | 
196 |       expect(data.valid).toBe(false);
197 |       expect(data.errors).toBeDefined();
198 | 
199 |       const errorCodes = data.errors!.map(e => e.details?.code || e.code);
200 |       expect(errorCodes).toContain('MISSING_CODE');
201 |     });
202 | 
203 |     it('should validate valid Code Tool', async () => {
204 |       const codeTool = createCodeToolNode({
205 |         name: 'Code Tool',
206 |         toolDescription: 'Calculates the sum of two numbers',
207 |         code: 'return { sum: Number(a) + Number(b) };'
208 |       });
209 | 
210 |       const workflow = createAIWorkflow(
211 |         [codeTool],
212 |         {},
213 |         {
214 |           name: createTestWorkflowName('Code Tool - Valid'),
215 |           tags: ['mcp-integration-test', 'ai-validation']
216 |         }
217 |       );
218 | 
219 |       const created = await client.createWorkflow(workflow);
220 |       context.trackWorkflow(created.id!);
221 | 
222 |       const response = await handleValidateWorkflow(
223 |         { id: created.id },
224 |         repository,
225 |         mcpContext
226 |       );
227 | 
228 |       expect(response.success).toBe(true);
229 |       const data = response.data as ValidationResponse;
230 | 
231 |       expect(data.valid).toBe(true);
232 |       expect(data.errors).toBeUndefined();
233 |     });
234 |   });
235 | 
236 |   // ======================================================================
237 |   // Vector Store Tool Tests
238 |   // ======================================================================
239 | 
240 |   describe('Vector Store Tool', () => {
241 |     it('should detect missing toolDescription', async () => {
242 |       const vectorTool = createVectorStoreToolNode({
243 |         name: 'Vector Store Tool',
244 |         toolDescription: '' // Missing
245 |       });
246 | 
247 |       const workflow = createAIWorkflow(
248 |         [vectorTool],
249 |         {},
250 |         {
251 |           name: createTestWorkflowName('Vector Tool - No Description'),
252 |           tags: ['mcp-integration-test', 'ai-validation']
253 |         }
254 |       );
255 | 
256 |       const created = await client.createWorkflow(workflow);
257 |       context.trackWorkflow(created.id!);
258 | 
259 |       const response = await handleValidateWorkflow(
260 |         { id: created.id },
261 |         repository,
262 |         mcpContext
263 |       );
264 | 
265 |       expect(response.success).toBe(true);
266 |       const data = response.data as ValidationResponse;
267 | 
268 |       expect(data.valid).toBe(false);
269 |       expect(data.errors).toBeDefined();
270 | 
271 |       const errorCodes = data.errors!.map(e => e.details?.code || e.code);
272 |       expect(errorCodes).toContain('MISSING_TOOL_DESCRIPTION');
273 |     });
274 | 
275 |     it('should validate valid Vector Store Tool', async () => {
276 |       const vectorTool = createVectorStoreToolNode({
277 |         name: 'Vector Store Tool',
278 |         toolDescription: 'Searches documentation in vector database'
279 |       });
280 | 
281 |       const workflow = createAIWorkflow(
282 |         [vectorTool],
283 |         {},
284 |         {
285 |           name: createTestWorkflowName('Vector Tool - Valid'),
286 |           tags: ['mcp-integration-test', 'ai-validation']
287 |         }
288 |       );
289 | 
290 |       const created = await client.createWorkflow(workflow);
291 |       context.trackWorkflow(created.id!);
292 | 
293 |       const response = await handleValidateWorkflow(
294 |         { id: created.id },
295 |         repository,
296 |         mcpContext
297 |       );
298 | 
299 |       expect(response.success).toBe(true);
300 |       const data = response.data as ValidationResponse;
301 | 
302 |       expect(data.valid).toBe(true);
303 |       expect(data.errors).toBeUndefined();
304 |     });
305 |   });
306 | 
307 |   // ======================================================================
308 |   // Workflow Tool Tests
309 |   // ======================================================================
310 | 
311 |   describe('Workflow Tool', () => {
312 |     it('should detect missing workflowId', async () => {
313 |       const workflowTool = createWorkflowToolNode({
314 |         name: 'Workflow Tool',
315 |         toolDescription: 'Executes a sub-workflow',
316 |         workflowId: '' // Missing
317 |       });
318 | 
319 |       const workflow = createAIWorkflow(
320 |         [workflowTool],
321 |         {},
322 |         {
323 |           name: createTestWorkflowName('Workflow Tool - No ID'),
324 |           tags: ['mcp-integration-test', 'ai-validation']
325 |         }
326 |       );
327 | 
328 |       const created = await client.createWorkflow(workflow);
329 |       context.trackWorkflow(created.id!);
330 | 
331 |       const response = await handleValidateWorkflow(
332 |         { id: created.id },
333 |         repository,
334 |         mcpContext
335 |       );
336 | 
337 |       expect(response.success).toBe(true);
338 |       const data = response.data as ValidationResponse;
339 | 
340 |       expect(data.valid).toBe(false);
341 |       expect(data.errors).toBeDefined();
342 | 
343 |       const errorCodes = data.errors!.map(e => e.details?.code || e.code);
344 |       expect(errorCodes).toContain('MISSING_WORKFLOW_ID');
345 |     });
346 | 
347 |     it('should validate valid Workflow Tool', async () => {
348 |       const workflowTool = createWorkflowToolNode({
349 |         name: 'Workflow Tool',
350 |         toolDescription: 'Processes customer data through validation workflow',
351 |         workflowId: '123'
352 |       });
353 | 
354 |       const workflow = createAIWorkflow(
355 |         [workflowTool],
356 |         {},
357 |         {
358 |           name: createTestWorkflowName('Workflow Tool - Valid'),
359 |           tags: ['mcp-integration-test', 'ai-validation']
360 |         }
361 |       );
362 | 
363 |       const created = await client.createWorkflow(workflow);
364 |       context.trackWorkflow(created.id!);
365 | 
366 |       const response = await handleValidateWorkflow(
367 |         { id: created.id },
368 |         repository,
369 |         mcpContext
370 |       );
371 | 
372 |       expect(response.success).toBe(true);
373 |       const data = response.data as ValidationResponse;
374 | 
375 |       expect(data.valid).toBe(true);
376 |       expect(data.errors).toBeUndefined();
377 |     });
378 |   });
379 | 
380 |   // ======================================================================
381 |   // Calculator Tool Tests
382 |   // ======================================================================
383 | 
384 |   describe('Calculator Tool', () => {
385 |     it('should validate Calculator Tool (no configuration needed)', async () => {
386 |       const calcTool = createCalculatorToolNode({
387 |         name: 'Calculator'
388 |       });
389 | 
390 |       const workflow = createAIWorkflow(
391 |         [calcTool],
392 |         {},
393 |         {
394 |           name: createTestWorkflowName('Calculator Tool - Valid'),
395 |           tags: ['mcp-integration-test', 'ai-validation']
396 |         }
397 |       );
398 | 
399 |       const created = await client.createWorkflow(workflow);
400 |       context.trackWorkflow(created.id!);
401 | 
402 |       const response = await handleValidateWorkflow(
403 |         { id: created.id },
404 |         repository,
405 |         mcpContext
406 |       );
407 | 
408 |       expect(response.success).toBe(true);
409 |       const data = response.data as ValidationResponse;
410 | 
411 |       // Calculator has no required configuration
412 |       expect(data.valid).toBe(true);
413 |       expect(data.errors).toBeUndefined();
414 |     });
415 |   });
416 | });
417 | 
```

--------------------------------------------------------------------------------
/tests/unit/services/workflow-auto-fixer.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, it, expect, vi, beforeEach } from 'vitest';
  2 | import { WorkflowAutoFixer, isNodeFormatIssue } from '@/services/workflow-auto-fixer';
  3 | import { NodeRepository } from '@/database/node-repository';
  4 | import type { WorkflowValidationResult } from '@/services/workflow-validator';
  5 | import type { ExpressionFormatIssue } from '@/services/expression-format-validator';
  6 | import type { Workflow, WorkflowNode } from '@/types/n8n-api';
  7 | 
  8 | vi.mock('@/database/node-repository');
  9 | vi.mock('@/services/node-similarity-service');
 10 | 
 11 | describe('WorkflowAutoFixer', () => {
 12 |   let autoFixer: WorkflowAutoFixer;
 13 |   let mockRepository: NodeRepository;
 14 | 
 15 |   const createMockWorkflow = (nodes: WorkflowNode[]): Workflow => ({
 16 |     id: 'test-workflow',
 17 |     name: 'Test Workflow',
 18 |     active: false,
 19 |     nodes,
 20 |     connections: {},
 21 |     settings: {},
 22 |     createdAt: '',
 23 |     updatedAt: ''
 24 |   });
 25 | 
 26 |   const createMockNode = (id: string, type: string, parameters: any = {}): WorkflowNode => ({
 27 |     id,
 28 |     name: id,
 29 |     type,
 30 |     typeVersion: 1,
 31 |     position: [0, 0],
 32 |     parameters
 33 |   });
 34 | 
 35 |   beforeEach(() => {
 36 |     vi.clearAllMocks();
 37 |     mockRepository = new NodeRepository({} as any);
 38 |     autoFixer = new WorkflowAutoFixer(mockRepository);
 39 |   });
 40 | 
 41 |   describe('Type Guards', () => {
 42 |     it('should identify NodeFormatIssue correctly', () => {
 43 |       const validIssue: ExpressionFormatIssue = {
 44 |         fieldPath: 'url',
 45 |         currentValue: '{{ $json.url }}',
 46 |         correctedValue: '={{ $json.url }}',
 47 |         issueType: 'missing-prefix',
 48 |         severity: 'error',
 49 |         explanation: 'Missing = prefix'
 50 |       } as any;
 51 |       (validIssue as any).nodeName = 'httpRequest';
 52 |       (validIssue as any).nodeId = 'node-1';
 53 | 
 54 |       const invalidIssue: ExpressionFormatIssue = {
 55 |         fieldPath: 'url',
 56 |         currentValue: '{{ $json.url }}',
 57 |         correctedValue: '={{ $json.url }}',
 58 |         issueType: 'missing-prefix',
 59 |         severity: 'error',
 60 |         explanation: 'Missing = prefix'
 61 |       };
 62 | 
 63 |       expect(isNodeFormatIssue(validIssue)).toBe(true);
 64 |       expect(isNodeFormatIssue(invalidIssue)).toBe(false);
 65 |     });
 66 |   });
 67 | 
 68 |   describe('Expression Format Fixes', () => {
 69 |     it('should fix missing prefix in expressions', () => {
 70 |       const workflow = createMockWorkflow([
 71 |         createMockNode('node-1', 'nodes-base.httpRequest', {
 72 |           url: '{{ $json.url }}',
 73 |           method: 'GET'
 74 |         })
 75 |       ]);
 76 | 
 77 |       const formatIssues: ExpressionFormatIssue[] = [{
 78 |         fieldPath: 'url',
 79 |         currentValue: '{{ $json.url }}',
 80 |         correctedValue: '={{ $json.url }}',
 81 |         issueType: 'missing-prefix',
 82 |         severity: 'error',
 83 |         explanation: 'Expression must start with =',
 84 |         nodeName: 'node-1',
 85 |         nodeId: 'node-1'
 86 |       } as any];
 87 | 
 88 |       const validationResult: WorkflowValidationResult = {
 89 |         valid: false,
 90 |         errors: [],
 91 |         warnings: [],
 92 |         statistics: {
 93 |           totalNodes: 1,
 94 |           enabledNodes: 1,
 95 |           triggerNodes: 0,
 96 |           validConnections: 0,
 97 |           invalidConnections: 0,
 98 |           expressionsValidated: 0
 99 |         },
100 |         suggestions: []
101 |       };
102 | 
103 |       const result = autoFixer.generateFixes(workflow, validationResult, formatIssues);
104 | 
105 |       expect(result.fixes).toHaveLength(1);
106 |       expect(result.fixes[0].type).toBe('expression-format');
107 |       expect(result.fixes[0].before).toBe('{{ $json.url }}');
108 |       expect(result.fixes[0].after).toBe('={{ $json.url }}');
109 |       expect(result.fixes[0].confidence).toBe('high');
110 | 
111 |       expect(result.operations).toHaveLength(1);
112 |       expect(result.operations[0].type).toBe('updateNode');
113 |     });
114 | 
115 |     it('should handle multiple expression fixes in same node', () => {
116 |       const workflow = createMockWorkflow([
117 |         createMockNode('node-1', 'nodes-base.httpRequest', {
118 |           url: '{{ $json.url }}',
119 |           body: '{{ $json.body }}'
120 |         })
121 |       ]);
122 | 
123 |       const formatIssues: ExpressionFormatIssue[] = [
124 |         {
125 |           fieldPath: 'url',
126 |           currentValue: '{{ $json.url }}',
127 |           correctedValue: '={{ $json.url }}',
128 |           issueType: 'missing-prefix',
129 |           severity: 'error',
130 |           explanation: 'Expression must start with =',
131 |           nodeName: 'node-1',
132 |           nodeId: 'node-1'
133 |         } as any,
134 |         {
135 |           fieldPath: 'body',
136 |           currentValue: '{{ $json.body }}',
137 |           correctedValue: '={{ $json.body }}',
138 |           issueType: 'missing-prefix',
139 |           severity: 'error',
140 |           explanation: 'Expression must start with =',
141 |           nodeName: 'node-1',
142 |           nodeId: 'node-1'
143 |         } as any
144 |       ];
145 | 
146 |       const validationResult: WorkflowValidationResult = {
147 |         valid: false,
148 |         errors: [],
149 |         warnings: [],
150 |         statistics: {
151 |           totalNodes: 1,
152 |           enabledNodes: 1,
153 |           triggerNodes: 0,
154 |           validConnections: 0,
155 |           invalidConnections: 0,
156 |           expressionsValidated: 0
157 |         },
158 |         suggestions: []
159 |       };
160 | 
161 |       const result = autoFixer.generateFixes(workflow, validationResult, formatIssues);
162 | 
163 |       expect(result.fixes).toHaveLength(2);
164 |       expect(result.operations).toHaveLength(1); // Single update operation for the node
165 |     });
166 |   });
167 | 
168 |   describe('TypeVersion Fixes', () => {
169 |     it('should fix typeVersion exceeding maximum', () => {
170 |       const workflow = createMockWorkflow([
171 |         createMockNode('node-1', 'nodes-base.httpRequest', {})
172 |       ]);
173 | 
174 |       const validationResult: WorkflowValidationResult = {
175 |         valid: false,
176 |         errors: [{
177 |           type: 'error',
178 |           nodeId: 'node-1',
179 |           nodeName: 'node-1',
180 |           message: 'typeVersion 3.5 exceeds maximum supported version 2.0'
181 |         }],
182 |         warnings: [],
183 |         statistics: {
184 |           totalNodes: 1,
185 |           enabledNodes: 1,
186 |           triggerNodes: 0,
187 |           validConnections: 0,
188 |           invalidConnections: 0,
189 |           expressionsValidated: 0
190 |         },
191 |         suggestions: []
192 |       };
193 | 
194 |       const result = autoFixer.generateFixes(workflow, validationResult, []);
195 | 
196 |       expect(result.fixes).toHaveLength(1);
197 |       expect(result.fixes[0].type).toBe('typeversion-correction');
198 |       expect(result.fixes[0].before).toBe(3.5);
199 |       expect(result.fixes[0].after).toBe(2);
200 |       expect(result.fixes[0].confidence).toBe('medium');
201 |     });
202 |   });
203 | 
204 |   describe('Error Output Configuration Fixes', () => {
205 |     it('should remove conflicting onError setting', () => {
206 |       const workflow = createMockWorkflow([
207 |         createMockNode('node-1', 'nodes-base.httpRequest', {})
208 |       ]);
209 |       workflow.nodes[0].onError = 'continueErrorOutput';
210 | 
211 |       const validationResult: WorkflowValidationResult = {
212 |         valid: false,
213 |         errors: [{
214 |           type: 'error',
215 |           nodeId: 'node-1',
216 |           nodeName: 'node-1',
217 |           message: "Node has onError: 'continueErrorOutput' but no error output connections"
218 |         }],
219 |         warnings: [],
220 |         statistics: {
221 |           totalNodes: 1,
222 |           enabledNodes: 1,
223 |           triggerNodes: 0,
224 |           validConnections: 0,
225 |           invalidConnections: 0,
226 |           expressionsValidated: 0
227 |         },
228 |         suggestions: []
229 |       };
230 | 
231 |       const result = autoFixer.generateFixes(workflow, validationResult, []);
232 | 
233 |       expect(result.fixes).toHaveLength(1);
234 |       expect(result.fixes[0].type).toBe('error-output-config');
235 |       expect(result.fixes[0].before).toBe('continueErrorOutput');
236 |       expect(result.fixes[0].after).toBeUndefined();
237 |       expect(result.fixes[0].confidence).toBe('medium');
238 |     });
239 |   });
240 | 
241 |   describe('setNestedValue Validation', () => {
242 |     it('should throw error for non-object target', () => {
243 |       expect(() => {
244 |         autoFixer['setNestedValue'](null, ['field'], 'value');
245 |       }).toThrow('Cannot set value on non-object');
246 | 
247 |       expect(() => {
248 |         autoFixer['setNestedValue']('string', ['field'], 'value');
249 |       }).toThrow('Cannot set value on non-object');
250 |     });
251 | 
252 |     it('should throw error for empty path', () => {
253 |       expect(() => {
254 |         autoFixer['setNestedValue']({}, [], 'value');
255 |       }).toThrow('Cannot set value with empty path');
256 |     });
257 | 
258 |     it('should handle nested paths correctly', () => {
259 |       const obj = { level1: { level2: { level3: 'old' } } };
260 |       autoFixer['setNestedValue'](obj, ['level1', 'level2', 'level3'], 'new');
261 |       expect(obj.level1.level2.level3).toBe('new');
262 |     });
263 | 
264 |     it('should create missing nested objects', () => {
265 |       const obj = {};
266 |       autoFixer['setNestedValue'](obj, ['level1', 'level2', 'level3'], 'value');
267 |       expect(obj).toEqual({
268 |         level1: {
269 |           level2: {
270 |             level3: 'value'
271 |           }
272 |         }
273 |       });
274 |     });
275 | 
276 |     it('should handle array indices in paths', () => {
277 |       const obj: any = { items: [] };
278 |       autoFixer['setNestedValue'](obj, ['items[0]', 'name'], 'test');
279 |       expect(obj.items[0].name).toBe('test');
280 |     });
281 | 
282 |     it('should throw error for invalid array notation', () => {
283 |       const obj = {};
284 |       expect(() => {
285 |         autoFixer['setNestedValue'](obj, ['field[abc]'], 'value');
286 |       }).toThrow('Invalid array notation: field[abc]');
287 |     });
288 | 
289 |     it('should throw when trying to traverse non-object', () => {
290 |       const obj = { field: 'string' };
291 |       expect(() => {
292 |         autoFixer['setNestedValue'](obj, ['field', 'nested'], 'value');
293 |       }).toThrow('Cannot traverse through string at field');
294 |     });
295 |   });
296 | 
297 |   describe('Confidence Filtering', () => {
298 |     it('should filter fixes by confidence level', () => {
299 |       const workflow = createMockWorkflow([
300 |         createMockNode('node-1', 'nodes-base.httpRequest', { url: '{{ $json.url }}' })
301 |       ]);
302 | 
303 |       const formatIssues: ExpressionFormatIssue[] = [{
304 |         fieldPath: 'url',
305 |         currentValue: '{{ $json.url }}',
306 |         correctedValue: '={{ $json.url }}',
307 |         issueType: 'missing-prefix',
308 |         severity: 'error',
309 |         explanation: 'Expression must start with =',
310 |         nodeName: 'node-1',
311 |         nodeId: 'node-1'
312 |       } as any];
313 | 
314 |       const validationResult: WorkflowValidationResult = {
315 |         valid: false,
316 |         errors: [],
317 |         warnings: [],
318 |         statistics: {
319 |           totalNodes: 1,
320 |           enabledNodes: 1,
321 |           triggerNodes: 0,
322 |           validConnections: 0,
323 |           invalidConnections: 0,
324 |           expressionsValidated: 0
325 |         },
326 |         suggestions: []
327 |       };
328 | 
329 |       const result = autoFixer.generateFixes(workflow, validationResult, formatIssues, {
330 |         confidenceThreshold: 'low'
331 |       });
332 | 
333 |       expect(result.fixes.length).toBeGreaterThan(0);
334 |       expect(result.fixes.every(f => ['high', 'medium', 'low'].includes(f.confidence))).toBe(true);
335 |     });
336 |   });
337 | 
338 |   describe('Summary Generation', () => {
339 |     it('should generate appropriate summary for fixes', () => {
340 |       const workflow = createMockWorkflow([
341 |         createMockNode('node-1', 'nodes-base.httpRequest', { url: '{{ $json.url }}' })
342 |       ]);
343 | 
344 |       const formatIssues: ExpressionFormatIssue[] = [{
345 |         fieldPath: 'url',
346 |         currentValue: '{{ $json.url }}',
347 |         correctedValue: '={{ $json.url }}',
348 |         issueType: 'missing-prefix',
349 |         severity: 'error',
350 |         explanation: 'Expression must start with =',
351 |         nodeName: 'node-1',
352 |         nodeId: 'node-1'
353 |       } as any];
354 | 
355 |       const validationResult: WorkflowValidationResult = {
356 |         valid: false,
357 |         errors: [],
358 |         warnings: [],
359 |         statistics: {
360 |           totalNodes: 1,
361 |           enabledNodes: 1,
362 |           triggerNodes: 0,
363 |           validConnections: 0,
364 |           invalidConnections: 0,
365 |           expressionsValidated: 0
366 |         },
367 |         suggestions: []
368 |       };
369 | 
370 |       const result = autoFixer.generateFixes(workflow, validationResult, formatIssues);
371 | 
372 |       expect(result.summary).toContain('expression format');
373 |       expect(result.stats.total).toBe(1);
374 |       expect(result.stats.byType['expression-format']).toBe(1);
375 |     });
376 | 
377 |     it('should handle empty fixes gracefully', () => {
378 |       const workflow = createMockWorkflow([]);
379 |       const validationResult: WorkflowValidationResult = {
380 |         valid: true,
381 |         errors: [],
382 |         warnings: [],
383 |         statistics: {
384 |           totalNodes: 0,
385 |           enabledNodes: 0,
386 |           triggerNodes: 0,
387 |           validConnections: 0,
388 |           invalidConnections: 0,
389 |           expressionsValidated: 0
390 |         },
391 |         suggestions: []
392 |       };
393 | 
394 |       const result = autoFixer.generateFixes(workflow, validationResult, []);
395 | 
396 |       expect(result.summary).toBe('No fixes available');
397 |       expect(result.stats.total).toBe(0);
398 |       expect(result.operations).toEqual([]);
399 |     });
400 |   });
401 | });
```

--------------------------------------------------------------------------------
/tests/unit/utils/node-type-normalizer.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Tests for NodeTypeNormalizer
  3 |  *
  4 |  * Comprehensive test suite for the node type normalization utility
  5 |  * that fixes the critical issue of AI agents producing short-form node types
  6 |  */
  7 | 
  8 | import { describe, it, expect } from 'vitest';
  9 | import { NodeTypeNormalizer } from '../../../src/utils/node-type-normalizer';
 10 | 
 11 | describe('NodeTypeNormalizer', () => {
 12 |   describe('normalizeToFullForm', () => {
 13 |     describe('Base nodes', () => {
 14 |       it('should normalize full base form to short form', () => {
 15 |         expect(NodeTypeNormalizer.normalizeToFullForm('n8n-nodes-base.webhook'))
 16 |           .toBe('nodes-base.webhook');
 17 |       });
 18 | 
 19 |       it('should normalize full base form with different node names', () => {
 20 |         expect(NodeTypeNormalizer.normalizeToFullForm('n8n-nodes-base.httpRequest'))
 21 |           .toBe('nodes-base.httpRequest');
 22 |         expect(NodeTypeNormalizer.normalizeToFullForm('n8n-nodes-base.set'))
 23 |           .toBe('nodes-base.set');
 24 |         expect(NodeTypeNormalizer.normalizeToFullForm('n8n-nodes-base.slack'))
 25 |           .toBe('nodes-base.slack');
 26 |       });
 27 | 
 28 |       it('should leave short base form unchanged', () => {
 29 |         expect(NodeTypeNormalizer.normalizeToFullForm('nodes-base.webhook'))
 30 |           .toBe('nodes-base.webhook');
 31 |         expect(NodeTypeNormalizer.normalizeToFullForm('nodes-base.httpRequest'))
 32 |           .toBe('nodes-base.httpRequest');
 33 |       });
 34 |     });
 35 | 
 36 |     describe('LangChain nodes', () => {
 37 |       it('should normalize full langchain form to short form', () => {
 38 |         expect(NodeTypeNormalizer.normalizeToFullForm('@n8n/n8n-nodes-langchain.agent'))
 39 |           .toBe('nodes-langchain.agent');
 40 |         expect(NodeTypeNormalizer.normalizeToFullForm('@n8n/n8n-nodes-langchain.openAi'))
 41 |           .toBe('nodes-langchain.openAi');
 42 |       });
 43 | 
 44 |       it('should normalize langchain form with n8n- prefix but missing @n8n/', () => {
 45 |         expect(NodeTypeNormalizer.normalizeToFullForm('n8n-nodes-langchain.agent'))
 46 |           .toBe('nodes-langchain.agent');
 47 |       });
 48 | 
 49 |       it('should leave short langchain form unchanged', () => {
 50 |         expect(NodeTypeNormalizer.normalizeToFullForm('nodes-langchain.agent'))
 51 |           .toBe('nodes-langchain.agent');
 52 |         expect(NodeTypeNormalizer.normalizeToFullForm('nodes-langchain.openAi'))
 53 |           .toBe('nodes-langchain.openAi');
 54 |       });
 55 |     });
 56 | 
 57 |     describe('Edge cases', () => {
 58 |       it('should handle empty string', () => {
 59 |         expect(NodeTypeNormalizer.normalizeToFullForm('')).toBe('');
 60 |       });
 61 | 
 62 |       it('should handle null', () => {
 63 |         expect(NodeTypeNormalizer.normalizeToFullForm(null as any)).toBe(null);
 64 |       });
 65 | 
 66 |       it('should handle undefined', () => {
 67 |         expect(NodeTypeNormalizer.normalizeToFullForm(undefined as any)).toBe(undefined);
 68 |       });
 69 | 
 70 |       it('should handle non-string input', () => {
 71 |         expect(NodeTypeNormalizer.normalizeToFullForm(123 as any)).toBe(123);
 72 |         expect(NodeTypeNormalizer.normalizeToFullForm({} as any)).toEqual({});
 73 |       });
 74 | 
 75 |       it('should leave community nodes unchanged', () => {
 76 |         expect(NodeTypeNormalizer.normalizeToFullForm('custom-package.myNode'))
 77 |           .toBe('custom-package.myNode');
 78 |       });
 79 | 
 80 |       it('should leave nodes without prefixes unchanged', () => {
 81 |         expect(NodeTypeNormalizer.normalizeToFullForm('someRandomNode'))
 82 |           .toBe('someRandomNode');
 83 |       });
 84 |     });
 85 |   });
 86 | 
 87 |   describe('normalizeWithDetails', () => {
 88 |     it('should return normalization details for full base form', () => {
 89 |       const result = NodeTypeNormalizer.normalizeWithDetails('n8n-nodes-base.webhook');
 90 | 
 91 |       expect(result).toEqual({
 92 |         original: 'n8n-nodes-base.webhook',
 93 |         normalized: 'nodes-base.webhook',
 94 |         wasNormalized: true,
 95 |         package: 'base'
 96 |       });
 97 |     });
 98 | 
 99 |     it('should return normalization details for already short form', () => {
100 |       const result = NodeTypeNormalizer.normalizeWithDetails('nodes-base.webhook');
101 | 
102 |       expect(result).toEqual({
103 |         original: 'nodes-base.webhook',
104 |         normalized: 'nodes-base.webhook',
105 |         wasNormalized: false,
106 |         package: 'base'
107 |       });
108 |     });
109 | 
110 |     it('should detect langchain package', () => {
111 |       const result = NodeTypeNormalizer.normalizeWithDetails('@n8n/n8n-nodes-langchain.agent');
112 | 
113 |       expect(result).toEqual({
114 |         original: '@n8n/n8n-nodes-langchain.agent',
115 |         normalized: 'nodes-langchain.agent',
116 |         wasNormalized: true,
117 |         package: 'langchain'
118 |       });
119 |     });
120 | 
121 |     it('should detect community package', () => {
122 |       const result = NodeTypeNormalizer.normalizeWithDetails('custom-package.myNode');
123 | 
124 |       expect(result).toEqual({
125 |         original: 'custom-package.myNode',
126 |         normalized: 'custom-package.myNode',
127 |         wasNormalized: false,
128 |         package: 'community'
129 |       });
130 |     });
131 | 
132 |     it('should detect unknown package', () => {
133 |       const result = NodeTypeNormalizer.normalizeWithDetails('unknownNode');
134 | 
135 |       expect(result).toEqual({
136 |         original: 'unknownNode',
137 |         normalized: 'unknownNode',
138 |         wasNormalized: false,
139 |         package: 'unknown'
140 |       });
141 |     });
142 |   });
143 | 
144 |   describe('normalizeBatch', () => {
145 |     it('should normalize multiple node types', () => {
146 |       const types = ['n8n-nodes-base.webhook', 'n8n-nodes-base.set', '@n8n/n8n-nodes-langchain.agent'];
147 |       const result = NodeTypeNormalizer.normalizeBatch(types);
148 | 
149 |       expect(result.size).toBe(3);
150 |       expect(result.get('n8n-nodes-base.webhook')).toBe('nodes-base.webhook');
151 |       expect(result.get('n8n-nodes-base.set')).toBe('nodes-base.set');
152 |       expect(result.get('@n8n/n8n-nodes-langchain.agent')).toBe('nodes-langchain.agent');
153 |     });
154 | 
155 |     it('should handle empty array', () => {
156 |       const result = NodeTypeNormalizer.normalizeBatch([]);
157 |       expect(result.size).toBe(0);
158 |     });
159 | 
160 |     it('should handle mixed forms', () => {
161 |       const types = [
162 |         'n8n-nodes-base.webhook',
163 |         'nodes-base.set',
164 |         '@n8n/n8n-nodes-langchain.agent',
165 |         'nodes-langchain.openAi'
166 |       ];
167 |       const result = NodeTypeNormalizer.normalizeBatch(types);
168 | 
169 |       expect(result.size).toBe(4);
170 |       expect(result.get('n8n-nodes-base.webhook')).toBe('nodes-base.webhook');
171 |       expect(result.get('nodes-base.set')).toBe('nodes-base.set');
172 |       expect(result.get('@n8n/n8n-nodes-langchain.agent')).toBe('nodes-langchain.agent');
173 |       expect(result.get('nodes-langchain.openAi')).toBe('nodes-langchain.openAi');
174 |     });
175 |   });
176 | 
177 |   describe('normalizeWorkflowNodeTypes', () => {
178 |     it('should normalize all nodes in workflow', () => {
179 |       const workflow = {
180 |         nodes: [
181 |           { type: 'n8n-nodes-base.webhook', id: '1', name: 'Webhook', parameters: {}, position: [0, 0] },
182 |           { type: 'n8n-nodes-base.set', id: '2', name: 'Set', parameters: {}, position: [100, 100] }
183 |         ],
184 |         connections: {}
185 |       };
186 | 
187 |       const result = NodeTypeNormalizer.normalizeWorkflowNodeTypes(workflow);
188 | 
189 |       expect(result.nodes[0].type).toBe('nodes-base.webhook');
190 |       expect(result.nodes[1].type).toBe('nodes-base.set');
191 |     });
192 | 
193 |     it('should preserve all other node properties', () => {
194 |       const workflow = {
195 |         nodes: [
196 |           {
197 |             type: 'n8n-nodes-base.webhook',
198 |             id: 'test-id',
199 |             name: 'Test Webhook',
200 |             parameters: { path: '/test' },
201 |             position: [250, 300],
202 |             credentials: { webhookAuth: { id: '1', name: 'Test' } }
203 |           }
204 |         ],
205 |         connections: {}
206 |       };
207 | 
208 |       const result = NodeTypeNormalizer.normalizeWorkflowNodeTypes(workflow);
209 | 
210 |       expect(result.nodes[0]).toEqual({
211 |         type: 'nodes-base.webhook', // normalized to short form
212 |         id: 'test-id', // preserved
213 |         name: 'Test Webhook', // preserved
214 |         parameters: { path: '/test' }, // preserved
215 |         position: [250, 300], // preserved
216 |         credentials: { webhookAuth: { id: '1', name: 'Test' } } // preserved
217 |       });
218 |     });
219 | 
220 |     it('should preserve workflow properties', () => {
221 |       const workflow = {
222 |         name: 'Test Workflow',
223 |         active: true,
224 |         nodes: [
225 |           { type: 'n8n-nodes-base.webhook', id: '1', name: 'Webhook', parameters: {}, position: [0, 0] }
226 |         ],
227 |         connections: {
228 |           '1': { main: [[{ node: '2', type: 'main', index: 0 }]] }
229 |         }
230 |       };
231 | 
232 |       const result = NodeTypeNormalizer.normalizeWorkflowNodeTypes(workflow);
233 | 
234 |       expect(result.name).toBe('Test Workflow');
235 |       expect(result.active).toBe(true);
236 |       expect(result.connections).toEqual({
237 |         '1': { main: [[{ node: '2', type: 'main', index: 0 }]] }
238 |       });
239 |     });
240 | 
241 |     it('should handle workflow without nodes', () => {
242 |       const workflow = { connections: {} };
243 |       const result = NodeTypeNormalizer.normalizeWorkflowNodeTypes(workflow);
244 |       expect(result).toEqual(workflow);
245 |     });
246 | 
247 |     it('should handle null workflow', () => {
248 |       const result = NodeTypeNormalizer.normalizeWorkflowNodeTypes(null);
249 |       expect(result).toBe(null);
250 |     });
251 | 
252 |     it('should handle workflow with empty nodes array', () => {
253 |       const workflow = { nodes: [], connections: {} };
254 |       const result = NodeTypeNormalizer.normalizeWorkflowNodeTypes(workflow);
255 |       expect(result.nodes).toEqual([]);
256 |     });
257 |   });
258 | 
259 |   describe('isFullForm', () => {
260 |     it('should return true for full base form', () => {
261 |       expect(NodeTypeNormalizer.isFullForm('n8n-nodes-base.webhook')).toBe(true);
262 |     });
263 | 
264 |     it('should return true for full langchain form', () => {
265 |       expect(NodeTypeNormalizer.isFullForm('@n8n/n8n-nodes-langchain.agent')).toBe(true);
266 |       expect(NodeTypeNormalizer.isFullForm('n8n-nodes-langchain.agent')).toBe(true);
267 |     });
268 | 
269 |     it('should return false for short base form', () => {
270 |       expect(NodeTypeNormalizer.isFullForm('nodes-base.webhook')).toBe(false);
271 |     });
272 | 
273 |     it('should return false for short langchain form', () => {
274 |       expect(NodeTypeNormalizer.isFullForm('nodes-langchain.agent')).toBe(false);
275 |     });
276 | 
277 |     it('should return false for community nodes', () => {
278 |       expect(NodeTypeNormalizer.isFullForm('custom-package.myNode')).toBe(false);
279 |     });
280 | 
281 |     it('should return false for null/undefined', () => {
282 |       expect(NodeTypeNormalizer.isFullForm(null as any)).toBe(false);
283 |       expect(NodeTypeNormalizer.isFullForm(undefined as any)).toBe(false);
284 |     });
285 |   });
286 | 
287 |   describe('isShortForm', () => {
288 |     it('should return true for short base form', () => {
289 |       expect(NodeTypeNormalizer.isShortForm('nodes-base.webhook')).toBe(true);
290 |     });
291 | 
292 |     it('should return true for short langchain form', () => {
293 |       expect(NodeTypeNormalizer.isShortForm('nodes-langchain.agent')).toBe(true);
294 |     });
295 | 
296 |     it('should return false for full base form', () => {
297 |       expect(NodeTypeNormalizer.isShortForm('n8n-nodes-base.webhook')).toBe(false);
298 |     });
299 | 
300 |     it('should return false for full langchain form', () => {
301 |       expect(NodeTypeNormalizer.isShortForm('@n8n/n8n-nodes-langchain.agent')).toBe(false);
302 |       expect(NodeTypeNormalizer.isShortForm('n8n-nodes-langchain.agent')).toBe(false);
303 |     });
304 | 
305 |     it('should return false for community nodes', () => {
306 |       expect(NodeTypeNormalizer.isShortForm('custom-package.myNode')).toBe(false);
307 |     });
308 | 
309 |     it('should return false for null/undefined', () => {
310 |       expect(NodeTypeNormalizer.isShortForm(null as any)).toBe(false);
311 |       expect(NodeTypeNormalizer.isShortForm(undefined as any)).toBe(false);
312 |     });
313 |   });
314 | 
315 |   describe('Integration scenarios', () => {
316 |     it('should handle the critical use case from P0-R1', () => {
317 |       // This is the exact scenario - normalize full form to match database
318 |       const fullFormType = 'n8n-nodes-base.webhook'; // External source produces this
319 |       const normalized = NodeTypeNormalizer.normalizeToFullForm(fullFormType);
320 | 
321 |       expect(normalized).toBe('nodes-base.webhook'); // Database stores in short form
322 |     });
323 | 
324 |     it('should work correctly in a workflow validation scenario', () => {
325 |       const workflow = {
326 |         nodes: [
327 |           { type: 'n8n-nodes-base.webhook', id: '1', name: 'Webhook', parameters: {}, position: [0, 0] },
328 |           { type: 'n8n-nodes-base.httpRequest', id: '2', name: 'HTTP', parameters: {}, position: [200, 0] },
329 |           { type: 'nodes-base.set', id: '3', name: 'Set', parameters: {}, position: [400, 0] }
330 |         ],
331 |         connections: {}
332 |       };
333 | 
334 |       const normalized = NodeTypeNormalizer.normalizeWorkflowNodeTypes(workflow);
335 | 
336 |       // All node types should now be in short form for database lookup
337 |       expect(normalized.nodes.every((n: any) => n.type.startsWith('nodes-base.'))).toBe(true);
338 |     });
339 |   });
340 | });
341 | 
```

--------------------------------------------------------------------------------
/tests/unit/services/workflow-fixed-collection-validation.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Workflow Fixed Collection Validation Tests
  3 |  * Tests that workflow validation catches fixedCollection structure errors at the workflow level
  4 |  */
  5 | 
  6 | import { describe, test, expect, beforeEach, vi } from 'vitest';
  7 | import { WorkflowValidator } from '../../../src/services/workflow-validator';
  8 | import { EnhancedConfigValidator } from '../../../src/services/enhanced-config-validator';
  9 | import { NodeRepository } from '../../../src/database/node-repository';
 10 | 
 11 | describe('Workflow FixedCollection Validation', () => {
 12 |   let validator: WorkflowValidator;
 13 |   let mockNodeRepository: any;
 14 | 
 15 |   beforeEach(() => {
 16 |     // Create mock repository that returns basic node info for common nodes
 17 |     mockNodeRepository = {
 18 |       getNode: vi.fn().mockImplementation((type: string) => {
 19 |         const normalizedType = type.replace('n8n-nodes-base.', '').replace('nodes-base.', '');
 20 |         switch (normalizedType) {
 21 |           case 'webhook':
 22 |             return {
 23 |               nodeType: 'nodes-base.webhook',
 24 |               displayName: 'Webhook',
 25 |               properties: [
 26 |                 { name: 'path', type: 'string', required: true },
 27 |                 { name: 'httpMethod', type: 'options' }
 28 |               ]
 29 |             };
 30 |           case 'switch':
 31 |             return {
 32 |               nodeType: 'nodes-base.switch',
 33 |               displayName: 'Switch',
 34 |               properties: [
 35 |                 { name: 'rules', type: 'fixedCollection', required: true }
 36 |               ]
 37 |             };
 38 |           case 'if':
 39 |             return {
 40 |               nodeType: 'nodes-base.if',
 41 |               displayName: 'If',
 42 |               properties: [
 43 |                 { name: 'conditions', type: 'filter', required: true }
 44 |               ]
 45 |             };
 46 |           case 'filter':
 47 |             return {
 48 |               nodeType: 'nodes-base.filter',
 49 |               displayName: 'Filter',
 50 |               properties: [
 51 |                 { name: 'conditions', type: 'filter', required: true }
 52 |               ]
 53 |             };
 54 |           default:
 55 |             return null;
 56 |         }
 57 |       })
 58 |     };
 59 |     
 60 |     validator = new WorkflowValidator(mockNodeRepository, EnhancedConfigValidator);
 61 |   });
 62 | 
 63 |   test('should catch invalid Switch node structure in workflow validation', async () => {
 64 |     const workflow = {
 65 |       name: 'Test Workflow with Invalid Switch',
 66 |       nodes: [
 67 |         {
 68 |           id: 'webhook',
 69 |           name: 'Webhook',
 70 |           type: 'n8n-nodes-base.webhook',
 71 |           position: [0, 0] as [number, number],
 72 |           parameters: {
 73 |             path: 'test-webhook'
 74 |           }
 75 |         },
 76 |         {
 77 |           id: 'switch',
 78 |           name: 'Switch',
 79 |           type: 'n8n-nodes-base.switch',
 80 |           position: [200, 0] as [number, number],
 81 |           parameters: {
 82 |             // This is the problematic structure that causes "propertyValues[itemName] is not iterable"
 83 |             rules: {
 84 |               conditions: {
 85 |                 values: [
 86 |                   {
 87 |                     value1: '={{$json.status}}',
 88 |                     operation: 'equals',
 89 |                     value2: 'active'
 90 |                   }
 91 |                 ]
 92 |               }
 93 |             }
 94 |           }
 95 |         }
 96 |       ],
 97 |       connections: {
 98 |         Webhook: {
 99 |           main: [[{ node: 'Switch', type: 'main', index: 0 }]]
100 |         }
101 |       }
102 |     };
103 | 
104 |     const result = await validator.validateWorkflow(workflow, {
105 |       validateNodes: true,
106 |       profile: 'ai-friendly'
107 |     });
108 | 
109 |     expect(result.valid).toBe(false);
110 |     expect(result.errors).toHaveLength(1);
111 |     
112 |     const switchError = result.errors.find(e => e.nodeId === 'switch');
113 |     expect(switchError).toBeDefined();
114 |     expect(switchError!.message).toContain('propertyValues[itemName] is not iterable');
115 |     expect(switchError!.message).toContain('Invalid structure for nodes-base.switch node');
116 |   });
117 | 
118 |   test('should catch invalid If node structure in workflow validation', async () => {
119 |     const workflow = {
120 |       name: 'Test Workflow with Invalid If',
121 |       nodes: [
122 |         {
123 |           id: 'webhook',
124 |           name: 'Webhook',
125 |           type: 'n8n-nodes-base.webhook',
126 |           position: [0, 0] as [number, number],
127 |           parameters: {
128 |             path: 'test-webhook'
129 |           }
130 |         },
131 |         {
132 |           id: 'if',
133 |           name: 'If',
134 |           type: 'n8n-nodes-base.if',
135 |           position: [200, 0] as [number, number],
136 |           parameters: {
137 |             // This is the problematic structure
138 |             conditions: {
139 |               values: [
140 |                 {
141 |                   value1: '={{$json.age}}',
142 |                   operation: 'largerEqual',
143 |                   value2: 18
144 |                 }
145 |               ]
146 |             }
147 |           }
148 |         }
149 |       ],
150 |       connections: {
151 |         Webhook: {
152 |           main: [[{ node: 'If', type: 'main', index: 0 }]]
153 |         }
154 |       }
155 |     };
156 | 
157 |     const result = await validator.validateWorkflow(workflow, {
158 |       validateNodes: true,
159 |       profile: 'ai-friendly'
160 |     });
161 | 
162 |     expect(result.valid).toBe(false);
163 |     expect(result.errors).toHaveLength(1);
164 |     
165 |     const ifError = result.errors.find(e => e.nodeId === 'if');
166 |     expect(ifError).toBeDefined();
167 |     expect(ifError!.message).toContain('Invalid structure for nodes-base.if node');
168 |   });
169 | 
170 |   test('should accept valid Switch node structure in workflow validation', async () => {
171 |     const workflow = {
172 |       name: 'Test Workflow with Valid Switch',
173 |       nodes: [
174 |         {
175 |           id: 'webhook',
176 |           name: 'Webhook',
177 |           type: 'n8n-nodes-base.webhook',
178 |           position: [0, 0] as [number, number],
179 |           parameters: {
180 |             path: 'test-webhook'
181 |           }
182 |         },
183 |         {
184 |           id: 'switch',
185 |           name: 'Switch',
186 |           type: 'n8n-nodes-base.switch',
187 |           position: [200, 0] as [number, number],
188 |           parameters: {
189 |             // This is the correct structure
190 |             rules: {
191 |               values: [
192 |                 {
193 |                   conditions: {
194 |                     value1: '={{$json.status}}',
195 |                     operation: 'equals',
196 |                     value2: 'active'
197 |                   },
198 |                   outputKey: 'active'
199 |                 }
200 |               ]
201 |             }
202 |           }
203 |         }
204 |       ],
205 |       connections: {
206 |         Webhook: {
207 |           main: [[{ node: 'Switch', type: 'main', index: 0 }]]
208 |         }
209 |       }
210 |     };
211 | 
212 |     const result = await validator.validateWorkflow(workflow, {
213 |       validateNodes: true,
214 |       profile: 'ai-friendly'
215 |     });
216 | 
217 |     // Should not have fixedCollection structure errors
218 |     const hasFixedCollectionError = result.errors.some(e => 
219 |       e.message.includes('propertyValues[itemName] is not iterable')
220 |     );
221 |     expect(hasFixedCollectionError).toBe(false);
222 |   });
223 | 
224 |   test('should catch multiple fixedCollection errors in a single workflow', async () => {
225 |     const workflow = {
226 |       name: 'Test Workflow with Multiple Invalid Structures',
227 |       nodes: [
228 |         {
229 |           id: 'webhook',
230 |           name: 'Webhook',
231 |           type: 'n8n-nodes-base.webhook',
232 |           position: [0, 0] as [number, number],
233 |           parameters: {
234 |             path: 'test-webhook'
235 |           }
236 |         },
237 |         {
238 |           id: 'switch',
239 |           name: 'Switch',
240 |           type: 'n8n-nodes-base.switch',
241 |           position: [200, 0] as [number, number],
242 |           parameters: {
243 |             rules: {
244 |               conditions: {
245 |                 values: [{ value1: 'test', operation: 'equals', value2: 'test' }]
246 |               }
247 |             }
248 |           }
249 |         },
250 |         {
251 |           id: 'if',
252 |           name: 'If',
253 |           type: 'n8n-nodes-base.if',
254 |           position: [400, 0] as [number, number],
255 |           parameters: {
256 |             conditions: {
257 |               values: [{ value1: 'test', operation: 'equals', value2: 'test' }]
258 |             }
259 |           }
260 |         },
261 |         {
262 |           id: 'filter',
263 |           name: 'Filter',
264 |           type: 'n8n-nodes-base.filter',
265 |           position: [600, 0] as [number, number],
266 |           parameters: {
267 |             conditions: {
268 |               values: [{ value1: 'test', operation: 'equals', value2: 'test' }]
269 |             }
270 |           }
271 |         }
272 |       ],
273 |       connections: {
274 |         Webhook: {
275 |           main: [[{ node: 'Switch', type: 'main', index: 0 }]]
276 |         },
277 |         Switch: {
278 |           main: [
279 |             [{ node: 'If', type: 'main', index: 0 }],
280 |             [{ node: 'Filter', type: 'main', index: 0 }]
281 |           ]
282 |         }
283 |       }
284 |     };
285 | 
286 |     const result = await validator.validateWorkflow(workflow, {
287 |       validateNodes: true,
288 |       profile: 'ai-friendly'
289 |     });
290 | 
291 |     expect(result.valid).toBe(false);
292 |     expect(result.errors.length).toBeGreaterThanOrEqual(3); // At least one error for each problematic node
293 |     
294 |     // Check that each problematic node has an error
295 |     const switchError = result.errors.find(e => e.nodeId === 'switch');
296 |     const ifError = result.errors.find(e => e.nodeId === 'if');
297 |     const filterError = result.errors.find(e => e.nodeId === 'filter');
298 |     
299 |     expect(switchError).toBeDefined();
300 |     expect(ifError).toBeDefined();
301 |     expect(filterError).toBeDefined();
302 |   });
303 | 
304 |   test('should provide helpful statistics about fixedCollection errors', async () => {
305 |     const workflow = {
306 |       name: 'Test Workflow Statistics',
307 |       nodes: [
308 |         {
309 |           id: 'webhook',
310 |           name: 'Webhook',
311 |           type: 'n8n-nodes-base.webhook',
312 |           position: [0, 0] as [number, number],
313 |           parameters: { path: 'test' }
314 |         },
315 |         {
316 |           id: 'bad-switch',
317 |           name: 'Bad Switch',
318 |           type: 'n8n-nodes-base.switch',
319 |           position: [200, 0] as [number, number],
320 |           parameters: {
321 |             rules: {
322 |               conditions: { values: [{ value1: 'test', operation: 'equals', value2: 'test' }] }
323 |             }
324 |           }
325 |         },
326 |         {
327 |           id: 'good-switch',
328 |           name: 'Good Switch',
329 |           type: 'n8n-nodes-base.switch',
330 |           position: [400, 0] as [number, number],
331 |           parameters: {
332 |             rules: {
333 |               values: [{ conditions: { value1: 'test', operation: 'equals', value2: 'test' }, outputKey: 'out' }]
334 |             }
335 |           }
336 |         }
337 |       ],
338 |       connections: {
339 |         Webhook: {
340 |           main: [
341 |             [{ node: 'Bad Switch', type: 'main', index: 0 }],
342 |             [{ node: 'Good Switch', type: 'main', index: 0 }]
343 |           ]
344 |         }
345 |       }
346 |     };
347 | 
348 |     const result = await validator.validateWorkflow(workflow, {
349 |       validateNodes: true,
350 |       profile: 'ai-friendly'
351 |     });
352 | 
353 |     expect(result.statistics.totalNodes).toBe(3);
354 |     expect(result.statistics.enabledNodes).toBe(3);
355 |     expect(result.valid).toBe(false); // Should be invalid due to the bad switch
356 |     
357 |     // Should have at least one error for the bad switch
358 |     const badSwitchError = result.errors.find(e => e.nodeId === 'bad-switch');
359 |     expect(badSwitchError).toBeDefined();
360 |     
361 |     // Should not have errors for the good switch or webhook
362 |     const goodSwitchError = result.errors.find(e => e.nodeId === 'good-switch');
363 |     const webhookError = result.errors.find(e => e.nodeId === 'webhook');
364 |     
365 |     // These might have other validation errors, but not fixedCollection errors
366 |     if (goodSwitchError) {
367 |       expect(goodSwitchError.message).not.toContain('propertyValues[itemName] is not iterable');
368 |     }
369 |     if (webhookError) {
370 |       expect(webhookError.message).not.toContain('propertyValues[itemName] is not iterable');
371 |     }
372 |   });
373 | 
374 |   test('should work with different validation profiles', async () => {
375 |     const workflow = {
376 |       name: 'Test Profile Compatibility',
377 |       nodes: [
378 |         {
379 |           id: 'switch',
380 |           name: 'Switch',
381 |           type: 'n8n-nodes-base.switch',
382 |           position: [0, 0] as [number, number],
383 |           parameters: {
384 |             rules: {
385 |               conditions: {
386 |                 values: [{ value1: 'test', operation: 'equals', value2: 'test' }]
387 |               }
388 |             }
389 |           }
390 |         }
391 |       ],
392 |       connections: {}
393 |     };
394 | 
395 |     const profiles: Array<'strict' | 'runtime' | 'ai-friendly' | 'minimal'> = 
396 |       ['strict', 'runtime', 'ai-friendly', 'minimal'];
397 | 
398 |     for (const profile of profiles) {
399 |       const result = await validator.validateWorkflow(workflow, {
400 |         validateNodes: true,
401 |         profile
402 |       });
403 | 
404 |       // All profiles should catch this critical error
405 |       const hasCriticalError = result.errors.some(e => 
406 |         e.message.includes('propertyValues[itemName] is not iterable')
407 |       );
408 |       
409 |       expect(hasCriticalError, `Profile ${profile} should catch critical fixedCollection error`).toBe(true);
410 |       expect(result.valid, `Profile ${profile} should mark workflow as invalid`).toBe(false);
411 |     }
412 |   });
413 | });
```

--------------------------------------------------------------------------------
/tests/unit/services/fixed-collection-validation.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Fixed Collection Validation Tests
  3 |  * Tests for the fix of issue #90: "propertyValues[itemName] is not iterable" error
  4 |  * 
  5 |  * This ensures AI agents cannot create invalid fixedCollection structures that break n8n UI
  6 |  */
  7 | 
  8 | import { describe, test, expect } from 'vitest';
  9 | import { EnhancedConfigValidator } from '../../../src/services/enhanced-config-validator';
 10 | 
 11 | describe('FixedCollection Validation', () => {
 12 |   describe('Switch Node v2/v3 Validation', () => {
 13 |     test('should detect invalid nested conditions structure', () => {
 14 |       const invalidConfig = {
 15 |         rules: {
 16 |           conditions: {
 17 |             values: [
 18 |               {
 19 |                 value1: '={{$json.status}}',
 20 |                 operation: 'equals',
 21 |                 value2: 'active'
 22 |               }
 23 |             ]
 24 |           }
 25 |         }
 26 |       };
 27 | 
 28 |       const result = EnhancedConfigValidator.validateWithMode(
 29 |         'nodes-base.switch',
 30 |         invalidConfig,
 31 |         [],
 32 |         'operation',
 33 |         'ai-friendly'
 34 |       );
 35 | 
 36 |       expect(result.valid).toBe(false);
 37 |       expect(result.errors).toHaveLength(1);
 38 |       expect(result.errors[0].type).toBe('invalid_value');
 39 |       expect(result.errors[0].property).toBe('rules');
 40 |       expect(result.errors[0].message).toContain('propertyValues[itemName] is not iterable');
 41 |       expect(result.errors[0].fix).toContain('{ "rules": { "values": [{ "conditions": {...}, "outputKey": "output1" }] } }');
 42 |     });
 43 | 
 44 |     test('should detect direct conditions in rules (another invalid pattern)', () => {
 45 |       const invalidConfig = {
 46 |         rules: {
 47 |           conditions: {
 48 |             value1: '={{$json.status}}',
 49 |             operation: 'equals',
 50 |             value2: 'active'
 51 |           }
 52 |         }
 53 |       };
 54 | 
 55 |       const result = EnhancedConfigValidator.validateWithMode(
 56 |         'nodes-base.switch',
 57 |         invalidConfig,
 58 |         [],
 59 |         'operation',
 60 |         'ai-friendly'
 61 |       );
 62 | 
 63 |       expect(result.valid).toBe(false);
 64 |       expect(result.errors).toHaveLength(1);
 65 |       expect(result.errors[0].message).toContain('Invalid structure for nodes-base.switch node');
 66 |     });
 67 | 
 68 |     test('should provide auto-fix for invalid switch structure', () => {
 69 |       const invalidConfig = {
 70 |         rules: {
 71 |           conditions: {
 72 |             values: [
 73 |               {
 74 |                 value1: '={{$json.status}}',
 75 |                 operation: 'equals',
 76 |                 value2: 'active'
 77 |               }
 78 |             ]
 79 |           }
 80 |         }
 81 |       };
 82 | 
 83 |       const result = EnhancedConfigValidator.validateWithMode(
 84 |         'nodes-base.switch',
 85 |         invalidConfig,
 86 |         [],
 87 |         'operation',
 88 |         'ai-friendly'
 89 |       );
 90 | 
 91 |       expect(result.autofix).toBeDefined();
 92 |       expect(result.autofix!.rules).toBeDefined();
 93 |       expect(result.autofix!.rules.values).toBeInstanceOf(Array);
 94 |       expect(result.autofix!.rules.values).toHaveLength(1);
 95 |       expect(result.autofix!.rules.values[0]).toHaveProperty('conditions');
 96 |       expect(result.autofix!.rules.values[0]).toHaveProperty('outputKey');
 97 |     });
 98 | 
 99 |     test('should accept valid switch structure', () => {
100 |       const validConfig = {
101 |         rules: {
102 |           values: [
103 |             {
104 |               conditions: {
105 |                 value1: '={{$json.status}}',
106 |                 operation: 'equals',
107 |                 value2: 'active'
108 |               },
109 |               outputKey: 'active'
110 |             }
111 |           ]
112 |         }
113 |       };
114 | 
115 |       const result = EnhancedConfigValidator.validateWithMode(
116 |         'nodes-base.switch',
117 |         validConfig,
118 |         [],
119 |         'operation',
120 |         'ai-friendly'
121 |       );
122 | 
123 |       // Should not have the specific fixedCollection error
124 |       const hasFixedCollectionError = result.errors.some(e => 
125 |         e.message.includes('propertyValues[itemName] is not iterable')
126 |       );
127 |       expect(hasFixedCollectionError).toBe(false);
128 |     });
129 | 
130 |     test('should warn about missing outputKey in valid structure', () => {
131 |       const configMissingOutputKey = {
132 |         rules: {
133 |           values: [
134 |             {
135 |               conditions: {
136 |                 value1: '={{$json.status}}',
137 |                 operation: 'equals',
138 |                 value2: 'active'
139 |               }
140 |               // Missing outputKey
141 |             }
142 |           ]
143 |         }
144 |       };
145 | 
146 |       const result = EnhancedConfigValidator.validateWithMode(
147 |         'nodes-base.switch',
148 |         configMissingOutputKey,
149 |         [],
150 |         'operation',
151 |         'ai-friendly'
152 |       );
153 | 
154 |       const hasOutputKeyWarning = result.warnings.some(w => 
155 |         w.message.includes('missing "outputKey" property')
156 |       );
157 |       expect(hasOutputKeyWarning).toBe(true);
158 |     });
159 |   });
160 | 
161 |   describe('If Node Validation', () => {
162 |     test('should detect invalid nested values structure', () => {
163 |       const invalidConfig = {
164 |         conditions: {
165 |           values: [
166 |             {
167 |               value1: '={{$json.age}}',
168 |               operation: 'largerEqual',
169 |               value2: 18
170 |             }
171 |           ]
172 |         }
173 |       };
174 | 
175 |       const result = EnhancedConfigValidator.validateWithMode(
176 |         'nodes-base.if',
177 |         invalidConfig,
178 |         [],
179 |         'operation',
180 |         'ai-friendly'
181 |       );
182 | 
183 |       expect(result.valid).toBe(false);
184 |       expect(result.errors).toHaveLength(1);
185 |       expect(result.errors[0].type).toBe('invalid_value');
186 |       expect(result.errors[0].property).toBe('conditions');
187 |       expect(result.errors[0].message).toContain('Invalid structure for nodes-base.if node');
188 |       expect(result.errors[0].fix).toBe('Use: { "conditions": {...} } or { "conditions": [...] } directly, not nested under "values"');
189 |     });
190 | 
191 |     test('should provide auto-fix for invalid if structure', () => {
192 |       const invalidConfig = {
193 |         conditions: {
194 |           values: [
195 |             {
196 |               value1: '={{$json.age}}',
197 |               operation: 'largerEqual',
198 |               value2: 18
199 |             }
200 |           ]
201 |         }
202 |       };
203 | 
204 |       const result = EnhancedConfigValidator.validateWithMode(
205 |         'nodes-base.if',
206 |         invalidConfig,
207 |         [],
208 |         'operation',
209 |         'ai-friendly'
210 |       );
211 | 
212 |       expect(result.autofix).toBeDefined();
213 |       expect(result.autofix!.conditions).toEqual(invalidConfig.conditions.values);
214 |     });
215 | 
216 |     test('should accept valid if structure', () => {
217 |       const validConfig = {
218 |         conditions: {
219 |           value1: '={{$json.age}}',
220 |           operation: 'largerEqual',
221 |           value2: 18
222 |         }
223 |       };
224 | 
225 |       const result = EnhancedConfigValidator.validateWithMode(
226 |         'nodes-base.if',
227 |         validConfig,
228 |         [],
229 |         'operation',
230 |         'ai-friendly'
231 |       );
232 | 
233 |       // Should not have the specific structure error
234 |       const hasStructureError = result.errors.some(e => 
235 |         e.message.includes('should be a filter object/array directly')
236 |       );
237 |       expect(hasStructureError).toBe(false);
238 |     });
239 |   });
240 | 
241 |   describe('Filter Node Validation', () => {
242 |     test('should detect invalid nested values structure', () => {
243 |       const invalidConfig = {
244 |         conditions: {
245 |           values: [
246 |             {
247 |               value1: '={{$json.score}}',
248 |               operation: 'larger',
249 |               value2: 80
250 |             }
251 |           ]
252 |         }
253 |       };
254 | 
255 |       const result = EnhancedConfigValidator.validateWithMode(
256 |         'nodes-base.filter',
257 |         invalidConfig,
258 |         [],
259 |         'operation',
260 |         'ai-friendly'
261 |       );
262 | 
263 |       expect(result.valid).toBe(false);
264 |       expect(result.errors).toHaveLength(1);
265 |       expect(result.errors[0].type).toBe('invalid_value');
266 |       expect(result.errors[0].property).toBe('conditions');
267 |       expect(result.errors[0].message).toContain('Invalid structure for nodes-base.filter node');
268 |     });
269 | 
270 |     test('should accept valid filter structure', () => {
271 |       const validConfig = {
272 |         conditions: {
273 |           value1: '={{$json.score}}',
274 |           operation: 'larger',
275 |           value2: 80
276 |         }
277 |       };
278 | 
279 |       const result = EnhancedConfigValidator.validateWithMode(
280 |         'nodes-base.filter',
281 |         validConfig,
282 |         [],
283 |         'operation',
284 |         'ai-friendly'
285 |       );
286 | 
287 |       // Should not have the specific structure error
288 |       const hasStructureError = result.errors.some(e => 
289 |         e.message.includes('should be a filter object/array directly')
290 |       );
291 |       expect(hasStructureError).toBe(false);
292 |     });
293 |   });
294 | 
295 |   describe('Edge Cases', () => {
296 |     test('should not validate non-problematic nodes', () => {
297 |       const config = {
298 |         someProperty: {
299 |           conditions: {
300 |             values: ['should', 'not', 'trigger', 'validation']
301 |           }
302 |         }
303 |       };
304 | 
305 |       const result = EnhancedConfigValidator.validateWithMode(
306 |         'nodes-base.httpRequest',
307 |         config,
308 |         [],
309 |         'operation',
310 |         'ai-friendly'
311 |       );
312 | 
313 |       // Should not have fixedCollection errors for non-problematic nodes
314 |       const hasFixedCollectionError = result.errors.some(e => 
315 |         e.message.includes('propertyValues[itemName] is not iterable')
316 |       );
317 |       expect(hasFixedCollectionError).toBe(false);
318 |     });
319 | 
320 |     test('should handle empty config gracefully', () => {
321 |       const result = EnhancedConfigValidator.validateWithMode(
322 |         'nodes-base.switch',
323 |         {},
324 |         [],
325 |         'operation',
326 |         'ai-friendly'
327 |       );
328 | 
329 |       // Should not crash or produce false positives
330 |       expect(result).toBeDefined();
331 |       expect(result.errors).toBeInstanceOf(Array);
332 |     });
333 | 
334 |     test('should handle non-object property values', () => {
335 |       const config = {
336 |         rules: 'not an object'
337 |       };
338 | 
339 |       const result = EnhancedConfigValidator.validateWithMode(
340 |         'nodes-base.switch',
341 |         config,
342 |         [],
343 |         'operation',
344 |         'ai-friendly'
345 |       );
346 | 
347 |       // Should not crash on non-object values
348 |       expect(result).toBeDefined();
349 |       expect(result.errors).toBeInstanceOf(Array);
350 |     });
351 |   });
352 | 
353 |   describe('Real-world AI Agent Patterns', () => {
354 |     test('should catch common ChatGPT/Claude switch patterns', () => {
355 |       // This is a pattern commonly generated by AI agents
356 |       const aiGeneratedConfig = {
357 |         rules: {
358 |           conditions: {
359 |             values: [
360 |               {
361 |                 "value1": "={{$json.status}}",
362 |                 "operation": "equals", 
363 |                 "value2": "active"
364 |               },
365 |               {
366 |                 "value1": "={{$json.priority}}",
367 |                 "operation": "equals",
368 |                 "value2": "high"
369 |               }
370 |             ]
371 |           }
372 |         }
373 |       };
374 | 
375 |       const result = EnhancedConfigValidator.validateWithMode(
376 |         'nodes-base.switch',
377 |         aiGeneratedConfig,
378 |         [],
379 |         'operation',
380 |         'ai-friendly'
381 |       );
382 | 
383 |       expect(result.valid).toBe(false);
384 |       expect(result.errors).toHaveLength(1);
385 |       expect(result.errors[0].message).toContain('propertyValues[itemName] is not iterable');
386 |       
387 |       // Check auto-fix generates correct structure
388 |       expect(result.autofix!.rules.values).toHaveLength(2);
389 |       result.autofix!.rules.values.forEach((rule: any) => {
390 |         expect(rule).toHaveProperty('conditions');
391 |         expect(rule).toHaveProperty('outputKey');
392 |       });
393 |     });
394 | 
395 |     test('should catch common AI if/filter patterns', () => {
396 |       const aiGeneratedIfConfig = {
397 |         conditions: {
398 |           values: {
399 |             "value1": "={{$json.age}}",
400 |             "operation": "largerEqual",
401 |             "value2": 21
402 |           }
403 |         }
404 |       };
405 | 
406 |       const result = EnhancedConfigValidator.validateWithMode(
407 |         'nodes-base.if',
408 |         aiGeneratedIfConfig,
409 |         [],
410 |         'operation',
411 |         'ai-friendly'
412 |       );
413 | 
414 |       expect(result.valid).toBe(false);
415 |       expect(result.errors[0].message).toContain('Invalid structure for nodes-base.if node');
416 |     });
417 |   });
418 | 
419 |   describe('Version Compatibility', () => {
420 |     test('should work across different validation profiles', () => {
421 |       const invalidConfig = {
422 |         rules: {
423 |           conditions: {
424 |             values: [{ value1: 'test', operation: 'equals', value2: 'test' }]
425 |           }
426 |         }
427 |       };
428 | 
429 |       const profiles: Array<'strict' | 'runtime' | 'ai-friendly' | 'minimal'> = 
430 |         ['strict', 'runtime', 'ai-friendly', 'minimal'];
431 | 
432 |       profiles.forEach(profile => {
433 |         const result = EnhancedConfigValidator.validateWithMode(
434 |           'nodes-base.switch',
435 |           invalidConfig,
436 |           [],
437 |           'operation',
438 |           profile
439 |         );
440 | 
441 |         // All profiles should catch this critical error
442 |         const hasCriticalError = result.errors.some(e => 
443 |           e.message.includes('propertyValues[itemName] is not iterable')
444 |         );
445 |         
446 |         expect(hasCriticalError, `Profile ${profile} should catch critical fixedCollection error`).toBe(true);
447 |       });
448 |     });
449 |   });
450 | });
```

--------------------------------------------------------------------------------
/src/telemetry/config-manager.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Telemetry Configuration Manager
  3 |  * Handles telemetry settings, opt-in/opt-out, and first-run detection
  4 |  */
  5 | 
  6 | import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
  7 | import { join, resolve, dirname } from 'path';
  8 | import { homedir } from 'os';
  9 | import { createHash } from 'crypto';
 10 | import { hostname, platform, arch } from 'os';
 11 | 
 12 | export interface TelemetryConfig {
 13 |   enabled: boolean;
 14 |   userId: string;
 15 |   firstRun?: string;
 16 |   lastModified?: string;
 17 |   version?: string;
 18 | }
 19 | 
 20 | export class TelemetryConfigManager {
 21 |   private static instance: TelemetryConfigManager;
 22 |   private readonly configDir: string;
 23 |   private readonly configPath: string;
 24 |   private config: TelemetryConfig | null = null;
 25 | 
 26 |   private constructor() {
 27 |     this.configDir = join(homedir(), '.n8n-mcp');
 28 |     this.configPath = join(this.configDir, 'telemetry.json');
 29 |   }
 30 | 
 31 |   static getInstance(): TelemetryConfigManager {
 32 |     if (!TelemetryConfigManager.instance) {
 33 |       TelemetryConfigManager.instance = new TelemetryConfigManager();
 34 |     }
 35 |     return TelemetryConfigManager.instance;
 36 |   }
 37 | 
 38 |   /**
 39 |    * Generate a deterministic anonymous user ID based on machine characteristics
 40 |    * Uses Docker/cloud-specific method for containerized environments
 41 |    */
 42 |   private generateUserId(): string {
 43 |     // Use boot_id for all Docker/cloud environments (stable across container updates)
 44 |     if (process.env.IS_DOCKER === 'true' || this.isCloudEnvironment()) {
 45 |       return this.generateDockerStableId();
 46 |     }
 47 | 
 48 |     // Local installations use file-based method with hostname
 49 |     const machineId = `${hostname()}-${platform()}-${arch()}-${homedir()}`;
 50 |     return createHash('sha256').update(machineId).digest('hex').substring(0, 16);
 51 |   }
 52 | 
 53 |   /**
 54 |    * Generate stable user ID for Docker/cloud environments
 55 |    * Priority: boot_id → combined signals → generic fallback
 56 |    */
 57 |   private generateDockerStableId(): string {
 58 |     // Priority 1: Try boot_id (stable across container recreations)
 59 |     const bootId = this.readBootId();
 60 |     if (bootId) {
 61 |       const fingerprint = `${bootId}-${platform()}-${arch()}`;
 62 |       return createHash('sha256').update(fingerprint).digest('hex').substring(0, 16);
 63 |     }
 64 | 
 65 |     // Priority 2: Try combined host signals
 66 |     const combinedFingerprint = this.generateCombinedFingerprint();
 67 |     if (combinedFingerprint) {
 68 |       return combinedFingerprint;
 69 |     }
 70 | 
 71 |     // Priority 3: Generic Docker ID (allows aggregate statistics)
 72 |     const genericId = `docker-${platform()}-${arch()}`;
 73 |     return createHash('sha256').update(genericId).digest('hex').substring(0, 16);
 74 |   }
 75 | 
 76 |   /**
 77 |    * Read host boot_id from /proc (available in Linux containers)
 78 |    * Returns null if not available or invalid format
 79 |    */
 80 |   private readBootId(): string | null {
 81 |     try {
 82 |       const bootIdPath = '/proc/sys/kernel/random/boot_id';
 83 | 
 84 |       if (!existsSync(bootIdPath)) {
 85 |         return null;
 86 |       }
 87 | 
 88 |       const bootId = readFileSync(bootIdPath, 'utf-8').trim();
 89 | 
 90 |       // Validate UUID format (8-4-4-4-12 hex digits)
 91 |       const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
 92 |       if (!uuidRegex.test(bootId)) {
 93 |         return null;
 94 |       }
 95 | 
 96 |       return bootId;
 97 |     } catch (error) {
 98 |       // File not readable or other error
 99 |       return null;
100 |     }
101 |   }
102 | 
103 |   /**
104 |    * Generate fingerprint from combined host signals
105 |    * Fallback for environments where boot_id is not available
106 |    */
107 |   private generateCombinedFingerprint(): string | null {
108 |     try {
109 |       const signals: string[] = [];
110 | 
111 |       // CPU cores (stable)
112 |       if (existsSync('/proc/cpuinfo')) {
113 |         const cpuinfo = readFileSync('/proc/cpuinfo', 'utf-8');
114 |         const cores = (cpuinfo.match(/processor\s*:/g) || []).length;
115 |         if (cores > 0) {
116 |           signals.push(`cores:${cores}`);
117 |         }
118 |       }
119 | 
120 |       // Memory (stable)
121 |       if (existsSync('/proc/meminfo')) {
122 |         const meminfo = readFileSync('/proc/meminfo', 'utf-8');
123 |         const totalMatch = meminfo.match(/MemTotal:\s+(\d+)/);
124 |         if (totalMatch) {
125 |           signals.push(`mem:${totalMatch[1]}`);
126 |         }
127 |       }
128 | 
129 |       // Kernel version (stable)
130 |       if (existsSync('/proc/version')) {
131 |         const version = readFileSync('/proc/version', 'utf-8');
132 |         const kernelMatch = version.match(/Linux version ([\d.]+)/);
133 |         if (kernelMatch) {
134 |           signals.push(`kernel:${kernelMatch[1]}`);
135 |         }
136 |       }
137 | 
138 |       // Platform and arch
139 |       signals.push(platform(), arch());
140 | 
141 |       // Need at least 3 signals for reasonable uniqueness
142 |       if (signals.length < 3) {
143 |         return null;
144 |       }
145 | 
146 |       const fingerprint = signals.join('-');
147 |       return createHash('sha256').update(fingerprint).digest('hex').substring(0, 16);
148 |     } catch (error) {
149 |       return null;
150 |     }
151 |   }
152 | 
153 |   /**
154 |    * Check if running in a cloud environment
155 |    */
156 |   private isCloudEnvironment(): boolean {
157 |     return !!(
158 |       process.env.RAILWAY_ENVIRONMENT ||
159 |       process.env.RENDER ||
160 |       process.env.FLY_APP_NAME ||
161 |       process.env.HEROKU_APP_NAME ||
162 |       process.env.AWS_EXECUTION_ENV ||
163 |       process.env.KUBERNETES_SERVICE_HOST ||
164 |       process.env.GOOGLE_CLOUD_PROJECT ||
165 |       process.env.AZURE_FUNCTIONS_ENVIRONMENT
166 |     );
167 |   }
168 | 
169 |   /**
170 |    * Load configuration from disk or create default
171 |    */
172 |   loadConfig(): TelemetryConfig {
173 |     if (this.config) {
174 |       return this.config;
175 |     }
176 | 
177 |     if (!existsSync(this.configPath)) {
178 |       // First run - create default config
179 |       const version = this.getPackageVersion();
180 | 
181 |       // Check if telemetry is disabled via environment variable
182 |       const envDisabled = this.isDisabledByEnvironment();
183 | 
184 |       this.config = {
185 |         enabled: !envDisabled, // Respect env var on first run
186 |         userId: this.generateUserId(),
187 |         firstRun: new Date().toISOString(),
188 |         version
189 |       };
190 | 
191 |       this.saveConfig();
192 | 
193 |       // Only show notice if not disabled via environment
194 |       if (!envDisabled) {
195 |         this.showFirstRunNotice();
196 |       }
197 | 
198 |       return this.config;
199 |     }
200 | 
201 |     try {
202 |       const rawConfig = readFileSync(this.configPath, 'utf-8');
203 |       this.config = JSON.parse(rawConfig);
204 | 
205 |       // Ensure userId exists (for upgrades from older versions)
206 |       if (!this.config!.userId) {
207 |         this.config!.userId = this.generateUserId();
208 |         this.saveConfig();
209 |       }
210 | 
211 |       return this.config!;
212 |     } catch (error) {
213 |       console.error('Failed to load telemetry config, using defaults:', error);
214 |       this.config = {
215 |         enabled: false,
216 |         userId: this.generateUserId()
217 |       };
218 |       return this.config;
219 |     }
220 |   }
221 | 
222 |   /**
223 |    * Save configuration to disk
224 |    */
225 |   private saveConfig(): void {
226 |     if (!this.config) return;
227 | 
228 |     try {
229 |       if (!existsSync(this.configDir)) {
230 |         mkdirSync(this.configDir, { recursive: true });
231 |       }
232 | 
233 |       this.config.lastModified = new Date().toISOString();
234 |       writeFileSync(this.configPath, JSON.stringify(this.config, null, 2));
235 |     } catch (error) {
236 |       console.error('Failed to save telemetry config:', error);
237 |     }
238 |   }
239 | 
240 |   /**
241 |    * Check if telemetry is enabled
242 |    * Priority: Environment variable > Config file > Default (true)
243 |    */
244 |   isEnabled(): boolean {
245 |     // Check environment variables first (for Docker users)
246 |     if (this.isDisabledByEnvironment()) {
247 |       return false;
248 |     }
249 | 
250 |     const config = this.loadConfig();
251 |     return config.enabled;
252 |   }
253 | 
254 |   /**
255 |    * Check if telemetry is disabled via environment variable
256 |    */
257 |   private isDisabledByEnvironment(): boolean {
258 |     const envVars = [
259 |       'N8N_MCP_TELEMETRY_DISABLED',
260 |       'TELEMETRY_DISABLED',
261 |       'DISABLE_TELEMETRY'
262 |     ];
263 | 
264 |     for (const varName of envVars) {
265 |       const value = process.env[varName];
266 |       if (value !== undefined) {
267 |         const normalized = value.toLowerCase().trim();
268 | 
269 |         // Warn about invalid values
270 |         if (!['true', 'false', '1', '0', ''].includes(normalized)) {
271 |           console.warn(
272 |             `⚠️  Invalid telemetry environment variable value: ${varName}="${value}"\n` +
273 |             `   Use "true" to disable or "false" to enable telemetry.`
274 |           );
275 |         }
276 | 
277 |         // Accept common truthy values
278 |         if (normalized === 'true' || normalized === '1') {
279 |           return true;
280 |         }
281 |       }
282 |     }
283 | 
284 |     return false;
285 |   }
286 | 
287 |   /**
288 |    * Get the anonymous user ID
289 |    */
290 |   getUserId(): string {
291 |     const config = this.loadConfig();
292 |     return config.userId;
293 |   }
294 | 
295 |   /**
296 |    * Check if this is the first run
297 |    */
298 |   isFirstRun(): boolean {
299 |     return !existsSync(this.configPath);
300 |   }
301 | 
302 |   /**
303 |    * Enable telemetry
304 |    */
305 |   enable(): void {
306 |     const config = this.loadConfig();
307 |     config.enabled = true;
308 |     this.config = config;
309 |     this.saveConfig();
310 |     console.log('✓ Anonymous telemetry enabled');
311 |   }
312 | 
313 |   /**
314 |    * Disable telemetry
315 |    */
316 |   disable(): void {
317 |     const config = this.loadConfig();
318 |     config.enabled = false;
319 |     this.config = config;
320 |     this.saveConfig();
321 |     console.log('✓ Anonymous telemetry disabled');
322 |   }
323 | 
324 |   /**
325 |    * Get current status
326 |    */
327 |   getStatus(): string {
328 |     const config = this.loadConfig();
329 | 
330 |     // Check if disabled by environment
331 |     const envDisabled = this.isDisabledByEnvironment();
332 | 
333 |     let status = config.enabled ? 'ENABLED' : 'DISABLED';
334 |     if (envDisabled) {
335 |       status = 'DISABLED (via environment variable)';
336 |     }
337 | 
338 |     return `
339 | Telemetry Status: ${status}
340 | Anonymous ID: ${config.userId}
341 | First Run: ${config.firstRun || 'Unknown'}
342 | Config Path: ${this.configPath}
343 | 
344 | To opt-out: npx n8n-mcp telemetry disable
345 | To opt-in:  npx n8n-mcp telemetry enable
346 | 
347 | For Docker: Set N8N_MCP_TELEMETRY_DISABLED=true
348 | `;
349 |   }
350 | 
351 |   /**
352 |    * Show first-run notice to user
353 |    */
354 |   private showFirstRunNotice(): void {
355 |     console.log(`
356 | ╔════════════════════════════════════════════════════════════╗
357 | ║              Anonymous Usage Statistics                     ║
358 | ╠════════════════════════════════════════════════════════════╣
359 | ║                                                             ║
360 | ║  n8n-mcp collects anonymous usage data to improve the      ║
361 | ║  tool and understand how it's being used.                  ║
362 | ║                                                             ║
363 | ║  We track:                                                 ║
364 | ║  • Which MCP tools are used (no parameters)                ║
365 | ║  • Workflow structures (sanitized, no sensitive data)      ║
366 | ║  • Error patterns (hashed, no details)                     ║
367 | ║  • Performance metrics (timing, success rates)             ║
368 | ║                                                             ║
369 | ║  We NEVER collect:                                         ║
370 | ║  • URLs, API keys, or credentials                          ║
371 | ║  • Workflow content or actual data                         ║
372 | ║  • Personal or identifiable information                    ║
373 | ║  • n8n instance details or locations                       ║
374 | ║                                                             ║
375 | ║  Your anonymous ID: ${this.config?.userId || 'generating...'}          ║
376 | ║                                                             ║
377 | ║  This helps me understand usage patterns and improve       ║
378 | ║  n8n-mcp for everyone. Thank you for your support!         ║
379 | ║                                                             ║
380 | ║  To opt-out at any time:                                   ║
381 | ║  npx n8n-mcp telemetry disable                            ║
382 | ║                                                             ║
383 | ║  Data deletion requests:                                   ║
384 | ║  Email [email protected] with your anonymous ID          ║
385 | ║                                                             ║
386 | ║  Learn more:                                               ║
387 | ║  https://github.com/czlonkowski/n8n-mcp/blob/main/PRIVACY.md ║
388 | ║                                                             ║
389 | ╚════════════════════════════════════════════════════════════╝
390 | `);
391 |   }
392 | 
393 |   /**
394 |    * Get package version safely
395 |    */
396 |   private getPackageVersion(): string {
397 |     try {
398 |       // Try multiple approaches to find package.json
399 |       const possiblePaths = [
400 |         resolve(__dirname, '..', '..', 'package.json'),
401 |         resolve(process.cwd(), 'package.json'),
402 |         resolve(__dirname, '..', '..', '..', 'package.json')
403 |       ];
404 | 
405 |       for (const packagePath of possiblePaths) {
406 |         if (existsSync(packagePath)) {
407 |           const packageJson = JSON.parse(readFileSync(packagePath, 'utf-8'));
408 |           if (packageJson.version) {
409 |             return packageJson.version;
410 |           }
411 |         }
412 |       }
413 | 
414 |       // Fallback: try require (works in some environments)
415 |       try {
416 |         const packageJson = require('../../package.json');
417 |         return packageJson.version || 'unknown';
418 |       } catch {
419 |         // Ignore require error
420 |       }
421 | 
422 |       return 'unknown';
423 |     } catch (error) {
424 |       return 'unknown';
425 |     }
426 |   }
427 | }
```

--------------------------------------------------------------------------------
/scripts/prepare-release.js:
--------------------------------------------------------------------------------

```javascript
  1 | #!/usr/bin/env node
  2 | 
  3 | /**
  4 |  * Pre-release preparation script
  5 |  * Validates and prepares everything needed for a successful release
  6 |  */
  7 | 
  8 | const fs = require('fs');
  9 | const path = require('path');
 10 | const { execSync, spawnSync } = require('child_process');
 11 | const readline = require('readline');
 12 | 
 13 | // Color codes
 14 | const colors = {
 15 |   reset: '\x1b[0m',
 16 |   red: '\x1b[31m',
 17 |   green: '\x1b[32m',
 18 |   yellow: '\x1b[33m',
 19 |   blue: '\x1b[34m',
 20 |   magenta: '\x1b[35m',
 21 |   cyan: '\x1b[36m'
 22 | };
 23 | 
 24 | function log(message, color = 'reset') {
 25 |   console.log(`${colors[color]}${message}${colors.reset}`);
 26 | }
 27 | 
 28 | function success(message) {
 29 |   log(`✅ ${message}`, 'green');
 30 | }
 31 | 
 32 | function warning(message) {
 33 |   log(`⚠️  ${message}`, 'yellow');
 34 | }
 35 | 
 36 | function error(message) {
 37 |   log(`❌ ${message}`, 'red');
 38 | }
 39 | 
 40 | function info(message) {
 41 |   log(`ℹ️  ${message}`, 'blue');
 42 | }
 43 | 
 44 | function header(title) {
 45 |   log(`\n${'='.repeat(60)}`, 'cyan');
 46 |   log(`🚀 ${title}`, 'cyan');
 47 |   log(`${'='.repeat(60)}`, 'cyan');
 48 | }
 49 | 
 50 | class ReleasePreparation {
 51 |   constructor() {
 52 |     this.rootDir = path.resolve(__dirname, '..');
 53 |     this.rl = readline.createInterface({
 54 |       input: process.stdin,
 55 |       output: process.stdout
 56 |     });
 57 |   }
 58 | 
 59 |   async askQuestion(question) {
 60 |     return new Promise((resolve) => {
 61 |       this.rl.question(question, resolve);
 62 |     });
 63 |   }
 64 | 
 65 |   /**
 66 |    * Get current version and ask for new version
 67 |    */
 68 |   async getVersionInfo() {
 69 |     const packageJson = require(path.join(this.rootDir, 'package.json'));
 70 |     const currentVersion = packageJson.version;
 71 |     
 72 |     log(`\nCurrent version: ${currentVersion}`, 'blue');
 73 |     
 74 |     const newVersion = await this.askQuestion('\nEnter new version (e.g., 2.10.0): ');
 75 |     
 76 |     if (!newVersion || !this.isValidSemver(newVersion)) {
 77 |       error('Invalid semantic version format');
 78 |       throw new Error('Invalid version');
 79 |     }
 80 |     
 81 |     if (this.compareVersions(newVersion, currentVersion) <= 0) {
 82 |       error('New version must be greater than current version');
 83 |       throw new Error('Version not incremented');
 84 |     }
 85 |     
 86 |     return { currentVersion, newVersion };
 87 |   }
 88 | 
 89 |   /**
 90 |    * Validate semantic version format (strict semver compliance)
 91 |    */
 92 |   isValidSemver(version) {
 93 |     // Strict semantic versioning regex
 94 |     const semverRegex = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/;
 95 |     return semverRegex.test(version);
 96 |   }
 97 | 
 98 |   /**
 99 |    * Compare two semantic versions
100 |    */
101 |   compareVersions(v1, v2) {
102 |     const parseVersion = (v) => v.split('-')[0].split('.').map(Number);
103 |     const [v1Parts, v2Parts] = [parseVersion(v1), parseVersion(v2)];
104 |     
105 |     for (let i = 0; i < 3; i++) {
106 |       if (v1Parts[i] > v2Parts[i]) return 1;
107 |       if (v1Parts[i] < v2Parts[i]) return -1;
108 |     }
109 |     return 0;
110 |   }
111 | 
112 |   /**
113 |    * Update version in package files
114 |    */
115 |   updateVersions(newVersion) {
116 |     log('\n📝 Updating version in package files...', 'blue');
117 |     
118 |     // Update package.json
119 |     const packageJsonPath = path.join(this.rootDir, 'package.json');
120 |     const packageJson = require(packageJsonPath);
121 |     packageJson.version = newVersion;
122 |     fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2) + '\n');
123 |     success('Updated package.json');
124 |     
125 |     // Sync to runtime package
126 |     try {
127 |       execSync('npm run sync:runtime-version', { cwd: this.rootDir, stdio: 'pipe' });
128 |       success('Synced package.runtime.json');
129 |     } catch (err) {
130 |       warning('Could not sync runtime version automatically');
131 |       
132 |       // Manual sync
133 |       const runtimeJsonPath = path.join(this.rootDir, 'package.runtime.json');
134 |       if (fs.existsSync(runtimeJsonPath)) {
135 |         const runtimeJson = require(runtimeJsonPath);
136 |         runtimeJson.version = newVersion;
137 |         fs.writeFileSync(runtimeJsonPath, JSON.stringify(runtimeJson, null, 2) + '\n');
138 |         success('Manually synced package.runtime.json');
139 |       }
140 |     }
141 |   }
142 | 
143 |   /**
144 |    * Update changelog
145 |    */
146 |   async updateChangelog(newVersion) {
147 |     const changelogPath = path.join(this.rootDir, 'docs/CHANGELOG.md');
148 |     
149 |     if (!fs.existsSync(changelogPath)) {
150 |       warning('Changelog file not found, skipping update');
151 |       return;
152 |     }
153 |     
154 |     log('\n📋 Updating changelog...', 'blue');
155 |     
156 |     const content = fs.readFileSync(changelogPath, 'utf8');
157 |     const today = new Date().toISOString().split('T')[0];
158 |     
159 |     // Check if version already exists in changelog
160 |     const versionRegex = new RegExp(`^## \\[${newVersion.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\]`, 'm');
161 |     if (versionRegex.test(content)) {
162 |       info(`Version ${newVersion} already exists in changelog`);
163 |       return;
164 |     }
165 |     
166 |     // Find the Unreleased section
167 |     const unreleasedMatch = content.match(/^## \[Unreleased\]\s*\n([\s\S]*?)(?=\n## \[|$)/m);
168 |     
169 |     if (unreleasedMatch) {
170 |       const unreleasedContent = unreleasedMatch[1].trim();
171 |       
172 |       if (unreleasedContent) {
173 |         log('\nFound content in Unreleased section:', 'blue');
174 |         log(unreleasedContent.substring(0, 200) + '...', 'yellow');
175 |         
176 |         const moveContent = await this.askQuestion('\nMove this content to the new version? (y/n): ');
177 |         
178 |         if (moveContent.toLowerCase() === 'y') {
179 |           // Move unreleased content to new version
180 |           const newVersionSection = `## [${newVersion}] - ${today}\n\n${unreleasedContent}\n\n`;
181 |           const updatedContent = content.replace(
182 |             /^## \[Unreleased\]\s*\n[\s\S]*?(?=\n## \[)/m,
183 |             `## [Unreleased]\n\n${newVersionSection}## [`
184 |           );
185 |           
186 |           fs.writeFileSync(changelogPath, updatedContent);
187 |           success(`Moved unreleased content to version ${newVersion}`);
188 |         } else {
189 |           // Just add empty version section
190 |           const newVersionSection = `## [${newVersion}] - ${today}\n\n### Added\n- \n\n### Changed\n- \n\n### Fixed\n- \n\n`;
191 |           const updatedContent = content.replace(
192 |             /^## \[Unreleased\]\s*\n/m,
193 |             `## [Unreleased]\n\n${newVersionSection}`
194 |           );
195 |           
196 |           fs.writeFileSync(changelogPath, updatedContent);
197 |           warning(`Added empty version section for ${newVersion} - please fill in the changes`);
198 |         }
199 |       } else {
200 |         // Add empty version section
201 |         const newVersionSection = `## [${newVersion}] - ${today}\n\n### Added\n- \n\n### Changed\n- \n\n### Fixed\n- \n\n`;
202 |         const updatedContent = content.replace(
203 |           /^## \[Unreleased\]\s*\n/m,
204 |           `## [Unreleased]\n\n${newVersionSection}`
205 |         );
206 |         
207 |         fs.writeFileSync(changelogPath, updatedContent);
208 |         warning(`Added empty version section for ${newVersion} - please fill in the changes`);
209 |       }
210 |     } else {
211 |       warning('Could not find Unreleased section in changelog');
212 |     }
213 |     
214 |     info('Please review and edit the changelog before committing');
215 |   }
216 | 
217 |   /**
218 |    * Run tests and build
219 |    */
220 |   async runChecks() {
221 |     log('\n🧪 Running pre-release checks...', 'blue');
222 |     
223 |     try {
224 |       // Run tests
225 |       log('Running tests...', 'blue');
226 |       execSync('npm test', { cwd: this.rootDir, stdio: 'inherit' });
227 |       success('All tests passed');
228 |       
229 |       // Run build
230 |       log('Building project...', 'blue');
231 |       execSync('npm run build', { cwd: this.rootDir, stdio: 'inherit' });
232 |       success('Build completed');
233 |       
234 |       // Rebuild database
235 |       log('Rebuilding database...', 'blue');
236 |       execSync('npm run rebuild', { cwd: this.rootDir, stdio: 'inherit' });
237 |       success('Database rebuilt');
238 |       
239 |       // Run type checking
240 |       log('Type checking...', 'blue');
241 |       execSync('npm run typecheck', { cwd: this.rootDir, stdio: 'inherit' });
242 |       success('Type checking passed');
243 |       
244 |     } catch (err) {
245 |       error('Pre-release checks failed');
246 |       throw err;
247 |     }
248 |   }
249 | 
250 |   /**
251 |    * Create git commit
252 |    */
253 |   async createCommit(newVersion) {
254 |     log('\n📝 Creating git commit...', 'blue');
255 |     
256 |     try {
257 |       // Check git status
258 |       const status = execSync('git status --porcelain', { 
259 |         cwd: this.rootDir, 
260 |         encoding: 'utf8' 
261 |       });
262 |       
263 |       if (!status.trim()) {
264 |         info('No changes to commit');
265 |         return;
266 |       }
267 |       
268 |       // Show what will be committed
269 |       log('\nFiles to be committed:', 'blue');
270 |       execSync('git diff --name-only', { cwd: this.rootDir, stdio: 'inherit' });
271 |       
272 |       const commit = await this.askQuestion('\nCreate commit for release? (y/n): ');
273 |       
274 |       if (commit.toLowerCase() === 'y') {
275 |         // Add files
276 |         execSync('git add package.json package.runtime.json docs/CHANGELOG.md', { 
277 |           cwd: this.rootDir, 
278 |           stdio: 'pipe' 
279 |         });
280 |         
281 |         // Create commit
282 |         const commitMessage = `chore: release v${newVersion}
283 | 
284 | 🤖 Generated with [Claude Code](https://claude.ai/code)
285 | 
286 | Co-Authored-By: Claude <[email protected]>`;
287 |         
288 |         const result = spawnSync('git', ['commit', '-m', commitMessage], { 
289 |           cwd: this.rootDir, 
290 |           stdio: 'pipe',
291 |           encoding: 'utf8'
292 |         });
293 |         
294 |         if (result.error || result.status !== 0) {
295 |           throw new Error(`Git commit failed: ${result.stderr || result.error?.message}`);
296 |         }
297 |         
298 |         success(`Created commit for v${newVersion}`);
299 |         
300 |         const push = await this.askQuestion('\nPush to trigger release workflow? (y/n): ');
301 |         
302 |         if (push.toLowerCase() === 'y') {
303 |           // Add confirmation for destructive operation
304 |           warning('\n⚠️  DESTRUCTIVE OPERATION WARNING ⚠️');
305 |           warning('This will trigger a PUBLIC RELEASE that cannot be undone!');
306 |           warning('The following will happen automatically:');
307 |           warning('• Create GitHub release with tag');
308 |           warning('• Publish package to NPM registry');
309 |           warning('• Build and push Docker images');
310 |           warning('• Update documentation');
311 |           
312 |           const confirmation = await this.askQuestion('\nType "RELEASE" (all caps) to confirm: ');
313 |           
314 |           if (confirmation === 'RELEASE') {
315 |             execSync('git push', { cwd: this.rootDir, stdio: 'inherit' });
316 |             success('Pushed to remote repository');
317 |             log('\n🎉 Release workflow will be triggered automatically!', 'green');
318 |             log('Monitor progress at: https://github.com/czlonkowski/n8n-mcp/actions', 'blue');
319 |           } else {
320 |             warning('Release cancelled. Commit created but not pushed.');
321 |             info('You can push manually later to trigger the release.');
322 |           }
323 |         } else {
324 |           info('Commit created but not pushed. Push manually to trigger release.');
325 |         }
326 |       }
327 |       
328 |     } catch (err) {
329 |       error(`Git operations failed: ${err.message}`);
330 |       throw err;
331 |     }
332 |   }
333 | 
334 |   /**
335 |    * Display final instructions
336 |    */
337 |   displayInstructions(newVersion) {
338 |     header('Release Preparation Complete');
339 |     
340 |     log('📋 What happens next:', 'blue');
341 |     log(`1. The GitHub Actions workflow will detect the version change to v${newVersion}`, 'green');
342 |     log('2. It will automatically:', 'green');
343 |     log('   • Create a GitHub release with changelog content', 'green');
344 |     log('   • Publish the npm package', 'green');
345 |     log('   • Build and push Docker images', 'green');
346 |     log('   • Update documentation badges', 'green');
347 |     log('\n🔍 Monitor the release at:', 'blue');
348 |     log('   • GitHub Actions: https://github.com/czlonkowski/n8n-mcp/actions', 'blue');
349 |     log('   • NPM Package: https://www.npmjs.com/package/n8n-mcp', 'blue');
350 |     log('   • Docker Images: https://github.com/czlonkowski/n8n-mcp/pkgs/container/n8n-mcp', 'blue');
351 |     
352 |     log('\n✅ Release preparation completed successfully!', 'green');
353 |   }
354 | 
355 |   /**
356 |    * Main execution flow
357 |    */
358 |   async run() {
359 |     try {
360 |       header('n8n-MCP Release Preparation');
361 |       
362 |       // Get version information
363 |       const { currentVersion, newVersion } = await this.getVersionInfo();
364 |       
365 |       log(`\n🔄 Preparing release: ${currentVersion} → ${newVersion}`, 'magenta');
366 |       
367 |       // Update versions
368 |       this.updateVersions(newVersion);
369 |       
370 |       // Update changelog
371 |       await this.updateChangelog(newVersion);
372 |       
373 |       // Run pre-release checks
374 |       await this.runChecks();
375 |       
376 |       // Create git commit
377 |       await this.createCommit(newVersion);
378 |       
379 |       // Display final instructions
380 |       this.displayInstructions(newVersion);
381 |       
382 |     } catch (err) {
383 |       error(`Release preparation failed: ${err.message}`);
384 |       process.exit(1);
385 |     } finally {
386 |       this.rl.close();
387 |     }
388 |   }
389 | }
390 | 
391 | // Run the script
392 | if (require.main === module) {
393 |   const preparation = new ReleasePreparation();
394 |   preparation.run().catch(err => {
395 |     console.error('Release preparation failed:', err);
396 |     process.exit(1);
397 |   });
398 | }
399 | 
400 | module.exports = ReleasePreparation;
```

--------------------------------------------------------------------------------
/tests/unit/mcp/search-nodes-examples.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
  2 | import { N8NDocumentationMCPServer } from '../../../src/mcp/server';
  3 | import { createDatabaseAdapter } from '../../../src/database/database-adapter';
  4 | import path from 'path';
  5 | import fs from 'fs';
  6 | 
  7 | /**
  8 |  * Unit tests for search_nodes with includeExamples parameter
  9 |  * Testing P0-R3 feature: Template-based configuration examples
 10 |  */
 11 | 
 12 | describe('search_nodes with includeExamples', () => {
 13 |   let server: N8NDocumentationMCPServer;
 14 |   let dbPath: string;
 15 | 
 16 |   beforeEach(async () => {
 17 |     // Use in-memory database for testing
 18 |     process.env.NODE_DB_PATH = ':memory:';
 19 |     server = new N8NDocumentationMCPServer();
 20 |     await (server as any).initialized;
 21 | 
 22 |     // Populate in-memory database with test nodes
 23 |     // NOTE: Database stores nodes in SHORT form (nodes-base.xxx, not n8n-nodes-base.xxx)
 24 |     const testNodes = [
 25 |       {
 26 |         node_type: 'nodes-base.webhook',
 27 |         package_name: 'n8n-nodes-base',
 28 |         display_name: 'Webhook',
 29 |         description: 'Starts workflow on webhook call',
 30 |         category: 'Core Nodes',
 31 |         is_ai_tool: 0,
 32 |         is_trigger: 1,
 33 |         is_webhook: 1,
 34 |         is_versioned: 1,
 35 |         version: '1',
 36 |         properties_schema: JSON.stringify([]),
 37 |         operations: JSON.stringify([])
 38 |       },
 39 |       {
 40 |         node_type: 'nodes-base.httpRequest',
 41 |         package_name: 'n8n-nodes-base',
 42 |         display_name: 'HTTP Request',
 43 |         description: 'Makes an HTTP request',
 44 |         category: 'Core Nodes',
 45 |         is_ai_tool: 0,
 46 |         is_trigger: 0,
 47 |         is_webhook: 0,
 48 |         is_versioned: 1,
 49 |         version: '1',
 50 |         properties_schema: JSON.stringify([]),
 51 |         operations: JSON.stringify([])
 52 |       }
 53 |     ];
 54 | 
 55 |     // Insert test nodes into the in-memory database
 56 |     const db = (server as any).db;
 57 |     if (db) {
 58 |       const insertStmt = db.prepare(`
 59 |         INSERT INTO nodes (
 60 |           node_type, package_name, display_name, description, category,
 61 |           is_ai_tool, is_trigger, is_webhook, is_versioned, version,
 62 |           properties_schema, operations
 63 |         ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
 64 |       `);
 65 | 
 66 |       for (const node of testNodes) {
 67 |         insertStmt.run(
 68 |           node.node_type,
 69 |           node.package_name,
 70 |           node.display_name,
 71 |           node.description,
 72 |           node.category,
 73 |           node.is_ai_tool,
 74 |           node.is_trigger,
 75 |           node.is_webhook,
 76 |           node.is_versioned,
 77 |           node.version,
 78 |           node.properties_schema,
 79 |           node.operations
 80 |         );
 81 |       }
 82 |       // Note: FTS table is not created in test environment
 83 |       // searchNodes will fall back to LIKE search when FTS doesn't exist
 84 |     }
 85 |   });
 86 | 
 87 |   afterEach(() => {
 88 |     delete process.env.NODE_DB_PATH;
 89 |   });
 90 | 
 91 |   describe('includeExamples parameter', () => {
 92 |     it('should not include examples when includeExamples is false', async () => {
 93 |       const result = await (server as any).searchNodes('webhook', 5, { includeExamples: false });
 94 | 
 95 |       expect(result.results).toBeDefined();
 96 |       if (result.results.length > 0) {
 97 |         result.results.forEach((node: any) => {
 98 |           expect(node.examples).toBeUndefined();
 99 |         });
100 |       }
101 |     });
102 | 
103 |     it('should not include examples when includeExamples is undefined', async () => {
104 |       const result = await (server as any).searchNodes('webhook', 5, {});
105 | 
106 |       expect(result.results).toBeDefined();
107 |       if (result.results.length > 0) {
108 |         result.results.forEach((node: any) => {
109 |           expect(node.examples).toBeUndefined();
110 |         });
111 |       }
112 |     });
113 | 
114 |     it('should include examples when includeExamples is true', async () => {
115 |       const result = await (server as any).searchNodes('webhook', 5, { includeExamples: true });
116 | 
117 |       expect(result.results).toBeDefined();
118 |       // Note: In-memory test database may not have template configs
119 |       // This test validates the parameter is processed correctly
120 |     });
121 | 
122 |     it('should handle nodes without examples gracefully', async () => {
123 |       const result = await (server as any).searchNodes('nonexistent', 5, { includeExamples: true });
124 | 
125 |       expect(result.results).toBeDefined();
126 |       expect(result.results).toHaveLength(0);
127 |     });
128 | 
129 |     it('should limit examples to top 2 per node', async () => {
130 |       // This test would need a database with actual template_node_configs data
131 |       // In a real scenario, we'd verify that only 2 examples are returned
132 |       const result = await (server as any).searchNodes('http', 5, { includeExamples: true });
133 | 
134 |       expect(result.results).toBeDefined();
135 |       if (result.results.length > 0) {
136 |         result.results.forEach((node: any) => {
137 |           if (node.examples) {
138 |             expect(node.examples.length).toBeLessThanOrEqual(2);
139 |           }
140 |         });
141 |       }
142 |     });
143 |   });
144 | 
145 |   describe('example data structure', () => {
146 |     it('should return examples with correct structure when present', async () => {
147 |       // Mock database to return example data
148 |       const mockDb = (server as any).db;
149 |       if (mockDb) {
150 |         const originalPrepare = mockDb.prepare.bind(mockDb);
151 |         mockDb.prepare = vi.fn((query: string) => {
152 |           if (query.includes('template_node_configs')) {
153 |             return {
154 |               all: vi.fn(() => [
155 |                 {
156 |                   parameters_json: JSON.stringify({
157 |                     httpMethod: 'POST',
158 |                     path: 'webhook-test'
159 |                   }),
160 |                   template_name: 'Test Template',
161 |                   template_views: 1000
162 |                 },
163 |                 {
164 |                   parameters_json: JSON.stringify({
165 |                     httpMethod: 'GET',
166 |                     path: 'webhook-get'
167 |                   }),
168 |                   template_name: 'Another Template',
169 |                   template_views: 500
170 |                 }
171 |               ])
172 |             };
173 |           }
174 |           return originalPrepare(query);
175 |         });
176 | 
177 |         const result = await (server as any).searchNodes('webhook', 5, { includeExamples: true });
178 | 
179 |         if (result.results.length > 0 && result.results[0].examples) {
180 |           const example = result.results[0].examples[0];
181 |           expect(example).toHaveProperty('configuration');
182 |           expect(example).toHaveProperty('template');
183 |           expect(example).toHaveProperty('views');
184 |           expect(typeof example.configuration).toBe('object');
185 |           expect(typeof example.template).toBe('string');
186 |           expect(typeof example.views).toBe('number');
187 |         }
188 |       }
189 |     });
190 |   });
191 | 
192 |   describe('backward compatibility', () => {
193 |     it('should maintain backward compatibility when includeExamples not specified', async () => {
194 |       const resultWithoutParam = await (server as any).searchNodes('http', 5);
195 |       const resultWithFalse = await (server as any).searchNodes('http', 5, { includeExamples: false });
196 | 
197 |       expect(resultWithoutParam.results).toBeDefined();
198 |       expect(resultWithFalse.results).toBeDefined();
199 | 
200 |       // Both should have same structure (no examples)
201 |       if (resultWithoutParam.results.length > 0) {
202 |         expect(resultWithoutParam.results[0].examples).toBeUndefined();
203 |       }
204 |       if (resultWithFalse.results.length > 0) {
205 |         expect(resultWithFalse.results[0].examples).toBeUndefined();
206 |       }
207 |     });
208 |   });
209 | 
210 |   describe('performance considerations', () => {
211 |     it('should not significantly impact performance when includeExamples is false', async () => {
212 |       const startWithout = Date.now();
213 |       await (server as any).searchNodes('http', 20, { includeExamples: false });
214 |       const durationWithout = Date.now() - startWithout;
215 | 
216 |       const startWith = Date.now();
217 |       await (server as any).searchNodes('http', 20, { includeExamples: true });
218 |       const durationWith = Date.now() - startWith;
219 | 
220 |       // Both should complete quickly (under 100ms)
221 |       expect(durationWithout).toBeLessThan(100);
222 |       expect(durationWith).toBeLessThan(200);
223 |     });
224 |   });
225 | 
226 |   describe('error handling', () => {
227 |     it('should continue to work even if example fetch fails', async () => {
228 |       // Mock database to throw error on example fetch
229 |       const mockDb = (server as any).db;
230 |       if (mockDb) {
231 |         const originalPrepare = mockDb.prepare.bind(mockDb);
232 |         mockDb.prepare = vi.fn((query: string) => {
233 |           if (query.includes('template_node_configs')) {
234 |             throw new Error('Database error');
235 |           }
236 |           return originalPrepare(query);
237 |         });
238 | 
239 |         // Should not throw, should return results without examples
240 |         const result = await (server as any).searchNodes('webhook', 5, { includeExamples: true });
241 | 
242 |         expect(result.results).toBeDefined();
243 |         // Examples should be undefined due to error
244 |         if (result.results.length > 0) {
245 |           expect(result.results[0].examples).toBeUndefined();
246 |         }
247 |       }
248 |     });
249 | 
250 |     it('should handle malformed parameters_json gracefully', async () => {
251 |       const mockDb = (server as any).db;
252 |       if (mockDb) {
253 |         const originalPrepare = mockDb.prepare.bind(mockDb);
254 |         mockDb.prepare = vi.fn((query: string) => {
255 |           if (query.includes('template_node_configs')) {
256 |             return {
257 |               all: vi.fn(() => [
258 |                 {
259 |                   parameters_json: 'invalid json',
260 |                   template_name: 'Test Template',
261 |                   template_views: 1000
262 |                 }
263 |               ])
264 |             };
265 |           }
266 |           return originalPrepare(query);
267 |         });
268 | 
269 |         // Should not throw
270 |         const result = await (server as any).searchNodes('webhook', 5, { includeExamples: true });
271 |         expect(result).toBeDefined();
272 |       }
273 |     });
274 |   });
275 | });
276 | 
277 | describe('searchNodesLIKE with includeExamples', () => {
278 |   let server: N8NDocumentationMCPServer;
279 | 
280 |   beforeEach(async () => {
281 |     process.env.NODE_DB_PATH = ':memory:';
282 |     server = new N8NDocumentationMCPServer();
283 |     await (server as any).initialized;
284 | 
285 |     // Populate in-memory database with test nodes
286 |     const testNodes = [
287 |       {
288 |         node_type: 'nodes-base.webhook',
289 |         package_name: 'n8n-nodes-base',
290 |         display_name: 'Webhook',
291 |         description: 'Starts workflow on webhook call',
292 |         category: 'Core Nodes',
293 |         is_ai_tool: 0,
294 |         is_trigger: 1,
295 |         is_webhook: 1,
296 |         is_versioned: 1,
297 |         version: '1',
298 |         properties_schema: JSON.stringify([]),
299 |         operations: JSON.stringify([])
300 |       }
301 |     ];
302 | 
303 |     const db = (server as any).db;
304 |     if (db) {
305 |       const insertStmt = db.prepare(`
306 |         INSERT INTO nodes (
307 |           node_type, package_name, display_name, description, category,
308 |           is_ai_tool, is_trigger, is_webhook, is_versioned, version,
309 |           properties_schema, operations
310 |         ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
311 |       `);
312 | 
313 |       for (const node of testNodes) {
314 |         insertStmt.run(
315 |           node.node_type,
316 |           node.package_name,
317 |           node.display_name,
318 |           node.description,
319 |           node.category,
320 |           node.is_ai_tool,
321 |           node.is_trigger,
322 |           node.is_webhook,
323 |           node.is_versioned,
324 |           node.version,
325 |           node.properties_schema,
326 |           node.operations
327 |         );
328 |       }
329 |     }
330 |   });
331 | 
332 |   afterEach(() => {
333 |     delete process.env.NODE_DB_PATH;
334 |   });
335 | 
336 |   it('should support includeExamples in LIKE search', async () => {
337 |     const result = await (server as any).searchNodesLIKE('webhook', 5, { includeExamples: true });
338 | 
339 |     expect(result).toBeDefined();
340 |     expect(result.results).toBeDefined();
341 |     expect(Array.isArray(result.results)).toBe(true);
342 |   });
343 | 
344 |   it('should not include examples when includeExamples is false', async () => {
345 |     const result = await (server as any).searchNodesLIKE('webhook', 5, { includeExamples: false });
346 | 
347 |     expect(result).toBeDefined();
348 |     expect(result.results).toBeDefined();
349 |     if (result.results.length > 0) {
350 |       result.results.forEach((node: any) => {
351 |         expect(node.examples).toBeUndefined();
352 |       });
353 |     }
354 |   });
355 | });
356 | 
357 | describe('searchNodesFTS with includeExamples', () => {
358 |   let server: N8NDocumentationMCPServer;
359 | 
360 |   beforeEach(async () => {
361 |     process.env.NODE_DB_PATH = ':memory:';
362 |     server = new N8NDocumentationMCPServer();
363 |     await (server as any).initialized;
364 |   });
365 | 
366 |   afterEach(() => {
367 |     delete process.env.NODE_DB_PATH;
368 |   });
369 | 
370 |   it('should support includeExamples in FTS search', async () => {
371 |     const result = await (server as any).searchNodesFTS('webhook', 5, 'OR', { includeExamples: true });
372 | 
373 |     expect(result.results).toBeDefined();
374 |     expect(Array.isArray(result.results)).toBe(true);
375 |   });
376 | 
377 |   it('should pass options to example fetching logic', async () => {
378 |     const result = await (server as any).searchNodesFTS('http', 5, 'AND', { includeExamples: true });
379 | 
380 |     expect(result).toBeDefined();
381 |     expect(result.results).toBeDefined();
382 |   });
383 | });
384 | 
```
Page 18/59FirstPrevNextLast