#
tokens: 48032/50000 9/620 files (page 23/60)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 23 of 60. 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
│   ├── CI_TEST_INFRASTRUCTURE.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
│   │   ├── skills.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
│       ├── expression-utils.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
│   │   │   ├── expression-utils.test.ts
│   │   │   ├── fixed-collection-validator.test.ts
│   │   │   ├── n8n-errors.test.ts
│   │   │   ├── node-type-normalizer.test.ts
│   │   │   ├── node-type-utils.test.ts
│   │   │   ├── node-utils.test.ts
│   │   │   ├── simple-cache-memory-leak-fix.test.ts
│   │   │   ├── ssrf-protection.test.ts
│   │   │   └── template-node-resolver.test.ts
│   │   └── validation-fixes.test.ts
│   └── utils
│       ├── assertions.ts
│       ├── builders
│       │   └── workflow.builder.ts
│       ├── data-generators.ts
│       ├── database-utils.ts
│       ├── README.md
│       └── test-helpers.ts
├── thumbnail.png
├── tsconfig.build.json
├── tsconfig.json
├── types
│   ├── mcp.d.ts
│   └── test-env.d.ts
├── verify-telemetry-fix.js
├── versioned-nodes.md
├── vitest.config.benchmark.ts
├── vitest.config.integration.ts
└── vitest.config.ts
```

# Files

--------------------------------------------------------------------------------
/src/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 | }
```

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

```typescript
  1 | import { describe, it, expect, vi, beforeEach } from 'vitest';
  2 | import { NodeParser } from '@/parsers/node-parser';
  3 | import { PropertyExtractor } from '@/parsers/property-extractor';
  4 | 
  5 | // Mock PropertyExtractor
  6 | vi.mock('@/parsers/property-extractor');
  7 | 
  8 | describe('NodeParser - Output Extraction', () => {
  9 |   let parser: NodeParser;
 10 |   let mockPropertyExtractor: any;
 11 | 
 12 |   beforeEach(() => {
 13 |     vi.clearAllMocks();
 14 |     
 15 |     mockPropertyExtractor = {
 16 |       extractProperties: vi.fn().mockReturnValue([]),
 17 |       extractCredentials: vi.fn().mockReturnValue([]),
 18 |       detectAIToolCapability: vi.fn().mockReturnValue(false),
 19 |       extractOperations: vi.fn().mockReturnValue([])
 20 |     };
 21 |     
 22 |     (PropertyExtractor as any).mockImplementation(() => mockPropertyExtractor);
 23 |     
 24 |     parser = new NodeParser();
 25 |   });
 26 | 
 27 |   describe('extractOutputs method', () => {
 28 |     it('should extract outputs array from base description', () => {
 29 |       const outputs = [
 30 |         { displayName: 'Done', description: 'Final results when loop completes' },
 31 |         { displayName: 'Loop', description: 'Current batch data during iteration' }
 32 |       ];
 33 |       
 34 |       const nodeDescription = {
 35 |         name: 'splitInBatches',
 36 |         displayName: 'Split In Batches',
 37 |         outputs
 38 |       };
 39 |       
 40 |       const NodeClass = class {
 41 |         description = nodeDescription;
 42 |       };
 43 |       
 44 |       const result = parser.parse(NodeClass as any, 'n8n-nodes-base');
 45 |       
 46 |       expect(result.outputs).toEqual(outputs);
 47 |       expect(result.outputNames).toBeUndefined();
 48 |     });
 49 | 
 50 |     it('should extract outputNames array from base description', () => {
 51 |       const outputNames = ['done', 'loop'];
 52 |       
 53 |       const nodeDescription = {
 54 |         name: 'splitInBatches',
 55 |         displayName: 'Split In Batches',
 56 |         outputNames
 57 |       };
 58 |       
 59 |       const NodeClass = class {
 60 |         description = nodeDescription;
 61 |       };
 62 |       
 63 |       const result = parser.parse(NodeClass as any, 'n8n-nodes-base');
 64 |       
 65 |       expect(result.outputNames).toEqual(outputNames);
 66 |       expect(result.outputs).toBeUndefined();
 67 |     });
 68 | 
 69 |     it('should extract both outputs and outputNames when both are present', () => {
 70 |       const outputs = [
 71 |         { displayName: 'Done', description: 'Final results when loop completes' },
 72 |         { displayName: 'Loop', description: 'Current batch data during iteration' }
 73 |       ];
 74 |       const outputNames = ['done', 'loop'];
 75 |       
 76 |       const nodeDescription = {
 77 |         name: 'splitInBatches',
 78 |         displayName: 'Split In Batches',
 79 |         outputs,
 80 |         outputNames
 81 |       };
 82 |       
 83 |       const NodeClass = class {
 84 |         description = nodeDescription;
 85 |       };
 86 |       
 87 |       const result = parser.parse(NodeClass as any, 'n8n-nodes-base');
 88 |       
 89 |       expect(result.outputs).toEqual(outputs);
 90 |       expect(result.outputNames).toEqual(outputNames);
 91 |     });
 92 | 
 93 |     it('should convert single output to array format', () => {
 94 |       const singleOutput = { displayName: 'Output', description: 'Single output' };
 95 |       
 96 |       const nodeDescription = {
 97 |         name: 'singleOutputNode',
 98 |         displayName: 'Single Output Node',
 99 |         outputs: singleOutput
100 |       };
101 |       
102 |       const NodeClass = class {
103 |         description = nodeDescription;
104 |       };
105 |       
106 |       const result = parser.parse(NodeClass as any, 'n8n-nodes-base');
107 |       
108 |       expect(result.outputs).toEqual([singleOutput]);
109 |     });
110 | 
111 |     it('should convert single outputName to array format', () => {
112 |       const nodeDescription = {
113 |         name: 'singleOutputNode',
114 |         displayName: 'Single Output Node',
115 |         outputNames: 'main'
116 |       };
117 |       
118 |       const NodeClass = class {
119 |         description = nodeDescription;
120 |       };
121 |       
122 |       const result = parser.parse(NodeClass as any, 'n8n-nodes-base');
123 |       
124 |       expect(result.outputNames).toEqual(['main']);
125 |     });
126 | 
127 |     it('should extract outputs from versioned node when not in base description', () => {
128 |       const versionedOutputs = [
129 |         { displayName: 'True', description: 'Items that match condition' },
130 |         { displayName: 'False', description: 'Items that do not match condition' }
131 |       ];
132 |       
133 |       const NodeClass = class {
134 |         description = {
135 |           name: 'if',
136 |           displayName: 'IF'
137 |           // No outputs in base description
138 |         };
139 |         
140 |         nodeVersions = {
141 |           1: {
142 |             description: {
143 |               outputs: versionedOutputs
144 |             }
145 |           },
146 |           2: {
147 |             description: {
148 |               outputs: versionedOutputs,
149 |               outputNames: ['true', 'false']
150 |             }
151 |           }
152 |         };
153 |       };
154 |       
155 |       const result = parser.parse(NodeClass as any, 'n8n-nodes-base');
156 |       
157 |       // Should get outputs from latest version (2)
158 |       expect(result.outputs).toEqual(versionedOutputs);
159 |       expect(result.outputNames).toEqual(['true', 'false']);
160 |     });
161 | 
162 |     it('should handle node instantiation failure gracefully', () => {
163 |       const NodeClass = class {
164 |         // Static description that can be accessed when instantiation fails
165 |         static description = {
166 |           name: 'problematic',
167 |           displayName: 'Problematic Node'
168 |         };
169 |         
170 |         constructor() {
171 |           throw new Error('Cannot instantiate');
172 |         }
173 |       };
174 |       
175 |       const result = parser.parse(NodeClass as any, 'n8n-nodes-base');
176 |       
177 |       expect(result.outputs).toBeUndefined();
178 |       expect(result.outputNames).toBeUndefined();
179 |     });
180 | 
181 |     it('should return empty result when no outputs found anywhere', () => {
182 |       const nodeDescription = {
183 |         name: 'noOutputs',
184 |         displayName: 'No Outputs Node'
185 |         // No outputs or outputNames
186 |       };
187 |       
188 |       const NodeClass = class {
189 |         description = nodeDescription;
190 |       };
191 |       
192 |       const result = parser.parse(NodeClass as any, 'n8n-nodes-base');
193 |       
194 |       expect(result.outputs).toBeUndefined();
195 |       expect(result.outputNames).toBeUndefined();
196 |     });
197 | 
198 |     it('should handle complex versioned node structure', () => {
199 |       const NodeClass = class VersionedNodeType {
200 |         baseDescription = {
201 |           name: 'complexVersioned',
202 |           displayName: 'Complex Versioned Node',
203 |           defaultVersion: 3
204 |         };
205 |         
206 |         nodeVersions = {
207 |           1: {
208 |             description: {
209 |               outputs: [{ displayName: 'V1 Output' }]
210 |             }
211 |           },
212 |           2: {
213 |             description: {
214 |               outputs: [
215 |                 { displayName: 'V2 Output 1' },
216 |                 { displayName: 'V2 Output 2' }
217 |               ]
218 |             }
219 |           },
220 |           3: {
221 |             description: {
222 |               outputs: [
223 |                 { displayName: 'V3 True', description: 'True branch' },
224 |                 { displayName: 'V3 False', description: 'False branch' }
225 |               ],
226 |               outputNames: ['true', 'false']
227 |             }
228 |           }
229 |         };
230 |       };
231 |       
232 |       const result = parser.parse(NodeClass as any, 'n8n-nodes-base');
233 |       
234 |       // Should use latest version (3)
235 |       expect(result.outputs).toEqual([
236 |         { displayName: 'V3 True', description: 'True branch' },
237 |         { displayName: 'V3 False', description: 'False branch' }
238 |       ]);
239 |       expect(result.outputNames).toEqual(['true', 'false']);
240 |     });
241 | 
242 |     it('should prefer base description outputs over versioned when both exist', () => {
243 |       const baseOutputs = [{ displayName: 'Base Output' }];
244 |       const versionedOutputs = [{ displayName: 'Versioned Output' }];
245 |       
246 |       const NodeClass = class {
247 |         description = {
248 |           name: 'preferBase',
249 |           displayName: 'Prefer Base',
250 |           outputs: baseOutputs
251 |         };
252 |         
253 |         nodeVersions = {
254 |           1: {
255 |             description: {
256 |               outputs: versionedOutputs
257 |             }
258 |           }
259 |         };
260 |       };
261 |       
262 |       const result = parser.parse(NodeClass as any, 'n8n-nodes-base');
263 |       
264 |       expect(result.outputs).toEqual(baseOutputs);
265 |     });
266 | 
267 |     it('should handle IF node with typical output structure', () => {
268 |       const ifOutputs = [
269 |         { displayName: 'True', description: 'Items that match the condition' },
270 |         { displayName: 'False', description: 'Items that do not match the condition' }
271 |       ];
272 |       
273 |       const NodeClass = class {
274 |         description = {
275 |           name: 'if',
276 |           displayName: 'IF',
277 |           outputs: ifOutputs,
278 |           outputNames: ['true', 'false']
279 |         };
280 |       };
281 |       
282 |       const result = parser.parse(NodeClass as any, 'n8n-nodes-base');
283 |       
284 |       expect(result.outputs).toEqual(ifOutputs);
285 |       expect(result.outputNames).toEqual(['true', 'false']);
286 |     });
287 | 
288 |     it('should handle SplitInBatches node with counterintuitive output structure', () => {
289 |       const splitInBatchesOutputs = [
290 |         { displayName: 'Done', description: 'Final results when loop completes' },
291 |         { displayName: 'Loop', description: 'Current batch data during iteration' }
292 |       ];
293 |       
294 |       const NodeClass = class {
295 |         description = {
296 |           name: 'splitInBatches',
297 |           displayName: 'Split In Batches',
298 |           outputs: splitInBatchesOutputs,
299 |           outputNames: ['done', 'loop']
300 |         };
301 |       };
302 |       
303 |       const result = parser.parse(NodeClass as any, 'n8n-nodes-base');
304 |       
305 |       expect(result.outputs).toEqual(splitInBatchesOutputs);
306 |       expect(result.outputNames).toEqual(['done', 'loop']);
307 |       
308 |       // Verify the counterintuitive order: done=0, loop=1
309 |       expect(result.outputs).toBeDefined();
310 |       expect(result.outputNames).toBeDefined();
311 |       expect(result.outputs![0].displayName).toBe('Done');
312 |       expect(result.outputs![1].displayName).toBe('Loop');
313 |       expect(result.outputNames![0]).toBe('done');
314 |       expect(result.outputNames![1]).toBe('loop');
315 |     });
316 | 
317 |     it('should handle Switch node with multiple outputs', () => {
318 |       const switchOutputs = [
319 |         { displayName: 'Output 1', description: 'First branch' },
320 |         { displayName: 'Output 2', description: 'Second branch' },
321 |         { displayName: 'Output 3', description: 'Third branch' },
322 |         { displayName: 'Fallback', description: 'Default branch when no conditions match' }
323 |       ];
324 |       
325 |       const NodeClass = class {
326 |         description = {
327 |           name: 'switch',
328 |           displayName: 'Switch',
329 |           outputs: switchOutputs,
330 |           outputNames: ['0', '1', '2', 'fallback']
331 |         };
332 |       };
333 |       
334 |       const result = parser.parse(NodeClass as any, 'n8n-nodes-base');
335 |       
336 |       expect(result.outputs).toEqual(switchOutputs);
337 |       expect(result.outputNames).toEqual(['0', '1', '2', 'fallback']);
338 |     });
339 | 
340 |     it('should handle empty outputs array', () => {
341 |       const NodeClass = class {
342 |         description = {
343 |           name: 'emptyOutputs',
344 |           displayName: 'Empty Outputs',
345 |           outputs: [],
346 |           outputNames: []
347 |         };
348 |       };
349 |       
350 |       const result = parser.parse(NodeClass as any, 'n8n-nodes-base');
351 |       
352 |       expect(result.outputs).toEqual([]);
353 |       expect(result.outputNames).toEqual([]);
354 |     });
355 | 
356 |     it('should handle mismatched outputs and outputNames arrays', () => {
357 |       const outputs = [
358 |         { displayName: 'Output 1' },
359 |         { displayName: 'Output 2' }
360 |       ];
361 |       const outputNames = ['first', 'second', 'third']; // One extra
362 |       
363 |       const NodeClass = class {
364 |         description = {
365 |           name: 'mismatched',
366 |           displayName: 'Mismatched Arrays',
367 |           outputs,
368 |           outputNames
369 |         };
370 |       };
371 |       
372 |       const result = parser.parse(NodeClass as any, 'n8n-nodes-base');
373 |       
374 |       expect(result.outputs).toEqual(outputs);
375 |       expect(result.outputNames).toEqual(outputNames);
376 |     });
377 |   });
378 | 
379 |   describe('real-world node structures', () => {
380 |     it('should handle actual n8n SplitInBatches node structure', () => {
381 |       // This mimics the actual structure from n8n-nodes-base
382 |       const NodeClass = class {
383 |         description = {
384 |           name: 'splitInBatches',
385 |           displayName: 'Split In Batches',
386 |           description: 'Split data into batches and iterate over each batch',
387 |           icon: 'fa:th-large',
388 |           group: ['transform'],
389 |           version: 3,
390 |           outputs: [
391 |             {
392 |               displayName: 'Done',
393 |               name: 'done',
394 |               type: 'main',
395 |               hint: 'Receives the final data after all batches have been processed'
396 |             },
397 |             {
398 |               displayName: 'Loop',
399 |               name: 'loop', 
400 |               type: 'main',
401 |               hint: 'Receives the current batch data during each iteration'
402 |             }
403 |           ],
404 |           outputNames: ['done', 'loop']
405 |         };
406 |       };
407 |       
408 |       const result = parser.parse(NodeClass as any, 'n8n-nodes-base');
409 |       
410 |       expect(result.outputs).toHaveLength(2);
411 |       expect(result.outputs).toBeDefined();
412 |       expect(result.outputs![0].displayName).toBe('Done');
413 |       expect(result.outputs![1].displayName).toBe('Loop');
414 |       expect(result.outputNames).toEqual(['done', 'loop']);
415 |     });
416 | 
417 |     it('should handle actual n8n IF node structure', () => {
418 |       // This mimics the actual structure from n8n-nodes-base
419 |       const NodeClass = class {
420 |         description = {
421 |           name: 'if',
422 |           displayName: 'IF',
423 |           description: 'Route items to different outputs based on conditions',
424 |           icon: 'fa:map-signs',
425 |           group: ['transform'],
426 |           version: 2,
427 |           outputs: [
428 |             {
429 |               displayName: 'True',
430 |               name: 'true',
431 |               type: 'main',
432 |               hint: 'Items that match the condition'
433 |             },
434 |             {
435 |               displayName: 'False',
436 |               name: 'false',
437 |               type: 'main',
438 |               hint: 'Items that do not match the condition'
439 |             }
440 |           ],
441 |           outputNames: ['true', 'false']
442 |         };
443 |       };
444 |       
445 |       const result = parser.parse(NodeClass as any, 'n8n-nodes-base');
446 |       
447 |       expect(result.outputs).toHaveLength(2);
448 |       expect(result.outputs).toBeDefined();
449 |       expect(result.outputs![0].displayName).toBe('True');
450 |       expect(result.outputs![1].displayName).toBe('False');
451 |       expect(result.outputNames).toEqual(['true', 'false']);
452 |     });
453 | 
454 |     it('should handle single-output nodes like HTTP Request', () => {
455 |       const NodeClass = class {
456 |         description = {
457 |           name: 'httpRequest',
458 |           displayName: 'HTTP Request',
459 |           description: 'Make HTTP requests',
460 |           icon: 'fa:at',
461 |           group: ['input'],
462 |           version: 4
463 |           // No outputs specified - single main output implied
464 |         };
465 |       };
466 |       
467 |       const result = parser.parse(NodeClass as any, 'n8n-nodes-base');
468 |       
469 |       expect(result.outputs).toBeUndefined();
470 |       expect(result.outputNames).toBeUndefined();
471 |     });
472 |   });
473 | });
```

--------------------------------------------------------------------------------
/src/data/canonical-ai-tool-examples.json:
--------------------------------------------------------------------------------

```json
  1 | {
  2 |   "description": "Canonical configuration examples for critical AI tools based on FINAL_AI_VALIDATION_SPEC.md",
  3 |   "version": "1.0.0",
  4 |   "examples": [
  5 |     {
  6 |       "node_type": "@n8n/n8n-nodes-langchain.toolHttpRequest",
  7 |       "display_name": "HTTP Request Tool",
  8 |       "examples": [
  9 |         {
 10 |           "name": "Weather API Tool",
 11 |           "use_case": "Fetch current weather data for AI Agent",
 12 |           "complexity": "simple",
 13 |           "parameters": {
 14 |             "method": "GET",
 15 |             "url": "https://api.weatherapi.com/v1/current.json?key={{$credentials.weatherApiKey}}&q={city}",
 16 |             "toolDescription": "Get current weather conditions for a city. Provide the city name (e.g., 'London', 'New York') and receive temperature, humidity, wind speed, and conditions.",
 17 |             "placeholderDefinitions": {
 18 |               "values": [
 19 |                 {
 20 |                   "name": "city",
 21 |                   "description": "Name of the city to get weather for",
 22 |                   "type": "string"
 23 |                 }
 24 |               ]
 25 |             },
 26 |             "authentication": "predefinedCredentialType",
 27 |             "nodeCredentialType": "weatherApiApi"
 28 |           },
 29 |           "credentials": {
 30 |             "weatherApiApi": {
 31 |               "id": "1",
 32 |               "name": "Weather API account"
 33 |             }
 34 |           },
 35 |           "notes": "Example shows proper toolDescription, URL with placeholder, and credential configuration"
 36 |         },
 37 |         {
 38 |           "name": "GitHub Issues Tool",
 39 |           "use_case": "Create GitHub issues from AI Agent conversations",
 40 |           "complexity": "medium",
 41 |           "parameters": {
 42 |             "method": "POST",
 43 |             "url": "https://api.github.com/repos/{owner}/{repo}/issues",
 44 |             "toolDescription": "Create a new GitHub issue. Requires owner (repo owner username), repo (repository name), title, and body. Returns the created issue URL and number.",
 45 |             "placeholderDefinitions": {
 46 |               "values": [
 47 |                 {
 48 |                   "name": "owner",
 49 |                   "description": "GitHub repository owner username",
 50 |                   "type": "string"
 51 |                 },
 52 |                 {
 53 |                   "name": "repo",
 54 |                   "description": "Repository name",
 55 |                   "type": "string"
 56 |                 },
 57 |                 {
 58 |                   "name": "title",
 59 |                   "description": "Issue title",
 60 |                   "type": "string"
 61 |                 },
 62 |                 {
 63 |                   "name": "body",
 64 |                   "description": "Issue description and details",
 65 |                   "type": "string"
 66 |                 }
 67 |               ]
 68 |             },
 69 |             "sendBody": true,
 70 |             "specifyBody": "json",
 71 |             "jsonBody": "={{ { \"title\": $json.title, \"body\": $json.body } }}",
 72 |             "authentication": "predefinedCredentialType",
 73 |             "nodeCredentialType": "githubApi"
 74 |           },
 75 |           "credentials": {
 76 |             "githubApi": {
 77 |               "id": "2",
 78 |               "name": "GitHub credentials"
 79 |             }
 80 |           },
 81 |           "notes": "Example shows POST request with JSON body, multiple placeholders, and expressions"
 82 |         },
 83 |         {
 84 |           "name": "Slack Message Tool",
 85 |           "use_case": "Send Slack messages from AI Agent",
 86 |           "complexity": "simple",
 87 |           "parameters": {
 88 |             "method": "POST",
 89 |             "url": "https://slack.com/api/chat.postMessage",
 90 |             "toolDescription": "Send a message to a Slack channel. Provide channel ID or name (e.g., '#general', 'C1234567890') and message text.",
 91 |             "placeholderDefinitions": {
 92 |               "values": [
 93 |                 {
 94 |                   "name": "channel",
 95 |                   "description": "Channel ID or name (e.g., #general)",
 96 |                   "type": "string"
 97 |                 },
 98 |                 {
 99 |                   "name": "text",
100 |                   "description": "Message text to send",
101 |                   "type": "string"
102 |                 }
103 |               ]
104 |             },
105 |             "sendHeaders": true,
106 |             "headerParameters": {
107 |               "parameters": [
108 |                 {
109 |                   "name": "Content-Type",
110 |                   "value": "application/json; charset=utf-8"
111 |                 },
112 |                 {
113 |                   "name": "Authorization",
114 |                   "value": "=Bearer {{$credentials.slackApi.accessToken}}"
115 |                 }
116 |               ]
117 |             },
118 |             "sendBody": true,
119 |             "specifyBody": "json",
120 |             "jsonBody": "={{ { \"channel\": $json.channel, \"text\": $json.text } }}",
121 |             "authentication": "predefinedCredentialType",
122 |             "nodeCredentialType": "slackApi"
123 |           },
124 |           "credentials": {
125 |             "slackApi": {
126 |               "id": "3",
127 |               "name": "Slack account"
128 |             }
129 |           },
130 |           "notes": "Example shows headers with credential expressions and JSON body construction"
131 |         }
132 |       ]
133 |     },
134 |     {
135 |       "node_type": "@n8n/n8n-nodes-langchain.toolCode",
136 |       "display_name": "Code Tool",
137 |       "examples": [
138 |         {
139 |           "name": "Calculate Shipping Cost",
140 |           "use_case": "Calculate shipping costs based on weight and distance",
141 |           "complexity": "simple",
142 |           "parameters": {
143 |             "name": "calculate_shipping_cost",
144 |             "description": "Calculate shipping cost based on package weight (in kg) and distance (in km). Returns the cost in USD.",
145 |             "language": "javaScript",
146 |             "code": "const baseRate = 5;\nconst perKgRate = 2;\nconst perKmRate = 0.1;\n\nconst weight = $input.weight || 0;\nconst distance = $input.distance || 0;\n\nconst cost = baseRate + (weight * perKgRate) + (distance * perKmRate);\n\nreturn { cost: parseFloat(cost.toFixed(2)), currency: 'USD' };",
147 |             "specifyInputSchema": true,
148 |             "schemaType": "manual",
149 |             "inputSchema": "{\n  \"type\": \"object\",\n  \"properties\": {\n    \"weight\": {\n      \"type\": \"number\",\n      \"description\": \"Package weight in kilograms\"\n    },\n    \"distance\": {\n      \"type\": \"number\",\n      \"description\": \"Shipping distance in kilometers\"\n    }\n  },\n  \"required\": [\"weight\", \"distance\"]\n}"
150 |           },
151 |           "notes": "Example shows proper function naming, detailed description, input schema, and return value"
152 |         },
153 |         {
154 |           "name": "Format Customer Data",
155 |           "use_case": "Transform and validate customer information",
156 |           "complexity": "medium",
157 |           "parameters": {
158 |             "name": "format_customer_data",
159 |             "description": "Format and validate customer data. Takes raw customer info (name, email, phone) and returns formatted object with validation status.",
160 |             "language": "javaScript",
161 |             "code": "const { name, email, phone } = $input;\n\n// Validation\nconst emailRegex = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\nconst phoneRegex = /^\\+?[1-9]\\d{1,14}$/;\n\nconst errors = [];\nif (!emailRegex.test(email)) errors.push('Invalid email format');\nif (!phoneRegex.test(phone)) errors.push('Invalid phone format');\n\n// Formatting\nconst formatted = {\n  name: name.trim(),\n  email: email.toLowerCase().trim(),\n  phone: phone.replace(/\\s/g, ''),\n  valid: errors.length === 0,\n  errors: errors\n};\n\nreturn formatted;",
162 |             "specifyInputSchema": true,
163 |             "schemaType": "manual",
164 |             "inputSchema": "{\n  \"type\": \"object\",\n  \"properties\": {\n    \"name\": {\n      \"type\": \"string\",\n      \"description\": \"Customer full name\"\n    },\n    \"email\": {\n      \"type\": \"string\",\n      \"description\": \"Customer email address\"\n    },\n    \"phone\": {\n      \"type\": \"string\",\n      \"description\": \"Customer phone number\"\n    }\n  },\n  \"required\": [\"name\", \"email\", \"phone\"]\n}"
165 |           },
166 |           "notes": "Example shows data validation, formatting, and structured error handling"
167 |         },
168 |         {
169 |           "name": "Parse Date Range",
170 |           "use_case": "Convert natural language date ranges to ISO format",
171 |           "complexity": "medium",
172 |           "parameters": {
173 |             "name": "parse_date_range",
174 |             "description": "Parse natural language date ranges (e.g., 'last 7 days', 'this month', 'Q1 2024') into start and end dates in ISO format.",
175 |             "language": "javaScript",
176 |             "code": "const input = $input.dateRange || '';\nconst now = new Date();\nlet start, end;\n\nif (input.includes('last') && input.includes('days')) {\n  const days = parseInt(input.match(/\\d+/)[0]);\n  start = new Date(now.getTime() - (days * 24 * 60 * 60 * 1000));\n  end = now;\n} else if (input === 'this month') {\n  start = new Date(now.getFullYear(), now.getMonth(), 1);\n  end = new Date(now.getFullYear(), now.getMonth() + 1, 0);\n} else if (input === 'this year') {\n  start = new Date(now.getFullYear(), 0, 1);\n  end = new Date(now.getFullYear(), 11, 31);\n} else {\n  throw new Error('Unsupported date range format');\n}\n\nreturn {\n  startDate: start.toISOString().split('T')[0],\n  endDate: end.toISOString().split('T')[0],\n  daysCount: Math.ceil((end - start) / (24 * 60 * 60 * 1000))\n};",
177 |             "specifyInputSchema": true,
178 |             "schemaType": "manual",
179 |             "inputSchema": "{\n  \"type\": \"object\",\n  \"properties\": {\n    \"dateRange\": {\n      \"type\": \"string\",\n      \"description\": \"Natural language date range (e.g., 'last 7 days', 'this month')\"\n    }\n  },\n  \"required\": [\"dateRange\"]\n}"
180 |           },
181 |           "notes": "Example shows complex logic, error handling, and date manipulation"
182 |         }
183 |       ]
184 |     },
185 |     {
186 |       "node_type": "@n8n/n8n-nodes-langchain.agentTool",
187 |       "display_name": "AI Agent Tool",
188 |       "examples": [
189 |         {
190 |           "name": "Research Specialist Agent",
191 |           "use_case": "Specialized sub-agent for in-depth research tasks",
192 |           "complexity": "medium",
193 |           "parameters": {
194 |             "name": "research_specialist",
195 |             "description": "Expert research agent that can search multiple sources, synthesize information, and provide comprehensive analysis on any topic. Use this when you need detailed, well-researched information.",
196 |             "promptType": "define",
197 |             "text": "You are a research specialist. Your role is to:\n1. Search for relevant information from multiple sources\n2. Synthesize findings into a coherent analysis\n3. Cite your sources\n4. Highlight key insights and patterns\n\nProvide thorough, well-structured research that answers the user's question comprehensively.",
198 |             "systemMessage": "You are a meticulous researcher focused on accuracy and completeness. Always cite sources and acknowledge limitations in available information."
199 |           },
200 |           "connections": {
201 |             "ai_languageModel": [
202 |               {
203 |                 "node": "OpenAI GPT-4",
204 |                 "type": "ai_languageModel",
205 |                 "index": 0
206 |               }
207 |             ],
208 |             "ai_tool": [
209 |               {
210 |                 "node": "SerpApi Tool",
211 |                 "type": "ai_tool",
212 |                 "index": 0
213 |               },
214 |               {
215 |                 "node": "Wikipedia Tool",
216 |                 "type": "ai_tool",
217 |                 "index": 0
218 |               }
219 |             ]
220 |           },
221 |           "notes": "Example shows specialized sub-agent with custom prompt, specific system message, and multiple search tools"
222 |         },
223 |         {
224 |           "name": "Data Analysis Agent",
225 |           "use_case": "Sub-agent for analyzing and visualizing data",
226 |           "complexity": "complex",
227 |           "parameters": {
228 |             "name": "data_analyst",
229 |             "description": "Data analysis specialist that can process datasets, calculate statistics, identify trends, and generate insights. Use for any data analysis or statistical questions.",
230 |             "promptType": "auto",
231 |             "systemMessage": "You are a data analyst with expertise in statistics and data interpretation. Break down complex datasets into understandable insights. Use the Code Tool to perform calculations when needed.",
232 |             "maxIterations": 10
233 |           },
234 |           "connections": {
235 |             "ai_languageModel": [
236 |               {
237 |                 "node": "Anthropic Claude",
238 |                 "type": "ai_languageModel",
239 |                 "index": 0
240 |               }
241 |             ],
242 |             "ai_tool": [
243 |               {
244 |                 "node": "Code Tool - Stats",
245 |                 "type": "ai_tool",
246 |                 "index": 0
247 |               },
248 |               {
249 |                 "node": "HTTP Request Tool - Data API",
250 |                 "type": "ai_tool",
251 |                 "index": 0
252 |               }
253 |             ]
254 |           },
255 |           "notes": "Example shows auto prompt type with specialized system message and analytical tools"
256 |         }
257 |       ]
258 |     },
259 |     {
260 |       "node_type": "@n8n/n8n-nodes-langchain.mcpClientTool",
261 |       "display_name": "MCP Client Tool",
262 |       "examples": [
263 |         {
264 |           "name": "Filesystem MCP Tool",
265 |           "use_case": "Access filesystem operations via MCP protocol",
266 |           "complexity": "medium",
267 |           "parameters": {
268 |             "description": "Access file system operations through MCP. Can read files, list directories, create files, and search for content.",
269 |             "mcpServer": {
270 |               "transport": "stdio",
271 |               "command": "npx",
272 |               "args": ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/allowed/directory"]
273 |             },
274 |             "tool": "read_file"
275 |           },
276 |           "notes": "Example shows stdio transport MCP server with filesystem access tool"
277 |         },
278 |         {
279 |           "name": "Puppeteer MCP Tool",
280 |           "use_case": "Browser automation via MCP for AI Agents",
281 |           "complexity": "complex",
282 |           "parameters": {
283 |             "description": "Control a web browser to navigate pages, take screenshots, and extract content. Useful for web scraping and automated testing.",
284 |             "mcpServer": {
285 |               "transport": "stdio",
286 |               "command": "npx",
287 |               "args": ["-y", "@modelcontextprotocol/server-puppeteer"]
288 |             },
289 |             "tool": "puppeteer_navigate"
290 |           },
291 |           "notes": "Example shows Puppeteer MCP server for browser automation"
292 |         },
293 |         {
294 |           "name": "Database MCP Tool",
295 |           "use_case": "Query databases via MCP protocol",
296 |           "complexity": "complex",
297 |           "parameters": {
298 |             "description": "Execute SQL queries and retrieve data from PostgreSQL databases. Supports SELECT, INSERT, UPDATE operations with proper escaping.",
299 |             "mcpServer": {
300 |               "transport": "sse",
301 |               "url": "https://mcp-server.example.com/database"
302 |             },
303 |             "tool": "execute_query"
304 |           },
305 |           "notes": "Example shows SSE transport MCP server for remote database access"
306 |         }
307 |       ]
308 |     }
309 |   ]
310 | }
311 | 
```

--------------------------------------------------------------------------------
/tests/unit/services/workflow-validator-with-mocks.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, it, expect, vi, beforeEach } from 'vitest';
  2 | import { WorkflowValidator } from '@/services/workflow-validator';
  3 | import { EnhancedConfigValidator } from '@/services/enhanced-config-validator';
  4 | 
  5 | // Mock logger to prevent console output
  6 | vi.mock('@/utils/logger', () => ({
  7 |   Logger: vi.fn().mockImplementation(() => ({
  8 |     error: vi.fn(),
  9 |     warn: vi.fn(),
 10 |     info: vi.fn()
 11 |   }))
 12 | }));
 13 | 
 14 | describe('WorkflowValidator - Simple Unit Tests', () => {
 15 |   let validator: WorkflowValidator;
 16 |   
 17 |   // Create a simple mock repository
 18 |   const createMockRepository = (nodeData: Record<string, any>) => ({
 19 |     getNode: vi.fn((type: string) => nodeData[type] || null),
 20 |     findSimilarNodes: vi.fn().mockReturnValue([])
 21 |   });
 22 | 
 23 |   // Create a simple mock validator class
 24 |   const createMockValidatorClass = (validationResult: any) => ({
 25 |     validateWithMode: vi.fn().mockReturnValue(validationResult)
 26 |   });
 27 | 
 28 |   beforeEach(() => {
 29 |     vi.clearAllMocks();
 30 |   });
 31 | 
 32 |   describe('Basic validation scenarios', () => {
 33 |     it('should pass validation for a webhook workflow with single node', async () => {
 34 |       // Arrange
 35 |       const nodeData = {
 36 |         'n8n-nodes-base.webhook': {
 37 |           type: 'nodes-base.webhook',
 38 |           displayName: 'Webhook',
 39 |           name: 'webhook',
 40 |           version: 1,
 41 |           isVersioned: true,
 42 |           properties: []
 43 |         },
 44 |         'nodes-base.webhook': {
 45 |           type: 'nodes-base.webhook',
 46 |           displayName: 'Webhook',
 47 |           name: 'webhook',
 48 |           version: 1,
 49 |           isVersioned: true,
 50 |           properties: []
 51 |         }
 52 |       };
 53 | 
 54 |       const mockRepository = createMockRepository(nodeData);
 55 |       const mockValidatorClass = createMockValidatorClass({
 56 |         valid: true,
 57 |         errors: [],
 58 |         warnings: [],
 59 |         suggestions: []
 60 |       });
 61 | 
 62 |       validator = new WorkflowValidator(mockRepository as any, mockValidatorClass as any);
 63 | 
 64 |       const workflow = {
 65 |         name: 'Webhook Workflow',
 66 |         nodes: [
 67 |           {
 68 |             id: '1',
 69 |             name: 'Webhook',
 70 |             type: 'n8n-nodes-base.webhook',
 71 |             typeVersion: 1,
 72 |             position: [250, 300] as [number, number],
 73 |             parameters: {}
 74 |           }
 75 |         ],
 76 |         connections: {}
 77 |       };
 78 | 
 79 |       // Act
 80 |       const result = await validator.validateWorkflow(workflow as any);
 81 | 
 82 |       // Assert
 83 |       expect(result.valid).toBe(true);
 84 |       expect(result.errors).toHaveLength(0);
 85 |       // Single webhook node should just have a warning about no connections
 86 |       expect(result.warnings.some(w => w.message.includes('no connections'))).toBe(true);
 87 |     });
 88 | 
 89 |     it('should fail validation for unknown node types', async () => {
 90 |       // Arrange
 91 |       const mockRepository = createMockRepository({}); // Empty node data
 92 |       const mockValidatorClass = createMockValidatorClass({
 93 |         valid: true,
 94 |         errors: [],
 95 |         warnings: [],
 96 |         suggestions: []
 97 |       });
 98 | 
 99 |       validator = new WorkflowValidator(mockRepository as any, mockValidatorClass as any);
100 | 
101 |       const workflow = {
102 |         name: 'Test Workflow',
103 |         nodes: [
104 |           {
105 |             id: '1',
106 |             name: 'Unknown',
107 |             type: 'n8n-nodes-base.unknownNode',
108 |             position: [250, 300] as [number, number],
109 |             parameters: {}
110 |           }
111 |         ],
112 |         connections: {}
113 |       };
114 | 
115 |       // Act
116 |       const result = await validator.validateWorkflow(workflow as any);
117 | 
118 |       // Assert
119 |       expect(result.valid).toBe(false);
120 |       // Check for either the error message or valid being false
121 |       const hasUnknownNodeError = result.errors.some(e =>
122 |         e.message && (e.message.includes('Unknown node type') || e.message.includes('unknown-node-type'))
123 |       );
124 |       expect(result.errors.length > 0 || hasUnknownNodeError).toBe(true);
125 |     });
126 | 
127 |     it('should detect duplicate node names', async () => {
128 |       // Arrange
129 |       const mockRepository = createMockRepository({});
130 |       const mockValidatorClass = createMockValidatorClass({
131 |         valid: true,
132 |         errors: [],
133 |         warnings: [],
134 |         suggestions: []
135 |       });
136 | 
137 |       validator = new WorkflowValidator(mockRepository as any, mockValidatorClass as any);
138 | 
139 |       const workflow = {
140 |         name: 'Duplicate Names',
141 |         nodes: [
142 |           {
143 |             id: '1',
144 |             name: 'HTTP Request',
145 |             type: 'n8n-nodes-base.httpRequest',
146 |             position: [250, 300] as [number, number],
147 |             parameters: {}
148 |           },
149 |           {
150 |             id: '2',
151 |             name: 'HTTP Request', // Duplicate name
152 |             type: 'n8n-nodes-base.httpRequest',
153 |             position: [450, 300] as [number, number],
154 |             parameters: {}
155 |           }
156 |         ],
157 |         connections: {}
158 |       };
159 | 
160 |       // Act
161 |       const result = await validator.validateWorkflow(workflow as any);
162 | 
163 |       // Assert
164 |       expect(result.valid).toBe(false);
165 |       expect(result.errors.some(e => e.message.includes('Duplicate node name'))).toBe(true);
166 |     });
167 | 
168 |     it('should validate connections properly', async () => {
169 |       // Arrange
170 |       const nodeData = {
171 |         'n8n-nodes-base.manualTrigger': {
172 |           type: 'nodes-base.manualTrigger',
173 |           displayName: 'Manual Trigger',
174 |           isVersioned: false,
175 |           properties: []
176 |         },
177 |         'nodes-base.manualTrigger': {
178 |           type: 'nodes-base.manualTrigger',
179 |           displayName: 'Manual Trigger',
180 |           isVersioned: false,
181 |           properties: []
182 |         },
183 |         'n8n-nodes-base.set': {
184 |           type: 'nodes-base.set',
185 |           displayName: 'Set',
186 |           version: 2,
187 |           isVersioned: true,
188 |           properties: []
189 |         },
190 |         'nodes-base.set': {
191 |           type: 'nodes-base.set',
192 |           displayName: 'Set',
193 |           version: 2,
194 |           isVersioned: true,
195 |           properties: []
196 |         }
197 |       };
198 | 
199 |       const mockRepository = createMockRepository(nodeData);
200 |       const mockValidatorClass = createMockValidatorClass({
201 |         valid: true,
202 |         errors: [],
203 |         warnings: [],
204 |         suggestions: []
205 |       });
206 | 
207 |       validator = new WorkflowValidator(mockRepository as any, mockValidatorClass as any);
208 | 
209 |       const workflow = {
210 |         name: 'Connected Workflow',
211 |         nodes: [
212 |           {
213 |             id: '1',
214 |             name: 'Manual Trigger',
215 |             type: 'n8n-nodes-base.manualTrigger',
216 |             position: [250, 300] as [number, number],
217 |             parameters: {}
218 |           },
219 |           {
220 |             id: '2',
221 |             name: 'Set',
222 |             type: 'n8n-nodes-base.set',
223 |             typeVersion: 2,
224 |             position: [450, 300] as [number, number],
225 |             parameters: {}
226 |           }
227 |         ],
228 |         connections: {
229 |           'Manual Trigger': {
230 |             main: [[{ node: 'Set', type: 'main', index: 0 }]]
231 |           }
232 |         }
233 |       };
234 | 
235 |       // Act
236 |       const result = await validator.validateWorkflow(workflow as any);
237 | 
238 |       // Assert
239 |       expect(result.valid).toBe(true);
240 |       expect(result.statistics.validConnections).toBe(1);
241 |       expect(result.statistics.invalidConnections).toBe(0);
242 |     });
243 | 
244 |     it('should detect workflow cycles', async () => {
245 |       // Arrange
246 |       const nodeData = {
247 |         'n8n-nodes-base.set': {
248 |           type: 'nodes-base.set',
249 |           displayName: 'Set',
250 |           isVersioned: true,
251 |           version: 2,
252 |           properties: []
253 |         },
254 |         'nodes-base.set': {
255 |           type: 'nodes-base.set',
256 |           displayName: 'Set',
257 |           isVersioned: true,
258 |           version: 2,
259 |           properties: []
260 |         }
261 |       };
262 | 
263 |       const mockRepository = createMockRepository(nodeData);
264 |       const mockValidatorClass = createMockValidatorClass({
265 |         valid: true,
266 |         errors: [],
267 |         warnings: [],
268 |         suggestions: []
269 |       });
270 | 
271 |       validator = new WorkflowValidator(mockRepository as any, mockValidatorClass as any);
272 | 
273 |       const workflow = {
274 |         name: 'Cyclic Workflow',
275 |         nodes: [
276 |           {
277 |             id: '1',
278 |             name: 'Node A',
279 |             type: 'n8n-nodes-base.set',
280 |             typeVersion: 2,
281 |             position: [250, 300] as [number, number],
282 |             parameters: {}
283 |           },
284 |           {
285 |             id: '2',
286 |             name: 'Node B',
287 |             type: 'n8n-nodes-base.set',
288 |             typeVersion: 2,
289 |             position: [450, 300] as [number, number],
290 |             parameters: {}
291 |           }
292 |         ],
293 |         connections: {
294 |           'Node A': {
295 |             main: [[{ node: 'Node B', type: 'main', index: 0 }]]
296 |           },
297 |           'Node B': {
298 |             main: [[{ node: 'Node A', type: 'main', index: 0 }]] // Creates a cycle
299 |           }
300 |         }
301 |       };
302 | 
303 |       // Act
304 |       const result = await validator.validateWorkflow(workflow as any);
305 | 
306 |       // Assert
307 |       expect(result.valid).toBe(false);
308 |       expect(result.errors.some(e => e.message.includes('cycle'))).toBe(true);
309 |     });
310 | 
311 |     it('should handle null workflow gracefully', async () => {
312 |       // Arrange
313 |       const mockRepository = createMockRepository({});
314 |       const mockValidatorClass = createMockValidatorClass({
315 |         valid: true,
316 |         errors: [],
317 |         warnings: [],
318 |         suggestions: []
319 |       });
320 | 
321 |       validator = new WorkflowValidator(mockRepository as any, mockValidatorClass as any);
322 | 
323 |       // Act
324 |       const result = await validator.validateWorkflow(null as any);
325 | 
326 |       // Assert
327 |       expect(result.valid).toBe(false);
328 |       expect(result.errors[0].message).toContain('workflow is null or undefined');
329 |     });
330 | 
331 |     it('should require connections for multi-node workflows', async () => {
332 |       // Arrange
333 |       const nodeData = {
334 |         'n8n-nodes-base.manualTrigger': {
335 |           type: 'nodes-base.manualTrigger',
336 |           displayName: 'Manual Trigger',
337 |           properties: []
338 |         },
339 |         'nodes-base.manualTrigger': {
340 |           type: 'nodes-base.manualTrigger',
341 |           displayName: 'Manual Trigger',
342 |           properties: []
343 |         },
344 |         'n8n-nodes-base.set': {
345 |           type: 'nodes-base.set',
346 |           displayName: 'Set',
347 |           version: 2,
348 |           isVersioned: true,
349 |           properties: []
350 |         },
351 |         'nodes-base.set': {
352 |           type: 'nodes-base.set',
353 |           displayName: 'Set',
354 |           version: 2,
355 |           isVersioned: true,
356 |           properties: []
357 |         }
358 |       };
359 | 
360 |       const mockRepository = createMockRepository(nodeData);
361 |       const mockValidatorClass = createMockValidatorClass({
362 |         valid: true,
363 |         errors: [],
364 |         warnings: [],
365 |         suggestions: []
366 |       });
367 | 
368 |       validator = new WorkflowValidator(mockRepository as any, mockValidatorClass as any);
369 | 
370 |       const workflow = {
371 |         name: 'No Connections',
372 |         nodes: [
373 |           {
374 |             id: '1',
375 |             name: 'Manual Trigger',
376 |             type: 'n8n-nodes-base.manualTrigger',
377 |             position: [250, 300] as [number, number],
378 |             parameters: {}
379 |           },
380 |           {
381 |             id: '2',
382 |             name: 'Set',
383 |             type: 'n8n-nodes-base.set',
384 |             typeVersion: 2,
385 |             position: [450, 300] as [number, number],
386 |             parameters: {}
387 |           }
388 |         ],
389 |         connections: {} // No connections between nodes
390 |       };
391 | 
392 |       // Act
393 |       const result = await validator.validateWorkflow(workflow as any);
394 | 
395 |       // Assert
396 |       expect(result.valid).toBe(false);
397 |       expect(result.errors.some(e => e.message.includes('Multi-node workflow has no connections'))).toBe(true);
398 |     });
399 | 
400 |     it('should validate typeVersion for versioned nodes', async () => {
401 |       // Arrange
402 |       const nodeData = {
403 |         'n8n-nodes-base.httpRequest': {
404 |           type: 'nodes-base.httpRequest',
405 |           displayName: 'HTTP Request',
406 |           isVersioned: true,
407 |           version: 3, // Latest version is 3
408 |           properties: []
409 |         },
410 |         'nodes-base.httpRequest': {
411 |           type: 'nodes-base.httpRequest',
412 |           displayName: 'HTTP Request',
413 |           isVersioned: true,
414 |           version: 3,
415 |           properties: []
416 |         }
417 |       };
418 | 
419 |       const mockRepository = createMockRepository(nodeData);
420 |       const mockValidatorClass = createMockValidatorClass({
421 |         valid: true,
422 |         errors: [],
423 |         warnings: [],
424 |         suggestions: []
425 |       });
426 | 
427 |       validator = new WorkflowValidator(mockRepository as any, mockValidatorClass as any);
428 | 
429 |       const workflow = {
430 |         name: 'Version Test',
431 |         nodes: [
432 |           {
433 |             id: '1',
434 |             name: 'HTTP Request',
435 |             type: 'n8n-nodes-base.httpRequest',
436 |             typeVersion: 2, // Outdated version
437 |             position: [250, 300] as [number, number],
438 |             parameters: {}
439 |           }
440 |         ],
441 |         connections: {}
442 |       };
443 | 
444 |       // Act
445 |       const result = await validator.validateWorkflow(workflow as any);
446 | 
447 |       // Assert
448 |       expect(result.warnings.some(w => w.message.includes('Outdated typeVersion'))).toBe(true);
449 |     });
450 | 
451 |     it('should normalize and validate nodes-base prefix to find the node', async () => {
452 |       // Arrange - Test that full-form types are normalized to short form to find the node
453 |       // The repository only has the node under the SHORT normalized key (database format)
454 |       const nodeData = {
455 |         'nodes-base.webhook': {  // Repository has it under SHORT form (database format)
456 |           type: 'nodes-base.webhook',
457 |           displayName: 'Webhook',
458 |           isVersioned: true,
459 |           version: 2,
460 |           properties: []
461 |         }
462 |       };
463 | 
464 |       // Mock repository that simulates the normalization behavior
465 |       // After our changes, getNode is called with the already-normalized type (short form)
466 |       const mockRepository = {
467 |         getNode: vi.fn((type: string) => {
468 |           // The validator now normalizes to short form before calling getNode
469 |           // So getNode receives 'nodes-base.webhook'
470 |           if (type === 'nodes-base.webhook') {
471 |             return nodeData['nodes-base.webhook'];
472 |           }
473 |           return null;
474 |         }),
475 |         findSimilarNodes: vi.fn().mockReturnValue([])
476 |       };
477 | 
478 |       const mockValidatorClass = createMockValidatorClass({
479 |         valid: true,
480 |         errors: [],
481 |         warnings: [],
482 |         suggestions: []
483 |       });
484 | 
485 |       validator = new WorkflowValidator(mockRepository as any, mockValidatorClass as any);
486 | 
487 |       const workflow = {
488 |         name: 'Valid Alternative Prefix',
489 |         nodes: [
490 |           {
491 |             id: '1',
492 |             name: 'Webhook',
493 |             type: 'n8n-nodes-base.webhook', // Using the full-form prefix (will be normalized to short)
494 |             position: [250, 300] as [number, number],
495 |             parameters: {},
496 |             typeVersion: 2
497 |           }
498 |         ],
499 |         connections: {}
500 |       };
501 | 
502 |       // Act
503 |       const result = await validator.validateWorkflow(workflow as any);
504 | 
505 |       // Assert - The node should be found through normalization
506 |       expect(result.valid).toBe(true);
507 |       expect(result.errors).toHaveLength(0);
508 | 
509 |       // Verify the repository was called (once with original, once with normalized)
510 |       expect(mockRepository.getNode).toHaveBeenCalled();
511 |     });
512 |   });
513 | });
```

--------------------------------------------------------------------------------
/tests/unit/mcp/lru-cache-behavior.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Comprehensive unit tests for LRU cache behavior in handlers-n8n-manager.ts
  3 |  *
  4 |  * This test file focuses specifically on cache behavior, TTL, eviction, and dispose callbacks
  5 |  */
  6 | 
  7 | import { describe, it, expect, beforeEach, afterEach, vi, Mock } from 'vitest';
  8 | import { LRUCache } from 'lru-cache';
  9 | import { createHash } from 'crypto';
 10 | import { getN8nApiClient } from '../../../src/mcp/handlers-n8n-manager';
 11 | import { InstanceContext, validateInstanceContext } from '../../../src/types/instance-context';
 12 | import { N8nApiClient } from '../../../src/services/n8n-api-client';
 13 | import { getN8nApiConfigFromContext } from '../../../src/config/n8n-api';
 14 | import { logger } from '../../../src/utils/logger';
 15 | 
 16 | // Mock dependencies
 17 | vi.mock('../../../src/services/n8n-api-client');
 18 | vi.mock('../../../src/config/n8n-api');
 19 | vi.mock('../../../src/utils/logger');
 20 | vi.mock('../../../src/types/instance-context', async () => {
 21 |   const actual = await vi.importActual('../../../src/types/instance-context');
 22 |   return {
 23 |     ...actual,
 24 |     validateInstanceContext: vi.fn()
 25 |   };
 26 | });
 27 | 
 28 | describe('LRU Cache Behavior Tests', () => {
 29 |   let mockN8nApiClient: Mock;
 30 |   let mockGetN8nApiConfigFromContext: Mock;
 31 |   let mockLogger: any; // Logger mock has complex type
 32 |   let mockValidateInstanceContext: Mock;
 33 | 
 34 |   beforeEach(() => {
 35 |     vi.resetAllMocks();
 36 |     vi.resetModules();
 37 |     vi.clearAllMocks();
 38 | 
 39 |     mockN8nApiClient = vi.mocked(N8nApiClient);
 40 |     mockGetN8nApiConfigFromContext = vi.mocked(getN8nApiConfigFromContext);
 41 |     mockLogger = vi.mocked(logger);
 42 |     mockValidateInstanceContext = vi.mocked(validateInstanceContext);
 43 | 
 44 |     // Default mock returns valid config
 45 |     mockGetN8nApiConfigFromContext.mockReturnValue({
 46 |       baseUrl: 'https://api.n8n.cloud',
 47 |       apiKey: 'test-key',
 48 |       timeout: 30000,
 49 |       maxRetries: 3
 50 |     });
 51 | 
 52 |     // Default mock returns valid context validation
 53 |     mockValidateInstanceContext.mockReturnValue({
 54 |       valid: true,
 55 |       errors: undefined
 56 |     });
 57 | 
 58 |     // Force re-import of the module to get fresh cache state
 59 |     vi.resetModules();
 60 |   });
 61 | 
 62 |   afterEach(() => {
 63 |     vi.clearAllMocks();
 64 |   });
 65 | 
 66 |   describe('Cache Key Generation and Collision', () => {
 67 |     it('should generate different cache keys for different contexts', () => {
 68 |       const context1: InstanceContext = {
 69 |         n8nApiUrl: 'https://api1.n8n.cloud',
 70 |         n8nApiKey: 'key1',
 71 |         instanceId: 'instance1'
 72 |       };
 73 | 
 74 |       const context2: InstanceContext = {
 75 |         n8nApiUrl: 'https://api2.n8n.cloud',
 76 |         n8nApiKey: 'key2',
 77 |         instanceId: 'instance2'
 78 |       };
 79 | 
 80 |       // Generate expected hashes manually
 81 |       const hash1 = createHash('sha256')
 82 |         .update(`${context1.n8nApiUrl}:${context1.n8nApiKey}:${context1.instanceId}`)
 83 |         .digest('hex');
 84 | 
 85 |       const hash2 = createHash('sha256')
 86 |         .update(`${context2.n8nApiUrl}:${context2.n8nApiKey}:${context2.instanceId}`)
 87 |         .digest('hex');
 88 | 
 89 |       expect(hash1).not.toBe(hash2);
 90 | 
 91 |       // Create clients to verify different cache entries
 92 |       const client1 = getN8nApiClient(context1);
 93 |       const client2 = getN8nApiClient(context2);
 94 | 
 95 |       expect(mockN8nApiClient).toHaveBeenCalledTimes(2);
 96 |     });
 97 | 
 98 |     it('should generate same cache key for identical contexts', () => {
 99 |       const context: InstanceContext = {
100 |         n8nApiUrl: 'https://api.n8n.cloud',
101 |         n8nApiKey: 'same-key',
102 |         instanceId: 'same-instance'
103 |       };
104 | 
105 |       const client1 = getN8nApiClient(context);
106 |       const client2 = getN8nApiClient(context);
107 | 
108 |       // Should only create one client (cache hit)
109 |       expect(mockN8nApiClient).toHaveBeenCalledTimes(1);
110 |       expect(client1).toBe(client2);
111 |     });
112 | 
113 |     it('should handle potential cache key collisions gracefully', () => {
114 |       // Create contexts that might produce similar hashes but are valid
115 |       const contexts = [
116 |         {
117 |           n8nApiUrl: 'https://a.com',
118 |           n8nApiKey: 'keyb',
119 |           instanceId: 'c'
120 |         },
121 |         {
122 |           n8nApiUrl: 'https://ab.com',
123 |           n8nApiKey: 'key',
124 |           instanceId: 'bc'
125 |         },
126 |         {
127 |           n8nApiUrl: 'https://abc.com',
128 |           n8nApiKey: 'differentkey',  // Fixed: empty string causes config creation to fail
129 |           instanceId: 'key'
130 |         }
131 |       ];
132 | 
133 |       contexts.forEach((context, index) => {
134 |         const client = getN8nApiClient(context);
135 |         expect(client).toBeDefined();
136 |       });
137 | 
138 |       // Each should create a separate client due to different hashes
139 |       expect(mockN8nApiClient).toHaveBeenCalledTimes(3);
140 |     });
141 |   });
142 | 
143 |   describe('LRU Eviction Behavior', () => {
144 |     it('should evict oldest entries when cache is full', async () => {
145 |       const loggerDebugSpy = vi.spyOn(logger, 'debug');
146 | 
147 |       // Create 101 different contexts to exceed max cache size of 100
148 |       const contexts: InstanceContext[] = [];
149 |       for (let i = 0; i < 101; i++) {
150 |         contexts.push({
151 |           n8nApiUrl: 'https://api.n8n.cloud',
152 |           n8nApiKey: `key-${i}`,
153 |           instanceId: `instance-${i}`
154 |         });
155 |       }
156 | 
157 |       // Create clients for all contexts
158 |       contexts.forEach(context => {
159 |         getN8nApiClient(context);
160 |       });
161 | 
162 |       // Should have called dispose callback for evicted entries
163 |       expect(loggerDebugSpy).toHaveBeenCalledWith(
164 |         'Evicting API client from cache',
165 |         expect.objectContaining({
166 |           cacheKey: expect.stringMatching(/^[a-f0-9]{8}\.\.\.$/i)
167 |         })
168 |       );
169 | 
170 |       // Verify dispose was called at least once
171 |       expect(loggerDebugSpy).toHaveBeenCalled();
172 |     });
173 | 
174 |     it('should maintain LRU order during access', () => {
175 |       const contexts: InstanceContext[] = [];
176 |       for (let i = 0; i < 5; i++) {
177 |         contexts.push({
178 |           n8nApiUrl: 'https://api.n8n.cloud',
179 |           n8nApiKey: `key-${i}`,
180 |           instanceId: `instance-${i}`
181 |         });
182 |       }
183 | 
184 |       // Create initial clients
185 |       contexts.forEach(context => {
186 |         getN8nApiClient(context);
187 |       });
188 | 
189 |       expect(mockN8nApiClient).toHaveBeenCalledTimes(5);
190 | 
191 |       // Access first context again (should move to most recent)
192 |       getN8nApiClient(contexts[0]);
193 | 
194 |       // Should not create new client (cache hit)
195 |       expect(mockN8nApiClient).toHaveBeenCalledTimes(5);
196 |     });
197 | 
198 |     it('should handle rapid successive access patterns', () => {
199 |       const context: InstanceContext = {
200 |         n8nApiUrl: 'https://api.n8n.cloud',
201 |         n8nApiKey: 'rapid-access-key',
202 |         instanceId: 'rapid-instance'
203 |       };
204 | 
205 |       // Rapidly access same context multiple times
206 |       for (let i = 0; i < 10; i++) {
207 |         getN8nApiClient(context);
208 |       }
209 | 
210 |       // Should only create one client despite multiple accesses
211 |       expect(mockN8nApiClient).toHaveBeenCalledTimes(1);
212 |     });
213 |   });
214 | 
215 |   describe('TTL (Time To Live) Behavior', () => {
216 |     it('should respect TTL settings', async () => {
217 |       const context: InstanceContext = {
218 |         n8nApiUrl: 'https://api.n8n.cloud',
219 |         n8nApiKey: 'ttl-test-key',
220 |         instanceId: 'ttl-instance'
221 |       };
222 | 
223 |       // Create initial client
224 |       const client1 = getN8nApiClient(context);
225 |       expect(mockN8nApiClient).toHaveBeenCalledTimes(1);
226 | 
227 |       // Access again immediately (should hit cache)
228 |       const client2 = getN8nApiClient(context);
229 |       expect(mockN8nApiClient).toHaveBeenCalledTimes(1);
230 |       expect(client1).toBe(client2);
231 | 
232 |       // Note: We can't easily test TTL expiration in unit tests
233 |       // as it requires actual time passage, but we can verify
234 |       // the updateAgeOnGet behavior
235 |     });
236 | 
237 |     it('should update age on cache access (updateAgeOnGet)', () => {
238 |       const context: InstanceContext = {
239 |         n8nApiUrl: 'https://api.n8n.cloud',
240 |         n8nApiKey: 'age-update-key',
241 |         instanceId: 'age-instance'
242 |       };
243 | 
244 |       // Create and access multiple times
245 |       getN8nApiClient(context);
246 |       getN8nApiClient(context);
247 |       getN8nApiClient(context);
248 | 
249 |       // Should only create one client due to cache hits
250 |       expect(mockN8nApiClient).toHaveBeenCalledTimes(1);
251 |     });
252 |   });
253 | 
254 |   describe('Dispose Callback Security and Logging', () => {
255 |     it('should sanitize cache keys in dispose callback logs', () => {
256 |       const loggerDebugSpy = vi.spyOn(logger, 'debug');
257 | 
258 |       // Create enough contexts to trigger eviction
259 |       const contexts: InstanceContext[] = [];
260 |       for (let i = 0; i < 102; i++) {
261 |         contexts.push({
262 |           n8nApiUrl: 'https://sensitive-api.n8n.cloud',
263 |           n8nApiKey: `super-secret-key-${i}`,
264 |           instanceId: `sensitive-instance-${i}`
265 |         });
266 |       }
267 | 
268 |       // Create clients to trigger eviction
269 |       contexts.forEach(context => {
270 |         getN8nApiClient(context);
271 |       });
272 | 
273 |       // Verify dispose callback logs don't contain sensitive data
274 |       const logCalls = loggerDebugSpy.mock.calls.filter(call =>
275 |         call[0] === 'Evicting API client from cache'
276 |       );
277 | 
278 |       logCalls.forEach(call => {
279 |         const logData = call[1] as any;
280 | 
281 |         // Should only log partial cache key (first 8 chars + ...)
282 |         expect(logData.cacheKey).toMatch(/^[a-f0-9]{8}\.\.\.$/i);
283 | 
284 |         // Should not contain any sensitive information
285 |         const logString = JSON.stringify(call);
286 |         expect(logString).not.toContain('super-secret-key');
287 |         expect(logString).not.toContain('sensitive-api');
288 |         expect(logString).not.toContain('sensitive-instance');
289 |       });
290 |     });
291 | 
292 |     it('should handle dispose callback with undefined client', () => {
293 |       const loggerDebugSpy = vi.spyOn(logger, 'debug');
294 | 
295 |       // Create many contexts to trigger disposal
296 |       for (let i = 0; i < 105; i++) {
297 |         const context: InstanceContext = {
298 |           n8nApiUrl: 'https://api.n8n.cloud',
299 |           n8nApiKey: `disposal-key-${i}`,
300 |           instanceId: `disposal-${i}`
301 |         };
302 |         getN8nApiClient(context);
303 |       }
304 | 
305 |       // Should handle disposal gracefully
306 |       expect(() => {
307 |         // The dispose callback should have been called
308 |         expect(loggerDebugSpy).toHaveBeenCalled();
309 |       }).not.toThrow();
310 |     });
311 |   });
312 | 
313 |   describe('Cache Memory Management', () => {
314 |     it('should maintain consistent cache size limits', () => {
315 |       // Create exactly 100 contexts (max cache size)
316 |       const contexts: InstanceContext[] = [];
317 |       for (let i = 0; i < 100; i++) {
318 |         contexts.push({
319 |           n8nApiUrl: 'https://api.n8n.cloud',
320 |           n8nApiKey: `memory-key-${i}`,
321 |           instanceId: `memory-${i}`
322 |         });
323 |       }
324 | 
325 |       // Create all clients
326 |       contexts.forEach(context => {
327 |         getN8nApiClient(context);
328 |       });
329 | 
330 |       // All should be cached
331 |       expect(mockN8nApiClient).toHaveBeenCalledTimes(100);
332 | 
333 |       // Access all again - should hit cache
334 |       contexts.forEach(context => {
335 |         getN8nApiClient(context);
336 |       });
337 | 
338 |       // Should not create additional clients
339 |       expect(mockN8nApiClient).toHaveBeenCalledTimes(100);
340 |     });
341 | 
342 |     it('should handle edge case of single cache entry', () => {
343 |       const context: InstanceContext = {
344 |         n8nApiUrl: 'https://api.n8n.cloud',
345 |         n8nApiKey: 'single-key',
346 |         instanceId: 'single-instance'
347 |       };
348 | 
349 |       // Create and access multiple times
350 |       for (let i = 0; i < 5; i++) {
351 |         getN8nApiClient(context);
352 |       }
353 | 
354 |       expect(mockN8nApiClient).toHaveBeenCalledTimes(1);
355 |     });
356 |   });
357 | 
358 |   describe('Cache Configuration Validation', () => {
359 |     it('should use reasonable cache limits', () => {
360 |       // These values should match the actual cache configuration
361 |       const MAX_CACHE_SIZE = 100;
362 |       const TTL_MINUTES = 30;
363 |       const TTL_MS = TTL_MINUTES * 60 * 1000;
364 | 
365 |       // Verify limits are reasonable
366 |       expect(MAX_CACHE_SIZE).toBeGreaterThan(0);
367 |       expect(MAX_CACHE_SIZE).toBeLessThanOrEqual(1000);
368 |       expect(TTL_MS).toBeGreaterThan(0);
369 |       expect(TTL_MS).toBeLessThanOrEqual(60 * 60 * 1000); // Max 1 hour
370 |     });
371 |   });
372 | 
373 |   describe('Cache Interaction with Validation', () => {
374 |     it('should not cache when context validation fails', () => {
375 |       // Reset mocks to ensure clean state for this test
376 |       vi.clearAllMocks();
377 |       mockValidateInstanceContext.mockClear();
378 | 
379 |       const invalidContext: InstanceContext = {
380 |         n8nApiUrl: 'invalid-url',
381 |         n8nApiKey: 'test-key',
382 |         instanceId: 'invalid-instance'
383 |       };
384 | 
385 |       // Mock validation failure
386 |       mockValidateInstanceContext.mockReturnValue({
387 |         valid: false,
388 |         errors: ['Invalid n8nApiUrl format']
389 |       });
390 | 
391 |       const client = getN8nApiClient(invalidContext);
392 | 
393 |       // Should not create client or cache anything
394 |       expect(client).toBeNull();
395 |       expect(mockN8nApiClient).not.toHaveBeenCalled();
396 |     });
397 | 
398 |     it('should handle cache when config creation fails', () => {
399 |       const context: InstanceContext = {
400 |         n8nApiUrl: 'https://api.n8n.cloud',
401 |         n8nApiKey: 'test-key',
402 |         instanceId: 'config-fail'
403 |       };
404 | 
405 |       // Mock config creation failure
406 |       mockGetN8nApiConfigFromContext.mockReturnValue(null);
407 | 
408 |       const client = getN8nApiClient(context);
409 | 
410 |       expect(client).toBeNull();
411 |     });
412 |   });
413 | 
414 |   describe('Complex Cache Scenarios', () => {
415 |     it('should handle mixed valid and invalid contexts', () => {
416 |       // Reset mocks to ensure clean state for this test
417 |       vi.clearAllMocks();
418 |       mockValidateInstanceContext.mockClear();
419 | 
420 |       // First, set up default valid behavior
421 |       mockValidateInstanceContext.mockReturnValue({
422 |         valid: true,
423 |         errors: undefined
424 |       });
425 | 
426 |       const validContext: InstanceContext = {
427 |         n8nApiUrl: 'https://api.n8n.cloud',
428 |         n8nApiKey: 'valid-key',
429 |         instanceId: 'valid'
430 |       };
431 | 
432 |       const invalidContext: InstanceContext = {
433 |         n8nApiUrl: 'invalid-url',
434 |         n8nApiKey: 'key',
435 |         instanceId: 'invalid'
436 |       };
437 | 
438 |       // Valid context should work
439 |       const validClient = getN8nApiClient(validContext);
440 |       expect(validClient).toBeDefined();
441 | 
442 |       // Change mock for invalid context
443 |       mockValidateInstanceContext.mockReturnValueOnce({
444 |         valid: false,
445 |         errors: ['Invalid URL']
446 |       });
447 | 
448 |       const invalidClient = getN8nApiClient(invalidContext);
449 |       expect(invalidClient).toBeNull();
450 | 
451 |       // Reset mock back to valid for subsequent calls
452 |       mockValidateInstanceContext.mockReturnValue({
453 |         valid: true,
454 |         errors: undefined
455 |       });
456 | 
457 |       // Valid context should still work (cache hit)
458 |       const validClient2 = getN8nApiClient(validContext);
459 |       expect(validClient2).toBe(validClient);
460 |     });
461 | 
462 |     it('should handle concurrent access to same cache key', () => {
463 |       const context: InstanceContext = {
464 |         n8nApiUrl: 'https://api.n8n.cloud',
465 |         n8nApiKey: 'concurrent-key',
466 |         instanceId: 'concurrent'
467 |       };
468 | 
469 |       // Simulate concurrent access
470 |       const promises = Array(10).fill(null).map(() =>
471 |         Promise.resolve(getN8nApiClient(context))
472 |       );
473 | 
474 |       return Promise.all(promises).then(clients => {
475 |         // All should return the same cached client
476 |         const firstClient = clients[0];
477 |         clients.forEach(client => {
478 |           expect(client).toBe(firstClient);
479 |         });
480 | 
481 |         // Should only create one client
482 |         expect(mockN8nApiClient).toHaveBeenCalledTimes(1);
483 |       });
484 |     });
485 |   });
486 | });
```

--------------------------------------------------------------------------------
/tests/unit/templates/metadata-generator.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, it, expect, vi, beforeEach } from 'vitest';
  2 | import { MetadataGenerator, TemplateMetadataSchema, MetadataRequest } from '../../../src/templates/metadata-generator';
  3 | 
  4 | // Mock OpenAI
  5 | vi.mock('openai', () => {
  6 |   return {
  7 |     default: vi.fn().mockImplementation(() => ({
  8 |       chat: {
  9 |         completions: {
 10 |           create: vi.fn()
 11 |         }
 12 |       }
 13 |     }))
 14 |   };
 15 | });
 16 | 
 17 | describe('MetadataGenerator', () => {
 18 |   let generator: MetadataGenerator;
 19 |   
 20 |   beforeEach(() => {
 21 |     generator = new MetadataGenerator('test-api-key', 'gpt-5-mini-2025-08-07');
 22 |   });
 23 |   
 24 |   describe('createBatchRequest', () => {
 25 |     it('should create a valid batch request', () => {
 26 |       const template: MetadataRequest = {
 27 |         templateId: 123,
 28 |         name: 'Test Workflow',
 29 |         description: 'A test workflow',
 30 |         nodes: ['n8n-nodes-base.webhook', 'n8n-nodes-base.httpRequest', 'n8n-nodes-base.slack']
 31 |       };
 32 |       
 33 |       const request = generator.createBatchRequest(template);
 34 |       
 35 |       expect(request.custom_id).toBe('template-123');
 36 |       expect(request.method).toBe('POST');
 37 |       expect(request.url).toBe('/v1/chat/completions');
 38 |       expect(request.body.model).toBe('gpt-5-mini-2025-08-07');
 39 |       expect(request.body.response_format.type).toBe('json_schema');
 40 |       expect(request.body.response_format.json_schema.strict).toBe(true);
 41 |       expect(request.body.messages).toHaveLength(2);
 42 |     });
 43 |     
 44 |     it('should summarize nodes effectively', () => {
 45 |       const template: MetadataRequest = {
 46 |         templateId: 456,
 47 |         name: 'Complex Workflow',
 48 |         nodes: [
 49 |           'n8n-nodes-base.webhook',
 50 |           'n8n-nodes-base.httpRequest',
 51 |           'n8n-nodes-base.httpRequest',
 52 |           'n8n-nodes-base.postgres',
 53 |           'n8n-nodes-base.slack',
 54 |           '@n8n/n8n-nodes-langchain.agent'
 55 |         ]
 56 |       };
 57 |       
 58 |       const request = generator.createBatchRequest(template);
 59 |       const userMessage = request.body.messages[1].content;
 60 |       
 61 |       expect(userMessage).toContain('Complex Workflow');
 62 |       expect(userMessage).toContain('Nodes Used (6)');
 63 |       expect(userMessage).toContain('HTTP/Webhooks');
 64 |     });
 65 |   });
 66 |   
 67 |   describe('parseResult', () => {
 68 |     it('should parse a successful result', () => {
 69 |       const mockResult = {
 70 |         custom_id: 'template-789',
 71 |         response: {
 72 |           body: {
 73 |             choices: [{
 74 |               message: {
 75 |                 content: JSON.stringify({
 76 |                   categories: ['automation', 'integration'],
 77 |                   complexity: 'medium',
 78 |                   use_cases: ['API integration', 'Data sync'],
 79 |                   estimated_setup_minutes: 30,
 80 |                   required_services: ['Slack API'],
 81 |                   key_features: ['Webhook triggers', 'API calls'],
 82 |                   target_audience: ['developers']
 83 |                 })
 84 |               },
 85 |               finish_reason: 'stop'
 86 |             }]
 87 |           }
 88 |         }
 89 |       };
 90 |       
 91 |       const result = generator.parseResult(mockResult);
 92 |       
 93 |       expect(result.templateId).toBe(789);
 94 |       expect(result.metadata.categories).toEqual(['automation', 'integration']);
 95 |       expect(result.metadata.complexity).toBe('medium');
 96 |       expect(result.error).toBeUndefined();
 97 |     });
 98 |     
 99 |     it('should handle error results', () => {
100 |       const mockResult = {
101 |         custom_id: 'template-999',
102 |         error: {
103 |           message: 'API error'
104 |         }
105 |       };
106 |       
107 |       const result = generator.parseResult(mockResult);
108 |       
109 |       expect(result.templateId).toBe(999);
110 |       expect(result.error).toBe('API error');
111 |       expect(result.metadata).toBeDefined();
112 |       expect(result.metadata.complexity).toBe('medium'); // Default metadata
113 |     });
114 |     
115 |     it('should handle malformed responses', () => {
116 |       const mockResult = {
117 |         custom_id: 'template-111',
118 |         response: {
119 |           body: {
120 |             choices: [{
121 |               message: {
122 |                 content: 'not valid json'
123 |               },
124 |               finish_reason: 'stop'
125 |             }]
126 |           }
127 |         }
128 |       };
129 |       
130 |       const result = generator.parseResult(mockResult);
131 |       
132 |       expect(result.templateId).toBe(111);
133 |       expect(result.error).toContain('Unexpected token');
134 |       expect(result.metadata).toBeDefined();
135 |     });
136 |   });
137 |   
138 |   describe('TemplateMetadataSchema', () => {
139 |     it('should validate correct metadata', () => {
140 |       const validMetadata = {
141 |         categories: ['automation', 'integration'],
142 |         complexity: 'simple' as const,
143 |         use_cases: ['API calls', 'Data processing'],
144 |         estimated_setup_minutes: 15,
145 |         required_services: [],
146 |         key_features: ['Fast processing'],
147 |         target_audience: ['developers']
148 |       };
149 |       
150 |       const result = TemplateMetadataSchema.safeParse(validMetadata);
151 |       
152 |       expect(result.success).toBe(true);
153 |     });
154 |     
155 |     it('should reject invalid complexity', () => {
156 |       const invalidMetadata = {
157 |         categories: ['automation'],
158 |         complexity: 'very-hard', // Invalid
159 |         use_cases: ['API calls'],
160 |         estimated_setup_minutes: 15,
161 |         required_services: [],
162 |         key_features: ['Fast'],
163 |         target_audience: ['developers']
164 |       };
165 |       
166 |       const result = TemplateMetadataSchema.safeParse(invalidMetadata);
167 |       
168 |       expect(result.success).toBe(false);
169 |     });
170 |     
171 |     it('should enforce array limits', () => {
172 |       const tooManyCategories = {
173 |         categories: ['a', 'b', 'c', 'd', 'e', 'f'], // Max 5
174 |         complexity: 'simple' as const,
175 |         use_cases: ['API calls'],
176 |         estimated_setup_minutes: 15,
177 |         required_services: [],
178 |         key_features: ['Fast'],
179 |         target_audience: ['developers']
180 |       };
181 |       
182 |       const result = TemplateMetadataSchema.safeParse(tooManyCategories);
183 |       
184 |       expect(result.success).toBe(false);
185 |     });
186 |     
187 |     it('should enforce time limits', () => {
188 |       const tooLongSetup = {
189 |         categories: ['automation'],
190 |         complexity: 'complex' as const,
191 |         use_cases: ['API calls'],
192 |         estimated_setup_minutes: 500, // Max 480
193 |         required_services: [],
194 |         key_features: ['Fast'],
195 |         target_audience: ['developers']
196 |       };
197 |       
198 |       const result = TemplateMetadataSchema.safeParse(tooLongSetup);
199 |       
200 |       expect(result.success).toBe(false);
201 |     });
202 |   });
203 | 
204 |   describe('Input Sanitization and Security', () => {
205 |     it('should handle malicious template names safely', () => {
206 |       const maliciousTemplate: MetadataRequest = {
207 |         templateId: 123,
208 |         name: '<script>alert("xss")</script>',
209 |         description: 'javascript:alert(1)',
210 |         nodes: ['n8n-nodes-base.webhook']
211 |       };
212 |       
213 |       const request = generator.createBatchRequest(maliciousTemplate);
214 |       const userMessage = request.body.messages[1].content;
215 |       
216 |       // Should contain the malicious content as-is (OpenAI will handle it)
217 |       // but should not cause any injection in our code
218 |       expect(userMessage).toContain('<script>alert("xss")</script>');
219 |       expect(userMessage).toContain('javascript:alert(1)');
220 |       expect(request.body.model).toBe('gpt-5-mini-2025-08-07');
221 |     });
222 | 
223 |     it('should handle extremely long template names', () => {
224 |       const longName = 'A'.repeat(10000); // Very long name
225 |       const template: MetadataRequest = {
226 |         templateId: 456,
227 |         name: longName,
228 |         nodes: ['n8n-nodes-base.webhook']
229 |       };
230 |       
231 |       const request = generator.createBatchRequest(template);
232 |       
233 |       expect(request.custom_id).toBe('template-456');
234 |       expect(request.body.messages[1].content).toContain(longName);
235 |     });
236 | 
237 |     it('should handle special characters in node names', () => {
238 |       const template: MetadataRequest = {
239 |         templateId: 789,
240 |         name: 'Test Workflow',
241 |         nodes: [
242 |           'n8n-nodes-base.webhook',
243 |           '@n8n/custom-node.with.dots',
244 |           'custom-package/node-with-slashes',
245 |           'node_with_underscore',
246 |           'node-with-unicode-名前'
247 |         ]
248 |       };
249 |       
250 |       const request = generator.createBatchRequest(template);
251 |       const userMessage = request.body.messages[1].content;
252 |       
253 |       expect(userMessage).toContain('HTTP/Webhooks');
254 |       expect(userMessage).toContain('custom-node.with.dots');
255 |     });
256 | 
257 |     it('should handle empty or undefined descriptions safely', () => {
258 |       const template: MetadataRequest = {
259 |         templateId: 100,
260 |         name: 'Test',
261 |         description: undefined,
262 |         nodes: ['n8n-nodes-base.webhook']
263 |       };
264 |       
265 |       const request = generator.createBatchRequest(template);
266 |       const userMessage = request.body.messages[1].content;
267 |       
268 |       // Should not include undefined or null in the message
269 |       expect(userMessage).not.toContain('undefined');
270 |       expect(userMessage).not.toContain('null');
271 |       expect(userMessage).toContain('Test');
272 |     });
273 | 
274 |     it('should limit context size for very large workflows', () => {
275 |       const manyNodes = Array.from({ length: 1000 }, (_, i) => `n8n-nodes-base.node${i}`);
276 |       const template: MetadataRequest = {
277 |         templateId: 200,
278 |         name: 'Huge Workflow',
279 |         nodes: manyNodes,
280 |         workflow: {
281 |           nodes: Array.from({ length: 500 }, (_, i) => ({ id: `node${i}` })),
282 |           connections: {}
283 |         }
284 |       };
285 |       
286 |       const request = generator.createBatchRequest(template);
287 |       const userMessage = request.body.messages[1].content;
288 |       
289 |       // Should handle large amounts of data gracefully
290 |       expect(userMessage.length).toBeLessThan(50000); // Reasonable limit
291 |       expect(userMessage).toContain('Huge Workflow');
292 |     });
293 |   });
294 | 
295 |   describe('Error Handling and Edge Cases', () => {
296 |     it('should handle malformed OpenAI responses', () => {
297 |       const malformedResults = [
298 |         {
299 |           custom_id: 'template-111',
300 |           response: {
301 |             body: {
302 |               choices: [{
303 |                 message: {
304 |                   content: '{"invalid": json syntax}'
305 |                 },
306 |                 finish_reason: 'stop'
307 |               }]
308 |             }
309 |           }
310 |         },
311 |         {
312 |           custom_id: 'template-222', 
313 |           response: {
314 |             body: {
315 |               choices: [{
316 |                 message: {
317 |                   content: null
318 |                 },
319 |                 finish_reason: 'stop'
320 |               }]
321 |             }
322 |           }
323 |         },
324 |         {
325 |           custom_id: 'template-333',
326 |           response: {
327 |             body: {
328 |               choices: []
329 |             }
330 |           }
331 |         }
332 |       ];
333 |       
334 |       malformedResults.forEach(result => {
335 |         const parsed = generator.parseResult(result);
336 |         expect(parsed.error).toBeDefined();
337 |         expect(parsed.metadata).toBeDefined();
338 |         expect(parsed.metadata.complexity).toBe('medium'); // Default metadata
339 |       });
340 |     });
341 | 
342 |     it('should handle Zod validation failures', () => {
343 |       const invalidResponse = {
344 |         custom_id: 'template-444',
345 |         response: {
346 |           body: {
347 |             choices: [{
348 |               message: {
349 |                 content: JSON.stringify({
350 |                   categories: ['too', 'many', 'categories', 'here', 'way', 'too', 'many'],
351 |                   complexity: 'invalid-complexity',
352 |                   use_cases: [],
353 |                   estimated_setup_minutes: -5, // Invalid negative time
354 |                   required_services: 'not-an-array',
355 |                   key_features: null,
356 |                   target_audience: ['too', 'many', 'audiences', 'here']
357 |                 })
358 |               },
359 |               finish_reason: 'stop'
360 |             }]
361 |           }
362 |         }
363 |       };
364 |       
365 |       const result = generator.parseResult(invalidResponse);
366 |       
367 |       expect(result.templateId).toBe(444);
368 |       expect(result.error).toBeDefined();
369 |       expect(result.metadata).toEqual(generator['getDefaultMetadata']());
370 |     });
371 | 
372 |     it('should handle network timeouts gracefully in generateSingle', async () => {
373 |       // Create a new generator with mocked OpenAI client
374 |       const mockClient = {
375 |         chat: {
376 |           completions: {
377 |             create: vi.fn().mockRejectedValue(new Error('Request timed out'))
378 |           }
379 |         }
380 |       };
381 |       
382 |       // Override the client property using Object.defineProperty
383 |       Object.defineProperty(generator, 'client', {
384 |         value: mockClient,
385 |         writable: true
386 |       });
387 |       
388 |       const template: MetadataRequest = {
389 |         templateId: 555,
390 |         name: 'Timeout Test',
391 |         nodes: ['n8n-nodes-base.webhook']
392 |       };
393 |       
394 |       const result = await generator.generateSingle(template);
395 |       
396 |       // Should return default metadata instead of throwing
397 |       expect(result).toEqual(generator['getDefaultMetadata']());
398 |     });
399 |   });
400 | 
401 |   describe('Node Summarization Logic', () => {
402 |     it('should group similar nodes correctly', () => {
403 |       const template: MetadataRequest = {
404 |         templateId: 666,
405 |         name: 'Complex Workflow',
406 |         nodes: [
407 |           'n8n-nodes-base.webhook',
408 |           'n8n-nodes-base.httpRequest',
409 |           'n8n-nodes-base.postgres',
410 |           'n8n-nodes-base.mysql',
411 |           'n8n-nodes-base.slack',
412 |           'n8n-nodes-base.gmail',
413 |           '@n8n/n8n-nodes-langchain.openAi',
414 |           '@n8n/n8n-nodes-langchain.agent',
415 |           'n8n-nodes-base.googleSheets',
416 |           'n8n-nodes-base.excel'
417 |         ]
418 |       };
419 |       
420 |       const request = generator.createBatchRequest(template);
421 |       const userMessage = request.body.messages[1].content;
422 |       
423 |       expect(userMessage).toContain('HTTP/Webhooks (2)');
424 |       expect(userMessage).toContain('Database (2)');
425 |       expect(userMessage).toContain('Communication (2)');
426 |       expect(userMessage).toContain('AI/ML (2)');
427 |       expect(userMessage).toContain('Spreadsheets (2)');
428 |     });
429 | 
430 |     it('should handle unknown node types gracefully', () => {
431 |       const template: MetadataRequest = {
432 |         templateId: 777,
433 |         name: 'Unknown Nodes',
434 |         nodes: [
435 |           'custom-package.unknownNode',
436 |           'another-package.weirdNodeType',
437 |           'someNodeTrigger',
438 |           'anotherNode'
439 |         ]
440 |       };
441 |       
442 |       const request = generator.createBatchRequest(template);
443 |       const userMessage = request.body.messages[1].content;
444 |       
445 |       // Should handle unknown nodes without crashing
446 |       expect(userMessage).toContain('unknownNode');
447 |       expect(userMessage).toContain('weirdNodeType');
448 |       expect(userMessage).toContain('someNode'); // Trigger suffix removed
449 |     });
450 | 
451 |     it('should limit node summary length', () => {
452 |       const manyNodes = Array.from({ length: 50 }, (_, i) => 
453 |         `n8n-nodes-base.customNode${i}`
454 |       );
455 |       
456 |       const template: MetadataRequest = {
457 |         templateId: 888,
458 |         name: 'Many Nodes',
459 |         nodes: manyNodes
460 |       };
461 |       
462 |       const request = generator.createBatchRequest(template);
463 |       const userMessage = request.body.messages[1].content;
464 |       
465 |       // Should limit to top 10 groups
466 |       const summaryLine = userMessage.split('\n').find((line: string) => 
467 |         line.includes('Nodes Used (50)')
468 |       );
469 |       
470 |       expect(summaryLine).toBeDefined();
471 |       const nodeGroups = summaryLine!.split(': ')[1].split(', ');
472 |       expect(nodeGroups.length).toBeLessThanOrEqual(10);
473 |     });
474 |   });
475 | });
```

--------------------------------------------------------------------------------
/tests/integration/docker/docker-config.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, it, expect, beforeAll, afterAll, beforeEach, afterEach } from 'vitest';
  2 | import { execSync, spawn } from 'child_process';
  3 | import path from 'path';
  4 | import fs from 'fs';
  5 | import os from 'os';
  6 | import { exec, waitForHealthy, isRunningInHttpMode, getProcessEnv } from './test-helpers';
  7 | 
  8 | // Skip tests if not in CI or if Docker is not available
  9 | const SKIP_DOCKER_TESTS = process.env.CI !== 'true' && !process.env.RUN_DOCKER_TESTS;
 10 | const describeDocker = SKIP_DOCKER_TESTS ? describe.skip : describe;
 11 | 
 12 | // Helper to check if Docker is available
 13 | async function isDockerAvailable(): Promise<boolean> {
 14 |   try {
 15 |     await exec('docker --version');
 16 |     return true;
 17 |   } catch {
 18 |     return false;
 19 |   }
 20 | }
 21 | 
 22 | // Helper to generate unique container names
 23 | function generateContainerName(suffix: string): string {
 24 |   return `n8n-mcp-test-${Date.now()}-${suffix}`;
 25 | }
 26 | 
 27 | // Helper to clean up containers
 28 | async function cleanupContainer(containerName: string) {
 29 |   try {
 30 |     await exec(`docker stop ${containerName}`);
 31 |     await exec(`docker rm ${containerName}`);
 32 |   } catch {
 33 |     // Ignore errors - container might not exist
 34 |   }
 35 | }
 36 | 
 37 | describeDocker('Docker Config File Integration', () => {
 38 |   let tempDir: string;
 39 |   let dockerAvailable: boolean;
 40 |   const imageName = 'n8n-mcp-test:latest';
 41 |   const containers: string[] = [];
 42 | 
 43 |   beforeAll(async () => {
 44 |     dockerAvailable = await isDockerAvailable();
 45 |     if (!dockerAvailable) {
 46 |       console.warn('Docker not available, skipping Docker integration tests');
 47 |       return;
 48 |     }
 49 | 
 50 |     // Check if image exists
 51 |     let imageExists = false;
 52 |     try {
 53 |       await exec(`docker image inspect ${imageName}`);
 54 |       imageExists = true;
 55 |     } catch {
 56 |       imageExists = false;
 57 |     }
 58 | 
 59 |     // Build test image if in CI or if explicitly requested or if image doesn't exist
 60 |     if (!imageExists || process.env.CI === 'true' || process.env.BUILD_DOCKER_TEST_IMAGE === 'true') {
 61 |       const projectRoot = path.resolve(__dirname, '../../../');
 62 |       console.log('Building Docker image for tests...');
 63 |       try {
 64 |         execSync(`docker build -t ${imageName} .`, {
 65 |           cwd: projectRoot,
 66 |           stdio: 'inherit'
 67 |         });
 68 |         console.log('Docker image built successfully');
 69 |       } catch (error) {
 70 |         console.error('Failed to build Docker image:', error);
 71 |         throw new Error('Docker image build failed - tests cannot continue');
 72 |       }
 73 |     } else {
 74 |       console.log(`Using existing Docker image: ${imageName}`);
 75 |     }
 76 |   }, 60000); // Increase timeout to 60s for Docker build
 77 | 
 78 |   beforeEach(() => {
 79 |     tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'docker-config-test-'));
 80 |   });
 81 | 
 82 |   afterEach(async () => {
 83 |     // Clean up containers
 84 |     for (const container of containers) {
 85 |       await cleanupContainer(container);
 86 |     }
 87 |     containers.length = 0;
 88 | 
 89 |     // Clean up temp directory
 90 |     if (fs.existsSync(tempDir)) {
 91 |       fs.rmSync(tempDir, { recursive: true });
 92 |     }
 93 |   });
 94 | 
 95 |   describe('Config file loading', () => {
 96 |     it('should load config.json and set environment variables', async () => {
 97 |       if (!dockerAvailable) return;
 98 | 
 99 |       const containerName = generateContainerName('config-load');
100 |       containers.push(containerName);
101 | 
102 |       // Create config file
103 |       const configPath = path.join(tempDir, 'config.json');
104 |       const config = {
105 |         mcp_mode: 'http',
106 |         auth_token: 'test-token-from-config',
107 |         port: 3456,
108 |         database: {
109 |           path: '/data/custom.db'
110 |         }
111 |       };
112 |       fs.writeFileSync(configPath, JSON.stringify(config));
113 | 
114 |       // Run container with config file mounted
115 |       const { stdout } = await exec(
116 |         `docker run --name ${containerName} -v "${configPath}:/app/config.json:ro" ${imageName} sh -c "env | grep -E '^(MCP_MODE|AUTH_TOKEN|PORT|DATABASE_PATH)=' | sort"`
117 |       );
118 | 
119 |       const envVars = stdout.trim().split('\n').reduce((acc, line) => {
120 |         const [key, value] = line.split('=');
121 |         acc[key] = value;
122 |         return acc;
123 |       }, {} as Record<string, string>);
124 | 
125 |       expect(envVars.MCP_MODE).toBe('http');
126 |       expect(envVars.AUTH_TOKEN).toBe('test-token-from-config');
127 |       expect(envVars.PORT).toBe('3456');
128 |       expect(envVars.DATABASE_PATH).toBe('/data/custom.db');
129 |     });
130 | 
131 |     it('should give precedence to environment variables over config file', async () => {
132 |       if (!dockerAvailable) return;
133 | 
134 |       const containerName = generateContainerName('env-precedence');
135 |       containers.push(containerName);
136 | 
137 |       // Create config file
138 |       const configPath = path.join(tempDir, 'config.json');
139 |       const config = {
140 |         mcp_mode: 'stdio',
141 |         auth_token: 'config-token',
142 |         custom_var: 'from-config'
143 |       };
144 |       fs.writeFileSync(configPath, JSON.stringify(config));
145 | 
146 |       // Run container with both env vars and config file
147 |       const { stdout } = await exec(
148 |         `docker run --name ${containerName} ` +
149 |         `-e MCP_MODE=http ` +
150 |         `-e AUTH_TOKEN=env-token ` +
151 |         `-v "${configPath}:/app/config.json:ro" ` +
152 |         `${imageName} sh -c "env | grep -E '^(MCP_MODE|AUTH_TOKEN|CUSTOM_VAR)=' | sort"`
153 |       );
154 | 
155 |       const envVars = stdout.trim().split('\n').reduce((acc, line) => {
156 |         const [key, value] = line.split('=');
157 |         acc[key] = value;
158 |         return acc;
159 |       }, {} as Record<string, string>);
160 | 
161 |       expect(envVars.MCP_MODE).toBe('http'); // From env var
162 |       expect(envVars.AUTH_TOKEN).toBe('env-token'); // From env var
163 |       expect(envVars.CUSTOM_VAR).toBe('from-config'); // From config file
164 |     });
165 | 
166 |     it('should handle missing config file gracefully', async () => {
167 |       if (!dockerAvailable) return;
168 | 
169 |       const containerName = generateContainerName('no-config');
170 |       containers.push(containerName);
171 | 
172 |       // Run container without config file
173 |       const { stdout, stderr } = await exec(
174 |         `docker run --name ${containerName} ${imageName} echo "Container started successfully"`
175 |       );
176 | 
177 |       expect(stdout.trim()).toBe('Container started successfully');
178 |       expect(stderr).toBe('');
179 |     });
180 | 
181 |     it('should handle invalid JSON in config file gracefully', async () => {
182 |       if (!dockerAvailable) return;
183 | 
184 |       const containerName = generateContainerName('invalid-json');
185 |       containers.push(containerName);
186 | 
187 |       // Create invalid config file
188 |       const configPath = path.join(tempDir, 'config.json');
189 |       fs.writeFileSync(configPath, '{ invalid json }');
190 | 
191 |       // Container should still start despite invalid config
192 |       const { stdout } = await exec(
193 |         `docker run --name ${containerName} -v "${configPath}:/app/config.json:ro" ${imageName} echo "Started despite invalid config"`
194 |       );
195 | 
196 |       expect(stdout.trim()).toBe('Started despite invalid config');
197 |     });
198 |   });
199 | 
200 |   describe('n8n-mcp serve command', () => {
201 |     it('should automatically set MCP_MODE=http for "n8n-mcp serve" command', async () => {
202 |       if (!dockerAvailable) return;
203 | 
204 |       const containerName = generateContainerName('serve-command');
205 |       containers.push(containerName);
206 | 
207 |       // Run container with n8n-mcp serve command
208 |       // Start the container in detached mode
209 |       await exec(
210 |         `docker run -d --name ${containerName} -e AUTH_TOKEN=test-token -p 13001:3000 ${imageName} n8n-mcp serve`
211 |       );
212 |       
213 |       // Give it time to start
214 |       await new Promise(resolve => setTimeout(resolve, 3000));
215 |       
216 |       // Verify it's running in HTTP mode by checking the health endpoint
217 |       const { stdout } = await exec(
218 |         `docker exec ${containerName} curl -s http://localhost:3000/health || echo 'Server not responding'`
219 |       );
220 | 
221 |       // If HTTP mode is active, health endpoint should respond
222 |       expect(stdout).toContain('ok');
223 |     });
224 | 
225 |     it('should preserve additional arguments when using "n8n-mcp serve"', async () => {
226 |       if (!dockerAvailable) return;
227 | 
228 |       const containerName = generateContainerName('serve-args');
229 |       containers.push(containerName);
230 | 
231 |       // Test that additional arguments are passed through
232 |       // Note: This test is checking the command construction, not actual execution
233 |       const result = await exec(
234 |         `docker run --name ${containerName} ${imageName} sh -c "set -x; n8n-mcp serve --port 8080 2>&1 | grep -E 'node.*index.js.*--port.*8080' || echo 'Pattern not found'"`
235 |       );
236 | 
237 |       // The serve command should transform to node command with arguments preserved
238 |       expect(result.stdout).toBeTruthy();
239 |     });
240 |   });
241 | 
242 |   describe('Database initialization', () => {
243 |     it('should initialize database when not present', async () => {
244 |       if (!dockerAvailable) return;
245 | 
246 |       const containerName = generateContainerName('db-init');
247 |       containers.push(containerName);
248 | 
249 |       // Run container and check database initialization
250 |       const { stdout } = await exec(
251 |         `docker run --name ${containerName} ${imageName} sh -c "ls -la /app/data/nodes.db && echo 'Database initialized'"`
252 |       );
253 | 
254 |       expect(stdout).toContain('nodes.db');
255 |       expect(stdout).toContain('Database initialized');
256 |     });
257 | 
258 |     it('should respect NODE_DB_PATH from config file', async () => {
259 |       if (!dockerAvailable) return;
260 | 
261 |       const containerName = generateContainerName('custom-db-path');
262 |       containers.push(containerName);
263 | 
264 |       // Create config with custom database path
265 |       const configPath = path.join(tempDir, 'config.json');
266 |       const config = {
267 |         NODE_DB_PATH: '/app/data/custom/custom.db'  // Use uppercase and a writable path
268 |       };
269 |       fs.writeFileSync(configPath, JSON.stringify(config));
270 | 
271 |       // Run container in detached mode to check environment after initialization
272 |       // Set MCP_MODE=http so the server keeps running (stdio mode exits when stdin is closed in detached mode)
273 |       await exec(
274 |         `docker run -d --name ${containerName} -e MCP_MODE=http -e AUTH_TOKEN=test -v "${configPath}:/app/config.json:ro" ${imageName}`
275 |       );
276 |       
277 |       // Give it time to load config and start
278 |       await new Promise(resolve => setTimeout(resolve, 2000));
279 |       
280 |       // Check the actual process environment
281 |       const { stdout } = await exec(
282 |         `docker exec ${containerName} sh -c "cat /proc/1/environ | tr '\\0' '\\n' | grep NODE_DB_PATH || echo 'NODE_DB_PATH not found'"`
283 |       );
284 | 
285 |       expect(stdout.trim()).toBe('NODE_DB_PATH=/app/data/custom/custom.db');
286 |     });
287 |   });
288 | 
289 |   describe('Authentication configuration', () => {
290 |     it('should enforce AUTH_TOKEN requirement in HTTP mode', async () => {
291 |       if (!dockerAvailable) return;
292 | 
293 |       const containerName = generateContainerName('auth-required');
294 |       containers.push(containerName);
295 | 
296 |       // Try to run in HTTP mode without auth token
297 |       try {
298 |         await exec(
299 |           `docker run --name ${containerName} -e MCP_MODE=http ${imageName} echo "Should not reach here"`
300 |         );
301 |         expect.fail('Container should have exited with error');
302 |       } catch (error: any) {
303 |         expect(error.stderr).toContain('AUTH_TOKEN or AUTH_TOKEN_FILE is required for HTTP mode');
304 |       }
305 |     });
306 | 
307 |     it('should accept AUTH_TOKEN from config file', async () => {
308 |       if (!dockerAvailable) return;
309 | 
310 |       const containerName = generateContainerName('auth-config');
311 |       containers.push(containerName);
312 | 
313 |       // Create config with auth token
314 |       const configPath = path.join(tempDir, 'config.json');
315 |       const config = {
316 |         mcp_mode: 'http',
317 |         auth_token: 'config-auth-token'
318 |       };
319 |       fs.writeFileSync(configPath, JSON.stringify(config));
320 | 
321 |       // Run container with config file
322 |       const { stdout } = await exec(
323 |         `docker run --name ${containerName} -v "${configPath}:/app/config.json:ro" ${imageName} sh -c "env | grep AUTH_TOKEN"`
324 |       );
325 | 
326 |       expect(stdout.trim()).toBe('AUTH_TOKEN=config-auth-token');
327 |     });
328 |   });
329 | 
330 |   describe('Security and permissions', () => {
331 |     it('should handle malicious config values safely', async () => {
332 |       if (!dockerAvailable) return;
333 | 
334 |       const containerName = generateContainerName('security-test');
335 |       containers.push(containerName);
336 | 
337 |       // Create config with potentially malicious values
338 |       const configPath = path.join(tempDir, 'config.json');
339 |       const config = {
340 |         malicious1: "'; echo 'hacked' > /tmp/hacked.txt; '",
341 |         malicious2: "$( touch /tmp/command-injection.txt )",
342 |         malicious3: "`touch /tmp/backtick-injection.txt`"
343 |       };
344 |       fs.writeFileSync(configPath, JSON.stringify(config));
345 | 
346 |       // Run container and check that no files were created
347 |       const { stdout } = await exec(
348 |         `docker run --name ${containerName} -v "${configPath}:/app/config.json:ro" ${imageName} sh -c "ls -la /tmp/ | grep -E '(hacked|injection)' || echo 'No malicious files created'"`
349 |       );
350 | 
351 |       expect(stdout.trim()).toBe('No malicious files created');
352 |     });
353 | 
354 |     it('should run as non-root user by default', async () => {
355 |       if (!dockerAvailable) return;
356 | 
357 |       const containerName = generateContainerName('non-root');
358 |       containers.push(containerName);
359 | 
360 |       // Check user inside container
361 |       const { stdout } = await exec(
362 |         `docker run --name ${containerName} ${imageName} whoami`
363 |       );
364 | 
365 |       expect(stdout.trim()).toBe('nodejs');
366 |     });
367 |   });
368 | 
369 |   describe('Complex configuration scenarios', () => {
370 |     it('should handle nested configuration with all supported types', async () => {
371 |       if (!dockerAvailable) return;
372 | 
373 |       const containerName = generateContainerName('complex-config');
374 |       containers.push(containerName);
375 | 
376 |       // Create complex config
377 |       const configPath = path.join(tempDir, 'config.json');
378 |       const config = {
379 |         server: {
380 |           http: {
381 |             port: 8080,
382 |             host: '0.0.0.0',
383 |             ssl: {
384 |               enabled: true,
385 |               cert_path: '/certs/server.crt'
386 |             }
387 |           }
388 |         },
389 |         features: {
390 |           debug: false,
391 |           metrics: true,
392 |           logging: {
393 |             level: 'info',
394 |             format: 'json'
395 |           }
396 |         },
397 |         limits: {
398 |           max_connections: 100,
399 |           timeout_seconds: 30
400 |         }
401 |       };
402 |       fs.writeFileSync(configPath, JSON.stringify(config));
403 | 
404 |       // Run container and verify all variables
405 |       const { stdout } = await exec(
406 |         `docker run --name ${containerName} -v "${configPath}:/app/config.json:ro" ${imageName} sh -c "env | grep -E '^(SERVER_|FEATURES_|LIMITS_)' | sort"`
407 |       );
408 | 
409 |       const lines = stdout.trim().split('\n');
410 |       const envVars = lines.reduce((acc, line) => {
411 |         const [key, value] = line.split('=');
412 |         acc[key] = value;
413 |         return acc;
414 |       }, {} as Record<string, string>);
415 | 
416 |       // Verify nested values are correctly flattened
417 |       expect(envVars.SERVER_HTTP_PORT).toBe('8080');
418 |       expect(envVars.SERVER_HTTP_HOST).toBe('0.0.0.0');
419 |       expect(envVars.SERVER_HTTP_SSL_ENABLED).toBe('true');
420 |       expect(envVars.SERVER_HTTP_SSL_CERT_PATH).toBe('/certs/server.crt');
421 |       expect(envVars.FEATURES_DEBUG).toBe('false');
422 |       expect(envVars.FEATURES_METRICS).toBe('true');
423 |       expect(envVars.FEATURES_LOGGING_LEVEL).toBe('info');
424 |       expect(envVars.FEATURES_LOGGING_FORMAT).toBe('json');
425 |       expect(envVars.LIMITS_MAX_CONNECTIONS).toBe('100');
426 |       expect(envVars.LIMITS_TIMEOUT_SECONDS).toBe('30');
427 |     });
428 |   });
429 | });
```

--------------------------------------------------------------------------------
/tests/unit/docker/config-security.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, it, expect, beforeEach, afterEach } from 'vitest';
  2 | import { execSync } from 'child_process';
  3 | import fs from 'fs';
  4 | import path from 'path';
  5 | import os from 'os';
  6 | 
  7 | describe('Config File Security Tests', () => {
  8 |   let tempDir: string;
  9 |   let configPath: string;
 10 |   const parseConfigPath = path.resolve(__dirname, '../../../docker/parse-config.js');
 11 |   
 12 |   // Clean environment for tests - only include essential variables
 13 |   const cleanEnv = { 
 14 |     PATH: process.env.PATH, 
 15 |     HOME: process.env.HOME,
 16 |     NODE_ENV: process.env.NODE_ENV 
 17 |   };
 18 | 
 19 |   beforeEach(() => {
 20 |     tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'config-security-test-'));
 21 |     configPath = path.join(tempDir, 'config.json');
 22 |   });
 23 | 
 24 |   afterEach(() => {
 25 |     if (fs.existsSync(tempDir)) {
 26 |       fs.rmSync(tempDir, { recursive: true });
 27 |     }
 28 |   });
 29 | 
 30 |   describe('Command injection prevention', () => {
 31 |     it('should prevent basic command injection attempts', () => {
 32 |       const maliciousConfigs = [
 33 |         { cmd: "'; echo 'hacked' > /tmp/hacked.txt; '" },
 34 |         { cmd: '"; echo "hacked" > /tmp/hacked.txt; "' },
 35 |         { cmd: '`echo hacked > /tmp/hacked.txt`' },
 36 |         { cmd: '$(echo hacked > /tmp/hacked.txt)' },
 37 |         { cmd: '| echo hacked > /tmp/hacked.txt' },
 38 |         { cmd: '|| echo hacked > /tmp/hacked.txt' },
 39 |         { cmd: '& echo hacked > /tmp/hacked.txt' },
 40 |         { cmd: '&& echo hacked > /tmp/hacked.txt' },
 41 |         { cmd: '; echo hacked > /tmp/hacked.txt' },
 42 |         { cmd: '\n echo hacked > /tmp/hacked.txt \n' },
 43 |         { cmd: '\r\n echo hacked > /tmp/hacked.txt \r\n' }
 44 |       ];
 45 | 
 46 |       maliciousConfigs.forEach((config, index) => {
 47 |         fs.writeFileSync(configPath, JSON.stringify(config));
 48 |         const output = execSync(`node "${parseConfigPath}" "${configPath}"`, { 
 49 |           encoding: 'utf8',
 50 |           env: cleanEnv
 51 |         });
 52 |         
 53 |         // The output should safely quote the malicious content
 54 |         expect(output).toContain("export CMD='");
 55 |         
 56 |         // Verify that the output contains a properly quoted export
 57 |         expect(output).toContain("export CMD='");
 58 |         
 59 |         // Create a test script to verify safety
 60 |         const testScript = `#!/bin/sh
 61 | set -e
 62 | ${output}
 63 | # If command injection worked, this would fail
 64 | test -f /tmp/hacked.txt && exit 1
 65 | echo "SUCCESS: No injection occurred"
 66 | `;
 67 |         
 68 |         const tempScript = path.join(tempDir, `test-injection-${index}.sh`);
 69 |         fs.writeFileSync(tempScript, testScript);
 70 |         fs.chmodSync(tempScript, '755');
 71 |         
 72 |         const result = execSync(tempScript, { encoding: 'utf8', env: cleanEnv });
 73 |         expect(result.trim()).toBe('SUCCESS: No injection occurred');
 74 |         
 75 |         // Double-check no files were created
 76 |         expect(fs.existsSync('/tmp/hacked.txt')).toBe(false);
 77 |       });
 78 |     });
 79 | 
 80 |     it('should handle complex nested injection attempts', () => {
 81 |       const config = {
 82 |         database: {
 83 |           host: "localhost'; DROP TABLE users; --",
 84 |           port: 5432,
 85 |           credentials: {
 86 |             password: "$( cat /etc/passwd )",
 87 |             backup_cmd: "`rm -rf /`"
 88 |           }
 89 |         },
 90 |         scripts: {
 91 |           init: "#!/bin/bash\nrm -rf /\nexit 0"
 92 |         }
 93 |       };
 94 |       fs.writeFileSync(configPath, JSON.stringify(config));
 95 | 
 96 |       const output = execSync(`node "${parseConfigPath}" "${configPath}"`, { 
 97 |         encoding: 'utf8',
 98 |         env: cleanEnv
 99 |       });
100 |       
101 |       // All values should be safely quoted
102 |       expect(output).toContain("DATABASE_HOST='localhost'\"'\"'; DROP TABLE users; --'");
103 |       expect(output).toContain("DATABASE_CREDENTIALS_PASSWORD='$( cat /etc/passwd )'");
104 |       expect(output).toContain("DATABASE_CREDENTIALS_BACKUP_CMD='`rm -rf /`'");
105 |       expect(output).toContain("SCRIPTS_INIT='#!/bin/bash\nrm -rf /\nexit 0'");
106 |     });
107 | 
108 |     it('should handle Unicode and special characters safely', () => {
109 |       const config = {
110 |         unicode: "Hello 世界 🌍",
111 |         emoji: "🚀 Deploy! 🎉",
112 |         special: "Line1\nLine2\tTab\rCarriage",
113 |         quotes_mix: `It's a "test" with 'various' quotes`,
114 |         backslash: "C:\\Users\\test\\path",
115 |         regex: "^[a-zA-Z0-9]+$",
116 |         json_string: '{"key": "value"}',
117 |         xml_string: '<tag attr="value">content</tag>',
118 |         sql_injection: "1' OR '1'='1",
119 |         null_byte: "test\x00null",
120 |         escape_sequences: "test\\n\\r\\t\\b\\f"
121 |       };
122 |       fs.writeFileSync(configPath, JSON.stringify(config));
123 | 
124 |       const output = execSync(`node "${parseConfigPath}" "${configPath}"`, { 
125 |         encoding: 'utf8',
126 |         env: cleanEnv
127 |       });
128 |       
129 |       // All special characters should be preserved within quotes
130 |       expect(output).toContain("UNICODE='Hello 世界 🌍'");
131 |       expect(output).toContain("EMOJI='🚀 Deploy! 🎉'");
132 |       expect(output).toContain("SPECIAL='Line1\nLine2\tTab\rCarriage'");
133 |       expect(output).toContain("BACKSLASH='C:\\Users\\test\\path'");
134 |       expect(output).toContain("REGEX='^[a-zA-Z0-9]+$'");
135 |       expect(output).toContain("SQL_INJECTION='1'\"'\"' OR '\"'\"'1'\"'\"'='\"'\"'1'");
136 |     });
137 |   });
138 | 
139 |   describe('Shell metacharacter handling', () => {
140 |     it('should safely handle all shell metacharacters', () => {
141 |       const config = {
142 |         dollar: "$HOME $USER ${PATH}",
143 |         backtick: "`date` `whoami`",
144 |         parentheses: "$(date) $(whoami)",
145 |         semicolon: "cmd1; cmd2; cmd3",
146 |         ampersand: "cmd1 & cmd2 && cmd3",
147 |         pipe: "cmd1 | cmd2 || cmd3",
148 |         redirect: "cmd > file < input >> append",
149 |         glob: "*.txt ?.log [a-z]*",
150 |         tilde: "~/home ~/.config",
151 |         exclamation: "!history !!",
152 |         question: "file? test?",
153 |         asterisk: "*.* *",
154 |         brackets: "[abc] [0-9]",
155 |         braces: "{a,b,c} ${var}",
156 |         caret: "^pattern^replacement^",
157 |         hash: "#comment # another",
158 |         at: "@variable @{array}"
159 |       };
160 |       fs.writeFileSync(configPath, JSON.stringify(config));
161 | 
162 |       const output = execSync(`node "${parseConfigPath}" "${configPath}"`, { 
163 |         encoding: 'utf8',
164 |         env: cleanEnv
165 |       });
166 |       
167 |       // Verify all metacharacters are safely quoted
168 |       const lines = output.trim().split('\n');
169 |       lines.forEach(line => {
170 |         // Each line should be in the format: export KEY='value'
171 |         expect(line).toMatch(/^export [A-Z_]+='.*'$/);
172 |       });
173 |       
174 |       // Test that the values are safe when evaluated
175 |       const testScript = `
176 | #!/bin/sh
177 | set -e
178 | ${output}
179 | # If any metacharacters were unescaped, these would fail
180 | test "\$DOLLAR" = '\$HOME \$USER \${PATH}'
181 | test "\$BACKTICK" = '\`date\` \`whoami\`'
182 | test "\$PARENTHESES" = '\$(date) \$(whoami)'
183 | test "\$SEMICOLON" = 'cmd1; cmd2; cmd3'
184 | test "\$PIPE" = 'cmd1 | cmd2 || cmd3'
185 | echo "SUCCESS: All metacharacters safely contained"
186 | `;
187 |       
188 |       const tempScript = path.join(tempDir, 'test-metachar.sh');
189 |       fs.writeFileSync(tempScript, testScript);
190 |       fs.chmodSync(tempScript, '755');
191 |       
192 |       const result = execSync(tempScript, { encoding: 'utf8', env: cleanEnv });
193 |       expect(result.trim()).toBe('SUCCESS: All metacharacters safely contained');
194 |     });
195 |   });
196 | 
197 |   describe('Escaping edge cases', () => {
198 |     it('should handle consecutive single quotes', () => {
199 |       const config = {
200 |         test1: "'''",
201 |         test2: "It'''s",
202 |         test3: "start'''middle'''end",
203 |         test4: "''''''''",
204 |       };
205 |       fs.writeFileSync(configPath, JSON.stringify(config));
206 | 
207 |       const output = execSync(`node "${parseConfigPath}" "${configPath}"`, { 
208 |         encoding: 'utf8',
209 |         env: cleanEnv
210 |       });
211 |       
212 |       // Verify the escaping is correct
213 |       expect(output).toContain(`TEST1=''"'"''"'"''"'"'`);
214 |       expect(output).toContain(`TEST2='It'"'"''"'"''"'"'s'`);
215 |     });
216 | 
217 |     it('should handle empty and whitespace-only values', () => {
218 |       const config = {
219 |         empty: "",
220 |         space: " ",
221 |         spaces: "   ",
222 |         tab: "\t",
223 |         newline: "\n",
224 |         mixed_whitespace: " \t\n\r "
225 |       };
226 |       fs.writeFileSync(configPath, JSON.stringify(config));
227 | 
228 |       const output = execSync(`node "${parseConfigPath}" "${configPath}"`, { 
229 |         encoding: 'utf8',
230 |         env: cleanEnv
231 |       });
232 |       
233 |       expect(output).toContain("EMPTY=''");
234 |       expect(output).toContain("SPACE=' '");
235 |       expect(output).toContain("SPACES='   '");
236 |       expect(output).toContain("TAB='\t'");
237 |       expect(output).toContain("NEWLINE='\n'");
238 |       expect(output).toContain("MIXED_WHITESPACE=' \t\n\r '");
239 |     });
240 | 
241 |     it('should handle very long values', () => {
242 |       const longString = 'a'.repeat(10000) + "'; echo 'injection'; '" + 'b'.repeat(10000);
243 |       const config = {
244 |         long_value: longString
245 |       };
246 |       fs.writeFileSync(configPath, JSON.stringify(config));
247 | 
248 |       const output = execSync(`node "${parseConfigPath}" "${configPath}"`, { 
249 |         encoding: 'utf8',
250 |         env: cleanEnv
251 |       });
252 |       
253 |       expect(output).toContain('LONG_VALUE=');
254 |       expect(output.length).toBeGreaterThan(20000);
255 |       // The injection attempt should be safely quoted
256 |       expect(output).toContain("'\"'\"'; echo '\"'\"'injection'\"'\"'; '\"'\"'");
257 |     });
258 |   });
259 | 
260 |   describe('Environment variable name security', () => {
261 |     it('should handle potentially dangerous key names', () => {
262 |       const config = {
263 |         "PATH": "should-not-override",
264 |         "LD_PRELOAD": "dangerous",
265 |         "valid_key": "safe_value",
266 |         "123invalid": "should-be-skipped",
267 |         "key-with-dash": "should-work",
268 |         "key.with.dots": "should-work",
269 |         "KEY WITH SPACES": "should-work"
270 |       };
271 |       fs.writeFileSync(configPath, JSON.stringify(config));
272 | 
273 |       const output = execSync(`node "${parseConfigPath}" "${configPath}"`, { 
274 |         encoding: 'utf8',
275 |         env: cleanEnv
276 |       });
277 |       
278 |       // Dangerous variables should be blocked
279 |       expect(output).not.toContain("export PATH=");
280 |       expect(output).not.toContain("export LD_PRELOAD=");
281 |       
282 |       // Valid keys should be converted to safe names
283 |       expect(output).toContain("export VALID_KEY='safe_value'");
284 |       expect(output).toContain("export KEY_WITH_DASH='should-work'");
285 |       expect(output).toContain("export KEY_WITH_DOTS='should-work'");
286 |       expect(output).toContain("export KEY_WITH_SPACES='should-work'");
287 |       
288 |       // Invalid starting with number should be prefixed with _
289 |       expect(output).toContain("export _123INVALID='should-be-skipped'");
290 |     });
291 |   });
292 | 
293 |   describe('Real-world attack scenarios', () => {
294 |     it('should prevent path traversal attempts', () => {
295 |       const config = {
296 |         file_path: "../../../etc/passwd",
297 |         backup_location: "../../../../../../tmp/evil",
298 |         template: "${../../secret.key}",
299 |         include: "<?php include('/etc/passwd'); ?>"
300 |       };
301 |       fs.writeFileSync(configPath, JSON.stringify(config));
302 | 
303 |       const output = execSync(`node "${parseConfigPath}" "${configPath}"`, { 
304 |         encoding: 'utf8',
305 |         env: cleanEnv
306 |       });
307 |       
308 |       // Path traversal attempts should be preserved as strings, not resolved
309 |       expect(output).toContain("FILE_PATH='../../../etc/passwd'");
310 |       expect(output).toContain("BACKUP_LOCATION='../../../../../../tmp/evil'");
311 |       expect(output).toContain("TEMPLATE='${../../secret.key}'");
312 |       expect(output).toContain("INCLUDE='<?php include('\"'\"'/etc/passwd'\"'\"'); ?>'");
313 |     });
314 | 
315 |     it('should handle polyglot payloads safely', () => {
316 |       const config = {
317 |         // JavaScript/Shell polyglot
318 |         polyglot1: "';alert(String.fromCharCode(88,83,83))//';alert(String.fromCharCode(88,83,83))//\";alert(String.fromCharCode(88,83,83))//\";alert(String.fromCharCode(88,83,83))//--></SCRIPT>\">'><SCRIPT>alert(String.fromCharCode(88,83,83))</SCRIPT>",
319 |         // SQL/Shell polyglot
320 |         polyglot2: "1' OR '1'='1' /*' or 1=1 # ' or 1=1-- ' or 1=1;--",
321 |         // XML/Shell polyglot
322 |         polyglot3: "<?xml version=\"1.0\"?><!DOCTYPE foo [<!ENTITY xxe SYSTEM \"file:///etc/passwd\">]><foo>&xxe;</foo>"
323 |       };
324 |       fs.writeFileSync(configPath, JSON.stringify(config));
325 | 
326 |       const output = execSync(`node "${parseConfigPath}" "${configPath}"`, { 
327 |         encoding: 'utf8',
328 |         env: cleanEnv
329 |       });
330 |       
331 |       // All polyglot payloads should be safely quoted
332 |       const lines = output.trim().split('\n');
333 |       lines.forEach(line => {
334 |         if (line.startsWith('export POLYGLOT')) {
335 |           // Should be safely wrapped in single quotes with proper escaping
336 |           expect(line).toMatch(/^export POLYGLOT[0-9]='.*'$/);
337 |           // The dangerous content is there but safely quoted
338 |           // What matters is that when evaluated, it's just a string
339 |         }
340 |       });
341 |     });
342 |   });
343 | 
344 |   describe('Stress testing', () => {
345 |     it('should handle deeply nested malicious structures', () => {
346 |       const createNestedMalicious = (depth: number): any => {
347 |         if (depth === 0) {
348 |           return "'; rm -rf /; '";
349 |         }
350 |         return {
351 |           [`level${depth}`]: createNestedMalicious(depth - 1),
352 |           [`inject${depth}`]: "$( echo 'level " + depth + "' )"
353 |         };
354 |       };
355 | 
356 |       const config = createNestedMalicious(10);
357 |       fs.writeFileSync(configPath, JSON.stringify(config));
358 | 
359 |       const output = execSync(`node "${parseConfigPath}" "${configPath}"`, { 
360 |         encoding: 'utf8',
361 |         env: cleanEnv
362 |       });
363 |       
364 |       // Should handle deep nesting without issues
365 |       expect(output).toContain("LEVEL10_LEVEL9_LEVEL8");
366 |       expect(output).toContain("'\"'\"'; rm -rf /; '\"'\"'");
367 |       
368 |       // All injection attempts should be quoted
369 |       const lines = output.trim().split('\n');
370 |       lines.forEach(line => {
371 |         if (line.includes('INJECT')) {
372 |           expect(line).toContain("$( echo '\"'\"'level");
373 |         }
374 |       });
375 |     });
376 | 
377 |     it('should handle mixed attack vectors in single config', () => {
378 |       const config = {
379 |         normal_value: "This is safe",
380 |         sql_injection: "1' OR '1'='1",
381 |         cmd_injection: "; cat /etc/passwd",
382 |         xxe_attempt: '<!ENTITY xxe SYSTEM "file:///etc/passwd">',
383 |         code_injection: "${constructor.constructor('return process')().exit()}",
384 |         format_string: "%s%s%s%s%s%s%s%s%s%s",
385 |         buffer_overflow: "A".repeat(10000),
386 |         null_injection: "test\x00admin",
387 |         ldap_injection: "*)(&(1=1",
388 |         xpath_injection: "' or '1'='1",
389 |         template_injection: "{{7*7}}",
390 |         ssti: "${7*7}",
391 |         crlf_injection: "test\r\nSet-Cookie: admin=true",
392 |         host_header: "evil.com\r\nX-Forwarded-Host: evil.com",
393 |         cache_poisoning: "index.html%0d%0aContent-Length:%200%0d%0a%0d%0aHTTP/1.1%20200%20OK"
394 |       };
395 |       fs.writeFileSync(configPath, JSON.stringify(config));
396 | 
397 |       const output = execSync(`node "${parseConfigPath}" "${configPath}"`, { 
398 |         encoding: 'utf8',
399 |         env: cleanEnv
400 |       });
401 |       
402 |       // Verify each attack vector is safely handled
403 |       expect(output).toContain("NORMAL_VALUE='This is safe'");
404 |       expect(output).toContain("SQL_INJECTION='1'\"'\"' OR '\"'\"'1'\"'\"'='\"'\"'1'");
405 |       expect(output).toContain("CMD_INJECTION='; cat /etc/passwd'");
406 |       expect(output).toContain("XXE_ATTEMPT='<!ENTITY xxe SYSTEM \"file:///etc/passwd\">'");
407 |       expect(output).toContain("CODE_INJECTION='${constructor.constructor('\"'\"'return process'\"'\"')().exit()}'");
408 |       
409 |       // Verify no actual code execution occurs
410 |       const evalTest = `${output}\necho "Test completed successfully"`;
411 |       const result = execSync(evalTest, { shell: '/bin/sh', encoding: 'utf8' });
412 |       expect(result).toContain("Test completed successfully");
413 |     });
414 |   });
415 | });
```

--------------------------------------------------------------------------------
/tests/unit/mcp/get-node-essentials-examples.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
  2 | import { N8NDocumentationMCPServer } from '../../../src/mcp/server';
  3 | 
  4 | /**
  5 |  * Unit tests for get_node_essentials with includeExamples parameter
  6 |  * Testing P0-R3 feature: Template-based configuration examples with metadata
  7 |  */
  8 | 
  9 | describe('get_node_essentials with includeExamples', () => {
 10 |   let server: N8NDocumentationMCPServer;
 11 | 
 12 |   beforeEach(async () => {
 13 |     process.env.NODE_DB_PATH = ':memory:';
 14 |     server = new N8NDocumentationMCPServer();
 15 |     await (server as any).initialized;
 16 | 
 17 |     // Populate in-memory database with test nodes
 18 |     // NOTE: Database stores nodes in SHORT form (nodes-base.xxx, not n8n-nodes-base.xxx)
 19 |     const testNodes = [
 20 |       {
 21 |         node_type: 'nodes-base.httpRequest',
 22 |         package_name: 'n8n-nodes-base',
 23 |         display_name: 'HTTP Request',
 24 |         description: 'Makes an HTTP request',
 25 |         category: 'Core Nodes',
 26 |         is_ai_tool: 0,
 27 |         is_trigger: 0,
 28 |         is_webhook: 0,
 29 |         is_versioned: 1,
 30 |         version: '1',
 31 |         properties_schema: JSON.stringify([]),
 32 |         operations: JSON.stringify([])
 33 |       },
 34 |       {
 35 |         node_type: 'nodes-base.webhook',
 36 |         package_name: 'n8n-nodes-base',
 37 |         display_name: 'Webhook',
 38 |         description: 'Starts workflow on webhook call',
 39 |         category: 'Core Nodes',
 40 |         is_ai_tool: 0,
 41 |         is_trigger: 1,
 42 |         is_webhook: 1,
 43 |         is_versioned: 1,
 44 |         version: '1',
 45 |         properties_schema: JSON.stringify([]),
 46 |         operations: JSON.stringify([])
 47 |       },
 48 |       {
 49 |         node_type: 'nodes-base.test',
 50 |         package_name: 'n8n-nodes-base',
 51 |         display_name: 'Test Node',
 52 |         description: 'Test node for examples',
 53 |         category: 'Core Nodes',
 54 |         is_ai_tool: 0,
 55 |         is_trigger: 0,
 56 |         is_webhook: 0,
 57 |         is_versioned: 1,
 58 |         version: '1',
 59 |         properties_schema: JSON.stringify([]),
 60 |         operations: JSON.stringify([])
 61 |       }
 62 |     ];
 63 | 
 64 |     // Insert test nodes into the in-memory database
 65 |     const db = (server as any).db;
 66 |     if (db) {
 67 |       const insertStmt = db.prepare(`
 68 |         INSERT INTO nodes (
 69 |           node_type, package_name, display_name, description, category,
 70 |           is_ai_tool, is_trigger, is_webhook, is_versioned, version,
 71 |           properties_schema, operations
 72 |         ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
 73 |       `);
 74 | 
 75 |       for (const node of testNodes) {
 76 |         insertStmt.run(
 77 |           node.node_type,
 78 |           node.package_name,
 79 |           node.display_name,
 80 |           node.description,
 81 |           node.category,
 82 |           node.is_ai_tool,
 83 |           node.is_trigger,
 84 |           node.is_webhook,
 85 |           node.is_versioned,
 86 |           node.version,
 87 |           node.properties_schema,
 88 |           node.operations
 89 |         );
 90 |       }
 91 |     }
 92 |   });
 93 | 
 94 |   afterEach(() => {
 95 |     delete process.env.NODE_DB_PATH;
 96 |   });
 97 | 
 98 |   describe('includeExamples parameter', () => {
 99 |     it('should not include examples when includeExamples is false', async () => {
100 |       const result = await (server as any).getNodeEssentials('nodes-base.httpRequest', false);
101 | 
102 |       expect(result).toBeDefined();
103 |       expect(result.examples).toBeUndefined();
104 |     });
105 | 
106 |     it('should not include examples when includeExamples is undefined', async () => {
107 |       const result = await (server as any).getNodeEssentials('nodes-base.httpRequest', undefined);
108 | 
109 |       expect(result).toBeDefined();
110 |       expect(result.examples).toBeUndefined();
111 |     });
112 | 
113 |     it('should include examples when includeExamples is true', async () => {
114 |       const result = await (server as any).getNodeEssentials('nodes-base.httpRequest', true);
115 | 
116 |       expect(result).toBeDefined();
117 |       // Note: In-memory test database may not have template configs
118 |       // This test validates the parameter is processed correctly
119 |     });
120 | 
121 |     it('should limit examples to top 3 per node', async () => {
122 |       const result = await (server as any).getNodeEssentials('nodes-base.webhook', true);
123 | 
124 |       expect(result).toBeDefined();
125 |       if (result.examples) {
126 |         expect(result.examples.length).toBeLessThanOrEqual(3);
127 |       }
128 |     });
129 |   });
130 | 
131 |   describe('example data structure with metadata', () => {
132 |     it('should return examples with full metadata structure', async () => {
133 |       // Mock database to return example data with metadata
134 |       const mockDb = (server as any).db;
135 |       if (mockDb) {
136 |         const originalPrepare = mockDb.prepare.bind(mockDb);
137 |         mockDb.prepare = vi.fn((query: string) => {
138 |           if (query.includes('template_node_configs')) {
139 |             return {
140 |               all: vi.fn(() => [
141 |                 {
142 |                   parameters_json: JSON.stringify({
143 |                     httpMethod: 'POST',
144 |                     path: 'webhook-test',
145 |                     responseMode: 'lastNode'
146 |                   }),
147 |                   template_name: 'Webhook Template',
148 |                   template_views: 2000,
149 |                   complexity: 'simple',
150 |                   use_cases: JSON.stringify(['webhook processing', 'API integration']),
151 |                   has_credentials: 0,
152 |                   has_expressions: 1
153 |                 }
154 |               ])
155 |             };
156 |           }
157 |           return originalPrepare(query);
158 |         });
159 | 
160 |         const result = await (server as any).getNodeEssentials('nodes-base.webhook', true);
161 | 
162 |         if (result.examples && result.examples.length > 0) {
163 |           const example = result.examples[0];
164 | 
165 |           // Verify structure
166 |           expect(example).toHaveProperty('configuration');
167 |           expect(example).toHaveProperty('source');
168 |           expect(example).toHaveProperty('useCases');
169 |           expect(example).toHaveProperty('metadata');
170 | 
171 |           // Verify source structure
172 |           expect(example.source).toHaveProperty('template');
173 |           expect(example.source).toHaveProperty('views');
174 |           expect(example.source).toHaveProperty('complexity');
175 | 
176 |           // Verify metadata structure
177 |           expect(example.metadata).toHaveProperty('hasCredentials');
178 |           expect(example.metadata).toHaveProperty('hasExpressions');
179 | 
180 |           // Verify types
181 |           expect(typeof example.configuration).toBe('object');
182 |           expect(typeof example.source.template).toBe('string');
183 |           expect(typeof example.source.views).toBe('number');
184 |           expect(typeof example.source.complexity).toBe('string');
185 |           expect(Array.isArray(example.useCases)).toBe(true);
186 |           expect(typeof example.metadata.hasCredentials).toBe('boolean');
187 |           expect(typeof example.metadata.hasExpressions).toBe('boolean');
188 |         }
189 |       }
190 |     });
191 | 
192 |     it('should include complexity in source metadata', async () => {
193 |       const mockDb = (server as any).db;
194 |       if (mockDb) {
195 |         const originalPrepare = mockDb.prepare.bind(mockDb);
196 |         mockDb.prepare = vi.fn((query: string) => {
197 |           if (query.includes('template_node_configs')) {
198 |             return {
199 |               all: vi.fn(() => [
200 |                 {
201 |                   parameters_json: JSON.stringify({ url: 'https://api.example.com' }),
202 |                   template_name: 'Simple HTTP Request',
203 |                   template_views: 500,
204 |                   complexity: 'simple',
205 |                   use_cases: JSON.stringify([]),
206 |                   has_credentials: 0,
207 |                   has_expressions: 0
208 |                 },
209 |                 {
210 |                   parameters_json: JSON.stringify({
211 |                     url: '={{ $json.url }}',
212 |                     options: { timeout: 30000 }
213 |                   }),
214 |                   template_name: 'Complex HTTP Request',
215 |                   template_views: 300,
216 |                   complexity: 'complex',
217 |                   use_cases: JSON.stringify(['advanced API calls']),
218 |                   has_credentials: 1,
219 |                   has_expressions: 1
220 |                 }
221 |               ])
222 |             };
223 |           }
224 |           return originalPrepare(query);
225 |         });
226 | 
227 |         const result = await (server as any).getNodeEssentials('nodes-base.httpRequest', true);
228 | 
229 |         if (result.examples && result.examples.length >= 2) {
230 |           expect(result.examples[0].source.complexity).toBe('simple');
231 |           expect(result.examples[1].source.complexity).toBe('complex');
232 |         }
233 |       }
234 |     });
235 | 
236 |     it('should limit use cases to 2 items', async () => {
237 |       const mockDb = (server as any).db;
238 |       if (mockDb) {
239 |         const originalPrepare = mockDb.prepare.bind(mockDb);
240 |         mockDb.prepare = vi.fn((query: string) => {
241 |           if (query.includes('template_node_configs')) {
242 |             return {
243 |               all: vi.fn(() => [
244 |                 {
245 |                   parameters_json: JSON.stringify({}),
246 |                   template_name: 'Test Template',
247 |                   template_views: 100,
248 |                   complexity: 'medium',
249 |                   use_cases: JSON.stringify([
250 |                     'use case 1',
251 |                     'use case 2',
252 |                     'use case 3',
253 |                     'use case 4'
254 |                   ]),
255 |                   has_credentials: 0,
256 |                   has_expressions: 0
257 |                 }
258 |               ])
259 |             };
260 |           }
261 |           return originalPrepare(query);
262 |         });
263 | 
264 |         const result = await (server as any).getNodeEssentials('nodes-base.test', true);
265 | 
266 |         if (result.examples && result.examples.length > 0) {
267 |           expect(result.examples[0].useCases.length).toBeLessThanOrEqual(2);
268 |         }
269 |       }
270 |     });
271 | 
272 |     it('should handle empty use_cases gracefully', async () => {
273 |       const mockDb = (server as any).db;
274 |       if (mockDb) {
275 |         const originalPrepare = mockDb.prepare.bind(mockDb);
276 |         mockDb.prepare = vi.fn((query: string) => {
277 |           if (query.includes('template_node_configs')) {
278 |             return {
279 |               all: vi.fn(() => [
280 |                 {
281 |                   parameters_json: JSON.stringify({}),
282 |                   template_name: 'Test Template',
283 |                   template_views: 100,
284 |                   complexity: 'medium',
285 |                   use_cases: null,
286 |                   has_credentials: 0,
287 |                   has_expressions: 0
288 |                 }
289 |               ])
290 |             };
291 |           }
292 |           return originalPrepare(query);
293 |         });
294 | 
295 |         const result = await (server as any).getNodeEssentials('nodes-base.test', true);
296 | 
297 |         if (result.examples && result.examples.length > 0) {
298 |           expect(result.examples[0].useCases).toEqual([]);
299 |         }
300 |       }
301 |     });
302 |   });
303 | 
304 |   describe('caching behavior with includeExamples', () => {
305 |     it('should use different cache keys for with/without examples', async () => {
306 |       const cache = (server as any).cache;
307 |       const cacheGetSpy = vi.spyOn(cache, 'get');
308 | 
309 |       // First call without examples
310 |       await (server as any).getNodeEssentials('nodes-base.httpRequest', false);
311 |       expect(cacheGetSpy).toHaveBeenCalledWith(expect.stringContaining('basic'));
312 | 
313 |       // Second call with examples
314 |       await (server as any).getNodeEssentials('nodes-base.httpRequest', true);
315 |       expect(cacheGetSpy).toHaveBeenCalledWith(expect.stringContaining('withExamples'));
316 |     });
317 | 
318 |     it('should cache results separately for different includeExamples values', async () => {
319 |       // Call with examples
320 |       const resultWithExamples1 = await (server as any).getNodeEssentials('nodes-base.httpRequest', true);
321 | 
322 |       // Call without examples
323 |       const resultWithoutExamples = await (server as any).getNodeEssentials('nodes-base.httpRequest', false);
324 | 
325 |       // Call with examples again (should be cached)
326 |       const resultWithExamples2 = await (server as any).getNodeEssentials('nodes-base.httpRequest', true);
327 | 
328 |       // Results with examples should match
329 |       expect(resultWithExamples1).toEqual(resultWithExamples2);
330 | 
331 |       // Result without examples should not have examples
332 |       expect(resultWithoutExamples.examples).toBeUndefined();
333 |     });
334 |   });
335 | 
336 |   describe('backward compatibility', () => {
337 |     it('should maintain backward compatibility when includeExamples not specified', async () => {
338 |       const result = await (server as any).getNodeEssentials('nodes-base.httpRequest');
339 | 
340 |       expect(result).toBeDefined();
341 |       expect(result.nodeType).toBeDefined();
342 |       expect(result.displayName).toBeDefined();
343 |       expect(result.examples).toBeUndefined();
344 |     });
345 | 
346 |     it('should return same core data regardless of includeExamples value', async () => {
347 |       const resultWithout = await (server as any).getNodeEssentials('nodes-base.httpRequest', false);
348 |       const resultWith = await (server as any).getNodeEssentials('nodes-base.httpRequest', true);
349 | 
350 |       // Core fields should be identical
351 |       expect(resultWithout.nodeType).toBe(resultWith.nodeType);
352 |       expect(resultWithout.displayName).toBe(resultWith.displayName);
353 |       expect(resultWithout.description).toBe(resultWith.description);
354 |     });
355 |   });
356 | 
357 |   describe('error handling', () => {
358 |     it('should continue to work even if example fetch fails', async () => {
359 |       const mockDb = (server as any).db;
360 |       if (mockDb) {
361 |         const originalPrepare = mockDb.prepare.bind(mockDb);
362 |         mockDb.prepare = vi.fn((query: string) => {
363 |           if (query.includes('template_node_configs')) {
364 |             throw new Error('Database error');
365 |           }
366 |           return originalPrepare(query);
367 |         });
368 | 
369 |         // Should not throw
370 |         const result = await (server as any).getNodeEssentials('nodes-base.webhook', true);
371 | 
372 |         expect(result).toBeDefined();
373 |         expect(result.nodeType).toBeDefined();
374 |         // Examples should be empty array due to error (fallback behavior)
375 |         expect(result.examples).toEqual([]);
376 |         expect(result.examplesCount).toBe(0);
377 |       }
378 |     });
379 | 
380 |     it('should handle malformed JSON in template configs gracefully', async () => {
381 |       const mockDb = (server as any).db;
382 |       if (mockDb) {
383 |         const originalPrepare = mockDb.prepare.bind(mockDb);
384 |         mockDb.prepare = vi.fn((query: string) => {
385 |           if (query.includes('template_node_configs')) {
386 |             return {
387 |               all: vi.fn(() => [
388 |                 {
389 |                   parameters_json: 'invalid json',
390 |                   template_name: 'Test',
391 |                   template_views: 100,
392 |                   complexity: 'medium',
393 |                   use_cases: 'also invalid',
394 |                   has_credentials: 0,
395 |                   has_expressions: 0
396 |                 }
397 |               ])
398 |             };
399 |           }
400 |           return originalPrepare(query);
401 |         });
402 | 
403 |         // Should not throw
404 |         const result = await (server as any).getNodeEssentials('nodes-base.test', true);
405 |         expect(result).toBeDefined();
406 |       }
407 |     });
408 |   });
409 | 
410 |   describe('performance', () => {
411 |     it('should complete in reasonable time with examples', async () => {
412 |       const start = Date.now();
413 |       await (server as any).getNodeEssentials('nodes-base.httpRequest', true);
414 |       const duration = Date.now() - start;
415 | 
416 |       // Should complete under 100ms
417 |       expect(duration).toBeLessThan(100);
418 |     });
419 | 
420 |     it('should not add significant overhead when includeExamples is false', async () => {
421 |       const startWithout = Date.now();
422 |       await (server as any).getNodeEssentials('nodes-base.httpRequest', false);
423 |       const durationWithout = Date.now() - startWithout;
424 | 
425 |       const startWith = Date.now();
426 |       await (server as any).getNodeEssentials('nodes-base.httpRequest', true);
427 |       const durationWith = Date.now() - startWith;
428 | 
429 |       // Both should be fast
430 |       expect(durationWithout).toBeLessThan(50);
431 |       expect(durationWith).toBeLessThan(100);
432 |     });
433 |   });
434 | });
435 | 
```
Page 23/60FirstPrevNextLast