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

# Directory Structure

```
├── _config.yml
├── .claude
│   └── agents
│       ├── code-reviewer.md
│       ├── context-manager.md
│       ├── debugger.md
│       ├── deployment-engineer.md
│       ├── mcp-backend-engineer.md
│       ├── n8n-mcp-tester.md
│       ├── technical-researcher.md
│       └── test-automator.md
├── .dockerignore
├── .env.docker
├── .env.example
├── .env.n8n.example
├── .env.test
├── .env.test.example
├── .github
│   ├── ABOUT.md
│   ├── BENCHMARK_THRESHOLDS.md
│   ├── FUNDING.yml
│   ├── gh-pages.yml
│   ├── secret_scanning.yml
│   └── workflows
│       ├── benchmark-pr.yml
│       ├── benchmark.yml
│       ├── docker-build-fast.yml
│       ├── docker-build-n8n.yml
│       ├── docker-build.yml
│       ├── release.yml
│       ├── test.yml
│       └── update-n8n-deps.yml
├── .gitignore
├── .npmignore
├── ATTRIBUTION.md
├── CHANGELOG.md
├── CLAUDE.md
├── codecov.yml
├── coverage.json
├── data
│   ├── .gitkeep
│   ├── nodes.db
│   ├── nodes.db-shm
│   ├── nodes.db-wal
│   └── templates.db
├── deploy
│   └── quick-deploy-n8n.sh
├── docker
│   ├── docker-entrypoint.sh
│   ├── n8n-mcp
│   ├── parse-config.js
│   └── README.md
├── docker-compose.buildkit.yml
├── docker-compose.extract.yml
├── docker-compose.n8n.yml
├── docker-compose.override.yml.example
├── docker-compose.test-n8n.yml
├── docker-compose.yml
├── Dockerfile
├── Dockerfile.railway
├── Dockerfile.test
├── docs
│   ├── AUTOMATED_RELEASES.md
│   ├── BENCHMARKS.md
│   ├── CHANGELOG.md
│   ├── 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
│       ├── fixed-collection-validator.ts
│       ├── logger.ts
│       ├── mcp-client.ts
│       ├── n8n-errors.ts
│       ├── node-source-extractor.ts
│       ├── node-type-normalizer.ts
│       ├── node-type-utils.ts
│       ├── node-utils.ts
│       ├── npm-version-checker.ts
│       ├── protocol-version.ts
│       ├── simple-cache.ts
│       ├── ssrf-protection.ts
│       ├── template-node-resolver.ts
│       ├── template-sanitizer.ts
│       ├── url-detector.ts
│       ├── validation-schemas.ts
│       └── version.ts
├── test-output.txt
├── test-reinit-fix.sh
├── tests
│   ├── __snapshots__
│   │   └── .gitkeep
│   ├── auth.test.ts
│   ├── benchmarks
│   │   ├── database-queries.bench.ts
│   │   ├── index.ts
│   │   ├── mcp-tools.bench.ts
│   │   ├── mcp-tools.bench.ts.disabled
│   │   ├── mcp-tools.bench.ts.skip
│   │   ├── node-loading.bench.ts.disabled
│   │   ├── README.md
│   │   ├── search-operations.bench.ts.disabled
│   │   └── validation-performance.bench.ts.disabled
│   ├── bridge.test.ts
│   ├── comprehensive-extraction-test.js
│   ├── data
│   │   └── .gitkeep
│   ├── debug-slack-doc.js
│   ├── demo-enhanced-documentation.js
│   ├── docker-tests-README.md
│   ├── error-handler.test.ts
│   ├── examples
│   │   └── using-database-utils.test.ts
│   ├── extracted-nodes-db
│   │   ├── database-import.json
│   │   ├── extraction-report.json
│   │   ├── insert-nodes.sql
│   │   ├── n8n-nodes-base__Airtable.json
│   │   ├── n8n-nodes-base__Discord.json
│   │   ├── n8n-nodes-base__Function.json
│   │   ├── n8n-nodes-base__HttpRequest.json
│   │   ├── n8n-nodes-base__If.json
│   │   ├── n8n-nodes-base__Slack.json
│   │   ├── n8n-nodes-base__SplitInBatches.json
│   │   └── n8n-nodes-base__Webhook.json
│   ├── factories
│   │   ├── node-factory.ts
│   │   └── property-definition-factory.ts
│   ├── fixtures
│   │   ├── .gitkeep
│   │   ├── database
│   │   │   └── test-nodes.json
│   │   ├── factories
│   │   │   ├── node.factory.ts
│   │   │   └── parser-node.factory.ts
│   │   └── template-configs.ts
│   ├── helpers
│   │   └── env-helpers.ts
│   ├── http-server-auth.test.ts
│   ├── integration
│   │   ├── ai-validation
│   │   │   ├── ai-agent-validation.test.ts
│   │   │   ├── ai-tool-validation.test.ts
│   │   │   ├── chat-trigger-validation.test.ts
│   │   │   ├── e2e-validation.test.ts
│   │   │   ├── helpers.ts
│   │   │   ├── llm-chain-validation.test.ts
│   │   │   ├── README.md
│   │   │   └── TEST_REPORT.md
│   │   ├── ci
│   │   │   └── database-population.test.ts
│   │   ├── database
│   │   │   ├── connection-management.test.ts
│   │   │   ├── empty-database.test.ts
│   │   │   ├── fts5-search.test.ts
│   │   │   ├── node-fts5-search.test.ts
│   │   │   ├── node-repository.test.ts
│   │   │   ├── performance.test.ts
│   │   │   ├── sqljs-memory-leak.test.ts
│   │   │   ├── template-node-configs.test.ts
│   │   │   ├── template-repository.test.ts
│   │   │   ├── test-utils.ts
│   │   │   └── transactions.test.ts
│   │   ├── database-integration.test.ts
│   │   ├── docker
│   │   │   ├── docker-config.test.ts
│   │   │   ├── docker-entrypoint.test.ts
│   │   │   └── test-helpers.ts
│   │   ├── flexible-instance-config.test.ts
│   │   ├── mcp
│   │   │   └── template-examples-e2e.test.ts
│   │   ├── mcp-protocol
│   │   │   ├── basic-connection.test.ts
│   │   │   ├── error-handling.test.ts
│   │   │   ├── performance.test.ts
│   │   │   ├── protocol-compliance.test.ts
│   │   │   ├── README.md
│   │   │   ├── session-management.test.ts
│   │   │   ├── test-helpers.ts
│   │   │   ├── tool-invocation.test.ts
│   │   │   └── workflow-error-validation.test.ts
│   │   ├── msw-setup.test.ts
│   │   ├── n8n-api
│   │   │   ├── executions
│   │   │   │   ├── delete-execution.test.ts
│   │   │   │   ├── get-execution.test.ts
│   │   │   │   ├── list-executions.test.ts
│   │   │   │   └── trigger-webhook.test.ts
│   │   │   ├── scripts
│   │   │   │   └── cleanup-orphans.ts
│   │   │   ├── system
│   │   │   │   ├── diagnostic.test.ts
│   │   │   │   ├── health-check.test.ts
│   │   │   │   └── list-tools.test.ts
│   │   │   ├── test-connection.ts
│   │   │   ├── types
│   │   │   │   └── mcp-responses.ts
│   │   │   ├── utils
│   │   │   │   ├── cleanup-helpers.ts
│   │   │   │   ├── credentials.ts
│   │   │   │   ├── factories.ts
│   │   │   │   ├── fixtures.ts
│   │   │   │   ├── mcp-context.ts
│   │   │   │   ├── n8n-client.ts
│   │   │   │   ├── node-repository.ts
│   │   │   │   ├── response-types.ts
│   │   │   │   ├── test-context.ts
│   │   │   │   └── webhook-workflows.ts
│   │   │   └── workflows
│   │   │       ├── autofix-workflow.test.ts
│   │   │       ├── create-workflow.test.ts
│   │   │       ├── delete-workflow.test.ts
│   │   │       ├── get-workflow-details.test.ts
│   │   │       ├── get-workflow-minimal.test.ts
│   │   │       ├── get-workflow-structure.test.ts
│   │   │       ├── get-workflow.test.ts
│   │   │       ├── list-workflows.test.ts
│   │   │       ├── smart-parameters.test.ts
│   │   │       ├── update-partial-workflow.test.ts
│   │   │       ├── update-workflow.test.ts
│   │   │       └── validate-workflow.test.ts
│   │   ├── security
│   │   │   ├── command-injection-prevention.test.ts
│   │   │   └── rate-limiting.test.ts
│   │   ├── setup
│   │   │   ├── integration-setup.ts
│   │   │   └── msw-test-server.ts
│   │   ├── telemetry
│   │   │   ├── docker-user-id-stability.test.ts
│   │   │   └── mcp-telemetry.test.ts
│   │   ├── templates
│   │   │   └── metadata-operations.test.ts
│   │   └── workflow-creation-node-type-format.test.ts
│   ├── logger.test.ts
│   ├── MOCKING_STRATEGY.md
│   ├── mocks
│   │   ├── n8n-api
│   │   │   ├── data
│   │   │   │   ├── credentials.ts
│   │   │   │   ├── executions.ts
│   │   │   │   └── workflows.ts
│   │   │   ├── handlers.ts
│   │   │   └── index.ts
│   │   └── README.md
│   ├── node-storage-export.json
│   ├── setup
│   │   ├── global-setup.ts
│   │   ├── msw-setup.ts
│   │   ├── TEST_ENV_DOCUMENTATION.md
│   │   └── test-env.ts
│   ├── test-database-extraction.js
│   ├── test-direct-extraction.js
│   ├── test-enhanced-documentation.js
│   ├── test-enhanced-integration.js
│   ├── test-mcp-extraction.js
│   ├── test-mcp-server-extraction.js
│   ├── test-mcp-tools-integration.js
│   ├── test-node-documentation-service.js
│   ├── test-node-list.js
│   ├── test-package-info.js
│   ├── test-parsing-operations.js
│   ├── test-slack-node-complete.js
│   ├── test-small-rebuild.js
│   ├── test-sqlite-search.js
│   ├── test-storage-system.js
│   ├── unit
│   │   ├── __mocks__
│   │   │   ├── n8n-nodes-base.test.ts
│   │   │   ├── n8n-nodes-base.ts
│   │   │   └── README.md
│   │   ├── database
│   │   │   ├── __mocks__
│   │   │   │   └── better-sqlite3.ts
│   │   │   ├── database-adapter-unit.test.ts
│   │   │   ├── node-repository-core.test.ts
│   │   │   ├── node-repository-operations.test.ts
│   │   │   ├── node-repository-outputs.test.ts
│   │   │   ├── README.md
│   │   │   └── template-repository-core.test.ts
│   │   ├── docker
│   │   │   ├── config-security.test.ts
│   │   │   ├── edge-cases.test.ts
│   │   │   ├── parse-config.test.ts
│   │   │   └── serve-command.test.ts
│   │   ├── errors
│   │   │   └── validation-service-error.test.ts
│   │   ├── examples
│   │   │   └── using-n8n-nodes-base-mock.test.ts
│   │   ├── flexible-instance-security-advanced.test.ts
│   │   ├── flexible-instance-security.test.ts
│   │   ├── http-server
│   │   │   └── multi-tenant-support.test.ts
│   │   ├── http-server-n8n-mode.test.ts
│   │   ├── http-server-n8n-reinit.test.ts
│   │   ├── http-server-session-management.test.ts
│   │   ├── loaders
│   │   │   └── node-loader.test.ts
│   │   ├── mappers
│   │   │   └── docs-mapper.test.ts
│   │   ├── mcp
│   │   │   ├── get-node-essentials-examples.test.ts
│   │   │   ├── handlers-n8n-manager-simple.test.ts
│   │   │   ├── handlers-n8n-manager.test.ts
│   │   │   ├── handlers-workflow-diff.test.ts
│   │   │   ├── lru-cache-behavior.test.ts
│   │   │   ├── multi-tenant-tool-listing.test.ts.disabled
│   │   │   ├── parameter-validation.test.ts
│   │   │   ├── search-nodes-examples.test.ts
│   │   │   ├── tools-documentation.test.ts
│   │   │   └── tools.test.ts
│   │   ├── monitoring
│   │   │   └── cache-metrics.test.ts
│   │   ├── MULTI_TENANT_TEST_COVERAGE.md
│   │   ├── multi-tenant-integration.test.ts
│   │   ├── parsers
│   │   │   ├── node-parser-outputs.test.ts
│   │   │   ├── node-parser.test.ts
│   │   │   ├── property-extractor.test.ts
│   │   │   └── simple-parser.test.ts
│   │   ├── scripts
│   │   │   └── fetch-templates-extraction.test.ts
│   │   ├── services
│   │   │   ├── ai-node-validator.test.ts
│   │   │   ├── ai-tool-validators.test.ts
│   │   │   ├── confidence-scorer.test.ts
│   │   │   ├── config-validator-basic.test.ts
│   │   │   ├── config-validator-edge-cases.test.ts
│   │   │   ├── config-validator-node-specific.test.ts
│   │   │   ├── config-validator-security.test.ts
│   │   │   ├── debug-validator.test.ts
│   │   │   ├── enhanced-config-validator-integration.test.ts
│   │   │   ├── enhanced-config-validator-operations.test.ts
│   │   │   ├── enhanced-config-validator.test.ts
│   │   │   ├── example-generator.test.ts
│   │   │   ├── execution-processor.test.ts
│   │   │   ├── expression-format-validator.test.ts
│   │   │   ├── expression-validator-edge-cases.test.ts
│   │   │   ├── expression-validator.test.ts
│   │   │   ├── fixed-collection-validation.test.ts
│   │   │   ├── loop-output-edge-cases.test.ts
│   │   │   ├── n8n-api-client.test.ts
│   │   │   ├── n8n-validation.test.ts
│   │   │   ├── node-sanitizer.test.ts
│   │   │   ├── node-similarity-service.test.ts
│   │   │   ├── node-specific-validators.test.ts
│   │   │   ├── operation-similarity-service-comprehensive.test.ts
│   │   │   ├── operation-similarity-service.test.ts
│   │   │   ├── property-dependencies.test.ts
│   │   │   ├── property-filter-edge-cases.test.ts
│   │   │   ├── property-filter.test.ts
│   │   │   ├── resource-similarity-service-comprehensive.test.ts
│   │   │   ├── resource-similarity-service.test.ts
│   │   │   ├── task-templates.test.ts
│   │   │   ├── template-service.test.ts
│   │   │   ├── universal-expression-validator.test.ts
│   │   │   ├── validation-fixes.test.ts
│   │   │   ├── workflow-auto-fixer.test.ts
│   │   │   ├── workflow-diff-engine.test.ts
│   │   │   ├── workflow-fixed-collection-validation.test.ts
│   │   │   ├── workflow-validator-comprehensive.test.ts
│   │   │   ├── workflow-validator-edge-cases.test.ts
│   │   │   ├── workflow-validator-error-outputs.test.ts
│   │   │   ├── workflow-validator-expression-format.test.ts
│   │   │   ├── workflow-validator-loops-simple.test.ts
│   │   │   ├── workflow-validator-loops.test.ts
│   │   │   ├── workflow-validator-mocks.test.ts
│   │   │   ├── workflow-validator-performance.test.ts
│   │   │   ├── workflow-validator-with-mocks.test.ts
│   │   │   └── workflow-validator.test.ts
│   │   ├── telemetry
│   │   │   ├── batch-processor.test.ts
│   │   │   ├── config-manager.test.ts
│   │   │   ├── event-tracker.test.ts
│   │   │   ├── event-validator.test.ts
│   │   │   ├── rate-limiter.test.ts
│   │   │   ├── telemetry-error.test.ts
│   │   │   ├── telemetry-manager.test.ts
│   │   │   ├── v2.18.3-fixes-verification.test.ts
│   │   │   └── workflow-sanitizer.test.ts
│   │   ├── templates
│   │   │   ├── batch-processor.test.ts
│   │   │   ├── metadata-generator.test.ts
│   │   │   ├── template-repository-metadata.test.ts
│   │   │   └── template-repository-security.test.ts
│   │   ├── test-env-example.test.ts
│   │   ├── test-infrastructure.test.ts
│   │   ├── types
│   │   │   ├── instance-context-coverage.test.ts
│   │   │   └── instance-context-multi-tenant.test.ts
│   │   ├── utils
│   │   │   ├── auth-timing-safe.test.ts
│   │   │   ├── cache-utils.test.ts
│   │   │   ├── console-manager.test.ts
│   │   │   ├── database-utils.test.ts
│   │   │   ├── fixed-collection-validator.test.ts
│   │   │   ├── n8n-errors.test.ts
│   │   │   ├── node-type-normalizer.test.ts
│   │   │   ├── node-type-utils.test.ts
│   │   │   ├── node-utils.test.ts
│   │   │   ├── simple-cache-memory-leak-fix.test.ts
│   │   │   ├── ssrf-protection.test.ts
│   │   │   └── template-node-resolver.test.ts
│   │   └── validation-fixes.test.ts
│   └── utils
│       ├── assertions.ts
│       ├── builders
│       │   └── workflow.builder.ts
│       ├── data-generators.ts
│       ├── database-utils.ts
│       ├── README.md
│       └── test-helpers.ts
├── thumbnail.png
├── tsconfig.build.json
├── tsconfig.json
├── types
│   ├── mcp.d.ts
│   └── test-env.d.ts
├── verify-telemetry-fix.js
├── versioned-nodes.md
├── vitest.config.benchmark.ts
├── vitest.config.integration.ts
└── vitest.config.ts
```

# Files

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

```typescript
  1 | import { describe, it, expect } from 'vitest';
  2 | import { n8nDocumentationToolsFinal } from '@/mcp/tools';
  3 | import { z } from 'zod';
  4 | 
  5 | describe('n8nDocumentationToolsFinal', () => {
  6 |   describe('Tool Structure Validation', () => {
  7 |     it('should have all required properties for each tool', () => {
  8 |       n8nDocumentationToolsFinal.forEach(tool => {
  9 |         // Check required properties exist
 10 |         expect(tool).toHaveProperty('name');
 11 |         expect(tool).toHaveProperty('description');
 12 |         expect(tool).toHaveProperty('inputSchema');
 13 | 
 14 |         // Check property types
 15 |         expect(typeof tool.name).toBe('string');
 16 |         expect(typeof tool.description).toBe('string');
 17 |         expect(tool.inputSchema).toBeTypeOf('object');
 18 | 
 19 |         // Name should be non-empty
 20 |         expect(tool.name.length).toBeGreaterThan(0);
 21 |         
 22 |         // Description should be meaningful
 23 |         expect(tool.description.length).toBeGreaterThan(10);
 24 |       });
 25 |     });
 26 | 
 27 |     it('should have unique tool names', () => {
 28 |       const names = n8nDocumentationToolsFinal.map(tool => tool.name);
 29 |       const uniqueNames = new Set(names);
 30 |       expect(names.length).toBe(uniqueNames.size);
 31 |     });
 32 | 
 33 |     it('should have valid JSON Schema for all inputSchemas', () => {
 34 |       // Define a minimal JSON Schema validator using Zod
 35 |       const jsonSchemaValidator = z.object({
 36 |         type: z.literal('object'),
 37 |         properties: z.record(z.any()).optional(),
 38 |         required: z.array(z.string()).optional(),
 39 |       });
 40 | 
 41 |       n8nDocumentationToolsFinal.forEach(tool => {
 42 |         expect(() => {
 43 |           jsonSchemaValidator.parse(tool.inputSchema);
 44 |         }).not.toThrow();
 45 |       });
 46 |     });
 47 |   });
 48 | 
 49 |   describe('Individual Tool Validation', () => {
 50 |     describe('tools_documentation', () => {
 51 |       const tool = n8nDocumentationToolsFinal.find(t => t.name === 'tools_documentation');
 52 | 
 53 |       it('should exist', () => {
 54 |         expect(tool).toBeDefined();
 55 |       });
 56 | 
 57 |       it('should have correct schema', () => {
 58 |         expect(tool?.inputSchema).toMatchObject({
 59 |           type: 'object',
 60 |           properties: {
 61 |             topic: {
 62 |               type: 'string',
 63 |               description: expect.any(String)
 64 |             },
 65 |             depth: {
 66 |               type: 'string',
 67 |               enum: ['essentials', 'full'],
 68 |               description: expect.any(String),
 69 |               default: 'essentials'
 70 |             }
 71 |           }
 72 |         });
 73 |       });
 74 | 
 75 |       it('should have helpful description', () => {
 76 |         expect(tool?.description).toContain('documentation');
 77 |         expect(tool?.description).toContain('MCP tools');
 78 |       });
 79 |     });
 80 | 
 81 |     describe('list_nodes', () => {
 82 |       const tool = n8nDocumentationToolsFinal.find(t => t.name === 'list_nodes');
 83 | 
 84 |       it('should exist', () => {
 85 |         expect(tool).toBeDefined();
 86 |       });
 87 | 
 88 |       it('should have correct schema properties', () => {
 89 |         const properties = tool?.inputSchema.properties;
 90 |         expect(properties).toHaveProperty('package');
 91 |         expect(properties).toHaveProperty('category');
 92 |         expect(properties).toHaveProperty('developmentStyle');
 93 |         expect(properties).toHaveProperty('isAITool');
 94 |         expect(properties).toHaveProperty('limit');
 95 |       });
 96 | 
 97 |       it('should have correct defaults', () => {
 98 |         expect(tool?.inputSchema.properties.limit.default).toBe(50);
 99 |       });
100 | 
101 |       it('should have proper enum values', () => {
102 |         expect(tool?.inputSchema.properties.developmentStyle.enum).toEqual(['declarative', 'programmatic']);
103 |       });
104 |     });
105 | 
106 |     describe('get_node_info', () => {
107 |       const tool = n8nDocumentationToolsFinal.find(t => t.name === 'get_node_info');
108 | 
109 |       it('should exist', () => {
110 |         expect(tool).toBeDefined();
111 |       });
112 | 
113 |       it('should have nodeType as required parameter', () => {
114 |         expect(tool?.inputSchema.required).toContain('nodeType');
115 |       });
116 | 
117 |       it('should mention performance implications in description', () => {
118 |         expect(tool?.description).toMatch(/100KB\+|large|full/i);
119 |       });
120 |     });
121 | 
122 |     describe('search_nodes', () => {
123 |       const tool = n8nDocumentationToolsFinal.find(t => t.name === 'search_nodes');
124 | 
125 |       it('should exist', () => {
126 |         expect(tool).toBeDefined();
127 |       });
128 | 
129 |       it('should have query as required parameter', () => {
130 |         expect(tool?.inputSchema.required).toContain('query');
131 |       });
132 | 
133 |       it('should have mode enum with correct values', () => {
134 |         expect(tool?.inputSchema.properties.mode.enum).toEqual(['OR', 'AND', 'FUZZY']);
135 |         expect(tool?.inputSchema.properties.mode.default).toBe('OR');
136 |       });
137 | 
138 |       it('should have limit with default value', () => {
139 |         expect(tool?.inputSchema.properties.limit.default).toBe(20);
140 |       });
141 |     });
142 | 
143 |     describe('validate_workflow', () => {
144 |       const tool = n8nDocumentationToolsFinal.find(t => t.name === 'validate_workflow');
145 | 
146 |       it('should exist', () => {
147 |         expect(tool).toBeDefined();
148 |       });
149 | 
150 |       it('should have workflow as required parameter', () => {
151 |         expect(tool?.inputSchema.required).toContain('workflow');
152 |       });
153 | 
154 |       it('should have options with correct validation settings', () => {
155 |         const options = tool?.inputSchema.properties.options.properties;
156 |         expect(options).toHaveProperty('validateNodes');
157 |         expect(options).toHaveProperty('validateConnections');
158 |         expect(options).toHaveProperty('validateExpressions');
159 |         expect(options).toHaveProperty('profile');
160 |       });
161 | 
162 |       it('should have correct profile enum values', () => {
163 |         const profile = tool?.inputSchema.properties.options.properties.profile;
164 |         expect(profile.enum).toEqual(['minimal', 'runtime', 'ai-friendly', 'strict']);
165 |         expect(profile.default).toBe('runtime');
166 |       });
167 |     });
168 | 
169 |     describe('get_templates_for_task', () => {
170 |       const tool = n8nDocumentationToolsFinal.find(t => t.name === 'get_templates_for_task');
171 | 
172 |       it('should exist', () => {
173 |         expect(tool).toBeDefined();
174 |       });
175 | 
176 |       it('should have task as required parameter', () => {
177 |         expect(tool?.inputSchema.required).toContain('task');
178 |       });
179 | 
180 |       it('should have correct task enum values', () => {
181 |         const expectedTasks = [
182 |           'ai_automation',
183 |           'data_sync',
184 |           'webhook_processing',
185 |           'email_automation',
186 |           'slack_integration',
187 |           'data_transformation',
188 |           'file_processing',
189 |           'scheduling',
190 |           'api_integration',
191 |           'database_operations'
192 |         ];
193 |         expect(tool?.inputSchema.properties.task.enum).toEqual(expectedTasks);
194 |       });
195 |     });
196 |   });
197 | 
198 |   describe('Tool Description Quality', () => {
199 |     it('should have concise descriptions that fit in one line', () => {
200 |       n8nDocumentationToolsFinal.forEach(tool => {
201 |         // Descriptions should be informative but not overly long
202 |         expect(tool.description.length).toBeLessThan(300);
203 |       });
204 |     });
205 | 
206 |     it('should include examples or key information in descriptions', () => {
207 |       const toolsWithExamples = [
208 |         'list_nodes',
209 |         'get_node_info',
210 |         'search_nodes',
211 |         'get_node_essentials',
212 |         'get_node_documentation'
213 |       ];
214 | 
215 |       toolsWithExamples.forEach(toolName => {
216 |         const tool = n8nDocumentationToolsFinal.find(t => t.name === toolName);
217 |         // Should include either example usage, format information, or "nodes-base"
218 |         expect(tool?.description).toMatch(/example|Example|format|Format|nodes-base|Common:/i);
219 |       });
220 |     });
221 |   });
222 | 
223 |   describe('Schema Consistency', () => {
224 |     it('should use consistent parameter naming', () => {
225 |       const toolsWithNodeType = n8nDocumentationToolsFinal.filter(tool => 
226 |         tool.inputSchema.properties?.nodeType
227 |       );
228 | 
229 |       toolsWithNodeType.forEach(tool => {
230 |         const nodeTypeParam = tool.inputSchema.properties.nodeType;
231 |         expect(nodeTypeParam.type).toBe('string');
232 |         // Should mention the prefix requirement
233 |         expect(nodeTypeParam.description).toMatch(/nodes-base|prefix/i);
234 |       });
235 |     });
236 | 
237 |     it('should have consistent limit parameter defaults', () => {
238 |       const toolsWithLimit = n8nDocumentationToolsFinal.filter(tool => 
239 |         tool.inputSchema.properties?.limit
240 |       );
241 | 
242 |       toolsWithLimit.forEach(tool => {
243 |         const limitParam = tool.inputSchema.properties.limit;
244 |         expect(limitParam.type).toBe('number');
245 |         expect(limitParam.default).toBeDefined();
246 |         expect(limitParam.default).toBeGreaterThan(0);
247 |       });
248 |     });
249 |   });
250 | 
251 |   describe('Tool Categories Coverage', () => {
252 |     it('should have tools for all major categories', () => {
253 |       const categories = {
254 |         discovery: ['list_nodes', 'search_nodes', 'list_ai_tools'],
255 |         configuration: ['get_node_info', 'get_node_essentials', 'get_node_documentation'],
256 |         validation: ['validate_node_operation', 'validate_workflow', 'validate_node_minimal'],
257 |         templates: ['list_tasks', 'search_templates', 'list_templates', 'get_template', 'list_node_templates'], // get_node_for_task removed in v2.15.0
258 |         documentation: ['tools_documentation']
259 |       };
260 | 
261 |       Object.entries(categories).forEach(([category, expectedTools]) => {
262 |         expectedTools.forEach(toolName => {
263 |           const tool = n8nDocumentationToolsFinal.find(t => t.name === toolName);
264 |           expect(tool).toBeDefined();
265 |         });
266 |       });
267 |     });
268 |   });
269 | 
270 |   describe('Parameter Validation', () => {
271 |     it('should have proper type definitions for all parameters', () => {
272 |       const validTypes = ['string', 'number', 'boolean', 'object', 'array'];
273 | 
274 |       n8nDocumentationToolsFinal.forEach(tool => {
275 |         if (tool.inputSchema.properties) {
276 |           Object.entries(tool.inputSchema.properties).forEach(([paramName, param]) => {
277 |             expect(validTypes).toContain(param.type);
278 |             expect(param.description).toBeDefined();
279 |           });
280 |         }
281 |       });
282 |     });
283 | 
284 |     it('should mark required parameters correctly', () => {
285 |       const toolsWithRequired = n8nDocumentationToolsFinal.filter(tool => 
286 |         tool.inputSchema.required && tool.inputSchema.required.length > 0
287 |       );
288 | 
289 |       toolsWithRequired.forEach(tool => {
290 |         tool.inputSchema.required!.forEach(requiredParam => {
291 |           expect(tool.inputSchema.properties).toHaveProperty(requiredParam);
292 |         });
293 |       });
294 |     });
295 |   });
296 | 
297 |   describe('Edge Cases', () => {
298 |     it('should handle tools with no parameters', () => {
299 |       const toolsWithNoParams = ['list_ai_tools', 'get_database_statistics'];
300 |       
301 |       toolsWithNoParams.forEach(toolName => {
302 |         const tool = n8nDocumentationToolsFinal.find(t => t.name === toolName);
303 |         expect(tool).toBeDefined();
304 |         expect(Object.keys(tool?.inputSchema.properties || {}).length).toBe(0);
305 |       });
306 |     });
307 | 
308 |     it('should have array parameters defined correctly', () => {
309 |       const toolsWithArrays = ['list_node_templates'];
310 |       
311 |       toolsWithArrays.forEach(toolName => {
312 |         const tool = n8nDocumentationToolsFinal.find(t => t.name === toolName);
313 |         const arrayParam = tool?.inputSchema.properties.nodeTypes;
314 |         expect(arrayParam?.type).toBe('array');
315 |         expect(arrayParam?.items).toBeDefined();
316 |         expect(arrayParam?.items.type).toBe('string');
317 |       });
318 |     });
319 |   });
320 | 
321 |   describe('New Template Tools', () => {
322 |     describe('list_templates', () => {
323 |       const tool = n8nDocumentationToolsFinal.find(t => t.name === 'list_templates');
324 | 
325 |       it('should exist and be properly defined', () => {
326 |         expect(tool).toBeDefined();
327 |         expect(tool?.description).toContain('minimal data');
328 |       });
329 | 
330 |       it('should have correct parameters', () => {
331 |         expect(tool?.inputSchema.properties).toHaveProperty('limit');
332 |         expect(tool?.inputSchema.properties).toHaveProperty('offset');
333 |         expect(tool?.inputSchema.properties).toHaveProperty('sortBy');
334 | 
335 |         const limitParam = tool?.inputSchema.properties.limit;
336 |         expect(limitParam.type).toBe('number');
337 |         expect(limitParam.minimum).toBe(1);
338 |         expect(limitParam.maximum).toBe(100);
339 | 
340 |         const offsetParam = tool?.inputSchema.properties.offset;
341 |         expect(offsetParam.type).toBe('number');
342 |         expect(offsetParam.minimum).toBe(0);
343 | 
344 |         const sortByParam = tool?.inputSchema.properties.sortBy;
345 |         expect(sortByParam.enum).toEqual(['views', 'created_at', 'name']);
346 |       });
347 | 
348 |       it('should have no required parameters', () => {
349 |         expect(tool?.inputSchema.required).toBeUndefined();
350 |       });
351 |     });
352 | 
353 |     describe('get_template (enhanced)', () => {
354 |       const tool = n8nDocumentationToolsFinal.find(t => t.name === 'get_template');
355 | 
356 |       it('should exist and support mode parameter', () => {
357 |         expect(tool).toBeDefined();
358 |         expect(tool?.description).toContain('mode');
359 |       });
360 | 
361 |       it('should have mode parameter with correct values', () => {
362 |         expect(tool?.inputSchema.properties).toHaveProperty('mode');
363 | 
364 |         const modeParam = tool?.inputSchema.properties.mode;
365 |         expect(modeParam.enum).toEqual(['nodes_only', 'structure', 'full']);
366 |         expect(modeParam.default).toBe('full');
367 |       });
368 | 
369 |       it('should require templateId parameter', () => {
370 |         expect(tool?.inputSchema.required).toContain('templateId');
371 |       });
372 |     });
373 | 
374 |     describe('search_templates_by_metadata', () => {
375 |       const tool = n8nDocumentationToolsFinal.find(t => t.name === 'search_templates_by_metadata');
376 | 
377 |       it('should exist in the tools array', () => {
378 |         expect(tool).toBeDefined();
379 |         expect(tool?.name).toBe('search_templates_by_metadata');
380 |       });
381 | 
382 |       it('should have proper description', () => {
383 |         expect(tool?.description).toContain('Search templates by AI-generated metadata');
384 |         expect(tool?.description).toContain('category');
385 |         expect(tool?.description).toContain('complexity');
386 |       });
387 | 
388 |       it('should have correct input schema structure', () => {
389 |         expect(tool?.inputSchema.type).toBe('object');
390 |         expect(tool?.inputSchema.properties).toBeDefined();
391 |         expect(tool?.inputSchema.required).toBeUndefined(); // All parameters are optional
392 |       });
393 | 
394 |       it('should have category parameter with proper schema', () => {
395 |         const categoryProp = tool?.inputSchema.properties?.category;
396 |         expect(categoryProp).toBeDefined();
397 |         expect(categoryProp.type).toBe('string');
398 |         expect(categoryProp.description).toContain('category');
399 |       });
400 | 
401 |       it('should have complexity parameter with enum values', () => {
402 |         const complexityProp = tool?.inputSchema.properties?.complexity;
403 |         expect(complexityProp).toBeDefined();
404 |         expect(complexityProp.enum).toEqual(['simple', 'medium', 'complex']);
405 |         expect(complexityProp.description).toContain('complexity');
406 |       });
407 | 
408 |       it('should have time-based parameters with numeric constraints', () => {
409 |         const maxTimeProp = tool?.inputSchema.properties?.maxSetupMinutes;
410 |         const minTimeProp = tool?.inputSchema.properties?.minSetupMinutes;
411 |         
412 |         expect(maxTimeProp).toBeDefined();
413 |         expect(maxTimeProp.type).toBe('number');
414 |         expect(maxTimeProp.maximum).toBe(480);
415 |         expect(maxTimeProp.minimum).toBe(5);
416 |         
417 |         expect(minTimeProp).toBeDefined();
418 |         expect(minTimeProp.type).toBe('number');
419 |         expect(minTimeProp.maximum).toBe(480);
420 |         expect(minTimeProp.minimum).toBe(5);
421 |       });
422 | 
423 |       it('should have service and audience parameters', () => {
424 |         const serviceProp = tool?.inputSchema.properties?.requiredService;
425 |         const audienceProp = tool?.inputSchema.properties?.targetAudience;
426 |         
427 |         expect(serviceProp).toBeDefined();
428 |         expect(serviceProp.type).toBe('string');
429 |         expect(serviceProp.description).toContain('service');
430 |         
431 |         expect(audienceProp).toBeDefined();
432 |         expect(audienceProp.type).toBe('string');
433 |         expect(audienceProp.description).toContain('audience');
434 |       });
435 | 
436 |       it('should have pagination parameters', () => {
437 |         const limitProp = tool?.inputSchema.properties?.limit;
438 |         const offsetProp = tool?.inputSchema.properties?.offset;
439 |         
440 |         expect(limitProp).toBeDefined();
441 |         expect(limitProp.type).toBe('number');
442 |         expect(limitProp.default).toBe(20);
443 |         expect(limitProp.maximum).toBe(100);
444 |         expect(limitProp.minimum).toBe(1);
445 |         
446 |         expect(offsetProp).toBeDefined();
447 |         expect(offsetProp.type).toBe('number');
448 |         expect(offsetProp.default).toBe(0);
449 |         expect(offsetProp.minimum).toBe(0);
450 |       });
451 | 
452 |       it('should include all expected properties', () => {
453 |         const properties = Object.keys(tool?.inputSchema.properties || {});
454 |         const expectedProperties = [
455 |           'category',
456 |           'complexity', 
457 |           'maxSetupMinutes',
458 |           'minSetupMinutes',
459 |           'requiredService',
460 |           'targetAudience',
461 |           'limit',
462 |           'offset'
463 |         ];
464 |         
465 |         expectedProperties.forEach(prop => {
466 |           expect(properties).toContain(prop);
467 |         });
468 |       });
469 | 
470 |       it('should have appropriate additionalProperties setting', () => {
471 |         expect(tool?.inputSchema.additionalProperties).toBe(false);
472 |       });
473 |     });
474 | 
475 |     describe('Enhanced pagination support', () => {
476 |       const paginatedTools = ['list_node_templates', 'search_templates', 'get_templates_for_task', 'search_templates_by_metadata'];
477 | 
478 |       paginatedTools.forEach(toolName => {
479 |         describe(toolName, () => {
480 |           const tool = n8nDocumentationToolsFinal.find(t => t.name === toolName);
481 | 
482 |           it('should support limit parameter', () => {
483 |             expect(tool?.inputSchema.properties).toHaveProperty('limit');
484 |             const limitParam = tool?.inputSchema.properties.limit;
485 |             expect(limitParam.type).toBe('number');
486 |             expect(limitParam.minimum).toBeGreaterThanOrEqual(1);
487 |             expect(limitParam.maximum).toBeGreaterThanOrEqual(50);
488 |           });
489 | 
490 |           it('should support offset parameter', () => {
491 |             expect(tool?.inputSchema.properties).toHaveProperty('offset');
492 |             const offsetParam = tool?.inputSchema.properties.offset;
493 |             expect(offsetParam.type).toBe('number');
494 |             expect(offsetParam.minimum).toBe(0);
495 |           });
496 |         });
497 |       });
498 |     });
499 |   });
500 | });
```

--------------------------------------------------------------------------------
/tests/unit/mcp/handlers-workflow-diff.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, it, expect, vi, beforeEach } from 'vitest';
  2 | import { handleUpdatePartialWorkflow } from '@/mcp/handlers-workflow-diff';
  3 | import { WorkflowDiffEngine } from '@/services/workflow-diff-engine';
  4 | import { N8nApiClient } from '@/services/n8n-api-client';
  5 | import {
  6 |   N8nApiError,
  7 |   N8nAuthenticationError,
  8 |   N8nNotFoundError,
  9 |   N8nValidationError,
 10 |   N8nRateLimitError,
 11 |   N8nServerError,
 12 | } from '@/utils/n8n-errors';
 13 | import { z } from 'zod';
 14 | 
 15 | // Mock dependencies
 16 | vi.mock('@/services/workflow-diff-engine');
 17 | vi.mock('@/services/n8n-api-client');
 18 | vi.mock('@/config/n8n-api');
 19 | vi.mock('@/utils/logger');
 20 | vi.mock('@/mcp/handlers-n8n-manager', () => ({
 21 |   getN8nApiClient: vi.fn(),
 22 | }));
 23 | 
 24 | // Import mocked modules
 25 | import { getN8nApiClient } from '@/mcp/handlers-n8n-manager';
 26 | import { logger } from '@/utils/logger';
 27 | 
 28 | describe('handlers-workflow-diff', () => {
 29 |   let mockApiClient: any;
 30 |   let mockDiffEngine: any;
 31 | 
 32 |   // Helper function to create test workflow
 33 |   const createTestWorkflow = (overrides = {}) => ({
 34 |     id: 'test-workflow-id',
 35 |     name: 'Test Workflow',
 36 |     active: true,
 37 |     nodes: [
 38 |       {
 39 |         id: 'node1',
 40 |         name: 'Start',
 41 |         type: 'n8n-nodes-base.start',
 42 |         typeVersion: 1,
 43 |         position: [100, 100],
 44 |         parameters: {},
 45 |       },
 46 |       {
 47 |         id: 'node2',
 48 |         name: 'HTTP Request',
 49 |         type: 'n8n-nodes-base.httpRequest',
 50 |         typeVersion: 3,
 51 |         position: [300, 100],
 52 |         parameters: { url: 'https://api.test.com' },
 53 |       },
 54 |     ],
 55 |     connections: {
 56 |       'Start': {
 57 |         main: [[{ node: 'HTTP Request', type: 'main', index: 0 }]],
 58 |       },
 59 |     },
 60 |     createdAt: '2024-01-01T00:00:00Z',
 61 |     updatedAt: '2024-01-01T00:00:00Z',
 62 |     tags: [],
 63 |     settings: {},
 64 |     ...overrides,
 65 |   });
 66 | 
 67 |   beforeEach(() => {
 68 |     vi.clearAllMocks();
 69 | 
 70 |     // Setup mock API client
 71 |     mockApiClient = {
 72 |       getWorkflow: vi.fn(),
 73 |       updateWorkflow: vi.fn(),
 74 |     };
 75 | 
 76 |     // Setup mock diff engine
 77 |     mockDiffEngine = {
 78 |       applyDiff: vi.fn(),
 79 |     };
 80 | 
 81 |     // Mock the API client getter
 82 |     vi.mocked(getN8nApiClient).mockReturnValue(mockApiClient);
 83 | 
 84 |     // Mock WorkflowDiffEngine constructor
 85 |     vi.mocked(WorkflowDiffEngine).mockImplementation(() => mockDiffEngine);
 86 | 
 87 |     // Set up default environment
 88 |     process.env.DEBUG_MCP = 'false';
 89 |   });
 90 | 
 91 |   describe('handleUpdatePartialWorkflow', () => {
 92 |     it('should apply diff operations successfully', async () => {
 93 |       const testWorkflow = createTestWorkflow();
 94 |       const updatedWorkflow = {
 95 |         ...testWorkflow,
 96 |         nodes: [
 97 |           ...testWorkflow.nodes,
 98 |           {
 99 |             id: 'node3',
100 |             name: 'New Node',
101 |             type: 'n8n-nodes-base.set',
102 |             typeVersion: 1,
103 |             position: [500, 100],
104 |             parameters: {},
105 |           },
106 |         ],
107 |         connections: {
108 |           ...testWorkflow.connections,
109 |           'HTTP Request': {
110 |             main: [[{ node: 'New Node', type: 'main', index: 0 }]],
111 |           },
112 |         },
113 |       };
114 | 
115 |       const diffRequest = {
116 |         id: 'test-workflow-id',
117 |         operations: [
118 |           {
119 |             type: 'addNode',
120 |             node: {
121 |               id: 'node3',
122 |               name: 'New Node',
123 |               type: 'n8n-nodes-base.set',
124 |               typeVersion: 1,
125 |               position: [500, 100],
126 |               parameters: {},
127 |             },
128 |           },
129 |         ],
130 |       };
131 | 
132 |       mockApiClient.getWorkflow.mockResolvedValue(testWorkflow);
133 |       mockDiffEngine.applyDiff.mockResolvedValue({
134 |         success: true,
135 |         workflow: updatedWorkflow,
136 |         operationsApplied: 1,
137 |         message: 'Successfully applied 1 operation',
138 |         errors: [],
139 |         applied: [0],
140 |         failed: [],
141 |       });
142 |       mockApiClient.updateWorkflow.mockResolvedValue(updatedWorkflow);
143 | 
144 |       const result = await handleUpdatePartialWorkflow(diffRequest);
145 | 
146 |       expect(result).toEqual({
147 |         success: true,
148 |         data: updatedWorkflow,
149 |         message: 'Workflow "Test Workflow" updated successfully. Applied 1 operations.',
150 |         details: {
151 |           operationsApplied: 1,
152 |           workflowId: 'test-workflow-id',
153 |           workflowName: 'Test Workflow',
154 |           applied: [0],
155 |           failed: [],
156 |           errors: [],
157 |         },
158 |       });
159 | 
160 |       expect(mockApiClient.getWorkflow).toHaveBeenCalledWith('test-workflow-id');
161 |       expect(mockDiffEngine.applyDiff).toHaveBeenCalledWith(testWorkflow, diffRequest);
162 |       expect(mockApiClient.updateWorkflow).toHaveBeenCalledWith('test-workflow-id', updatedWorkflow);
163 |     });
164 | 
165 |     it('should handle validation-only mode', async () => {
166 |       const testWorkflow = createTestWorkflow();
167 |       const diffRequest = {
168 |         id: 'test-workflow-id',
169 |         operations: [
170 |           {
171 |             type: 'updateNode',
172 |             nodeId: 'node2',
173 |             updates: { name: 'Updated HTTP Request' },
174 |           },
175 |         ],
176 |         validateOnly: true,
177 |       };
178 | 
179 |       mockApiClient.getWorkflow.mockResolvedValue(testWorkflow);
180 |       mockDiffEngine.applyDiff.mockResolvedValue({
181 |         success: true,
182 |         workflow: testWorkflow,
183 |         operationsApplied: 1,
184 |         message: 'Validation successful',
185 |         errors: [],
186 |       });
187 | 
188 |       const result = await handleUpdatePartialWorkflow(diffRequest);
189 | 
190 |       expect(result).toEqual({
191 |         success: true,
192 |         message: 'Validation successful',
193 |         data: {
194 |           valid: true,
195 |           operationsToApply: 1,
196 |         },
197 |       });
198 | 
199 |       expect(mockApiClient.updateWorkflow).not.toHaveBeenCalled();
200 |     });
201 | 
202 |     it('should handle multiple operations', async () => {
203 |       const testWorkflow = createTestWorkflow();
204 |       const diffRequest = {
205 |         id: 'test-workflow-id',
206 |         operations: [
207 |           {
208 |             type: 'updateNode',
209 |             nodeId: 'node1',
210 |             updates: { name: 'Updated Start' },
211 |           },
212 |           {
213 |             type: 'addNode',
214 |             node: {
215 |               id: 'node3',
216 |               name: 'Set Node',
217 |               type: 'n8n-nodes-base.set',
218 |               typeVersion: 1,
219 |               position: [500, 100],
220 |               parameters: {},
221 |             },
222 |           },
223 |           {
224 |             type: 'addConnection',
225 |             source: 'node2',
226 |             target: 'node3',
227 |             sourceOutput: 'main',
228 |             targetInput: 'main',
229 |           },
230 |         ],
231 |       };
232 | 
233 |       mockApiClient.getWorkflow.mockResolvedValue(testWorkflow);
234 |       mockDiffEngine.applyDiff.mockResolvedValue({
235 |         success: true,
236 |         workflow: {
237 |           ...testWorkflow,
238 |           nodes: [
239 |             { ...testWorkflow.nodes[0], name: 'Updated Start' },
240 |             testWorkflow.nodes[1],
241 |             {
242 |               id: 'node3',
243 |               name: 'Set Node',
244 |               type: 'n8n-nodes-base.set',
245 |               typeVersion: 1,
246 |               position: [500, 100],
247 |               parameters: {},
248 |             }
249 |           ],
250 |           connections: {
251 |             'Updated Start': testWorkflow.connections['Start'],
252 |             'HTTP Request': {
253 |               main: [[{ node: 'Set Node', type: 'main', index: 0 }]],
254 |             },
255 |           },
256 |         },
257 |         operationsApplied: 3,
258 |         message: 'Successfully applied 3 operations',
259 |         errors: [],
260 |         applied: [0, 1, 2],
261 |         failed: [],
262 |       });
263 |       mockApiClient.updateWorkflow.mockResolvedValue({ ...testWorkflow });
264 | 
265 |       const result = await handleUpdatePartialWorkflow(diffRequest);
266 | 
267 |       expect(result.success).toBe(true);
268 |       expect(result.message).toContain('Applied 3 operations');
269 |     });
270 | 
271 |     it('should handle diff application failures', async () => {
272 |       const testWorkflow = createTestWorkflow();
273 |       const diffRequest = {
274 |         id: 'test-workflow-id',
275 |         operations: [
276 |           {
277 |             type: 'updateNode',
278 |             nodeId: 'non-existent-node',
279 |             updates: { name: 'Updated' },
280 |           },
281 |         ],
282 |       };
283 | 
284 |       mockApiClient.getWorkflow.mockResolvedValue(testWorkflow);
285 |       mockDiffEngine.applyDiff.mockResolvedValue({
286 |         success: false,
287 |         workflow: null,
288 |         operationsApplied: 0,
289 |         message: 'Failed to apply operations',
290 |         errors: ['Node "non-existent-node" not found'],
291 |         applied: [],
292 |         failed: [0],
293 |       });
294 | 
295 |       const result = await handleUpdatePartialWorkflow(diffRequest);
296 | 
297 |       expect(result).toEqual({
298 |         success: false,
299 |         error: 'Failed to apply diff operations',
300 |         details: {
301 |           errors: ['Node "non-existent-node" not found'],
302 |           operationsApplied: 0,
303 |           applied: [],
304 |           failed: [0],
305 |         },
306 |       });
307 | 
308 |       expect(mockApiClient.updateWorkflow).not.toHaveBeenCalled();
309 |     });
310 | 
311 |     it('should handle API not configured error', async () => {
312 |       vi.mocked(getN8nApiClient).mockReturnValue(null);
313 | 
314 |       const result = await handleUpdatePartialWorkflow({
315 |         id: 'test-id',
316 |         operations: [],
317 |       });
318 | 
319 |       expect(result).toEqual({
320 |         success: false,
321 |         error: 'n8n API not configured. Please set N8N_API_URL and N8N_API_KEY environment variables.',
322 |       });
323 |     });
324 | 
325 |     it('should handle workflow not found error', async () => {
326 |       const notFoundError = new N8nNotFoundError('Workflow', 'non-existent');
327 |       mockApiClient.getWorkflow.mockRejectedValue(notFoundError);
328 | 
329 |       const result = await handleUpdatePartialWorkflow({
330 |         id: 'non-existent',
331 |         operations: [],
332 |       });
333 | 
334 |       expect(result).toEqual({
335 |         success: false,
336 |         error: 'Workflow with ID non-existent not found',
337 |         code: 'NOT_FOUND',
338 |       });
339 |     });
340 | 
341 |     it('should handle API errors during update', async () => {
342 |       const testWorkflow = createTestWorkflow();
343 |       const validationError = new N8nValidationError('Invalid workflow structure', {
344 |         field: 'connections',
345 |         message: 'Invalid connection configuration',
346 |       });
347 | 
348 |       mockApiClient.getWorkflow.mockResolvedValue(testWorkflow);
349 |       mockDiffEngine.applyDiff.mockResolvedValue({
350 |         success: true,
351 |         workflow: testWorkflow,
352 |         operationsApplied: 1,
353 |         message: 'Success',
354 |         errors: [],
355 |       });
356 |       mockApiClient.updateWorkflow.mockRejectedValue(validationError);
357 | 
358 |       const result = await handleUpdatePartialWorkflow({
359 |         id: 'test-id',
360 |         operations: [{ type: 'updateNode', nodeId: 'node1', updates: {} }],
361 |       });
362 | 
363 |       expect(result).toEqual({
364 |         success: false,
365 |         error: 'Invalid request: Invalid workflow structure',
366 |         code: 'VALIDATION_ERROR',
367 |         details: {
368 |           field: 'connections',
369 |           message: 'Invalid connection configuration',
370 |         },
371 |       });
372 |     });
373 | 
374 |     it('should handle input validation errors', async () => {
375 |       const invalidInput = {
376 |         id: 'test-id',
377 |         operations: [
378 |           {
379 |             // Missing required 'type' field
380 |             nodeId: 'node1',
381 |             updates: {},
382 |           },
383 |         ],
384 |       };
385 | 
386 |       const result = await handleUpdatePartialWorkflow(invalidInput);
387 | 
388 |       expect(result.success).toBe(false);
389 |       expect(result.error).toBe('Invalid input');
390 |       expect(result.details).toHaveProperty('errors');
391 |       expect(result.details?.errors).toBeInstanceOf(Array);
392 |     });
393 | 
394 |     it('should handle complex operation types', async () => {
395 |       const testWorkflow = createTestWorkflow();
396 |       const diffRequest = {
397 |         id: 'test-workflow-id',
398 |         operations: [
399 |           {
400 |             type: 'moveNode',
401 |             nodeId: 'node2',
402 |             position: [400, 200],
403 |           },
404 |           {
405 |             type: 'removeConnection',
406 |             source: 'node1',
407 |             target: 'node2',
408 |             sourceOutput: 'main',
409 |             targetInput: 'main',
410 |           },
411 |           {
412 |             type: 'updateSettings',
413 |             settings: {
414 |               executionOrder: 'v1',
415 |               timezone: 'America/New_York',
416 |             },
417 |           },
418 |           {
419 |             type: 'addTag',
420 |             tag: 'automated',
421 |           },
422 |         ],
423 |       };
424 | 
425 |       mockApiClient.getWorkflow.mockResolvedValue(testWorkflow);
426 |       mockDiffEngine.applyDiff.mockResolvedValue({
427 |         success: true,
428 |         workflow: { ...testWorkflow, settings: { executionOrder: 'v1' } },
429 |         operationsApplied: 4,
430 |         message: 'Successfully applied 4 operations',
431 |         errors: [],
432 |       });
433 |       mockApiClient.updateWorkflow.mockResolvedValue({ ...testWorkflow });
434 | 
435 |       const result = await handleUpdatePartialWorkflow(diffRequest);
436 | 
437 |       expect(result.success).toBe(true);
438 |       expect(mockDiffEngine.applyDiff).toHaveBeenCalledWith(testWorkflow, diffRequest);
439 |     });
440 | 
441 |     it('should handle debug logging when enabled', async () => {
442 |       process.env.DEBUG_MCP = 'true';
443 |       const testWorkflow = createTestWorkflow();
444 | 
445 |       mockApiClient.getWorkflow.mockResolvedValue(testWorkflow);
446 |       mockDiffEngine.applyDiff.mockResolvedValue({
447 |         success: true,
448 |         workflow: testWorkflow,
449 |         operationsApplied: 1,
450 |         message: 'Success',
451 |         errors: [],
452 |       });
453 |       mockApiClient.updateWorkflow.mockResolvedValue(testWorkflow);
454 | 
455 |       await handleUpdatePartialWorkflow({
456 |         id: 'test-id',
457 |         operations: [{ type: 'updateNode', nodeId: 'node1', updates: {} }],
458 |       });
459 | 
460 |       expect(logger.debug).toHaveBeenCalledWith(
461 |         'Workflow diff request received',
462 |         expect.objectContaining({
463 |           argsType: 'object',
464 |           operationCount: 1,
465 |         })
466 |       );
467 |     });
468 | 
469 |     it('should handle generic errors', async () => {
470 |       const genericError = new Error('Something went wrong');
471 |       mockApiClient.getWorkflow.mockRejectedValue(genericError);
472 | 
473 |       const result = await handleUpdatePartialWorkflow({
474 |         id: 'test-id',
475 |         operations: [],
476 |       });
477 | 
478 |       expect(result).toEqual({
479 |         success: false,
480 |         error: 'Something went wrong',
481 |       });
482 |       expect(logger.error).toHaveBeenCalledWith('Failed to update partial workflow', genericError);
483 |     });
484 | 
485 |     it('should handle authentication errors', async () => {
486 |       const authError = new N8nAuthenticationError('Invalid API key');
487 |       mockApiClient.getWorkflow.mockRejectedValue(authError);
488 | 
489 |       const result = await handleUpdatePartialWorkflow({
490 |         id: 'test-id',
491 |         operations: [],
492 |       });
493 | 
494 |       expect(result).toEqual({
495 |         success: false,
496 |         error: 'Failed to authenticate with n8n. Please check your API key.',
497 |         code: 'AUTHENTICATION_ERROR',
498 |       });
499 |     });
500 | 
501 |     it('should handle rate limit errors', async () => {
502 |       const rateLimitError = new N8nRateLimitError(60);
503 |       mockApiClient.getWorkflow.mockRejectedValue(rateLimitError);
504 | 
505 |       const result = await handleUpdatePartialWorkflow({
506 |         id: 'test-id',
507 |         operations: [],
508 |       });
509 | 
510 |       expect(result).toEqual({
511 |         success: false,
512 |         error: 'Too many requests. Please wait a moment and try again.',
513 |         code: 'RATE_LIMIT_ERROR',
514 |       });
515 |     });
516 | 
517 |     it('should handle server errors', async () => {
518 |       const serverError = new N8nServerError('Internal server error');
519 |       mockApiClient.getWorkflow.mockRejectedValue(serverError);
520 | 
521 |       const result = await handleUpdatePartialWorkflow({
522 |         id: 'test-id',
523 |         operations: [],
524 |       });
525 | 
526 |       expect(result).toEqual({
527 |         success: false,
528 |         error: 'Internal server error',
529 |         code: 'SERVER_ERROR',
530 |       });
531 |     });
532 | 
533 |     it('should validate operation structure', async () => {
534 |       const testWorkflow = createTestWorkflow();
535 |       const diffRequest = {
536 |         id: 'test-workflow-id',
537 |         operations: [
538 |           {
539 |             type: 'updateNode',
540 |             nodeId: 'node1',
541 |             nodeName: 'Start', // Both nodeId and nodeName provided
542 |             updates: { name: 'New Start' },
543 |             description: 'Update start node name',
544 |           },
545 |           {
546 |             type: 'addConnection',
547 |             source: 'node1',
548 |             target: 'node2',
549 |             sourceOutput: 'main',
550 |             targetInput: 'main',
551 |             sourceIndex: 0,
552 |             targetIndex: 0,
553 |           },
554 |         ],
555 |       };
556 | 
557 |       mockApiClient.getWorkflow.mockResolvedValue(testWorkflow);
558 |       mockDiffEngine.applyDiff.mockResolvedValue({
559 |         success: true,
560 |         workflow: testWorkflow,
561 |         operationsApplied: 2,
562 |         message: 'Success',
563 |         errors: [],
564 |       });
565 |       mockApiClient.updateWorkflow.mockResolvedValue(testWorkflow);
566 | 
567 |       const result = await handleUpdatePartialWorkflow(diffRequest);
568 | 
569 |       expect(result.success).toBe(true);
570 |       expect(mockDiffEngine.applyDiff).toHaveBeenCalledWith(testWorkflow, diffRequest);
571 |     });
572 | 
573 |     it('should handle empty operations array', async () => {
574 |       const testWorkflow = createTestWorkflow();
575 |       const diffRequest = {
576 |         id: 'test-workflow-id',
577 |         operations: [],
578 |       };
579 | 
580 |       mockApiClient.getWorkflow.mockResolvedValue(testWorkflow);
581 |       mockDiffEngine.applyDiff.mockResolvedValue({
582 |         success: true,
583 |         workflow: testWorkflow,
584 |         operationsApplied: 0,
585 |         message: 'No operations to apply',
586 |         errors: [],
587 |       });
588 |       mockApiClient.updateWorkflow.mockResolvedValue(testWorkflow);
589 | 
590 |       const result = await handleUpdatePartialWorkflow(diffRequest);
591 | 
592 |       expect(result.success).toBe(true);
593 |       expect(result.message).toContain('Applied 0 operations');
594 |     });
595 | 
596 |     it('should handle partial diff application', async () => {
597 |       const testWorkflow = createTestWorkflow();
598 |       const diffRequest = {
599 |         id: 'test-workflow-id',
600 |         operations: [
601 |           { type: 'updateNode', nodeId: 'node1', updates: { name: 'Updated' } },
602 |           { type: 'updateNode', nodeId: 'invalid-node', updates: { name: 'Fail' } },
603 |           { type: 'addTag', tag: 'test' },
604 |         ],
605 |       };
606 | 
607 |       mockApiClient.getWorkflow.mockResolvedValue(testWorkflow);
608 |       mockDiffEngine.applyDiff.mockResolvedValue({
609 |         success: false,
610 |         workflow: null,
611 |         operationsApplied: 1,
612 |         message: 'Partially applied operations',
613 |         errors: ['Operation 2 failed: Node "invalid-node" not found'],
614 |       });
615 | 
616 |       const result = await handleUpdatePartialWorkflow(diffRequest);
617 | 
618 |       expect(result).toEqual({
619 |         success: false,
620 |         error: 'Failed to apply diff operations',
621 |         details: {
622 |           errors: ['Operation 2 failed: Node "invalid-node" not found'],
623 |           operationsApplied: 1,
624 |         },
625 |       });
626 |     });
627 |   });
628 | });
```

--------------------------------------------------------------------------------
/src/mcp/tool-docs/workflow_management/n8n-update-partial-workflow.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { ToolDocumentation } from '../types';
  2 | 
  3 | export const n8nUpdatePartialWorkflowDoc: ToolDocumentation = {
  4 |   name: 'n8n_update_partial_workflow',
  5 |   category: 'workflow_management',
  6 |   essentials: {
  7 |     description: 'Update workflow incrementally with diff operations. Types: addNode, removeNode, updateNode, moveNode, enable/disableNode, addConnection, removeConnection, rewireConnection, cleanStaleConnections, replaceConnections, updateSettings, updateName, add/removeTag. Supports smart parameters (branch, case) for multi-output nodes. Full support for AI connections (ai_languageModel, ai_tool, ai_memory, ai_embedding, ai_vectorStore, ai_document, ai_textSplitter, ai_outputParser).',
  8 |     keyParameters: ['id', 'operations', 'continueOnError'],
  9 |     example: 'n8n_update_partial_workflow({id: "wf_123", operations: [{type: "rewireConnection", source: "IF", from: "Old", to: "New", branch: "true"}]})',
 10 |     performance: 'Fast (50-200ms)',
 11 |     tips: [
 12 |       'Use rewireConnection to change connection targets',
 13 |       'Use branch="true"/"false" for IF nodes',
 14 |       'Use case=N for Switch nodes',
 15 |       'Use cleanStaleConnections to auto-remove broken connections',
 16 |       'Set ignoreErrors:true on removeConnection for cleanup',
 17 |       'Use continueOnError mode for best-effort bulk operations',
 18 |       'Validate with validateOnly first',
 19 |       'For AI connections, specify sourceOutput type (ai_languageModel, ai_tool, etc.)',
 20 |       'Batch AI component connections for atomic updates',
 21 |       'Auto-sanitization: ALL nodes auto-fixed during updates (operator structures, missing metadata)'
 22 |     ]
 23 |   },
 24 |   full: {
 25 |     description: `Updates workflows using surgical diff operations instead of full replacement. Supports 15 operation types for precise modifications. Operations are validated and applied atomically by default - all succeed or none are applied.
 26 | 
 27 | ## Available Operations:
 28 | 
 29 | ### Node Operations (6 types):
 30 | - **addNode**: Add a new node with name, type, and position (required)
 31 | - **removeNode**: Remove a node by ID or name
 32 | - **updateNode**: Update node properties using dot notation (e.g., 'parameters.url')
 33 | - **moveNode**: Change node position [x, y]
 34 | - **enableNode**: Enable a disabled node
 35 | - **disableNode**: Disable an active node
 36 | 
 37 | ### Connection Operations (5 types):
 38 | - **addConnection**: Connect nodes (source→target). Supports smart parameters: branch="true"/"false" for IF nodes, case=N for Switch nodes.
 39 | - **removeConnection**: Remove connection between nodes (supports ignoreErrors flag)
 40 | - **rewireConnection**: Change connection target from one node to another. Supports smart parameters.
 41 | - **cleanStaleConnections**: Auto-remove all connections referencing non-existent nodes
 42 | - **replaceConnections**: Replace entire connections object
 43 | 
 44 | ### Metadata Operations (4 types):
 45 | - **updateSettings**: Modify workflow settings
 46 | - **updateName**: Rename the workflow
 47 | - **addTag**: Add a workflow tag
 48 | - **removeTag**: Remove a workflow tag
 49 | 
 50 | ## Smart Parameters for Multi-Output Nodes
 51 | 
 52 | For **IF nodes**, use semantic 'branch' parameter instead of technical sourceIndex:
 53 | - **branch="true"**: Routes to true branch (sourceIndex=0)
 54 | - **branch="false"**: Routes to false branch (sourceIndex=1)
 55 | 
 56 | For **Switch nodes**, use semantic 'case' parameter:
 57 | - **case=0**: First output
 58 | - **case=1**: Second output
 59 | - **case=N**: Nth output
 60 | 
 61 | Works with addConnection and rewireConnection operations. Explicit sourceIndex overrides smart parameters.
 62 | 
 63 | ## AI Connection Support
 64 | 
 65 | Full support for all 8 AI connection types used in n8n AI workflows:
 66 | 
 67 | **Connection Types**:
 68 | - **ai_languageModel**: Connect language models (OpenAI, Anthropic, Google Gemini) to AI Agents
 69 | - **ai_tool**: Connect tools (HTTP Request Tool, Code Tool, etc.) to AI Agents
 70 | - **ai_memory**: Connect memory systems (Window Buffer, Conversation Summary) to AI Agents
 71 | - **ai_outputParser**: Connect output parsers (Structured, JSON) to AI Agents
 72 | - **ai_embedding**: Connect embedding models to Vector Stores
 73 | - **ai_vectorStore**: Connect vector stores to Vector Store Tools
 74 | - **ai_document**: Connect document loaders to Vector Stores
 75 | - **ai_textSplitter**: Connect text splitters to document processing chains
 76 | 
 77 | **AI Connection Examples**:
 78 | - Single connection: \`{type: "addConnection", source: "OpenAI", target: "AI Agent", sourceOutput: "ai_languageModel"}\`
 79 | - Fallback model: Use targetIndex (0=primary, 1=fallback) for dual language model setup
 80 | - Multiple tools: Batch multiple \`sourceOutput: "ai_tool"\` connections to one AI Agent
 81 | - Vector retrieval: Chain ai_embedding → ai_vectorStore → ai_tool → AI Agent
 82 | 
 83 | **Best Practices**:
 84 | - Always specify \`sourceOutput\` for AI connections (defaults to "main" if omitted)
 85 | - Connect language model BEFORE creating/enabling AI Agent (validation requirement)
 86 | - Use atomic mode (default) when setting up AI workflows to ensure complete configuration
 87 | - Validate AI workflows after changes with \`n8n_validate_workflow\` tool
 88 | 
 89 | ## Cleanup & Recovery Features
 90 | 
 91 | ### Automatic Cleanup
 92 | The **cleanStaleConnections** operation automatically removes broken connection references after node renames/deletions. Essential for workflow recovery.
 93 | 
 94 | ### Best-Effort Mode
 95 | Set **continueOnError: true** to apply valid operations even if some fail. Returns detailed results showing which operations succeeded/failed. Perfect for bulk cleanup operations.
 96 | 
 97 | ### Graceful Error Handling
 98 | Add **ignoreErrors: true** to removeConnection operations to prevent failures when connections don't exist.
 99 | 
100 | ## Auto-Sanitization System
101 | 
102 | ### What Gets Auto-Fixed
103 | When ANY workflow update is made, ALL nodes in the workflow are automatically sanitized to ensure complete metadata and correct structure:
104 | 
105 | 1. **Operator Structure Fixes**:
106 |    - Binary operators (equals, contains, greaterThan, etc.) automatically have \`singleValue\` removed
107 |    - Unary operators (isEmpty, isNotEmpty, true, false) automatically get \`singleValue: true\` added
108 |    - Invalid operator structures (e.g., \`{type: "isNotEmpty"}\`) are corrected to \`{type: "boolean", operation: "isNotEmpty"}\`
109 | 
110 | 2. **Missing Metadata Added**:
111 |    - IF v2.2+ nodes get complete \`conditions.options\` structure if missing
112 |    - Switch v3.2+ nodes get complete \`conditions.options\` for all rules
113 |    - Required fields: \`{version: 2, leftValue: "", caseSensitive: true, typeValidation: "strict"}\`
114 | 
115 | ### Sanitization Scope
116 | - Runs on **ALL nodes** in the workflow, not just modified ones
117 | - Triggered by ANY update operation (addNode, updateNode, addConnection, etc.)
118 | - Prevents workflow corruption that would make UI unrenderable
119 | 
120 | ### Limitations
121 | Auto-sanitization CANNOT fix:
122 | - Broken connections (connections referencing non-existent nodes) - use \`cleanStaleConnections\`
123 | - Branch count mismatches (e.g., Switch with 3 rules but only 2 outputs) - requires manual connection fixes
124 | - Workflows in paradoxical corrupt states (API returns corrupt data, API rejects updates) - must recreate workflow
125 | 
126 | ### Recovery Guidance
127 | If validation still fails after auto-sanitization:
128 | 1. Check error details for specific issues
129 | 2. Use \`validate_workflow\` to see all validation errors
130 | 3. For connection issues, use \`cleanStaleConnections\` operation
131 | 4. For branch mismatches, add missing output connections
132 | 5. For paradoxical corrupted workflows, create new workflow and migrate nodes`,
133 |     parameters: {
134 |       id: { type: 'string', required: true, description: 'Workflow ID to update' },
135 |       operations: {
136 |         type: 'array',
137 |         required: true,
138 |         description: 'Array of diff operations. Each must have "type" field and operation-specific properties. Nodes can be referenced by ID or name.'
139 |       },
140 |       validateOnly: { type: 'boolean', description: 'If true, only validate operations without applying them' },
141 |       continueOnError: { type: 'boolean', description: 'If true, apply valid operations even if some fail (best-effort mode). Returns applied and failed operation indices. Default: false (atomic)' }
142 |     },
143 |     returns: 'Updated workflow object or validation results if validateOnly=true',
144 |     examples: [
145 |       '// Add a basic node (minimal configuration)\nn8n_update_partial_workflow({id: "abc", operations: [{type: "addNode", node: {name: "Process Data", type: "n8n-nodes-base.set", position: [400, 300], parameters: {}}}]})',
146 |       '// Add node with full configuration\nn8n_update_partial_workflow({id: "def", operations: [{type: "addNode", node: {name: "Send Slack Alert", type: "n8n-nodes-base.slack", position: [600, 300], typeVersion: 2, parameters: {resource: "message", operation: "post", channel: "#alerts", text: "Success!"}}}]})',
147 |       '// Add node AND connect it (common pattern)\nn8n_update_partial_workflow({id: "ghi", operations: [\n  {type: "addNode", node: {name: "HTTP Request", type: "n8n-nodes-base.httpRequest", position: [400, 300], parameters: {url: "https://api.example.com", method: "GET"}}},\n  {type: "addConnection", source: "Webhook", target: "HTTP Request"}\n]})',
148 |       '// Rewire connection from one target to another\nn8n_update_partial_workflow({id: "xyz", operations: [{type: "rewireConnection", source: "Webhook", from: "Old Handler", to: "New Handler"}]})',
149 |       '// Smart parameter: IF node true branch\nn8n_update_partial_workflow({id: "abc", operations: [{type: "addConnection", source: "IF", target: "Success Handler", branch: "true"}]})',
150 |       '// Smart parameter: IF node false branch\nn8n_update_partial_workflow({id: "def", operations: [{type: "addConnection", source: "IF", target: "Error Handler", branch: "false"}]})',
151 |       '// Smart parameter: Switch node case routing\nn8n_update_partial_workflow({id: "ghi", operations: [\n  {type: "addConnection", source: "Switch", target: "Handler A", case: 0},\n  {type: "addConnection", source: "Switch", target: "Handler B", case: 1},\n  {type: "addConnection", source: "Switch", target: "Handler C", case: 2}\n]})',
152 |       '// Rewire with smart parameter\nn8n_update_partial_workflow({id: "jkl", operations: [{type: "rewireConnection", source: "IF", from: "Old True Handler", to: "New True Handler", branch: "true"}]})',
153 |       '// Add multiple nodes in batch\nn8n_update_partial_workflow({id: "mno", operations: [\n  {type: "addNode", node: {name: "Filter", type: "n8n-nodes-base.filter", position: [400, 300], parameters: {}}},\n  {type: "addNode", node: {name: "Transform", type: "n8n-nodes-base.set", position: [600, 300], parameters: {}}},\n  {type: "addConnection", source: "Filter", target: "Transform"}\n]})',
154 |       '// Clean up stale connections after node renames/deletions\nn8n_update_partial_workflow({id: "pqr", operations: [{type: "cleanStaleConnections"}]})',
155 |       '// Remove connection gracefully (no error if it doesn\'t exist)\nn8n_update_partial_workflow({id: "stu", operations: [{type: "removeConnection", source: "Old Node", target: "Target", ignoreErrors: true}]})',
156 |       '// Best-effort mode: apply what works, report what fails\nn8n_update_partial_workflow({id: "vwx", operations: [\n  {type: "updateName", name: "Fixed Workflow"},\n  {type: "removeConnection", source: "Broken", target: "Node"},\n  {type: "cleanStaleConnections"}\n], continueOnError: true})',
157 |       '// Update node parameter\nn8n_update_partial_workflow({id: "yza", operations: [{type: "updateNode", nodeName: "HTTP Request", updates: {"parameters.url": "https://api.example.com"}}]})',
158 |       '// Validate before applying\nn8n_update_partial_workflow({id: "bcd", operations: [{type: "removeNode", nodeName: "Old Process"}], validateOnly: true})',
159 |       '\n// ============ AI CONNECTION EXAMPLES ============',
160 |       '// Connect language model to AI Agent\nn8n_update_partial_workflow({id: "ai1", operations: [{type: "addConnection", source: "OpenAI Chat Model", target: "AI Agent", sourceOutput: "ai_languageModel"}]})',
161 |       '// Connect tool to AI Agent\nn8n_update_partial_workflow({id: "ai2", operations: [{type: "addConnection", source: "HTTP Request Tool", target: "AI Agent", sourceOutput: "ai_tool"}]})',
162 |       '// Connect memory to AI Agent\nn8n_update_partial_workflow({id: "ai3", operations: [{type: "addConnection", source: "Window Buffer Memory", target: "AI Agent", sourceOutput: "ai_memory"}]})',
163 |       '// Connect output parser to AI Agent\nn8n_update_partial_workflow({id: "ai4", operations: [{type: "addConnection", source: "Structured Output Parser", target: "AI Agent", sourceOutput: "ai_outputParser"}]})',
164 |       '// Complete AI Agent setup: Add language model, tools, and memory\nn8n_update_partial_workflow({id: "ai5", operations: [\n  {type: "addConnection", source: "OpenAI Chat Model", target: "AI Agent", sourceOutput: "ai_languageModel"},\n  {type: "addConnection", source: "HTTP Request Tool", target: "AI Agent", sourceOutput: "ai_tool"},\n  {type: "addConnection", source: "Code Tool", target: "AI Agent", sourceOutput: "ai_tool"},\n  {type: "addConnection", source: "Window Buffer Memory", target: "AI Agent", sourceOutput: "ai_memory"}\n]})',
165 |       '// Add fallback model to AI Agent (requires v2.1+)\nn8n_update_partial_workflow({id: "ai6", operations: [\n  {type: "addConnection", source: "OpenAI Chat Model", target: "AI Agent", sourceOutput: "ai_languageModel", targetIndex: 0},\n  {type: "addConnection", source: "Anthropic Chat Model", target: "AI Agent", sourceOutput: "ai_languageModel", targetIndex: 1}\n]})',
166 |       '// Vector Store setup: Connect embeddings and documents\nn8n_update_partial_workflow({id: "ai7", operations: [\n  {type: "addConnection", source: "Embeddings OpenAI", target: "Pinecone Vector Store", sourceOutput: "ai_embedding"},\n  {type: "addConnection", source: "Default Data Loader", target: "Pinecone Vector Store", sourceOutput: "ai_document"}\n]})',
167 |       '// Connect Vector Store Tool to AI Agent (retrieval setup)\nn8n_update_partial_workflow({id: "ai8", operations: [\n  {type: "addConnection", source: "Pinecone Vector Store", target: "Vector Store Tool", sourceOutput: "ai_vectorStore"},\n  {type: "addConnection", source: "Vector Store Tool", target: "AI Agent", sourceOutput: "ai_tool"}\n]})',
168 |       '// Rewire AI Agent to use different language model\nn8n_update_partial_workflow({id: "ai9", operations: [{type: "rewireConnection", source: "AI Agent", from: "OpenAI Chat Model", to: "Anthropic Chat Model", sourceOutput: "ai_languageModel"}]})',
169 |       '// Replace all AI tools for an agent\nn8n_update_partial_workflow({id: "ai10", operations: [\n  {type: "removeConnection", source: "Old Tool 1", target: "AI Agent", sourceOutput: "ai_tool"},\n  {type: "removeConnection", source: "Old Tool 2", target: "AI Agent", sourceOutput: "ai_tool"},\n  {type: "addConnection", source: "New HTTP Tool", target: "AI Agent", sourceOutput: "ai_tool"},\n  {type: "addConnection", source: "New Code Tool", target: "AI Agent", sourceOutput: "ai_tool"}\n]})'
170 |     ],
171 |     useCases: [
172 |       'Rewire connections when replacing nodes',
173 |       'Route IF/Switch node outputs with semantic parameters',
174 |       'Clean up broken workflows after node renames/deletions',
175 |       'Bulk connection cleanup with best-effort mode',
176 |       'Update single node parameters',
177 |       'Replace all connections at once',
178 |       'Graceful cleanup operations that don\'t fail',
179 |       'Enable/disable nodes',
180 |       'Rename workflows or nodes',
181 |       'Manage tags efficiently',
182 |       'Connect AI components (language models, tools, memory, parsers)',
183 |       'Set up AI Agent workflows with multiple tools',
184 |       'Add fallback language models to AI Agents',
185 |       'Configure Vector Store retrieval systems',
186 |       'Swap language models in existing AI workflows',
187 |       'Batch-update AI tool connections'
188 |     ],
189 |     performance: 'Very fast - typically 50-200ms. Much faster than full updates as only changes are processed.',
190 |     bestPractices: [
191 |       'Use rewireConnection instead of remove+add for changing targets',
192 |       'Use branch="true"/"false" for IF nodes instead of sourceIndex',
193 |       'Use case=N for Switch nodes instead of sourceIndex',
194 |       'Use cleanStaleConnections after renaming/removing nodes',
195 |       'Use continueOnError for bulk cleanup operations',
196 |       'Set ignoreErrors:true on removeConnection for graceful cleanup',
197 |       'Use validateOnly to test operations before applying',
198 |       'Group related changes in one call',
199 |       'Check operation order for dependencies',
200 |       'Use atomic mode (default) for critical updates',
201 |       'For AI connections, always specify sourceOutput (ai_languageModel, ai_tool, ai_memory, etc.)',
202 |       'Connect language model BEFORE adding AI Agent to ensure validation passes',
203 |       'Use targetIndex for fallback models (primary=0, fallback=1)',
204 |       'Batch AI component connections in a single operation for atomicity',
205 |       'Validate AI workflows after connection changes to catch configuration errors'
206 |     ],
207 |     pitfalls: [
208 |       '**REQUIRES N8N_API_URL and N8N_API_KEY environment variables** - will not work without n8n API access',
209 |       'Atomic mode (default): all operations must succeed or none are applied',
210 |       'continueOnError breaks atomic guarantees - use with caution',
211 |       'Order matters for dependent operations (e.g., must add node before connecting to it)',
212 |       'Node references accept ID or name, but name must be unique',
213 |       'Node names with special characters (apostrophes, quotes) work correctly',
214 |       'For best compatibility, prefer node IDs over names when dealing with special characters',
215 |       'Use "updates" property for updateNode operations: {type: "updateNode", updates: {...}}',
216 |       'Smart parameters (branch, case) only work with IF and Switch nodes - ignored for other node types',
217 |       'Explicit sourceIndex overrides smart parameters (branch, case) if both provided',
218 |       'cleanStaleConnections removes ALL broken connections - cannot be selective',
219 |       'replaceConnections overwrites entire connections object - all previous connections lost',
220 |       '**Auto-sanitization behavior**: Binary operators (equals, contains) automatically have singleValue removed; unary operators (isEmpty, isNotEmpty) automatically get singleValue:true added',
221 |       '**Auto-sanitization runs on ALL nodes**: When ANY update is made, ALL nodes in the workflow are sanitized (not just modified ones)',
222 |       '**Auto-sanitization cannot fix everything**: It fixes operator structures and missing metadata, but cannot fix broken connections or branch mismatches',
223 |       '**Corrupted workflows beyond repair**: Workflows in paradoxical states (API returns corrupt, API rejects updates) cannot be fixed via API - must be recreated'
224 |     ],
225 |     relatedTools: ['n8n_update_full_workflow', 'n8n_get_workflow', 'validate_workflow', 'tools_documentation']
226 |   }
227 | };
```

--------------------------------------------------------------------------------
/tests/unit/docker/edge-cases.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('Docker Config Edge Cases', () => {
  8 |   let tempDir: string;
  9 |   let configPath: string;
 10 |   const parseConfigPath = path.resolve(__dirname, '../../../docker/parse-config.js');
 11 | 
 12 |   beforeEach(() => {
 13 |     tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'edge-cases-test-'));
 14 |     configPath = path.join(tempDir, 'config.json');
 15 |   });
 16 | 
 17 |   afterEach(() => {
 18 |     if (fs.existsSync(tempDir)) {
 19 |       fs.rmSync(tempDir, { recursive: true });
 20 |     }
 21 |   });
 22 | 
 23 |   describe('Data type edge cases', () => {
 24 |     it('should handle JavaScript number edge cases', () => {
 25 |       // Note: JSON.stringify converts Infinity/-Infinity/NaN to null
 26 |       // So we need to test with a pre-stringified JSON that would have these values
 27 |       const configJson = `{
 28 |         "max_safe_int": ${Number.MAX_SAFE_INTEGER},
 29 |         "min_safe_int": ${Number.MIN_SAFE_INTEGER},
 30 |         "positive_zero": 0,
 31 |         "negative_zero": -0,
 32 |         "very_small": 1e-308,
 33 |         "very_large": 1e308,
 34 |         "float_precision": 0.30000000000000004
 35 |       }`;
 36 |       fs.writeFileSync(configPath, configJson);
 37 | 
 38 |       const output = execSync(`node "${parseConfigPath}" "${configPath}"`, { encoding: 'utf8' });
 39 |       
 40 |       expect(output).toContain(`export MAX_SAFE_INT='${Number.MAX_SAFE_INTEGER}'`);
 41 |       expect(output).toContain(`export MIN_SAFE_INT='${Number.MIN_SAFE_INTEGER}'`);
 42 |       expect(output).toContain("export POSITIVE_ZERO='0'");
 43 |       expect(output).toContain("export NEGATIVE_ZERO='0'"); // -0 becomes 0 in JSON
 44 |       expect(output).toContain("export VERY_SMALL='1e-308'");
 45 |       expect(output).toContain("export VERY_LARGE='1e+308'");
 46 |       expect(output).toContain("export FLOAT_PRECISION='0.30000000000000004'");
 47 |       
 48 |       // Test null values (what Infinity/NaN become in JSON)
 49 |       const configWithNull = { test_null: null, test_array: [1, 2], test_undefined: undefined };
 50 |       fs.writeFileSync(configPath, JSON.stringify(configWithNull));
 51 |       const output2 = execSync(`node "${parseConfigPath}" "${configPath}"`, { encoding: 'utf8' });
 52 |       // null values and arrays are skipped
 53 |       expect(output2).toBe('');
 54 |     });
 55 | 
 56 |     it('should handle unusual but valid JSON structures', () => {
 57 |       const config = {
 58 |         "": "empty key",
 59 |         "123": "numeric key",
 60 |         "true": "boolean key",
 61 |         "null": "null key",
 62 |         "undefined": "undefined key",
 63 |         "[object Object]": "object string key",
 64 |         "key\nwith\nnewlines": "multiline key",
 65 |         "key\twith\ttabs": "tab key",
 66 |         "🔑": "emoji key",
 67 |         "ключ": "cyrillic key",
 68 |         "キー": "japanese key",
 69 |         "مفتاح": "arabic key"
 70 |       };
 71 |       fs.writeFileSync(configPath, JSON.stringify(config));
 72 | 
 73 |       const output = execSync(`node "${parseConfigPath}" "${configPath}"`, { encoding: 'utf8' });
 74 |       
 75 |       // Empty key is skipped (becomes EMPTY_KEY and then filtered out)
 76 |       expect(output).not.toContain("empty key");
 77 |       
 78 |       // Numeric key gets prefixed with underscore
 79 |       expect(output).toContain("export _123='numeric key'");
 80 |       
 81 |       // Other keys are transformed
 82 |       expect(output).toContain("export TRUE='boolean key'");
 83 |       expect(output).toContain("export NULL='null key'");
 84 |       expect(output).toContain("export UNDEFINED='undefined key'");
 85 |       expect(output).toContain("export OBJECT_OBJECT='object string key'");
 86 |       expect(output).toContain("export KEY_WITH_NEWLINES='multiline key'");
 87 |       expect(output).toContain("export KEY_WITH_TABS='tab key'");
 88 |       
 89 |       // Non-ASCII characters are replaced with underscores
 90 |       // But if the result is empty after sanitization, they're skipped
 91 |       const lines = output.trim().split('\n');
 92 |       // emoji, cyrillic, japanese, arabic keys all become empty after sanitization and are skipped
 93 |       expect(lines.length).toBe(7); // Only the ASCII-based keys remain
 94 |     });
 95 | 
 96 |     it('should handle circular reference prevention in nested configs', () => {
 97 |       // Create a config that would have circular references if not handled properly
 98 |       const config = {
 99 |         level1: {
100 |           level2: {
101 |             level3: {
102 |               circular_ref: "This would reference level1 in a real circular structure"
103 |             }
104 |           },
105 |           sibling: {
106 |             ref_to_level2: "Reference to sibling"
107 |           }
108 |         }
109 |       };
110 |       fs.writeFileSync(configPath, JSON.stringify(config));
111 | 
112 |       const output = execSync(`node "${parseConfigPath}" "${configPath}"`, { encoding: 'utf8' });
113 |       
114 |       expect(output).toContain("export LEVEL1_LEVEL2_LEVEL3_CIRCULAR_REF='This would reference level1 in a real circular structure'");
115 |       expect(output).toContain("export LEVEL1_SIBLING_REF_TO_LEVEL2='Reference to sibling'");
116 |     });
117 |   });
118 | 
119 |   describe('File system edge cases', () => {
120 |     it('should handle permission errors gracefully', () => {
121 |       if (process.platform === 'win32') {
122 |         // Skip on Windows as permission handling is different
123 |         return;
124 |       }
125 | 
126 |       // Create a file with no read permissions
127 |       fs.writeFileSync(configPath, '{"test": "value"}');
128 |       fs.chmodSync(configPath, 0o000);
129 | 
130 |       try {
131 |         const output = execSync(`node "${parseConfigPath}" "${configPath}" 2>&1`, { encoding: 'utf8' });
132 |         // Should exit silently even with permission error
133 |         expect(output).toBe('');
134 |       } finally {
135 |         // Restore permissions for cleanup
136 |         fs.chmodSync(configPath, 0o644);
137 |       }
138 |     });
139 | 
140 |     it('should handle symlinks correctly', () => {
141 |       const actualConfig = path.join(tempDir, 'actual-config.json');
142 |       const symlinkPath = path.join(tempDir, 'symlink-config.json');
143 |       
144 |       fs.writeFileSync(actualConfig, '{"symlink_test": "value"}');
145 |       fs.symlinkSync(actualConfig, symlinkPath);
146 | 
147 |       const output = execSync(`node "${parseConfigPath}" "${symlinkPath}"`, { encoding: 'utf8' });
148 |       
149 |       expect(output).toContain("export SYMLINK_TEST='value'");
150 |     });
151 | 
152 |     it('should handle very large config files', () => {
153 |       // Create a large config with many keys
154 |       const largeConfig: Record<string, any> = {};
155 |       for (let i = 0; i < 10000; i++) {
156 |         largeConfig[`key_${i}`] = `value_${i}`;
157 |       }
158 |       fs.writeFileSync(configPath, JSON.stringify(largeConfig));
159 | 
160 |       const output = execSync(`node "${parseConfigPath}" "${configPath}"`, { encoding: 'utf8' });
161 |       
162 |       const lines = output.trim().split('\n');
163 |       expect(lines.length).toBe(10000);
164 |       expect(output).toContain("export KEY_0='value_0'");
165 |       expect(output).toContain("export KEY_9999='value_9999'");
166 |     });
167 |   });
168 | 
169 |   describe('JSON parsing edge cases', () => {
170 |     it('should handle various invalid JSON formats', () => {
171 |       const invalidJsonCases = [
172 |         '{invalid}', // Missing quotes
173 |         "{'single': 'quotes'}", // Single quotes
174 |         '{test: value}', // Unquoted keys
175 |         '{"test": undefined}', // Undefined value
176 |         '{"test": function() {}}', // Function
177 |         '{,}', // Invalid structure
178 |         '{"a": 1,}', // Trailing comma
179 |         'null', // Just null
180 |         'true', // Just boolean
181 |         '"string"', // Just string
182 |         '123', // Just number
183 |         '[]', // Empty array
184 |         '[1, 2, 3]', // Array
185 |       ];
186 | 
187 |       invalidJsonCases.forEach(invalidJson => {
188 |         fs.writeFileSync(configPath, invalidJson);
189 |         const output = execSync(`node "${parseConfigPath}" "${configPath}" 2>&1`, { encoding: 'utf8' });
190 |         // Should exit silently on invalid JSON
191 |         expect(output).toBe('');
192 |       });
193 |     });
194 | 
195 |     it('should handle Unicode edge cases in JSON', () => {
196 |       const config = {
197 |         // Various Unicode scenarios
198 |         zero_width: "test\u200B\u200C\u200Dtest", // Zero-width characters
199 |         bom: "\uFEFFtest", // Byte order mark
200 |         surrogate_pair: "𝕳𝖊𝖑𝖑𝖔", // Mathematical bold text
201 |         rtl_text: "مرحبا mixed עברית", // Right-to-left text
202 |         combining: "é" + "é", // Combining vs precomposed
203 |         control_chars: "test\u0001\u0002\u0003test",
204 |         emoji_zwj: "👨‍👩‍👧‍👦", // Family emoji with ZWJ
205 |         invalid_surrogate: "test\uD800test", // Invalid surrogate
206 |       };
207 |       fs.writeFileSync(configPath, JSON.stringify(config));
208 | 
209 |       const output = execSync(`node "${parseConfigPath}" "${configPath}"`, { encoding: 'utf8' });
210 |       
211 |       // All Unicode should be preserved in values
212 |       expect(output).toContain("export ZERO_WIDTH='test\u200B\u200C\u200Dtest'");
213 |       expect(output).toContain("export BOM='\uFEFFtest'");
214 |       expect(output).toContain("export SURROGATE_PAIR='𝕳𝖊𝖑𝖑𝖔'");
215 |       expect(output).toContain("export RTL_TEXT='مرحبا mixed עברית'");
216 |       expect(output).toContain("export COMBINING='éé'");
217 |       expect(output).toContain("export CONTROL_CHARS='test\u0001\u0002\u0003test'");
218 |       expect(output).toContain("export EMOJI_ZWJ='👨‍👩‍👧‍👦'");
219 |       // Invalid surrogate gets replaced with replacement character
220 |       expect(output).toContain("export INVALID_SURROGATE='test�test'");
221 |     });
222 |   });
223 | 
224 |   describe('Environment variable edge cases', () => {
225 |     it('should handle environment variable name transformations', () => {
226 |       const config = {
227 |         "lowercase": "value",
228 |         "UPPERCASE": "value",
229 |         "camelCase": "value",
230 |         "PascalCase": "value",
231 |         "snake_case": "value",
232 |         "kebab-case": "value",
233 |         "dot.notation": "value",
234 |         "space separated": "value",
235 |         "special!@#$%^&*()": "value",
236 |         "123starting-with-number": "value",
237 |         "ending-with-number123": "value",
238 |         "-starting-with-dash": "value",
239 |         "_starting_with_underscore": "value"
240 |       };
241 |       fs.writeFileSync(configPath, JSON.stringify(config));
242 | 
243 |       const output = execSync(`node "${parseConfigPath}" "${configPath}"`, { encoding: 'utf8' });
244 |       
245 |       // Check transformations
246 |       expect(output).toContain("export LOWERCASE='value'");
247 |       expect(output).toContain("export UPPERCASE='value'");
248 |       expect(output).toContain("export CAMELCASE='value'");
249 |       expect(output).toContain("export PASCALCASE='value'");
250 |       expect(output).toContain("export SNAKE_CASE='value'");
251 |       expect(output).toContain("export KEBAB_CASE='value'");
252 |       expect(output).toContain("export DOT_NOTATION='value'");
253 |       expect(output).toContain("export SPACE_SEPARATED='value'");
254 |       expect(output).toContain("export SPECIAL='value'"); // special chars removed
255 |       expect(output).toContain("export _123STARTING_WITH_NUMBER='value'"); // prefixed
256 |       expect(output).toContain("export ENDING_WITH_NUMBER123='value'");
257 |       expect(output).toContain("export STARTING_WITH_DASH='value'"); // dash removed
258 |       expect(output).toContain("export STARTING_WITH_UNDERSCORE='value'"); // Leading underscore is trimmed
259 |     });
260 | 
261 |     it('should handle conflicting keys after transformation', () => {
262 |       const config = {
263 |         "test_key": "underscore",
264 |         "test-key": "dash",
265 |         "test.key": "dot",
266 |         "test key": "space",
267 |         "TEST_KEY": "uppercase",
268 |         nested: {
269 |           "test_key": "nested_underscore"
270 |         }
271 |       };
272 |       fs.writeFileSync(configPath, JSON.stringify(config));
273 | 
274 |       const output = execSync(`node "${parseConfigPath}" "${configPath}"`, { encoding: 'utf8' });
275 |       
276 |       // All should be transformed to TEST_KEY
277 |       const lines = output.trim().split('\n');
278 |       const testKeyLines = lines.filter(line => line.includes("TEST_KEY='"));
279 |       
280 |       // Script outputs all unique TEST_KEY values it encounters
281 |       // The parser processes keys in order, outputting each unique var name once
282 |       expect(testKeyLines.length).toBeGreaterThanOrEqual(1);
283 |       
284 |       // Nested one has different prefix
285 |       expect(output).toContain("export NESTED_TEST_KEY='nested_underscore'");
286 |     });
287 |   });
288 | 
289 |   describe('Performance edge cases', () => {
290 |     it('should handle extremely deep nesting efficiently', () => {
291 |       // Create very deep nesting (script allows up to depth 10, which is 11 levels)
292 |       const createDeepNested = (depth: number, value: any = "deep_value"): any => {
293 |         if (depth === 0) return value;
294 |         return { nested: createDeepNested(depth - 1, value) };
295 |       };
296 | 
297 |       // Create nested object with exactly 10 levels
298 |       const config = createDeepNested(10);
299 |       fs.writeFileSync(configPath, JSON.stringify(config));
300 | 
301 |       const start = Date.now();
302 |       const output = execSync(`node "${parseConfigPath}" "${configPath}"`, { encoding: 'utf8' });
303 |       const duration = Date.now() - start;
304 | 
305 |       // Should complete in reasonable time even with deep nesting
306 |       expect(duration).toBeLessThan(1000); // Less than 1 second
307 |       
308 |       // Should produce the deeply nested key with 10 levels
309 |       const expectedKey = Array(10).fill('NESTED').join('_');
310 |       expect(output).toContain(`export ${expectedKey}='deep_value'`);
311 |       
312 |       // Test that 11 levels also works (script allows up to depth 10 = 11 levels)
313 |       const deepConfig = createDeepNested(11);
314 |       fs.writeFileSync(configPath, JSON.stringify(deepConfig));
315 |       const output2 = execSync(`node "${parseConfigPath}" "${configPath}"`, { encoding: 'utf8' });
316 |       const elevenLevelKey = Array(11).fill('NESTED').join('_');
317 |       expect(output2).toContain(`export ${elevenLevelKey}='deep_value'`); // 11 levels present
318 |       
319 |       // Test that 12 levels gets completely blocked (beyond depth limit)
320 |       const veryDeepConfig = createDeepNested(12);
321 |       fs.writeFileSync(configPath, JSON.stringify(veryDeepConfig));
322 |       const output3 = execSync(`node "${parseConfigPath}" "${configPath}"`, { encoding: 'utf8' });
323 |       // With 12 levels, recursion limit is exceeded and no output is produced
324 |       expect(output3).toBe(''); // No output at all
325 |     });
326 | 
327 |     it('should handle wide objects efficiently', () => {
328 |       // Create object with many keys at same level
329 |       const config: Record<string, any> = {};
330 |       for (let i = 0; i < 1000; i++) {
331 |         config[`key_${i}`] = {
332 |           nested_a: `value_a_${i}`,
333 |           nested_b: `value_b_${i}`,
334 |           nested_c: {
335 |             deep: `deep_${i}`
336 |           }
337 |         };
338 |       }
339 |       fs.writeFileSync(configPath, JSON.stringify(config));
340 | 
341 |       const start = Date.now();
342 |       const output = execSync(`node "${parseConfigPath}" "${configPath}"`, { encoding: 'utf8' });
343 |       const duration = Date.now() - start;
344 | 
345 |       // Should complete efficiently
346 |       expect(duration).toBeLessThan(2000); // Less than 2 seconds
347 |       
348 |       const lines = output.trim().split('\n');
349 |       expect(lines.length).toBe(3000); // 3 values per key × 1000 keys (nested_c.deep is flattened)
350 |       
351 |       // Verify format
352 |       expect(output).toContain("export KEY_0_NESTED_A='value_a_0'");
353 |       expect(output).toContain("export KEY_999_NESTED_C_DEEP='deep_999'");
354 |     });
355 |   });
356 | 
357 |   describe('Mixed content edge cases', () => {
358 |     it('should handle mixed valid and invalid content', () => {
359 |       const config = {
360 |         valid_string: "normal value",
361 |         valid_number: 42,
362 |         valid_bool: true,
363 |         invalid_undefined: undefined,
364 |         invalid_function: null, // Would be a function but JSON.stringify converts to null
365 |         invalid_symbol: null, // Would be a Symbol but JSON.stringify converts to null
366 |         valid_nested: {
367 |           inner_valid: "works",
368 |           inner_array: ["ignored", "array"],
369 |           inner_null: null
370 |         }
371 |       };
372 |       fs.writeFileSync(configPath, JSON.stringify(config));
373 | 
374 |       const output = execSync(`node "${parseConfigPath}" "${configPath}"`, { encoding: 'utf8' });
375 |       
376 |       // Only valid values should be exported
377 |       expect(output).toContain("export VALID_STRING='normal value'");
378 |       expect(output).toContain("export VALID_NUMBER='42'");
379 |       expect(output).toContain("export VALID_BOOL='true'");
380 |       expect(output).toContain("export VALID_NESTED_INNER_VALID='works'");
381 |       
382 |       // null values, undefined (becomes undefined in JSON), and arrays are not exported
383 |       expect(output).not.toContain('INVALID_UNDEFINED');
384 |       expect(output).not.toContain('INVALID_FUNCTION');
385 |       expect(output).not.toContain('INVALID_SYMBOL');
386 |       expect(output).not.toContain('INNER_ARRAY');
387 |       expect(output).not.toContain('INNER_NULL');
388 |     });
389 |   });
390 | 
391 |   describe('Real-world configuration scenarios', () => {
392 |     it('should handle typical n8n-mcp configuration', () => {
393 |       const config = {
394 |         mcp_mode: "http",
395 |         auth_token: "bearer-token-123",
396 |         server: {
397 |           host: "0.0.0.0",
398 |           port: 3000,
399 |           cors: {
400 |             enabled: true,
401 |             origins: ["http://localhost:3000", "https://app.example.com"]
402 |           }
403 |         },
404 |         database: {
405 |           node_db_path: "/data/nodes.db",
406 |           template_cache_size: 100
407 |         },
408 |         logging: {
409 |           level: "info",
410 |           format: "json",
411 |           disable_console_output: false
412 |         },
413 |         features: {
414 |           enable_templates: true,
415 |           enable_validation: true,
416 |           validation_profile: "ai-friendly"
417 |         }
418 |       };
419 |       fs.writeFileSync(configPath, JSON.stringify(config));
420 | 
421 |       // Run with a clean set of environment variables to avoid conflicts
422 |       // We need to preserve PATH so node can be found
423 |       const output = execSync(`node "${parseConfigPath}" "${configPath}"`, { 
424 |         encoding: 'utf8',
425 |         env: { PATH: process.env.PATH, NODE_ENV: 'test' } // Only include PATH and NODE_ENV
426 |       });
427 |       
428 |       // Verify all configuration is properly exported with export prefix
429 |       expect(output).toContain("export MCP_MODE='http'");
430 |       expect(output).toContain("export AUTH_TOKEN='bearer-token-123'");
431 |       expect(output).toContain("export SERVER_HOST='0.0.0.0'");
432 |       expect(output).toContain("export SERVER_PORT='3000'");
433 |       expect(output).toContain("export SERVER_CORS_ENABLED='true'");
434 |       expect(output).toContain("export DATABASE_NODE_DB_PATH='/data/nodes.db'");
435 |       expect(output).toContain("export DATABASE_TEMPLATE_CACHE_SIZE='100'");
436 |       expect(output).toContain("export LOGGING_LEVEL='info'");
437 |       expect(output).toContain("export LOGGING_FORMAT='json'");
438 |       expect(output).toContain("export LOGGING_DISABLE_CONSOLE_OUTPUT='false'");
439 |       expect(output).toContain("export FEATURES_ENABLE_TEMPLATES='true'");
440 |       expect(output).toContain("export FEATURES_ENABLE_VALIDATION='true'");
441 |       expect(output).toContain("export FEATURES_VALIDATION_PROFILE='ai-friendly'");
442 |       
443 |       // Arrays should be ignored
444 |       expect(output).not.toContain('ORIGINS');
445 |     });
446 |   });
447 | });
```

--------------------------------------------------------------------------------
/src/services/property-filter.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * PropertyFilter Service
  3 |  * 
  4 |  * Intelligently filters node properties to return only essential and commonly-used ones.
  5 |  * Reduces property count from 200+ to 10-20 for better AI agent usability.
  6 |  */
  7 | 
  8 | export interface SimplifiedProperty {
  9 |   name: string;
 10 |   displayName: string;
 11 |   type: string;
 12 |   description: string;
 13 |   default?: any;
 14 |   options?: Array<{ value: string; label: string }>;
 15 |   required?: boolean;
 16 |   placeholder?: string;
 17 |   showWhen?: Record<string, any>;
 18 |   usageHint?: string;
 19 | }
 20 | 
 21 | export interface EssentialConfig {
 22 |   required: string[];
 23 |   common: string[];
 24 |   categoryPriority?: string[];
 25 | }
 26 | 
 27 | export interface FilteredProperties {
 28 |   required: SimplifiedProperty[];
 29 |   common: SimplifiedProperty[];
 30 | }
 31 | 
 32 | export class PropertyFilter {
 33 |   /**
 34 |    * Curated lists of essential properties for the most commonly used nodes.
 35 |    * Based on analysis of typical workflows and AI agent needs.
 36 |    */
 37 |   private static ESSENTIAL_PROPERTIES: Record<string, EssentialConfig> = {
 38 |     // HTTP Request - Most used node
 39 |     'nodes-base.httpRequest': {
 40 |       required: ['url'],
 41 |       common: ['method', 'authentication', 'sendBody', 'contentType', 'sendHeaders'],
 42 |       categoryPriority: ['basic', 'authentication', 'request', 'response', 'advanced']
 43 |     },
 44 |     
 45 |     // Webhook - Entry point for many workflows
 46 |     'nodes-base.webhook': {
 47 |       required: [],
 48 |       common: ['httpMethod', 'path', 'responseMode', 'responseData', 'responseCode'],
 49 |       categoryPriority: ['basic', 'response', 'advanced']
 50 |     },
 51 |     
 52 |     // Code - For custom logic
 53 |     'nodes-base.code': {
 54 |       required: [],
 55 |       common: ['language', 'jsCode', 'pythonCode', 'mode'],
 56 |       categoryPriority: ['basic', 'code', 'advanced']
 57 |     },
 58 |     
 59 |     // Set - Data manipulation
 60 |     'nodes-base.set': {
 61 |       required: [],
 62 |       common: ['mode', 'assignments', 'includeOtherFields', 'options'],
 63 |       categoryPriority: ['basic', 'data', 'advanced']
 64 |     },
 65 |     
 66 |     // If - Conditional logic
 67 |     'nodes-base.if': {
 68 |       required: [],
 69 |       common: ['conditions', 'combineOperation'],
 70 |       categoryPriority: ['basic', 'conditions', 'advanced']
 71 |     },
 72 |     
 73 |     // PostgreSQL - Database operations
 74 |     'nodes-base.postgres': {
 75 |       required: [],
 76 |       common: ['operation', 'table', 'query', 'additionalFields', 'returnAll'],
 77 |       categoryPriority: ['basic', 'query', 'options', 'advanced']
 78 |     },
 79 |     
 80 |     // OpenAI - AI operations
 81 |     'nodes-base.openAi': {
 82 |       required: [],
 83 |       common: ['resource', 'operation', 'modelId', 'prompt', 'messages', 'maxTokens'],
 84 |       categoryPriority: ['basic', 'model', 'input', 'options', 'advanced']
 85 |     },
 86 |     
 87 |     // Google Sheets - Spreadsheet operations
 88 |     'nodes-base.googleSheets': {
 89 |       required: [],
 90 |       common: ['operation', 'documentId', 'sheetName', 'range', 'dataStartRow'],
 91 |       categoryPriority: ['basic', 'location', 'data', 'options', 'advanced']
 92 |     },
 93 |     
 94 |     // Slack - Messaging
 95 |     'nodes-base.slack': {
 96 |       required: [],
 97 |       common: ['resource', 'operation', 'channel', 'text', 'attachments', 'blocks'],
 98 |       categoryPriority: ['basic', 'message', 'formatting', 'advanced']
 99 |     },
100 |     
101 |     // Email - Email operations
102 |     'nodes-base.email': {
103 |       required: [],
104 |       common: ['resource', 'operation', 'fromEmail', 'toEmail', 'subject', 'text', 'html'],
105 |       categoryPriority: ['basic', 'recipients', 'content', 'advanced']
106 |     },
107 |     
108 |     // Merge - Combining data streams
109 |     'nodes-base.merge': {
110 |       required: [],
111 |       common: ['mode', 'joinMode', 'propertyName1', 'propertyName2', 'outputDataFrom'],
112 |       categoryPriority: ['basic', 'merge', 'advanced']
113 |     },
114 |     
115 |     // Function (legacy) - Custom functions
116 |     'nodes-base.function': {
117 |       required: [],
118 |       common: ['functionCode'],
119 |       categoryPriority: ['basic', 'code', 'advanced']
120 |     },
121 |     
122 |     // Split In Batches - Batch processing
123 |     'nodes-base.splitInBatches': {
124 |       required: [],
125 |       common: ['batchSize', 'options'],
126 |       categoryPriority: ['basic', 'options', 'advanced']
127 |     },
128 |     
129 |     // Redis - Cache operations
130 |     'nodes-base.redis': {
131 |       required: [],
132 |       common: ['operation', 'key', 'value', 'keyType', 'expire'],
133 |       categoryPriority: ['basic', 'data', 'options', 'advanced']
134 |     },
135 |     
136 |     // MongoDB - NoSQL operations
137 |     'nodes-base.mongoDb': {
138 |       required: [],
139 |       common: ['operation', 'collection', 'query', 'fields', 'limit'],
140 |       categoryPriority: ['basic', 'query', 'options', 'advanced']
141 |     },
142 |     
143 |     // MySQL - Database operations
144 |     'nodes-base.mySql': {
145 |       required: [],
146 |       common: ['operation', 'table', 'query', 'columns', 'additionalFields'],
147 |       categoryPriority: ['basic', 'query', 'options', 'advanced']
148 |     },
149 |     
150 |     // FTP - File transfer
151 |     'nodes-base.ftp': {
152 |       required: [],
153 |       common: ['operation', 'path', 'fileName', 'binaryData'],
154 |       categoryPriority: ['basic', 'file', 'options', 'advanced']
155 |     },
156 |     
157 |     // SSH - Remote execution
158 |     'nodes-base.ssh': {
159 |       required: [],
160 |       common: ['resource', 'operation', 'command', 'path', 'cwd'],
161 |       categoryPriority: ['basic', 'command', 'options', 'advanced']
162 |     },
163 |     
164 |     // Execute Command - Local execution
165 |     'nodes-base.executeCommand': {
166 |       required: [],
167 |       common: ['command', 'cwd'],
168 |       categoryPriority: ['basic', 'advanced']
169 |     },
170 |     
171 |     // GitHub - Version control operations
172 |     'nodes-base.github': {
173 |       required: [],
174 |       common: ['resource', 'operation', 'owner', 'repository', 'title', 'body'],
175 |       categoryPriority: ['basic', 'repository', 'content', 'advanced']
176 |     }
177 |   };
178 |   
179 |   /**
180 |    * Deduplicate properties based on name and display conditions
181 |    */
182 |   static deduplicateProperties(properties: any[]): any[] {
183 |     const seen = new Map<string, any>();
184 |     
185 |     return properties.filter(prop => {
186 |       // Skip null/undefined properties
187 |       if (!prop || !prop.name) {
188 |         return false;
189 |       }
190 |       
191 |       // Create unique key from name + conditions
192 |       const conditions = JSON.stringify(prop.displayOptions || {});
193 |       const key = `${prop.name}_${conditions}`;
194 |       
195 |       if (seen.has(key)) {
196 |         return false; // Skip duplicate
197 |       }
198 |       
199 |       seen.set(key, prop);
200 |       return true;
201 |     });
202 |   }
203 |   
204 |   /**
205 |    * Get essential properties for a node type
206 |    */
207 |   static getEssentials(allProperties: any[], nodeType: string): FilteredProperties {
208 |     // Handle null/undefined properties
209 |     if (!allProperties) {
210 |       return { required: [], common: [] };
211 |     }
212 |     
213 |     // Deduplicate first
214 |     const uniqueProperties = this.deduplicateProperties(allProperties);
215 |     const config = this.ESSENTIAL_PROPERTIES[nodeType];
216 |     
217 |     if (!config) {
218 |       // Fallback for unconfigured nodes
219 |       return this.inferEssentials(uniqueProperties);
220 |     }
221 |     
222 |     // Extract required properties
223 |     const required = this.extractProperties(uniqueProperties, config.required, true);
224 |     
225 |     // Extract common properties (excluding any already in required)
226 |     const requiredNames = new Set(required.map(p => p.name));
227 |     const common = this.extractProperties(uniqueProperties, config.common, false)
228 |       .filter(p => !requiredNames.has(p.name));
229 |     
230 |     return { required, common };
231 |   }
232 |   
233 |   /**
234 |    * Extract and simplify specified properties
235 |    */
236 |   private static extractProperties(
237 |     allProperties: any[], 
238 |     propertyNames: string[], 
239 |     markAsRequired: boolean
240 |   ): SimplifiedProperty[] {
241 |     const extracted: SimplifiedProperty[] = [];
242 |     
243 |     for (const name of propertyNames) {
244 |       const property = this.findPropertyByName(allProperties, name);
245 |       if (property) {
246 |         const simplified = this.simplifyProperty(property);
247 |         if (markAsRequired) {
248 |           simplified.required = true;
249 |         }
250 |         extracted.push(simplified);
251 |       }
252 |     }
253 |     
254 |     return extracted;
255 |   }
256 |   
257 |   /**
258 |    * Find a property by name, including in nested collections
259 |    */
260 |   private static findPropertyByName(properties: any[], name: string): any | undefined {
261 |     for (const prop of properties) {
262 |       if (prop.name === name) {
263 |         return prop;
264 |       }
265 |       
266 |       // Check in nested collections
267 |       if (prop.type === 'collection' && prop.options) {
268 |         const found = this.findPropertyByName(prop.options, name);
269 |         if (found) return found;
270 |       }
271 |       
272 |       // Check in fixed collections
273 |       if (prop.type === 'fixedCollection' && prop.options) {
274 |         for (const option of prop.options) {
275 |           if (option.values) {
276 |             const found = this.findPropertyByName(option.values, name);
277 |             if (found) return found;
278 |           }
279 |         }
280 |       }
281 |     }
282 |     
283 |     return undefined;
284 |   }
285 |   
286 |   /**
287 |    * Simplify a property for AI consumption
288 |    */
289 |   private static simplifyProperty(prop: any): SimplifiedProperty {
290 |     const simplified: SimplifiedProperty = {
291 |       name: prop.name,
292 |       displayName: prop.displayName || prop.name,
293 |       type: prop.type || 'string', // Default to string if no type specified
294 |       description: this.extractDescription(prop),
295 |       required: prop.required || false
296 |     };
297 |     
298 |     // Include default value if it's simple
299 |     if (prop.default !== undefined && 
300 |         typeof prop.default !== 'object' || 
301 |         prop.type === 'options' || 
302 |         prop.type === 'multiOptions') {
303 |       simplified.default = prop.default;
304 |     }
305 |     
306 |     // Include placeholder
307 |     if (prop.placeholder) {
308 |       simplified.placeholder = prop.placeholder;
309 |     }
310 |     
311 |     // Simplify options for select fields
312 |     if (prop.options && Array.isArray(prop.options)) {
313 |       // Limit options to first 20 for better usability
314 |       const limitedOptions = prop.options.slice(0, 20);
315 |       simplified.options = limitedOptions.map((opt: any) => {
316 |         if (typeof opt === 'string') {
317 |           return { value: opt, label: opt };
318 |         }
319 |         return {
320 |           value: opt.value || opt.name,
321 |           label: opt.name || opt.value || opt.displayName
322 |         };
323 |       });
324 |     }
325 |     
326 |     // Include simple display conditions (max 2 conditions)
327 |     if (prop.displayOptions?.show) {
328 |       const conditions = Object.keys(prop.displayOptions.show);
329 |       if (conditions.length <= 2) {
330 |         simplified.showWhen = prop.displayOptions.show;
331 |       }
332 |     }
333 |     
334 |     // Add usage hints based on property characteristics
335 |     simplified.usageHint = this.generateUsageHint(prop);
336 |     
337 |     return simplified;
338 |   }
339 |   
340 |   /**
341 |    * Generate helpful usage hints for properties
342 |    */
343 |   private static generateUsageHint(prop: any): string | undefined {
344 |     // URL properties
345 |     if (prop.name.toLowerCase().includes('url') || prop.name === 'endpoint') {
346 |       return 'Enter the full URL including https://';
347 |     }
348 |     
349 |     // Authentication properties
350 |     if (prop.name.includes('auth') || prop.name.includes('credential')) {
351 |       return 'Select authentication method or credentials';
352 |     }
353 |     
354 |     // JSON properties
355 |     if (prop.type === 'json' || prop.name.includes('json')) {
356 |       return 'Enter valid JSON data';
357 |     }
358 |     
359 |     // Code properties
360 |     if (prop.type === 'code' || prop.name.includes('code')) {
361 |       return 'Enter your code here';
362 |     }
363 |     
364 |     // Boolean with specific behaviors
365 |     if (prop.type === 'boolean' && prop.displayOptions) {
366 |       return 'Enabling this will show additional options';
367 |     }
368 |     
369 |     return undefined;
370 |   }
371 |   
372 |   /**
373 |    * Extract description from various possible fields
374 |    */
375 |   private static extractDescription(prop: any): string {
376 |     // Try multiple fields where description might be stored
377 |     const description = prop.description || 
378 |                        prop.hint || 
379 |                        prop.placeholder || 
380 |                        prop.displayName ||
381 |                        '';
382 |     
383 |     // If still empty, generate based on property characteristics
384 |     if (!description) {
385 |       return this.generateDescription(prop);
386 |     }
387 |     
388 |     return description;
389 |   }
390 |   
391 |   /**
392 |    * Generate a description based on property characteristics
393 |    */
394 |   private static generateDescription(prop: any): string {
395 |     const name = prop.name.toLowerCase();
396 |     const type = prop.type;
397 |     
398 |     // Common property descriptions
399 |     const commonDescriptions: Record<string, string> = {
400 |       'url': 'The URL to make the request to',
401 |       'method': 'HTTP method to use for the request',
402 |       'authentication': 'Authentication method to use',
403 |       'sendbody': 'Whether to send a request body',
404 |       'contenttype': 'Content type of the request body',
405 |       'sendheaders': 'Whether to send custom headers',
406 |       'jsonbody': 'JSON data to send in the request body',
407 |       'headers': 'Custom headers to send with the request',
408 |       'timeout': 'Request timeout in milliseconds',
409 |       'query': 'SQL query to execute',
410 |       'table': 'Database table name',
411 |       'operation': 'Operation to perform',
412 |       'path': 'Webhook path or file path',
413 |       'httpmethod': 'HTTP method to accept',
414 |       'responsemode': 'How to respond to the webhook',
415 |       'responsecode': 'HTTP response code to return',
416 |       'channel': 'Slack channel to send message to',
417 |       'text': 'Text content of the message',
418 |       'subject': 'Email subject line',
419 |       'fromemail': 'Sender email address',
420 |       'toemail': 'Recipient email address',
421 |       'language': 'Programming language to use',
422 |       'jscode': 'JavaScript code to execute',
423 |       'pythoncode': 'Python code to execute'
424 |     };
425 |     
426 |     // Check for exact match
427 |     if (commonDescriptions[name]) {
428 |       return commonDescriptions[name];
429 |     }
430 |     
431 |     // Check for partial matches
432 |     for (const [key, desc] of Object.entries(commonDescriptions)) {
433 |       if (name.includes(key)) {
434 |         return desc;
435 |       }
436 |     }
437 |     
438 |     // Type-based descriptions
439 |     if (type === 'boolean') {
440 |       return `Enable or disable ${prop.displayName || name}`;
441 |     } else if (type === 'options') {
442 |       return `Select ${prop.displayName || name}`;
443 |     } else if (type === 'string') {
444 |       return `Enter ${prop.displayName || name}`;
445 |     } else if (type === 'number') {
446 |       return `Number value for ${prop.displayName || name}`;
447 |     } else if (type === 'json') {
448 |       return `JSON data for ${prop.displayName || name}`;
449 |     }
450 |     
451 |     return `Configure ${prop.displayName || name}`;
452 |   }
453 |   
454 |   /**
455 |    * Infer essentials for nodes without curated lists
456 |    */
457 |   private static inferEssentials(properties: any[]): FilteredProperties {
458 |     // Extract explicitly required properties (limit to prevent huge results)
459 |     const required = properties
460 |       .filter(p => p.name && p.required === true)
461 |       .slice(0, 10) // Limit required properties
462 |       .map(p => this.simplifyProperty(p));
463 |     
464 |     // Find common properties (simple, always visible, at root level)
465 |     const common = properties
466 |       .filter(p => {
467 |         return p.name && // Ensure property has a name
468 |                !p.required && 
469 |                !p.displayOptions && 
470 |                p.type !== 'hidden' && // Filter out hidden properties
471 |                p.type !== 'notice' &&  // Filter out notice properties
472 |                !p.name.startsWith('options') &&
473 |                !p.name.startsWith('_'); // Filter out internal properties
474 |       })
475 |       .slice(0, 10) // Take first 10 simple properties
476 |       .map(p => this.simplifyProperty(p));
477 |     
478 |     // If we have very few properties, include some conditional ones
479 |     if (required.length + common.length < 10) {
480 |       const additional = properties
481 |         .filter(p => {
482 |           return p.name && // Ensure property has a name
483 |                  !p.required &&
484 |                  p.type !== 'hidden' && // Filter out hidden properties
485 |                  p.displayOptions &&
486 |                  Object.keys(p.displayOptions.show || {}).length === 1;
487 |         })
488 |         .slice(0, 10 - (required.length + common.length))
489 |         .map(p => this.simplifyProperty(p));
490 |       
491 |       common.push(...additional);
492 |     }
493 |     
494 |     // Total should not exceed 30 properties
495 |     const totalLimit = 30;
496 |     if (required.length + common.length > totalLimit) {
497 |       // Prioritize required properties
498 |       const requiredCount = Math.min(required.length, 15);
499 |       const commonCount = totalLimit - requiredCount;
500 |       return {
501 |         required: required.slice(0, requiredCount),
502 |         common: common.slice(0, commonCount)
503 |       };
504 |     }
505 |     
506 |     return { required, common };
507 |   }
508 |   
509 |   /**
510 |    * Search for properties matching a query
511 |    */
512 |   static searchProperties(
513 |     allProperties: any[], 
514 |     query: string,
515 |     maxResults: number = 20
516 |   ): SimplifiedProperty[] {
517 |     // Return empty array for empty query
518 |     if (!query || query.trim() === '') {
519 |       return [];
520 |     }
521 |     
522 |     const lowerQuery = query.toLowerCase();
523 |     const matches: Array<{ property: any; score: number; path: string }> = [];
524 |     
525 |     this.searchPropertiesRecursive(allProperties, lowerQuery, matches);
526 |     
527 |     // Sort by score and return top results
528 |     return matches
529 |       .sort((a, b) => b.score - a.score)
530 |       .slice(0, maxResults)
531 |       .map(match => ({
532 |         ...this.simplifyProperty(match.property),
533 |         path: match.path
534 |       } as SimplifiedProperty & { path: string }));
535 |   }
536 |   
537 |   /**
538 |    * Recursively search properties including nested ones
539 |    */
540 |   private static searchPropertiesRecursive(
541 |     properties: any[],
542 |     query: string,
543 |     matches: Array<{ property: any; score: number; path: string }>,
544 |     path: string = ''
545 |   ): void {
546 |     for (const prop of properties) {
547 |       const currentPath = path ? `${path}.${prop.name}` : prop.name;
548 |       let score = 0;
549 |       
550 |       // Check name match
551 |       if (prop.name.toLowerCase() === query) {
552 |         score = 10; // Exact match
553 |       } else if (prop.name.toLowerCase().startsWith(query)) {
554 |         score = 8; // Prefix match
555 |       } else if (prop.name.toLowerCase().includes(query)) {
556 |         score = 5; // Contains match
557 |       }
558 |       
559 |       // Check display name match
560 |       if (prop.displayName?.toLowerCase().includes(query)) {
561 |         score = Math.max(score, 4);
562 |       }
563 |       
564 |       // Check description match
565 |       if (prop.description?.toLowerCase().includes(query)) {
566 |         score = Math.max(score, 3);
567 |       }
568 |       
569 |       if (score > 0) {
570 |         matches.push({ property: prop, score, path: currentPath });
571 |       }
572 |       
573 |       // Search nested properties
574 |       if (prop.type === 'collection' && prop.options) {
575 |         this.searchPropertiesRecursive(prop.options, query, matches, currentPath);
576 |       } else if (prop.type === 'fixedCollection' && prop.options) {
577 |         for (const option of prop.options) {
578 |           if (option.values) {
579 |             this.searchPropertiesRecursive(
580 |               option.values, 
581 |               query, 
582 |               matches, 
583 |               `${currentPath}.${option.name}`
584 |             );
585 |           }
586 |         }
587 |       }
588 |     }
589 |   }
590 | }
```

--------------------------------------------------------------------------------
/src/services/operation-similarity-service.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { NodeRepository } from '../database/node-repository';
  2 | import { logger } from '../utils/logger';
  3 | import { ValidationServiceError } from '../errors/validation-service-error';
  4 | 
  5 | export interface OperationSuggestion {
  6 |   value: string;
  7 |   confidence: number;
  8 |   reason: string;
  9 |   resource?: string;
 10 |   description?: string;
 11 | }
 12 | 
 13 | interface OperationPattern {
 14 |   pattern: string;
 15 |   suggestion: string;
 16 |   confidence: number;
 17 |   reason: string;
 18 | }
 19 | 
 20 | export class OperationSimilarityService {
 21 |   private static readonly CACHE_DURATION_MS = 5 * 60 * 1000; // 5 minutes
 22 |   private static readonly MIN_CONFIDENCE = 0.3; // 30% minimum confidence to suggest
 23 |   private static readonly MAX_SUGGESTIONS = 5;
 24 | 
 25 |   // Confidence thresholds for better code clarity
 26 |   private static readonly CONFIDENCE_THRESHOLDS = {
 27 |     EXACT: 1.0,
 28 |     VERY_HIGH: 0.95,
 29 |     HIGH: 0.8,
 30 |     MEDIUM: 0.6,
 31 |     MIN_SUBSTRING: 0.7
 32 |   } as const;
 33 | 
 34 |   private repository: NodeRepository;
 35 |   private operationCache: Map<string, { operations: any[], timestamp: number }> = new Map();
 36 |   private suggestionCache: Map<string, OperationSuggestion[]> = new Map();
 37 |   private commonPatterns: Map<string, OperationPattern[]>;
 38 | 
 39 |   constructor(repository: NodeRepository) {
 40 |     this.repository = repository;
 41 |     this.commonPatterns = this.initializeCommonPatterns();
 42 |   }
 43 | 
 44 |   /**
 45 |    * Clean up expired cache entries to prevent memory leaks
 46 |    * Should be called periodically or before cache operations
 47 |    */
 48 |   private cleanupExpiredEntries(): void {
 49 |     const now = Date.now();
 50 | 
 51 |     // Clean operation cache
 52 |     for (const [key, value] of this.operationCache.entries()) {
 53 |       if (now - value.timestamp >= OperationSimilarityService.CACHE_DURATION_MS) {
 54 |         this.operationCache.delete(key);
 55 |       }
 56 |     }
 57 | 
 58 |     // Clean suggestion cache - these don't have timestamps, so clear if cache is too large
 59 |     if (this.suggestionCache.size > 100) {
 60 |       // Keep only the most recent 50 entries
 61 |       const entries = Array.from(this.suggestionCache.entries());
 62 |       this.suggestionCache.clear();
 63 |       entries.slice(-50).forEach(([key, value]) => {
 64 |         this.suggestionCache.set(key, value);
 65 |       });
 66 |     }
 67 |   }
 68 | 
 69 |   /**
 70 |    * Initialize common operation mistake patterns
 71 |    */
 72 |   private initializeCommonPatterns(): Map<string, OperationPattern[]> {
 73 |     const patterns = new Map<string, OperationPattern[]>();
 74 | 
 75 |     // Google Drive patterns
 76 |     patterns.set('googleDrive', [
 77 |       { pattern: 'listFiles', suggestion: 'search', confidence: 0.85, reason: 'Use "search" with resource: "fileFolder" to list files' },
 78 |       { pattern: 'uploadFile', suggestion: 'upload', confidence: 0.95, reason: 'Use "upload" instead of "uploadFile"' },
 79 |       { pattern: 'deleteFile', suggestion: 'deleteFile', confidence: 1.0, reason: 'Exact match' },
 80 |       { pattern: 'downloadFile', suggestion: 'download', confidence: 0.95, reason: 'Use "download" instead of "downloadFile"' },
 81 |       { pattern: 'getFile', suggestion: 'download', confidence: 0.8, reason: 'Use "download" to retrieve file content' },
 82 |       { pattern: 'listFolders', suggestion: 'search', confidence: 0.85, reason: 'Use "search" with resource: "fileFolder"' },
 83 |     ]);
 84 | 
 85 |     // Slack patterns
 86 |     patterns.set('slack', [
 87 |       { pattern: 'sendMessage', suggestion: 'send', confidence: 0.95, reason: 'Use "send" instead of "sendMessage"' },
 88 |       { pattern: 'getMessage', suggestion: 'get', confidence: 0.9, reason: 'Use "get" to retrieve messages' },
 89 |       { pattern: 'postMessage', suggestion: 'send', confidence: 0.9, reason: 'Use "send" to post messages' },
 90 |       { pattern: 'deleteMessage', suggestion: 'delete', confidence: 0.95, reason: 'Use "delete" instead of "deleteMessage"' },
 91 |       { pattern: 'createChannel', suggestion: 'create', confidence: 0.9, reason: 'Use "create" with resource: "channel"' },
 92 |     ]);
 93 | 
 94 |     // Database patterns (postgres, mysql, mongodb)
 95 |     patterns.set('database', [
 96 |       { pattern: 'selectData', suggestion: 'select', confidence: 0.95, reason: 'Use "select" instead of "selectData"' },
 97 |       { pattern: 'insertData', suggestion: 'insert', confidence: 0.95, reason: 'Use "insert" instead of "insertData"' },
 98 |       { pattern: 'updateData', suggestion: 'update', confidence: 0.95, reason: 'Use "update" instead of "updateData"' },
 99 |       { pattern: 'deleteData', suggestion: 'delete', confidence: 0.95, reason: 'Use "delete" instead of "deleteData"' },
100 |       { pattern: 'query', suggestion: 'select', confidence: 0.7, reason: 'Use "select" for queries' },
101 |       { pattern: 'fetch', suggestion: 'select', confidence: 0.7, reason: 'Use "select" to fetch data' },
102 |     ]);
103 | 
104 |     // HTTP patterns
105 |     patterns.set('httpRequest', [
106 |       { pattern: 'fetch', suggestion: 'GET', confidence: 0.8, reason: 'Use "GET" method for fetching data' },
107 |       { pattern: 'send', suggestion: 'POST', confidence: 0.7, reason: 'Use "POST" method for sending data' },
108 |       { pattern: 'create', suggestion: 'POST', confidence: 0.8, reason: 'Use "POST" method for creating resources' },
109 |       { pattern: 'update', suggestion: 'PUT', confidence: 0.8, reason: 'Use "PUT" method for updating resources' },
110 |       { pattern: 'delete', suggestion: 'DELETE', confidence: 0.9, reason: 'Use "DELETE" method' },
111 |     ]);
112 | 
113 |     // Generic patterns
114 |     patterns.set('generic', [
115 |       { pattern: 'list', suggestion: 'get', confidence: 0.6, reason: 'Consider using "get" or "search"' },
116 |       { pattern: 'retrieve', suggestion: 'get', confidence: 0.8, reason: 'Use "get" to retrieve data' },
117 |       { pattern: 'fetch', suggestion: 'get', confidence: 0.8, reason: 'Use "get" to fetch data' },
118 |       { pattern: 'remove', suggestion: 'delete', confidence: 0.85, reason: 'Use "delete" to remove items' },
119 |       { pattern: 'add', suggestion: 'create', confidence: 0.7, reason: 'Use "create" to add new items' },
120 |     ]);
121 | 
122 |     return patterns;
123 |   }
124 | 
125 |   /**
126 |    * Find similar operations for an invalid operation using Levenshtein distance
127 |    * and pattern matching algorithms
128 |    *
129 |    * @param nodeType - The n8n node type (e.g., 'nodes-base.slack')
130 |    * @param invalidOperation - The invalid operation provided by the user
131 |    * @param resource - Optional resource to filter operations
132 |    * @param maxSuggestions - Maximum number of suggestions to return (default: 5)
133 |    * @returns Array of operation suggestions sorted by confidence
134 |    *
135 |    * @example
136 |    * findSimilarOperations('nodes-base.googleDrive', 'listFiles', 'fileFolder')
137 |    * // Returns: [{ value: 'search', confidence: 0.85, reason: 'Use "search" with resource: "fileFolder" to list files' }]
138 |    */
139 |   findSimilarOperations(
140 |     nodeType: string,
141 |     invalidOperation: string,
142 |     resource?: string,
143 |     maxSuggestions: number = OperationSimilarityService.MAX_SUGGESTIONS
144 |   ): OperationSuggestion[] {
145 |     // Clean up expired cache entries periodically
146 |     if (Math.random() < 0.1) { // 10% chance to cleanup on each call
147 |       this.cleanupExpiredEntries();
148 |     }
149 |     // Check cache first
150 |     const cacheKey = `${nodeType}:${invalidOperation}:${resource || ''}`;
151 |     if (this.suggestionCache.has(cacheKey)) {
152 |       return this.suggestionCache.get(cacheKey)!;
153 |     }
154 | 
155 |     const suggestions: OperationSuggestion[] = [];
156 | 
157 |     // Get valid operations for the node
158 |     let nodeInfo;
159 |     try {
160 |       nodeInfo = this.repository.getNode(nodeType);
161 |       if (!nodeInfo) {
162 |         return [];
163 |       }
164 |     } catch (error) {
165 |       logger.warn(`Error getting node ${nodeType}:`, error);
166 |       return [];
167 |     }
168 | 
169 |     const validOperations = this.getNodeOperations(nodeType, resource);
170 | 
171 |     // Early termination for exact match - no suggestions needed
172 |     for (const op of validOperations) {
173 |       const opValue = this.getOperationValue(op);
174 |       if (opValue.toLowerCase() === invalidOperation.toLowerCase()) {
175 |         return []; // Valid operation, no suggestions needed
176 |       }
177 |     }
178 | 
179 |     // Check for exact pattern matches first
180 |     const nodePatterns = this.getNodePatterns(nodeType);
181 |     for (const pattern of nodePatterns) {
182 |       if (pattern.pattern.toLowerCase() === invalidOperation.toLowerCase()) {
183 |         // Type-safe operation value extraction
184 |         const exists = validOperations.some(op => {
185 |           const opValue = this.getOperationValue(op);
186 |           return opValue === pattern.suggestion;
187 |         });
188 |         if (exists) {
189 |           suggestions.push({
190 |             value: pattern.suggestion,
191 |             confidence: pattern.confidence,
192 |             reason: pattern.reason,
193 |             resource
194 |           });
195 |         }
196 |       }
197 |     }
198 | 
199 |     // Calculate similarity for all valid operations
200 |     for (const op of validOperations) {
201 |       const opValue = this.getOperationValue(op);
202 | 
203 |       const similarity = this.calculateSimilarity(invalidOperation, opValue);
204 | 
205 |       if (similarity >= OperationSimilarityService.MIN_CONFIDENCE) {
206 |         // Don't add if already suggested by pattern
207 |         if (!suggestions.some(s => s.value === opValue)) {
208 |           suggestions.push({
209 |             value: opValue,
210 |             confidence: similarity,
211 |             reason: this.getSimilarityReason(similarity, invalidOperation, opValue),
212 |             resource: typeof op === 'object' ? op.resource : undefined,
213 |             description: typeof op === 'object' ? (op.description || op.name) : undefined
214 |           });
215 |         }
216 |       }
217 |     }
218 | 
219 |     // Sort by confidence and limit
220 |     suggestions.sort((a, b) => b.confidence - a.confidence);
221 |     const topSuggestions = suggestions.slice(0, maxSuggestions);
222 | 
223 |     // Cache the result
224 |     this.suggestionCache.set(cacheKey, topSuggestions);
225 | 
226 |     return topSuggestions;
227 |   }
228 | 
229 |   /**
230 |    * Type-safe extraction of operation value from various formats
231 |    * @param op - Operation object or string
232 |    * @returns The operation value as a string
233 |    */
234 |   private getOperationValue(op: any): string {
235 |     if (typeof op === 'string') {
236 |       return op;
237 |     }
238 |     if (typeof op === 'object' && op !== null) {
239 |       return op.operation || op.value || '';
240 |     }
241 |     return '';
242 |   }
243 | 
244 |   /**
245 |    * Type-safe extraction of resource value
246 |    * @param resource - Resource object or string
247 |    * @returns The resource value as a string
248 |    */
249 |   private getResourceValue(resource: any): string {
250 |     if (typeof resource === 'string') {
251 |       return resource;
252 |     }
253 |     if (typeof resource === 'object' && resource !== null) {
254 |       return resource.value || '';
255 |     }
256 |     return '';
257 |   }
258 | 
259 |   /**
260 |    * Get operations for a node, handling resource filtering
261 |    */
262 |   private getNodeOperations(nodeType: string, resource?: string): any[] {
263 |     // Cleanup cache periodically
264 |     if (Math.random() < 0.05) { // 5% chance
265 |       this.cleanupExpiredEntries();
266 |     }
267 | 
268 |     const cacheKey = `${nodeType}:${resource || 'all'}`;
269 |     const cached = this.operationCache.get(cacheKey);
270 | 
271 |     if (cached && Date.now() - cached.timestamp < OperationSimilarityService.CACHE_DURATION_MS) {
272 |       return cached.operations;
273 |     }
274 | 
275 |     const nodeInfo = this.repository.getNode(nodeType);
276 |     if (!nodeInfo) return [];
277 | 
278 |     let operations: any[] = [];
279 | 
280 |     // Parse operations from the node with safe JSON parsing
281 |     try {
282 |       const opsData = nodeInfo.operations;
283 |       if (typeof opsData === 'string') {
284 |         // Safe JSON parsing
285 |         try {
286 |           operations = JSON.parse(opsData);
287 |         } catch (parseError) {
288 |           logger.error(`JSON parse error for operations in ${nodeType}:`, parseError);
289 |           throw ValidationServiceError.jsonParseError(nodeType, parseError as Error);
290 |         }
291 |       } else if (Array.isArray(opsData)) {
292 |         operations = opsData;
293 |       } else if (opsData && typeof opsData === 'object') {
294 |         operations = Object.values(opsData).flat();
295 |       }
296 |     } catch (error) {
297 |       // Re-throw ValidationServiceError, log and continue for others
298 |       if (error instanceof ValidationServiceError) {
299 |         throw error;
300 |       }
301 |       logger.warn(`Failed to process operations for ${nodeType}:`, error);
302 |     }
303 | 
304 |     // Also check properties for operation fields
305 |     try {
306 |       const properties = nodeInfo.properties || [];
307 |       for (const prop of properties) {
308 |         if (prop.name === 'operation' && prop.options) {
309 |           // Filter by resource if specified
310 |           if (prop.displayOptions?.show?.resource) {
311 |             const allowedResources = Array.isArray(prop.displayOptions.show.resource)
312 |               ? prop.displayOptions.show.resource
313 |               : [prop.displayOptions.show.resource];
314 |             // Only filter if a specific resource is requested
315 |             if (resource && !allowedResources.includes(resource)) {
316 |               continue;
317 |             }
318 |             // If no resource specified, include all operations
319 |           }
320 | 
321 |           operations.push(...prop.options.map((opt: any) => ({
322 |             operation: opt.value,
323 |             name: opt.name,
324 |             description: opt.description,
325 |             resource
326 |           })));
327 |         }
328 |       }
329 |     } catch (error) {
330 |       logger.warn(`Failed to extract operations from properties for ${nodeType}:`, error);
331 |     }
332 | 
333 |     // Cache and return
334 |     this.operationCache.set(cacheKey, { operations, timestamp: Date.now() });
335 |     return operations;
336 |   }
337 | 
338 |   /**
339 |    * Get patterns for a specific node type
340 |    */
341 |   private getNodePatterns(nodeType: string): OperationPattern[] {
342 |     const patterns: OperationPattern[] = [];
343 | 
344 |     // Add node-specific patterns
345 |     if (nodeType.includes('googleDrive')) {
346 |       patterns.push(...(this.commonPatterns.get('googleDrive') || []));
347 |     } else if (nodeType.includes('slack')) {
348 |       patterns.push(...(this.commonPatterns.get('slack') || []));
349 |     } else if (nodeType.includes('postgres') || nodeType.includes('mysql') || nodeType.includes('mongodb')) {
350 |       patterns.push(...(this.commonPatterns.get('database') || []));
351 |     } else if (nodeType.includes('httpRequest')) {
352 |       patterns.push(...(this.commonPatterns.get('httpRequest') || []));
353 |     }
354 | 
355 |     // Always add generic patterns
356 |     patterns.push(...(this.commonPatterns.get('generic') || []));
357 | 
358 |     return patterns;
359 |   }
360 | 
361 |   /**
362 |    * Calculate similarity between two strings using Levenshtein distance
363 |    */
364 |   private calculateSimilarity(str1: string, str2: string): number {
365 |     const s1 = str1.toLowerCase();
366 |     const s2 = str2.toLowerCase();
367 | 
368 |     // Exact match
369 |     if (s1 === s2) return 1.0;
370 | 
371 |     // One is substring of the other
372 |     if (s1.includes(s2) || s2.includes(s1)) {
373 |       const ratio = Math.min(s1.length, s2.length) / Math.max(s1.length, s2.length);
374 |       return Math.max(OperationSimilarityService.CONFIDENCE_THRESHOLDS.MIN_SUBSTRING, ratio);
375 |     }
376 | 
377 |     // Calculate Levenshtein distance
378 |     const distance = this.levenshteinDistance(s1, s2);
379 |     const maxLength = Math.max(s1.length, s2.length);
380 | 
381 |     // Convert distance to similarity (0 to 1)
382 |     let similarity = 1 - (distance / maxLength);
383 | 
384 |     // Boost confidence for single character typos and transpositions in short words
385 |     if (distance === 1 && maxLength <= 5) {
386 |       similarity = Math.max(similarity, 0.75);
387 |     } else if (distance === 2 && maxLength <= 5) {
388 |       // Boost for transpositions
389 |       similarity = Math.max(similarity, 0.72);
390 |     }
391 | 
392 |     // Boost similarity for common patterns
393 |     if (this.areCommonVariations(s1, s2)) {
394 |       return Math.min(1.0, similarity + 0.2);
395 |     }
396 | 
397 |     return similarity;
398 |   }
399 | 
400 |   /**
401 |    * Calculate Levenshtein distance between two strings
402 |    */
403 |   private levenshteinDistance(str1: string, str2: string): number {
404 |     const m = str1.length;
405 |     const n = str2.length;
406 |     const dp: number[][] = Array(m + 1).fill(null).map(() => Array(n + 1).fill(0));
407 | 
408 |     for (let i = 0; i <= m; i++) dp[i][0] = i;
409 |     for (let j = 0; j <= n; j++) dp[0][j] = j;
410 | 
411 |     for (let i = 1; i <= m; i++) {
412 |       for (let j = 1; j <= n; j++) {
413 |         if (str1[i - 1] === str2[j - 1]) {
414 |           dp[i][j] = dp[i - 1][j - 1];
415 |         } else {
416 |           dp[i][j] = Math.min(
417 |             dp[i - 1][j] + 1,    // deletion
418 |             dp[i][j - 1] + 1,    // insertion
419 |             dp[i - 1][j - 1] + 1 // substitution
420 |           );
421 |         }
422 |       }
423 |     }
424 | 
425 |     return dp[m][n];
426 |   }
427 | 
428 |   /**
429 |    * Check if two strings are common variations
430 |    */
431 |   private areCommonVariations(str1: string, str2: string): boolean {
432 |     // Handle edge cases first
433 |     if (str1 === '' || str2 === '' || str1 === str2) {
434 |       return false;
435 |     }
436 | 
437 |     // Check for common prefixes/suffixes
438 |     const commonPrefixes = ['get', 'set', 'create', 'delete', 'update', 'send', 'fetch'];
439 |     const commonSuffixes = ['data', 'item', 'record', 'message', 'file', 'folder'];
440 | 
441 |     for (const prefix of commonPrefixes) {
442 |       if ((str1.startsWith(prefix) && !str2.startsWith(prefix)) ||
443 |           (!str1.startsWith(prefix) && str2.startsWith(prefix))) {
444 |         const s1Clean = str1.startsWith(prefix) ? str1.slice(prefix.length) : str1;
445 |         const s2Clean = str2.startsWith(prefix) ? str2.slice(prefix.length) : str2;
446 |         // Only return true if at least one string was actually cleaned (not empty after cleaning)
447 |         if ((str1.startsWith(prefix) && s1Clean !== str1) || (str2.startsWith(prefix) && s2Clean !== str2)) {
448 |           if (s1Clean === s2Clean || this.levenshteinDistance(s1Clean, s2Clean) <= 2) {
449 |             return true;
450 |           }
451 |         }
452 |       }
453 |     }
454 | 
455 |     for (const suffix of commonSuffixes) {
456 |       if ((str1.endsWith(suffix) && !str2.endsWith(suffix)) ||
457 |           (!str1.endsWith(suffix) && str2.endsWith(suffix))) {
458 |         const s1Clean = str1.endsWith(suffix) ? str1.slice(0, -suffix.length) : str1;
459 |         const s2Clean = str2.endsWith(suffix) ? str2.slice(0, -suffix.length) : str2;
460 |         // Only return true if at least one string was actually cleaned (not empty after cleaning)
461 |         if ((str1.endsWith(suffix) && s1Clean !== str1) || (str2.endsWith(suffix) && s2Clean !== str2)) {
462 |           if (s1Clean === s2Clean || this.levenshteinDistance(s1Clean, s2Clean) <= 2) {
463 |             return true;
464 |           }
465 |         }
466 |       }
467 |     }
468 | 
469 |     return false;
470 |   }
471 | 
472 |   /**
473 |    * Generate a human-readable reason for the similarity
474 |    * @param confidence - Similarity confidence score
475 |    * @param invalid - The invalid operation string
476 |    * @param valid - The valid operation string
477 |    * @returns Human-readable explanation of the similarity
478 |    */
479 |   private getSimilarityReason(confidence: number, invalid: string, valid: string): string {
480 |     const { VERY_HIGH, HIGH, MEDIUM } = OperationSimilarityService.CONFIDENCE_THRESHOLDS;
481 | 
482 |     if (confidence >= VERY_HIGH) {
483 |       return 'Almost exact match - likely a typo';
484 |     } else if (confidence >= HIGH) {
485 |       return 'Very similar - common variation';
486 |     } else if (confidence >= MEDIUM) {
487 |       return 'Similar operation';
488 |     } else if (invalid.includes(valid) || valid.includes(invalid)) {
489 |       return 'Partial match';
490 |     } else {
491 |       return 'Possibly related operation';
492 |     }
493 |   }
494 | 
495 |   /**
496 |    * Clear caches
497 |    */
498 |   clearCache(): void {
499 |     this.operationCache.clear();
500 |     this.suggestionCache.clear();
501 |   }
502 | }
```

--------------------------------------------------------------------------------
/tests/integration/database/test-utils.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import * as fs from 'fs';
  2 | import * as path from 'path';
  3 | import Database from 'better-sqlite3';
  4 | import { execSync } from 'child_process';
  5 | import type { DatabaseAdapter } from '../../../src/database/database-adapter';
  6 | 
  7 | /**
  8 |  * Configuration options for creating test databases
  9 |  */
 10 | export interface TestDatabaseOptions {
 11 |   /** Database mode - in-memory for fast tests, file for persistence tests */
 12 |   mode: 'memory' | 'file';
 13 |   /** Custom database filename (only for file mode) */
 14 |   name?: string;
 15 |   /** Enable Write-Ahead Logging for better concurrency (file mode only) */
 16 |   enableWAL?: boolean;
 17 |   /** Enable FTS5 full-text search extension */
 18 |   enableFTS5?: boolean;
 19 | }
 20 | 
 21 | /**
 22 |  * Test database utility for creating isolated database instances for testing.
 23 |  * Provides automatic schema setup, cleanup, and various helper methods.
 24 |  * 
 25 |  * @example
 26 |  * ```typescript
 27 |  * // Create in-memory database for unit tests
 28 |  * const testDb = await TestDatabase.createIsolated({ mode: 'memory' });
 29 |  * const db = testDb.getDatabase();
 30 |  * // ... run tests
 31 |  * await testDb.cleanup();
 32 |  * 
 33 |  * // Create file-based database for integration tests
 34 |  * const testDb = await TestDatabase.createIsolated({ 
 35 |  *   mode: 'file',
 36 |  *   enableWAL: true 
 37 |  * });
 38 |  * ```
 39 |  */
 40 | export class TestDatabase {
 41 |   private db: Database.Database | null = null;
 42 |   private dbPath?: string;
 43 |   private options: TestDatabaseOptions;
 44 | 
 45 |   constructor(options: TestDatabaseOptions = { mode: 'memory' }) {
 46 |     this.options = options;
 47 |   }
 48 | 
 49 |   /**
 50 |    * Creates an isolated test database instance with automatic cleanup.
 51 |    * Each instance gets a unique name to prevent conflicts in parallel tests.
 52 |    * 
 53 |    * @param options - Database configuration options
 54 |    * @returns Promise resolving to initialized TestDatabase instance
 55 |    */
 56 |   static async createIsolated(options: TestDatabaseOptions = { mode: 'memory' }): Promise<TestDatabase> {
 57 |     const testDb = new TestDatabase({
 58 |       ...options,
 59 |       name: options.name || `isolated-${Date.now()}-${Math.random().toString(36).substr(2, 9)}.db`
 60 |     });
 61 |     await testDb.initialize();
 62 |     return testDb;
 63 |   }
 64 | 
 65 |   async initialize(): Promise<Database.Database> {
 66 |     if (this.db) return this.db;
 67 | 
 68 |     if (this.options.mode === 'file') {
 69 |       const testDir = path.join(__dirname, '../../../.test-dbs');
 70 |       if (!fs.existsSync(testDir)) {
 71 |         fs.mkdirSync(testDir, { recursive: true });
 72 |       }
 73 |       this.dbPath = path.join(testDir, this.options.name || `test-${Date.now()}.db`);
 74 |       this.db = new Database(this.dbPath);
 75 |     } else {
 76 |       this.db = new Database(':memory:');
 77 |     }
 78 | 
 79 |     // Enable WAL mode for file databases
 80 |     if (this.options.mode === 'file' && this.options.enableWAL !== false) {
 81 |       this.db.exec('PRAGMA journal_mode = WAL');
 82 |     }
 83 | 
 84 |     // Load FTS5 extension if requested
 85 |     if (this.options.enableFTS5) {
 86 |       // FTS5 is built into SQLite by default in better-sqlite3
 87 |       try {
 88 |         this.db.exec('CREATE VIRTUAL TABLE test_fts USING fts5(content)');
 89 |         this.db.exec('DROP TABLE test_fts');
 90 |       } catch (error) {
 91 |         throw new Error('FTS5 extension not available');
 92 |       }
 93 |     }
 94 | 
 95 |     // Apply schema
 96 |     await this.applySchema();
 97 | 
 98 |     return this.db;
 99 |   }
100 | 
101 |   private async applySchema(): Promise<void> {
102 |     if (!this.db) throw new Error('Database not initialized');
103 | 
104 |     const schemaPath = path.join(__dirname, '../../../src/database/schema.sql');
105 |     const schema = fs.readFileSync(schemaPath, 'utf-8');
106 | 
107 |     // Parse SQL statements properly (handles BEGIN...END blocks in triggers)
108 |     const statements = this.parseSQLStatements(schema);
109 | 
110 |     for (const statement of statements) {
111 |       this.db.exec(statement);
112 |     }
113 |   }
114 | 
115 |   /**
116 |    * Parse SQL statements from schema file, properly handling multi-line statements
117 |    * including triggers with BEGIN...END blocks
118 |    */
119 |   private parseSQLStatements(sql: string): string[] {
120 |     const statements: string[] = [];
121 |     let current = '';
122 |     let inBlock = false;
123 | 
124 |     const lines = sql.split('\n');
125 | 
126 |     for (const line of lines) {
127 |       const trimmed = line.trim().toUpperCase();
128 | 
129 |       // Skip comments and empty lines
130 |       if (trimmed.startsWith('--') || trimmed === '') {
131 |         continue;
132 |       }
133 | 
134 |       // Track BEGIN...END blocks (triggers, procedures)
135 |       if (trimmed.includes('BEGIN')) {
136 |         inBlock = true;
137 |       }
138 | 
139 |       current += line + '\n';
140 | 
141 |       // End of block (trigger/procedure)
142 |       if (inBlock && trimmed === 'END;') {
143 |         statements.push(current.trim());
144 |         current = '';
145 |         inBlock = false;
146 |         continue;
147 |       }
148 | 
149 |       // Regular statement end (not in block)
150 |       if (!inBlock && trimmed.endsWith(';')) {
151 |         statements.push(current.trim());
152 |         current = '';
153 |       }
154 |     }
155 | 
156 |     // Add any remaining content
157 |     if (current.trim()) {
158 |       statements.push(current.trim());
159 |     }
160 | 
161 |     return statements.filter(s => s.length > 0);
162 |   }
163 | 
164 |   /**
165 |    * Gets the underlying better-sqlite3 database instance.
166 |    * @throws Error if database is not initialized
167 |    * @returns The database instance
168 |    */
169 |   getDatabase(): Database.Database {
170 |     if (!this.db) throw new Error('Database not initialized');
171 |     return this.db;
172 |   }
173 | 
174 |   /**
175 |    * Cleans up the database connection and removes any created files.
176 |    * Should be called in afterEach/afterAll hooks to prevent resource leaks.
177 |    */
178 |   async cleanup(): Promise<void> {
179 |     if (this.db) {
180 |       this.db.close();
181 |       this.db = null;
182 |     }
183 | 
184 |     if (this.dbPath && fs.existsSync(this.dbPath)) {
185 |       fs.unlinkSync(this.dbPath);
186 |       // Also remove WAL and SHM files if they exist
187 |       const walPath = `${this.dbPath}-wal`;
188 |       const shmPath = `${this.dbPath}-shm`;
189 |       if (fs.existsSync(walPath)) fs.unlinkSync(walPath);
190 |       if (fs.existsSync(shmPath)) fs.unlinkSync(shmPath);
191 |     }
192 |   }
193 | 
194 |   /**
195 |    * Checks if the database is currently locked by another process.
196 |    * Useful for testing concurrent access scenarios.
197 |    * 
198 |    * @returns true if database is locked, false otherwise
199 |    */
200 |   isLocked(): boolean {
201 |     if (!this.db) return false;
202 |     try {
203 |       this.db.exec('BEGIN IMMEDIATE');
204 |       this.db.exec('ROLLBACK');
205 |       return false;
206 |     } catch (error: any) {
207 |       return error.code === 'SQLITE_BUSY';
208 |     }
209 |   }
210 | }
211 | 
212 | /**
213 |  * Performance monitoring utility for measuring test execution times.
214 |  * Collects timing data and provides statistical analysis.
215 |  * 
216 |  * @example
217 |  * ```typescript
218 |  * const monitor = new PerformanceMonitor();
219 |  * 
220 |  * // Measure single operation
221 |  * const stop = monitor.start('database-query');
222 |  * await db.query('SELECT * FROM nodes');
223 |  * stop();
224 |  * 
225 |  * // Get statistics
226 |  * const stats = monitor.getStats('database-query');
227 |  * console.log(`Average: ${stats.average}ms`);
228 |  * ```
229 |  */
230 | export class PerformanceMonitor {
231 |   private measurements: Map<string, number[]> = new Map();
232 | 
233 |   /**
234 |    * Starts timing for a labeled operation.
235 |    * Returns a function that should be called to stop timing.
236 |    * 
237 |    * @param label - Unique label for the operation being measured
238 |    * @returns Stop function to call when operation completes
239 |    */
240 |   start(label: string): () => void {
241 |     const startTime = process.hrtime.bigint();
242 |     return () => {
243 |       const endTime = process.hrtime.bigint();
244 |       const duration = Number(endTime - startTime) / 1_000_000; // Convert to milliseconds
245 |       
246 |       if (!this.measurements.has(label)) {
247 |         this.measurements.set(label, []);
248 |       }
249 |       this.measurements.get(label)!.push(duration);
250 |     };
251 |   }
252 | 
253 |   /**
254 |    * Gets statistical analysis of all measurements for a given label.
255 |    * 
256 |    * @param label - The operation label to get stats for
257 |    * @returns Statistics object or null if no measurements exist
258 |    */
259 |   getStats(label: string): {
260 |     count: number;
261 |     total: number;
262 |     average: number;
263 |     min: number;
264 |     max: number;
265 |     median: number;
266 |   } | null {
267 |     const durations = this.measurements.get(label);
268 |     if (!durations || durations.length === 0) return null;
269 | 
270 |     const sorted = [...durations].sort((a, b) => a - b);
271 |     const total = durations.reduce((sum, d) => sum + d, 0);
272 | 
273 |     return {
274 |       count: durations.length,
275 |       total,
276 |       average: total / durations.length,
277 |       min: sorted[0],
278 |       max: sorted[sorted.length - 1],
279 |       median: sorted[Math.floor(sorted.length / 2)]
280 |     };
281 |   }
282 | 
283 |   /**
284 |    * Clears all collected measurements.
285 |    */
286 |   clear(): void {
287 |     this.measurements.clear();
288 |   }
289 | }
290 | 
291 | /**
292 |  * Test data generator for creating mock nodes, templates, and other test objects.
293 |  * Provides consistent test data with sensible defaults and easy customization.
294 |  */
295 | export class TestDataGenerator {
296 |   /**
297 |    * Generates a mock node object with default values and custom overrides.
298 |    * 
299 |    * @param overrides - Properties to override in the generated node
300 |    * @returns Complete node object suitable for testing
301 |    * 
302 |    * @example
303 |    * ```typescript
304 |    * const node = TestDataGenerator.generateNode({
305 |    *   displayName: 'Custom Node',
306 |    *   isAITool: true
307 |    * });
308 |    * ```
309 |    */
310 |   static generateNode(overrides: any = {}): any {
311 |     const nodeName = overrides.name || `testNode${Math.random().toString(36).substr(2, 9)}`;
312 |     return {
313 |       nodeType: overrides.nodeType || `n8n-nodes-base.${nodeName}`,
314 |       packageName: overrides.packageName || overrides.package || 'n8n-nodes-base',
315 |       displayName: overrides.displayName || 'Test Node',
316 |       description: overrides.description || 'A test node for integration testing',
317 |       category: overrides.category || 'automation',
318 |       developmentStyle: overrides.developmentStyle || overrides.style || 'programmatic',
319 |       isAITool: overrides.isAITool || false,
320 |       isTrigger: overrides.isTrigger || false,
321 |       isWebhook: overrides.isWebhook || false,
322 |       isVersioned: overrides.isVersioned !== undefined ? overrides.isVersioned : true,
323 |       version: overrides.version || '1',
324 |       documentation: overrides.documentation || null,
325 |       properties: overrides.properties || [],
326 |       operations: overrides.operations || [],
327 |       credentials: overrides.credentials || [],
328 |       ...overrides
329 |     };
330 |   }
331 | 
332 |   /**
333 |    * Generates multiple nodes with sequential naming.
334 |    * 
335 |    * @param count - Number of nodes to generate
336 |    * @param template - Common properties to apply to all nodes
337 |    * @returns Array of generated nodes
338 |    */
339 |   static generateNodes(count: number, template: any = {}): any[] {
340 |     return Array.from({ length: count }, (_, i) => 
341 |       this.generateNode({
342 |         ...template,
343 |         name: `testNode${i}`,
344 |         displayName: `Test Node ${i}`,
345 |         nodeType: `n8n-nodes-base.testNode${i}`
346 |       })
347 |     );
348 |   }
349 | 
350 |   /**
351 |    * Generates a mock workflow template.
352 |    * 
353 |    * @param overrides - Properties to override in the template
354 |    * @returns Template object suitable for testing
355 |    */
356 |   static generateTemplate(overrides: any = {}): any {
357 |     return {
358 |       id: Math.floor(Math.random() * 100000),
359 |       name: `Test Workflow ${Math.random().toString(36).substr(2, 9)}`,
360 |       totalViews: Math.floor(Math.random() * 1000),
361 |       nodeTypes: ['n8n-nodes-base.webhook', 'n8n-nodes-base.httpRequest'],
362 |       categories: [{ id: 1, name: 'automation' }],
363 |       description: 'A test workflow template',
364 |       workflowInfo: {
365 |         nodeCount: 5,
366 |         webhookCount: 1
367 |       },
368 |       ...overrides
369 |     };
370 |   }
371 | 
372 |   /**
373 |    * Generates multiple workflow templates.
374 |    * 
375 |    * @param count - Number of templates to generate
376 |    * @returns Array of template objects
377 |    */
378 |   static generateTemplates(count: number): any[] {
379 |     return Array.from({ length: count }, () => this.generateTemplate());
380 |   }
381 | }
382 | 
383 | /**
384 |  * Runs a function within a database transaction with automatic rollback on error.
385 |  * Useful for testing transactional behavior and ensuring test isolation.
386 |  * 
387 |  * @param db - Database instance
388 |  * @param fn - Function to run within transaction
389 |  * @returns Promise resolving to function result
390 |  * @throws Rolls back transaction and rethrows any errors
391 |  * 
392 |  * @example
393 |  * ```typescript
394 |  * await runInTransaction(db, () => {
395 |  *   db.prepare('INSERT INTO nodes ...').run();
396 |  *   db.prepare('UPDATE nodes ...').run();
397 |  *   // If any operation fails, all are rolled back
398 |  * });
399 |  * ```
400 |  */
401 | export async function runInTransaction<T>(
402 |   db: Database.Database,
403 |   fn: () => T
404 | ): Promise<T> {
405 |   db.exec('BEGIN');
406 |   try {
407 |     const result = await fn();
408 |     db.exec('COMMIT');
409 |     return result;
410 |   } catch (error) {
411 |     db.exec('ROLLBACK');
412 |     throw error;
413 |   }
414 | }
415 | 
416 | /**
417 |  * Simulates concurrent database access using worker processes.
418 |  * Useful for testing database locking and concurrency handling.
419 |  * 
420 |  * @param dbPath - Path to the database file
421 |  * @param workerCount - Number of concurrent workers to spawn
422 |  * @param operations - Number of operations each worker should perform
423 |  * @param workerScript - JavaScript code to execute in each worker
424 |  * @returns Results with success/failure counts and total duration
425 |  * 
426 |  * @example
427 |  * ```typescript
428 |  * const results = await simulateConcurrentAccess(
429 |  *   dbPath,
430 |  *   10, // 10 workers
431 |  *   100, // 100 operations each
432 |  *   `
433 |  *     const db = require('better-sqlite3')(process.env.DB_PATH);
434 |  *     for (let i = 0; i < process.env.OPERATIONS; i++) {
435 |  *       db.prepare('INSERT INTO test VALUES (?)').run(i);
436 |  *     }
437 |  *   `
438 |  * );
439 |  * ```
440 |  */
441 | export async function simulateConcurrentAccess(
442 |   dbPath: string,
443 |   workerCount: number,
444 |   operations: number,
445 |   workerScript: string
446 | ): Promise<{ success: number; failed: number; duration: number }> {
447 |   const startTime = Date.now();
448 |   const results = { success: 0, failed: 0 };
449 | 
450 |   // Create worker processes
451 |   const workers = Array.from({ length: workerCount }, (_, i) => {
452 |     return new Promise<void>((resolve) => {
453 |       try {
454 |         const output = execSync(
455 |           `node -e "${workerScript}"`,
456 |           {
457 |             env: {
458 |               ...process.env,
459 |               DB_PATH: dbPath,
460 |               WORKER_ID: i.toString(),
461 |               OPERATIONS: operations.toString()
462 |             }
463 |           }
464 |         );
465 |         results.success++;
466 |       } catch (error) {
467 |         results.failed++;
468 |       }
469 |       resolve();
470 |     });
471 |   });
472 | 
473 |   await Promise.all(workers);
474 | 
475 |   return {
476 |     ...results,
477 |     duration: Date.now() - startTime
478 |   };
479 | }
480 | 
481 | /**
482 |  * Performs comprehensive database integrity checks including foreign keys and schema.
483 |  * 
484 |  * @param db - Database instance to check
485 |  * @returns Object with validation status and any error messages
486 |  * 
487 |  * @example
488 |  * ```typescript
489 |  * const integrity = checkDatabaseIntegrity(db);
490 |  * if (!integrity.isValid) {
491 |  *   console.error('Database issues:', integrity.errors);
492 |  * }
493 |  * ```
494 |  */
495 | export function checkDatabaseIntegrity(db: Database.Database): {
496 |   isValid: boolean;
497 |   errors: string[];
498 | } {
499 |   const errors: string[] = [];
500 | 
501 |   try {
502 |     // Run integrity check
503 |     const result = db.prepare('PRAGMA integrity_check').all() as Array<{ integrity_check: string }>;
504 |     if (result.length !== 1 || result[0].integrity_check !== 'ok') {
505 |       errors.push('Database integrity check failed');
506 |     }
507 | 
508 |     // Check foreign key constraints
509 |     const fkResult = db.prepare('PRAGMA foreign_key_check').all();
510 |     if (fkResult.length > 0) {
511 |       errors.push(`Foreign key violations: ${JSON.stringify(fkResult)}`);
512 |     }
513 | 
514 |     // Check table existence
515 |     const tables = db.prepare(`
516 |       SELECT name FROM sqlite_master 
517 |       WHERE type = 'table' AND name = 'nodes'
518 |     `).all();
519 |     
520 |     if (tables.length === 0) {
521 |       errors.push('nodes table does not exist');
522 |     }
523 | 
524 |   } catch (error: any) {
525 |     errors.push(`Integrity check error: ${error.message}`);
526 |   }
527 | 
528 |   return {
529 |     isValid: errors.length === 0,
530 |     errors
531 |   };
532 | }
533 | 
534 | /**
535 |  * Creates a DatabaseAdapter interface from a better-sqlite3 instance.
536 |  * This adapter provides a consistent interface for database operations across the codebase.
537 |  * 
538 |  * @param db - better-sqlite3 database instance
539 |  * @returns DatabaseAdapter implementation
540 |  * 
541 |  * @example
542 |  * ```typescript
543 |  * const db = new Database(':memory:');
544 |  * const adapter = createTestDatabaseAdapter(db);
545 |  * const stmt = adapter.prepare('SELECT * FROM nodes WHERE type = ?');
546 |  * const nodes = stmt.all('webhook');
547 |  * ```
548 |  */
549 | export function createTestDatabaseAdapter(db: Database.Database): DatabaseAdapter {
550 |   return {
551 |     prepare: (sql: string) => {
552 |       const stmt = db.prepare(sql);
553 |       return {
554 |         run: (...params: any[]) => stmt.run(...params),
555 |         get: (...params: any[]) => stmt.get(...params),
556 |         all: (...params: any[]) => stmt.all(...params),
557 |         iterate: (...params: any[]) => stmt.iterate(...params),
558 |         pluck: function(enabled?: boolean) { stmt.pluck(enabled); return this; },
559 |         expand: function(enabled?: boolean) { stmt.expand?.(enabled); return this; },
560 |         raw: function(enabled?: boolean) { stmt.raw?.(enabled); return this; },
561 |         columns: () => stmt.columns?.() || [],
562 |         bind: function(...params: any[]) { stmt.bind(...params); return this; }
563 |       } as any;
564 |     },
565 |     exec: (sql: string) => db.exec(sql),
566 |     close: () => db.close(),
567 |     pragma: (key: string, value?: any) => db.pragma(key, value),
568 |     get inTransaction() { return db.inTransaction; },
569 |     transaction: <T>(fn: () => T) => db.transaction(fn)(),
570 |     checkFTS5Support: () => {
571 |       try {
572 |         db.exec('CREATE VIRTUAL TABLE test_fts5_check USING fts5(content)');
573 |         db.exec('DROP TABLE test_fts5_check');
574 |         return true;
575 |       } catch {
576 |         return false;
577 |       }
578 |     }
579 |   };
580 | }
581 | 
582 | /**
583 |  * Pre-configured mock nodes for common testing scenarios.
584 |  * These represent the most commonly used n8n nodes with realistic configurations.
585 |  */
586 | export const MOCK_NODES = {
587 |   webhook: {
588 |     nodeType: 'n8n-nodes-base.webhook',
589 |     packageName: 'n8n-nodes-base',
590 |     displayName: 'Webhook',
591 |     description: 'Starts the workflow when a webhook is called',
592 |     category: 'trigger',
593 |     developmentStyle: 'programmatic',
594 |     isAITool: false,
595 |     isTrigger: true,
596 |     isWebhook: true,
597 |     isVersioned: true,
598 |     version: '1',
599 |     documentation: 'Webhook documentation',
600 |     properties: [
601 |       {
602 |         displayName: 'HTTP Method',
603 |         name: 'httpMethod',
604 |         type: 'options',
605 |         options: [
606 |           { name: 'GET', value: 'GET' },
607 |           { name: 'POST', value: 'POST' }
608 |         ],
609 |         default: 'GET'
610 |       }
611 |     ],
612 |     operations: [],
613 |     credentials: []
614 |   },
615 |   httpRequest: {
616 |     nodeType: 'n8n-nodes-base.httpRequest',
617 |     packageName: 'n8n-nodes-base',
618 |     displayName: 'HTTP Request',
619 |     description: 'Makes an HTTP request and returns the response',
620 |     category: 'automation',
621 |     developmentStyle: 'programmatic',
622 |     isAITool: false,
623 |     isTrigger: false,
624 |     isWebhook: false,
625 |     isVersioned: true,
626 |     version: '1',
627 |     documentation: 'HTTP Request documentation',
628 |     properties: [
629 |       {
630 |         displayName: 'URL',
631 |         name: 'url',
632 |         type: 'string',
633 |         required: true,
634 |         default: ''
635 |       }
636 |     ],
637 |     operations: [],
638 |     credentials: []
639 |   }
640 | };
```
Page 27/59FirstPrevNextLast