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

# Directory Structure

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

# Files

--------------------------------------------------------------------------------
/tests/unit/utils/fixed-collection-validator.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, test, expect } from 'vitest';
  2 | import { FixedCollectionValidator, NodeConfig, NodeConfigValue } from '../../../src/utils/fixed-collection-validator';
  3 | 
  4 | // Type guard helper for tests
  5 | function isNodeConfig(value: NodeConfig | NodeConfigValue[] | undefined): value is NodeConfig {
  6 |   return typeof value === 'object' && value !== null && !Array.isArray(value);
  7 | }
  8 | 
  9 | describe('FixedCollectionValidator', () => {
 10 |   describe('Core Functionality', () => {
 11 |     test('should return valid for non-susceptible nodes', () => {
 12 |       const result = FixedCollectionValidator.validate('n8n-nodes-base.cron', {
 13 |         triggerTimes: { hour: 10, minute: 30 }
 14 |       });
 15 |       
 16 |       expect(result.isValid).toBe(true);
 17 |       expect(result.errors).toHaveLength(0);
 18 |     });
 19 | 
 20 |     test('should normalize node types correctly', () => {
 21 |       const nodeTypes = [
 22 |         'n8n-nodes-base.switch',
 23 |         'nodes-base.switch',
 24 |         '@n8n/n8n-nodes-langchain.switch',
 25 |         'SWITCH'
 26 |       ];
 27 | 
 28 |       nodeTypes.forEach(nodeType => {
 29 |         expect(FixedCollectionValidator.isNodeSusceptible(nodeType)).toBe(true);
 30 |       });
 31 |     });
 32 | 
 33 |     test('should get all known patterns', () => {
 34 |       const patterns = FixedCollectionValidator.getAllPatterns();
 35 |       expect(patterns.length).toBeGreaterThan(10); // We have at least 11 patterns
 36 |       expect(patterns.some(p => p.nodeType === 'switch')).toBe(true);
 37 |       expect(patterns.some(p => p.nodeType === 'summarize')).toBe(true);
 38 |     });
 39 |   });
 40 | 
 41 |   describe('Switch Node Validation', () => {
 42 |     test('should detect invalid nested conditions structure', () => {
 43 |       const invalidConfig = {
 44 |         rules: {
 45 |           conditions: {
 46 |             values: [
 47 |               {
 48 |                 value1: '={{$json.status}}',
 49 |                 operation: 'equals',
 50 |                 value2: 'active'
 51 |               }
 52 |             ]
 53 |           }
 54 |         }
 55 |       };
 56 | 
 57 |       const result = FixedCollectionValidator.validate('n8n-nodes-base.switch', invalidConfig);
 58 |       
 59 |       expect(result.isValid).toBe(false);
 60 |       expect(result.errors).toHaveLength(2); // Both rules.conditions and rules.conditions.values match
 61 |       // Check that we found the specific pattern
 62 |       const conditionsValuesError = result.errors.find(e => e.pattern === 'rules.conditions.values');
 63 |       expect(conditionsValuesError).toBeDefined();
 64 |       expect(conditionsValuesError!.message).toContain('propertyValues[itemName] is not iterable');
 65 |       expect(result.autofix).toBeDefined();
 66 |       expect(isNodeConfig(result.autofix)).toBe(true);
 67 |       if (isNodeConfig(result.autofix)) {
 68 |         expect(result.autofix.rules).toBeDefined();
 69 |         expect((result.autofix.rules as any).values).toBeDefined();
 70 |         expect((result.autofix.rules as any).values[0].outputKey).toBe('output1');
 71 |       }
 72 |     });
 73 | 
 74 |     test('should provide correct autofix for switch node', () => {
 75 |       const invalidConfig = {
 76 |         rules: {
 77 |           conditions: {
 78 |             values: [
 79 |               { value1: '={{$json.a}}', operation: 'equals', value2: '1' },
 80 |               { value1: '={{$json.b}}', operation: 'equals', value2: '2' }
 81 |             ]
 82 |           }
 83 |         }
 84 |       };
 85 | 
 86 |       const result = FixedCollectionValidator.validate('switch', invalidConfig);
 87 |       
 88 |       expect(isNodeConfig(result.autofix)).toBe(true);
 89 |       if (isNodeConfig(result.autofix)) {
 90 |         expect((result.autofix.rules as any).values).toHaveLength(2);
 91 |         expect((result.autofix.rules as any).values[0].outputKey).toBe('output1');
 92 |         expect((result.autofix.rules as any).values[1].outputKey).toBe('output2');
 93 |       }
 94 |     });
 95 |   });
 96 | 
 97 |   describe('If/Filter Node Validation', () => {
 98 |     test('should detect invalid nested values structure', () => {
 99 |       const invalidConfig = {
100 |         conditions: {
101 |           values: [
102 |             {
103 |               value1: '={{$json.age}}',
104 |               operation: 'largerEqual',
105 |               value2: 18
106 |             }
107 |           ]
108 |         }
109 |       };
110 | 
111 |       const ifResult = FixedCollectionValidator.validate('n8n-nodes-base.if', invalidConfig);
112 |       const filterResult = FixedCollectionValidator.validate('n8n-nodes-base.filter', invalidConfig);
113 |       
114 |       expect(ifResult.isValid).toBe(false);
115 |       expect(ifResult.errors[0].fix).toContain('directly, not nested under "values"');
116 |       expect(ifResult.autofix).toEqual([
117 |         {
118 |           value1: '={{$json.age}}',
119 |           operation: 'largerEqual',
120 |           value2: 18
121 |         }
122 |       ]);
123 | 
124 |       expect(filterResult.isValid).toBe(false);
125 |       expect(filterResult.autofix).toEqual(ifResult.autofix);
126 |     });
127 |   });
128 | 
129 |   describe('New Nodes Validation', () => {
130 |     test('should validate Summarize node', () => {
131 |       const invalidConfig = {
132 |         fieldsToSummarize: {
133 |           values: {
134 |             values: [
135 |               { field: 'amount', aggregation: 'sum' },
136 |               { field: 'count', aggregation: 'count' }
137 |             ]
138 |           }
139 |         }
140 |       };
141 | 
142 |       const result = FixedCollectionValidator.validate('summarize', invalidConfig);
143 |       
144 |       expect(result.isValid).toBe(false);
145 |       expect(result.errors[0].pattern).toBe('fieldsToSummarize.values.values');
146 |       expect(result.errors[0].fix).toContain('not nested values.values');
147 |       expect(isNodeConfig(result.autofix)).toBe(true);
148 |       if (isNodeConfig(result.autofix)) {
149 |         expect((result.autofix.fieldsToSummarize as any).values).toHaveLength(2);
150 |       }
151 |     });
152 | 
153 |     test('should validate Compare Datasets node', () => {
154 |       const invalidConfig = {
155 |         mergeByFields: {
156 |           values: {
157 |             values: [
158 |               { field1: 'id', field2: 'userId' }
159 |             ]
160 |           }
161 |         }
162 |       };
163 | 
164 |       const result = FixedCollectionValidator.validate('compareDatasets', invalidConfig);
165 |       
166 |       expect(result.isValid).toBe(false);
167 |       expect(result.errors[0].pattern).toBe('mergeByFields.values.values');
168 |       expect(isNodeConfig(result.autofix)).toBe(true);
169 |       if (isNodeConfig(result.autofix)) {
170 |         expect((result.autofix.mergeByFields as any).values).toHaveLength(1);
171 |       }
172 |     });
173 | 
174 |     test('should validate Sort node', () => {
175 |       const invalidConfig = {
176 |         sortFieldsUi: {
177 |           sortField: {
178 |             values: [
179 |               { fieldName: 'date', order: 'descending' }
180 |             ]
181 |           }
182 |         }
183 |       };
184 | 
185 |       const result = FixedCollectionValidator.validate('sort', invalidConfig);
186 |       
187 |       expect(result.isValid).toBe(false);
188 |       expect(result.errors[0].pattern).toBe('sortFieldsUi.sortField.values');
189 |       expect(result.errors[0].fix).toContain('not sortField.values');
190 |       expect(isNodeConfig(result.autofix)).toBe(true);
191 |       if (isNodeConfig(result.autofix)) {
192 |         expect((result.autofix.sortFieldsUi as any).sortField).toHaveLength(1);
193 |       }
194 |     });
195 | 
196 |     test('should validate Aggregate node', () => {
197 |       const invalidConfig = {
198 |         fieldsToAggregate: {
199 |           fieldToAggregate: {
200 |             values: [
201 |               { fieldToAggregate: 'price', aggregation: 'average' }
202 |             ]
203 |           }
204 |         }
205 |       };
206 | 
207 |       const result = FixedCollectionValidator.validate('aggregate', invalidConfig);
208 |       
209 |       expect(result.isValid).toBe(false);
210 |       expect(result.errors[0].pattern).toBe('fieldsToAggregate.fieldToAggregate.values');
211 |       expect(isNodeConfig(result.autofix)).toBe(true);
212 |       if (isNodeConfig(result.autofix)) {
213 |         expect((result.autofix.fieldsToAggregate as any).fieldToAggregate).toHaveLength(1);
214 |       }
215 |     });
216 | 
217 |     test('should validate Set node', () => {
218 |       const invalidConfig = {
219 |         fields: {
220 |           values: {
221 |             values: [
222 |               { name: 'status', value: 'active' }
223 |             ]
224 |           }
225 |         }
226 |       };
227 | 
228 |       const result = FixedCollectionValidator.validate('set', invalidConfig);
229 |       
230 |       expect(result.isValid).toBe(false);
231 |       expect(result.errors[0].pattern).toBe('fields.values.values');
232 |       expect(isNodeConfig(result.autofix)).toBe(true);
233 |       if (isNodeConfig(result.autofix)) {
234 |         expect((result.autofix.fields as any).values).toHaveLength(1);
235 |       }
236 |     });
237 | 
238 |     test('should validate HTML node', () => {
239 |       const invalidConfig = {
240 |         extractionValues: {
241 |           values: {
242 |             values: [
243 |               { key: 'title', cssSelector: 'h1' }
244 |             ]
245 |           }
246 |         }
247 |       };
248 | 
249 |       const result = FixedCollectionValidator.validate('html', invalidConfig);
250 |       
251 |       expect(result.isValid).toBe(false);
252 |       expect(result.errors[0].pattern).toBe('extractionValues.values.values');
253 |       expect(isNodeConfig(result.autofix)).toBe(true);
254 |       if (isNodeConfig(result.autofix)) {
255 |         expect((result.autofix.extractionValues as any).values).toHaveLength(1);
256 |       }
257 |     });
258 | 
259 |     test('should validate HTTP Request node', () => {
260 |       const invalidConfig = {
261 |         body: {
262 |           parameters: {
263 |             values: [
264 |               { name: 'api_key', value: '123' }
265 |             ]
266 |           }
267 |         }
268 |       };
269 | 
270 |       const result = FixedCollectionValidator.validate('httpRequest', invalidConfig);
271 |       
272 |       expect(result.isValid).toBe(false);
273 |       expect(result.errors[0].pattern).toBe('body.parameters.values');
274 |       expect(result.errors[0].fix).toContain('not parameters.values');
275 |       expect(isNodeConfig(result.autofix)).toBe(true);
276 |       if (isNodeConfig(result.autofix)) {
277 |         expect((result.autofix.body as any).parameters).toHaveLength(1);
278 |       }
279 |     });
280 | 
281 |     test('should validate Airtable node', () => {
282 |       const invalidConfig = {
283 |         sort: {
284 |           sortField: {
285 |             values: [
286 |               { fieldName: 'Created', direction: 'desc' }
287 |             ]
288 |           }
289 |         }
290 |       };
291 | 
292 |       const result = FixedCollectionValidator.validate('airtable', invalidConfig);
293 |       
294 |       expect(result.isValid).toBe(false);
295 |       expect(result.errors[0].pattern).toBe('sort.sortField.values');
296 |       expect(isNodeConfig(result.autofix)).toBe(true);
297 |       if (isNodeConfig(result.autofix)) {
298 |         expect((result.autofix.sort as any).sortField).toHaveLength(1);
299 |       }
300 |     });
301 |   });
302 | 
303 |   describe('Edge Cases', () => {
304 |     test('should handle empty config', () => {
305 |       const result = FixedCollectionValidator.validate('switch', {});
306 |       expect(result.isValid).toBe(true);
307 |     });
308 | 
309 |     test('should handle null/undefined properties', () => {
310 |       const result = FixedCollectionValidator.validate('switch', {
311 |         rules: null
312 |       });
313 |       expect(result.isValid).toBe(true);
314 |     });
315 | 
316 |     test('should handle valid structures', () => {
317 |       const validSwitch = {
318 |         rules: {
319 |           values: [
320 |             {
321 |               conditions: { value1: '={{$json.x}}', operation: 'equals', value2: 1 },
322 |               outputKey: 'output1'
323 |             }
324 |           ]
325 |         }
326 |       };
327 | 
328 |       const result = FixedCollectionValidator.validate('switch', validSwitch);
329 |       expect(result.isValid).toBe(true);
330 |       expect(result.errors).toHaveLength(0);
331 |     });
332 | 
333 |     test('should handle deeply nested invalid structures', () => {
334 |       const deeplyNested = {
335 |         rules: {
336 |           conditions: {
337 |             values: [
338 |               {
339 |                 value1: '={{$json.deep}}',
340 |                 operation: 'equals',
341 |                 value2: 'nested'
342 |               }
343 |             ]
344 |           }
345 |         }
346 |       };
347 | 
348 |       const result = FixedCollectionValidator.validate('switch', deeplyNested);
349 |       expect(result.isValid).toBe(false);
350 |       expect(result.errors).toHaveLength(2); // Both patterns match
351 |     });
352 |   });
353 | 
354 |   describe('Private Method Testing (through public API)', () => {
355 |     describe('isNodeConfig Type Guard', () => {
356 |       test('should return true for plain objects', () => {
357 |         const validConfig = { property: 'value' };
358 |         const result = FixedCollectionValidator.validate('switch', validConfig);
359 |         // Type guard is tested indirectly through validation
360 |         expect(result).toBeDefined();
361 |       });
362 | 
363 |       test('should handle null values correctly', () => {
364 |         const result = FixedCollectionValidator.validate('switch', null as any);
365 |         expect(result.isValid).toBe(true);
366 |         expect(result.errors).toHaveLength(0);
367 |       });
368 | 
369 |       test('should handle undefined values correctly', () => {
370 |         const result = FixedCollectionValidator.validate('switch', undefined as any);
371 |         expect(result.isValid).toBe(true);
372 |         expect(result.errors).toHaveLength(0);
373 |       });
374 | 
375 |       test('should handle arrays correctly', () => {
376 |         const result = FixedCollectionValidator.validate('switch', [] as any);
377 |         expect(result.isValid).toBe(true);
378 |         expect(result.errors).toHaveLength(0);
379 |       });
380 | 
381 |       test('should handle primitive values correctly', () => {
382 |         const result1 = FixedCollectionValidator.validate('switch', 'string' as any);
383 |         expect(result1.isValid).toBe(true);
384 |         
385 |         const result2 = FixedCollectionValidator.validate('switch', 123 as any);
386 |         expect(result2.isValid).toBe(true);
387 |         
388 |         const result3 = FixedCollectionValidator.validate('switch', true as any);
389 |         expect(result3.isValid).toBe(true);
390 |       });
391 |     });
392 | 
393 |     describe('getNestedValue Testing', () => {
394 |       test('should handle simple nested paths', () => {
395 |         const config = {
396 |           rules: {
397 |             conditions: {
398 |               values: [{ test: 'value' }]
399 |             }
400 |           }
401 |         };
402 |         
403 |         const result = FixedCollectionValidator.validate('switch', config);
404 |         expect(result.isValid).toBe(false); // This tests the nested value extraction
405 |       });
406 | 
407 |       test('should handle non-existent paths gracefully', () => {
408 |         const config = {
409 |           rules: {
410 |             // missing conditions property
411 |           }
412 |         };
413 |         
414 |         const result = FixedCollectionValidator.validate('switch', config);
415 |         expect(result.isValid).toBe(true); // Should not find invalid structure
416 |       });
417 | 
418 |       test('should handle interrupted paths (null/undefined in middle)', () => {
419 |         const config = {
420 |           rules: null
421 |         };
422 |         
423 |         const result = FixedCollectionValidator.validate('switch', config);
424 |         expect(result.isValid).toBe(true);
425 |       });
426 | 
427 |       test('should handle array interruptions in path', () => {
428 |         const config = {
429 |           rules: [1, 2, 3] // array instead of object
430 |         };
431 |         
432 |         const result = FixedCollectionValidator.validate('switch', config);
433 |         expect(result.isValid).toBe(true); // Should not find the pattern
434 |       });
435 |     });
436 | 
437 |     describe('Circular Reference Protection', () => {
438 |       test('should handle circular references in config', () => {
439 |         const config: any = {
440 |           rules: {
441 |             conditions: {}
442 |           }
443 |         };
444 |         // Create circular reference
445 |         config.rules.conditions.circular = config.rules;
446 |         
447 |         const result = FixedCollectionValidator.validate('switch', config);
448 |         // Should not crash and should detect the pattern (result is false because it finds rules.conditions)
449 |         expect(result.isValid).toBe(false);
450 |         expect(result.errors.length).toBeGreaterThan(0);
451 |       });
452 | 
453 |       test('should handle self-referencing objects', () => {
454 |         const config: any = {
455 |           rules: {}
456 |         };
457 |         config.rules.self = config.rules;
458 |         
459 |         const result = FixedCollectionValidator.validate('switch', config);
460 |         expect(result.isValid).toBe(true);
461 |       });
462 | 
463 |       test('should handle deeply nested circular references', () => {
464 |         const config: any = {
465 |           rules: {
466 |             conditions: {
467 |               values: {}
468 |             }
469 |           }
470 |         };
471 |         config.rules.conditions.values.back = config;
472 |         
473 |         const result = FixedCollectionValidator.validate('switch', config);
474 |         // Should detect the problematic pattern: rules.conditions.values exists
475 |         expect(result.isValid).toBe(false);
476 |         expect(result.errors.length).toBeGreaterThan(0);
477 |       });
478 |     });
479 | 
480 |     describe('Deep Copying in getAllPatterns', () => {
481 |       test('should return independent copies of patterns', () => {
482 |         const patterns1 = FixedCollectionValidator.getAllPatterns();
483 |         const patterns2 = FixedCollectionValidator.getAllPatterns();
484 |         
485 |         // Modify one copy
486 |         patterns1[0].invalidPatterns.push('test.pattern');
487 |         
488 |         // Other copy should be unaffected
489 |         expect(patterns2[0].invalidPatterns).not.toContain('test.pattern');
490 |       });
491 | 
492 |       test('should deep copy invalidPatterns arrays', () => {
493 |         const patterns = FixedCollectionValidator.getAllPatterns();
494 |         const switchPattern = patterns.find(p => p.nodeType === 'switch')!;
495 |         
496 |         expect(switchPattern.invalidPatterns).toBeInstanceOf(Array);
497 |         expect(switchPattern.invalidPatterns.length).toBeGreaterThan(0);
498 |         
499 |         // Ensure it's a different array instance
500 |         const originalPatterns = FixedCollectionValidator.getAllPatterns();
501 |         const originalSwitch = originalPatterns.find(p => p.nodeType === 'switch')!;
502 |         
503 |         expect(switchPattern.invalidPatterns).not.toBe(originalSwitch.invalidPatterns);
504 |         expect(switchPattern.invalidPatterns).toEqual(originalSwitch.invalidPatterns);
505 |       });
506 |     });
507 |   });
508 | 
509 |   describe('Enhanced Edge Cases', () => {
510 |     test('should handle hasOwnProperty edge case', () => {
511 |       const config = Object.create(null);
512 |       config.rules = {
513 |         conditions: {
514 |           values: [{ test: 'value' }]
515 |         }
516 |       };
517 |       
518 |       const result = FixedCollectionValidator.validate('switch', config);
519 |       expect(result.isValid).toBe(false); // Should still detect the pattern
520 |     });
521 | 
522 |     test('should handle prototype pollution attempts', () => {
523 |       const config = {
524 |         rules: {
525 |           conditions: {
526 |             values: [{ test: 'value' }]
527 |           }
528 |         }
529 |       };
530 |       
531 |       // Add prototype property (should be ignored by hasOwnProperty check)
532 |       (Object.prototype as any).maliciousProperty = 'evil';
533 |       
534 |       try {
535 |         const result = FixedCollectionValidator.validate('switch', config);
536 |         expect(result.isValid).toBe(false);
537 |         expect(result.errors).toHaveLength(2);
538 |       } finally {
539 |         delete (Object.prototype as any).maliciousProperty;
540 |       }
541 |     });
542 | 
543 |     test('should handle objects with numeric keys', () => {
544 |       const config = {
545 |         rules: {
546 |           '0': {
547 |             values: [{ test: 'value' }]
548 |           }
549 |         }
550 |       };
551 |       
552 |       const result = FixedCollectionValidator.validate('switch', config);
553 |       expect(result.isValid).toBe(true); // Should not match 'conditions' pattern
554 |     });
555 | 
556 |     test('should handle very deep nesting without crashing', () => {
557 |       let deepConfig: any = {};
558 |       let current = deepConfig;
559 |       
560 |       // Create 100 levels deep
561 |       for (let i = 0; i < 100; i++) {
562 |         current.next = {};
563 |         current = current.next;
564 |       }
565 |       
566 |       const result = FixedCollectionValidator.validate('switch', deepConfig);
567 |       expect(result.isValid).toBe(true);
568 |     });
569 |   });
570 | 
571 |   describe('Alternative Node Type Formats', () => {
572 |     test('should handle all node type normalization cases', () => {
573 |       const testCases = [
574 |         'n8n-nodes-base.switch',
575 |         'nodes-base.switch', 
576 |         '@n8n/n8n-nodes-langchain.switch',
577 |         'SWITCH',
578 |         'Switch',
579 |         'sWiTcH'
580 |       ];
581 |       
582 |       testCases.forEach(nodeType => {
583 |         expect(FixedCollectionValidator.isNodeSusceptible(nodeType)).toBe(true);
584 |       });
585 |     });
586 | 
587 |     test('should handle empty and invalid node types', () => {
588 |       expect(FixedCollectionValidator.isNodeSusceptible('')).toBe(false);
589 |       expect(FixedCollectionValidator.isNodeSusceptible('unknown-node')).toBe(false);
590 |       expect(FixedCollectionValidator.isNodeSusceptible('n8n-nodes-base.unknown')).toBe(false);
591 |     });
592 |   });
593 | 
594 |   describe('Complex Autofix Scenarios', () => {
595 |     test('should handle switch autofix with non-array values', () => {
596 |       const invalidConfig = {
597 |         rules: {
598 |           conditions: {
599 |             values: { single: 'condition' } // Object instead of array
600 |           }
601 |         }
602 |       };
603 | 
604 |       const result = FixedCollectionValidator.validate('switch', invalidConfig);
605 |       expect(result.isValid).toBe(false);
606 |       expect(isNodeConfig(result.autofix)).toBe(true);
607 |       
608 |       if (isNodeConfig(result.autofix)) {
609 |         const values = (result.autofix.rules as any).values;
610 |         expect(values).toHaveLength(1);
611 |         expect(values[0].conditions).toEqual({ single: 'condition' });
612 |         expect(values[0].outputKey).toBe('output1');
613 |       }
614 |     });
615 | 
616 |     test('should handle if/filter autofix with object values', () => {
617 |       const invalidConfig = {
618 |         conditions: {
619 |           values: { type: 'single', condition: 'test' }
620 |         }
621 |       };
622 | 
623 |       const result = FixedCollectionValidator.validate('if', invalidConfig);
624 |       expect(result.isValid).toBe(false);
625 |       expect(result.autofix).toEqual({ type: 'single', condition: 'test' });
626 |     });
627 | 
628 |     test('should handle applyAutofix for if/filter with null values', () => {
629 |       const invalidConfig = {
630 |         conditions: {
631 |           values: null
632 |         }
633 |       };
634 | 
635 |       const pattern = FixedCollectionValidator.getAllPatterns().find(p => p.nodeType === 'if')!;
636 |       const fixed = FixedCollectionValidator.applyAutofix(invalidConfig, pattern);
637 |       
638 |       // Should return the original config when values is null
639 |       expect(fixed).toEqual(invalidConfig);
640 |     });
641 | 
642 |     test('should handle applyAutofix for if/filter with undefined values', () => {
643 |       const invalidConfig = {
644 |         conditions: {
645 |           values: undefined
646 |         }
647 |       };
648 | 
649 |       const pattern = FixedCollectionValidator.getAllPatterns().find(p => p.nodeType === 'if')!;
650 |       const fixed = FixedCollectionValidator.applyAutofix(invalidConfig, pattern);
651 |       
652 |       // Should return the original config when values is undefined
653 |       expect(fixed).toEqual(invalidConfig);
654 |     });
655 |   });
656 | 
657 |   describe('applyAutofix Method', () => {
658 |     test('should apply autofix correctly for if/filter nodes', () => {
659 |       const invalidConfig = {
660 |         conditions: {
661 |           values: [
662 |             { value1: '={{$json.test}}', operation: 'equals', value2: 'yes' }
663 |           ]
664 |         }
665 |       };
666 | 
667 |       const pattern = FixedCollectionValidator.getAllPatterns().find(p => p.nodeType === 'if');
668 |       const fixed = FixedCollectionValidator.applyAutofix(invalidConfig, pattern!);
669 |       
670 |       expect(fixed).toEqual([
671 |         { value1: '={{$json.test}}', operation: 'equals', value2: 'yes' }
672 |       ]);
673 |     });
674 | 
675 |     test('should return original config for non-if/filter nodes', () => {
676 |       const invalidConfig = {
677 |         fieldsToSummarize: {
678 |           values: {
679 |             values: [{ field: 'test' }]
680 |           }
681 |         }
682 |       };
683 | 
684 |       const pattern = FixedCollectionValidator.getAllPatterns().find(p => p.nodeType === 'summarize');
685 |       const fixed = FixedCollectionValidator.applyAutofix(invalidConfig, pattern!);
686 |       
687 |       expect(isNodeConfig(fixed)).toBe(true);
688 |       if (isNodeConfig(fixed)) {
689 |         expect((fixed.fieldsToSummarize as any).values).toEqual([{ field: 'test' }]);
690 |       }
691 |     });
692 | 
693 |     test('should handle filter node applyAutofix edge cases', () => {
694 |       const invalidConfig = {
695 |         conditions: {
696 |           values: 'string-value' // Invalid type
697 |         }
698 |       };
699 | 
700 |       const pattern = FixedCollectionValidator.getAllPatterns().find(p => p.nodeType === 'filter');
701 |       const fixed = FixedCollectionValidator.applyAutofix(invalidConfig, pattern!);
702 |       
703 |       // Should return original config when values is not object/array
704 |       expect(fixed).toEqual(invalidConfig);
705 |     });
706 |   });
707 | 
708 |   describe('Missing Function Coverage Tests', () => {
709 |     test('should test all generateFixMessage cases', () => {
710 |       // Test each node type's fix message generation through validation
711 |       const nodeConfigs = [
712 |         { nodeType: 'switch', config: { rules: { conditions: { values: [] } } } },
713 |         { nodeType: 'if', config: { conditions: { values: [] } } },
714 |         { nodeType: 'filter', config: { conditions: { values: [] } } },
715 |         { nodeType: 'summarize', config: { fieldsToSummarize: { values: { values: [] } } } },
716 |         { nodeType: 'comparedatasets', config: { mergeByFields: { values: { values: [] } } } },
717 |         { nodeType: 'sort', config: { sortFieldsUi: { sortField: { values: [] } } } },
718 |         { nodeType: 'aggregate', config: { fieldsToAggregate: { fieldToAggregate: { values: [] } } } },
719 |         { nodeType: 'set', config: { fields: { values: { values: [] } } } },
720 |         { nodeType: 'html', config: { extractionValues: { values: { values: [] } } } },
721 |         { nodeType: 'httprequest', config: { body: { parameters: { values: [] } } } },
722 |         { nodeType: 'airtable', config: { sort: { sortField: { values: [] } } } },
723 |       ];
724 | 
725 |       nodeConfigs.forEach(({ nodeType, config }) => {
726 |         const result = FixedCollectionValidator.validate(nodeType, config);
727 |         expect(result.isValid).toBe(false);
728 |         expect(result.errors.length).toBeGreaterThan(0);
729 |         expect(result.errors[0].fix).toBeDefined();
730 |         expect(typeof result.errors[0].fix).toBe('string');
731 |       });
732 |     });
733 | 
734 |     test('should test default case in generateFixMessage', () => {
735 |       // Create a custom pattern with unknown nodeType to test default case
736 |       const mockPattern = {
737 |         nodeType: 'unknown-node-type',
738 |         property: 'testProperty',
739 |         expectedStructure: 'test.structure',
740 |         invalidPatterns: ['test.invalid.pattern']
741 |       };
742 | 
743 |       // We can't directly test the private generateFixMessage method,
744 |       // but we can test through the validation logic by temporarily adding to KNOWN_PATTERNS
745 |       // Instead, let's verify the method works by checking error messages contain the expected structure
746 |       const patterns = FixedCollectionValidator.getAllPatterns();
747 |       expect(patterns.length).toBeGreaterThan(0);
748 |       
749 |       // Ensure we have patterns that would exercise different fix message paths
750 |       const switchPattern = patterns.find(p => p.nodeType === 'switch');
751 |       expect(switchPattern).toBeDefined();
752 |       expect(switchPattern!.expectedStructure).toBe('rules.values array');
753 |     });
754 | 
755 |     test('should exercise hasInvalidStructure edge cases', () => {
756 |       // Test with property that exists but is not at the end of the pattern
757 |       const config = {
758 |         rules: {
759 |           conditions: 'string-value' // Not an object, so traversal should stop
760 |         }
761 |       };
762 | 
763 |       const result = FixedCollectionValidator.validate('switch', config);
764 |       expect(result.isValid).toBe(false); // Should still detect rules.conditions pattern
765 |     });
766 | 
767 |     test('should test getNestedValue with complex paths', () => {
768 |       // Test through hasInvalidStructure which uses getNestedValue
769 |       const config = {
770 |         deeply: {
771 |           nested: {
772 |             path: {
773 |               to: {
774 |                 value: 'exists'
775 |               }
776 |             }
777 |           }
778 |         }
779 |       };
780 | 
781 |       // This would exercise the getNestedValue function through hasInvalidStructure
782 |       const result = FixedCollectionValidator.validate('switch', config);
783 |       expect(result.isValid).toBe(true); // No matching patterns
784 |     });
785 |   });
786 | });
```

--------------------------------------------------------------------------------
/tests/unit/templates/template-repository-metadata.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
  2 | import { TemplateRepository } from '../../../src/templates/template-repository';
  3 | import { DatabaseAdapter, PreparedStatement, RunResult } from '../../../src/database/database-adapter';
  4 | import { logger } from '../../../src/utils/logger';
  5 | 
  6 | // Mock logger
  7 | vi.mock('../../../src/utils/logger', () => ({
  8 |   logger: {
  9 |     info: vi.fn(),
 10 |     warn: vi.fn(),
 11 |     error: vi.fn(),
 12 |     debug: vi.fn()
 13 |   }
 14 | }));
 15 | 
 16 | // Mock template sanitizer
 17 | vi.mock('../../../src/utils/template-sanitizer', () => {
 18 |   class MockTemplateSanitizer {
 19 |     sanitizeWorkflow = vi.fn((workflow) => ({ sanitized: workflow, wasModified: false }));
 20 |     detectTokens = vi.fn(() => []);
 21 |   }
 22 | 
 23 |   return {
 24 |     TemplateSanitizer: MockTemplateSanitizer
 25 |   };
 26 | });
 27 | 
 28 | // Create mock database adapter
 29 | class MockDatabaseAdapter implements DatabaseAdapter {
 30 |   private statements = new Map<string, MockPreparedStatement>();
 31 |   private execCalls: string[] = [];
 32 |   private _fts5Support = true;
 33 | 
 34 |   prepare = vi.fn((sql: string) => {
 35 |     if (!this.statements.has(sql)) {
 36 |       this.statements.set(sql, new MockPreparedStatement(sql));
 37 |     }
 38 |     return this.statements.get(sql)!;
 39 |   });
 40 | 
 41 |   exec = vi.fn((sql: string) => {
 42 |     this.execCalls.push(sql);
 43 |   });
 44 |   close = vi.fn();
 45 |   pragma = vi.fn();
 46 |   transaction = vi.fn((fn: () => any) => fn());
 47 |   checkFTS5Support = vi.fn(() => this._fts5Support);
 48 |   inTransaction = false;
 49 | 
 50 |   _setFTS5Support(supported: boolean) {
 51 |     this._fts5Support = supported;
 52 |   }
 53 | 
 54 |   _getStatement(sql: string) {
 55 |     return this.statements.get(sql);
 56 |   }
 57 | 
 58 |   _getExecCalls() {
 59 |     return this.execCalls;
 60 |   }
 61 | 
 62 |   _clearExecCalls() {
 63 |     this.execCalls = [];
 64 |   }
 65 | }
 66 | 
 67 | class MockPreparedStatement implements PreparedStatement {
 68 |   public mockResults: any[] = [];
 69 |   public capturedParams: any[][] = [];
 70 | 
 71 |   run = vi.fn((...params: any[]): RunResult => {
 72 |     this.capturedParams.push(params);
 73 |     return { changes: 1, lastInsertRowid: 1 };
 74 |   });
 75 | 
 76 |   get = vi.fn((...params: any[]) => {
 77 |     this.capturedParams.push(params);
 78 |     return this.mockResults[0] || null;
 79 |   });
 80 | 
 81 |   all = vi.fn((...params: any[]) => {
 82 |     this.capturedParams.push(params);
 83 |     return this.mockResults;
 84 |   });
 85 | 
 86 |   iterate = vi.fn();
 87 |   pluck = vi.fn(() => this);
 88 |   expand = vi.fn(() => this);
 89 |   raw = vi.fn(() => this);
 90 |   columns = vi.fn(() => []);
 91 |   bind = vi.fn(() => this);
 92 | 
 93 |   constructor(private sql: string) {}
 94 | 
 95 |   _setMockResults(results: any[]) {
 96 |     this.mockResults = results;
 97 |   }
 98 | 
 99 |   _getCapturedParams() {
100 |     return this.capturedParams;
101 |   }
102 | }
103 | 
104 | describe('TemplateRepository - Metadata Filter Tests', () => {
105 |   let repository: TemplateRepository;
106 |   let mockAdapter: MockDatabaseAdapter;
107 | 
108 |   beforeEach(() => {
109 |     vi.clearAllMocks();
110 |     mockAdapter = new MockDatabaseAdapter();
111 |     repository = new TemplateRepository(mockAdapter);
112 |   });
113 | 
114 |   afterEach(() => {
115 |     vi.clearAllMocks();
116 |   });
117 | 
118 |   describe('buildMetadataFilterConditions - All Filter Combinations', () => {
119 |     it('should build conditions with no filters', () => {
120 |       const stmt = new MockPreparedStatement('');
121 |       stmt._setMockResults([]);
122 |       mockAdapter.prepare = vi.fn().mockReturnValue(stmt);
123 | 
124 |       repository.searchTemplatesByMetadata({}, 10, 0);
125 | 
126 |       const prepareCall = mockAdapter.prepare.mock.calls[0][0];
127 |       // Should only have the base condition
128 |       expect(prepareCall).toContain('metadata_json IS NOT NULL');
129 |       // Should not have any additional conditions
130 |       expect(prepareCall).not.toContain("json_extract(metadata_json, '$.categories')");
131 |       expect(prepareCall).not.toContain("json_extract(metadata_json, '$.complexity')");
132 |     });
133 | 
134 |     it('should build conditions with only category filter', () => {
135 |       const stmt = new MockPreparedStatement('');
136 |       stmt._setMockResults([]);
137 |       mockAdapter.prepare = vi.fn().mockReturnValue(stmt);
138 | 
139 |       repository.searchTemplatesByMetadata({ category: 'automation' }, 10, 0);
140 | 
141 |       const prepareCall = mockAdapter.prepare.mock.calls[0][0];
142 |       expect(prepareCall).toContain('metadata_json IS NOT NULL');
143 |       expect(prepareCall).toContain("json_extract(metadata_json, '$.categories') LIKE '%' || ? || '%'");
144 | 
145 |       const capturedParams = stmt._getCapturedParams();
146 |       expect(capturedParams[0][0]).toBe('automation');
147 |     });
148 | 
149 |     it('should build conditions with only complexity filter', () => {
150 |       const stmt = new MockPreparedStatement('');
151 |       stmt._setMockResults([]);
152 |       mockAdapter.prepare = vi.fn().mockReturnValue(stmt);
153 | 
154 |       repository.searchTemplatesByMetadata({ complexity: 'simple' }, 10, 0);
155 | 
156 |       const prepareCall = mockAdapter.prepare.mock.calls[0][0];
157 |       expect(prepareCall).toContain('metadata_json IS NOT NULL');
158 |       expect(prepareCall).toContain("json_extract(metadata_json, '$.complexity') = ?");
159 | 
160 |       const capturedParams = stmt._getCapturedParams();
161 |       expect(capturedParams[0][0]).toBe('simple');
162 |     });
163 | 
164 |     it('should build conditions with only maxSetupMinutes filter', () => {
165 |       const stmt = new MockPreparedStatement('');
166 |       stmt._setMockResults([]);
167 |       mockAdapter.prepare = vi.fn().mockReturnValue(stmt);
168 | 
169 |       repository.searchTemplatesByMetadata({ maxSetupMinutes: 30 }, 10, 0);
170 | 
171 |       const prepareCall = mockAdapter.prepare.mock.calls[0][0];
172 |       expect(prepareCall).toContain('metadata_json IS NOT NULL');
173 |       expect(prepareCall).toContain("CAST(json_extract(metadata_json, '$.estimated_setup_minutes') AS INTEGER) <= ?");
174 | 
175 |       const capturedParams = stmt._getCapturedParams();
176 |       expect(capturedParams[0][0]).toBe(30);
177 |     });
178 | 
179 |     it('should build conditions with only minSetupMinutes filter', () => {
180 |       const stmt = new MockPreparedStatement('');
181 |       stmt._setMockResults([]);
182 |       mockAdapter.prepare = vi.fn().mockReturnValue(stmt);
183 | 
184 |       repository.searchTemplatesByMetadata({ minSetupMinutes: 10 }, 10, 0);
185 | 
186 |       const prepareCall = mockAdapter.prepare.mock.calls[0][0];
187 |       expect(prepareCall).toContain('metadata_json IS NOT NULL');
188 |       expect(prepareCall).toContain("CAST(json_extract(metadata_json, '$.estimated_setup_minutes') AS INTEGER) >= ?");
189 | 
190 |       const capturedParams = stmt._getCapturedParams();
191 |       expect(capturedParams[0][0]).toBe(10);
192 |     });
193 | 
194 |     it('should build conditions with only requiredService filter', () => {
195 |       const stmt = new MockPreparedStatement('');
196 |       stmt._setMockResults([]);
197 |       mockAdapter.prepare = vi.fn().mockReturnValue(stmt);
198 | 
199 |       repository.searchTemplatesByMetadata({ requiredService: 'slack' }, 10, 0);
200 | 
201 |       const prepareCall = mockAdapter.prepare.mock.calls[0][0];
202 |       expect(prepareCall).toContain('metadata_json IS NOT NULL');
203 |       expect(prepareCall).toContain("json_extract(metadata_json, '$.required_services') LIKE '%' || ? || '%'");
204 | 
205 |       const capturedParams = stmt._getCapturedParams();
206 |       expect(capturedParams[0][0]).toBe('slack');
207 |     });
208 | 
209 |     it('should build conditions with only targetAudience filter', () => {
210 |       const stmt = new MockPreparedStatement('');
211 |       stmt._setMockResults([]);
212 |       mockAdapter.prepare = vi.fn().mockReturnValue(stmt);
213 | 
214 |       repository.searchTemplatesByMetadata({ targetAudience: 'developers' }, 10, 0);
215 | 
216 |       const prepareCall = mockAdapter.prepare.mock.calls[0][0];
217 |       expect(prepareCall).toContain('metadata_json IS NOT NULL');
218 |       expect(prepareCall).toContain("json_extract(metadata_json, '$.target_audience') LIKE '%' || ? || '%'");
219 | 
220 |       const capturedParams = stmt._getCapturedParams();
221 |       expect(capturedParams[0][0]).toBe('developers');
222 |     });
223 | 
224 |     it('should build conditions with all filters combined', () => {
225 |       const stmt = new MockPreparedStatement('');
226 |       stmt._setMockResults([]);
227 |       mockAdapter.prepare = vi.fn().mockReturnValue(stmt);
228 | 
229 |       repository.searchTemplatesByMetadata({
230 |         category: 'automation',
231 |         complexity: 'medium',
232 |         maxSetupMinutes: 60,
233 |         minSetupMinutes: 15,
234 |         requiredService: 'openai',
235 |         targetAudience: 'marketers'
236 |       }, 10, 0);
237 | 
238 |       const prepareCall = mockAdapter.prepare.mock.calls[0][0];
239 |       expect(prepareCall).toContain('metadata_json IS NOT NULL');
240 |       expect(prepareCall).toContain("json_extract(metadata_json, '$.categories') LIKE '%' || ? || '%'");
241 |       expect(prepareCall).toContain("json_extract(metadata_json, '$.complexity') = ?");
242 |       expect(prepareCall).toContain("CAST(json_extract(metadata_json, '$.estimated_setup_minutes') AS INTEGER) <= ?");
243 |       expect(prepareCall).toContain("CAST(json_extract(metadata_json, '$.estimated_setup_minutes') AS INTEGER) >= ?");
244 |       expect(prepareCall).toContain("json_extract(metadata_json, '$.required_services') LIKE '%' || ? || '%'");
245 |       expect(prepareCall).toContain("json_extract(metadata_json, '$.target_audience') LIKE '%' || ? || '%'");
246 | 
247 |       const capturedParams = stmt._getCapturedParams();
248 |       expect(capturedParams[0]).toEqual(['automation', 'medium', 60, 15, 'openai', 'marketers', 10, 0]);
249 |     });
250 | 
251 |     it('should build conditions with partial filter combinations', () => {
252 |       const stmt = new MockPreparedStatement('');
253 |       stmt._setMockResults([]);
254 |       mockAdapter.prepare = vi.fn().mockReturnValue(stmt);
255 | 
256 |       repository.searchTemplatesByMetadata({
257 |         category: 'data-processing',
258 |         maxSetupMinutes: 45,
259 |         targetAudience: 'analysts'
260 |       }, 10, 0);
261 | 
262 |       const prepareCall = mockAdapter.prepare.mock.calls[0][0];
263 |       expect(prepareCall).toContain('metadata_json IS NOT NULL');
264 |       expect(prepareCall).toContain("json_extract(metadata_json, '$.categories') LIKE '%' || ? || '%'");
265 |       expect(prepareCall).toContain("CAST(json_extract(metadata_json, '$.estimated_setup_minutes') AS INTEGER) <= ?");
266 |       expect(prepareCall).toContain("json_extract(metadata_json, '$.target_audience') LIKE '%' || ? || '%'");
267 |       // Should not have complexity, minSetupMinutes, or requiredService conditions
268 |       expect(prepareCall).not.toContain("json_extract(metadata_json, '$.complexity') = ?");
269 |       expect(prepareCall).not.toContain("CAST(json_extract(metadata_json, '$.estimated_setup_minutes') AS INTEGER) >= ?");
270 |       expect(prepareCall).not.toContain("json_extract(metadata_json, '$.required_services') LIKE '%' || ? || '%'");
271 | 
272 |       const capturedParams = stmt._getCapturedParams();
273 |       expect(capturedParams[0]).toEqual(['data-processing', 45, 'analysts', 10, 0]);
274 |     });
275 | 
276 |     it('should handle complexity variations', () => {
277 |       const stmt = new MockPreparedStatement('');
278 |       stmt._setMockResults([]);
279 |       mockAdapter.prepare = vi.fn().mockReturnValue(stmt);
280 | 
281 |       // Test each complexity level
282 |       const complexityLevels: Array<'simple' | 'medium' | 'complex'> = ['simple', 'medium', 'complex'];
283 | 
284 |       complexityLevels.forEach((complexity) => {
285 |         vi.clearAllMocks();
286 |         stmt.capturedParams = [];
287 | 
288 |         repository.searchTemplatesByMetadata({ complexity }, 10, 0);
289 | 
290 |         const capturedParams = stmt._getCapturedParams();
291 |         expect(capturedParams[0][0]).toBe(complexity);
292 |       });
293 |     });
294 | 
295 |     it('should handle setup minutes edge cases', () => {
296 |       const stmt = new MockPreparedStatement('');
297 |       stmt._setMockResults([]);
298 |       mockAdapter.prepare = vi.fn().mockReturnValue(stmt);
299 | 
300 |       // Test zero values
301 |       repository.searchTemplatesByMetadata({ maxSetupMinutes: 0, minSetupMinutes: 0 }, 10, 0);
302 | 
303 |       let capturedParams = stmt._getCapturedParams();
304 |       expect(capturedParams[0]).toContain(0);
305 | 
306 |       // Test very large values
307 |       vi.clearAllMocks();
308 |       stmt.capturedParams = [];
309 |       repository.searchTemplatesByMetadata({ maxSetupMinutes: 999999 }, 10, 0);
310 | 
311 |       capturedParams = stmt._getCapturedParams();
312 |       expect(capturedParams[0]).toContain(999999);
313 | 
314 |       // Test negative values (should still work, though might not make sense semantically)
315 |       vi.clearAllMocks();
316 |       stmt.capturedParams = [];
317 |       repository.searchTemplatesByMetadata({ minSetupMinutes: -10 }, 10, 0);
318 | 
319 |       capturedParams = stmt._getCapturedParams();
320 |       expect(capturedParams[0]).toContain(-10);
321 |     });
322 | 
323 |     it('should sanitize special characters in string filters', () => {
324 |       const stmt = new MockPreparedStatement('');
325 |       stmt._setMockResults([]);
326 |       mockAdapter.prepare = vi.fn().mockReturnValue(stmt);
327 | 
328 |       const specialCategory = 'test"with\'quotes';
329 |       const specialService = 'service\\with\\backslashes';
330 |       const specialAudience = 'audience\nwith\nnewlines';
331 | 
332 |       repository.searchTemplatesByMetadata({
333 |         category: specialCategory,
334 |         requiredService: specialService,
335 |         targetAudience: specialAudience
336 |       }, 10, 0);
337 | 
338 |       const capturedParams = stmt._getCapturedParams();
339 |       // JSON.stringify escapes special characters, then slice(1, -1) removes quotes
340 |       expect(capturedParams[0][0]).toBe(JSON.stringify(specialCategory).slice(1, -1));
341 |       expect(capturedParams[0][1]).toBe(JSON.stringify(specialService).slice(1, -1));
342 |       expect(capturedParams[0][2]).toBe(JSON.stringify(specialAudience).slice(1, -1));
343 |     });
344 |   });
345 | 
346 |   describe('Performance Logging and Timing', () => {
347 |     it('should log debug info on successful search', () => {
348 |       const stmt = new MockPreparedStatement('');
349 |       stmt._setMockResults([
350 |         { id: 1 },
351 |         { id: 2 }
352 |       ]);
353 | 
354 |       const stmt2 = new MockPreparedStatement('');
355 |       stmt2._setMockResults([
356 |         { id: 1, workflow_id: 1, name: 'Template 1', workflow_json: '{}' },
357 |         { id: 2, workflow_id: 2, name: 'Template 2', workflow_json: '{}' }
358 |       ]);
359 | 
360 |       let callCount = 0;
361 |       mockAdapter.prepare = vi.fn((sql: string) => {
362 |         callCount++;
363 |         return callCount === 1 ? stmt : stmt2;
364 |       });
365 | 
366 |       repository.searchTemplatesByMetadata({ complexity: 'simple' }, 10, 0);
367 | 
368 |       expect(logger.debug).toHaveBeenCalledWith(
369 |         expect.stringContaining('Metadata search found'),
370 |         expect.objectContaining({
371 |           filters: { complexity: 'simple' },
372 |           count: 2,
373 |           phase1Ms: expect.any(Number),
374 |           phase2Ms: expect.any(Number),
375 |           totalMs: expect.any(Number),
376 |           optimization: 'two-phase-with-ordering'
377 |         })
378 |       );
379 |     });
380 | 
381 |     it('should log debug info on empty results', () => {
382 |       const stmt = new MockPreparedStatement('');
383 |       stmt._setMockResults([]);
384 |       mockAdapter.prepare = vi.fn().mockReturnValue(stmt);
385 | 
386 |       repository.searchTemplatesByMetadata({ category: 'nonexistent' }, 10, 0);
387 | 
388 |       expect(logger.debug).toHaveBeenCalledWith(
389 |         'Metadata search found 0 results',
390 |         expect.objectContaining({
391 |           filters: { category: 'nonexistent' },
392 |           phase1Ms: expect.any(Number)
393 |         })
394 |       );
395 |     });
396 | 
397 |     it('should include all filter types in logs', () => {
398 |       const stmt = new MockPreparedStatement('');
399 |       stmt._setMockResults([]);
400 |       mockAdapter.prepare = vi.fn().mockReturnValue(stmt);
401 | 
402 |       const filters = {
403 |         category: 'automation',
404 |         complexity: 'medium' as const,
405 |         maxSetupMinutes: 60,
406 |         minSetupMinutes: 15,
407 |         requiredService: 'slack',
408 |         targetAudience: 'developers'
409 |       };
410 | 
411 |       repository.searchTemplatesByMetadata(filters, 10, 0);
412 | 
413 |       expect(logger.debug).toHaveBeenCalledWith(
414 |         expect.any(String),
415 |         expect.objectContaining({
416 |           filters: filters
417 |         })
418 |       );
419 |     });
420 |   });
421 | 
422 |   describe('ID Filtering and Validation', () => {
423 |     it('should filter out negative IDs', () => {
424 |       const stmt1 = new MockPreparedStatement('');
425 |       stmt1._setMockResults([
426 |         { id: 1 },
427 |         { id: -5 },
428 |         { id: 2 }
429 |       ]);
430 | 
431 |       const stmt2 = new MockPreparedStatement('');
432 |       stmt2._setMockResults([
433 |         { id: 1, workflow_id: 1, name: 'Template 1', workflow_json: '{}' },
434 |         { id: 2, workflow_id: 2, name: 'Template 2', workflow_json: '{}' }
435 |       ]);
436 | 
437 |       let callCount = 0;
438 |       mockAdapter.prepare = vi.fn((sql: string) => {
439 |         callCount++;
440 |         return callCount === 1 ? stmt1 : stmt2;
441 |       });
442 | 
443 |       repository.searchTemplatesByMetadata({}, 10, 0);
444 | 
445 |       // Should only fetch valid IDs (1 and 2)
446 |       const prepareCall = mockAdapter.prepare.mock.calls[1][0];
447 |       expect(prepareCall).toContain('(1, 0)');
448 |       expect(prepareCall).toContain('(2, 1)');
449 |       expect(prepareCall).not.toContain('-5');
450 |     });
451 | 
452 |     it('should filter out zero IDs', () => {
453 |       const stmt1 = new MockPreparedStatement('');
454 |       stmt1._setMockResults([
455 |         { id: 0 },
456 |         { id: 1 }
457 |       ]);
458 | 
459 |       const stmt2 = new MockPreparedStatement('');
460 |       stmt2._setMockResults([
461 |         { id: 1, workflow_id: 1, name: 'Template 1', workflow_json: '{}' }
462 |       ]);
463 | 
464 |       let callCount = 0;
465 |       mockAdapter.prepare = vi.fn((sql: string) => {
466 |         callCount++;
467 |         return callCount === 1 ? stmt1 : stmt2;
468 |       });
469 | 
470 |       repository.searchTemplatesByMetadata({}, 10, 0);
471 | 
472 |       // Should only fetch valid ID (1)
473 |       const prepareCall = mockAdapter.prepare.mock.calls[1][0];
474 |       expect(prepareCall).toContain('(1, 0)');
475 |       expect(prepareCall).not.toContain('(0,');
476 |     });
477 | 
478 |     it('should filter out non-integer IDs', () => {
479 |       const stmt1 = new MockPreparedStatement('');
480 |       stmt1._setMockResults([
481 |         { id: 1 },
482 |         { id: 2.5 },
483 |         { id: 3 }
484 |       ]);
485 | 
486 |       const stmt2 = new MockPreparedStatement('');
487 |       stmt2._setMockResults([
488 |         { id: 1, workflow_id: 1, name: 'Template 1', workflow_json: '{}' },
489 |         { id: 3, workflow_id: 3, name: 'Template 3', workflow_json: '{}' }
490 |       ]);
491 | 
492 |       let callCount = 0;
493 |       mockAdapter.prepare = vi.fn((sql: string) => {
494 |         callCount++;
495 |         return callCount === 1 ? stmt1 : stmt2;
496 |       });
497 | 
498 |       repository.searchTemplatesByMetadata({}, 10, 0);
499 | 
500 |       // Should only fetch integer IDs (1 and 3)
501 |       const prepareCall = mockAdapter.prepare.mock.calls[1][0];
502 |       expect(prepareCall).toContain('(1, 0)');
503 |       expect(prepareCall).toContain('(3, 1)');
504 |       expect(prepareCall).not.toContain('2.5');
505 |     });
506 | 
507 |     it('should filter out null IDs', () => {
508 |       const stmt1 = new MockPreparedStatement('');
509 |       stmt1._setMockResults([
510 |         { id: 1 },
511 |         { id: null },
512 |         { id: 2 }
513 |       ]);
514 | 
515 |       const stmt2 = new MockPreparedStatement('');
516 |       stmt2._setMockResults([
517 |         { id: 1, workflow_id: 1, name: 'Template 1', workflow_json: '{}' },
518 |         { id: 2, workflow_id: 2, name: 'Template 2', workflow_json: '{}' }
519 |       ]);
520 | 
521 |       let callCount = 0;
522 |       mockAdapter.prepare = vi.fn((sql: string) => {
523 |         callCount++;
524 |         return callCount === 1 ? stmt1 : stmt2;
525 |       });
526 | 
527 |       repository.searchTemplatesByMetadata({}, 10, 0);
528 | 
529 |       // Should only fetch valid IDs (1 and 2)
530 |       const prepareCall = mockAdapter.prepare.mock.calls[1][0];
531 |       expect(prepareCall).toContain('(1, 0)');
532 |       expect(prepareCall).toContain('(2, 1)');
533 |       expect(prepareCall).not.toContain('null');
534 |     });
535 | 
536 |     it('should warn when no valid IDs after filtering', () => {
537 |       const stmt = new MockPreparedStatement('');
538 |       stmt._setMockResults([
539 |         { id: -1 },
540 |         { id: 0 },
541 |         { id: null }
542 |       ]);
543 |       mockAdapter.prepare = vi.fn().mockReturnValue(stmt);
544 | 
545 |       const result = repository.searchTemplatesByMetadata({}, 10, 0);
546 | 
547 |       expect(result).toHaveLength(0);
548 |       expect(logger.warn).toHaveBeenCalledWith(
549 |         'No valid IDs after filtering',
550 |         expect.objectContaining({
551 |           filters: {},
552 |           originalCount: 3
553 |         })
554 |       );
555 |     });
556 | 
557 |     it('should warn when some IDs are filtered out', () => {
558 |       const stmt1 = new MockPreparedStatement('');
559 |       stmt1._setMockResults([
560 |         { id: 1 },
561 |         { id: -2 },
562 |         { id: 3 },
563 |         { id: null }
564 |       ]);
565 | 
566 |       const stmt2 = new MockPreparedStatement('');
567 |       stmt2._setMockResults([
568 |         { id: 1, workflow_id: 1, name: 'Template 1', workflow_json: '{}' },
569 |         { id: 3, workflow_id: 3, name: 'Template 3', workflow_json: '{}' }
570 |       ]);
571 | 
572 |       let callCount = 0;
573 |       mockAdapter.prepare = vi.fn((sql: string) => {
574 |         callCount++;
575 |         return callCount === 1 ? stmt1 : stmt2;
576 |       });
577 | 
578 |       repository.searchTemplatesByMetadata({}, 10, 0);
579 | 
580 |       expect(logger.warn).toHaveBeenCalledWith(
581 |         'Some IDs were filtered out as invalid',
582 |         expect.objectContaining({
583 |           original: 4,
584 |           valid: 2,
585 |           filtered: 2
586 |         })
587 |       );
588 |     });
589 | 
590 |     it('should not warn when all IDs are valid', () => {
591 |       const stmt1 = new MockPreparedStatement('');
592 |       stmt1._setMockResults([
593 |         { id: 1 },
594 |         { id: 2 },
595 |         { id: 3 }
596 |       ]);
597 | 
598 |       const stmt2 = new MockPreparedStatement('');
599 |       stmt2._setMockResults([
600 |         { id: 1, workflow_id: 1, name: 'Template 1', workflow_json: '{}' },
601 |         { id: 2, workflow_id: 2, name: 'Template 2', workflow_json: '{}' },
602 |         { id: 3, workflow_id: 3, name: 'Template 3', workflow_json: '{}' }
603 |       ]);
604 | 
605 |       let callCount = 0;
606 |       mockAdapter.prepare = vi.fn((sql: string) => {
607 |         callCount++;
608 |         return callCount === 1 ? stmt1 : stmt2;
609 |       });
610 | 
611 |       repository.searchTemplatesByMetadata({}, 10, 0);
612 | 
613 |       expect(logger.warn).not.toHaveBeenCalledWith(
614 |         'Some IDs were filtered out as invalid',
615 |         expect.any(Object)
616 |       );
617 |     });
618 |   });
619 | 
620 |   describe('getMetadataSearchCount - Shared Helper Usage', () => {
621 |     it('should use buildMetadataFilterConditions for category', () => {
622 |       const stmt = new MockPreparedStatement('');
623 |       stmt._setMockResults([{ count: 5 }]);
624 |       mockAdapter.prepare = vi.fn().mockReturnValue(stmt);
625 | 
626 |       const result = repository.getMetadataSearchCount({ category: 'automation' });
627 | 
628 |       expect(result).toBe(5);
629 |       const prepareCall = mockAdapter.prepare.mock.calls[0][0];
630 |       expect(prepareCall).toContain("json_extract(metadata_json, '$.categories') LIKE '%' || ? || '%'");
631 | 
632 |       const capturedParams = stmt._getCapturedParams();
633 |       expect(capturedParams[0][0]).toBe('automation');
634 |     });
635 | 
636 |     it('should use buildMetadataFilterConditions for complexity', () => {
637 |       const stmt = new MockPreparedStatement('');
638 |       stmt._setMockResults([{ count: 10 }]);
639 |       mockAdapter.prepare = vi.fn().mockReturnValue(stmt);
640 | 
641 |       const result = repository.getMetadataSearchCount({ complexity: 'medium' });
642 | 
643 |       expect(result).toBe(10);
644 |       const prepareCall = mockAdapter.prepare.mock.calls[0][0];
645 |       expect(prepareCall).toContain("json_extract(metadata_json, '$.complexity') = ?");
646 |     });
647 | 
648 |     it('should use buildMetadataFilterConditions for setup minutes', () => {
649 |       const stmt = new MockPreparedStatement('');
650 |       stmt._setMockResults([{ count: 3 }]);
651 |       mockAdapter.prepare = vi.fn().mockReturnValue(stmt);
652 | 
653 |       const result = repository.getMetadataSearchCount({
654 |         maxSetupMinutes: 30,
655 |         minSetupMinutes: 10
656 |       });
657 | 
658 |       expect(result).toBe(3);
659 |       const prepareCall = mockAdapter.prepare.mock.calls[0][0];
660 |       expect(prepareCall).toContain("CAST(json_extract(metadata_json, '$.estimated_setup_minutes') AS INTEGER) <= ?");
661 |       expect(prepareCall).toContain("CAST(json_extract(metadata_json, '$.estimated_setup_minutes') AS INTEGER) >= ?");
662 |     });
663 | 
664 |     it('should use buildMetadataFilterConditions for service and audience', () => {
665 |       const stmt = new MockPreparedStatement('');
666 |       stmt._setMockResults([{ count: 7 }]);
667 |       mockAdapter.prepare = vi.fn().mockReturnValue(stmt);
668 | 
669 |       const result = repository.getMetadataSearchCount({
670 |         requiredService: 'openai',
671 |         targetAudience: 'developers'
672 |       });
673 | 
674 |       expect(result).toBe(7);
675 |       const prepareCall = mockAdapter.prepare.mock.calls[0][0];
676 |       expect(prepareCall).toContain("json_extract(metadata_json, '$.required_services') LIKE '%' || ? || '%'");
677 |       expect(prepareCall).toContain("json_extract(metadata_json, '$.target_audience') LIKE '%' || ? || '%'");
678 |     });
679 | 
680 |     it('should use buildMetadataFilterConditions with all filters', () => {
681 |       const stmt = new MockPreparedStatement('');
682 |       stmt._setMockResults([{ count: 2 }]);
683 |       mockAdapter.prepare = vi.fn().mockReturnValue(stmt);
684 | 
685 |       const result = repository.getMetadataSearchCount({
686 |         category: 'integration',
687 |         complexity: 'complex',
688 |         maxSetupMinutes: 120,
689 |         minSetupMinutes: 30,
690 |         requiredService: 'slack',
691 |         targetAudience: 'marketers'
692 |       });
693 | 
694 |       expect(result).toBe(2);
695 |       const prepareCall = mockAdapter.prepare.mock.calls[0][0];
696 |       expect(prepareCall).toContain("json_extract(metadata_json, '$.categories') LIKE '%' || ? || '%'");
697 |       expect(prepareCall).toContain("json_extract(metadata_json, '$.complexity') = ?");
698 |       expect(prepareCall).toContain("CAST(json_extract(metadata_json, '$.estimated_setup_minutes') AS INTEGER) <= ?");
699 |       expect(prepareCall).toContain("CAST(json_extract(metadata_json, '$.estimated_setup_minutes') AS INTEGER) >= ?");
700 |       expect(prepareCall).toContain("json_extract(metadata_json, '$.required_services') LIKE '%' || ? || '%'");
701 |       expect(prepareCall).toContain("json_extract(metadata_json, '$.target_audience') LIKE '%' || ? || '%'");
702 | 
703 |       const capturedParams = stmt._getCapturedParams();
704 |       expect(capturedParams[0]).toEqual(['integration', 'complex', 120, 30, 'slack', 'marketers']);
705 |     });
706 | 
707 |     it('should return 0 when no matches', () => {
708 |       const stmt = new MockPreparedStatement('');
709 |       stmt._setMockResults([{ count: 0 }]);
710 |       mockAdapter.prepare = vi.fn().mockReturnValue(stmt);
711 | 
712 |       const result = repository.getMetadataSearchCount({ category: 'nonexistent' });
713 | 
714 |       expect(result).toBe(0);
715 |     });
716 |   });
717 | 
718 |   describe('Two-Phase Query Optimization', () => {
719 |     it('should execute two separate queries', () => {
720 |       const stmt1 = new MockPreparedStatement('');
721 |       stmt1._setMockResults([{ id: 1 }, { id: 2 }]);
722 | 
723 |       const stmt2 = new MockPreparedStatement('');
724 |       stmt2._setMockResults([
725 |         { id: 1, workflow_id: 1, name: 'Template 1', workflow_json: '{}' },
726 |         { id: 2, workflow_id: 2, name: 'Template 2', workflow_json: '{}' }
727 |       ]);
728 | 
729 |       let callCount = 0;
730 |       mockAdapter.prepare = vi.fn((sql: string) => {
731 |         callCount++;
732 |         return callCount === 1 ? stmt1 : stmt2;
733 |       });
734 | 
735 |       repository.searchTemplatesByMetadata({ complexity: 'simple' }, 10, 0);
736 | 
737 |       expect(mockAdapter.prepare).toHaveBeenCalledTimes(2);
738 | 
739 |       // First query should select only ID
740 |       const phase1Query = mockAdapter.prepare.mock.calls[0][0];
741 |       expect(phase1Query).toContain('SELECT id FROM templates');
742 |       expect(phase1Query).toContain('ORDER BY views DESC, created_at DESC, id ASC');
743 | 
744 |       // Second query should use CTE with ordered IDs
745 |       const phase2Query = mockAdapter.prepare.mock.calls[1][0];
746 |       expect(phase2Query).toContain('WITH ordered_ids(id, sort_order) AS');
747 |       expect(phase2Query).toContain('VALUES (1, 0), (2, 1)');
748 |       expect(phase2Query).toContain('SELECT t.* FROM templates t');
749 |       expect(phase2Query).toContain('INNER JOIN ordered_ids o ON t.id = o.id');
750 |       expect(phase2Query).toContain('ORDER BY o.sort_order');
751 |     });
752 | 
753 |     it('should skip phase 2 when no IDs found', () => {
754 |       const stmt = new MockPreparedStatement('');
755 |       stmt._setMockResults([]);
756 |       mockAdapter.prepare = vi.fn().mockReturnValue(stmt);
757 | 
758 |       const result = repository.searchTemplatesByMetadata({ category: 'nonexistent' }, 10, 0);
759 | 
760 |       expect(result).toHaveLength(0);
761 |       // Should only call prepare once (phase 1)
762 |       expect(mockAdapter.prepare).toHaveBeenCalledTimes(1);
763 |     });
764 | 
765 |     it('should preserve ordering with stable sort', () => {
766 |       const stmt1 = new MockPreparedStatement('');
767 |       stmt1._setMockResults([
768 |         { id: 5 },
769 |         { id: 3 },
770 |         { id: 1 }
771 |       ]);
772 | 
773 |       const stmt2 = new MockPreparedStatement('');
774 |       stmt2._setMockResults([
775 |         { id: 5, workflow_id: 5, name: 'Template 5', workflow_json: '{}' },
776 |         { id: 3, workflow_id: 3, name: 'Template 3', workflow_json: '{}' },
777 |         { id: 1, workflow_id: 1, name: 'Template 1', workflow_json: '{}' }
778 |       ]);
779 | 
780 |       let callCount = 0;
781 |       mockAdapter.prepare = vi.fn((sql: string) => {
782 |         callCount++;
783 |         return callCount === 1 ? stmt1 : stmt2;
784 |       });
785 | 
786 |       repository.searchTemplatesByMetadata({}, 10, 0);
787 | 
788 |       // Check that phase 2 query maintains order: (5,0), (3,1), (1,2)
789 |       const phase2Query = mockAdapter.prepare.mock.calls[1][0];
790 |       expect(phase2Query).toContain('VALUES (5, 0), (3, 1), (1, 2)');
791 |     });
792 |   });
793 | });
794 | 
```

--------------------------------------------------------------------------------
/src/services/example-generator.ts:
--------------------------------------------------------------------------------

```typescript
   1 | /**
   2 |  * ExampleGenerator Service
   3 |  * 
   4 |  * Provides concrete, working examples for n8n nodes to help AI agents
   5 |  * understand how to configure them properly.
   6 |  */
   7 | 
   8 | export interface NodeExamples {
   9 |   minimal: Record<string, any>;
  10 |   common?: Record<string, any>;
  11 |   advanced?: Record<string, any>;
  12 | }
  13 | 
  14 | export class ExampleGenerator {
  15 |   /**
  16 |    * Curated examples for the most commonly used nodes.
  17 |    * Each example is a valid configuration that can be used directly.
  18 |    */
  19 |   private static NODE_EXAMPLES: Record<string, NodeExamples> = {
  20 |     // HTTP Request - Most versatile node
  21 |     'nodes-base.httpRequest': {
  22 |       minimal: {
  23 |         url: 'https://api.example.com/data'
  24 |       },
  25 |       common: {
  26 |         method: 'POST',
  27 |         url: 'https://api.example.com/users',
  28 |         sendBody: true,
  29 |         contentType: 'json',
  30 |         specifyBody: 'json',
  31 |         jsonBody: '{\n  "name": "John Doe",\n  "email": "[email protected]"\n}'
  32 |       },
  33 |       advanced: {
  34 |         method: 'POST',
  35 |         url: 'https://api.example.com/protected/resource',
  36 |         authentication: 'genericCredentialType',
  37 |         genericAuthType: 'headerAuth',
  38 |         sendHeaders: true,
  39 |         headerParameters: {
  40 |           parameters: [
  41 |             {
  42 |               name: 'X-API-Version',
  43 |               value: 'v2'
  44 |             }
  45 |           ]
  46 |         },
  47 |         sendBody: true,
  48 |         contentType: 'json',
  49 |         specifyBody: 'json',
  50 |         jsonBody: '{\n  "action": "update",\n  "data": {}\n}',
  51 |         // Error handling for API calls
  52 |         onError: 'continueRegularOutput',
  53 |         retryOnFail: true,
  54 |         maxTries: 3,
  55 |         waitBetweenTries: 1000,
  56 |         alwaysOutputData: true
  57 |       }
  58 |     },
  59 |     
  60 |     // Webhook - Entry point for workflows
  61 |     'nodes-base.webhook': {
  62 |       minimal: {
  63 |         path: 'my-webhook',
  64 |         httpMethod: 'POST'
  65 |       },
  66 |       common: {
  67 |         path: 'webhook-endpoint',
  68 |         httpMethod: 'POST',
  69 |         responseMode: 'lastNode',
  70 |         responseData: 'allEntries',
  71 |         responseCode: 200,
  72 |         // Webhooks should continue on fail to avoid blocking responses
  73 |         onError: 'continueRegularOutput',
  74 |         alwaysOutputData: true
  75 |       }
  76 |     },
  77 |     
  78 |     // Webhook data processing example
  79 |     'nodes-base.code.webhookProcessing': {
  80 |       minimal: {
  81 |         language: 'javaScript',
  82 |         jsCode: `// ⚠️ CRITICAL: Webhook data is nested under 'body' property!
  83 | // This Code node should be connected after a Webhook node
  84 | 
  85 | // ❌ WRONG - This will be undefined:
  86 | // const command = items[0].json.testCommand;
  87 | 
  88 | // ✅ CORRECT - Access webhook data through body:
  89 | const webhookData = items[0].json.body;
  90 | const headers = items[0].json.headers;
  91 | const query = items[0].json.query;
  92 | 
  93 | // Process webhook payload
  94 | return [{
  95 |   json: {
  96 |     // Extract data from webhook body
  97 |     command: webhookData.testCommand,
  98 |     userId: webhookData.userId,
  99 |     data: webhookData.data,
 100 |     
 101 |     // Add metadata
 102 |     timestamp: DateTime.now().toISO(),
 103 |     requestId: headers['x-request-id'] || crypto.randomUUID(),
 104 |     source: query.source || 'webhook',
 105 |     
 106 |     // Original webhook info
 107 |     httpMethod: items[0].json.httpMethod,
 108 |     webhookPath: items[0].json.webhookPath
 109 |   }
 110 | }];`
 111 |       }
 112 |     },
 113 |     
 114 |     // Code - Custom logic
 115 |     'nodes-base.code': {
 116 |       minimal: {
 117 |         language: 'javaScript',
 118 |         jsCode: 'return [{json: {result: "success"}}];'
 119 |       },
 120 |       common: {
 121 |         language: 'javaScript',
 122 |         jsCode: `// Process each item and add timestamp
 123 | return items.map(item => ({
 124 |   json: {
 125 |     ...item.json,
 126 |     processed: true,
 127 |     timestamp: DateTime.now().toISO()
 128 |   }
 129 | }));`,
 130 |         onError: 'continueRegularOutput'
 131 |       },
 132 |       advanced: {
 133 |         language: 'javaScript',
 134 |         jsCode: `// Advanced data processing with proper helper checks
 135 | const crypto = require('crypto');
 136 | const results = [];
 137 | 
 138 | for (const item of items) {
 139 |   try {
 140 |     // Validate required fields
 141 |     if (!item.json.email || !item.json.name) {
 142 |       throw new Error('Missing required fields: email or name');
 143 |     }
 144 |     
 145 |     // Generate secure API key
 146 |     const apiKey = crypto.randomBytes(16).toString('hex');
 147 |     
 148 |     // Check if $helpers is available before using
 149 |     let response;
 150 |     if (typeof $helpers !== 'undefined' && $helpers.httpRequest) {
 151 |       response = await $helpers.httpRequest({
 152 |         method: 'POST',
 153 |         url: 'https://api.example.com/process',
 154 |         body: {
 155 |           email: item.json.email,
 156 |           name: item.json.name,
 157 |           apiKey
 158 |         },
 159 |         headers: {
 160 |           'Content-Type': 'application/json'
 161 |         }
 162 |       });
 163 |     } else {
 164 |       // Fallback if $helpers not available
 165 |       response = { message: 'HTTP requests not available in this n8n version' };
 166 |     }
 167 |     
 168 |     // Add to results with response data
 169 |     results.push({
 170 |       json: {
 171 |         ...item.json,
 172 |         apiResponse: response,
 173 |         processedAt: DateTime.now().toISO(),
 174 |         status: 'success'
 175 |       }
 176 |     });
 177 |     
 178 |   } catch (error) {
 179 |     // Include failed items with error info
 180 |     results.push({
 181 |       json: {
 182 |         ...item.json,
 183 |         error: error.message,
 184 |         status: 'failed',
 185 |         processedAt: DateTime.now().toISO()
 186 |       }
 187 |     });
 188 |   }
 189 | }
 190 | 
 191 | return results;`,
 192 |         onError: 'continueRegularOutput',
 193 |         retryOnFail: true,
 194 |         maxTries: 2
 195 |       }
 196 |     },
 197 |     
 198 |     // Additional Code node examples
 199 |     'nodes-base.code.dataTransform': {
 200 |       minimal: {
 201 |         language: 'javaScript',
 202 |         jsCode: `// Transform CSV-like data to JSON
 203 | return items.map(item => {
 204 |   const lines = item.json.data.split('\\n');
 205 |   const headers = lines[0].split(',');
 206 |   const rows = lines.slice(1).map(line => {
 207 |     const values = line.split(',');
 208 |     return headers.reduce((obj, header, i) => {
 209 |       obj[header.trim()] = values[i]?.trim() || '';
 210 |       return obj;
 211 |     }, {});
 212 |   });
 213 |   
 214 |   return {json: {rows, count: rows.length}};
 215 | });`
 216 |       }
 217 |     },
 218 |     
 219 |     'nodes-base.code.aggregation': {
 220 |       minimal: {
 221 |         language: 'javaScript',
 222 |         jsCode: `// Aggregate data from all items
 223 | const totals = items.reduce((acc, item) => {
 224 |   acc.count++;
 225 |   acc.sum += item.json.amount || 0;
 226 |   acc.categories[item.json.category] = (acc.categories[item.json.category] || 0) + 1;
 227 |   return acc;
 228 | }, {count: 0, sum: 0, categories: {}});
 229 | 
 230 | return [{
 231 |   json: {
 232 |     totalItems: totals.count,
 233 |     totalAmount: totals.sum,
 234 |     averageAmount: totals.sum / totals.count,
 235 |     categoryCounts: totals.categories,
 236 |     processedAt: DateTime.now().toISO()
 237 |   }
 238 | }];`
 239 |       }
 240 |     },
 241 |     
 242 |     'nodes-base.code.filtering': {
 243 |       minimal: {
 244 |         language: 'javaScript',
 245 |         jsCode: `// Filter items based on conditions
 246 | return items
 247 |   .filter(item => {
 248 |     const amount = item.json.amount || 0;
 249 |     const status = item.json.status || '';
 250 |     return amount > 100 && status === 'active';
 251 |   })
 252 |   .map(item => ({json: item.json}));`
 253 |       }
 254 |     },
 255 |     
 256 |     'nodes-base.code.jmespathFiltering': {
 257 |       minimal: {
 258 |         language: 'javaScript',
 259 |         jsCode: `// JMESPath filtering - IMPORTANT: Use backticks for numeric literals!
 260 | const allItems = items.map(item => item.json);
 261 | 
 262 | // ✅ CORRECT - Filter with numeric literals using backticks
 263 | const expensiveItems = $jmespath(allItems, '[?price >= \`100\`]');
 264 | const lowStock = $jmespath(allItems, '[?inventory < \`10\`]');
 265 | const highPriority = $jmespath(allItems, '[?priority == \`1\`]');
 266 | 
 267 | // Combine multiple conditions
 268 | const urgentExpensive = $jmespath(allItems, '[?price >= \`100\` && priority == \`1\`]');
 269 | 
 270 | // String comparisons don't need backticks
 271 | const activeItems = $jmespath(allItems, '[?status == "active"]');
 272 | 
 273 | // Return filtered results
 274 | return expensiveItems.map(item => ({json: item}));`
 275 |       }
 276 |     },
 277 |     
 278 |     'nodes-base.code.pythonExample': {
 279 |       minimal: {
 280 |         language: 'python',
 281 |         pythonCode: `# Python data processing - use underscore prefix for built-in variables
 282 | import json
 283 | from datetime import datetime
 284 | import re
 285 | 
 286 | results = []
 287 | 
 288 | # Use _input.all() to get items in Python
 289 | for item in _input.all():
 290 |     # Convert JsProxy to Python dict to avoid issues with null values
 291 |     item_data = item.json.to_py()
 292 |     
 293 |     # Clean email addresses
 294 |     email = item_data.get('email', '')
 295 |     if email and re.match(r'^[\\w\\.-]+@[\\w\\.-]+\\.\\w+$', email):
 296 |         cleaned_data = {
 297 |             'email': email.lower(),
 298 |             'name': item_data.get('name', '').title(),
 299 |             'validated': True,
 300 |             'timestamp': datetime.now().isoformat()
 301 |         }
 302 |     else:
 303 |         # Spread operator doesn't work with JsProxy, use dict()
 304 |         cleaned_data = dict(item_data)
 305 |         cleaned_data['validated'] = False
 306 |         cleaned_data['error'] = 'Invalid email format'
 307 |     
 308 |     results.append({'json': cleaned_data})
 309 | 
 310 | return results`
 311 |       }
 312 |     },
 313 |     
 314 |     'nodes-base.code.aiTool': {
 315 |       minimal: {
 316 |         language: 'javaScript',
 317 |         mode: 'runOnceForEachItem',
 318 |         jsCode: `// Code node as AI tool - calculate discount
 319 | const quantity = $json.quantity || 1;
 320 | const price = $json.price || 0;
 321 | 
 322 | let discountRate = 0;
 323 | if (quantity >= 100) discountRate = 0.20;
 324 | else if (quantity >= 50) discountRate = 0.15;
 325 | else if (quantity >= 20) discountRate = 0.10;
 326 | else if (quantity >= 10) discountRate = 0.05;
 327 | 
 328 | const subtotal = price * quantity;
 329 | const discount = subtotal * discountRate;
 330 | const total = subtotal - discount;
 331 | 
 332 | return [{
 333 |   json: {
 334 |     quantity,
 335 |     price,
 336 |     subtotal,
 337 |     discountRate: discountRate * 100,
 338 |     discountAmount: discount,
 339 |     total,
 340 |     savings: discount
 341 |   }
 342 | }];`
 343 |       }
 344 |     },
 345 |     
 346 |     'nodes-base.code.crypto': {
 347 |       minimal: {
 348 |         language: 'javaScript',
 349 |         jsCode: `// Using crypto in Code nodes - it IS available!
 350 | const crypto = require('crypto');
 351 | 
 352 | // Generate secure tokens
 353 | const token = crypto.randomBytes(32).toString('hex');
 354 | const uuid = crypto.randomUUID();
 355 | 
 356 | // Create hashes
 357 | const hash = crypto.createHash('sha256')
 358 |   .update(items[0].json.data || 'test')
 359 |   .digest('hex');
 360 | 
 361 | return [{
 362 |   json: {
 363 |     token,
 364 |     uuid,
 365 |     hash,
 366 |     timestamp: DateTime.now().toISO()
 367 |   }
 368 | }];`
 369 |       }
 370 |     },
 371 |     
 372 |     'nodes-base.code.staticData': {
 373 |       minimal: {
 374 |         language: 'javaScript',
 375 |         jsCode: `// Using workflow static data correctly
 376 | // IMPORTANT: $getWorkflowStaticData is a standalone function!
 377 | const staticData = $getWorkflowStaticData('global');
 378 | 
 379 | // Initialize counter if not exists
 380 | if (!staticData.processCount) {
 381 |   staticData.processCount = 0;
 382 |   staticData.firstRun = DateTime.now().toISO();
 383 | }
 384 | 
 385 | // Update counter
 386 | staticData.processCount++;
 387 | staticData.lastRun = DateTime.now().toISO();
 388 | 
 389 | // Process items
 390 | const results = items.map(item => ({
 391 |   json: {
 392 |     ...item.json,
 393 |     runNumber: staticData.processCount,
 394 |     processed: true
 395 |   }
 396 | }));
 397 | 
 398 | return results;`
 399 |       }
 400 |     },
 401 |     
 402 |     // Set - Data manipulation
 403 |     'nodes-base.set': {
 404 |       minimal: {
 405 |         mode: 'manual',
 406 |         assignments: {
 407 |           assignments: [
 408 |             {
 409 |               id: '1',
 410 |               name: 'status',
 411 |               value: 'active',
 412 |               type: 'string'
 413 |             }
 414 |           ]
 415 |         }
 416 |       },
 417 |       common: {
 418 |         mode: 'manual',
 419 |         includeOtherFields: true,
 420 |         assignments: {
 421 |           assignments: [
 422 |             {
 423 |               id: '1',
 424 |               name: 'status',
 425 |               value: 'processed',
 426 |               type: 'string'
 427 |             },
 428 |             {
 429 |               id: '2',
 430 |               name: 'processedAt',
 431 |               value: '={{ $now.toISO() }}',
 432 |               type: 'string'
 433 |             },
 434 |             {
 435 |               id: '3',
 436 |               name: 'itemCount',
 437 |               value: '={{ $items().length }}',
 438 |               type: 'number'
 439 |             }
 440 |           ]
 441 |         }
 442 |       }
 443 |     },
 444 |     
 445 |     // If - Conditional logic
 446 |     'nodes-base.if': {
 447 |       minimal: {
 448 |         conditions: {
 449 |           conditions: [
 450 |             {
 451 |               id: '1',
 452 |               leftValue: '={{ $json.status }}',
 453 |               rightValue: 'active',
 454 |               operator: {
 455 |                 type: 'string',
 456 |                 operation: 'equals'
 457 |               }
 458 |             }
 459 |           ]
 460 |         }
 461 |       },
 462 |       common: {
 463 |         conditions: {
 464 |           conditions: [
 465 |             {
 466 |               id: '1',
 467 |               leftValue: '={{ $json.status }}',
 468 |               rightValue: 'active',
 469 |               operator: {
 470 |                 type: 'string',
 471 |                 operation: 'equals'
 472 |               }
 473 |             },
 474 |             {
 475 |               id: '2',
 476 |               leftValue: '={{ $json.count }}',
 477 |               rightValue: 10,
 478 |               operator: {
 479 |                 type: 'number',
 480 |                 operation: 'gt'
 481 |               }
 482 |             }
 483 |           ]
 484 |         },
 485 |         combineOperation: 'all'
 486 |       }
 487 |     },
 488 |     
 489 |     // PostgreSQL - Database operations
 490 |     'nodes-base.postgres': {
 491 |       minimal: {
 492 |         operation: 'executeQuery',
 493 |         query: 'SELECT * FROM users LIMIT 10'
 494 |       },
 495 |       common: {
 496 |         operation: 'insert',
 497 |         table: 'users',
 498 |         columns: 'name,email,created_at',
 499 |         additionalFields: {}
 500 |       },
 501 |       advanced: {
 502 |         operation: 'executeQuery',
 503 |         query: `INSERT INTO users (name, email, status)
 504 | VALUES ($1, $2, $3)
 505 | ON CONFLICT (email) 
 506 | DO UPDATE SET 
 507 |   name = EXCLUDED.name,
 508 |   updated_at = NOW()
 509 | RETURNING *;`,
 510 |         additionalFields: {
 511 |           queryParams: '={{ $json.name }},{{ $json.email }},active'
 512 |         },
 513 |         // Database operations should retry on connection errors
 514 |         retryOnFail: true,
 515 |         maxTries: 3,
 516 |         waitBetweenTries: 2000,
 517 |         onError: 'continueErrorOutput'
 518 |       }
 519 |     },
 520 |     
 521 |     // OpenAI - AI operations
 522 |     'nodes-base.openAi': {
 523 |       minimal: {
 524 |         resource: 'chat',
 525 |         operation: 'message',
 526 |         modelId: 'gpt-3.5-turbo',
 527 |         messages: {
 528 |           values: [
 529 |             {
 530 |               role: 'user',
 531 |               content: 'Hello, how can you help me?'
 532 |             }
 533 |           ]
 534 |         }
 535 |       },
 536 |       common: {
 537 |         resource: 'chat',
 538 |         operation: 'message',
 539 |         modelId: 'gpt-4',
 540 |         messages: {
 541 |           values: [
 542 |             {
 543 |               role: 'system',
 544 |               content: 'You are a helpful assistant that summarizes text concisely.'
 545 |             },
 546 |             {
 547 |               role: 'user',
 548 |               content: '={{ $json.text }}'
 549 |             }
 550 |           ]
 551 |         },
 552 |         options: {
 553 |           maxTokens: 150,
 554 |           temperature: 0.7
 555 |         },
 556 |         // AI calls should handle rate limits and transient errors
 557 |         retryOnFail: true,
 558 |         maxTries: 3,
 559 |         waitBetweenTries: 5000,
 560 |         onError: 'continueRegularOutput',
 561 |         alwaysOutputData: true
 562 |       }
 563 |     },
 564 |     
 565 |     // Google Sheets - Spreadsheet operations
 566 |     'nodes-base.googleSheets': {
 567 |       minimal: {
 568 |         operation: 'read',
 569 |         documentId: {
 570 |           __rl: true,
 571 |           value: 'https://docs.google.com/spreadsheets/d/your-sheet-id',
 572 |           mode: 'url'
 573 |         },
 574 |         sheetName: 'Sheet1'
 575 |       },
 576 |       common: {
 577 |         operation: 'append',
 578 |         documentId: {
 579 |           __rl: true,
 580 |           value: 'your-sheet-id',
 581 |           mode: 'id'
 582 |         },
 583 |         sheetName: 'Sheet1',
 584 |         dataStartRow: 2,
 585 |         columns: {
 586 |           mappingMode: 'defineBelow',
 587 |           value: {
 588 |             'Name': '={{ $json.name }}',
 589 |             'Email': '={{ $json.email }}',
 590 |             'Date': '={{ $now.toISO() }}'
 591 |           }
 592 |         }
 593 |       }
 594 |     },
 595 |     
 596 |     // Slack - Messaging
 597 |     'nodes-base.slack': {
 598 |       minimal: {
 599 |         resource: 'message',
 600 |         operation: 'post',
 601 |         channel: '#general',
 602 |         text: 'Hello from n8n!'
 603 |       },
 604 |       common: {
 605 |         resource: 'message',
 606 |         operation: 'post',
 607 |         channel: '#notifications',
 608 |         text: 'New order received!',
 609 |         attachments: [
 610 |           {
 611 |             color: '#36a64f',
 612 |             title: 'Order #{{ $json.orderId }}',
 613 |             fields: {
 614 |               item: [
 615 |                 {
 616 |                   title: 'Customer',
 617 |                   value: '{{ $json.customerName }}',
 618 |                   short: true
 619 |                 },
 620 |                 {
 621 |                   title: 'Amount',
 622 |                   value: '${{ $json.amount }}',
 623 |                   short: true
 624 |                 }
 625 |               ]
 626 |             }
 627 |           }
 628 |         ],
 629 |         // Messaging services should handle rate limits
 630 |         retryOnFail: true,
 631 |         maxTries: 2,
 632 |         waitBetweenTries: 3000,
 633 |         onError: 'continueRegularOutput'
 634 |       }
 635 |     },
 636 |     
 637 |     // Email - Email operations
 638 |     'nodes-base.emailSend': {
 639 |       minimal: {
 640 |         fromEmail: '[email protected]',
 641 |         toEmail: '[email protected]',
 642 |         subject: 'Test Email',
 643 |         text: 'This is a test email from n8n.'
 644 |       },
 645 |       common: {
 646 |         fromEmail: '[email protected]',
 647 |         toEmail: '={{ $json.email }}',
 648 |         subject: 'Welcome to our service, {{ $json.name }}!',
 649 |         html: `<h1>Welcome!</h1>
 650 | <p>Hi {{ $json.name }},</p>
 651 | <p>Thank you for signing up. We're excited to have you on board!</p>
 652 | <p>Best regards,<br>The Team</p>`,
 653 |         options: {
 654 |           ccEmail: '[email protected]'
 655 |         },
 656 |         // Email sending should handle transient failures
 657 |         retryOnFail: true,
 658 |         maxTries: 3,
 659 |         waitBetweenTries: 2000,
 660 |         onError: 'continueRegularOutput'
 661 |       }
 662 |     },
 663 |     
 664 |     // Merge - Combining data
 665 |     'nodes-base.merge': {
 666 |       minimal: {
 667 |         mode: 'append'
 668 |       },
 669 |       common: {
 670 |         mode: 'mergeByKey',
 671 |         propertyName1: 'id',
 672 |         propertyName2: 'userId'
 673 |       }
 674 |     },
 675 |     
 676 |     // Function - Legacy custom functions
 677 |     'nodes-base.function': {
 678 |       minimal: {
 679 |         functionCode: 'return items;'
 680 |       },
 681 |       common: {
 682 |         functionCode: `// Add a timestamp to each item
 683 | const processedItems = items.map(item => {
 684 |   return {
 685 |     ...item,
 686 |     json: {
 687 |       ...item.json,
 688 |       processedAt: new Date().toISOString()
 689 |     }
 690 |   };
 691 | });
 692 | 
 693 | return processedItems;`
 694 |       }
 695 |     },
 696 |     
 697 |     // Split In Batches - Batch processing
 698 |     'nodes-base.splitInBatches': {
 699 |       minimal: {
 700 |         batchSize: 10
 701 |       },
 702 |       common: {
 703 |         batchSize: 100,
 704 |         options: {
 705 |           reset: false
 706 |         }
 707 |       }
 708 |     },
 709 |     
 710 |     // Redis - Cache operations
 711 |     'nodes-base.redis': {
 712 |       minimal: {
 713 |         operation: 'set',
 714 |         key: 'myKey',
 715 |         value: 'myValue'
 716 |       },
 717 |       common: {
 718 |         operation: 'set',
 719 |         key: 'user:{{ $json.userId }}',
 720 |         value: '={{ JSON.stringify($json) }}',
 721 |         expire: true,
 722 |         ttl: 3600
 723 |       }
 724 |     },
 725 |     
 726 |     // MongoDB - NoSQL operations
 727 |     'nodes-base.mongoDb': {
 728 |       minimal: {
 729 |         operation: 'find',
 730 |         collection: 'users'
 731 |       },
 732 |       common: {
 733 |         operation: 'findOneAndUpdate',
 734 |         collection: 'users',
 735 |         query: '{ "email": "{{ $json.email }}" }',
 736 |         update: '{ "$set": { "lastLogin": "{{ $now.toISO() }}" } }',
 737 |         options: {
 738 |           upsert: true,
 739 |           returnNewDocument: true
 740 |         },
 741 |         // NoSQL operations should handle connection issues
 742 |         retryOnFail: true,
 743 |         maxTries: 3,
 744 |         waitBetweenTries: 1000,
 745 |         onError: 'continueErrorOutput'
 746 |       }
 747 |     },
 748 |     
 749 |     // MySQL - Database operations
 750 |     'nodes-base.mySql': {
 751 |       minimal: {
 752 |         operation: 'executeQuery',
 753 |         query: 'SELECT * FROM products WHERE active = 1'
 754 |       },
 755 |       common: {
 756 |         operation: 'insert',
 757 |         table: 'orders',
 758 |         columns: 'customer_id,product_id,quantity,order_date',
 759 |         options: {
 760 |           queryBatching: 'independently'
 761 |         },
 762 |         // Database writes should handle connection errors
 763 |         retryOnFail: true,
 764 |         maxTries: 3,
 765 |         waitBetweenTries: 2000,
 766 |         onError: 'stopWorkflow'
 767 |       }
 768 |     },
 769 |     
 770 |     // FTP - File transfer
 771 |     'nodes-base.ftp': {
 772 |       minimal: {
 773 |         operation: 'download',
 774 |         path: '/files/data.csv'
 775 |       },
 776 |       common: {
 777 |         operation: 'upload',
 778 |         path: '/uploads/',
 779 |         fileName: 'report_{{ $now.format("yyyy-MM-dd") }}.csv',
 780 |         binaryData: true,
 781 |         binaryPropertyName: 'data'
 782 |       }
 783 |     },
 784 |     
 785 |     // SSH - Remote execution
 786 |     'nodes-base.ssh': {
 787 |       minimal: {
 788 |         resource: 'command',
 789 |         operation: 'execute',
 790 |         command: 'ls -la'
 791 |       },
 792 |       common: {
 793 |         resource: 'command',
 794 |         operation: 'execute',
 795 |         command: 'cd /var/logs && tail -n 100 app.log | grep ERROR',
 796 |         cwd: '/home/user'
 797 |       }
 798 |     },
 799 |     
 800 |     // Execute Command - Local execution
 801 |     'nodes-base.executeCommand': {
 802 |       minimal: {
 803 |         command: 'echo "Hello from n8n"'
 804 |       },
 805 |       common: {
 806 |         command: 'node process-data.js --input "{{ $json.filename }}"',
 807 |         cwd: '/app/scripts'
 808 |       }
 809 |     },
 810 |     
 811 |     // GitHub - Version control
 812 |     'nodes-base.github': {
 813 |       minimal: {
 814 |         resource: 'issue',
 815 |         operation: 'get',
 816 |         owner: 'n8n-io',
 817 |         repository: 'n8n',
 818 |         issueNumber: 123
 819 |       },
 820 |       common: {
 821 |         resource: 'issue',
 822 |         operation: 'create',
 823 |         owner: '={{ $json.organization }}',
 824 |         repository: '={{ $json.repo }}',
 825 |         title: 'Bug: {{ $json.title }}',
 826 |         body: `## Description
 827 | {{ $json.description }}
 828 | 
 829 | ## Steps to Reproduce
 830 | {{ $json.steps }}
 831 | 
 832 | ## Expected Behavior
 833 | {{ $json.expected }}`,
 834 |         assignees: ['maintainer'],
 835 |         labels: ['bug', 'needs-triage']
 836 |       }
 837 |     },
 838 |     
 839 |     // Error Handling Examples and Patterns
 840 |     'error-handling.modern-patterns': {
 841 |       minimal: {
 842 |         // Basic error handling - continue on error
 843 |         onError: 'continueRegularOutput'
 844 |       },
 845 |       common: {
 846 |         // Use error output for special handling
 847 |         onError: 'continueErrorOutput',
 848 |         alwaysOutputData: true
 849 |       },
 850 |       advanced: {
 851 |         // Stop workflow on critical errors
 852 |         onError: 'stopWorkflow',
 853 |         // But retry first
 854 |         retryOnFail: true,
 855 |         maxTries: 3,
 856 |         waitBetweenTries: 2000
 857 |       }
 858 |     },
 859 |     
 860 |     'error-handling.api-with-retry': {
 861 |       minimal: {
 862 |         url: 'https://api.example.com/data',
 863 |         retryOnFail: true,
 864 |         maxTries: 3,
 865 |         waitBetweenTries: 1000
 866 |       },
 867 |       common: {
 868 |         method: 'GET',
 869 |         url: 'https://api.example.com/users/{{ $json.userId }}',
 870 |         retryOnFail: true,
 871 |         maxTries: 5,
 872 |         waitBetweenTries: 2000,
 873 |         alwaysOutputData: true,
 874 |         // Headers for better debugging
 875 |         sendHeaders: true,
 876 |         headerParameters: {
 877 |           parameters: [
 878 |             {
 879 |               name: 'X-Request-ID',
 880 |               value: '={{ $workflow.id }}-{{ $execution.id }}'
 881 |             }
 882 |           ]
 883 |         }
 884 |       },
 885 |       advanced: {
 886 |         method: 'POST',
 887 |         url: 'https://api.example.com/critical-operation',
 888 |         sendBody: true,
 889 |         contentType: 'json',
 890 |         specifyBody: 'json',
 891 |         jsonBody: '{{ JSON.stringify($json) }}',
 892 |         // Exponential backoff pattern
 893 |         retryOnFail: true,
 894 |         maxTries: 5,
 895 |         waitBetweenTries: 1000,
 896 |         // Always output for debugging
 897 |         alwaysOutputData: true,
 898 |         // Stop workflow on error for critical operations
 899 |         onError: 'stopWorkflow'
 900 |       }
 901 |     },
 902 |     
 903 |     'error-handling.fault-tolerant': {
 904 |       minimal: {
 905 |         // For non-critical operations
 906 |         onError: 'continueRegularOutput'
 907 |       },
 908 |       common: {
 909 |         // Data processing that shouldn't stop the workflow
 910 |         onError: 'continueRegularOutput',
 911 |         alwaysOutputData: true
 912 |       },
 913 |       advanced: {
 914 |         // Combination for resilient processing
 915 |         onError: 'continueRegularOutput',
 916 |         retryOnFail: true,
 917 |         maxTries: 2,
 918 |         waitBetweenTries: 500,
 919 |         alwaysOutputData: true
 920 |       }
 921 |     },
 922 |     
 923 |     'error-handling.database-patterns': {
 924 |       minimal: {
 925 |         // Database reads can continue on error
 926 |         onError: 'continueRegularOutput',
 927 |         alwaysOutputData: true
 928 |       },
 929 |       common: {
 930 |         // Database writes should retry then stop
 931 |         retryOnFail: true,
 932 |         maxTries: 3,
 933 |         waitBetweenTries: 2000,
 934 |         onError: 'stopWorkflow'
 935 |       },
 936 |       advanced: {
 937 |         // Transaction-safe operations
 938 |         onError: 'continueErrorOutput',
 939 |         retryOnFail: false, // Don't retry transactions
 940 |         alwaysOutputData: true
 941 |       }
 942 |     },
 943 |     
 944 |     'error-handling.webhook-patterns': {
 945 |       minimal: {
 946 |         // Always respond to webhooks
 947 |         onError: 'continueRegularOutput',
 948 |         alwaysOutputData: true
 949 |       },
 950 |       common: {
 951 |         // Process errors separately
 952 |         onError: 'continueErrorOutput',
 953 |         alwaysOutputData: true,
 954 |         // Add custom error response
 955 |         responseCode: 200,
 956 |         responseData: 'allEntries'
 957 |       }
 958 |     },
 959 |     
 960 |     'error-handling.ai-patterns': {
 961 |       minimal: {
 962 |         // AI calls should handle rate limits
 963 |         retryOnFail: true,
 964 |         maxTries: 3,
 965 |         waitBetweenTries: 5000,
 966 |         onError: 'continueRegularOutput'
 967 |       },
 968 |       common: {
 969 |         // Exponential backoff for rate limits
 970 |         retryOnFail: true,
 971 |         maxTries: 5,
 972 |         waitBetweenTries: 2000,
 973 |         onError: 'continueRegularOutput',
 974 |         alwaysOutputData: true
 975 |       }
 976 |     }
 977 |   };
 978 |   
 979 |   /**
 980 |    * Get examples for a specific node type
 981 |    */
 982 |   static getExamples(nodeType: string, essentials?: any): NodeExamples {
 983 |     // Return curated examples if available
 984 |     const examples = this.NODE_EXAMPLES[nodeType];
 985 |     if (examples) {
 986 |       return examples;
 987 |     }
 988 |     
 989 |     // Generate basic examples for unconfigured nodes
 990 |     return this.generateBasicExamples(nodeType, essentials);
 991 |   }
 992 |   
 993 |   /**
 994 |    * Generate basic examples for nodes without curated ones
 995 |    */
 996 |   private static generateBasicExamples(nodeType: string, essentials?: any): NodeExamples {
 997 |     const minimal: Record<string, any> = {};
 998 |     
 999 |     // Add required fields with sensible defaults
1000 |     if (essentials?.required) {
1001 |       for (const prop of essentials.required) {
1002 |         minimal[prop.name] = this.getDefaultValue(prop);
1003 |       }
1004 |     }
1005 |     
1006 |     // Add first common property if no required fields
1007 |     if (Object.keys(minimal).length === 0 && essentials?.common?.length > 0) {
1008 |       const firstCommon = essentials.common[0];
1009 |       minimal[firstCommon.name] = this.getDefaultValue(firstCommon);
1010 |     }
1011 |     
1012 |     return { minimal };
1013 |   }
1014 |   
1015 |   /**
1016 |    * Generate a sensible default value for a property
1017 |    */
1018 |   private static getDefaultValue(prop: any): any {
1019 |     // Use configured default if available
1020 |     if (prop.default !== undefined) {
1021 |       return prop.default;
1022 |     }
1023 |     
1024 |     // Generate based on type and name
1025 |     switch (prop.type) {
1026 |       case 'string':
1027 |         return this.getStringDefault(prop);
1028 |       
1029 |       case 'number':
1030 |         return prop.name.includes('port') ? 80 : 
1031 |                prop.name.includes('timeout') ? 30000 : 
1032 |                prop.name.includes('limit') ? 10 : 0;
1033 |       
1034 |       case 'boolean':
1035 |         return false;
1036 |       
1037 |       case 'options':
1038 |       case 'multiOptions':
1039 |         return prop.options?.[0]?.value || '';
1040 |       
1041 |       case 'json':
1042 |         return '{\n  "key": "value"\n}';
1043 |       
1044 |       case 'collection':
1045 |       case 'fixedCollection':
1046 |         return {};
1047 |       
1048 |       default:
1049 |         return '';
1050 |     }
1051 |   }
1052 |   
1053 |   /**
1054 |    * Get default value for string properties based on name
1055 |    */
1056 |   private static getStringDefault(prop: any): string {
1057 |     const name = prop.name.toLowerCase();
1058 |     
1059 |     // URL/endpoint fields
1060 |     if (name.includes('url') || name === 'endpoint') {
1061 |       return 'https://api.example.com';
1062 |     }
1063 |     
1064 |     // Email fields
1065 |     if (name.includes('email')) {
1066 |       return name.includes('from') ? '[email protected]' : '[email protected]';
1067 |     }
1068 |     
1069 |     // Path fields
1070 |     if (name.includes('path')) {
1071 |       return name.includes('webhook') ? 'my-webhook' : '/path/to/file';
1072 |     }
1073 |     
1074 |     // Name fields
1075 |     if (name === 'name' || name.includes('username')) {
1076 |       return 'John Doe';
1077 |     }
1078 |     
1079 |     // Key fields
1080 |     if (name.includes('key')) {
1081 |       return 'myKey';
1082 |     }
1083 |     
1084 |     // Query fields
1085 |     if (name === 'query' || name.includes('sql')) {
1086 |       return 'SELECT * FROM table_name LIMIT 10';
1087 |     }
1088 |     
1089 |     // Collection/table fields
1090 |     if (name === 'collection' || name === 'table') {
1091 |       return 'users';
1092 |     }
1093 |     
1094 |     // Use placeholder if available
1095 |     if (prop.placeholder) {
1096 |       return prop.placeholder;
1097 |     }
1098 |     
1099 |     return '';
1100 |   }
1101 |   
1102 |   /**
1103 |    * Get example for a specific use case
1104 |    */
1105 |   static getTaskExample(nodeType: string, task: string): Record<string, any> | undefined {
1106 |     const examples = this.NODE_EXAMPLES[nodeType];
1107 |     if (!examples) return undefined;
1108 |     
1109 |     // Map common tasks to example types
1110 |     const taskMap: Record<string, keyof NodeExamples> = {
1111 |       'basic': 'minimal',
1112 |       'simple': 'minimal',
1113 |       'typical': 'common',
1114 |       'standard': 'common',
1115 |       'complex': 'advanced',
1116 |       'full': 'advanced'
1117 |     };
1118 |     
1119 |     const exampleType = taskMap[task] || 'common';
1120 |     return examples[exampleType] || examples.minimal;
1121 |   }
1122 | }
```

--------------------------------------------------------------------------------
/tests/unit/telemetry/config-manager.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
  2 | import { TelemetryConfigManager } from '../../../src/telemetry/config-manager';
  3 | import { existsSync, readFileSync, writeFileSync, mkdirSync, rmSync } from 'fs';
  4 | import { join } from 'path';
  5 | import { homedir } from 'os';
  6 | 
  7 | // Mock fs module
  8 | vi.mock('fs', async () => {
  9 |   const actual = await vi.importActual<typeof import('fs')>('fs');
 10 |   return {
 11 |     ...actual,
 12 |     existsSync: vi.fn(),
 13 |     readFileSync: vi.fn(),
 14 |     writeFileSync: vi.fn(),
 15 |     mkdirSync: vi.fn()
 16 |   };
 17 | });
 18 | 
 19 | describe('TelemetryConfigManager', () => {
 20 |   let manager: TelemetryConfigManager;
 21 | 
 22 |   beforeEach(() => {
 23 |     vi.clearAllMocks();
 24 |     // Clear singleton instance
 25 |     (TelemetryConfigManager as any).instance = null;
 26 | 
 27 |     // Mock console.log to suppress first-run notice in tests
 28 |     vi.spyOn(console, 'log').mockImplementation(() => {});
 29 |   });
 30 | 
 31 |   afterEach(() => {
 32 |     vi.restoreAllMocks();
 33 |   });
 34 | 
 35 |   describe('getInstance', () => {
 36 |     it('should return singleton instance', () => {
 37 |       const instance1 = TelemetryConfigManager.getInstance();
 38 |       const instance2 = TelemetryConfigManager.getInstance();
 39 |       expect(instance1).toBe(instance2);
 40 |     });
 41 |   });
 42 | 
 43 |   describe('loadConfig', () => {
 44 |     it('should create default config on first run', () => {
 45 |       vi.mocked(existsSync).mockReturnValue(false);
 46 | 
 47 |       manager = TelemetryConfigManager.getInstance();
 48 |       const config = manager.loadConfig();
 49 | 
 50 |       expect(config.enabled).toBe(true);
 51 |       expect(config.userId).toMatch(/^[a-f0-9]{16}$/);
 52 |       expect(config.firstRun).toBeDefined();
 53 |       expect(vi.mocked(mkdirSync)).toHaveBeenCalledWith(
 54 |         join(homedir(), '.n8n-mcp'),
 55 |         { recursive: true }
 56 |       );
 57 |       expect(vi.mocked(writeFileSync)).toHaveBeenCalled();
 58 |     });
 59 | 
 60 |     it('should load existing config from disk', () => {
 61 |       const mockConfig = {
 62 |         enabled: false,
 63 |         userId: 'test-user-id',
 64 |         firstRun: '2024-01-01T00:00:00Z'
 65 |       };
 66 | 
 67 |       vi.mocked(existsSync).mockReturnValue(true);
 68 |       vi.mocked(readFileSync).mockReturnValue(JSON.stringify(mockConfig));
 69 | 
 70 |       manager = TelemetryConfigManager.getInstance();
 71 |       const config = manager.loadConfig();
 72 | 
 73 |       expect(config).toEqual(mockConfig);
 74 |     });
 75 | 
 76 |     it('should handle corrupted config file gracefully', () => {
 77 |       vi.mocked(existsSync).mockReturnValue(true);
 78 |       vi.mocked(readFileSync).mockReturnValue('invalid json');
 79 | 
 80 |       manager = TelemetryConfigManager.getInstance();
 81 |       const config = manager.loadConfig();
 82 | 
 83 |       expect(config.enabled).toBe(false);
 84 |       expect(config.userId).toMatch(/^[a-f0-9]{16}$/);
 85 |     });
 86 | 
 87 |     it('should add userId to config if missing', () => {
 88 |       const mockConfig = {
 89 |         enabled: true,
 90 |         firstRun: '2024-01-01T00:00:00Z'
 91 |       };
 92 | 
 93 |       vi.mocked(existsSync).mockReturnValue(true);
 94 |       vi.mocked(readFileSync).mockReturnValue(JSON.stringify(mockConfig));
 95 | 
 96 |       manager = TelemetryConfigManager.getInstance();
 97 |       const config = manager.loadConfig();
 98 | 
 99 |       expect(config.userId).toMatch(/^[a-f0-9]{16}$/);
100 |       expect(vi.mocked(writeFileSync)).toHaveBeenCalled();
101 |     });
102 |   });
103 | 
104 |   describe('isEnabled', () => {
105 |     it('should return true when telemetry is enabled', () => {
106 |       vi.mocked(existsSync).mockReturnValue(true);
107 |       vi.mocked(readFileSync).mockReturnValue(JSON.stringify({
108 |         enabled: true,
109 |         userId: 'test-id'
110 |       }));
111 | 
112 |       manager = TelemetryConfigManager.getInstance();
113 |       expect(manager.isEnabled()).toBe(true);
114 |     });
115 | 
116 |     it('should return false when telemetry is disabled', () => {
117 |       vi.mocked(existsSync).mockReturnValue(true);
118 |       vi.mocked(readFileSync).mockReturnValue(JSON.stringify({
119 |         enabled: false,
120 |         userId: 'test-id'
121 |       }));
122 | 
123 |       manager = TelemetryConfigManager.getInstance();
124 |       expect(manager.isEnabled()).toBe(false);
125 |     });
126 |   });
127 | 
128 |   describe('getUserId', () => {
129 |     it('should return consistent user ID', () => {
130 |       vi.mocked(existsSync).mockReturnValue(true);
131 |       vi.mocked(readFileSync).mockReturnValue(JSON.stringify({
132 |         enabled: true,
133 |         userId: 'test-user-id-123'
134 |       }));
135 | 
136 |       manager = TelemetryConfigManager.getInstance();
137 |       expect(manager.getUserId()).toBe('test-user-id-123');
138 |     });
139 |   });
140 | 
141 |   describe('isFirstRun', () => {
142 |     it('should return true if config file does not exist', () => {
143 |       vi.mocked(existsSync).mockReturnValue(false);
144 | 
145 |       manager = TelemetryConfigManager.getInstance();
146 |       expect(manager.isFirstRun()).toBe(true);
147 |     });
148 | 
149 |     it('should return false if config file exists', () => {
150 |       vi.mocked(existsSync).mockReturnValue(true);
151 | 
152 |       manager = TelemetryConfigManager.getInstance();
153 |       expect(manager.isFirstRun()).toBe(false);
154 |     });
155 |   });
156 | 
157 |   describe('enable/disable', () => {
158 |     beforeEach(() => {
159 |       vi.mocked(existsSync).mockReturnValue(true);
160 |       vi.mocked(readFileSync).mockReturnValue(JSON.stringify({
161 |         enabled: false,
162 |         userId: 'test-id'
163 |       }));
164 |     });
165 | 
166 |     it('should enable telemetry', () => {
167 |       manager = TelemetryConfigManager.getInstance();
168 |       manager.enable();
169 | 
170 |       const calls = vi.mocked(writeFileSync).mock.calls;
171 |       expect(calls.length).toBeGreaterThan(0);
172 |       const lastCall = calls[calls.length - 1];
173 |       expect(lastCall[1]).toContain('"enabled": true');
174 |     });
175 | 
176 |     it('should disable telemetry', () => {
177 |       manager = TelemetryConfigManager.getInstance();
178 |       manager.disable();
179 | 
180 |       const calls = vi.mocked(writeFileSync).mock.calls;
181 |       expect(calls.length).toBeGreaterThan(0);
182 |       const lastCall = calls[calls.length - 1];
183 |       expect(lastCall[1]).toContain('"enabled": false');
184 |     });
185 |   });
186 | 
187 |   describe('getStatus', () => {
188 |     it('should return formatted status string', () => {
189 |       vi.mocked(existsSync).mockReturnValue(true);
190 |       vi.mocked(readFileSync).mockReturnValue(JSON.stringify({
191 |         enabled: true,
192 |         userId: 'test-id',
193 |         firstRun: '2024-01-01T00:00:00Z'
194 |       }));
195 | 
196 |       manager = TelemetryConfigManager.getInstance();
197 |       const status = manager.getStatus();
198 | 
199 |       expect(status).toContain('ENABLED');
200 |       expect(status).toContain('test-id');
201 |       expect(status).toContain('2024-01-01T00:00:00Z');
202 |       expect(status).toContain('npx n8n-mcp telemetry');
203 |     });
204 |   });
205 | 
206 |   describe('edge cases and error handling', () => {
207 |     it('should handle file system errors during config creation', () => {
208 |       vi.mocked(existsSync).mockReturnValue(false);
209 |       vi.mocked(mkdirSync).mockImplementation(() => {
210 |         throw new Error('Permission denied');
211 |       });
212 | 
213 |       // Should not crash on file system errors
214 |       expect(() => TelemetryConfigManager.getInstance()).not.toThrow();
215 |     });
216 | 
217 |     it('should handle write errors during config save', () => {
218 |       vi.mocked(existsSync).mockReturnValue(true);
219 |       vi.mocked(readFileSync).mockReturnValue(JSON.stringify({
220 |         enabled: false,
221 |         userId: 'test-id'
222 |       }));
223 |       vi.mocked(writeFileSync).mockImplementation(() => {
224 |         throw new Error('Disk full');
225 |       });
226 | 
227 |       manager = TelemetryConfigManager.getInstance();
228 | 
229 |       // Should not crash on write errors
230 |       expect(() => manager.enable()).not.toThrow();
231 |       expect(() => manager.disable()).not.toThrow();
232 |     });
233 | 
234 |     it('should handle missing home directory', () => {
235 |       // Mock homedir to return empty string
236 |       const originalHomedir = require('os').homedir;
237 |       vi.doMock('os', () => ({
238 |         homedir: () => ''
239 |       }));
240 | 
241 |       vi.mocked(existsSync).mockReturnValue(false);
242 | 
243 |       expect(() => TelemetryConfigManager.getInstance()).not.toThrow();
244 |     });
245 | 
246 |     it('should generate valid user ID when crypto.randomBytes fails', () => {
247 |       vi.mocked(existsSync).mockReturnValue(false);
248 | 
249 |       // Mock crypto to fail
250 |       vi.doMock('crypto', () => ({
251 |         randomBytes: () => {
252 |           throw new Error('Crypto not available');
253 |         }
254 |       }));
255 | 
256 |       manager = TelemetryConfigManager.getInstance();
257 |       const config = manager.loadConfig();
258 | 
259 |       expect(config.userId).toBeDefined();
260 |       expect(config.userId).toMatch(/^[a-f0-9]{16}$/);
261 |     });
262 | 
263 |     it('should handle concurrent access to config file', () => {
264 |       let readCount = 0;
265 |       vi.mocked(existsSync).mockReturnValue(true);
266 |       vi.mocked(readFileSync).mockImplementation(() => {
267 |         readCount++;
268 |         if (readCount === 1) {
269 |           return JSON.stringify({
270 |             enabled: false,
271 |             userId: 'test-id-1'
272 |           });
273 |         }
274 |         return JSON.stringify({
275 |           enabled: true,
276 |           userId: 'test-id-2'
277 |         });
278 |       });
279 | 
280 |       const manager1 = TelemetryConfigManager.getInstance();
281 |       const manager2 = TelemetryConfigManager.getInstance();
282 | 
283 |       // Should be same instance due to singleton pattern
284 |       expect(manager1).toBe(manager2);
285 |     });
286 | 
287 |     it('should handle environment variable overrides', () => {
288 |       const originalEnv = process.env.N8N_MCP_TELEMETRY_DISABLED;
289 | 
290 |       // Test with environment variable set to disable telemetry
291 |       process.env.N8N_MCP_TELEMETRY_DISABLED = 'true';
292 |       vi.mocked(existsSync).mockReturnValue(true);
293 |       vi.mocked(readFileSync).mockReturnValue(JSON.stringify({
294 |         enabled: true,
295 |         userId: 'test-id'
296 |       }));
297 | 
298 |       (TelemetryConfigManager as any).instance = null;
299 |       manager = TelemetryConfigManager.getInstance();
300 | 
301 |       expect(manager.isEnabled()).toBe(false);
302 | 
303 |       // Test with environment variable set to enable telemetry
304 |       process.env.N8N_MCP_TELEMETRY_DISABLED = 'false';
305 |       (TelemetryConfigManager as any).instance = null;
306 |       vi.mocked(readFileSync).mockReturnValue(JSON.stringify({
307 |         enabled: true,
308 |         userId: 'test-id'
309 |       }));
310 |       manager = TelemetryConfigManager.getInstance();
311 | 
312 |       expect(manager.isEnabled()).toBe(true);
313 | 
314 |       // Restore original environment
315 |       process.env.N8N_MCP_TELEMETRY_DISABLED = originalEnv;
316 |     });
317 | 
318 |     it('should handle invalid JSON in config file gracefully', () => {
319 |       vi.mocked(existsSync).mockReturnValue(true);
320 |       vi.mocked(readFileSync).mockReturnValue('{ invalid json syntax');
321 | 
322 |       manager = TelemetryConfigManager.getInstance();
323 |       const config = manager.loadConfig();
324 | 
325 |       expect(config.enabled).toBe(false); // Default to disabled on corrupt config
326 |       expect(config.userId).toMatch(/^[a-f0-9]{16}$/); // Should generate new user ID
327 |     });
328 | 
329 |     it('should handle config file with partial structure', () => {
330 |       vi.mocked(existsSync).mockReturnValue(true);
331 |       vi.mocked(readFileSync).mockReturnValue(JSON.stringify({
332 |         enabled: true
333 |         // Missing userId and firstRun
334 |       }));
335 | 
336 |       manager = TelemetryConfigManager.getInstance();
337 |       const config = manager.loadConfig();
338 | 
339 |       expect(config.enabled).toBe(true);
340 |       expect(config.userId).toMatch(/^[a-f0-9]{16}$/);
341 |       // firstRun might not be defined if config is partial and loaded from disk
342 |       // The implementation only adds firstRun on first creation
343 |     });
344 | 
345 |     it('should handle config file with invalid data types', () => {
346 |       vi.mocked(existsSync).mockReturnValue(true);
347 |       vi.mocked(readFileSync).mockReturnValue(JSON.stringify({
348 |         enabled: 'not-a-boolean',
349 |         userId: 12345, // Not a string
350 |         firstRun: null
351 |       }));
352 | 
353 |       manager = TelemetryConfigManager.getInstance();
354 |       const config = manager.loadConfig();
355 | 
356 |       // The config manager loads the data as-is, so we get the original types
357 |       // The validation happens during usage, not loading
358 |       expect(config.enabled).toBe('not-a-boolean');
359 |       expect(config.userId).toBe(12345);
360 |     });
361 | 
362 |     it('should handle very large config files', () => {
363 |       const largeConfig = {
364 |         enabled: true,
365 |         userId: 'test-id',
366 |         firstRun: '2024-01-01T00:00:00Z',
367 |         extraData: 'x'.repeat(1000000) // 1MB of data
368 |       };
369 | 
370 |       vi.mocked(existsSync).mockReturnValue(true);
371 |       vi.mocked(readFileSync).mockReturnValue(JSON.stringify(largeConfig));
372 | 
373 |       expect(() => TelemetryConfigManager.getInstance()).not.toThrow();
374 |     });
375 | 
376 |     it('should handle config directory creation race conditions', () => {
377 |       vi.mocked(existsSync).mockReturnValue(false);
378 |       let mkdirCallCount = 0;
379 |       vi.mocked(mkdirSync).mockImplementation(() => {
380 |         mkdirCallCount++;
381 |         if (mkdirCallCount === 1) {
382 |           throw new Error('EEXIST: file already exists');
383 |         }
384 |         return undefined;
385 |       });
386 | 
387 |       expect(() => TelemetryConfigManager.getInstance()).not.toThrow();
388 |     });
389 | 
390 |     it('should handle file system permission changes', () => {
391 |       vi.mocked(existsSync).mockReturnValue(true);
392 |       vi.mocked(readFileSync).mockReturnValue(JSON.stringify({
393 |         enabled: false,
394 |         userId: 'test-id'
395 |       }));
396 | 
397 |       manager = TelemetryConfigManager.getInstance();
398 | 
399 |       // Simulate permission denied on subsequent write
400 |       vi.mocked(writeFileSync).mockImplementationOnce(() => {
401 |         throw new Error('EACCES: permission denied');
402 |       });
403 | 
404 |       expect(() => manager.enable()).not.toThrow();
405 |     });
406 | 
407 |     it('should handle system clock changes affecting timestamps', () => {
408 |       const futureDate = new Date(Date.now() + 365 * 24 * 60 * 60 * 1000); // 1 year in future
409 |       const pastDate = new Date(Date.now() - 365 * 24 * 60 * 60 * 1000); // 1 year in past
410 | 
411 |       vi.mocked(existsSync).mockReturnValue(true);
412 |       vi.mocked(readFileSync).mockReturnValue(JSON.stringify({
413 |         enabled: true,
414 |         userId: 'test-id',
415 |         firstRun: futureDate.toISOString()
416 |       }));
417 | 
418 |       manager = TelemetryConfigManager.getInstance();
419 |       const config = manager.loadConfig();
420 | 
421 |       expect(config.firstRun).toBeDefined();
422 |       expect(new Date(config.firstRun as string).getTime()).toBeGreaterThan(0);
423 |     });
424 | 
425 |     it('should handle config updates during runtime', () => {
426 |       vi.mocked(existsSync).mockReturnValue(true);
427 |       vi.mocked(readFileSync).mockReturnValue(JSON.stringify({
428 |         enabled: false,
429 |         userId: 'test-id'
430 |       }));
431 | 
432 |       manager = TelemetryConfigManager.getInstance();
433 |       expect(manager.isEnabled()).toBe(false);
434 | 
435 |       // Simulate external config change by clearing cache first
436 |       (manager as any).config = null;
437 |       vi.mocked(readFileSync).mockReturnValue(JSON.stringify({
438 |         enabled: true,
439 |         userId: 'test-id'
440 |       }));
441 | 
442 |       // Now calling loadConfig should pick up changes
443 |       const newConfig = manager.loadConfig();
444 |       expect(newConfig.enabled).toBe(true);
445 |       expect(manager.isEnabled()).toBe(true);
446 |     });
447 | 
448 |     it('should handle multiple rapid enable/disable calls', () => {
449 |       vi.mocked(existsSync).mockReturnValue(true);
450 |       vi.mocked(readFileSync).mockReturnValue(JSON.stringify({
451 |         enabled: false,
452 |         userId: 'test-id'
453 |       }));
454 | 
455 |       manager = TelemetryConfigManager.getInstance();
456 | 
457 |       // Rapidly toggle state
458 |       for (let i = 0; i < 100; i++) {
459 |         if (i % 2 === 0) {
460 |           manager.enable();
461 |         } else {
462 |           manager.disable();
463 |         }
464 |       }
465 | 
466 |       // Should not crash and maintain consistent state
467 |       expect(typeof manager.isEnabled()).toBe('boolean');
468 |     });
469 | 
470 |     it('should handle user ID collision (extremely unlikely)', () => {
471 |       vi.mocked(existsSync).mockReturnValue(false);
472 | 
473 |       // Mock crypto to always return same bytes
474 |       const mockBytes = Buffer.from([1, 2, 3, 4, 5, 6, 7, 8]);
475 |       vi.doMock('crypto', () => ({
476 |         randomBytes: () => mockBytes
477 |       }));
478 | 
479 |       (TelemetryConfigManager as any).instance = null;
480 |       const manager1 = TelemetryConfigManager.getInstance();
481 |       const userId1 = manager1.getUserId();
482 | 
483 |       (TelemetryConfigManager as any).instance = null;
484 |       const manager2 = TelemetryConfigManager.getInstance();
485 |       const userId2 = manager2.getUserId();
486 | 
487 |       // Should generate same ID from same random bytes
488 |       expect(userId1).toBe(userId2);
489 |       expect(userId1).toMatch(/^[a-f0-9]{16}$/);
490 |     });
491 | 
492 |     it('should handle status generation with missing fields', () => {
493 |       vi.mocked(existsSync).mockReturnValue(true);
494 |       vi.mocked(readFileSync).mockReturnValue(JSON.stringify({
495 |         enabled: true
496 |         // Missing userId and firstRun
497 |       }));
498 | 
499 |       manager = TelemetryConfigManager.getInstance();
500 |       const status = manager.getStatus();
501 | 
502 |       expect(status).toContain('ENABLED');
503 |       expect(status).toBeDefined();
504 |       expect(typeof status).toBe('string');
505 |     });
506 |   });
507 | 
508 |   describe('Docker/Cloud user ID generation', () => {
509 |     let originalIsDocker: string | undefined;
510 |     let originalRailway: string | undefined;
511 | 
512 |     beforeEach(() => {
513 |       originalIsDocker = process.env.IS_DOCKER;
514 |       originalRailway = process.env.RAILWAY_ENVIRONMENT;
515 |     });
516 | 
517 |     afterEach(() => {
518 |       if (originalIsDocker === undefined) {
519 |         delete process.env.IS_DOCKER;
520 |       } else {
521 |         process.env.IS_DOCKER = originalIsDocker;
522 |       }
523 | 
524 |       if (originalRailway === undefined) {
525 |         delete process.env.RAILWAY_ENVIRONMENT;
526 |       } else {
527 |         process.env.RAILWAY_ENVIRONMENT = originalRailway;
528 |       }
529 |     });
530 | 
531 |     describe('boot_id reading', () => {
532 |       it('should read valid boot_id from /proc/sys/kernel/random/boot_id', () => {
533 |         const mockBootId = 'f3c371fe-8a77-4592-8332-7a4d0d88d4ac';
534 |         process.env.IS_DOCKER = 'true';
535 | 
536 |         vi.mocked(existsSync).mockImplementation((path: any) => {
537 |           if (path === '/proc/sys/kernel/random/boot_id') return true;
538 |           return false;
539 |         });
540 | 
541 |         vi.mocked(readFileSync).mockImplementation((path: any) => {
542 |           if (path === '/proc/sys/kernel/random/boot_id') return mockBootId;
543 |           throw new Error('File not found');
544 |         });
545 | 
546 |         (TelemetryConfigManager as any).instance = null;
547 |         manager = TelemetryConfigManager.getInstance();
548 |         const userId = manager.getUserId();
549 | 
550 |         expect(userId).toMatch(/^[a-f0-9]{16}$/);
551 |         expect(vi.mocked(readFileSync)).toHaveBeenCalledWith(
552 |           '/proc/sys/kernel/random/boot_id',
553 |           'utf-8'
554 |         );
555 |       });
556 | 
557 |       it('should validate boot_id UUID format', () => {
558 |         const invalidBootId = 'not-a-valid-uuid';
559 |         process.env.IS_DOCKER = 'true';
560 | 
561 |         vi.mocked(existsSync).mockImplementation((path: any) => {
562 |           if (path === '/proc/sys/kernel/random/boot_id') return true;
563 |           if (path === '/proc/cpuinfo') return true;
564 |           if (path === '/proc/meminfo') return true;
565 |           return false;
566 |         });
567 | 
568 |         vi.mocked(readFileSync).mockImplementation((path: any) => {
569 |           if (path === '/proc/sys/kernel/random/boot_id') return invalidBootId;
570 |           if (path === '/proc/cpuinfo') return 'processor: 0\nprocessor: 1\n';
571 |           if (path === '/proc/meminfo') return 'MemTotal: 8040052 kB\n';
572 |           throw new Error('File not found');
573 |         });
574 | 
575 |         (TelemetryConfigManager as any).instance = null;
576 |         manager = TelemetryConfigManager.getInstance();
577 |         const userId = manager.getUserId();
578 | 
579 |         // Should fallback to combined fingerprint, not use invalid boot_id
580 |         expect(userId).toMatch(/^[a-f0-9]{16}$/);
581 |       });
582 | 
583 |       it('should handle boot_id file not existing', () => {
584 |         process.env.IS_DOCKER = 'true';
585 | 
586 |         vi.mocked(existsSync).mockImplementation((path: any) => {
587 |           if (path === '/proc/sys/kernel/random/boot_id') return false;
588 |           if (path === '/proc/cpuinfo') return true;
589 |           if (path === '/proc/meminfo') return true;
590 |           return false;
591 |         });
592 | 
593 |         vi.mocked(readFileSync).mockImplementation((path: any) => {
594 |           if (path === '/proc/cpuinfo') return 'processor: 0\nprocessor: 1\n';
595 |           if (path === '/proc/meminfo') return 'MemTotal: 8040052 kB\n';
596 |           throw new Error('File not found');
597 |         });
598 | 
599 |         (TelemetryConfigManager as any).instance = null;
600 |         manager = TelemetryConfigManager.getInstance();
601 |         const userId = manager.getUserId();
602 | 
603 |         // Should fallback to combined fingerprint
604 |         expect(userId).toMatch(/^[a-f0-9]{16}$/);
605 |       });
606 | 
607 |       it('should handle boot_id read errors gracefully', () => {
608 |         process.env.IS_DOCKER = 'true';
609 | 
610 |         vi.mocked(existsSync).mockImplementation((path: any) => {
611 |           if (path === '/proc/sys/kernel/random/boot_id') return true;
612 |           return false;
613 |         });
614 | 
615 |         vi.mocked(readFileSync).mockImplementation((path: any) => {
616 |           if (path === '/proc/sys/kernel/random/boot_id') {
617 |             throw new Error('Permission denied');
618 |           }
619 |           throw new Error('File not found');
620 |         });
621 | 
622 |         (TelemetryConfigManager as any).instance = null;
623 |         manager = TelemetryConfigManager.getInstance();
624 |         const userId = manager.getUserId();
625 | 
626 |         // Should fallback gracefully
627 |         expect(userId).toMatch(/^[a-f0-9]{16}$/);
628 |       });
629 | 
630 |       it('should generate consistent user ID from same boot_id', () => {
631 |         const mockBootId = 'f3c371fe-8a77-4592-8332-7a4d0d88d4ac';
632 |         process.env.IS_DOCKER = 'true';
633 | 
634 |         vi.mocked(existsSync).mockImplementation((path: any) => {
635 |           if (path === '/proc/sys/kernel/random/boot_id') return true;
636 |           return false;
637 |         });
638 | 
639 |         vi.mocked(readFileSync).mockImplementation((path: any) => {
640 |           if (path === '/proc/sys/kernel/random/boot_id') return mockBootId;
641 |           throw new Error('File not found');
642 |         });
643 | 
644 |         (TelemetryConfigManager as any).instance = null;
645 |         const manager1 = TelemetryConfigManager.getInstance();
646 |         const userId1 = manager1.getUserId();
647 | 
648 |         (TelemetryConfigManager as any).instance = null;
649 |         const manager2 = TelemetryConfigManager.getInstance();
650 |         const userId2 = manager2.getUserId();
651 | 
652 |         // Same boot_id should produce same user_id
653 |         expect(userId1).toBe(userId2);
654 |       });
655 |     });
656 | 
657 |     describe('combined fingerprint fallback', () => {
658 |       it('should generate fingerprint from CPU, memory, and kernel', () => {
659 |         process.env.IS_DOCKER = 'true';
660 | 
661 |         vi.mocked(existsSync).mockImplementation((path: any) => {
662 |           if (path === '/proc/sys/kernel/random/boot_id') return false;
663 |           if (path === '/proc/cpuinfo') return true;
664 |           if (path === '/proc/meminfo') return true;
665 |           if (path === '/proc/version') return true;
666 |           return false;
667 |         });
668 | 
669 |         vi.mocked(readFileSync).mockImplementation((path: any) => {
670 |           if (path === '/proc/cpuinfo') return 'processor: 0\nprocessor: 1\nprocessor: 2\nprocessor: 3\n';
671 |           if (path === '/proc/meminfo') return 'MemTotal: 8040052 kB\n';
672 |           if (path === '/proc/version') return 'Linux version 5.15.49-linuxkit';
673 |           throw new Error('File not found');
674 |         });
675 | 
676 |         (TelemetryConfigManager as any).instance = null;
677 |         manager = TelemetryConfigManager.getInstance();
678 |         const userId = manager.getUserId();
679 | 
680 |         expect(userId).toMatch(/^[a-f0-9]{16}$/);
681 |       });
682 | 
683 |       it('should require at least 3 signals for combined fingerprint', () => {
684 |         process.env.IS_DOCKER = 'true';
685 | 
686 |         vi.mocked(existsSync).mockImplementation((path: any) => {
687 |           if (path === '/proc/sys/kernel/random/boot_id') return false;
688 |           // Only platform and arch available (2 signals)
689 |           return false;
690 |         });
691 | 
692 |         (TelemetryConfigManager as any).instance = null;
693 |         manager = TelemetryConfigManager.getInstance();
694 |         const userId = manager.getUserId();
695 | 
696 |         // Should fallback to generic Docker ID
697 |         expect(userId).toMatch(/^[a-f0-9]{16}$/);
698 |       });
699 | 
700 |       it('should handle partial /proc data', () => {
701 |         process.env.IS_DOCKER = 'true';
702 | 
703 |         vi.mocked(existsSync).mockImplementation((path: any) => {
704 |           if (path === '/proc/sys/kernel/random/boot_id') return false;
705 |           if (path === '/proc/cpuinfo') return true;
706 |           // meminfo missing
707 |           return false;
708 |         });
709 | 
710 |         vi.mocked(readFileSync).mockImplementation((path: any) => {
711 |           if (path === '/proc/cpuinfo') return 'processor: 0\nprocessor: 1\n';
712 |           throw new Error('File not found');
713 |         });
714 | 
715 |         (TelemetryConfigManager as any).instance = null;
716 |         manager = TelemetryConfigManager.getInstance();
717 |         const userId = manager.getUserId();
718 | 
719 |         // Should include platform and arch, so 4 signals total
720 |         expect(userId).toMatch(/^[a-f0-9]{16}$/);
721 |       });
722 |     });
723 | 
724 |     describe('environment detection', () => {
725 |       it('should use Docker method when IS_DOCKER=true', () => {
726 |         process.env.IS_DOCKER = 'true';
727 | 
728 |         vi.mocked(existsSync).mockReturnValue(false);
729 | 
730 |         (TelemetryConfigManager as any).instance = null;
731 |         manager = TelemetryConfigManager.getInstance();
732 |         const userId = manager.getUserId();
733 | 
734 |         expect(userId).toMatch(/^[a-f0-9]{16}$/);
735 |         // Should attempt to read boot_id
736 |         expect(vi.mocked(existsSync)).toHaveBeenCalledWith('/proc/sys/kernel/random/boot_id');
737 |       });
738 | 
739 |       it('should use Docker method for Railway environment', () => {
740 |         process.env.RAILWAY_ENVIRONMENT = 'production';
741 |         delete process.env.IS_DOCKER;
742 | 
743 |         vi.mocked(existsSync).mockReturnValue(false);
744 | 
745 |         (TelemetryConfigManager as any).instance = null;
746 |         manager = TelemetryConfigManager.getInstance();
747 |         const userId = manager.getUserId();
748 | 
749 |         expect(userId).toMatch(/^[a-f0-9]{16}$/);
750 |         // Should attempt to read boot_id
751 |         expect(vi.mocked(existsSync)).toHaveBeenCalledWith('/proc/sys/kernel/random/boot_id');
752 |       });
753 | 
754 |       it('should use file-based method for local installation', () => {
755 |         delete process.env.IS_DOCKER;
756 |         delete process.env.RAILWAY_ENVIRONMENT;
757 | 
758 |         vi.mocked(existsSync).mockReturnValue(false);
759 | 
760 |         (TelemetryConfigManager as any).instance = null;
761 |         manager = TelemetryConfigManager.getInstance();
762 |         const userId = manager.getUserId();
763 | 
764 |         expect(userId).toMatch(/^[a-f0-9]{16}$/);
765 |         // Should NOT attempt to read boot_id
766 |         const calls = vi.mocked(existsSync).mock.calls;
767 |         const bootIdCalls = calls.filter(call => call[0] === '/proc/sys/kernel/random/boot_id');
768 |         expect(bootIdCalls.length).toBe(0);
769 |       });
770 | 
771 |       it('should detect cloud platforms', () => {
772 |         const cloudEnvVars = [
773 |           'RAILWAY_ENVIRONMENT',
774 |           'RENDER',
775 |           'FLY_APP_NAME',
776 |           'HEROKU_APP_NAME',
777 |           'AWS_EXECUTION_ENV',
778 |           'KUBERNETES_SERVICE_HOST',
779 |           'GOOGLE_CLOUD_PROJECT',
780 |           'AZURE_FUNCTIONS_ENVIRONMENT'
781 |         ];
782 | 
783 |         cloudEnvVars.forEach(envVar => {
784 |           // Clear all env vars
785 |           cloudEnvVars.forEach(v => delete process.env[v]);
786 |           delete process.env.IS_DOCKER;
787 | 
788 |           // Set one cloud env var
789 |           process.env[envVar] = 'true';
790 | 
791 |           vi.mocked(existsSync).mockReturnValue(false);
792 | 
793 |           (TelemetryConfigManager as any).instance = null;
794 |           manager = TelemetryConfigManager.getInstance();
795 |           const userId = manager.getUserId();
796 | 
797 |           expect(userId).toMatch(/^[a-f0-9]{16}$/);
798 | 
799 |           // Should attempt to read boot_id
800 |           const calls = vi.mocked(existsSync).mock.calls;
801 |           const bootIdCalls = calls.filter(call => call[0] === '/proc/sys/kernel/random/boot_id');
802 |           expect(bootIdCalls.length).toBeGreaterThan(0);
803 | 
804 |           // Clean up
805 |           delete process.env[envVar];
806 |         });
807 |       });
808 |     });
809 | 
810 |     describe('fallback chain execution', () => {
811 |       it('should fallback from boot_id → combined → generic', () => {
812 |         process.env.IS_DOCKER = 'true';
813 | 
814 |         // All methods fail
815 |         vi.mocked(existsSync).mockReturnValue(false);
816 |         vi.mocked(readFileSync).mockImplementation(() => {
817 |           throw new Error('File not found');
818 |         });
819 | 
820 |         (TelemetryConfigManager as any).instance = null;
821 |         manager = TelemetryConfigManager.getInstance();
822 |         const userId = manager.getUserId();
823 | 
824 |         // Should still generate a generic Docker ID
825 |         expect(userId).toMatch(/^[a-f0-9]{16}$/);
826 |       });
827 | 
828 |       it('should use boot_id if available (highest priority)', () => {
829 |         const mockBootId = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee';
830 |         process.env.IS_DOCKER = 'true';
831 | 
832 |         vi.mocked(existsSync).mockImplementation((path: any) => {
833 |           if (path === '/proc/sys/kernel/random/boot_id') return true;
834 |           return true; // All other files available too
835 |         });
836 | 
837 |         vi.mocked(readFileSync).mockImplementation((path: any) => {
838 |           if (path === '/proc/sys/kernel/random/boot_id') return mockBootId;
839 |           if (path === '/proc/cpuinfo') return 'processor: 0\n';
840 |           if (path === '/proc/meminfo') return 'MemTotal: 1000000 kB\n';
841 |           return 'mock data';
842 |         });
843 | 
844 |         (TelemetryConfigManager as any).instance = null;
845 |         const manager1 = TelemetryConfigManager.getInstance();
846 |         const userId1 = manager1.getUserId();
847 | 
848 |         // Now break boot_id but keep combined signals
849 |         vi.mocked(existsSync).mockImplementation((path: any) => {
850 |           if (path === '/proc/sys/kernel/random/boot_id') return false;
851 |           return true;
852 |         });
853 | 
854 |         (TelemetryConfigManager as any).instance = null;
855 |         const manager2 = TelemetryConfigManager.getInstance();
856 |         const userId2 = manager2.getUserId();
857 | 
858 |         // Different methods should produce different IDs
859 |         expect(userId1).not.toBe(userId2);
860 |         expect(userId1).toMatch(/^[a-f0-9]{16}$/);
861 |         expect(userId2).toMatch(/^[a-f0-9]{16}$/);
862 |       });
863 |     });
864 |   });
865 | });
```
Page 37/59FirstPrevNextLast