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

# Directory Structure

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

# Files

--------------------------------------------------------------------------------
/tests/unit/utils/ssrf-protection.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
  2 | 
  3 | // Mock dns module before importing SSRFProtection
  4 | vi.mock('dns/promises', () => ({
  5 |   lookup: vi.fn(),
  6 | }));
  7 | 
  8 | import { SSRFProtection } from '../../../src/utils/ssrf-protection';
  9 | import * as dns from 'dns/promises';
 10 | 
 11 | /**
 12 |  * Unit tests for SSRFProtection with configurable security modes
 13 |  *
 14 |  * SECURITY: These tests verify SSRF protection blocks malicious URLs in all modes
 15 |  * See: https://github.com/czlonkowski/n8n-mcp/issues/265 (HIGH-03)
 16 |  */
 17 | describe('SSRFProtection', () => {
 18 |   const originalEnv = process.env.WEBHOOK_SECURITY_MODE;
 19 | 
 20 |   beforeEach(() => {
 21 |     // Clear all mocks before each test
 22 |     vi.clearAllMocks();
 23 |     // Default mock: simulate real DNS behavior - return the hostname as IP if it looks like an IP
 24 |     vi.mocked(dns.lookup).mockImplementation(async (hostname: any) => {
 25 |       // Handle special hostname "localhost"
 26 |       if (hostname === 'localhost') {
 27 |         return { address: '127.0.0.1', family: 4 } as any;
 28 |       }
 29 | 
 30 |       // If hostname is an IP address, return it as-is (simulating real DNS behavior)
 31 |       const ipv4Regex = /^(\d{1,3}\.){3}\d{1,3}$/;
 32 |       const ipv6Regex = /^([0-9a-fA-F]{0,4}:)+[0-9a-fA-F]{0,4}$/;
 33 | 
 34 |       if (ipv4Regex.test(hostname)) {
 35 |         return { address: hostname, family: 4 } as any;
 36 |       }
 37 |       if (ipv6Regex.test(hostname) || hostname === '::1') {
 38 |         return { address: hostname, family: 6 } as any;
 39 |       }
 40 | 
 41 |       // For actual hostnames, return a public IP by default
 42 |       return { address: '8.8.8.8', family: 4 } as any;
 43 |     });
 44 |   });
 45 | 
 46 |   afterEach(() => {
 47 |     // Restore original environment
 48 |     if (originalEnv) {
 49 |       process.env.WEBHOOK_SECURITY_MODE = originalEnv;
 50 |     } else {
 51 |       delete process.env.WEBHOOK_SECURITY_MODE;
 52 |     }
 53 |     vi.restoreAllMocks();
 54 |   });
 55 | 
 56 |   describe('Strict Mode (default)', () => {
 57 |     beforeEach(() => {
 58 |       delete process.env.WEBHOOK_SECURITY_MODE; // Use default strict
 59 |     });
 60 | 
 61 |     it('should block localhost', async () => {
 62 |       const localhostURLs = [
 63 |         'http://localhost:3000/webhook',
 64 |         'http://127.0.0.1/webhook',
 65 |         'http://[::1]/webhook',
 66 |       ];
 67 | 
 68 |       for (const url of localhostURLs) {
 69 |         const result = await SSRFProtection.validateWebhookUrl(url);
 70 |         expect(result.valid, `URL ${url} should be blocked but was valid`).toBe(false);
 71 |         expect(result.reason, `URL ${url} should have a reason`).toBeDefined();
 72 |       }
 73 |     });
 74 | 
 75 |     it('should block AWS metadata endpoint', async () => {
 76 |       const result = await SSRFProtection.validateWebhookUrl('http://169.254.169.254/latest/meta-data');
 77 |       expect(result.valid).toBe(false);
 78 |       expect(result.reason).toContain('Cloud metadata');
 79 |     });
 80 | 
 81 |     it('should block GCP metadata endpoint', async () => {
 82 |       const result = await SSRFProtection.validateWebhookUrl('http://metadata.google.internal/computeMetadata/v1/');
 83 |       expect(result.valid).toBe(false);
 84 |       expect(result.reason).toContain('Cloud metadata');
 85 |     });
 86 | 
 87 |     it('should block Alibaba Cloud metadata endpoint', async () => {
 88 |       const result = await SSRFProtection.validateWebhookUrl('http://100.100.100.200/latest/meta-data');
 89 |       expect(result.valid).toBe(false);
 90 |       expect(result.reason).toContain('Cloud metadata');
 91 |     });
 92 | 
 93 |     it('should block Oracle Cloud metadata endpoint', async () => {
 94 |       const result = await SSRFProtection.validateWebhookUrl('http://192.0.0.192/opc/v2/instance/');
 95 |       expect(result.valid).toBe(false);
 96 |       expect(result.reason).toContain('Cloud metadata');
 97 |     });
 98 | 
 99 |     it('should block private IP ranges', async () => {
100 |       const privateIPs = [
101 |         'http://10.0.0.1/webhook',
102 |         'http://192.168.1.1/webhook',
103 |         'http://172.16.0.1/webhook',
104 |         'http://172.31.255.255/webhook',
105 |       ];
106 | 
107 |       for (const url of privateIPs) {
108 |         const result = await SSRFProtection.validateWebhookUrl(url);
109 |         expect(result.valid).toBe(false);
110 |         expect(result.reason).toContain('Private IP');
111 |       }
112 |     });
113 | 
114 |     it('should allow public URLs', async () => {
115 |       const publicURLs = [
116 |         'https://hooks.example.com/webhook',
117 |         'https://api.external.com/callback',
118 |         'http://public-service.com:8080/hook',
119 |       ];
120 | 
121 |       for (const url of publicURLs) {
122 |         const result = await SSRFProtection.validateWebhookUrl(url);
123 |         expect(result.valid).toBe(true);
124 |         expect(result.reason).toBeUndefined();
125 |       }
126 |     });
127 | 
128 |     it('should block non-HTTP protocols', async () => {
129 |       const invalidProtocols = [
130 |         'file:///etc/passwd',
131 |         'ftp://internal-server/file',
132 |         'gopher://old-service',
133 |       ];
134 | 
135 |       for (const url of invalidProtocols) {
136 |         const result = await SSRFProtection.validateWebhookUrl(url);
137 |         expect(result.valid).toBe(false);
138 |         expect(result.reason).toContain('protocol');
139 |       }
140 |     });
141 |   });
142 | 
143 |   describe('Moderate Mode', () => {
144 |     beforeEach(() => {
145 |       process.env.WEBHOOK_SECURITY_MODE = 'moderate';
146 |     });
147 | 
148 |     it('should allow localhost', async () => {
149 |       const localhostURLs = [
150 |         'http://localhost:5678/webhook',
151 |         'http://127.0.0.1:5678/webhook',
152 |         'http://[::1]:5678/webhook',
153 |       ];
154 | 
155 |       for (const url of localhostURLs) {
156 |         const result = await SSRFProtection.validateWebhookUrl(url);
157 |         expect(result.valid).toBe(true);
158 |       }
159 |     });
160 | 
161 |     it('should still block private IPs', async () => {
162 |       const privateIPs = [
163 |         'http://10.0.0.1/webhook',
164 |         'http://192.168.1.1/webhook',
165 |         'http://172.16.0.1/webhook',
166 |       ];
167 | 
168 |       for (const url of privateIPs) {
169 |         const result = await SSRFProtection.validateWebhookUrl(url);
170 |         expect(result.valid).toBe(false);
171 |         expect(result.reason).toContain('Private IP');
172 |       }
173 |     });
174 | 
175 |     it('should still block cloud metadata', async () => {
176 |       const metadataURLs = [
177 |         'http://169.254.169.254/latest/meta-data',
178 |         'http://metadata.google.internal/computeMetadata/v1/',
179 |       ];
180 | 
181 |       for (const url of metadataURLs) {
182 |         const result = await SSRFProtection.validateWebhookUrl(url);
183 |         expect(result.valid).toBe(false);
184 |         expect(result.reason).toContain('metadata');
185 |       }
186 |     });
187 | 
188 |     it('should allow public URLs', async () => {
189 |       const result = await SSRFProtection.validateWebhookUrl('https://api.example.com/webhook');
190 |       expect(result.valid).toBe(true);
191 |     });
192 |   });
193 | 
194 |   describe('Permissive Mode', () => {
195 |     beforeEach(() => {
196 |       process.env.WEBHOOK_SECURITY_MODE = 'permissive';
197 |     });
198 | 
199 |     it('should allow localhost', async () => {
200 |       const result = await SSRFProtection.validateWebhookUrl('http://localhost:5678/webhook');
201 |       expect(result.valid).toBe(true);
202 |     });
203 | 
204 |     it('should allow private IPs', async () => {
205 |       const privateIPs = [
206 |         'http://10.0.0.1/webhook',
207 |         'http://192.168.1.1/webhook',
208 |         'http://172.16.0.1/webhook',
209 |       ];
210 | 
211 |       for (const url of privateIPs) {
212 |         const result = await SSRFProtection.validateWebhookUrl(url);
213 |         expect(result.valid).toBe(true);
214 |       }
215 |     });
216 | 
217 |     it('should still block cloud metadata', async () => {
218 |       const metadataURLs = [
219 |         'http://169.254.169.254/latest/meta-data',
220 |         'http://metadata.google.internal/computeMetadata/v1/',
221 |         'http://169.254.170.2/v2/metadata',
222 |       ];
223 | 
224 |       for (const url of metadataURLs) {
225 |         const result = await SSRFProtection.validateWebhookUrl(url);
226 |         expect(result.valid).toBe(false);
227 |         expect(result.reason).toContain('metadata');
228 |       }
229 |     });
230 | 
231 |     it('should allow public URLs', async () => {
232 |       const result = await SSRFProtection.validateWebhookUrl('https://api.example.com/webhook');
233 |       expect(result.valid).toBe(true);
234 |     });
235 |   });
236 | 
237 |   describe('DNS Rebinding Prevention', () => {
238 |     it('should block hostname resolving to private IP (strict mode)', async () => {
239 |       delete process.env.WEBHOOK_SECURITY_MODE; // strict
240 | 
241 |       // Mock DNS lookup to return private IP
242 |       vi.mocked(dns.lookup).mockResolvedValue({ address: '10.0.0.1', family: 4 } as any);
243 | 
244 |       const result = await SSRFProtection.validateWebhookUrl('http://evil.example.com/webhook');
245 |       expect(result.valid).toBe(false);
246 |       expect(result.reason).toContain('Private IP');
247 |     });
248 | 
249 |     it('should block hostname resolving to private IP (moderate mode)', async () => {
250 |       process.env.WEBHOOK_SECURITY_MODE = 'moderate';
251 | 
252 |       // Mock DNS lookup to return private IP
253 |       vi.mocked(dns.lookup).mockResolvedValue({ address: '192.168.1.100', family: 4 } as any);
254 | 
255 |       const result = await SSRFProtection.validateWebhookUrl('http://internal.company.com/webhook');
256 |       expect(result.valid).toBe(false);
257 |       expect(result.reason).toContain('Private IP');
258 |     });
259 | 
260 |     it('should allow hostname resolving to private IP (permissive mode)', async () => {
261 |       process.env.WEBHOOK_SECURITY_MODE = 'permissive';
262 | 
263 |       // Mock DNS lookup to return private IP
264 |       vi.mocked(dns.lookup).mockResolvedValue({ address: '192.168.1.100', family: 4 } as any);
265 | 
266 |       const result = await SSRFProtection.validateWebhookUrl('http://internal.company.com/webhook');
267 |       expect(result.valid).toBe(true);
268 |     });
269 | 
270 |     it('should block hostname resolving to cloud metadata (all modes)', async () => {
271 |       const modes = ['strict', 'moderate', 'permissive'];
272 | 
273 |       for (const mode of modes) {
274 |         process.env.WEBHOOK_SECURITY_MODE = mode;
275 | 
276 |         // Mock DNS lookup to return cloud metadata IP
277 |         vi.mocked(dns.lookup).mockResolvedValue({ address: '169.254.169.254', family: 4 } as any);
278 | 
279 |         const result = await SSRFProtection.validateWebhookUrl('http://evil-domain.com/webhook');
280 |         expect(result.valid).toBe(false);
281 |         expect(result.reason).toContain('metadata');
282 |       }
283 |     });
284 | 
285 |     it('should block hostname resolving to localhost IP (strict mode)', async () => {
286 |       delete process.env.WEBHOOK_SECURITY_MODE; // strict
287 | 
288 |       // Mock DNS lookup to return localhost IP
289 |       vi.mocked(dns.lookup).mockResolvedValue({ address: '127.0.0.1', family: 4 } as any);
290 | 
291 |       const result = await SSRFProtection.validateWebhookUrl('http://suspicious-domain.com/webhook');
292 |       expect(result.valid).toBe(false);
293 |       expect(result.reason).toBeDefined();
294 |     });
295 |   });
296 | 
297 |   describe('IPv6 Protection', () => {
298 |     it('should block IPv6 localhost (strict mode)', async () => {
299 |       delete process.env.WEBHOOK_SECURITY_MODE; // strict
300 | 
301 |       // Mock DNS to return IPv6 localhost
302 |       vi.mocked(dns.lookup).mockResolvedValue({ address: '::1', family: 6 } as any);
303 | 
304 |       const result = await SSRFProtection.validateWebhookUrl('http://ipv6-test.com/webhook');
305 |       expect(result.valid).toBe(false);
306 |       // Updated: IPv6 localhost is now caught by the localhost check, not IPv6 check
307 |       expect(result.reason).toContain('Localhost');
308 |     });
309 | 
310 |     it('should block IPv6 link-local (strict mode)', async () => {
311 |       delete process.env.WEBHOOK_SECURITY_MODE; // strict
312 | 
313 |       // Mock DNS to return IPv6 link-local
314 |       vi.mocked(dns.lookup).mockResolvedValue({ address: 'fe80::1', family: 6 } as any);
315 | 
316 |       const result = await SSRFProtection.validateWebhookUrl('http://ipv6-local.com/webhook');
317 |       expect(result.valid).toBe(false);
318 |       expect(result.reason).toContain('IPv6 private');
319 |     });
320 | 
321 |     it('should block IPv6 unique local (strict mode)', async () => {
322 |       delete process.env.WEBHOOK_SECURITY_MODE; // strict
323 | 
324 |       // Mock DNS to return IPv6 unique local
325 |       vi.mocked(dns.lookup).mockResolvedValue({ address: 'fc00::1', family: 6 } as any);
326 | 
327 |       const result = await SSRFProtection.validateWebhookUrl('http://ipv6-internal.com/webhook');
328 |       expect(result.valid).toBe(false);
329 |       expect(result.reason).toContain('IPv6 private');
330 |     });
331 | 
332 |     it('should block IPv6 unique local fd00::/8 (strict mode)', async () => {
333 |       delete process.env.WEBHOOK_SECURITY_MODE; // strict
334 | 
335 |       // Mock DNS to return IPv6 unique local fd00::/8
336 |       vi.mocked(dns.lookup).mockResolvedValue({ address: 'fd00::1', family: 6 } as any);
337 | 
338 |       const result = await SSRFProtection.validateWebhookUrl('http://ipv6-fd00.com/webhook');
339 |       expect(result.valid).toBe(false);
340 |       expect(result.reason).toContain('IPv6 private');
341 |     });
342 | 
343 |     it('should block IPv6 unspecified address (strict mode)', async () => {
344 |       delete process.env.WEBHOOK_SECURITY_MODE; // strict
345 | 
346 |       // Mock DNS to return IPv6 unspecified address
347 |       vi.mocked(dns.lookup).mockResolvedValue({ address: '::', family: 6 } as any);
348 | 
349 |       const result = await SSRFProtection.validateWebhookUrl('http://ipv6-unspecified.com/webhook');
350 |       expect(result.valid).toBe(false);
351 |       expect(result.reason).toContain('IPv6 private');
352 |     });
353 | 
354 |     it('should block IPv4-mapped IPv6 addresses (strict mode)', async () => {
355 |       delete process.env.WEBHOOK_SECURITY_MODE; // strict
356 | 
357 |       // Mock DNS to return IPv4-mapped IPv6 address
358 |       vi.mocked(dns.lookup).mockResolvedValue({ address: '::ffff:127.0.0.1', family: 6 } as any);
359 | 
360 |       const result = await SSRFProtection.validateWebhookUrl('http://ipv4-mapped.com/webhook');
361 |       expect(result.valid).toBe(false);
362 |       expect(result.reason).toContain('IPv6 private');
363 |     });
364 |   });
365 | 
366 |   describe('DNS Resolution Failures', () => {
367 |     it('should handle DNS resolution failure gracefully', async () => {
368 |       // Mock DNS lookup to fail
369 |       vi.mocked(dns.lookup).mockRejectedValue(new Error('ENOTFOUND'));
370 | 
371 |       const result = await SSRFProtection.validateWebhookUrl('http://non-existent-domain.invalid/webhook');
372 |       expect(result.valid).toBe(false);
373 |       expect(result.reason).toBe('DNS resolution failed');
374 |     });
375 |   });
376 | 
377 |   describe('Edge Cases', () => {
378 |     it('should handle malformed URLs', async () => {
379 |       const malformedURLs = [
380 |         'not-a-url',
381 |         'http://',
382 |         '://missing-protocol.com',
383 |       ];
384 | 
385 |       for (const url of malformedURLs) {
386 |         const result = await SSRFProtection.validateWebhookUrl(url);
387 |         expect(result.valid).toBe(false);
388 |         expect(result.reason).toBe('Invalid URL format');
389 |       }
390 |     });
391 | 
392 |     it('should handle URL with special characters safely', async () => {
393 |       const result = await SSRFProtection.validateWebhookUrl('https://example.com/webhook?param=value&other=123');
394 |       expect(result.valid).toBe(true);
395 |     });
396 |   });
397 | });
398 | 
```

--------------------------------------------------------------------------------
/tests/unit/services/example-generator.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, it, expect, vi, beforeEach } from 'vitest';
  2 | import { ExampleGenerator } from '@/services/example-generator';
  3 | import type { NodeExamples } from '@/services/example-generator';
  4 | 
  5 | // Mock the database
  6 | vi.mock('better-sqlite3');
  7 | 
  8 | describe('ExampleGenerator', () => {
  9 |   beforeEach(() => {
 10 |     vi.clearAllMocks();
 11 |   });
 12 | 
 13 |   describe('getExamples', () => {
 14 |     it('should return curated examples for HTTP Request node', () => {
 15 |       const examples = ExampleGenerator.getExamples('nodes-base.httpRequest');
 16 | 
 17 |       expect(examples).toHaveProperty('minimal');
 18 |       expect(examples).toHaveProperty('common');
 19 |       expect(examples).toHaveProperty('advanced');
 20 | 
 21 |       // Check minimal example
 22 |       expect(examples.minimal).toEqual({
 23 |         url: 'https://api.example.com/data'
 24 |       });
 25 | 
 26 |       // Check common example has required fields
 27 |       expect(examples.common).toMatchObject({
 28 |         method: 'POST',
 29 |         url: 'https://api.example.com/users',
 30 |         sendBody: true,
 31 |         contentType: 'json'
 32 |       });
 33 | 
 34 |       // Check advanced example has error handling
 35 |       expect(examples.advanced).toMatchObject({
 36 |         method: 'POST',
 37 |         onError: 'continueRegularOutput',
 38 |         retryOnFail: true,
 39 |         maxTries: 3
 40 |       });
 41 |     });
 42 | 
 43 |     it('should return curated examples for Webhook node', () => {
 44 |       const examples = ExampleGenerator.getExamples('nodes-base.webhook');
 45 | 
 46 |       expect(examples.minimal).toMatchObject({
 47 |         path: 'my-webhook',
 48 |         httpMethod: 'POST'
 49 |       });
 50 | 
 51 |       expect(examples.common).toMatchObject({
 52 |         responseMode: 'lastNode',
 53 |         responseData: 'allEntries',
 54 |         responseCode: 200
 55 |       });
 56 |     });
 57 | 
 58 |     it('should return curated examples for Code node', () => {
 59 |       const examples = ExampleGenerator.getExamples('nodes-base.code');
 60 | 
 61 |       expect(examples.minimal).toMatchObject({
 62 |         language: 'javaScript',
 63 |         jsCode: 'return [{json: {result: "success"}}];'
 64 |       });
 65 | 
 66 |       expect(examples.common?.jsCode).toContain('items.map');
 67 |       expect(examples.common?.jsCode).toContain('DateTime.now()');
 68 | 
 69 |       expect(examples.advanced?.jsCode).toContain('try');
 70 |       expect(examples.advanced?.jsCode).toContain('catch');
 71 |     });
 72 | 
 73 |     it('should generate basic examples for unconfigured nodes', () => {
 74 |       const essentials = {
 75 |         required: [
 76 |           { name: 'url', type: 'string' },
 77 |           { name: 'method', type: 'options', options: [{ value: 'GET' }, { value: 'POST' }] }
 78 |         ],
 79 |         common: [
 80 |           { name: 'timeout', type: 'number' }
 81 |         ]
 82 |       };
 83 | 
 84 |       const examples = ExampleGenerator.getExamples('nodes-base.unknownNode', essentials);
 85 | 
 86 |       expect(examples.minimal).toEqual({
 87 |         url: 'https://api.example.com',
 88 |         method: 'GET'
 89 |       });
 90 | 
 91 |       expect(examples.common).toBeUndefined();
 92 |       expect(examples.advanced).toBeUndefined();
 93 |     });
 94 | 
 95 |     it('should use common property if no required fields exist', () => {
 96 |       const essentials = {
 97 |         required: [],
 98 |         common: [
 99 |           { name: 'name', type: 'string' }
100 |         ]
101 |       };
102 | 
103 |       const examples = ExampleGenerator.getExamples('nodes-base.unknownNode', essentials);
104 | 
105 |       expect(examples.minimal).toEqual({
106 |         name: 'John Doe'
107 |       });
108 |     });
109 | 
110 |     it('should return empty minimal object if no essentials provided', () => {
111 |       const examples = ExampleGenerator.getExamples('nodes-base.unknownNode');
112 | 
113 |       expect(examples.minimal).toEqual({});
114 |     });
115 |   });
116 | 
117 |   describe('special example nodes', () => {
118 |     it('should provide webhook processing example', () => {
119 |       const examples = ExampleGenerator.getExamples('nodes-base.code.webhookProcessing');
120 | 
121 |       expect(examples.minimal?.jsCode).toContain('const webhookData = items[0].json.body');
122 |       expect(examples.minimal?.jsCode).toContain('// ❌ WRONG');
123 |       expect(examples.minimal?.jsCode).toContain('// ✅ CORRECT');
124 |     });
125 | 
126 |     it('should provide data transformation examples', () => {
127 |       const examples = ExampleGenerator.getExamples('nodes-base.code.dataTransform');
128 | 
129 |       expect(examples.minimal?.jsCode).toContain('CSV-like data to JSON');
130 |       expect(examples.minimal?.jsCode).toContain('split');
131 |     });
132 | 
133 |     it('should provide aggregation example', () => {
134 |       const examples = ExampleGenerator.getExamples('nodes-base.code.aggregation');
135 | 
136 |       expect(examples.minimal?.jsCode).toContain('items.reduce');
137 |       expect(examples.minimal?.jsCode).toContain('totalAmount');
138 |     });
139 | 
140 |     it('should provide JMESPath filtering example', () => {
141 |       const examples = ExampleGenerator.getExamples('nodes-base.code.jmespathFiltering');
142 | 
143 |       expect(examples.minimal?.jsCode).toContain('$jmespath');
144 |       expect(examples.minimal?.jsCode).toContain('`100`'); // Backticks for numeric literals
145 |       expect(examples.minimal?.jsCode).toContain('✅ CORRECT');
146 |     });
147 | 
148 |     it('should provide Python example', () => {
149 |       const examples = ExampleGenerator.getExamples('nodes-base.code.pythonExample');
150 | 
151 |       expect(examples.minimal?.pythonCode).toContain('_input.all()');
152 |       expect(examples.minimal?.pythonCode).toContain('to_py()');
153 |       expect(examples.minimal?.pythonCode).toContain('import json');
154 |     });
155 | 
156 |     it('should provide AI tool example', () => {
157 |       const examples = ExampleGenerator.getExamples('nodes-base.code.aiTool');
158 | 
159 |       expect(examples.minimal?.mode).toBe('runOnceForEachItem');
160 |       expect(examples.minimal?.jsCode).toContain('calculate discount');
161 |       expect(examples.minimal?.jsCode).toContain('$json.quantity');
162 |     });
163 | 
164 |     it('should provide crypto usage example', () => {
165 |       const examples = ExampleGenerator.getExamples('nodes-base.code.crypto');
166 | 
167 |       expect(examples.minimal?.jsCode).toContain("require('crypto')");
168 |       expect(examples.minimal?.jsCode).toContain('randomBytes');
169 |       expect(examples.minimal?.jsCode).toContain('createHash');
170 |     });
171 | 
172 |     it('should provide static data example', () => {
173 |       const examples = ExampleGenerator.getExamples('nodes-base.code.staticData');
174 | 
175 |       expect(examples.minimal?.jsCode).toContain('$getWorkflowStaticData');
176 |       expect(examples.minimal?.jsCode).toContain('processCount');
177 |     });
178 |   });
179 | 
180 |   describe('database node examples', () => {
181 |     it('should provide PostgreSQL examples', () => {
182 |       const examples = ExampleGenerator.getExamples('nodes-base.postgres');
183 | 
184 |       expect(examples.minimal).toMatchObject({
185 |         operation: 'executeQuery',
186 |         query: 'SELECT * FROM users LIMIT 10'
187 |       });
188 | 
189 |       expect(examples.advanced?.query).toContain('ON CONFLICT');
190 |       expect(examples.advanced?.retryOnFail).toBe(true);
191 |     });
192 | 
193 |     it('should provide MongoDB examples', () => {
194 |       const examples = ExampleGenerator.getExamples('nodes-base.mongoDb');
195 | 
196 |       expect(examples.minimal).toMatchObject({
197 |         operation: 'find',
198 |         collection: 'users'
199 |       });
200 | 
201 |       expect(examples.common).toMatchObject({
202 |         operation: 'findOneAndUpdate',
203 |         options: {
204 |           upsert: true,
205 |           returnNewDocument: true
206 |         }
207 |       });
208 |     });
209 | 
210 |     it('should provide MySQL examples', () => {
211 |       const examples = ExampleGenerator.getExamples('nodes-base.mySql');
212 | 
213 |       expect(examples.minimal?.query).toContain('SELECT * FROM products');
214 |       expect(examples.common?.operation).toBe('insert');
215 |     });
216 |   });
217 | 
218 |   describe('communication node examples', () => {
219 |     it('should provide Slack examples', () => {
220 |       const examples = ExampleGenerator.getExamples('nodes-base.slack');
221 | 
222 |       expect(examples.minimal).toMatchObject({
223 |         resource: 'message',
224 |         operation: 'post',
225 |         channel: '#general',
226 |         text: 'Hello from n8n!'
227 |       });
228 | 
229 |       expect(examples.common?.attachments).toBeDefined();
230 |       expect(examples.common?.retryOnFail).toBe(true);
231 |     });
232 | 
233 |     it('should provide Email examples', () => {
234 |       const examples = ExampleGenerator.getExamples('nodes-base.emailSend');
235 | 
236 |       expect(examples.minimal).toMatchObject({
237 |         fromEmail: '[email protected]',
238 |         toEmail: '[email protected]',
239 |         subject: 'Test Email'
240 |       });
241 | 
242 |       expect(examples.common?.html).toContain('<h1>Welcome!</h1>');
243 |     });
244 |   });
245 | 
246 |   describe('error handling patterns', () => {
247 |     it('should provide modern error handling patterns', () => {
248 |       const examples = ExampleGenerator.getExamples('error-handling.modern-patterns');
249 | 
250 |       expect(examples.minimal).toMatchObject({
251 |         onError: 'continueRegularOutput'
252 |       });
253 | 
254 |       expect(examples.advanced).toMatchObject({
255 |         onError: 'stopWorkflow',
256 |         retryOnFail: true,
257 |         maxTries: 3
258 |       });
259 |     });
260 | 
261 |     it('should provide API retry patterns', () => {
262 |       const examples = ExampleGenerator.getExamples('error-handling.api-with-retry');
263 | 
264 |       expect(examples.common?.retryOnFail).toBe(true);
265 |       expect(examples.common?.maxTries).toBe(5);
266 |       expect(examples.common?.alwaysOutputData).toBe(true);
267 |     });
268 | 
269 |     it('should provide database error patterns', () => {
270 |       const examples = ExampleGenerator.getExamples('error-handling.database-patterns');
271 | 
272 |       expect(examples.common).toMatchObject({
273 |         retryOnFail: true,
274 |         maxTries: 3,
275 |         onError: 'stopWorkflow'
276 |       });
277 |     });
278 | 
279 |     it('should provide webhook error patterns', () => {
280 |       const examples = ExampleGenerator.getExamples('error-handling.webhook-patterns');
281 | 
282 |       expect(examples.minimal?.alwaysOutputData).toBe(true);
283 |       expect(examples.common?.responseCode).toBe(200);
284 |     });
285 |   });
286 | 
287 |   describe('getTaskExample', () => {
288 |     it('should return minimal example for basic task', () => {
289 |       const example = ExampleGenerator.getTaskExample('nodes-base.httpRequest', 'basic');
290 | 
291 |       expect(example).toEqual({
292 |         url: 'https://api.example.com/data'
293 |       });
294 |     });
295 | 
296 |     it('should return common example for typical task', () => {
297 |       const example = ExampleGenerator.getTaskExample('nodes-base.httpRequest', 'typical');
298 | 
299 |       expect(example).toMatchObject({
300 |         method: 'POST',
301 |         sendBody: true
302 |       });
303 |     });
304 | 
305 |     it('should return advanced example for complex task', () => {
306 |       const example = ExampleGenerator.getTaskExample('nodes-base.httpRequest', 'complex');
307 | 
308 |       expect(example).toMatchObject({
309 |         retryOnFail: true,
310 |         maxTries: 3
311 |       });
312 |     });
313 | 
314 |     it('should default to common example for unknown task', () => {
315 |       const example = ExampleGenerator.getTaskExample('nodes-base.httpRequest', 'unknown');
316 | 
317 |       expect(example).toMatchObject({
318 |         method: 'POST' // This is from common example
319 |       });
320 |     });
321 | 
322 |     it('should return undefined for unknown node type', () => {
323 |       const example = ExampleGenerator.getTaskExample('nodes-base.unknownNode', 'basic');
324 | 
325 |       expect(example).toBeUndefined();
326 |     });
327 |   });
328 | 
329 |   describe('default value generation', () => {
330 |     it('should generate appropriate defaults for different property types', () => {
331 |       const essentials = {
332 |         required: [
333 |           { name: 'url', type: 'string' },
334 |           { name: 'port', type: 'number' },
335 |           { name: 'enabled', type: 'boolean' },
336 |           { name: 'method', type: 'options', options: [{ value: 'GET' }, { value: 'POST' }] },
337 |           { name: 'data', type: 'json' }
338 |         ],
339 |         common: []
340 |       };
341 | 
342 |       const examples = ExampleGenerator.getExamples('nodes-base.unknownNode', essentials);
343 | 
344 |       expect(examples.minimal).toEqual({
345 |         url: 'https://api.example.com',
346 |         port: 80,
347 |         enabled: false,
348 |         method: 'GET',
349 |         data: '{\n  "key": "value"\n}'
350 |       });
351 |     });
352 | 
353 |     it('should use property defaults when available', () => {
354 |       const essentials = {
355 |         required: [
356 |           { name: 'timeout', type: 'number', default: 5000 },
357 |           { name: 'retries', type: 'number', default: 3 }
358 |         ],
359 |         common: []
360 |       };
361 | 
362 |       const examples = ExampleGenerator.getExamples('nodes-base.unknownNode', essentials);
363 | 
364 |       expect(examples.minimal).toEqual({
365 |         timeout: 5000,
366 |         retries: 3
367 |       });
368 |     });
369 | 
370 |     it('should generate context-aware string defaults', () => {
371 |       const essentials = {
372 |         required: [
373 |           { name: 'fromEmail', type: 'string' },
374 |           { name: 'toEmail', type: 'string' },
375 |           { name: 'webhookPath', type: 'string' },
376 |           { name: 'username', type: 'string' },
377 |           { name: 'apiKey', type: 'string' },
378 |           { name: 'query', type: 'string' },
379 |           { name: 'collection', type: 'string' }
380 |         ],
381 |         common: []
382 |       };
383 | 
384 |       const examples = ExampleGenerator.getExamples('nodes-base.unknownNode', essentials);
385 | 
386 |       expect(examples.minimal).toEqual({
387 |         fromEmail: '[email protected]',
388 |         toEmail: '[email protected]',
389 |         webhookPath: 'my-webhook',
390 |         username: 'John Doe',
391 |         apiKey: 'myKey',
392 |         query: 'SELECT * FROM table_name LIMIT 10',
393 |         collection: 'users'
394 |       });
395 |     });
396 | 
397 |     it('should use placeholder as fallback for string defaults', () => {
398 |       const essentials = {
399 |         required: [
400 |           { name: 'customField', type: 'string', placeholder: 'Enter custom value' }
401 |         ],
402 |         common: []
403 |       };
404 | 
405 |       const examples = ExampleGenerator.getExamples('nodes-base.unknownNode', essentials);
406 | 
407 |       expect(examples.minimal).toEqual({
408 |         customField: 'Enter custom value'
409 |       });
410 |     });
411 |   });
412 | 
413 |   describe('edge cases', () => {
414 |     it('should handle empty essentials object', () => {
415 |       const essentials = {
416 |         required: [],
417 |         common: []
418 |       };
419 | 
420 |       const examples = ExampleGenerator.getExamples('nodes-base.unknownNode', essentials);
421 | 
422 |       expect(examples.minimal).toEqual({});
423 |     });
424 | 
425 |     it('should handle properties with missing options', () => {
426 |       const essentials = {
427 |         required: [
428 |           { name: 'choice', type: 'options' } // No options array
429 |         ],
430 |         common: []
431 |       };
432 | 
433 |       const examples = ExampleGenerator.getExamples('nodes-base.unknownNode', essentials);
434 | 
435 |       expect(examples.minimal).toEqual({
436 |         choice: ''
437 |       });
438 |     });
439 | 
440 |     it('should handle collection and fixedCollection types', () => {
441 |       const essentials = {
442 |         required: [
443 |           { name: 'headers', type: 'collection' },
444 |           { name: 'options', type: 'fixedCollection' }
445 |         ],
446 |         common: []
447 |       };
448 | 
449 |       const examples = ExampleGenerator.getExamples('nodes-base.unknownNode', essentials);
450 | 
451 |       expect(examples.minimal).toEqual({
452 |         headers: {},
453 |         options: {}
454 |       });
455 |     });
456 |   });
457 | });
```

--------------------------------------------------------------------------------
/tests/unit/services/property-filter-edge-cases.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, it, expect, vi, beforeEach } from 'vitest';
  2 | import { PropertyFilter } from '@/services/property-filter';
  3 | import type { SimplifiedProperty } from '@/services/property-filter';
  4 | 
  5 | // Mock the database
  6 | vi.mock('better-sqlite3');
  7 | 
  8 | describe('PropertyFilter - Edge Cases', () => {
  9 |   beforeEach(() => {
 10 |     vi.clearAllMocks();
 11 |   });
 12 | 
 13 |   describe('Null and Undefined Handling', () => {
 14 |     it('should handle null properties gracefully', () => {
 15 |       const result = PropertyFilter.getEssentials(null as any, 'nodes-base.http');
 16 |       expect(result).toEqual({ required: [], common: [] });
 17 |     });
 18 | 
 19 |     it('should handle undefined properties gracefully', () => {
 20 |       const result = PropertyFilter.getEssentials(undefined as any, 'nodes-base.http');
 21 |       expect(result).toEqual({ required: [], common: [] });
 22 |     });
 23 | 
 24 |     it('should handle null nodeType gracefully', () => {
 25 |       const properties = [{ name: 'test', type: 'string' }];
 26 |       const result = PropertyFilter.getEssentials(properties, null as any);
 27 |       // Should fallback to inferEssentials
 28 |       expect(result.required).toBeDefined();
 29 |       expect(result.common).toBeDefined();
 30 |     });
 31 | 
 32 |     it('should handle properties with null values', () => {
 33 |       const properties = [
 34 |         { name: 'prop1', type: 'string', displayName: null, description: null },
 35 |         null,
 36 |         undefined,
 37 |         { name: null, type: 'string' },
 38 |         { name: 'prop2', type: null }
 39 |       ];
 40 |       
 41 |       const result = PropertyFilter.getEssentials(properties as any, 'nodes-base.test');
 42 |       expect(() => result).not.toThrow();
 43 |       expect(result.required).toBeDefined();
 44 |       expect(result.common).toBeDefined();
 45 |     });
 46 |   });
 47 | 
 48 |   describe('Boundary Value Testing', () => {
 49 |     it('should handle empty properties array', () => {
 50 |       const result = PropertyFilter.getEssentials([], 'nodes-base.http');
 51 |       expect(result).toEqual({ required: [], common: [] });
 52 |     });
 53 | 
 54 |     it('should handle very large properties array', () => {
 55 |       const largeProperties = Array(10000).fill(null).map((_, i) => ({
 56 |         name: `prop${i}`,
 57 |         type: 'string',
 58 |         displayName: `Property ${i}`,
 59 |         description: `Description for property ${i}`,
 60 |         required: i % 100 === 0
 61 |       }));
 62 |       
 63 |       const start = Date.now();
 64 |       const result = PropertyFilter.getEssentials(largeProperties, 'nodes-base.test');
 65 |       const duration = Date.now() - start;
 66 |       
 67 |       expect(result).toBeDefined();
 68 |       expect(duration).toBeLessThan(1000); // Should filter within 1 second
 69 |       // For unconfigured nodes, it uses inferEssentials which limits results
 70 |       expect(result.required.length + result.common.length).toBeLessThanOrEqual(30);
 71 |     });
 72 | 
 73 |     it('should handle properties with extremely long strings', () => {
 74 |       const properties = [
 75 |         {
 76 |           name: 'longProp',
 77 |           type: 'string',
 78 |           displayName: 'A'.repeat(1000),
 79 |           description: 'B'.repeat(10000),
 80 |           placeholder: 'C'.repeat(5000),
 81 |           required: true
 82 |         }
 83 |       ];
 84 |       
 85 |       const result = PropertyFilter.getEssentials(properties, 'nodes-base.test');
 86 |       // For unconfigured nodes, this might be included as required
 87 |       const allProps = [...result.required, ...result.common];
 88 |       const longProp = allProps.find(p => p.name === 'longProp');
 89 |       if (longProp) {
 90 |         expect(longProp.displayName).toBeDefined();
 91 |       }
 92 |     });
 93 | 
 94 |     it('should limit options array size', () => {
 95 |       const manyOptions = Array(1000).fill(null).map((_, i) => ({
 96 |         value: `option${i}`,
 97 |         name: `Option ${i}`
 98 |       }));
 99 |       
100 |       const properties = [{
101 |         name: 'selectProp',
102 |         type: 'options',
103 |         displayName: 'Select Property',
104 |         options: manyOptions,
105 |         required: true
106 |       }];
107 |       
108 |       const result = PropertyFilter.getEssentials(properties, 'nodes-base.test');
109 |       const allProps = [...result.required, ...result.common];
110 |       const selectProp = allProps.find(p => p.name === 'selectProp');
111 |       
112 |       if (selectProp && selectProp.options) {
113 |         // Should limit options to reasonable number
114 |         expect(selectProp.options.length).toBeLessThanOrEqual(20);
115 |       }
116 |     });
117 |   });
118 | 
119 |   describe('Property Type Handling', () => {
120 |     it('should handle all n8n property types', () => {
121 |       const propertyTypes = [
122 |         'string', 'number', 'boolean', 'options', 'multiOptions',
123 |         'collection', 'fixedCollection', 'json', 'notice', 'assignmentCollection',
124 |         'resourceLocator', 'resourceMapper', 'filter', 'credentials'
125 |       ];
126 |       
127 |       const properties = propertyTypes.map(type => ({
128 |         name: `${type}Prop`,
129 |         type,
130 |         displayName: `${type} Property`,
131 |         description: `A ${type} property`
132 |       }));
133 |       
134 |       const result = PropertyFilter.getEssentials(properties, 'nodes-base.test');
135 |       expect(result).toBeDefined();
136 |       
137 |       const allProps = [...result.required, ...result.common];
138 |       // Should handle various types without crashing
139 |       expect(allProps.length).toBeGreaterThan(0);
140 |     });
141 | 
142 |     it('should handle nested collection properties', () => {
143 |       const properties = [{
144 |         name: 'collection',
145 |         type: 'collection',
146 |         displayName: 'Collection',
147 |         options: [
148 |           { name: 'nested1', type: 'string', displayName: 'Nested 1' },
149 |           { name: 'nested2', type: 'number', displayName: 'Nested 2' }
150 |         ]
151 |       }];
152 |       
153 |       const result = PropertyFilter.getEssentials(properties, 'nodes-base.test');
154 |       const allProps = [...result.required, ...result.common];
155 |       
156 |       // Should include the collection
157 |       expect(allProps.some(p => p.name === 'collection')).toBe(true);
158 |     });
159 | 
160 |     it('should handle fixedCollection properties', () => {
161 |       const properties = [{
162 |         name: 'headers',
163 |         type: 'fixedCollection',
164 |         displayName: 'Headers',
165 |         typeOptions: { multipleValues: true },
166 |         options: [{
167 |           name: 'parameter',
168 |           displayName: 'Parameter',
169 |           values: [
170 |             { name: 'name', type: 'string', displayName: 'Name' },
171 |             { name: 'value', type: 'string', displayName: 'Value' }
172 |           ]
173 |         }]
174 |       }];
175 |       
176 |       const result = PropertyFilter.getEssentials(properties, 'nodes-base.test');
177 |       const allProps = [...result.required, ...result.common];
178 |       
179 |       // Should include the fixed collection
180 |       expect(allProps.some(p => p.name === 'headers')).toBe(true);
181 |     });
182 |   });
183 | 
184 |   describe('Special Cases', () => {
185 |     it('should handle circular references in properties', () => {
186 |       const properties: any = [{
187 |         name: 'circular',
188 |         type: 'string',
189 |         displayName: 'Circular'
190 |       }];
191 |       properties[0].self = properties[0];
192 |       
193 |       expect(() => {
194 |         PropertyFilter.getEssentials(properties, 'nodes-base.test');
195 |       }).not.toThrow();
196 |     });
197 | 
198 |     it('should handle properties with special characters', () => {
199 |       const properties = [
200 |         { name: 'prop-with-dash', type: 'string', displayName: 'Prop With Dash' },
201 |         { name: 'prop_with_underscore', type: 'string', displayName: 'Prop With Underscore' },
202 |         { name: 'prop.with.dot', type: 'string', displayName: 'Prop With Dot' },
203 |         { name: 'prop@special', type: 'string', displayName: 'Prop Special' }
204 |       ];
205 |       
206 |       const result = PropertyFilter.getEssentials(properties, 'nodes-base.test');
207 |       expect(result).toBeDefined();
208 |     });
209 | 
210 |     it('should handle duplicate property names', () => {
211 |       const properties = [
212 |         { name: 'duplicate', type: 'string', displayName: 'First Duplicate' },
213 |         { name: 'duplicate', type: 'number', displayName: 'Second Duplicate' },
214 |         { name: 'duplicate', type: 'boolean', displayName: 'Third Duplicate' }
215 |       ];
216 |       
217 |       const result = PropertyFilter.getEssentials(properties, 'nodes-base.test');
218 |       const allProps = [...result.required, ...result.common];
219 |       
220 |       // Should deduplicate
221 |       const duplicates = allProps.filter(p => p.name === 'duplicate');
222 |       expect(duplicates.length).toBe(1);
223 |     });
224 |   });
225 | 
226 |   describe('Node-Specific Configurations', () => {
227 |     it('should apply HTTP Request specific filtering', () => {
228 |       const properties = [
229 |         { name: 'url', type: 'string', required: true },
230 |         { name: 'method', type: 'options', options: [{ value: 'GET' }, { value: 'POST' }] },
231 |         { name: 'authentication', type: 'options' },
232 |         { name: 'sendBody', type: 'boolean' },
233 |         { name: 'contentType', type: 'options' },
234 |         { name: 'sendHeaders', type: 'fixedCollection' },
235 |         { name: 'someObscureOption', type: 'string' }
236 |       ];
237 |       
238 |       const result = PropertyFilter.getEssentials(properties, 'nodes-base.httpRequest');
239 |       
240 |       expect(result.required.some(p => p.name === 'url')).toBe(true);
241 |       expect(result.common.some(p => p.name === 'method')).toBe(true);
242 |       expect(result.common.some(p => p.name === 'authentication')).toBe(true);
243 |       
244 |       // Should not include obscure option
245 |       const allProps = [...result.required, ...result.common];
246 |       expect(allProps.some(p => p.name === 'someObscureOption')).toBe(false);
247 |     });
248 | 
249 |     it('should apply Slack specific filtering', () => {
250 |       const properties = [
251 |         { name: 'resource', type: 'options', required: true },
252 |         { name: 'operation', type: 'options', required: true },
253 |         { name: 'channel', type: 'string' },
254 |         { name: 'text', type: 'string' },
255 |         { name: 'attachments', type: 'collection' },
256 |         { name: 'ts', type: 'string' },
257 |         { name: 'advancedOption1', type: 'string' },
258 |         { name: 'advancedOption2', type: 'boolean' }
259 |       ];
260 |       
261 |       const result = PropertyFilter.getEssentials(properties, 'nodes-base.slack');
262 |       
263 |       // In the actual config, resource and operation are in common, not required
264 |       expect(result.common.some(p => p.name === 'resource')).toBe(true);
265 |       expect(result.common.some(p => p.name === 'operation')).toBe(true);
266 |       expect(result.common.some(p => p.name === 'channel')).toBe(true);
267 |       expect(result.common.some(p => p.name === 'text')).toBe(true);
268 |     });
269 |   });
270 | 
271 |   describe('Fallback Behavior', () => {
272 |     it('should infer essentials for unconfigured nodes', () => {
273 |       const properties = [
274 |         { name: 'requiredProp', type: 'string', required: true },
275 |         { name: 'commonProp', type: 'string', displayName: 'Common Property' },
276 |         { name: 'advancedProp', type: 'json', displayName: 'Advanced Property' },
277 |         { name: 'debugProp', type: 'boolean', displayName: 'Debug Mode' },
278 |         { name: 'internalProp', type: 'hidden' }
279 |       ];
280 |       
281 |       const result = PropertyFilter.getEssentials(properties, 'nodes-base.unknownNode');
282 |       
283 |       // Should include required properties
284 |       expect(result.required.some(p => p.name === 'requiredProp')).toBe(true);
285 |       
286 |       // Should include some common properties
287 |       expect(result.common.length).toBeGreaterThan(0);
288 |       
289 |       // Should not include internal/hidden properties
290 |       const allProps = [...result.required, ...result.common];
291 |       expect(allProps.some(p => p.name === 'internalProp')).toBe(false);
292 |     });
293 | 
294 |     it('should handle nodes with only advanced properties', () => {
295 |       const properties = [
296 |         { name: 'advanced1', type: 'json', displayName: 'Advanced Option 1' },
297 |         { name: 'advanced2', type: 'collection', displayName: 'Advanced Collection' },
298 |         { name: 'advanced3', type: 'assignmentCollection', displayName: 'Advanced Assignment' }
299 |       ];
300 |       
301 |       const result = PropertyFilter.getEssentials(properties, 'nodes-base.advancedNode');
302 |       
303 |       // Should still return some properties
304 |       const allProps = [...result.required, ...result.common];
305 |       expect(allProps.length).toBeGreaterThan(0);
306 |     });
307 |   });
308 | 
309 |   describe('Property Simplification', () => {
310 |     it('should simplify complex property structures', () => {
311 |       const properties = [{
312 |         name: 'complexProp',
313 |         type: 'options',
314 |         displayName: 'Complex Property',
315 |         description: 'A'.repeat(500), // Long description
316 |         default: 'option1',
317 |         placeholder: 'Select an option',
318 |         hint: 'This is a hint',
319 |         displayOptions: { show: { mode: ['advanced'] } },
320 |         options: Array(50).fill(null).map((_, i) => ({
321 |           value: `option${i}`,
322 |           name: `Option ${i}`,
323 |           description: `Description for option ${i}`
324 |         }))
325 |       }];
326 |       
327 |       const result = PropertyFilter.getEssentials(properties, 'nodes-base.test');
328 |       const allProps = [...result.required, ...result.common];
329 |       const simplified = allProps.find(p => p.name === 'complexProp');
330 |       
331 |       if (simplified) {
332 |         // Should include essential fields
333 |         expect(simplified.name).toBe('complexProp');
334 |         expect(simplified.displayName).toBe('Complex Property');
335 |         expect(simplified.type).toBe('options');
336 |         
337 |         // Should limit options
338 |         if (simplified.options) {
339 |           expect(simplified.options.length).toBeLessThanOrEqual(20);
340 |         }
341 |       }
342 |     });
343 | 
344 |     it('should handle properties without display names', () => {
345 |       const properties = [
346 |         { name: 'prop_without_display', type: 'string', description: 'Property description' },
347 |         { name: 'anotherProp', displayName: '', type: 'number' }
348 |       ];
349 |       
350 |       const result = PropertyFilter.getEssentials(properties, 'nodes-base.test');
351 |       const allProps = [...result.required, ...result.common];
352 |       
353 |       allProps.forEach(prop => {
354 |         // Should have a displayName (fallback to name if needed)
355 |         expect(prop.displayName).toBeTruthy();
356 |         expect(prop.displayName.length).toBeGreaterThan(0);
357 |       });
358 |     });
359 |   });
360 | 
361 |   describe('Performance', () => {
362 |     it('should handle property filtering efficiently', () => {
363 |       const nodeTypes = [
364 |         'nodes-base.httpRequest',
365 |         'nodes-base.webhook',
366 |         'nodes-base.slack',
367 |         'nodes-base.googleSheets',
368 |         'nodes-base.postgres'
369 |       ];
370 |       
371 |       const properties = Array(100).fill(null).map((_, i) => ({
372 |         name: `prop${i}`,
373 |         type: i % 2 === 0 ? 'string' : 'options',
374 |         displayName: `Property ${i}`,
375 |         required: i < 5
376 |       }));
377 |       
378 |       const start = Date.now();
379 |       nodeTypes.forEach(nodeType => {
380 |         PropertyFilter.getEssentials(properties, nodeType);
381 |       });
382 |       const duration = Date.now() - start;
383 |       
384 |       // Should process multiple nodes quickly
385 |       expect(duration).toBeLessThan(50);
386 |     });
387 |   });
388 | });
```

--------------------------------------------------------------------------------
/tests/integration/n8n-api/system/diagnostic.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Integration Tests: handleDiagnostic
  3 |  *
  4 |  * Tests system diagnostic functionality.
  5 |  * Covers environment checks, API status, and verbose mode.
  6 |  */
  7 | 
  8 | import { describe, it, expect, beforeEach } from 'vitest';
  9 | import { createMcpContext } from '../utils/mcp-context';
 10 | import { InstanceContext } from '../../../../src/types/instance-context';
 11 | import { handleDiagnostic } from '../../../../src/mcp/handlers-n8n-manager';
 12 | import { DiagnosticResponse } from '../utils/response-types';
 13 | 
 14 | describe('Integration: handleDiagnostic', () => {
 15 |   let mcpContext: InstanceContext;
 16 | 
 17 |   beforeEach(() => {
 18 |     mcpContext = createMcpContext();
 19 |   });
 20 | 
 21 |   // ======================================================================
 22 |   // Basic Diagnostic
 23 |   // ======================================================================
 24 | 
 25 |   describe('Basic Diagnostic', () => {
 26 |     it('should run basic diagnostic check', async () => {
 27 |       const response = await handleDiagnostic(
 28 |         { params: { arguments: {} } },
 29 |         mcpContext
 30 |       );
 31 | 
 32 |       expect(response.success).toBe(true);
 33 |       expect(response.data).toBeDefined();
 34 | 
 35 |       const data = response.data as DiagnosticResponse;
 36 | 
 37 |       // Verify core diagnostic fields
 38 |       expect(data).toHaveProperty('timestamp');
 39 |       expect(data).toHaveProperty('environment');
 40 |       expect(data).toHaveProperty('apiConfiguration');
 41 |       expect(data).toHaveProperty('toolsAvailability');
 42 |       expect(data).toHaveProperty('versionInfo');
 43 |       expect(data).toHaveProperty('performance');
 44 | 
 45 |       // Verify timestamp format
 46 |       expect(typeof data.timestamp).toBe('string');
 47 |       const timestamp = new Date(data.timestamp);
 48 |       expect(timestamp.toString()).not.toBe('Invalid Date');
 49 | 
 50 |       // Verify version info
 51 |       expect(data.versionInfo).toBeDefined();
 52 |       if (data.versionInfo) {
 53 |         expect(data.versionInfo).toHaveProperty('current');
 54 |         expect(data.versionInfo).toHaveProperty('upToDate');
 55 |         expect(typeof data.versionInfo.upToDate).toBe('boolean');
 56 |       }
 57 | 
 58 |       // Verify performance metrics
 59 |       expect(data.performance).toBeDefined();
 60 |       if (data.performance) {
 61 |         expect(data.performance).toHaveProperty('diagnosticResponseTimeMs');
 62 |         expect(typeof data.performance.diagnosticResponseTimeMs).toBe('number');
 63 |       }
 64 |     });
 65 | 
 66 |     it('should include environment variables', async () => {
 67 |       const response = await handleDiagnostic(
 68 |         { params: { arguments: {} } },
 69 |         mcpContext
 70 |       );
 71 | 
 72 |       const data = response.data as DiagnosticResponse;
 73 | 
 74 |       expect(data.environment).toBeDefined();
 75 |       expect(data.environment).toHaveProperty('N8N_API_URL');
 76 |       expect(data.environment).toHaveProperty('N8N_API_KEY');
 77 |       expect(data.environment).toHaveProperty('NODE_ENV');
 78 |       expect(data.environment).toHaveProperty('MCP_MODE');
 79 |       expect(data.environment).toHaveProperty('isDocker');
 80 |       expect(data.environment).toHaveProperty('cloudPlatform');
 81 |       expect(data.environment).toHaveProperty('nodeVersion');
 82 |       expect(data.environment).toHaveProperty('platform');
 83 | 
 84 |       // API key should be masked
 85 |       if (data.environment.N8N_API_KEY) {
 86 |         expect(data.environment.N8N_API_KEY).toBe('***configured***');
 87 |       }
 88 | 
 89 |       // Environment detection types
 90 |       expect(typeof data.environment.isDocker).toBe('boolean');
 91 |       expect(typeof data.environment.nodeVersion).toBe('string');
 92 |       expect(typeof data.environment.platform).toBe('string');
 93 |     });
 94 | 
 95 |     it('should check API configuration and connectivity', async () => {
 96 |       const response = await handleDiagnostic(
 97 |         { params: { arguments: {} } },
 98 |         mcpContext
 99 |       );
100 | 
101 |       const data = response.data as DiagnosticResponse;
102 | 
103 |       expect(data.apiConfiguration).toBeDefined();
104 |       expect(data.apiConfiguration).toHaveProperty('configured');
105 |       expect(data.apiConfiguration).toHaveProperty('status');
106 | 
107 |       // In test environment, API should be configured
108 |       expect(data.apiConfiguration.configured).toBe(true);
109 | 
110 |       // Verify API status
111 |       const status = data.apiConfiguration.status;
112 |       expect(status).toHaveProperty('configured');
113 |       expect(status).toHaveProperty('connected');
114 | 
115 |       // Should successfully connect to n8n API
116 |       expect(status.connected).toBe(true);
117 | 
118 |       // If connected, should have version info
119 |       if (status.connected) {
120 |         expect(status).toHaveProperty('version');
121 |       }
122 | 
123 |       // Config details should be present when configured
124 |       if (data.apiConfiguration.configured) {
125 |         expect(data.apiConfiguration).toHaveProperty('config');
126 |         expect(data.apiConfiguration.config).toHaveProperty('baseUrl');
127 |         expect(data.apiConfiguration.config).toHaveProperty('timeout');
128 |         expect(data.apiConfiguration.config).toHaveProperty('maxRetries');
129 |       }
130 |     });
131 | 
132 |     it('should report tools availability', async () => {
133 |       const response = await handleDiagnostic(
134 |         { params: { arguments: {} } },
135 |         mcpContext
136 |       );
137 | 
138 |       const data = response.data as DiagnosticResponse;
139 | 
140 |       expect(data.toolsAvailability).toBeDefined();
141 |       expect(data.toolsAvailability).toHaveProperty('documentationTools');
142 |       expect(data.toolsAvailability).toHaveProperty('managementTools');
143 |       expect(data.toolsAvailability).toHaveProperty('totalAvailable');
144 | 
145 |       // Documentation tools should always be available
146 |       const docTools = data.toolsAvailability.documentationTools;
147 |       expect(docTools.count).toBeGreaterThan(0);
148 |       expect(docTools.enabled).toBe(true);
149 |       expect(docTools.description).toBeDefined();
150 | 
151 |       // Management tools should be available when API configured
152 |       const mgmtTools = data.toolsAvailability.managementTools;
153 |       expect(mgmtTools).toHaveProperty('count');
154 |       expect(mgmtTools).toHaveProperty('enabled');
155 |       expect(mgmtTools).toHaveProperty('description');
156 | 
157 |       // In test environment, management tools should be enabled
158 |       expect(mgmtTools.enabled).toBe(true);
159 |       expect(mgmtTools.count).toBeGreaterThan(0);
160 | 
161 |       // Total should be sum of both
162 |       expect(data.toolsAvailability.totalAvailable).toBe(
163 |         docTools.count + mgmtTools.count
164 |       );
165 |     });
166 | 
167 |     it('should include troubleshooting information', async () => {
168 |       const response = await handleDiagnostic(
169 |         { params: { arguments: {} } },
170 |         mcpContext
171 |       );
172 | 
173 |       const data = response.data as DiagnosticResponse;
174 | 
175 |       // Should have either nextSteps (if API connected) or setupGuide (if not configured)
176 |       const hasGuidance = data.nextSteps || data.setupGuide || data.troubleshooting;
177 |       expect(hasGuidance).toBeDefined();
178 | 
179 |       if (data.nextSteps) {
180 |         expect(data.nextSteps).toHaveProperty('message');
181 |         expect(data.nextSteps).toHaveProperty('recommended');
182 |         expect(Array.isArray(data.nextSteps.recommended)).toBe(true);
183 |       }
184 | 
185 |       if (data.setupGuide) {
186 |         expect(data.setupGuide).toHaveProperty('message');
187 |         expect(data.setupGuide).toHaveProperty('whatYouCanDoNow');
188 |         expect(data.setupGuide).toHaveProperty('whatYouCannotDo');
189 |         expect(data.setupGuide).toHaveProperty('howToEnable');
190 |       }
191 | 
192 |       if (data.troubleshooting) {
193 |         expect(data.troubleshooting).toHaveProperty('issue');
194 |         expect(data.troubleshooting).toHaveProperty('steps');
195 |         expect(Array.isArray(data.troubleshooting.steps)).toBe(true);
196 |       }
197 |     });
198 |   });
199 | 
200 |   // ======================================================================
201 |   // Environment Detection
202 |   // ======================================================================
203 | 
204 |   describe('Environment Detection', () => {
205 |     it('should provide mode-specific debugging suggestions', async () => {
206 |       const response = await handleDiagnostic(
207 |         { params: { arguments: {} } },
208 |         mcpContext
209 |       );
210 | 
211 |       const data = response.data as DiagnosticResponse;
212 | 
213 |       // Mode-specific debug should always be present
214 |       expect(data).toHaveProperty('modeSpecificDebug');
215 |       expect(data.modeSpecificDebug).toBeDefined();
216 |       expect(data.modeSpecificDebug).toHaveProperty('mode');
217 |       expect(data.modeSpecificDebug).toHaveProperty('troubleshooting');
218 |       expect(data.modeSpecificDebug).toHaveProperty('commonIssues');
219 | 
220 |       // Verify troubleshooting is an array with content
221 |       expect(Array.isArray(data.modeSpecificDebug.troubleshooting)).toBe(true);
222 |       expect(data.modeSpecificDebug.troubleshooting.length).toBeGreaterThan(0);
223 | 
224 |       // Verify common issues is an array with content
225 |       expect(Array.isArray(data.modeSpecificDebug.commonIssues)).toBe(true);
226 |       expect(data.modeSpecificDebug.commonIssues.length).toBeGreaterThan(0);
227 | 
228 |       // Mode should be either 'HTTP Server' or 'Standard I/O (Claude Desktop)'
229 |       expect(['HTTP Server', 'Standard I/O (Claude Desktop)']).toContain(data.modeSpecificDebug.mode);
230 |     });
231 | 
232 |     it('should include Docker debugging if IS_DOCKER is true', async () => {
233 |       // Save original value
234 |       const originalIsDocker = process.env.IS_DOCKER;
235 | 
236 |       try {
237 |         // Set IS_DOCKER for this test
238 |         process.env.IS_DOCKER = 'true';
239 | 
240 |         const response = await handleDiagnostic(
241 |           { params: { arguments: {} } },
242 |           mcpContext
243 |         );
244 | 
245 |         const data = response.data as DiagnosticResponse;
246 | 
247 |         // Should have Docker debug section
248 |         expect(data).toHaveProperty('dockerDebug');
249 |         expect(data.dockerDebug).toBeDefined();
250 |         expect(data.dockerDebug?.containerDetected).toBe(true);
251 |         expect(data.dockerDebug?.troubleshooting).toBeDefined();
252 |         expect(Array.isArray(data.dockerDebug?.troubleshooting)).toBe(true);
253 |         expect(data.dockerDebug?.commonIssues).toBeDefined();
254 |       } finally {
255 |         // Restore original value
256 |         if (originalIsDocker) {
257 |           process.env.IS_DOCKER = originalIsDocker;
258 |         } else {
259 |           delete process.env.IS_DOCKER;
260 |         }
261 |       }
262 |     });
263 | 
264 |     it('should not include Docker debugging if IS_DOCKER is false', async () => {
265 |       // Save original value
266 |       const originalIsDocker = process.env.IS_DOCKER;
267 | 
268 |       try {
269 |         // Unset IS_DOCKER for this test
270 |         delete process.env.IS_DOCKER;
271 | 
272 |         const response = await handleDiagnostic(
273 |           { params: { arguments: {} } },
274 |           mcpContext
275 |         );
276 | 
277 |         const data = response.data as DiagnosticResponse;
278 | 
279 |         // Should not have Docker debug section
280 |         expect(data.dockerDebug).toBeUndefined();
281 |       } finally {
282 |         // Restore original value
283 |         if (originalIsDocker) {
284 |           process.env.IS_DOCKER = originalIsDocker;
285 |         }
286 |       }
287 |     });
288 |   });
289 | 
290 |   // ======================================================================
291 |   // Verbose Mode
292 |   // ======================================================================
293 | 
294 |   describe('Verbose Mode', () => {
295 |     it('should include additional debug info in verbose mode', async () => {
296 |       const response = await handleDiagnostic(
297 |         { params: { arguments: { verbose: true } } },
298 |         mcpContext
299 |       );
300 | 
301 |       expect(response.success).toBe(true);
302 |       const data = response.data as DiagnosticResponse;
303 | 
304 |       // Verbose mode should add debug section
305 |       expect(data).toHaveProperty('debug');
306 |       expect(data.debug).toBeDefined();
307 | 
308 |       // Verify debug information
309 |       expect(data.debug).toBeDefined();
310 |       expect(data.debug).toHaveProperty('processEnv');
311 |       expect(data.debug).toHaveProperty('nodeVersion');
312 |       expect(data.debug).toHaveProperty('platform');
313 |       expect(data.debug).toHaveProperty('workingDirectory');
314 | 
315 |       // Process env should list relevant environment variables
316 |       expect(Array.isArray(data.debug?.processEnv)).toBe(true);
317 | 
318 |       // Node version should be a string
319 |       expect(typeof data.debug?.nodeVersion).toBe('string');
320 |       expect(data.debug?.nodeVersion).toMatch(/^v\d+\.\d+\.\d+/);
321 | 
322 |       // Platform should be a string (linux, darwin, win32, etc.)
323 |       expect(typeof data.debug?.platform).toBe('string');
324 |       expect(data.debug && data.debug.platform.length).toBeGreaterThan(0);
325 | 
326 |       // Working directory should be a path
327 |       expect(typeof data.debug?.workingDirectory).toBe('string');
328 |       expect(data.debug && data.debug.workingDirectory.length).toBeGreaterThan(0);
329 |     });
330 | 
331 |     it('should not include debug info when verbose is false', async () => {
332 |       const response = await handleDiagnostic(
333 |         { params: { arguments: { verbose: false } } },
334 |         mcpContext
335 |       );
336 | 
337 |       expect(response.success).toBe(true);
338 |       const data = response.data as DiagnosticResponse;
339 | 
340 |       // Debug section should not be present
341 |       expect(data.debug).toBeUndefined();
342 |     });
343 | 
344 |     it('should not include debug info by default', async () => {
345 |       const response = await handleDiagnostic(
346 |         { params: { arguments: {} } },
347 |         mcpContext
348 |       );
349 | 
350 |       expect(response.success).toBe(true);
351 |       const data = response.data as DiagnosticResponse;
352 | 
353 |       // Debug section should not be present when verbose not specified
354 |       expect(data.debug).toBeUndefined();
355 |     });
356 |   });
357 | 
358 |   // ======================================================================
359 |   // Response Format Verification
360 |   // ======================================================================
361 | 
362 |   describe('Response Format', () => {
363 |     it('should return complete diagnostic response structure', async () => {
364 |       const response = await handleDiagnostic(
365 |         { params: { arguments: {} } },
366 |         mcpContext
367 |       );
368 | 
369 |       expect(response.success).toBe(true);
370 |       expect(response.data).toBeDefined();
371 | 
372 |       const data = response.data as DiagnosticResponse;
373 | 
374 |       // Verify all required fields (always present)
375 |       const requiredFields = [
376 |         'timestamp',
377 |         'environment',
378 |         'apiConfiguration',
379 |         'toolsAvailability',
380 |         'versionInfo',
381 |         'performance'
382 |       ];
383 | 
384 |       requiredFields.forEach(field => {
385 |         expect(data).toHaveProperty(field);
386 |         expect(data[field]).toBeDefined();
387 |       });
388 | 
389 |       // Context-specific fields (at least one should be present)
390 |       const hasContextualGuidance = data.nextSteps || data.setupGuide || data.troubleshooting;
391 |       expect(hasContextualGuidance).toBeDefined();
392 | 
393 |       // Verify data types
394 |       expect(typeof data.timestamp).toBe('string');
395 |       expect(typeof data.environment).toBe('object');
396 |       expect(typeof data.apiConfiguration).toBe('object');
397 |       expect(typeof data.toolsAvailability).toBe('object');
398 |       expect(typeof data.versionInfo).toBe('object');
399 |       expect(typeof data.performance).toBe('object');
400 |     });
401 |   });
402 | });
403 | 
```

--------------------------------------------------------------------------------
/tests/integration/mcp/template-examples-e2e.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, it, expect, beforeEach, afterEach } from 'vitest';
  2 | import { createDatabaseAdapter, DatabaseAdapter } from '../../../src/database/database-adapter';
  3 | import fs from 'fs';
  4 | import path from 'path';
  5 | import { sampleConfigs, compressWorkflow, sampleWorkflows } from '../../fixtures/template-configs';
  6 | 
  7 | /**
  8 |  * End-to-end integration tests for template-based examples feature
  9 |  * Tests the complete flow: database -> MCP server -> examples in response
 10 |  */
 11 | 
 12 | describe('Template Examples E2E Integration', () => {
 13 |   let db: DatabaseAdapter;
 14 | 
 15 |   beforeEach(async () => {
 16 |     // Create in-memory database
 17 |     db = await createDatabaseAdapter(':memory:');
 18 | 
 19 |     // Apply schema
 20 |     const schemaPath = path.join(__dirname, '../../../src/database/schema.sql');
 21 |     const schema = fs.readFileSync(schemaPath, 'utf-8');
 22 |     db.exec(schema);
 23 | 
 24 |     // Apply migration
 25 |     const migrationPath = path.join(__dirname, '../../../src/database/migrations/add-template-node-configs.sql');
 26 |     const migration = fs.readFileSync(migrationPath, 'utf-8');
 27 |     db.exec(migration);
 28 | 
 29 |     // Seed test data
 30 |     seedTemplateConfigs();
 31 |   });
 32 | 
 33 |   afterEach(() => {
 34 |     if ('close' in db && typeof db.close === 'function') {
 35 |       db.close();
 36 |     }
 37 |   });
 38 | 
 39 |   function seedTemplateConfigs() {
 40 |     // Insert sample templates first to satisfy foreign key constraints
 41 |     // The sampleConfigs use template_id 1-4, edge cases use 998-999
 42 |     const templateIds = [1, 2, 3, 4, 998, 999];
 43 |     for (const id of templateIds) {
 44 |       db.prepare(`
 45 |         INSERT INTO templates (
 46 |           id, workflow_id, name, description, views,
 47 |           nodes_used, created_at, updated_at
 48 |         ) VALUES (?, ?, ?, ?, ?, ?, datetime('now'), datetime('now'))
 49 |       `).run(
 50 |         id,
 51 |         id,
 52 |         `Test Template ${id}`,
 53 |         'Test Description',
 54 |         1000,
 55 |         JSON.stringify(['n8n-nodes-base.webhook', 'n8n-nodes-base.httpRequest'])
 56 |       );
 57 |     }
 58 | 
 59 |     // Insert webhook configs
 60 |     db.prepare(`
 61 |       INSERT INTO template_node_configs (
 62 |         node_type, template_id, template_name, template_views,
 63 |         node_name, parameters_json, credentials_json,
 64 |         has_credentials, has_expressions, complexity, use_cases, rank
 65 |       ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
 66 |     `).run(
 67 |       ...Object.values(sampleConfigs.simpleWebhook)
 68 |     );
 69 | 
 70 |     db.prepare(`
 71 |       INSERT INTO template_node_configs (
 72 |         node_type, template_id, template_name, template_views,
 73 |         node_name, parameters_json, credentials_json,
 74 |         has_credentials, has_expressions, complexity, use_cases, rank
 75 |       ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
 76 |     `).run(
 77 |       ...Object.values(sampleConfigs.webhookWithAuth)
 78 |     );
 79 | 
 80 |     // Insert HTTP request configs
 81 |     db.prepare(`
 82 |       INSERT INTO template_node_configs (
 83 |         node_type, template_id, template_name, template_views,
 84 |         node_name, parameters_json, credentials_json,
 85 |         has_credentials, has_expressions, complexity, use_cases, rank
 86 |       ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
 87 |     `).run(
 88 |       ...Object.values(sampleConfigs.httpRequestBasic)
 89 |     );
 90 | 
 91 |     db.prepare(`
 92 |       INSERT INTO template_node_configs (
 93 |         node_type, template_id, template_name, template_views,
 94 |         node_name, parameters_json, credentials_json,
 95 |         has_credentials, has_expressions, complexity, use_cases, rank
 96 |       ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
 97 |     `).run(
 98 |       ...Object.values(sampleConfigs.httpRequestWithExpressions)
 99 |     );
100 |   }
101 | 
102 |   describe('Querying Examples Directly', () => {
103 |     it('should fetch top 2 examples for webhook node', () => {
104 |       const examples = db.prepare(`
105 |         SELECT
106 |           parameters_json,
107 |           template_name,
108 |           template_views
109 |         FROM template_node_configs
110 |         WHERE node_type = ?
111 |         ORDER BY rank
112 |         LIMIT 2
113 |       `).all('n8n-nodes-base.webhook') as any[];
114 | 
115 |       expect(examples).toHaveLength(2);
116 |       expect(examples[0].template_name).toBe('Simple Webhook Trigger');
117 |       expect(examples[1].template_name).toBe('Authenticated Webhook');
118 |     });
119 | 
120 |     it('should fetch top 3 examples with metadata for HTTP request node', () => {
121 |       const examples = db.prepare(`
122 |         SELECT
123 |           parameters_json,
124 |           template_name,
125 |           template_views,
126 |           complexity,
127 |           use_cases,
128 |           has_credentials,
129 |           has_expressions
130 |         FROM template_node_configs
131 |         WHERE node_type = ?
132 |         ORDER BY rank
133 |         LIMIT 3
134 |       `).all('n8n-nodes-base.httpRequest') as any[];
135 | 
136 |       expect(examples).toHaveLength(2); // Only 2 inserted
137 |       expect(examples[0].template_name).toBe('Basic HTTP GET Request');
138 |       expect(examples[0].complexity).toBe('simple');
139 |       expect(examples[0].has_expressions).toBe(0);
140 | 
141 |       expect(examples[1].template_name).toBe('Dynamic HTTP Request');
142 |       expect(examples[1].complexity).toBe('complex');
143 |       expect(examples[1].has_expressions).toBe(1);
144 |     });
145 |   });
146 | 
147 |   describe('Example Data Structure Validation', () => {
148 |     it('should have valid JSON in parameters_json', () => {
149 |       const examples = db.prepare(`
150 |         SELECT parameters_json
151 |         FROM template_node_configs
152 |         WHERE node_type = ?
153 |         LIMIT 1
154 |       `).all('n8n-nodes-base.webhook') as any[];
155 | 
156 |       expect(() => {
157 |         const params = JSON.parse(examples[0].parameters_json);
158 |         expect(params).toHaveProperty('httpMethod');
159 |         expect(params).toHaveProperty('path');
160 |       }).not.toThrow();
161 |     });
162 | 
163 |     it('should have valid JSON in use_cases', () => {
164 |       const examples = db.prepare(`
165 |         SELECT use_cases
166 |         FROM template_node_configs
167 |         WHERE node_type = ?
168 |         LIMIT 1
169 |       `).all('n8n-nodes-base.webhook') as any[];
170 | 
171 |       expect(() => {
172 |         const useCases = JSON.parse(examples[0].use_cases);
173 |         expect(Array.isArray(useCases)).toBe(true);
174 |       }).not.toThrow();
175 |     });
176 | 
177 |     it('should have credentials_json when has_credentials is 1', () => {
178 |       const examples = db.prepare(`
179 |         SELECT credentials_json, has_credentials
180 |         FROM template_node_configs
181 |         WHERE has_credentials = 1
182 |         LIMIT 1
183 |       `).all() as any[];
184 | 
185 |       if (examples.length > 0) {
186 |         expect(examples[0].credentials_json).not.toBeNull();
187 |         expect(() => {
188 |           JSON.parse(examples[0].credentials_json);
189 |         }).not.toThrow();
190 |       }
191 |     });
192 |   });
193 | 
194 |   describe('Ranked View Functionality', () => {
195 |     it('should return only top 5 ranked configs per node type from view', () => {
196 |       // Insert templates first to satisfy foreign key constraints
197 |       // Note: seedTemplateConfigs already created templates 1-4, so start from 5
198 |       for (let i = 5; i <= 14; i++) {
199 |         db.prepare(`
200 |           INSERT INTO templates (
201 |             id, workflow_id, name, description, views,
202 |             nodes_used, created_at, updated_at
203 |           ) VALUES (?, ?, ?, ?, ?, ?, datetime('now'), datetime('now'))
204 |         `).run(i, i, `Template ${i}`, 'Test', 1000 - (i * 50), '[]');
205 |       }
206 | 
207 |       // Insert 10 configs for same node type
208 |       for (let i = 5; i <= 14; i++) {
209 |         db.prepare(`
210 |           INSERT INTO template_node_configs (
211 |             node_type, template_id, template_name, template_views,
212 |             node_name, parameters_json, rank
213 |           ) VALUES (?, ?, ?, ?, ?, ?, ?)
214 |         `).run(
215 |           'n8n-nodes-base.webhook',
216 |           i,
217 |           `Template ${i}`,
218 |           1000 - (i * 50),
219 |           'Webhook',
220 |           '{}',
221 |           i
222 |         );
223 |       }
224 | 
225 |       const rankedConfigs = db.prepare(`
226 |         SELECT * FROM ranked_node_configs
227 |         WHERE node_type = ?
228 |       `).all('n8n-nodes-base.webhook') as any[];
229 | 
230 |       expect(rankedConfigs.length).toBeLessThanOrEqual(5);
231 |     });
232 |   });
233 | 
234 |   describe('Performance with Real-World Data Volume', () => {
235 |     beforeEach(() => {
236 |       // Insert templates first to satisfy foreign key constraints
237 |       for (let i = 1; i <= 100; i++) {
238 |         db.prepare(`
239 |           INSERT INTO templates (
240 |             id, workflow_id, name, description, views,
241 |             nodes_used, created_at, updated_at
242 |           ) VALUES (?, ?, ?, ?, ?, ?, datetime('now'), datetime('now'))
243 |         `).run(i + 100, i + 100, `Template ${i}`, 'Test', Math.floor(Math.random() * 10000), '[]');
244 |       }
245 | 
246 |       // Insert 100 configs across 10 different node types
247 |       const nodeTypes = [
248 |         'n8n-nodes-base.slack',
249 |         'n8n-nodes-base.googleSheets',
250 |         'n8n-nodes-base.code',
251 |         'n8n-nodes-base.if',
252 |         'n8n-nodes-base.switch',
253 |         'n8n-nodes-base.set',
254 |         'n8n-nodes-base.merge',
255 |         'n8n-nodes-base.splitInBatches',
256 |         'n8n-nodes-base.postgres',
257 |         'n8n-nodes-base.gmail'
258 |       ];
259 | 
260 |       for (let i = 1; i <= 100; i++) {
261 |         const nodeType = nodeTypes[i % nodeTypes.length];
262 |         db.prepare(`
263 |           INSERT INTO template_node_configs (
264 |             node_type, template_id, template_name, template_views,
265 |             node_name, parameters_json, rank
266 |           ) VALUES (?, ?, ?, ?, ?, ?, ?)
267 |         `).run(
268 |           nodeType,
269 |           i + 100, // Offset template_id
270 |           `Template ${i}`,
271 |           Math.floor(Math.random() * 10000),
272 |           'Node',
273 |           '{}',
274 |           (i % 10) + 1
275 |         );
276 |       }
277 |     });
278 | 
279 |     it('should query specific node type examples quickly', () => {
280 |       const start = Date.now();
281 |       const examples = db.prepare(`
282 |         SELECT * FROM template_node_configs
283 |         WHERE node_type = ?
284 |         ORDER BY rank
285 |         LIMIT 3
286 |       `).all('n8n-nodes-base.slack') as any[];
287 |       const duration = Date.now() - start;
288 | 
289 |       expect(examples.length).toBeGreaterThan(0);
290 |       expect(duration).toBeLessThan(5); // Should be very fast with index
291 |     });
292 | 
293 |     it('should filter by complexity efficiently', () => {
294 |       // Set complexity on configs
295 |       db.exec(`UPDATE template_node_configs SET complexity = 'simple' WHERE id % 3 = 0`);
296 |       db.exec(`UPDATE template_node_configs SET complexity = 'medium' WHERE id % 3 = 1`);
297 | 
298 |       const start = Date.now();
299 |       const examples = db.prepare(`
300 |         SELECT * FROM template_node_configs
301 |         WHERE node_type = ? AND complexity = ?
302 |         ORDER BY rank
303 |         LIMIT 3
304 |       `).all('n8n-nodes-base.code', 'simple') as any[];
305 |       const duration = Date.now() - start;
306 | 
307 |       expect(duration).toBeLessThan(5);
308 |     });
309 |   });
310 | 
311 |   describe('Edge Cases', () => {
312 |     it('should handle node types with no configs', () => {
313 |       const examples = db.prepare(`
314 |         SELECT * FROM template_node_configs
315 |         WHERE node_type = ?
316 |         LIMIT 2
317 |       `).all('n8n-nodes-base.nonexistent') as any[];
318 | 
319 |       expect(examples).toHaveLength(0);
320 |     });
321 | 
322 |     it('should handle very long parameters_json', () => {
323 |       const longParams = JSON.stringify({
324 |         options: {
325 |           queryParameters: Array.from({ length: 100 }, (_, i) => ({
326 |             name: `param${i}`,
327 |             value: `value${i}`.repeat(10)
328 |           }))
329 |         }
330 |       });
331 | 
332 |       db.prepare(`
333 |         INSERT INTO template_node_configs (
334 |           node_type, template_id, template_name, template_views,
335 |           node_name, parameters_json, rank
336 |         ) VALUES (?, ?, ?, ?, ?, ?, ?)
337 |       `).run(
338 |         'n8n-nodes-base.test',
339 |         999,
340 |         'Long Params Template',
341 |         100,
342 |         'Test',
343 |         longParams,
344 |         1
345 |       );
346 | 
347 |       const example = db.prepare(`
348 |         SELECT parameters_json FROM template_node_configs WHERE template_id = ?
349 |       `).get(999) as any;
350 | 
351 |       expect(() => {
352 |         const parsed = JSON.parse(example.parameters_json);
353 |         expect(parsed.options.queryParameters).toHaveLength(100);
354 |       }).not.toThrow();
355 |     });
356 | 
357 |     it('should handle special characters in parameters', () => {
358 |       const specialParams = JSON.stringify({
359 |         message: "Test with 'quotes' and \"double quotes\"",
360 |         unicode: "特殊文字 🎉 émojis",
361 |         symbols: "!@#$%^&*()_+-={}[]|\\:;<>?,./"
362 |       });
363 | 
364 |       db.prepare(`
365 |         INSERT INTO template_node_configs (
366 |           node_type, template_id, template_name, template_views,
367 |           node_name, parameters_json, rank
368 |         ) VALUES (?, ?, ?, ?, ?, ?, ?)
369 |       `).run(
370 |         'n8n-nodes-base.test',
371 |         998,
372 |         'Special Chars Template',
373 |         100,
374 |         'Test',
375 |         specialParams,
376 |         1
377 |       );
378 | 
379 |       const example = db.prepare(`
380 |         SELECT parameters_json FROM template_node_configs WHERE template_id = ?
381 |       `).get(998) as any;
382 | 
383 |       expect(() => {
384 |         const parsed = JSON.parse(example.parameters_json);
385 |         expect(parsed.message).toContain("'quotes'");
386 |         expect(parsed.unicode).toContain("🎉");
387 |       }).not.toThrow();
388 |     });
389 |   });
390 | 
391 |   describe('Data Integrity', () => {
392 |     it('should maintain referential integrity with templates table', () => {
393 |       // Try to insert config with non-existent template_id (with FK enabled)
394 |       db.exec('PRAGMA foreign_keys = ON');
395 | 
396 |       expect(() => {
397 |         db.prepare(`
398 |           INSERT INTO template_node_configs (
399 |             node_type, template_id, template_name, template_views,
400 |             node_name, parameters_json, rank
401 |           ) VALUES (?, ?, ?, ?, ?, ?, ?)
402 |         `).run(
403 |           'n8n-nodes-base.test',
404 |           999999, // Non-existent template_id
405 |           'Test',
406 |           100,
407 |           'Node',
408 |           '{}',
409 |           1
410 |         );
411 |       }).toThrow(); // Should fail due to FK constraint
412 |     });
413 | 
414 |     it('should cascade delete configs when template is deleted', () => {
415 |       db.exec('PRAGMA foreign_keys = ON');
416 | 
417 |       // Insert a new template (use id 1000 to avoid conflicts with seedTemplateConfigs)
418 |       db.prepare(`
419 |         INSERT INTO templates (
420 |           id, workflow_id, name, description, views,
421 |           nodes_used, created_at, updated_at
422 |         ) VALUES (?, ?, ?, ?, ?, ?, datetime('now'), datetime('now'))
423 |       `).run(1000, 1000, 'Test Template 1000', 'Desc', 100, '[]');
424 | 
425 |       db.prepare(`
426 |         INSERT INTO template_node_configs (
427 |           node_type, template_id, template_name, template_views,
428 |           node_name, parameters_json, rank
429 |         ) VALUES (?, ?, ?, ?, ?, ?, ?)
430 |       `).run(
431 |         'n8n-nodes-base.test',
432 |         1000,
433 |         'Test',
434 |         100,
435 |         'Node',
436 |         '{}',
437 |         1
438 |       );
439 | 
440 |       // Verify config exists
441 |       let config = db.prepare('SELECT * FROM template_node_configs WHERE template_id = ?').get(1000);
442 |       expect(config).toBeDefined();
443 | 
444 |       // Delete template
445 |       db.prepare('DELETE FROM templates WHERE id = ?').run(1000);
446 | 
447 |       // Verify config is deleted (CASCADE)
448 |       config = db.prepare('SELECT * FROM template_node_configs WHERE template_id = ?').get(1000);
449 |       expect(config).toBeUndefined();
450 |     });
451 |   });
452 | });
453 | 
```

--------------------------------------------------------------------------------
/src/templates/batch-processor.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import * as fs from 'fs';
  2 | import * as path from 'path';
  3 | import OpenAI from 'openai';
  4 | import { logger } from '../utils/logger';
  5 | import { MetadataGenerator, MetadataRequest, MetadataResult } from './metadata-generator';
  6 | 
  7 | export interface BatchProcessorOptions {
  8 |   apiKey: string;
  9 |   model?: string;
 10 |   batchSize?: number;
 11 |   outputDir?: string;
 12 | }
 13 | 
 14 | export interface BatchJob {
 15 |   id: string;
 16 |   status: 'validating' | 'in_progress' | 'finalizing' | 'completed' | 'failed' | 'expired' | 'cancelled';
 17 |   created_at: number;
 18 |   completed_at?: number;
 19 |   input_file_id: string;
 20 |   output_file_id?: string;
 21 |   error?: any;
 22 | }
 23 | 
 24 | export class BatchProcessor {
 25 |   private client: OpenAI;
 26 |   private generator: MetadataGenerator;
 27 |   private batchSize: number;
 28 |   private outputDir: string;
 29 |   
 30 |   constructor(options: BatchProcessorOptions) {
 31 |     this.client = new OpenAI({ apiKey: options.apiKey });
 32 |     this.generator = new MetadataGenerator(options.apiKey, options.model);
 33 |     this.batchSize = options.batchSize || 100;
 34 |     this.outputDir = options.outputDir || './temp';
 35 |     
 36 |     // Ensure output directory exists
 37 |     if (!fs.existsSync(this.outputDir)) {
 38 |       fs.mkdirSync(this.outputDir, { recursive: true });
 39 |     }
 40 |   }
 41 |   
 42 |   /**
 43 |    * Process templates in batches (parallel submission)
 44 |    */
 45 |   async processTemplates(
 46 |     templates: MetadataRequest[],
 47 |     progressCallback?: (message: string, current: number, total: number) => void
 48 |   ): Promise<Map<number, MetadataResult>> {
 49 |     const results = new Map<number, MetadataResult>();
 50 |     const batches = this.createBatches(templates);
 51 |     
 52 |     logger.info(`Processing ${templates.length} templates in ${batches.length} batches`);
 53 |     
 54 |     // Submit all batches in parallel
 55 |     console.log(`\n📤 Submitting ${batches.length} batch${batches.length > 1 ? 'es' : ''} to OpenAI...`);
 56 |     const batchJobs: Array<{ batchNum: number; jobPromise: Promise<any>; templates: MetadataRequest[] }> = [];
 57 |     
 58 |     for (let i = 0; i < batches.length; i++) {
 59 |       const batch = batches[i];
 60 |       const batchNum = i + 1;
 61 |       
 62 |       try {
 63 |         progressCallback?.(`Submitting batch ${batchNum}/${batches.length}`, i * this.batchSize, templates.length);
 64 |         
 65 |         // Submit batch (don't wait for completion)
 66 |         const jobPromise = this.submitBatch(batch, `batch_${batchNum}`);
 67 |         batchJobs.push({ batchNum, jobPromise, templates: batch });
 68 |         
 69 |         console.log(`   📨 Submitted batch ${batchNum}/${batches.length} (${batch.length} templates)`);
 70 |       } catch (error) {
 71 |         logger.error(`Error submitting batch ${batchNum}:`, error);
 72 |         console.error(`   ❌ Failed to submit batch ${batchNum}`);
 73 |       }
 74 |     }
 75 |     
 76 |     console.log(`\n⏳ All batches submitted. Waiting for completion...`);
 77 |     console.log(`   (Batches process in parallel - this is much faster than sequential processing)`);
 78 |     
 79 |     // Process all batches in parallel and collect results as they complete
 80 |     const batchPromises = batchJobs.map(async ({ batchNum, jobPromise, templates: batchTemplates }) => {
 81 |       try {
 82 |         const completedJob = await jobPromise;
 83 |         console.log(`\n📦 Retrieving results for batch ${batchNum}/${batches.length}...`);
 84 |         
 85 |         // Retrieve and parse results
 86 |         const batchResults = await this.retrieveResults(completedJob);
 87 |         
 88 |         logger.info(`Retrieved ${batchResults.length} results from batch ${batchNum}`);
 89 |         progressCallback?.(`Retrieved batch ${batchNum}/${batches.length}`, 
 90 |           Math.min(batchNum * this.batchSize, templates.length), templates.length);
 91 |         
 92 |         return { batchNum, results: batchResults };
 93 |       } catch (error) {
 94 |         logger.error(`Error processing batch ${batchNum}:`, error);
 95 |         console.error(`   ❌ Batch ${batchNum} failed:`, error);
 96 |         return { batchNum, results: [] };
 97 |       }
 98 |     });
 99 |     
100 |     // Wait for all batches to complete
101 |     const allBatchResults = await Promise.all(batchPromises);
102 |     
103 |     // Merge all results
104 |     for (const { batchNum, results: batchResults } of allBatchResults) {
105 |       for (const result of batchResults) {
106 |         results.set(result.templateId, result);
107 |       }
108 |       if (batchResults.length > 0) {
109 |         console.log(`   ✅ Merged ${batchResults.length} results from batch ${batchNum}`);
110 |       }
111 |     }
112 |     
113 |     logger.info(`Batch processing complete: ${results.size} results`);
114 |     return results;
115 |   }
116 |   
117 |   /**
118 |    * Submit a batch without waiting for completion
119 |    */
120 |   private async submitBatch(templates: MetadataRequest[], batchName: string): Promise<any> {
121 |     // Create JSONL file
122 |     const inputFile = await this.createBatchFile(templates, batchName);
123 |     
124 |     try {
125 |       // Upload file to OpenAI
126 |       const uploadedFile = await this.uploadFile(inputFile);
127 |       
128 |       // Create batch job
129 |       const batchJob = await this.createBatchJob(uploadedFile.id);
130 |       
131 |       // Start monitoring (returns promise that resolves when complete)
132 |       const monitoringPromise = this.monitorBatchJob(batchJob.id);
133 |       
134 |       // Clean up input file immediately
135 |       try {
136 |         fs.unlinkSync(inputFile);
137 |       } catch {}
138 |       
139 |       // Store file IDs for cleanup later
140 |       monitoringPromise.then(async (completedJob) => {
141 |         // Cleanup uploaded files after completion
142 |         try {
143 |           await this.client.files.del(uploadedFile.id);
144 |           if (completedJob.output_file_id) {
145 |             // Note: We'll delete output file after retrieving results
146 |           }
147 |         } catch (error) {
148 |           logger.warn(`Failed to cleanup files for batch ${batchName}`, error);
149 |         }
150 |       });
151 |       
152 |       return monitoringPromise;
153 |     } catch (error) {
154 |       // Cleanup on error
155 |       try {
156 |         fs.unlinkSync(inputFile);
157 |       } catch {}
158 |       throw error;
159 |     }
160 |   }
161 |   
162 |   /**
163 |    * Process a single batch
164 |    */
165 |   private async processBatch(templates: MetadataRequest[], batchName: string): Promise<MetadataResult[]> {
166 |     // Create JSONL file
167 |     const inputFile = await this.createBatchFile(templates, batchName);
168 |     
169 |     try {
170 |       // Upload file to OpenAI
171 |       const uploadedFile = await this.uploadFile(inputFile);
172 |       
173 |       // Create batch job
174 |       const batchJob = await this.createBatchJob(uploadedFile.id);
175 |       
176 |       // Monitor job until completion
177 |       const completedJob = await this.monitorBatchJob(batchJob.id);
178 |       
179 |       // Retrieve and parse results
180 |       const results = await this.retrieveResults(completedJob);
181 |       
182 |       // Cleanup
183 |       await this.cleanup(inputFile, uploadedFile.id, completedJob.output_file_id);
184 |       
185 |       return results;
186 |     } catch (error) {
187 |       // Cleanup on error
188 |       try {
189 |         fs.unlinkSync(inputFile);
190 |       } catch {}
191 |       throw error;
192 |     }
193 |   }
194 |   
195 |   /**
196 |    * Create batches from templates
197 |    */
198 |   private createBatches(templates: MetadataRequest[]): MetadataRequest[][] {
199 |     const batches: MetadataRequest[][] = [];
200 |     
201 |     for (let i = 0; i < templates.length; i += this.batchSize) {
202 |       batches.push(templates.slice(i, i + this.batchSize));
203 |     }
204 |     
205 |     return batches;
206 |   }
207 |   
208 |   /**
209 |    * Create JSONL batch file
210 |    */
211 |   private async createBatchFile(templates: MetadataRequest[], batchName: string): Promise<string> {
212 |     const filename = path.join(this.outputDir, `${batchName}_${Date.now()}.jsonl`);
213 |     const stream = fs.createWriteStream(filename);
214 |     
215 |     for (const template of templates) {
216 |       const request = this.generator.createBatchRequest(template);
217 |       stream.write(JSON.stringify(request) + '\n');
218 |     }
219 |     
220 |     stream.end();
221 |     
222 |     // Wait for stream to finish
223 |     await new Promise<void>((resolve, reject) => {
224 |       stream.on('finish', () => resolve());
225 |       stream.on('error', reject);
226 |     });
227 |     
228 |     logger.debug(`Created batch file: ${filename} with ${templates.length} requests`);
229 |     return filename;
230 |   }
231 |   
232 |   /**
233 |    * Upload file to OpenAI
234 |    */
235 |   private async uploadFile(filepath: string): Promise<any> {
236 |     const file = fs.createReadStream(filepath);
237 |     const uploadedFile = await this.client.files.create({
238 |       file,
239 |       purpose: 'batch'
240 |     });
241 |     
242 |     logger.debug(`Uploaded file: ${uploadedFile.id}`);
243 |     return uploadedFile;
244 |   }
245 |   
246 |   /**
247 |    * Create batch job
248 |    */
249 |   private async createBatchJob(fileId: string): Promise<any> {
250 |     const batchJob = await this.client.batches.create({
251 |       input_file_id: fileId,
252 |       endpoint: '/v1/chat/completions',
253 |       completion_window: '24h'
254 |     });
255 |     
256 |     logger.info(`Created batch job: ${batchJob.id}`);
257 |     return batchJob;
258 |   }
259 |   
260 |   /**
261 |    * Monitor batch job with fixed 1-minute polling interval
262 |    */
263 |   private async monitorBatchJob(batchId: string): Promise<any> {
264 |     const pollInterval = 60; // Check every 60 seconds (1 minute)
265 |     let attempts = 0;
266 |     const maxAttempts = 120; // 120 minutes max (2 hours)
267 |     const startTime = Date.now();
268 |     let lastStatus = '';
269 | 
270 |     while (attempts < maxAttempts) {
271 |       const batchJob = await this.client.batches.retrieve(batchId);
272 |       const elapsedMinutes = Math.floor((Date.now() - startTime) / 60000);
273 | 
274 |       // Log status on every check (not just on change)
275 |       const statusSymbol = batchJob.status === 'in_progress' ? '⚙️' :
276 |                           batchJob.status === 'finalizing' ? '📦' :
277 |                           batchJob.status === 'validating' ? '🔍' :
278 |                           batchJob.status === 'completed' ? '✅' :
279 |                           batchJob.status === 'failed' ? '❌' : '⏳';
280 | 
281 |       console.log(`   ${statusSymbol} Batch ${batchId.slice(-8)}: ${batchJob.status} (${elapsedMinutes} min, check ${attempts + 1})`);
282 | 
283 |       if (batchJob.status !== lastStatus) {
284 |         logger.info(`Batch ${batchId} status changed: ${lastStatus} -> ${batchJob.status}`);
285 |         lastStatus = batchJob.status;
286 |       }
287 | 
288 |       if (batchJob.status === 'completed') {
289 |         console.log(`   ✅ Batch ${batchId.slice(-8)} completed successfully in ${elapsedMinutes} minutes`);
290 |         logger.info(`Batch job ${batchId} completed successfully`);
291 |         return batchJob;
292 |       }
293 | 
294 |       if (['failed', 'expired', 'cancelled'].includes(batchJob.status)) {
295 |         logger.error(`Batch job ${batchId} failed with status: ${batchJob.status}`);
296 |         throw new Error(`Batch job failed with status: ${batchJob.status}`);
297 |       }
298 | 
299 |       // Wait before next check (always 1 minute)
300 |       logger.debug(`Waiting ${pollInterval} seconds before next check...`);
301 |       await this.sleep(pollInterval * 1000);
302 | 
303 |       attempts++;
304 |     }
305 | 
306 |     throw new Error(`Batch job monitoring timed out after ${maxAttempts} minutes`);
307 |   }
308 |   
309 |   /**
310 |    * Retrieve and parse results
311 |    */
312 |   private async retrieveResults(batchJob: any): Promise<MetadataResult[]> {
313 |     const results: MetadataResult[] = [];
314 | 
315 |     // Check if we have an output file (successful results)
316 |     if (batchJob.output_file_id) {
317 |       const fileResponse = await this.client.files.content(batchJob.output_file_id);
318 |       const fileContent = await fileResponse.text();
319 | 
320 |       const lines = fileContent.trim().split('\n');
321 |       for (const line of lines) {
322 |         if (!line) continue;
323 |         try {
324 |           const result = JSON.parse(line);
325 |           const parsed = this.generator.parseResult(result);
326 |           results.push(parsed);
327 |         } catch (error) {
328 |           logger.error('Error parsing result line:', error);
329 |         }
330 |       }
331 |       logger.info(`Retrieved ${results.length} successful results from batch job`);
332 |     }
333 | 
334 |     // Check if we have an error file (failed results)
335 |     if (batchJob.error_file_id) {
336 |       logger.warn(`Batch job has error file: ${batchJob.error_file_id}`);
337 | 
338 |       try {
339 |         const errorResponse = await this.client.files.content(batchJob.error_file_id);
340 |         const errorContent = await errorResponse.text();
341 | 
342 |         // Save error file locally for debugging
343 |         const errorFilePath = path.join(this.outputDir, `batch_${batchJob.id}_error.jsonl`);
344 |         fs.writeFileSync(errorFilePath, errorContent);
345 |         logger.warn(`Error file saved to: ${errorFilePath}`);
346 | 
347 |         // Parse errors and create default metadata for failed templates
348 |         const errorLines = errorContent.trim().split('\n');
349 |         logger.warn(`Found ${errorLines.length} failed requests in error file`);
350 | 
351 |         for (const line of errorLines) {
352 |           if (!line) continue;
353 |           try {
354 |             const errorResult = JSON.parse(line);
355 |             const templateId = parseInt(errorResult.custom_id?.replace('template-', '') || '0');
356 | 
357 |             if (templateId > 0) {
358 |               const errorMessage = errorResult.response?.body?.error?.message ||
359 |                                   errorResult.error?.message ||
360 |                                   'Unknown error';
361 | 
362 |               logger.debug(`Template ${templateId} failed: ${errorMessage}`);
363 | 
364 |               // Use getDefaultMetadata() from generator (it's private but accessible via bracket notation)
365 |               const defaultMeta = (this.generator as any).getDefaultMetadata();
366 |               results.push({
367 |                 templateId,
368 |                 metadata: defaultMeta,
369 |                 error: errorMessage
370 |               });
371 |             }
372 |           } catch (parseError) {
373 |             logger.error('Error parsing error line:', parseError);
374 |           }
375 |         }
376 |       } catch (error) {
377 |         logger.error('Failed to process error file:', error);
378 |       }
379 |     }
380 | 
381 |     // If we have no results at all, something is very wrong
382 |     if (results.length === 0 && !batchJob.output_file_id && !batchJob.error_file_id) {
383 |       throw new Error('No output file or error file available for batch job');
384 |     }
385 | 
386 |     logger.info(`Total results (successful + failed): ${results.length}`);
387 |     return results;
388 |   }
389 |   
390 |   /**
391 |    * Cleanup temporary files
392 |    */
393 |   private async cleanup(localFile: string, inputFileId: string, outputFileId?: string): Promise<void> {
394 |     // Delete local file
395 |     try {
396 |       fs.unlinkSync(localFile);
397 |       logger.debug(`Deleted local file: ${localFile}`);
398 |     } catch (error) {
399 |       logger.warn(`Failed to delete local file: ${localFile}`, error);
400 |     }
401 |     
402 |     // Delete uploaded files from OpenAI
403 |     try {
404 |       await this.client.files.del(inputFileId);
405 |       logger.debug(`Deleted input file from OpenAI: ${inputFileId}`);
406 |     } catch (error) {
407 |       logger.warn(`Failed to delete input file from OpenAI: ${inputFileId}`, error);
408 |     }
409 |     
410 |     if (outputFileId) {
411 |       try {
412 |         await this.client.files.del(outputFileId);
413 |         logger.debug(`Deleted output file from OpenAI: ${outputFileId}`);
414 |       } catch (error) {
415 |         logger.warn(`Failed to delete output file from OpenAI: ${outputFileId}`, error);
416 |       }
417 |     }
418 |   }
419 |   
420 |   /**
421 |    * Sleep helper
422 |    */
423 |   private sleep(ms: number): Promise<void> {
424 |     return new Promise(resolve => setTimeout(resolve, ms));
425 |   }
426 | }
```

--------------------------------------------------------------------------------
/tests/unit/mcp/tools-documentation.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, it, expect, vi, beforeEach } from 'vitest';
  2 | import {
  3 |   getToolDocumentation,
  4 |   getToolsOverview,
  5 |   searchToolDocumentation,
  6 |   getToolsByCategory,
  7 |   getAllCategories
  8 | } from '@/mcp/tools-documentation';
  9 | 
 10 | // Mock the tool-docs import
 11 | vi.mock('@/mcp/tool-docs', () => ({
 12 |   toolsDocumentation: {
 13 |     search_nodes: {
 14 |       name: 'search_nodes',
 15 |       category: 'discovery',
 16 |       essentials: {
 17 |         description: 'Search nodes by keywords',
 18 |         keyParameters: ['query', 'mode', 'limit'],
 19 |         example: 'search_nodes({query: "slack"})',
 20 |         performance: 'Instant (<10ms)',
 21 |         tips: ['Use single words for precision', 'Try FUZZY mode for typos']
 22 |       },
 23 |       full: {
 24 |         description: 'Full-text search across all n8n nodes with multiple matching modes',
 25 |         parameters: {
 26 |           query: {
 27 |             type: 'string',
 28 |             description: 'Search terms',
 29 |             required: true
 30 |           },
 31 |           mode: {
 32 |             type: 'string',
 33 |             description: 'Search mode',
 34 |             enum: ['OR', 'AND', 'FUZZY'],
 35 |             default: 'OR'
 36 |           },
 37 |           limit: {
 38 |             type: 'number',
 39 |             description: 'Max results',
 40 |             default: 20
 41 |           }
 42 |         },
 43 |         returns: 'Array of matching nodes with metadata',
 44 |         examples: [
 45 |           'search_nodes({query: "webhook"})',
 46 |           'search_nodes({query: "http request", mode: "AND"})'
 47 |         ],
 48 |         useCases: ['Finding integration nodes', 'Discovering available triggers'],
 49 |         performance: 'Instant - uses in-memory index',
 50 |         bestPractices: ['Start with single words', 'Use FUZZY for uncertain names'],
 51 |         pitfalls: ['Overly specific queries may return no results'],
 52 |         relatedTools: ['list_nodes', 'get_node_info']
 53 |       }
 54 |     },
 55 |     validate_workflow: {
 56 |       name: 'validate_workflow',
 57 |       category: 'validation',
 58 |       essentials: {
 59 |         description: 'Validate complete workflow structure',
 60 |         keyParameters: ['workflow', 'options'],
 61 |         example: 'validate_workflow(workflow)',
 62 |         performance: 'Moderate (100-500ms)',
 63 |         tips: ['Run before deployment', 'Check all validation types']
 64 |       },
 65 |       full: {
 66 |         description: 'Comprehensive workflow validation',
 67 |         parameters: {
 68 |           workflow: {
 69 |             type: 'object',
 70 |             description: 'Workflow JSON',
 71 |             required: true
 72 |           },
 73 |           options: {
 74 |             type: 'object',
 75 |             description: 'Validation options'
 76 |           }
 77 |         },
 78 |         returns: 'Validation results with errors and warnings',
 79 |         examples: ['validate_workflow(workflow)'],
 80 |         useCases: ['Pre-deployment checks', 'CI/CD validation'],
 81 |         performance: 'Depends on workflow complexity',
 82 |         bestPractices: ['Validate before saving', 'Fix errors first'],
 83 |         pitfalls: ['Large workflows may take time'],
 84 |         relatedTools: ['validate_node_operation']
 85 |       }
 86 |     },
 87 |     get_node_essentials: {
 88 |       name: 'get_node_essentials',
 89 |       category: 'configuration',
 90 |       essentials: {
 91 |         description: 'Get essential node properties only',
 92 |         keyParameters: ['nodeType'],
 93 |         example: 'get_node_essentials("nodes-base.slack")',
 94 |         performance: 'Fast (<100ms)',
 95 |         tips: ['Use this before get_node_info', 'Returns 95% smaller payload']
 96 |       },
 97 |       full: {
 98 |         description: 'Returns 10-20 most important properties',
 99 |         parameters: {
100 |           nodeType: {
101 |             type: 'string',
102 |             description: 'Full node type with prefix',
103 |             required: true
104 |           }
105 |         },
106 |         returns: 'Essential properties with examples',
107 |         examples: ['get_node_essentials("nodes-base.httpRequest")'],
108 |         useCases: ['Quick configuration', 'Property discovery'],
109 |         performance: 'Fast - pre-filtered data',
110 |         bestPractices: ['Always try essentials first'],
111 |         pitfalls: ['May not include all advanced options'],
112 |         relatedTools: ['get_node_info']
113 |       }
114 |     }
115 |   }
116 | }));
117 | 
118 | // No need to mock package.json - let the actual module read it
119 | 
120 | describe('tools-documentation', () => {
121 |   beforeEach(() => {
122 |     vi.clearAllMocks();
123 |   });
124 | 
125 |   describe('getToolDocumentation', () => {
126 |     describe('essentials mode', () => {
127 |       it('should return essential documentation for existing tool', () => {
128 |         const doc = getToolDocumentation('search_nodes', 'essentials');
129 |         
130 |         expect(doc).toContain('# search_nodes');
131 |         expect(doc).toContain('Search nodes by keywords');
132 |         expect(doc).toContain('**Example**: search_nodes({query: "slack"})');
133 |         expect(doc).toContain('**Key parameters**: query, mode, limit');
134 |         expect(doc).toContain('**Performance**: Instant (<10ms)');
135 |         expect(doc).toContain('- Use single words for precision');
136 |         expect(doc).toContain('- Try FUZZY mode for typos');
137 |         expect(doc).toContain('For full documentation, use: tools_documentation({topic: "search_nodes", depth: "full"})');
138 |       });
139 | 
140 |       it('should return error message for unknown tool', () => {
141 |         const doc = getToolDocumentation('unknown_tool', 'essentials');
142 |         expect(doc).toBe("Tool 'unknown_tool' not found. Use tools_documentation() to see available tools.");
143 |       });
144 | 
145 |       it('should use essentials as default depth', () => {
146 |         const docDefault = getToolDocumentation('search_nodes');
147 |         const docEssentials = getToolDocumentation('search_nodes', 'essentials');
148 |         expect(docDefault).toBe(docEssentials);
149 |       });
150 |     });
151 | 
152 |     describe('full mode', () => {
153 |       it('should return complete documentation for existing tool', () => {
154 |         const doc = getToolDocumentation('search_nodes', 'full');
155 |         
156 |         expect(doc).toContain('# search_nodes');
157 |         expect(doc).toContain('Full-text search across all n8n nodes');
158 |         expect(doc).toContain('## Parameters');
159 |         expect(doc).toContain('- **query** (string, required): Search terms');
160 |         expect(doc).toContain('- **mode** (string): Search mode');
161 |         expect(doc).toContain('- **limit** (number): Max results');
162 |         expect(doc).toContain('## Returns');
163 |         expect(doc).toContain('Array of matching nodes with metadata');
164 |         expect(doc).toContain('## Examples');
165 |         expect(doc).toContain('search_nodes({query: "webhook"})');
166 |         expect(doc).toContain('## Common Use Cases');
167 |         expect(doc).toContain('- Finding integration nodes');
168 |         expect(doc).toContain('## Performance');
169 |         expect(doc).toContain('Instant - uses in-memory index');
170 |         expect(doc).toContain('## Best Practices');
171 |         expect(doc).toContain('- Start with single words');
172 |         expect(doc).toContain('## Common Pitfalls');
173 |         expect(doc).toContain('- Overly specific queries');
174 |         expect(doc).toContain('## Related Tools');
175 |         expect(doc).toContain('- list_nodes');
176 |       });
177 |     });
178 | 
179 |     describe('special documentation topics', () => {
180 |       it('should return JavaScript Code node guide for javascript_code_node_guide', () => {
181 |         const doc = getToolDocumentation('javascript_code_node_guide', 'essentials');
182 |         expect(doc).toContain('# JavaScript Code Node Guide');
183 |         expect(doc).toContain('$input.all()');
184 |         expect(doc).toContain('DateTime');
185 |       });
186 | 
187 |       it('should return Python Code node guide for python_code_node_guide', () => {
188 |         const doc = getToolDocumentation('python_code_node_guide', 'essentials');
189 |         expect(doc).toContain('# Python Code Node Guide');
190 |         expect(doc).toContain('_input.all()');
191 |         expect(doc).toContain('_json');
192 |       });
193 | 
194 |       it('should return full JavaScript guide when requested', () => {
195 |         const doc = getToolDocumentation('javascript_code_node_guide', 'full');
196 |         expect(doc).toContain('# JavaScript Code Node Complete Guide');
197 |         expect(doc).toContain('## Data Access Patterns');
198 |         expect(doc).toContain('## Available Built-in Functions');
199 |         expect(doc).toContain('$helpers.httpRequest');
200 |       });
201 | 
202 |       it('should return full Python guide when requested', () => {
203 |         const doc = getToolDocumentation('python_code_node_guide', 'full');
204 |         expect(doc).toContain('# Python Code Node Complete Guide');
205 |         expect(doc).toContain('## Available Built-in Modules');
206 |         expect(doc).toContain('## Limitations & Workarounds');
207 |         expect(doc).toContain('import json');
208 |       });
209 |     });
210 |   });
211 | 
212 |   describe('getToolsOverview', () => {
213 |     describe('essentials mode', () => {
214 |       it('should return essential overview with categories', () => {
215 |         const overview = getToolsOverview('essentials');
216 |         
217 |         expect(overview).toContain('# n8n MCP Tools Reference');
218 |         expect(overview).toContain('## Important: Compatibility Notice');
219 |         // The tools-documentation module dynamically reads version from package.json
220 |         // so we need to read it the same way to match
221 |         const packageJson = require('../../../package.json');
222 |         const n8nVersion = packageJson.dependencies.n8n.replace(/[^0-9.]/g, '');
223 |         expect(overview).toContain(`n8n version ${n8nVersion}`);
224 |         expect(overview).toContain('## Code Node Configuration');
225 |         expect(overview).toContain('## Standard Workflow Pattern');
226 |         expect(overview).toContain('**Discovery Tools**');
227 |         expect(overview).toContain('**Configuration Tools**');
228 |         expect(overview).toContain('**Validation Tools**');
229 |         expect(overview).toContain('## Performance Characteristics');
230 |         expect(overview).toContain('- Instant (<10ms)');
231 |         expect(overview).toContain('tools_documentation({topic: "tool_name", depth: "full"})');
232 |       });
233 | 
234 |       it('should use essentials as default', () => {
235 |         const overviewDefault = getToolsOverview();
236 |         const overviewEssentials = getToolsOverview('essentials');
237 |         expect(overviewDefault).toBe(overviewEssentials);
238 |       });
239 |     });
240 | 
241 |     describe('full mode', () => {
242 |       it('should return complete overview with all tools', () => {
243 |         const overview = getToolsOverview('full');
244 |         
245 |         expect(overview).toContain('# n8n MCP Tools - Complete Reference');
246 |         expect(overview).toContain('## All Available Tools by Category');
247 |         expect(overview).toContain('### Discovery');
248 |         expect(overview).toContain('- **search_nodes**: Search nodes by keywords');
249 |         expect(overview).toContain('### Validation');
250 |         expect(overview).toContain('- **validate_workflow**: Validate complete workflow structure');
251 |         expect(overview).toContain('## Usage Notes');
252 |       });
253 |     });
254 |   });
255 | 
256 |   describe('searchToolDocumentation', () => {
257 |     it('should find tools matching keyword in name', () => {
258 |       const results = searchToolDocumentation('search');
259 |       expect(results).toContain('search_nodes');
260 |     });
261 | 
262 |     it('should find tools matching keyword in description', () => {
263 |       const results = searchToolDocumentation('workflow');
264 |       expect(results).toContain('validate_workflow');
265 |     });
266 | 
267 |     it('should be case insensitive', () => {
268 |       const resultsLower = searchToolDocumentation('search');
269 |       const resultsUpper = searchToolDocumentation('SEARCH');
270 |       expect(resultsLower).toEqual(resultsUpper);
271 |     });
272 | 
273 |     it('should return empty array for no matches', () => {
274 |       const results = searchToolDocumentation('nonexistentxyz123');
275 |       expect(results).toEqual([]);
276 |     });
277 | 
278 |     it('should search in both essentials and full descriptions', () => {
279 |       const results = searchToolDocumentation('validation');
280 |       expect(results.length).toBeGreaterThan(0);
281 |     });
282 |   });
283 | 
284 |   describe('getToolsByCategory', () => {
285 |     it('should return tools for discovery category', () => {
286 |       const tools = getToolsByCategory('discovery');
287 |       expect(tools).toContain('search_nodes');
288 |     });
289 | 
290 |     it('should return tools for validation category', () => {
291 |       const tools = getToolsByCategory('validation');
292 |       expect(tools).toContain('validate_workflow');
293 |     });
294 | 
295 |     it('should return tools for configuration category', () => {
296 |       const tools = getToolsByCategory('configuration');
297 |       expect(tools).toContain('get_node_essentials');
298 |     });
299 | 
300 |     it('should return empty array for unknown category', () => {
301 |       const tools = getToolsByCategory('unknown_category');
302 |       expect(tools).toEqual([]);
303 |     });
304 |   });
305 | 
306 |   describe('getAllCategories', () => {
307 |     it('should return all unique categories', () => {
308 |       const categories = getAllCategories();
309 |       expect(categories).toContain('discovery');
310 |       expect(categories).toContain('validation');
311 |       expect(categories).toContain('configuration');
312 |     });
313 | 
314 |     it('should not have duplicates', () => {
315 |       const categories = getAllCategories();
316 |       const uniqueCategories = new Set(categories);
317 |       expect(categories.length).toBe(uniqueCategories.size);
318 |     });
319 | 
320 |     it('should return non-empty array', () => {
321 |       const categories = getAllCategories();
322 |       expect(categories.length).toBeGreaterThan(0);
323 |     });
324 |   });
325 | 
326 |   describe('Error Handling', () => {
327 |     it('should handle missing tool gracefully', () => {
328 |       const doc = getToolDocumentation('missing_tool');
329 |       expect(doc).toContain("Tool 'missing_tool' not found");
330 |       expect(doc).toContain('Use tools_documentation()');
331 |     });
332 | 
333 |     it('should handle empty search query', () => {
334 |       const results = searchToolDocumentation('');
335 |       // Should match all tools since empty string is in everything
336 |       expect(results.length).toBeGreaterThan(0);
337 |     });
338 |   });
339 | 
340 |   describe('Documentation Quality', () => {
341 |     it('should format parameters correctly in full mode', () => {
342 |       const doc = getToolDocumentation('search_nodes', 'full');
343 |       
344 |       // Check parameter formatting
345 |       expect(doc).toMatch(/- \*\*query\*\* \(string, required\): Search terms/);
346 |       expect(doc).toMatch(/- \*\*mode\*\* \(string\): Search mode/);
347 |       expect(doc).toMatch(/- \*\*limit\*\* \(number\): Max results/);
348 |     });
349 | 
350 |     it('should include code blocks for examples', () => {
351 |       const doc = getToolDocumentation('search_nodes', 'full');
352 |       expect(doc).toContain('```javascript');
353 |       expect(doc).toContain('```');
354 |     });
355 | 
356 |     it('should have consistent section headers', () => {
357 |       const doc = getToolDocumentation('search_nodes', 'full');
358 |       const expectedSections = [
359 |         '## Parameters',
360 |         '## Returns',
361 |         '## Examples',
362 |         '## Common Use Cases',
363 |         '## Performance',
364 |         '## Best Practices',
365 |         '## Common Pitfalls',
366 |         '## Related Tools'
367 |       ];
368 |       
369 |       expectedSections.forEach(section => {
370 |         expect(doc).toContain(section);
371 |       });
372 |     });
373 |   });
374 | });
```

--------------------------------------------------------------------------------
/src/mcp/tools-n8n-manager.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { ToolDefinition } from '../types';
  2 | 
  3 | /**
  4 |  * n8n Management Tools
  5 |  * 
  6 |  * These tools enable AI agents to manage n8n workflows through the n8n API.
  7 |  * They require N8N_API_URL and N8N_API_KEY to be configured.
  8 |  */
  9 | export const n8nManagementTools: ToolDefinition[] = [
 10 |   // Workflow Management Tools
 11 |   {
 12 |     name: 'n8n_create_workflow',
 13 |     description: `Create workflow. Requires: name, nodes[], connections{}. Created inactive. Returns workflow with ID.`,
 14 |     inputSchema: {
 15 |       type: 'object',
 16 |       properties: {
 17 |         name: { 
 18 |           type: 'string', 
 19 |           description: 'Workflow name (required)' 
 20 |         },
 21 |         nodes: { 
 22 |           type: 'array', 
 23 |           description: 'Array of workflow nodes. Each node must have: id, name, type, typeVersion, position, and parameters',
 24 |           items: {
 25 |             type: 'object',
 26 |             required: ['id', 'name', 'type', 'typeVersion', 'position', 'parameters'],
 27 |             properties: {
 28 |               id: { type: 'string' },
 29 |               name: { type: 'string' },
 30 |               type: { type: 'string' },
 31 |               typeVersion: { type: 'number' },
 32 |               position: { 
 33 |                 type: 'array',
 34 |                 items: { type: 'number' },
 35 |                 minItems: 2,
 36 |                 maxItems: 2
 37 |               },
 38 |               parameters: { type: 'object' },
 39 |               credentials: { type: 'object' },
 40 |               disabled: { type: 'boolean' },
 41 |               notes: { type: 'string' },
 42 |               continueOnFail: { type: 'boolean' },
 43 |               retryOnFail: { type: 'boolean' },
 44 |               maxTries: { type: 'number' },
 45 |               waitBetweenTries: { type: 'number' }
 46 |             }
 47 |           }
 48 |         },
 49 |         connections: { 
 50 |           type: 'object', 
 51 |           description: 'Workflow connections object. Keys are source node IDs, values define output connections' 
 52 |         },
 53 |         settings: {
 54 |           type: 'object',
 55 |           description: 'Optional workflow settings (execution order, timezone, error handling)',
 56 |           properties: {
 57 |             executionOrder: { type: 'string', enum: ['v0', 'v1'] },
 58 |             timezone: { type: 'string' },
 59 |             saveDataErrorExecution: { type: 'string', enum: ['all', 'none'] },
 60 |             saveDataSuccessExecution: { type: 'string', enum: ['all', 'none'] },
 61 |             saveManualExecutions: { type: 'boolean' },
 62 |             saveExecutionProgress: { type: 'boolean' },
 63 |             executionTimeout: { type: 'number' },
 64 |             errorWorkflow: { type: 'string' }
 65 |           }
 66 |         }
 67 |       },
 68 |       required: ['name', 'nodes', 'connections']
 69 |     }
 70 |   },
 71 |   {
 72 |     name: 'n8n_get_workflow',
 73 |     description: `Get a workflow by ID. Returns the complete workflow including nodes, connections, and settings.`,
 74 |     inputSchema: {
 75 |       type: 'object',
 76 |       properties: {
 77 |         id: { 
 78 |           type: 'string', 
 79 |           description: 'Workflow ID' 
 80 |         }
 81 |       },
 82 |       required: ['id']
 83 |     }
 84 |   },
 85 |   {
 86 |     name: 'n8n_get_workflow_details',
 87 |     description: `Get workflow details with metadata, version, execution stats. More info than get_workflow.`,
 88 |     inputSchema: {
 89 |       type: 'object',
 90 |       properties: {
 91 |         id: { 
 92 |           type: 'string', 
 93 |           description: 'Workflow ID' 
 94 |         }
 95 |       },
 96 |       required: ['id']
 97 |     }
 98 |   },
 99 |   {
100 |     name: 'n8n_get_workflow_structure',
101 |     description: `Get workflow structure: nodes and connections only. No parameter details.`,
102 |     inputSchema: {
103 |       type: 'object',
104 |       properties: {
105 |         id: { 
106 |           type: 'string', 
107 |           description: 'Workflow ID' 
108 |         }
109 |       },
110 |       required: ['id']
111 |     }
112 |   },
113 |   {
114 |     name: 'n8n_get_workflow_minimal',
115 |     description: `Get minimal info: ID, name, active status, tags. Fast for listings.`,
116 |     inputSchema: {
117 |       type: 'object',
118 |       properties: {
119 |         id: { 
120 |           type: 'string', 
121 |           description: 'Workflow ID' 
122 |         }
123 |       },
124 |       required: ['id']
125 |     }
126 |   },
127 |   {
128 |     name: 'n8n_update_full_workflow',
129 |     description: `Full workflow update. Requires complete nodes[] and connections{}. For incremental use n8n_update_partial_workflow.`,
130 |     inputSchema: {
131 |       type: 'object',
132 |       properties: {
133 |         id: { 
134 |           type: 'string', 
135 |           description: 'Workflow ID to update' 
136 |         },
137 |         name: { 
138 |           type: 'string', 
139 |           description: 'New workflow name' 
140 |         },
141 |         nodes: { 
142 |           type: 'array', 
143 |           description: 'Complete array of workflow nodes (required if modifying workflow structure)',
144 |           items: {
145 |             type: 'object',
146 |             additionalProperties: true
147 |           }
148 |         },
149 |         connections: { 
150 |           type: 'object', 
151 |           description: 'Complete connections object (required if modifying workflow structure)' 
152 |         },
153 |         settings: { 
154 |           type: 'object', 
155 |           description: 'Workflow settings to update' 
156 |         }
157 |       },
158 |       required: ['id']
159 |     }
160 |   },
161 |   {
162 |     name: 'n8n_update_partial_workflow',
163 |     description: `Update workflow incrementally with diff operations. Types: addNode, removeNode, updateNode, moveNode, enable/disableNode, addConnection, removeConnection, updateSettings, updateName, add/removeTag. See tools_documentation("n8n_update_partial_workflow", "full") for details.`,
164 |     inputSchema: {
165 |       type: 'object',
166 |       additionalProperties: true,  // Allow any extra properties Claude Desktop might add
167 |       properties: {
168 |         id: { 
169 |           type: 'string', 
170 |           description: 'Workflow ID to update' 
171 |         },
172 |         operations: {
173 |           type: 'array',
174 |           description: 'Array of diff operations to apply. Each operation must have a "type" field and relevant properties for that operation type.',
175 |           items: {
176 |             type: 'object',
177 |             additionalProperties: true
178 |           }
179 |         },
180 |         validateOnly: {
181 |           type: 'boolean',
182 |           description: 'If true, only validate operations without applying them'
183 |         },
184 |         continueOnError: {
185 |           type: 'boolean',
186 |           description: 'If true, apply valid operations even if some fail (best-effort mode). Returns applied and failed operation indices. Default: false (atomic)'
187 |         }
188 |       },
189 |       required: ['id', 'operations']
190 |     }
191 |   },
192 |   {
193 |     name: 'n8n_delete_workflow',
194 |     description: `Permanently delete a workflow. This action cannot be undone.`,
195 |     inputSchema: {
196 |       type: 'object',
197 |       properties: {
198 |         id: { 
199 |           type: 'string', 
200 |           description: 'Workflow ID to delete' 
201 |         }
202 |       },
203 |       required: ['id']
204 |     }
205 |   },
206 |   {
207 |     name: 'n8n_list_workflows',
208 |     description: `List workflows (minimal metadata only). Returns id/name/active/dates/tags. Check hasMore/nextCursor for pagination.`,
209 |     inputSchema: {
210 |       type: 'object',
211 |       properties: {
212 |         limit: { 
213 |           type: 'number', 
214 |           description: 'Number of workflows to return (1-100, default: 100)' 
215 |         },
216 |         cursor: { 
217 |           type: 'string', 
218 |           description: 'Pagination cursor from previous response' 
219 |         },
220 |         active: { 
221 |           type: 'boolean', 
222 |           description: 'Filter by active status' 
223 |         },
224 |         tags: { 
225 |           type: 'array', 
226 |           items: { type: 'string' },
227 |           description: 'Filter by tags (exact match)' 
228 |         },
229 |         projectId: { 
230 |           type: 'string', 
231 |           description: 'Filter by project ID (enterprise feature)' 
232 |         },
233 |         excludePinnedData: { 
234 |           type: 'boolean', 
235 |           description: 'Exclude pinned data from response (default: true)' 
236 |         }
237 |       }
238 |     }
239 |   },
240 |   {
241 |     name: 'n8n_validate_workflow',
242 |     description: `Validate workflow by ID. Checks nodes, connections, expressions. Returns errors/warnings/suggestions.`,
243 |     inputSchema: {
244 |       type: 'object',
245 |       properties: {
246 |         id: { 
247 |           type: 'string', 
248 |           description: 'Workflow ID to validate' 
249 |         },
250 |         options: {
251 |           type: 'object',
252 |           description: 'Validation options',
253 |           properties: {
254 |             validateNodes: { 
255 |               type: 'boolean', 
256 |               description: 'Validate node configurations (default: true)' 
257 |             },
258 |             validateConnections: { 
259 |               type: 'boolean', 
260 |               description: 'Validate workflow connections (default: true)' 
261 |             },
262 |             validateExpressions: { 
263 |               type: 'boolean', 
264 |               description: 'Validate n8n expressions (default: true)' 
265 |             },
266 |             profile: { 
267 |               type: 'string', 
268 |               enum: ['minimal', 'runtime', 'ai-friendly', 'strict'],
269 |               description: 'Validation profile to use (default: runtime)' 
270 |             }
271 |           }
272 |         }
273 |       },
274 |       required: ['id']
275 |     }
276 |   },
277 |   {
278 |     name: 'n8n_autofix_workflow',
279 |     description: `Automatically fix common workflow validation errors. Preview fixes or apply them. Fixes expression format, typeVersion, error output config, webhook paths.`,
280 |     inputSchema: {
281 |       type: 'object',
282 |       properties: {
283 |         id: {
284 |           type: 'string',
285 |           description: 'Workflow ID to fix'
286 |         },
287 |         applyFixes: {
288 |           type: 'boolean',
289 |           description: 'Apply fixes to workflow (default: false - preview mode)'
290 |         },
291 |         fixTypes: {
292 |           type: 'array',
293 |           description: 'Types of fixes to apply (default: all)',
294 |           items: {
295 |             type: 'string',
296 |             enum: ['expression-format', 'typeversion-correction', 'error-output-config', 'node-type-correction', 'webhook-missing-path']
297 |           }
298 |         },
299 |         confidenceThreshold: {
300 |           type: 'string',
301 |           enum: ['high', 'medium', 'low'],
302 |           description: 'Minimum confidence level for fixes (default: medium)'
303 |         },
304 |         maxFixes: {
305 |           type: 'number',
306 |           description: 'Maximum number of fixes to apply (default: 50)'
307 |         }
308 |       },
309 |       required: ['id']
310 |     }
311 |   },
312 | 
313 |   // Execution Management Tools
314 |   {
315 |     name: 'n8n_trigger_webhook_workflow',
316 |     description: `Trigger workflow via webhook. Must be ACTIVE with Webhook node. Method must match config.`,
317 |     inputSchema: {
318 |       type: 'object',
319 |       properties: {
320 |         webhookUrl: { 
321 |           type: 'string', 
322 |           description: 'Full webhook URL from n8n workflow (e.g., https://n8n.example.com/webhook/abc-def-ghi)' 
323 |         },
324 |         httpMethod: { 
325 |           type: 'string', 
326 |           enum: ['GET', 'POST', 'PUT', 'DELETE'],
327 |           description: 'HTTP method (must match webhook configuration, often GET)' 
328 |         },
329 |         data: { 
330 |           type: 'object', 
331 |           description: 'Data to send with the webhook request' 
332 |         },
333 |         headers: { 
334 |           type: 'object', 
335 |           description: 'Additional HTTP headers' 
336 |         },
337 |         waitForResponse: { 
338 |           type: 'boolean', 
339 |           description: 'Wait for workflow completion (default: true)' 
340 |         }
341 |       },
342 |       required: ['webhookUrl']
343 |     }
344 |   },
345 |   {
346 |     name: 'n8n_get_execution',
347 |     description: `Get execution details with smart filtering. RECOMMENDED: Use mode='preview' first to assess data size.
348 | Examples:
349 | - {id, mode:'preview'} - Structure & counts (fast, no data)
350 | - {id, mode:'summary'} - 2 samples per node (default)
351 | - {id, mode:'filtered', itemsLimit:5} - 5 items per node
352 | - {id, nodeNames:['HTTP Request']} - Specific node only
353 | - {id, mode:'full'} - Complete data (use with caution)`,
354 |     inputSchema: {
355 |       type: 'object',
356 |       properties: {
357 |         id: {
358 |           type: 'string',
359 |           description: 'Execution ID'
360 |         },
361 |         mode: {
362 |           type: 'string',
363 |           enum: ['preview', 'summary', 'filtered', 'full'],
364 |           description: 'Data retrieval mode: preview=structure only, summary=2 items, filtered=custom, full=all data'
365 |         },
366 |         nodeNames: {
367 |           type: 'array',
368 |           items: { type: 'string' },
369 |           description: 'Filter to specific nodes by name (for filtered mode)'
370 |         },
371 |         itemsLimit: {
372 |           type: 'number',
373 |           description: 'Items per node: 0=structure only, 2=default, -1=unlimited (for filtered mode)'
374 |         },
375 |         includeInputData: {
376 |           type: 'boolean',
377 |           description: 'Include input data in addition to output (default: false)'
378 |         },
379 |         includeData: {
380 |           type: 'boolean',
381 |           description: 'Legacy: Include execution data. Maps to mode=summary if true (deprecated, use mode instead)'
382 |         }
383 |       },
384 |       required: ['id']
385 |     }
386 |   },
387 |   {
388 |     name: 'n8n_list_executions',
389 |     description: `List workflow executions (returns up to limit). Check hasMore/nextCursor for pagination.`,
390 |     inputSchema: {
391 |       type: 'object',
392 |       properties: {
393 |         limit: { 
394 |           type: 'number', 
395 |           description: 'Number of executions to return (1-100, default: 100)' 
396 |         },
397 |         cursor: { 
398 |           type: 'string', 
399 |           description: 'Pagination cursor from previous response' 
400 |         },
401 |         workflowId: { 
402 |           type: 'string', 
403 |           description: 'Filter by workflow ID' 
404 |         },
405 |         projectId: { 
406 |           type: 'string', 
407 |           description: 'Filter by project ID (enterprise feature)' 
408 |         },
409 |         status: { 
410 |           type: 'string', 
411 |           enum: ['success', 'error', 'waiting'],
412 |           description: 'Filter by execution status' 
413 |         },
414 |         includeData: { 
415 |           type: 'boolean', 
416 |           description: 'Include execution data (default: false)' 
417 |         }
418 |       }
419 |     }
420 |   },
421 |   {
422 |     name: 'n8n_delete_execution',
423 |     description: `Delete an execution record. This only removes the execution history, not any data processed.`,
424 |     inputSchema: {
425 |       type: 'object',
426 |       properties: {
427 |         id: { 
428 |           type: 'string', 
429 |           description: 'Execution ID to delete' 
430 |         }
431 |       },
432 |       required: ['id']
433 |     }
434 |   },
435 | 
436 |   // System Tools
437 |   {
438 |     name: 'n8n_health_check',
439 |     description: `Check n8n instance health and API connectivity. Returns status and available features.`,
440 |     inputSchema: {
441 |       type: 'object',
442 |       properties: {}
443 |     }
444 |   },
445 |   {
446 |     name: 'n8n_list_available_tools',
447 |     description: `List available n8n tools and capabilities.`,
448 |     inputSchema: {
449 |       type: 'object',
450 |       properties: {}
451 |     }
452 |   },
453 |   {
454 |     name: 'n8n_diagnostic',
455 |     description: `Diagnose n8n API config. Shows tool status, API connectivity, env vars. Helps troubleshoot missing tools.`,
456 |     inputSchema: {
457 |       type: 'object',
458 |       properties: {
459 |         verbose: {
460 |           type: 'boolean',
461 |           description: 'Include detailed debug information (default: false)'
462 |         }
463 |       }
464 |     }
465 |   }
466 | ];
```

--------------------------------------------------------------------------------
/src/utils/fixed-collection-validator.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Generic utility for validating and fixing fixedCollection structures in n8n nodes
  3 |  * Prevents the "propertyValues[itemName] is not iterable" error
  4 |  */
  5 | 
  6 | // Type definitions for node configurations
  7 | export type NodeConfigValue = string | number | boolean | null | undefined | NodeConfig | NodeConfigValue[];
  8 | 
  9 | export interface NodeConfig {
 10 |   [key: string]: NodeConfigValue;
 11 | }
 12 | 
 13 | export interface FixedCollectionPattern {
 14 |   nodeType: string;
 15 |   property: string;
 16 |   subProperty?: string;
 17 |   expectedStructure: string;
 18 |   invalidPatterns: string[];
 19 | }
 20 | 
 21 | export interface FixedCollectionValidationResult {
 22 |   isValid: boolean;
 23 |   errors: Array<{
 24 |     pattern: string;
 25 |     message: string;
 26 |     fix: string;
 27 |   }>;
 28 |   autofix?: NodeConfig | NodeConfigValue[];
 29 | }
 30 | 
 31 | export class FixedCollectionValidator {
 32 |   /**
 33 |    * Type guard to check if value is a NodeConfig
 34 |    */
 35 |   private static isNodeConfig(value: NodeConfigValue): value is NodeConfig {
 36 |     return typeof value === 'object' && value !== null && !Array.isArray(value);
 37 |   }
 38 | 
 39 |   /**
 40 |    * Safely get nested property value
 41 |    */
 42 |   private static getNestedValue(obj: NodeConfig, path: string): NodeConfigValue | undefined {
 43 |     const parts = path.split('.');
 44 |     let current: NodeConfigValue = obj;
 45 | 
 46 |     for (const part of parts) {
 47 |       if (!this.isNodeConfig(current)) {
 48 |         return undefined;
 49 |       }
 50 |       current = current[part];
 51 |     }
 52 | 
 53 |     return current;
 54 |   }
 55 |   /**
 56 |    * Known problematic patterns for various n8n nodes
 57 |    */
 58 |   private static readonly KNOWN_PATTERNS: FixedCollectionPattern[] = [
 59 |     // Conditional nodes (already fixed)
 60 |     {
 61 |       nodeType: 'switch',
 62 |       property: 'rules',
 63 |       expectedStructure: 'rules.values array',
 64 |       invalidPatterns: ['rules.conditions', 'rules.conditions.values']
 65 |     },
 66 |     {
 67 |       nodeType: 'if',
 68 |       property: 'conditions',
 69 |       expectedStructure: 'conditions array/object',
 70 |       invalidPatterns: ['conditions.values']
 71 |     },
 72 |     {
 73 |       nodeType: 'filter',
 74 |       property: 'conditions',
 75 |       expectedStructure: 'conditions array/object',
 76 |       invalidPatterns: ['conditions.values']
 77 |     },
 78 |     // New nodes identified by research
 79 |     {
 80 |       nodeType: 'summarize',
 81 |       property: 'fieldsToSummarize',
 82 |       subProperty: 'values',
 83 |       expectedStructure: 'fieldsToSummarize.values array',
 84 |       invalidPatterns: ['fieldsToSummarize.values.values']
 85 |     },
 86 |     {
 87 |       nodeType: 'comparedatasets',
 88 |       property: 'mergeByFields',
 89 |       subProperty: 'values',
 90 |       expectedStructure: 'mergeByFields.values array',
 91 |       invalidPatterns: ['mergeByFields.values.values']
 92 |     },
 93 |     {
 94 |       nodeType: 'sort',
 95 |       property: 'sortFieldsUi',
 96 |       subProperty: 'sortField',
 97 |       expectedStructure: 'sortFieldsUi.sortField array',
 98 |       invalidPatterns: ['sortFieldsUi.sortField.values']
 99 |     },
100 |     {
101 |       nodeType: 'aggregate',
102 |       property: 'fieldsToAggregate',
103 |       subProperty: 'fieldToAggregate',
104 |       expectedStructure: 'fieldsToAggregate.fieldToAggregate array',
105 |       invalidPatterns: ['fieldsToAggregate.fieldToAggregate.values']
106 |     },
107 |     {
108 |       nodeType: 'set',
109 |       property: 'fields',
110 |       subProperty: 'values',
111 |       expectedStructure: 'fields.values array',
112 |       invalidPatterns: ['fields.values.values']
113 |     },
114 |     {
115 |       nodeType: 'html',
116 |       property: 'extractionValues',
117 |       subProperty: 'values',
118 |       expectedStructure: 'extractionValues.values array',
119 |       invalidPatterns: ['extractionValues.values.values']
120 |     },
121 |     {
122 |       nodeType: 'httprequest',
123 |       property: 'body',
124 |       subProperty: 'parameters',
125 |       expectedStructure: 'body.parameters array',
126 |       invalidPatterns: ['body.parameters.values']
127 |     },
128 |     {
129 |       nodeType: 'airtable',
130 |       property: 'sort',
131 |       subProperty: 'sortField',
132 |       expectedStructure: 'sort.sortField array',
133 |       invalidPatterns: ['sort.sortField.values']
134 |     }
135 |   ];
136 | 
137 |   /**
138 |    * Validate a node configuration for fixedCollection issues
139 |    * Includes protection against circular references
140 |    */
141 |   static validate(
142 |     nodeType: string,
143 |     config: NodeConfig
144 |   ): FixedCollectionValidationResult {
145 |     // Early return for non-object configs
146 |     if (typeof config !== 'object' || config === null || Array.isArray(config)) {
147 |       return { isValid: true, errors: [] };
148 |     }
149 |     
150 |     const normalizedNodeType = this.normalizeNodeType(nodeType);
151 |     const pattern = this.getPatternForNode(normalizedNodeType);
152 |     
153 |     if (!pattern) {
154 |       return { isValid: true, errors: [] };
155 |     }
156 | 
157 |     const result: FixedCollectionValidationResult = {
158 |       isValid: true,
159 |       errors: []
160 |     };
161 | 
162 |     // Check for invalid patterns
163 |     for (const invalidPattern of pattern.invalidPatterns) {
164 |       if (this.hasInvalidStructure(config, invalidPattern)) {
165 |         result.isValid = false;
166 |         result.errors.push({
167 |           pattern: invalidPattern,
168 |           message: `Invalid structure for nodes-base.${pattern.nodeType} node: found nested "${invalidPattern}" but expected "${pattern.expectedStructure}". This causes "propertyValues[itemName] is not iterable" error in n8n.`,
169 |           fix: this.generateFixMessage(pattern)
170 |         });
171 | 
172 |         // Generate autofix
173 |         if (!result.autofix) {
174 |           result.autofix = this.generateAutofix(config, pattern);
175 |         }
176 |       }
177 |     }
178 | 
179 |     return result;
180 |   }
181 | 
182 |   /**
183 |    * Apply autofix to a configuration
184 |    */
185 |   static applyAutofix(
186 |     config: NodeConfig,
187 |     pattern: FixedCollectionPattern
188 |   ): NodeConfig | NodeConfigValue[] {
189 |     const fixedConfig = this.generateAutofix(config, pattern);
190 |     // For If/Filter nodes, the autofix might return just the values array
191 |     if (pattern.nodeType === 'if' || pattern.nodeType === 'filter') {
192 |       const conditions = config.conditions;
193 |       if (conditions && typeof conditions === 'object' && !Array.isArray(conditions) && 'values' in conditions) {
194 |         const values = conditions.values;
195 |         if (values !== undefined && values !== null && 
196 |             (Array.isArray(values) || typeof values === 'object')) {
197 |           return values as NodeConfig | NodeConfigValue[];
198 |         }
199 |       }
200 |     }
201 |     return fixedConfig;
202 |   }
203 | 
204 |   /**
205 |    * Normalize node type to handle various formats
206 |    */
207 |   private static normalizeNodeType(nodeType: string): string {
208 |     return nodeType
209 |       .replace('n8n-nodes-base.', '')
210 |       .replace('nodes-base.', '')
211 |       .replace('@n8n/n8n-nodes-langchain.', '')
212 |       .toLowerCase();
213 |   }
214 | 
215 |   /**
216 |    * Get pattern configuration for a specific node type
217 |    */
218 |   private static getPatternForNode(nodeType: string): FixedCollectionPattern | undefined {
219 |     return this.KNOWN_PATTERNS.find(p => p.nodeType === nodeType);
220 |   }
221 | 
222 |   /**
223 |    * Check if configuration has an invalid structure
224 |    * Includes circular reference protection
225 |    */
226 |   private static hasInvalidStructure(
227 |     config: NodeConfig,
228 |     pattern: string
229 |   ): boolean {
230 |     const parts = pattern.split('.');
231 |     let current: NodeConfigValue = config;
232 |     const visited = new WeakSet<object>();
233 | 
234 |     for (const part of parts) {
235 |       // Check for null/undefined
236 |       if (current === null || current === undefined) {
237 |         return false;
238 |       }
239 |       
240 |       // Check if it's an object (but not an array for property access)
241 |       if (typeof current !== 'object' || Array.isArray(current)) {
242 |         return false;
243 |       }
244 |       
245 |       // Check for circular reference
246 |       if (visited.has(current)) {
247 |         return false; // Circular reference detected, invalid structure
248 |       }
249 |       visited.add(current);
250 |       
251 |       // Check if property exists (using hasOwnProperty to avoid prototype pollution)
252 |       if (!Object.prototype.hasOwnProperty.call(current, part)) {
253 |         return false;
254 |       }
255 |       
256 |       const nextValue = (current as NodeConfig)[part];
257 |       if (typeof nextValue !== 'object' || nextValue === null) {
258 |         // If we have more parts to traverse but current value is not an object, invalid structure
259 |         if (parts.indexOf(part) < parts.length - 1) {
260 |           return false;
261 |         }
262 |       }
263 |       current = nextValue as NodeConfig;
264 |     }
265 | 
266 |     return true;
267 |   }
268 | 
269 |   /**
270 |    * Generate a fix message for the specific pattern
271 |    */
272 |   private static generateFixMessage(pattern: FixedCollectionPattern): string {
273 |     switch (pattern.nodeType) {
274 |       case 'switch':
275 |         return 'Use: { "rules": { "values": [{ "conditions": {...}, "outputKey": "output1" }] } }';
276 |       case 'if':
277 |       case 'filter':
278 |         return 'Use: { "conditions": {...} } or { "conditions": [...] } directly, not nested under "values"';
279 |       case 'summarize':
280 |         return 'Use: { "fieldsToSummarize": { "values": [...] } } not nested values.values';
281 |       case 'comparedatasets':
282 |         return 'Use: { "mergeByFields": { "values": [...] } } not nested values.values';
283 |       case 'sort':
284 |         return 'Use: { "sortFieldsUi": { "sortField": [...] } } not sortField.values';
285 |       case 'aggregate':
286 |         return 'Use: { "fieldsToAggregate": { "fieldToAggregate": [...] } } not fieldToAggregate.values';
287 |       case 'set':
288 |         return 'Use: { "fields": { "values": [...] } } not nested values.values';
289 |       case 'html':
290 |         return 'Use: { "extractionValues": { "values": [...] } } not nested values.values';
291 |       case 'httprequest':
292 |         return 'Use: { "body": { "parameters": [...] } } not parameters.values';
293 |       case 'airtable':
294 |         return 'Use: { "sort": { "sortField": [...] } } not sortField.values';
295 |       default:
296 |         return `Use ${pattern.expectedStructure} structure`;
297 |     }
298 |   }
299 | 
300 |   /**
301 |    * Generate autofix for invalid structures
302 |    */
303 |   private static generateAutofix(
304 |     config: NodeConfig,
305 |     pattern: FixedCollectionPattern
306 |   ): NodeConfig | NodeConfigValue[] {
307 |     const fixedConfig = { ...config };
308 | 
309 |     switch (pattern.nodeType) {
310 |       case 'switch': {
311 |         const rules = config.rules;
312 |         if (this.isNodeConfig(rules)) {
313 |           const conditions = rules.conditions;
314 |           if (this.isNodeConfig(conditions) && 'values' in conditions) {
315 |             const values = conditions.values;
316 |             fixedConfig.rules = {
317 |               values: Array.isArray(values)
318 |                 ? values.map((condition, index) => ({
319 |                     conditions: condition,
320 |                     outputKey: `output${index + 1}`
321 |                   }))
322 |                 : [{
323 |                     conditions: values,
324 |                     outputKey: 'output1'
325 |                   }]
326 |             };
327 |           } else if (conditions) {
328 |             fixedConfig.rules = {
329 |               values: [{
330 |                 conditions: conditions,
331 |                 outputKey: 'output1'
332 |               }]
333 |             };
334 |           }
335 |         }
336 |         break;
337 |       }
338 | 
339 |       case 'if':
340 |       case 'filter': {
341 |         const conditions = config.conditions;
342 |         if (this.isNodeConfig(conditions) && 'values' in conditions) {
343 |           const values = conditions.values;
344 |           if (values !== undefined && values !== null && 
345 |               (Array.isArray(values) || typeof values === 'object')) {
346 |             return values as NodeConfig | NodeConfigValue[];
347 |           }
348 |         }
349 |         break;
350 |       }
351 | 
352 |       case 'summarize': {
353 |         const fieldsToSummarize = config.fieldsToSummarize;
354 |         if (this.isNodeConfig(fieldsToSummarize)) {
355 |           const values = fieldsToSummarize.values;
356 |           if (this.isNodeConfig(values) && 'values' in values) {
357 |             fixedConfig.fieldsToSummarize = {
358 |               values: values.values
359 |             };
360 |           }
361 |         }
362 |         break;
363 |       }
364 | 
365 |       case 'comparedatasets': {
366 |         const mergeByFields = config.mergeByFields;
367 |         if (this.isNodeConfig(mergeByFields)) {
368 |           const values = mergeByFields.values;
369 |           if (this.isNodeConfig(values) && 'values' in values) {
370 |             fixedConfig.mergeByFields = {
371 |               values: values.values
372 |             };
373 |           }
374 |         }
375 |         break;
376 |       }
377 | 
378 |       case 'sort': {
379 |         const sortFieldsUi = config.sortFieldsUi;
380 |         if (this.isNodeConfig(sortFieldsUi)) {
381 |           const sortField = sortFieldsUi.sortField;
382 |           if (this.isNodeConfig(sortField) && 'values' in sortField) {
383 |             fixedConfig.sortFieldsUi = {
384 |               sortField: sortField.values
385 |             };
386 |           }
387 |         }
388 |         break;
389 |       }
390 | 
391 |       case 'aggregate': {
392 |         const fieldsToAggregate = config.fieldsToAggregate;
393 |         if (this.isNodeConfig(fieldsToAggregate)) {
394 |           const fieldToAggregate = fieldsToAggregate.fieldToAggregate;
395 |           if (this.isNodeConfig(fieldToAggregate) && 'values' in fieldToAggregate) {
396 |             fixedConfig.fieldsToAggregate = {
397 |               fieldToAggregate: fieldToAggregate.values
398 |             };
399 |           }
400 |         }
401 |         break;
402 |       }
403 | 
404 |       case 'set': {
405 |         const fields = config.fields;
406 |         if (this.isNodeConfig(fields)) {
407 |           const values = fields.values;
408 |           if (this.isNodeConfig(values) && 'values' in values) {
409 |             fixedConfig.fields = {
410 |               values: values.values
411 |             };
412 |           }
413 |         }
414 |         break;
415 |       }
416 | 
417 |       case 'html': {
418 |         const extractionValues = config.extractionValues;
419 |         if (this.isNodeConfig(extractionValues)) {
420 |           const values = extractionValues.values;
421 |           if (this.isNodeConfig(values) && 'values' in values) {
422 |             fixedConfig.extractionValues = {
423 |               values: values.values
424 |             };
425 |           }
426 |         }
427 |         break;
428 |       }
429 | 
430 |       case 'httprequest': {
431 |         const body = config.body;
432 |         if (this.isNodeConfig(body)) {
433 |           const parameters = body.parameters;
434 |           if (this.isNodeConfig(parameters) && 'values' in parameters) {
435 |             fixedConfig.body = {
436 |               ...body,
437 |               parameters: parameters.values
438 |             };
439 |           }
440 |         }
441 |         break;
442 |       }
443 | 
444 |       case 'airtable': {
445 |         const sort = config.sort;
446 |         if (this.isNodeConfig(sort)) {
447 |           const sortField = sort.sortField;
448 |           if (this.isNodeConfig(sortField) && 'values' in sortField) {
449 |             fixedConfig.sort = {
450 |               sortField: sortField.values
451 |             };
452 |           }
453 |         }
454 |         break;
455 |       }
456 |     }
457 | 
458 |     return fixedConfig;
459 |   }
460 | 
461 |   /**
462 |    * Get all known patterns (for testing and documentation)
463 |    * Returns a deep copy to prevent external modifications
464 |    */
465 |   static getAllPatterns(): FixedCollectionPattern[] {
466 |     return this.KNOWN_PATTERNS.map(pattern => ({
467 |       ...pattern,
468 |       invalidPatterns: [...pattern.invalidPatterns]
469 |     }));
470 |   }
471 | 
472 |   /**
473 |    * Check if a node type is susceptible to fixedCollection issues
474 |    */
475 |   static isNodeSusceptible(nodeType: string): boolean {
476 |     const normalizedType = this.normalizeNodeType(nodeType);
477 |     return this.KNOWN_PATTERNS.some(p => p.nodeType === normalizedType);
478 |   }
479 | }
```
Page 22/59FirstPrevNextLast