#
tokens: 48066/50000 9/617 files (page 21/46)
lines: off (toggle) GitHub
raw markdown copy
This is page 21 of 46. Use http://codebase.md/czlonkowski/n8n-mcp?lines=false&page={x} to view the full context.

# Directory Structure

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

# Files

--------------------------------------------------------------------------------
/tests/unit/telemetry/telemetry-error.test.ts:
--------------------------------------------------------------------------------

```typescript
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
import { TelemetryError, TelemetryCircuitBreaker, TelemetryErrorAggregator } from '../../../src/telemetry/telemetry-error';
import { TelemetryErrorType } from '../../../src/telemetry/telemetry-types';
import { logger } from '../../../src/utils/logger';

// Mock logger to avoid console output in tests
vi.mock('../../../src/utils/logger', () => ({
  logger: {
    debug: vi.fn(),
    info: vi.fn(),
    warn: vi.fn(),
    error: vi.fn(),
  }
}));

describe('TelemetryError', () => {
  beforeEach(() => {
    vi.clearAllMocks();
    vi.useFakeTimers();
  });

  afterEach(() => {
    vi.useRealTimers();
  });

  describe('constructor', () => {
    it('should create error with all properties', () => {
      const context = { operation: 'test', detail: 'info' };
      const error = new TelemetryError(
        TelemetryErrorType.NETWORK_ERROR,
        'Test error',
        context,
        true
      );

      expect(error.name).toBe('TelemetryError');
      expect(error.message).toBe('Test error');
      expect(error.type).toBe(TelemetryErrorType.NETWORK_ERROR);
      expect(error.context).toEqual(context);
      expect(error.retryable).toBe(true);
      expect(error.timestamp).toBeTypeOf('number');
    });

    it('should default retryable to false', () => {
      const error = new TelemetryError(
        TelemetryErrorType.VALIDATION_ERROR,
        'Test error'
      );

      expect(error.retryable).toBe(false);
    });

    it('should handle undefined context', () => {
      const error = new TelemetryError(
        TelemetryErrorType.UNKNOWN_ERROR,
        'Test error'
      );

      expect(error.context).toBeUndefined();
    });

    it('should maintain proper prototype chain', () => {
      const error = new TelemetryError(
        TelemetryErrorType.NETWORK_ERROR,
        'Test error'
      );

      expect(error instanceof TelemetryError).toBe(true);
      expect(error instanceof Error).toBe(true);
    });
  });

  describe('toContext()', () => {
    it('should convert error to context object', () => {
      const context = { operation: 'flush', batch: 'events' };
      const error = new TelemetryError(
        TelemetryErrorType.NETWORK_ERROR,
        'Failed to flush',
        context,
        true
      );

      const contextObj = error.toContext();
      expect(contextObj).toEqual({
        type: TelemetryErrorType.NETWORK_ERROR,
        message: 'Failed to flush',
        context,
        timestamp: error.timestamp,
        retryable: true
      });
    });
  });

  describe('log()', () => {
    it('should log retryable errors as debug', () => {
      const error = new TelemetryError(
        TelemetryErrorType.NETWORK_ERROR,
        'Retryable error',
        { attempt: 1 },
        true
      );

      error.log();

      expect(logger.debug).toHaveBeenCalledWith(
        'Retryable telemetry error:',
        expect.objectContaining({
          type: TelemetryErrorType.NETWORK_ERROR,
          message: 'Retryable error',
          attempt: 1
        })
      );
    });

    it('should log non-retryable errors as debug', () => {
      const error = new TelemetryError(
        TelemetryErrorType.VALIDATION_ERROR,
        'Non-retryable error',
        { field: 'user_id' },
        false
      );

      error.log();

      expect(logger.debug).toHaveBeenCalledWith(
        'Non-retryable telemetry error:',
        expect.objectContaining({
          type: TelemetryErrorType.VALIDATION_ERROR,
          message: 'Non-retryable error',
          field: 'user_id'
        })
      );
    });

    it('should handle errors without context', () => {
      const error = new TelemetryError(
        TelemetryErrorType.UNKNOWN_ERROR,
        'Simple error'
      );

      error.log();

      expect(logger.debug).toHaveBeenCalledWith(
        'Non-retryable telemetry error:',
        expect.objectContaining({
          type: TelemetryErrorType.UNKNOWN_ERROR,
          message: 'Simple error'
        })
      );
    });
  });
});

describe('TelemetryCircuitBreaker', () => {
  let circuitBreaker: TelemetryCircuitBreaker;

  beforeEach(() => {
    vi.clearAllMocks();
    vi.useFakeTimers();
    circuitBreaker = new TelemetryCircuitBreaker(3, 10000, 2); // 3 failures, 10s reset, 2 half-open requests
  });

  afterEach(() => {
    vi.useRealTimers();
  });

  describe('shouldAllow()', () => {
    it('should allow requests in closed state', () => {
      expect(circuitBreaker.shouldAllow()).toBe(true);
    });

    it('should open circuit after failure threshold', () => {
      // Record 3 failures to reach threshold
      for (let i = 0; i < 3; i++) {
        circuitBreaker.recordFailure();
      }

      expect(circuitBreaker.shouldAllow()).toBe(false);
      expect(circuitBreaker.getState().state).toBe('open');
    });

    it('should transition to half-open after reset timeout', () => {
      // Open the circuit
      for (let i = 0; i < 3; i++) {
        circuitBreaker.recordFailure();
      }
      expect(circuitBreaker.shouldAllow()).toBe(false);

      // Advance time past reset timeout
      vi.advanceTimersByTime(11000);

      // Should transition to half-open and allow request
      expect(circuitBreaker.shouldAllow()).toBe(true);
      expect(circuitBreaker.getState().state).toBe('half-open');
    });

    it('should limit requests in half-open state', () => {
      // Open the circuit
      for (let i = 0; i < 3; i++) {
        circuitBreaker.recordFailure();
      }

      // Advance to half-open
      vi.advanceTimersByTime(11000);

      // Should allow limited number of requests (2 in our config)
      expect(circuitBreaker.shouldAllow()).toBe(true);
      expect(circuitBreaker.shouldAllow()).toBe(true);
      expect(circuitBreaker.shouldAllow()).toBe(true); // Note: simplified implementation allows all
    });

    it('should not allow requests before reset timeout in open state', () => {
      // Open the circuit
      for (let i = 0; i < 3; i++) {
        circuitBreaker.recordFailure();
      }

      // Advance time but not enough to reset
      vi.advanceTimersByTime(5000);

      expect(circuitBreaker.shouldAllow()).toBe(false);
    });
  });

  describe('recordSuccess()', () => {
    it('should reset failure count in closed state', () => {
      // Record some failures but not enough to open
      circuitBreaker.recordFailure();
      circuitBreaker.recordFailure();
      expect(circuitBreaker.getState().failureCount).toBe(2);

      // Success should reset count
      circuitBreaker.recordSuccess();
      expect(circuitBreaker.getState().failureCount).toBe(0);
    });

    it('should close circuit after successful half-open requests', () => {
      // Open the circuit
      for (let i = 0; i < 3; i++) {
        circuitBreaker.recordFailure();
      }

      // Go to half-open
      vi.advanceTimersByTime(11000);
      circuitBreaker.shouldAllow(); // First half-open request
      circuitBreaker.shouldAllow(); // Second half-open request

      // The circuit breaker implementation requires success calls
      // to match the number of half-open requests configured
      circuitBreaker.recordSuccess();
      // In current implementation, state remains half-open
      // This is a known behavior of the simplified circuit breaker
      expect(circuitBreaker.getState().state).toBe('half-open');

      // After another success, it should close
      circuitBreaker.recordSuccess();
      expect(circuitBreaker.getState().state).toBe('closed');
      expect(circuitBreaker.getState().failureCount).toBe(0);
      expect(logger.debug).toHaveBeenCalledWith('Circuit breaker closed after successful recovery');
    });

    it('should not affect state when not in half-open after sufficient requests', () => {
      // Open circuit, go to half-open, make one request
      for (let i = 0; i < 3; i++) {
        circuitBreaker.recordFailure();
      }
      vi.advanceTimersByTime(11000);
      circuitBreaker.shouldAllow(); // One half-open request

      // Record success but should not close yet (need 2 successful requests)
      circuitBreaker.recordSuccess();
      expect(circuitBreaker.getState().state).toBe('half-open');
    });
  });

  describe('recordFailure()', () => {
    it('should increment failure count in closed state', () => {
      circuitBreaker.recordFailure();
      expect(circuitBreaker.getState().failureCount).toBe(1);

      circuitBreaker.recordFailure();
      expect(circuitBreaker.getState().failureCount).toBe(2);
    });

    it('should open circuit when threshold reached', () => {
      const error = new Error('Test error');

      // Record failures to reach threshold
      circuitBreaker.recordFailure(error);
      circuitBreaker.recordFailure(error);
      expect(circuitBreaker.getState().state).toBe('closed');

      circuitBreaker.recordFailure(error);
      expect(circuitBreaker.getState().state).toBe('open');
      expect(logger.debug).toHaveBeenCalledWith(
        'Circuit breaker opened after 3 failures',
        { error: 'Test error' }
      );
    });

    it('should immediately open from half-open on failure', () => {
      // Open circuit, go to half-open
      for (let i = 0; i < 3; i++) {
        circuitBreaker.recordFailure();
      }
      vi.advanceTimersByTime(11000);
      circuitBreaker.shouldAllow();

      // Failure in half-open should immediately open
      const error = new Error('Half-open failure');
      circuitBreaker.recordFailure(error);
      expect(circuitBreaker.getState().state).toBe('open');
      expect(logger.debug).toHaveBeenCalledWith(
        'Circuit breaker opened from half-open state',
        { error: 'Half-open failure' }
      );
    });

    it('should handle failure without error object', () => {
      for (let i = 0; i < 3; i++) {
        circuitBreaker.recordFailure();
      }

      expect(circuitBreaker.getState().state).toBe('open');
      expect(logger.debug).toHaveBeenCalledWith(
        'Circuit breaker opened after 3 failures',
        { error: undefined }
      );
    });
  });

  describe('getState()', () => {
    it('should return current state information', () => {
      const state = circuitBreaker.getState();
      expect(state).toEqual({
        state: 'closed',
        failureCount: 0,
        canRetry: true
      });
    });

    it('should reflect state changes', () => {
      circuitBreaker.recordFailure();
      circuitBreaker.recordFailure();

      const state = circuitBreaker.getState();
      expect(state).toEqual({
        state: 'closed',
        failureCount: 2,
        canRetry: true
      });

      // Open circuit
      circuitBreaker.recordFailure();
      const openState = circuitBreaker.getState();
      expect(openState).toEqual({
        state: 'open',
        failureCount: 3,
        canRetry: false
      });
    });
  });

  describe('reset()', () => {
    it('should reset circuit breaker to initial state', () => {
      // Open the circuit and advance time
      for (let i = 0; i < 3; i++) {
        circuitBreaker.recordFailure();
      }
      vi.advanceTimersByTime(11000);
      circuitBreaker.shouldAllow(); // Go to half-open

      // Reset
      circuitBreaker.reset();

      const state = circuitBreaker.getState();
      expect(state).toEqual({
        state: 'closed',
        failureCount: 0,
        canRetry: true
      });
    });
  });

  describe('different configurations', () => {
    it('should work with custom failure threshold', () => {
      const customBreaker = new TelemetryCircuitBreaker(1, 5000, 1); // 1 failure threshold

      expect(customBreaker.getState().state).toBe('closed');
      customBreaker.recordFailure();
      expect(customBreaker.getState().state).toBe('open');
    });

    it('should work with custom half-open request count', () => {
      const customBreaker = new TelemetryCircuitBreaker(1, 5000, 3); // 3 half-open requests

      // Open and go to half-open
      customBreaker.recordFailure();
      vi.advanceTimersByTime(6000);

      // Should allow 3 requests in half-open
      expect(customBreaker.shouldAllow()).toBe(true);
      expect(customBreaker.shouldAllow()).toBe(true);
      expect(customBreaker.shouldAllow()).toBe(true);
      expect(customBreaker.shouldAllow()).toBe(true); // Fourth also allowed in simplified implementation
    });
  });
});

describe('TelemetryErrorAggregator', () => {
  let aggregator: TelemetryErrorAggregator;

  beforeEach(() => {
    aggregator = new TelemetryErrorAggregator();
    vi.clearAllMocks();
  });

  describe('record()', () => {
    it('should record error and increment counter', () => {
      const error = new TelemetryError(
        TelemetryErrorType.NETWORK_ERROR,
        'Network failure'
      );

      aggregator.record(error);

      const stats = aggregator.getStats();
      expect(stats.totalErrors).toBe(1);
      expect(stats.errorsByType[TelemetryErrorType.NETWORK_ERROR]).toBe(1);
    });

    it('should increment counter for repeated error types', () => {
      const error1 = new TelemetryError(
        TelemetryErrorType.NETWORK_ERROR,
        'First failure'
      );
      const error2 = new TelemetryError(
        TelemetryErrorType.NETWORK_ERROR,
        'Second failure'
      );

      aggregator.record(error1);
      aggregator.record(error2);

      const stats = aggregator.getStats();
      expect(stats.totalErrors).toBe(2);
      expect(stats.errorsByType[TelemetryErrorType.NETWORK_ERROR]).toBe(2);
    });

    it('should maintain limited error detail history', () => {
      // Record more than max details (100) to test limiting
      for (let i = 0; i < 105; i++) {
        const error = new TelemetryError(
          TelemetryErrorType.VALIDATION_ERROR,
          `Error ${i}`
        );
        aggregator.record(error);
      }

      const stats = aggregator.getStats();
      expect(stats.totalErrors).toBe(105);
      expect(stats.recentErrors).toHaveLength(10); // Only last 10
    });

    it('should track different error types separately', () => {
      const networkError = new TelemetryError(
        TelemetryErrorType.NETWORK_ERROR,
        'Network issue'
      );
      const validationError = new TelemetryError(
        TelemetryErrorType.VALIDATION_ERROR,
        'Validation issue'
      );
      const rateLimitError = new TelemetryError(
        TelemetryErrorType.RATE_LIMIT_ERROR,
        'Rate limit hit'
      );

      aggregator.record(networkError);
      aggregator.record(networkError);
      aggregator.record(validationError);
      aggregator.record(rateLimitError);

      const stats = aggregator.getStats();
      expect(stats.totalErrors).toBe(4);
      expect(stats.errorsByType[TelemetryErrorType.NETWORK_ERROR]).toBe(2);
      expect(stats.errorsByType[TelemetryErrorType.VALIDATION_ERROR]).toBe(1);
      expect(stats.errorsByType[TelemetryErrorType.RATE_LIMIT_ERROR]).toBe(1);
    });
  });

  describe('getStats()', () => {
    it('should return empty stats when no errors recorded', () => {
      const stats = aggregator.getStats();
      expect(stats).toEqual({
        totalErrors: 0,
        errorsByType: {},
        mostCommonError: undefined,
        recentErrors: []
      });
    });

    it('should identify most common error type', () => {
      const networkError = new TelemetryError(
        TelemetryErrorType.NETWORK_ERROR,
        'Network issue'
      );
      const validationError = new TelemetryError(
        TelemetryErrorType.VALIDATION_ERROR,
        'Validation issue'
      );

      // Network errors more frequent
      aggregator.record(networkError);
      aggregator.record(networkError);
      aggregator.record(networkError);
      aggregator.record(validationError);

      const stats = aggregator.getStats();
      expect(stats.mostCommonError).toBe(TelemetryErrorType.NETWORK_ERROR);
    });

    it('should return recent errors in order', () => {
      const error1 = new TelemetryError(
        TelemetryErrorType.NETWORK_ERROR,
        'First error'
      );
      const error2 = new TelemetryError(
        TelemetryErrorType.VALIDATION_ERROR,
        'Second error'
      );
      const error3 = new TelemetryError(
        TelemetryErrorType.RATE_LIMIT_ERROR,
        'Third error'
      );

      aggregator.record(error1);
      aggregator.record(error2);
      aggregator.record(error3);

      const stats = aggregator.getStats();
      expect(stats.recentErrors).toHaveLength(3);
      expect(stats.recentErrors[0].message).toBe('First error');
      expect(stats.recentErrors[1].message).toBe('Second error');
      expect(stats.recentErrors[2].message).toBe('Third error');
    });

    it('should handle tie in most common error', () => {
      const networkError = new TelemetryError(
        TelemetryErrorType.NETWORK_ERROR,
        'Network issue'
      );
      const validationError = new TelemetryError(
        TelemetryErrorType.VALIDATION_ERROR,
        'Validation issue'
      );

      // Equal counts
      aggregator.record(networkError);
      aggregator.record(validationError);

      const stats = aggregator.getStats();
      // Should return one of them (implementation dependent)
      expect(stats.mostCommonError).toBeDefined();
      expect([TelemetryErrorType.NETWORK_ERROR, TelemetryErrorType.VALIDATION_ERROR])
        .toContain(stats.mostCommonError);
    });
  });

  describe('reset()', () => {
    it('should clear all error data', () => {
      const error = new TelemetryError(
        TelemetryErrorType.NETWORK_ERROR,
        'Test error'
      );
      aggregator.record(error);

      // Verify data exists
      expect(aggregator.getStats().totalErrors).toBe(1);

      // Reset
      aggregator.reset();

      // Verify cleared
      const stats = aggregator.getStats();
      expect(stats).toEqual({
        totalErrors: 0,
        errorsByType: {},
        mostCommonError: undefined,
        recentErrors: []
      });
    });
  });

  describe('error detail management', () => {
    it('should preserve error context in details', () => {
      const context = { operation: 'flush', batchSize: 50 };
      const error = new TelemetryError(
        TelemetryErrorType.NETWORK_ERROR,
        'Network failure',
        context,
        true
      );

      aggregator.record(error);

      const stats = aggregator.getStats();
      expect(stats.recentErrors[0]).toEqual({
        type: TelemetryErrorType.NETWORK_ERROR,
        message: 'Network failure',
        context,
        timestamp: error.timestamp,
        retryable: true
      });
    });

    it('should maintain error details queue with FIFO behavior', () => {
      // Add more than max to test queue behavior
      const errors = [];
      for (let i = 0; i < 15; i++) {
        const error = new TelemetryError(
          TelemetryErrorType.VALIDATION_ERROR,
          `Error ${i}`
        );
        errors.push(error);
        aggregator.record(error);
      }

      const stats = aggregator.getStats();
      // Should have last 10 errors (5-14)
      expect(stats.recentErrors).toHaveLength(10);
      expect(stats.recentErrors[0].message).toBe('Error 5');
      expect(stats.recentErrors[9].message).toBe('Error 14');
    });
  });
});
```

--------------------------------------------------------------------------------
/src/services/ai-tool-validators.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * AI Tool Sub-Node Validators
 *
 * Implements validation logic for all 13 AI tool sub-nodes from
 * docs/FINAL_AI_VALIDATION_SPEC.md
 *
 * Each validator checks configuration requirements, connections, and
 * parameters specific to that tool type.
 */

import { NodeTypeNormalizer } from '../utils/node-type-normalizer';

// Validation constants
const MIN_DESCRIPTION_LENGTH_SHORT = 10;
const MIN_DESCRIPTION_LENGTH_MEDIUM = 15;
const MIN_DESCRIPTION_LENGTH_LONG = 20;
const MAX_ITERATIONS_WARNING_THRESHOLD = 50;
const MAX_TOPK_WARNING_THRESHOLD = 20;

export interface WorkflowNode {
  id: string;
  name: string;
  type: string;
  position: [number, number];
  parameters: any;
  credentials?: any;
  disabled?: boolean;
  typeVersion?: number;
}

export interface WorkflowJson {
  name?: string;
  nodes: WorkflowNode[];
  connections: Record<string, any>;
  settings?: any;
}

export interface ReverseConnection {
  sourceName: string;
  sourceType: string;
  type: string;  // main, ai_tool, ai_languageModel, etc.
  index: number;
}

export interface ValidationIssue {
  severity: 'error' | 'warning' | 'info';
  nodeId?: string;
  nodeName?: string;
  message: string;
  code?: string;
}

/**
 * 1. HTTP Request Tool Validator
 * From spec lines 883-1123
 */
export function validateHTTPRequestTool(node: WorkflowNode): ValidationIssue[] {
  const issues: ValidationIssue[] = [];

  // 1. Check toolDescription (REQUIRED)
  if (!node.parameters.toolDescription) {
    issues.push({
      severity: 'error',
      nodeId: node.id,
      nodeName: node.name,
      message: `HTTP Request Tool "${node.name}" has no toolDescription. Add a clear description to help the LLM know when to use this API.`,
      code: 'MISSING_TOOL_DESCRIPTION'
    });
  } else if (node.parameters.toolDescription.trim().length < MIN_DESCRIPTION_LENGTH_MEDIUM) {
    issues.push({
      severity: 'warning',
      nodeId: node.id,
      nodeName: node.name,
      message: `HTTP Request Tool "${node.name}" toolDescription is too short (minimum ${MIN_DESCRIPTION_LENGTH_MEDIUM} characters). Explain what API this calls and when to use it.`
    });
  }

  // 2. Check URL (REQUIRED)
  if (!node.parameters.url) {
    issues.push({
      severity: 'error',
      nodeId: node.id,
      nodeName: node.name,
      message: `HTTP Request Tool "${node.name}" has no URL. Add the API endpoint URL.`,
      code: 'MISSING_URL'
    });
  } else {
    // Validate URL protocol (must be http or https)
    try {
      const urlObj = new URL(node.parameters.url);
      if (urlObj.protocol !== 'http:' && urlObj.protocol !== 'https:') {
        issues.push({
          severity: 'error',
          nodeId: node.id,
          nodeName: node.name,
          message: `HTTP Request Tool "${node.name}" has invalid URL protocol "${urlObj.protocol}". Use http:// or https:// only.`,
          code: 'INVALID_URL_PROTOCOL'
        });
      }
    } catch (e) {
      // URL parsing failed - invalid format
      // Only warn if it's not an n8n expression
      if (!node.parameters.url.includes('{{')) {
        issues.push({
          severity: 'warning',
          nodeId: node.id,
          nodeName: node.name,
          message: `HTTP Request Tool "${node.name}" has potentially invalid URL format. Ensure it's a valid URL or n8n expression.`
        });
      }
    }
  }

  // 3. Validate placeholders match definitions
  if (node.parameters.url || node.parameters.body || node.parameters.headers) {
    const placeholderRegex = /\{([^}]+)\}/g;
    const placeholders = new Set<string>();

    // Extract placeholders from URL, body, headers
    [node.parameters.url, node.parameters.body, JSON.stringify(node.parameters.headers || {})].forEach(text => {
      if (text) {
        let match;
        while ((match = placeholderRegex.exec(text)) !== null) {
          placeholders.add(match[1]);
        }
      }
    });

    // If placeholders exist in URL/body/headers
    if (placeholders.size > 0) {
      const definitions = node.parameters.placeholderDefinitions?.values || [];
      const definedNames = new Set(definitions.map((d: any) => d.name));

      // If no placeholderDefinitions at all, warn
      if (!node.parameters.placeholderDefinitions) {
        issues.push({
          severity: 'warning',
          nodeId: node.id,
          nodeName: node.name,
          message: `HTTP Request Tool "${node.name}" uses placeholders but has no placeholderDefinitions. Add definitions to describe the expected inputs.`
        });
      } else {
        // Has placeholderDefinitions, check each placeholder
        for (const placeholder of placeholders) {
          if (!definedNames.has(placeholder)) {
            issues.push({
              severity: 'error',
              nodeId: node.id,
              nodeName: node.name,
              message: `HTTP Request Tool "${node.name}" Placeholder "${placeholder}" in URL but it's not defined in placeholderDefinitions.`,
              code: 'UNDEFINED_PLACEHOLDER'
            });
          }
        }

        // Check for defined but unused placeholders
        for (const def of definitions) {
          if (!placeholders.has(def.name)) {
            issues.push({
              severity: 'warning',
              nodeId: node.id,
              nodeName: node.name,
              message: `HTTP Request Tool "${node.name}" defines placeholder "${def.name}" but doesn't use it.`
            });
          }
        }
      }
    }
  }

  // 4. Validate authentication
  if (node.parameters.authentication === 'predefinedCredentialType' &&
      (!node.credentials || Object.keys(node.credentials).length === 0)) {
    issues.push({
      severity: 'error',
      nodeId: node.id,
      nodeName: node.name,
      message: `HTTP Request Tool "${node.name}" requires credentials but none are configured.`,
      code: 'MISSING_CREDENTIALS'
    });
  }

  // 5. Validate HTTP method
  const validMethods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'];
  if (node.parameters.method && !validMethods.includes(node.parameters.method.toUpperCase())) {
    issues.push({
      severity: 'error',
      nodeId: node.id,
      nodeName: node.name,
      message: `HTTP Request Tool "${node.name}" has invalid HTTP method "${node.parameters.method}". Use one of: ${validMethods.join(', ')}.`,
      code: 'INVALID_HTTP_METHOD'
    });
  }

  // 6. Validate body for POST/PUT/PATCH
  if (['POST', 'PUT', 'PATCH'].includes(node.parameters.method?.toUpperCase())) {
    if (!node.parameters.body && !node.parameters.jsonBody) {
      issues.push({
        severity: 'warning',
        nodeId: node.id,
        nodeName: node.name,
        message: `HTTP Request Tool "${node.name}" uses ${node.parameters.method} but has no body. Consider adding a body or using GET instead.`
      });
    }
  }

  return issues;
}

/**
 * 2. Code Tool Validator
 * From spec lines 1125-1393
 */
export function validateCodeTool(node: WorkflowNode): ValidationIssue[] {
  const issues: ValidationIssue[] = [];

  // 1. Check toolDescription (REQUIRED)
  if (!node.parameters.toolDescription) {
    issues.push({
      severity: 'error',
      nodeId: node.id,
      nodeName: node.name,
      message: `Code Tool "${node.name}" has no toolDescription. Add one to help the LLM understand the tool's purpose.`,
      code: 'MISSING_TOOL_DESCRIPTION'
    });
  }

  // 2. Check jsCode exists (REQUIRED)
  if (!node.parameters.jsCode || node.parameters.jsCode.trim().length === 0) {
    issues.push({
      severity: 'error',
      nodeId: node.id,
      nodeName: node.name,
      message: `Code Tool "${node.name}" code is empty. Add the JavaScript code to execute.`,
      code: 'MISSING_CODE'
    });
  }

  // 3. Recommend input/output schema
  if (!node.parameters.inputSchema && !node.parameters.specifyInputSchema) {
    issues.push({
      severity: 'warning',
      nodeId: node.id,
      nodeName: node.name,
      message: `Code Tool "${node.name}" has no input schema. Consider adding one to validate LLM inputs.`
    });
  }

  return issues;
}

/**
 * 3. Vector Store Tool Validator
 * From spec lines 1395-1620
 */
export function validateVectorStoreTool(
  node: WorkflowNode,
  reverseConnections: Map<string, ReverseConnection[]>,
  workflow: WorkflowJson
): ValidationIssue[] {
  const issues: ValidationIssue[] = [];

  // 1. Check toolDescription (REQUIRED)
  if (!node.parameters.toolDescription) {
    issues.push({
      severity: 'error',
      nodeId: node.id,
      nodeName: node.name,
      message: `Vector Store Tool "${node.name}" has no toolDescription. Add one to explain what data it searches.`,
      code: 'MISSING_TOOL_DESCRIPTION'
    });
  }

  // 2. Validate topK parameter if specified
  if (node.parameters.topK !== undefined) {
    if (typeof node.parameters.topK !== 'number' || node.parameters.topK < 1) {
      issues.push({
        severity: 'error',
        nodeId: node.id,
        nodeName: node.name,
        message: `Vector Store Tool "${node.name}" has invalid topK value. Must be a positive number.`,
        code: 'INVALID_TOPK'
      });
    } else if (node.parameters.topK > MAX_TOPK_WARNING_THRESHOLD) {
      issues.push({
        severity: 'warning',
        nodeId: node.id,
        nodeName: node.name,
        message: `Vector Store Tool "${node.name}" has topK=${node.parameters.topK}. Large values (>${MAX_TOPK_WARNING_THRESHOLD}) may overwhelm the LLM context. Consider reducing to 10 or less.`
      });
    }
  }

  return issues;
}

/**
 * 4. Workflow Tool Validator
 * From spec lines 1622-1831 (already complete in spec)
 */
export function validateWorkflowTool(node: WorkflowNode, reverseConnections?: Map<string, ReverseConnection[]>): ValidationIssue[] {
  const issues: ValidationIssue[] = [];

  // 1. Check toolDescription (REQUIRED)
  if (!node.parameters.toolDescription) {
    issues.push({
      severity: 'error',
      nodeId: node.id,
      nodeName: node.name,
      message: `Workflow Tool "${node.name}" has no toolDescription. Add one to help the LLM know when to use this tool.`,
      code: 'MISSING_TOOL_DESCRIPTION'
    });
  }

  // 2. Check workflowId (REQUIRED)
  if (!node.parameters.workflowId) {
    issues.push({
      severity: 'error',
      nodeId: node.id,
      nodeName: node.name,
      message: `Workflow Tool "${node.name}" has no workflowId. Select a workflow to execute.`,
      code: 'MISSING_WORKFLOW_ID'
    });
  }

  return issues;
}

/**
 * 5. AI Agent Tool Validator
 * From spec lines 1882-2122
 */
export function validateAIAgentTool(
  node: WorkflowNode,
  reverseConnections: Map<string, ReverseConnection[]>
): ValidationIssue[] {
  const issues: ValidationIssue[] = [];

  // 1. Check toolDescription (REQUIRED)
  if (!node.parameters.toolDescription) {
    issues.push({
      severity: 'error',
      nodeId: node.id,
      nodeName: node.name,
      message: `AI Agent Tool "${node.name}" has no toolDescription. Add one to help the LLM know when to use this tool.`,
      code: 'MISSING_TOOL_DESCRIPTION'
    });
  }

  // 2. Validate maxIterations if specified
  if (node.parameters.maxIterations !== undefined) {
    if (typeof node.parameters.maxIterations !== 'number' || node.parameters.maxIterations < 1) {
      issues.push({
        severity: 'error',
        nodeId: node.id,
        nodeName: node.name,
        message: `AI Agent Tool "${node.name}" has invalid maxIterations. Must be a positive number.`,
        code: 'INVALID_MAX_ITERATIONS'
      });
    } else if (node.parameters.maxIterations > MAX_ITERATIONS_WARNING_THRESHOLD) {
      issues.push({
        severity: 'warning',
        nodeId: node.id,
        nodeName: node.name,
        message: `AI Agent Tool "${node.name}" has maxIterations=${node.parameters.maxIterations}. Large values (>${MAX_ITERATIONS_WARNING_THRESHOLD}) may lead to long execution times.`
      });
    }
  }

  return issues;
}

/**
 * 6. MCP Client Tool Validator
 * From spec lines 2124-2534 (already complete in spec)
 */
export function validateMCPClientTool(node: WorkflowNode): ValidationIssue[] {
  const issues: ValidationIssue[] = [];

  // 1. Check toolDescription (REQUIRED)
  if (!node.parameters.toolDescription) {
    issues.push({
      severity: 'error',
      nodeId: node.id,
      nodeName: node.name,
      message: `MCP Client Tool "${node.name}" has no toolDescription. Add one to help the LLM know when to use this tool.`,
      code: 'MISSING_TOOL_DESCRIPTION'
    });
  }

  // 2. Check serverUrl (REQUIRED)
  if (!node.parameters.serverUrl) {
    issues.push({
      severity: 'error',
      nodeId: node.id,
      nodeName: node.name,
      message: `MCP Client Tool "${node.name}" has no serverUrl. Configure the MCP server URL.`,
      code: 'MISSING_SERVER_URL'
    });
  }

  return issues;
}

/**
 * 7-8. Simple Tools (Calculator, Think) Validators
 * From spec lines 1868-2009
 */
export function validateCalculatorTool(node: WorkflowNode): ValidationIssue[] {
  const issues: ValidationIssue[] = [];

  // Calculator Tool has a built-in description and is self-explanatory
  // toolDescription is optional - no validation needed
  return issues;
}

export function validateThinkTool(node: WorkflowNode): ValidationIssue[] {
  const issues: ValidationIssue[] = [];

  // Think Tool has a built-in description and is self-explanatory
  // toolDescription is optional - no validation needed
  return issues;
}

/**
 * 9-12. Search Tools Validators
 * From spec lines 1833-2139
 */
export function validateSerpApiTool(node: WorkflowNode): ValidationIssue[] {
  const issues: ValidationIssue[] = [];

  // 1. Check toolDescription (REQUIRED)
  if (!node.parameters.toolDescription) {
    issues.push({
      severity: 'error',
      nodeId: node.id,
      nodeName: node.name,
      message: `SerpApi Tool "${node.name}" has no toolDescription. Add one to explain when to use Google search.`,
      code: 'MISSING_TOOL_DESCRIPTION'
    });
  }

  // 2. Check credentials (RECOMMENDED)
  if (!node.credentials || !node.credentials.serpApiApi) {
    issues.push({
      severity: 'warning',
      nodeId: node.id,
      nodeName: node.name,
      message: `SerpApi Tool "${node.name}" requires SerpApi credentials. Configure your API key.`
    });
  }

  return issues;
}

export function validateWikipediaTool(node: WorkflowNode): ValidationIssue[] {
  const issues: ValidationIssue[] = [];

  // 1. Check toolDescription (REQUIRED)
  if (!node.parameters.toolDescription) {
    issues.push({
      severity: 'error',
      nodeId: node.id,
      nodeName: node.name,
      message: `Wikipedia Tool "${node.name}" has no toolDescription. Add one to explain when to use Wikipedia.`,
      code: 'MISSING_TOOL_DESCRIPTION'
    });
  }

  // 2. Validate language if specified
  if (node.parameters.language) {
    const validLanguageCodes = /^[a-z]{2,3}$/;  // ISO 639 codes
    if (!validLanguageCodes.test(node.parameters.language)) {
      issues.push({
        severity: 'warning',
        nodeId: node.id,
        nodeName: node.name,
        message: `Wikipedia Tool "${node.name}" has potentially invalid language code "${node.parameters.language}". Use ISO 639 codes (e.g., "en", "es", "fr").`
      });
    }
  }

  return issues;
}

export function validateSearXngTool(node: WorkflowNode): ValidationIssue[] {
  const issues: ValidationIssue[] = [];

  // 1. Check toolDescription (REQUIRED)
  if (!node.parameters.toolDescription) {
    issues.push({
      severity: 'error',
      nodeId: node.id,
      nodeName: node.name,
      message: `SearXNG Tool "${node.name}" has no toolDescription. Add one to explain when to use SearXNG.`,
      code: 'MISSING_TOOL_DESCRIPTION'
    });
  }

  // 2. Check baseUrl (REQUIRED)
  if (!node.parameters.baseUrl) {
    issues.push({
      severity: 'error',
      nodeId: node.id,
      nodeName: node.name,
      message: `SearXNG Tool "${node.name}" has no baseUrl. Configure your SearXNG instance URL.`,
      code: 'MISSING_BASE_URL'
    });
  }

  return issues;
}

export function validateWolframAlphaTool(node: WorkflowNode): ValidationIssue[] {
  const issues: ValidationIssue[] = [];

  // 1. Check credentials (REQUIRED)
  if (!node.credentials || (!node.credentials.wolframAlpha && !node.credentials.wolframAlphaApi)) {
    issues.push({
      severity: 'error',
      nodeId: node.id,
      nodeName: node.name,
      message: `WolframAlpha Tool "${node.name}" requires Wolfram|Alpha API credentials. Configure your App ID.`,
      code: 'MISSING_CREDENTIALS'
    });
  }

  // 2. Check description (INFO)
  if (!node.parameters.description && !node.parameters.toolDescription) {
    issues.push({
      severity: 'info',
      nodeId: node.id,
      nodeName: node.name,
      message: `WolframAlpha Tool "${node.name}" has no custom description. Add one to explain when to use Wolfram|Alpha for computational queries.`
    });
  }

  return issues;
}

/**
 * Helper: Map node types to validator functions
 */
export const AI_TOOL_VALIDATORS = {
  'nodes-langchain.toolHttpRequest': validateHTTPRequestTool,
  'nodes-langchain.toolCode': validateCodeTool,
  'nodes-langchain.toolVectorStore': validateVectorStoreTool,
  'nodes-langchain.toolWorkflow': validateWorkflowTool,
  'nodes-langchain.agentTool': validateAIAgentTool,
  'nodes-langchain.mcpClientTool': validateMCPClientTool,
  'nodes-langchain.toolCalculator': validateCalculatorTool,
  'nodes-langchain.toolThink': validateThinkTool,
  'nodes-langchain.toolSerpApi': validateSerpApiTool,
  'nodes-langchain.toolWikipedia': validateWikipediaTool,
  'nodes-langchain.toolSearXng': validateSearXngTool,
  'nodes-langchain.toolWolframAlpha': validateWolframAlphaTool,
} as const;

/**
 * Check if a node type is an AI tool sub-node
 */
export function isAIToolSubNode(nodeType: string): boolean {
  const normalized = NodeTypeNormalizer.normalizeToFullForm(nodeType);
  return normalized in AI_TOOL_VALIDATORS;
}

/**
 * Validate an AI tool sub-node with the appropriate validator
 */
export function validateAIToolSubNode(
  node: WorkflowNode,
  nodeType: string,
  reverseConnections: Map<string, ReverseConnection[]>,
  workflow: WorkflowJson
): ValidationIssue[] {
  const normalized = NodeTypeNormalizer.normalizeToFullForm(nodeType);

  // Route to appropriate validator based on node type
  switch (normalized) {
    case 'nodes-langchain.toolHttpRequest':
      return validateHTTPRequestTool(node);
    case 'nodes-langchain.toolCode':
      return validateCodeTool(node);
    case 'nodes-langchain.toolVectorStore':
      return validateVectorStoreTool(node, reverseConnections, workflow);
    case 'nodes-langchain.toolWorkflow':
      return validateWorkflowTool(node);
    case 'nodes-langchain.agentTool':
      return validateAIAgentTool(node, reverseConnections);
    case 'nodes-langchain.mcpClientTool':
      return validateMCPClientTool(node);
    case 'nodes-langchain.toolCalculator':
      return validateCalculatorTool(node);
    case 'nodes-langchain.toolThink':
      return validateThinkTool(node);
    case 'nodes-langchain.toolSerpApi':
      return validateSerpApiTool(node);
    case 'nodes-langchain.toolWikipedia':
      return validateWikipediaTool(node);
    case 'nodes-langchain.toolSearXng':
      return validateSearXngTool(node);
    case 'nodes-langchain.toolWolframAlpha':
      return validateWolframAlphaTool(node);
    default:
      return [];
  }
}

```

--------------------------------------------------------------------------------
/src/services/workflow-auto-fixer.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Workflow Auto-Fixer Service
 *
 * Automatically generates fix operations for common workflow validation errors.
 * Converts validation results into diff operations that can be applied to fix the workflow.
 */

import crypto from 'crypto';
import { WorkflowValidationResult } from './workflow-validator';
import { ExpressionFormatIssue } from './expression-format-validator';
import { NodeSimilarityService } from './node-similarity-service';
import { NodeRepository } from '../database/node-repository';
import {
  WorkflowDiffOperation,
  UpdateNodeOperation
} from '../types/workflow-diff';
import { WorkflowNode, Workflow } from '../types/n8n-api';
import { Logger } from '../utils/logger';

const logger = new Logger({ prefix: '[WorkflowAutoFixer]' });

export type FixConfidenceLevel = 'high' | 'medium' | 'low';
export type FixType =
  | 'expression-format'
  | 'typeversion-correction'
  | 'error-output-config'
  | 'node-type-correction'
  | 'webhook-missing-path';

export interface AutoFixConfig {
  applyFixes: boolean;
  fixTypes?: FixType[];
  confidenceThreshold?: FixConfidenceLevel;
  maxFixes?: number;
}

export interface FixOperation {
  node: string;
  field: string;
  type: FixType;
  before: any;
  after: any;
  confidence: FixConfidenceLevel;
  description: string;
}

export interface AutoFixResult {
  operations: WorkflowDiffOperation[];
  fixes: FixOperation[];
  summary: string;
  stats: {
    total: number;
    byType: Record<FixType, number>;
    byConfidence: Record<FixConfidenceLevel, number>;
  };
}

export interface NodeFormatIssue extends ExpressionFormatIssue {
  nodeName: string;
  nodeId: string;
}

/**
 * Type guard to check if an issue has node information
 */
export function isNodeFormatIssue(issue: ExpressionFormatIssue): issue is NodeFormatIssue {
  return 'nodeName' in issue && 'nodeId' in issue &&
         typeof (issue as any).nodeName === 'string' &&
         typeof (issue as any).nodeId === 'string';
}

/**
 * Error with suggestions for node type issues
 */
export interface NodeTypeError {
  type: 'error';
  nodeId?: string;
  nodeName?: string;
  message: string;
  suggestions?: Array<{
    nodeType: string;
    confidence: number;
    reason: string;
  }>;
}

export class WorkflowAutoFixer {
  private readonly defaultConfig: AutoFixConfig = {
    applyFixes: false,
    confidenceThreshold: 'medium',
    maxFixes: 50
  };
  private similarityService: NodeSimilarityService | null = null;

  constructor(repository?: NodeRepository) {
    if (repository) {
      this.similarityService = new NodeSimilarityService(repository);
    }
  }

  /**
   * Generate fix operations from validation results
   */
  generateFixes(
    workflow: Workflow,
    validationResult: WorkflowValidationResult,
    formatIssues: ExpressionFormatIssue[] = [],
    config: Partial<AutoFixConfig> = {}
  ): AutoFixResult {
    const fullConfig = { ...this.defaultConfig, ...config };
    const operations: WorkflowDiffOperation[] = [];
    const fixes: FixOperation[] = [];

    // Create a map for quick node lookup
    const nodeMap = new Map<string, WorkflowNode>();
    workflow.nodes.forEach(node => {
      nodeMap.set(node.name, node);
      nodeMap.set(node.id, node);
    });

    // Process expression format issues (HIGH confidence)
    if (!fullConfig.fixTypes || fullConfig.fixTypes.includes('expression-format')) {
      this.processExpressionFormatFixes(formatIssues, nodeMap, operations, fixes);
    }

    // Process typeVersion errors (MEDIUM confidence)
    if (!fullConfig.fixTypes || fullConfig.fixTypes.includes('typeversion-correction')) {
      this.processTypeVersionFixes(validationResult, nodeMap, operations, fixes);
    }

    // Process error output configuration issues (MEDIUM confidence)
    if (!fullConfig.fixTypes || fullConfig.fixTypes.includes('error-output-config')) {
      this.processErrorOutputFixes(validationResult, nodeMap, workflow, operations, fixes);
    }

    // Process node type corrections (HIGH confidence only)
    if (!fullConfig.fixTypes || fullConfig.fixTypes.includes('node-type-correction')) {
      this.processNodeTypeFixes(validationResult, nodeMap, operations, fixes);
    }

    // Process webhook path fixes (HIGH confidence)
    if (!fullConfig.fixTypes || fullConfig.fixTypes.includes('webhook-missing-path')) {
      this.processWebhookPathFixes(validationResult, nodeMap, operations, fixes);
    }

    // Filter by confidence threshold
    const filteredFixes = this.filterByConfidence(fixes, fullConfig.confidenceThreshold);
    const filteredOperations = this.filterOperationsByFixes(operations, filteredFixes, fixes);

    // Apply max fixes limit
    const limitedFixes = filteredFixes.slice(0, fullConfig.maxFixes);
    const limitedOperations = this.filterOperationsByFixes(filteredOperations, limitedFixes, filteredFixes);

    // Generate summary
    const stats = this.calculateStats(limitedFixes);
    const summary = this.generateSummary(stats);

    return {
      operations: limitedOperations,
      fixes: limitedFixes,
      summary,
      stats
    };
  }

  /**
   * Process expression format fixes (missing = prefix)
   */
  private processExpressionFormatFixes(
    formatIssues: ExpressionFormatIssue[],
    nodeMap: Map<string, WorkflowNode>,
    operations: WorkflowDiffOperation[],
    fixes: FixOperation[]
  ): void {
    // Group fixes by node to create single update operation per node
    const fixesByNode = new Map<string, ExpressionFormatIssue[]>();

    for (const issue of formatIssues) {
      // Process both errors and warnings for missing-prefix issues
      if (issue.issueType === 'missing-prefix') {
        // Use type guard to ensure we have node information
        if (!isNodeFormatIssue(issue)) {
          logger.warn('Expression format issue missing node information', {
            fieldPath: issue.fieldPath,
            issueType: issue.issueType
          });
          continue;
        }

        const nodeName = issue.nodeName;

        if (!fixesByNode.has(nodeName)) {
          fixesByNode.set(nodeName, []);
        }
        fixesByNode.get(nodeName)!.push(issue);
      }
    }

    // Create update operations for each node
    for (const [nodeName, nodeIssues] of fixesByNode) {
      const node = nodeMap.get(nodeName);
      if (!node) continue;

      const updatedParameters = JSON.parse(JSON.stringify(node.parameters || {}));

      for (const issue of nodeIssues) {
        // Apply the fix to parameters
        // The fieldPath doesn't include node name, use as is
        const fieldPath = issue.fieldPath.split('.');
        this.setNestedValue(updatedParameters, fieldPath, issue.correctedValue);

        fixes.push({
          node: nodeName,
          field: issue.fieldPath,
          type: 'expression-format',
          before: issue.currentValue,
          after: issue.correctedValue,
          confidence: 'high',
          description: issue.explanation
        });
      }

      // Create update operation
      const operation: UpdateNodeOperation = {
        type: 'updateNode',
        nodeId: nodeName, // Can be name or ID
        updates: {
          parameters: updatedParameters
        }
      };
      operations.push(operation);
    }
  }

  /**
   * Process typeVersion fixes
   */
  private processTypeVersionFixes(
    validationResult: WorkflowValidationResult,
    nodeMap: Map<string, WorkflowNode>,
    operations: WorkflowDiffOperation[],
    fixes: FixOperation[]
  ): void {
    for (const error of validationResult.errors) {
      if (error.message.includes('typeVersion') && error.message.includes('exceeds maximum')) {
        // Extract version info from error message
        const versionMatch = error.message.match(/typeVersion (\d+(?:\.\d+)?) exceeds maximum supported version (\d+(?:\.\d+)?)/);
        if (versionMatch) {
          const currentVersion = parseFloat(versionMatch[1]);
          const maxVersion = parseFloat(versionMatch[2]);
          const nodeName = error.nodeName || error.nodeId;

          if (!nodeName) continue;

          const node = nodeMap.get(nodeName);
          if (!node) continue;

          fixes.push({
            node: nodeName,
            field: 'typeVersion',
            type: 'typeversion-correction',
            before: currentVersion,
            after: maxVersion,
            confidence: 'medium',
            description: `Corrected typeVersion from ${currentVersion} to maximum supported ${maxVersion}`
          });

          const operation: UpdateNodeOperation = {
            type: 'updateNode',
            nodeId: nodeName,
            updates: {
              typeVersion: maxVersion
            }
          };
          operations.push(operation);
        }
      }
    }
  }

  /**
   * Process error output configuration fixes
   */
  private processErrorOutputFixes(
    validationResult: WorkflowValidationResult,
    nodeMap: Map<string, WorkflowNode>,
    workflow: Workflow,
    operations: WorkflowDiffOperation[],
    fixes: FixOperation[]
  ): void {
    for (const error of validationResult.errors) {
      if (error.message.includes('onError: \'continueErrorOutput\'') &&
          error.message.includes('no error output connections')) {
        const nodeName = error.nodeName || error.nodeId;
        if (!nodeName) continue;

        const node = nodeMap.get(nodeName);
        if (!node) continue;

        // Remove the conflicting onError setting
        fixes.push({
          node: nodeName,
          field: 'onError',
          type: 'error-output-config',
          before: 'continueErrorOutput',
          after: undefined,
          confidence: 'medium',
          description: 'Removed onError setting due to missing error output connections'
        });

        const operation: UpdateNodeOperation = {
          type: 'updateNode',
          nodeId: nodeName,
          updates: {
            onError: undefined // This will remove the property
          }
        };
        operations.push(operation);
      }
    }
  }

  /**
   * Process node type corrections for unknown nodes
   */
  private processNodeTypeFixes(
    validationResult: WorkflowValidationResult,
    nodeMap: Map<string, WorkflowNode>,
    operations: WorkflowDiffOperation[],
    fixes: FixOperation[]
  ): void {
    // Only process if we have the similarity service
    if (!this.similarityService) {
      return;
    }

    for (const error of validationResult.errors) {
      // Type-safe check for unknown node type errors with suggestions
      const nodeError = error as NodeTypeError;

      if (error.message?.includes('Unknown node type:') && nodeError.suggestions) {
        // Only auto-fix if we have a high-confidence suggestion (>= 0.9)
        const highConfidenceSuggestion = nodeError.suggestions.find(s => s.confidence >= 0.9);

        if (highConfidenceSuggestion && nodeError.nodeId) {
          const node = nodeMap.get(nodeError.nodeId) || nodeMap.get(nodeError.nodeName || '');

          if (node) {
            fixes.push({
              node: node.name,
              field: 'type',
              type: 'node-type-correction',
              before: node.type,
              after: highConfidenceSuggestion.nodeType,
              confidence: 'high',
              description: `Fix node type: "${node.type}" → "${highConfidenceSuggestion.nodeType}" (${highConfidenceSuggestion.reason})`
            });

            const operation: UpdateNodeOperation = {
              type: 'updateNode',
              nodeId: node.name,
              updates: {
                type: highConfidenceSuggestion.nodeType
              }
            };
            operations.push(operation);
          }
        }
      }
    }
  }

  /**
   * Process webhook path fixes for webhook nodes missing path parameter
   */
  private processWebhookPathFixes(
    validationResult: WorkflowValidationResult,
    nodeMap: Map<string, WorkflowNode>,
    operations: WorkflowDiffOperation[],
    fixes: FixOperation[]
  ): void {
    for (const error of validationResult.errors) {
      // Check for webhook path required error
      if (error.message === 'Webhook path is required') {
        const nodeName = error.nodeName || error.nodeId;
        if (!nodeName) continue;

        const node = nodeMap.get(nodeName);
        if (!node) continue;

        // Only fix webhook nodes
        if (!node.type?.includes('webhook')) continue;

        // Generate a unique UUID for both path and webhookId
        const webhookId = crypto.randomUUID();

        // Check if we need to update typeVersion
        const currentTypeVersion = node.typeVersion || 1;
        const needsVersionUpdate = currentTypeVersion < 2.1;

        fixes.push({
          node: nodeName,
          field: 'path',
          type: 'webhook-missing-path',
          before: undefined,
          after: webhookId,
          confidence: 'high',
          description: needsVersionUpdate
            ? `Generated webhook path and ID: ${webhookId} (also updating typeVersion to 2.1)`
            : `Generated webhook path and ID: ${webhookId}`
        });

        // Create update operation with both path and webhookId
        // The updates object uses dot notation for nested properties
        const updates: Record<string, any> = {
          'parameters.path': webhookId,
          'webhookId': webhookId
        };

        // Only update typeVersion if it's older than 2.1
        if (needsVersionUpdate) {
          updates['typeVersion'] = 2.1;
        }

        const operation: UpdateNodeOperation = {
          type: 'updateNode',
          nodeId: nodeName,
          updates
        };
        operations.push(operation);
      }
    }
  }

  /**
   * Set a nested value in an object using a path array
   * Includes validation to prevent silent failures
   */
  private setNestedValue(obj: any, path: string[], value: any): void {
    if (!obj || typeof obj !== 'object') {
      throw new Error('Cannot set value on non-object');
    }

    if (path.length === 0) {
      throw new Error('Cannot set value with empty path');
    }

    try {
      let current = obj;

      for (let i = 0; i < path.length - 1; i++) {
        const key = path[i];

        // Handle array indices
        if (key.includes('[')) {
          const matches = key.match(/^([^[]+)\[(\d+)\]$/);
          if (!matches) {
            throw new Error(`Invalid array notation: ${key}`);
          }

          const [, arrayKey, indexStr] = matches;
          const index = parseInt(indexStr, 10);

          if (isNaN(index) || index < 0) {
            throw new Error(`Invalid array index: ${indexStr}`);
          }

          if (!current[arrayKey]) {
            current[arrayKey] = [];
          }

          if (!Array.isArray(current[arrayKey])) {
            throw new Error(`Expected array at ${arrayKey}, got ${typeof current[arrayKey]}`);
          }

          while (current[arrayKey].length <= index) {
            current[arrayKey].push({});
          }

          current = current[arrayKey][index];
        } else {
          if (current[key] === null || current[key] === undefined) {
            current[key] = {};
          }

          if (typeof current[key] !== 'object' || Array.isArray(current[key])) {
            throw new Error(`Cannot traverse through ${typeof current[key]} at ${key}`);
          }

          current = current[key];
        }
      }

      // Set the final value
      const lastKey = path[path.length - 1];

      if (lastKey.includes('[')) {
        const matches = lastKey.match(/^([^[]+)\[(\d+)\]$/);
        if (!matches) {
          throw new Error(`Invalid array notation: ${lastKey}`);
        }

        const [, arrayKey, indexStr] = matches;
        const index = parseInt(indexStr, 10);

        if (isNaN(index) || index < 0) {
          throw new Error(`Invalid array index: ${indexStr}`);
        }

        if (!current[arrayKey]) {
          current[arrayKey] = [];
        }

        if (!Array.isArray(current[arrayKey])) {
          throw new Error(`Expected array at ${arrayKey}, got ${typeof current[arrayKey]}`);
        }

        while (current[arrayKey].length <= index) {
          current[arrayKey].push(null);
        }

        current[arrayKey][index] = value;
      } else {
        current[lastKey] = value;
      }
    } catch (error) {
      logger.error('Failed to set nested value', {
        path: path.join('.'),
        error: error instanceof Error ? error.message : String(error)
      });
      throw error;
    }
  }

  /**
   * Filter fixes by confidence level
   */
  private filterByConfidence(
    fixes: FixOperation[],
    threshold?: FixConfidenceLevel
  ): FixOperation[] {
    if (!threshold) return fixes;

    const levels: FixConfidenceLevel[] = ['high', 'medium', 'low'];
    const thresholdIndex = levels.indexOf(threshold);

    return fixes.filter(fix => {
      const fixIndex = levels.indexOf(fix.confidence);
      return fixIndex <= thresholdIndex;
    });
  }

  /**
   * Filter operations to match filtered fixes
   */
  private filterOperationsByFixes(
    operations: WorkflowDiffOperation[],
    filteredFixes: FixOperation[],
    allFixes: FixOperation[]
  ): WorkflowDiffOperation[] {
    const fixedNodes = new Set(filteredFixes.map(f => f.node));
    return operations.filter(op => {
      if (op.type === 'updateNode') {
        return fixedNodes.has(op.nodeId || '');
      }
      return true;
    });
  }

  /**
   * Calculate statistics about fixes
   */
  private calculateStats(fixes: FixOperation[]): AutoFixResult['stats'] {
    const stats: AutoFixResult['stats'] = {
      total: fixes.length,
      byType: {
        'expression-format': 0,
        'typeversion-correction': 0,
        'error-output-config': 0,
        'node-type-correction': 0,
        'webhook-missing-path': 0
      },
      byConfidence: {
        'high': 0,
        'medium': 0,
        'low': 0
      }
    };

    for (const fix of fixes) {
      stats.byType[fix.type]++;
      stats.byConfidence[fix.confidence]++;
    }

    return stats;
  }

  /**
   * Generate a human-readable summary
   */
  private generateSummary(stats: AutoFixResult['stats']): string {
    if (stats.total === 0) {
      return 'No fixes available';
    }

    const parts: string[] = [];

    if (stats.byType['expression-format'] > 0) {
      parts.push(`${stats.byType['expression-format']} expression format ${stats.byType['expression-format'] === 1 ? 'error' : 'errors'}`);
    }
    if (stats.byType['typeversion-correction'] > 0) {
      parts.push(`${stats.byType['typeversion-correction']} version ${stats.byType['typeversion-correction'] === 1 ? 'issue' : 'issues'}`);
    }
    if (stats.byType['error-output-config'] > 0) {
      parts.push(`${stats.byType['error-output-config']} error output ${stats.byType['error-output-config'] === 1 ? 'configuration' : 'configurations'}`);
    }
    if (stats.byType['node-type-correction'] > 0) {
      parts.push(`${stats.byType['node-type-correction']} node type ${stats.byType['node-type-correction'] === 1 ? 'correction' : 'corrections'}`);
    }
    if (stats.byType['webhook-missing-path'] > 0) {
      parts.push(`${stats.byType['webhook-missing-path']} webhook ${stats.byType['webhook-missing-path'] === 1 ? 'path' : 'paths'}`);
    }

    if (parts.length === 0) {
      return `Fixed ${stats.total} ${stats.total === 1 ? 'issue' : 'issues'}`;
    }

    return `Fixed ${parts.join(', ')}`;
  }
}
```

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

```typescript
import { NodeRepository } from '../database/node-repository';
import { logger } from '../utils/logger';
import { ValidationServiceError } from '../errors/validation-service-error';

export interface ResourceSuggestion {
  value: string;
  confidence: number;
  reason: string;
  availableOperations?: string[];
}

interface ResourcePattern {
  pattern: string;
  suggestion: string;
  confidence: number;
  reason: string;
}

export class ResourceSimilarityService {
  private static readonly CACHE_DURATION_MS = 5 * 60 * 1000; // 5 minutes
  private static readonly MIN_CONFIDENCE = 0.3; // 30% minimum confidence to suggest
  private static readonly MAX_SUGGESTIONS = 5;

  // Confidence thresholds for better code clarity
  private static readonly CONFIDENCE_THRESHOLDS = {
    EXACT: 1.0,
    VERY_HIGH: 0.95,
    HIGH: 0.8,
    MEDIUM: 0.6,
    MIN_SUBSTRING: 0.7
  } as const;

  private repository: NodeRepository;
  private resourceCache: Map<string, { resources: any[], timestamp: number }> = new Map();
  private suggestionCache: Map<string, ResourceSuggestion[]> = new Map();
  private commonPatterns: Map<string, ResourcePattern[]>;

  constructor(repository: NodeRepository) {
    this.repository = repository;
    this.commonPatterns = this.initializeCommonPatterns();
  }

  /**
   * Clean up expired cache entries to prevent memory leaks
   */
  private cleanupExpiredEntries(): void {
    const now = Date.now();

    // Clean resource cache
    for (const [key, value] of this.resourceCache.entries()) {
      if (now - value.timestamp >= ResourceSimilarityService.CACHE_DURATION_MS) {
        this.resourceCache.delete(key);
      }
    }

    // Clean suggestion cache - these don't have timestamps, so clear if cache is too large
    if (this.suggestionCache.size > 100) {
      // Keep only the most recent 50 entries
      const entries = Array.from(this.suggestionCache.entries());
      this.suggestionCache.clear();
      entries.slice(-50).forEach(([key, value]) => {
        this.suggestionCache.set(key, value);
      });
    }
  }

  /**
   * Initialize common resource mistake patterns
   */
  private initializeCommonPatterns(): Map<string, ResourcePattern[]> {
    const patterns = new Map<string, ResourcePattern[]>();

    // Google Drive patterns
    patterns.set('googleDrive', [
      { pattern: 'files', suggestion: 'file', confidence: 0.95, reason: 'Use singular "file" not plural' },
      { pattern: 'folders', suggestion: 'folder', confidence: 0.95, reason: 'Use singular "folder" not plural' },
      { pattern: 'permissions', suggestion: 'permission', confidence: 0.9, reason: 'Use singular form' },
      { pattern: 'fileAndFolder', suggestion: 'fileFolder', confidence: 0.9, reason: 'Use "fileFolder" for combined operations' },
      { pattern: 'driveFiles', suggestion: 'file', confidence: 0.8, reason: 'Use "file" for file operations' },
      { pattern: 'sharedDrives', suggestion: 'drive', confidence: 0.85, reason: 'Use "drive" for shared drive operations' },
    ]);

    // Slack patterns
    patterns.set('slack', [
      { pattern: 'messages', suggestion: 'message', confidence: 0.95, reason: 'Use singular "message" not plural' },
      { pattern: 'channels', suggestion: 'channel', confidence: 0.95, reason: 'Use singular "channel" not plural' },
      { pattern: 'users', suggestion: 'user', confidence: 0.95, reason: 'Use singular "user" not plural' },
      { pattern: 'msg', suggestion: 'message', confidence: 0.85, reason: 'Use full "message" not abbreviation' },
      { pattern: 'dm', suggestion: 'message', confidence: 0.7, reason: 'Use "message" for direct messages' },
      { pattern: 'conversation', suggestion: 'channel', confidence: 0.7, reason: 'Use "channel" for conversations' },
    ]);

    // Database patterns (postgres, mysql, mongodb)
    patterns.set('database', [
      { pattern: 'tables', suggestion: 'table', confidence: 0.95, reason: 'Use singular "table" not plural' },
      { pattern: 'queries', suggestion: 'query', confidence: 0.95, reason: 'Use singular "query" not plural' },
      { pattern: 'collections', suggestion: 'collection', confidence: 0.95, reason: 'Use singular "collection" not plural' },
      { pattern: 'documents', suggestion: 'document', confidence: 0.95, reason: 'Use singular "document" not plural' },
      { pattern: 'records', suggestion: 'record', confidence: 0.85, reason: 'Use "record" or "document"' },
      { pattern: 'rows', suggestion: 'row', confidence: 0.9, reason: 'Use singular "row"' },
    ]);

    // Google Sheets patterns
    patterns.set('googleSheets', [
      { pattern: 'sheets', suggestion: 'sheet', confidence: 0.95, reason: 'Use singular "sheet" not plural' },
      { pattern: 'spreadsheets', suggestion: 'spreadsheet', confidence: 0.95, reason: 'Use singular "spreadsheet"' },
      { pattern: 'cells', suggestion: 'cell', confidence: 0.9, reason: 'Use singular "cell"' },
      { pattern: 'ranges', suggestion: 'range', confidence: 0.9, reason: 'Use singular "range"' },
      { pattern: 'worksheets', suggestion: 'sheet', confidence: 0.8, reason: 'Use "sheet" for worksheet operations' },
    ]);

    // Email patterns
    patterns.set('email', [
      { pattern: 'emails', suggestion: 'email', confidence: 0.95, reason: 'Use singular "email" not plural' },
      { pattern: 'messages', suggestion: 'message', confidence: 0.9, reason: 'Use "message" for email operations' },
      { pattern: 'mails', suggestion: 'email', confidence: 0.9, reason: 'Use "email" not "mail"' },
      { pattern: 'attachments', suggestion: 'attachment', confidence: 0.95, reason: 'Use singular "attachment"' },
    ]);

    // Generic plural/singular patterns
    patterns.set('generic', [
      { pattern: 'items', suggestion: 'item', confidence: 0.9, reason: 'Use singular form' },
      { pattern: 'objects', suggestion: 'object', confidence: 0.9, reason: 'Use singular form' },
      { pattern: 'entities', suggestion: 'entity', confidence: 0.9, reason: 'Use singular form' },
      { pattern: 'resources', suggestion: 'resource', confidence: 0.9, reason: 'Use singular form' },
      { pattern: 'elements', suggestion: 'element', confidence: 0.9, reason: 'Use singular form' },
    ]);

    return patterns;
  }

  /**
   * Find similar resources for an invalid resource using pattern matching
   * and Levenshtein distance algorithms
   *
   * @param nodeType - The n8n node type (e.g., 'nodes-base.googleDrive')
   * @param invalidResource - The invalid resource provided by the user
   * @param maxSuggestions - Maximum number of suggestions to return (default: 5)
   * @returns Array of resource suggestions sorted by confidence
   *
   * @example
   * findSimilarResources('nodes-base.googleDrive', 'files', 3)
   * // Returns: [{ value: 'file', confidence: 0.95, reason: 'Use singular "file" not plural' }]
   */
  findSimilarResources(
    nodeType: string,
    invalidResource: string,
    maxSuggestions: number = ResourceSimilarityService.MAX_SUGGESTIONS
  ): ResourceSuggestion[] {
    // Clean up expired cache entries periodically
    if (Math.random() < 0.1) { // 10% chance to cleanup on each call
      this.cleanupExpiredEntries();
    }
    // Check cache first
    const cacheKey = `${nodeType}:${invalidResource}`;
    if (this.suggestionCache.has(cacheKey)) {
      return this.suggestionCache.get(cacheKey)!;
    }

    const suggestions: ResourceSuggestion[] = [];

    // Get valid resources for the node
    const validResources = this.getNodeResources(nodeType);

    // Early termination for exact match - no suggestions needed
    for (const resource of validResources) {
      const resourceValue = this.getResourceValue(resource);
      if (resourceValue.toLowerCase() === invalidResource.toLowerCase()) {
        return []; // Valid resource, no suggestions needed
      }
    }

    // Check for exact pattern matches first
    const nodePatterns = this.getNodePatterns(nodeType);
    for (const pattern of nodePatterns) {
      if (pattern.pattern.toLowerCase() === invalidResource.toLowerCase()) {
        // Check if the suggested resource actually exists with type safety
        const exists = validResources.some(r => {
          const resourceValue = this.getResourceValue(r);
          return resourceValue === pattern.suggestion;
        });
        if (exists) {
          suggestions.push({
            value: pattern.suggestion,
            confidence: pattern.confidence,
            reason: pattern.reason
          });
        }
      }
    }

    // Handle automatic plural/singular conversion
    const singularForm = this.toSingular(invalidResource);
    const pluralForm = this.toPlural(invalidResource);

    for (const resource of validResources) {
      const resourceValue = this.getResourceValue(resource);

      // Check for plural/singular match
      if (resourceValue === singularForm || resourceValue === pluralForm) {
        if (!suggestions.some(s => s.value === resourceValue)) {
          suggestions.push({
            value: resourceValue,
            confidence: 0.9,
            reason: invalidResource.endsWith('s') ?
              'Use singular form for resources' :
              'Incorrect plural/singular form',
            availableOperations: typeof resource === 'object' ? resource.operations : undefined
          });
        }
      }

      // Calculate similarity
      const similarity = this.calculateSimilarity(invalidResource, resourceValue);
      if (similarity >= ResourceSimilarityService.MIN_CONFIDENCE) {
        if (!suggestions.some(s => s.value === resourceValue)) {
          suggestions.push({
            value: resourceValue,
            confidence: similarity,
            reason: this.getSimilarityReason(similarity, invalidResource, resourceValue),
            availableOperations: typeof resource === 'object' ? resource.operations : undefined
          });
        }
      }
    }

    // Sort by confidence and limit
    suggestions.sort((a, b) => b.confidence - a.confidence);
    const topSuggestions = suggestions.slice(0, maxSuggestions);

    // Cache the result
    this.suggestionCache.set(cacheKey, topSuggestions);

    return topSuggestions;
  }

  /**
   * Type-safe extraction of resource value from various formats
   * @param resource - Resource object or string
   * @returns The resource value as a string
   */
  private getResourceValue(resource: any): string {
    if (typeof resource === 'string') {
      return resource;
    }
    if (typeof resource === 'object' && resource !== null) {
      return resource.value || '';
    }
    return '';
  }

  /**
   * Get resources for a node with caching
   */
  private getNodeResources(nodeType: string): any[] {
    // Cleanup cache periodically
    if (Math.random() < 0.05) { // 5% chance
      this.cleanupExpiredEntries();
    }

    const cacheKey = nodeType;
    const cached = this.resourceCache.get(cacheKey);

    if (cached && Date.now() - cached.timestamp < ResourceSimilarityService.CACHE_DURATION_MS) {
      return cached.resources;
    }

    const nodeInfo = this.repository.getNode(nodeType);
    if (!nodeInfo) return [];

    const resources: any[] = [];
    const resourceMap: Map<string, string[]> = new Map();

    // Parse properties for resource fields
    try {
      const properties = nodeInfo.properties || [];
      for (const prop of properties) {
        if (prop.name === 'resource' && prop.options) {
          for (const option of prop.options) {
            resources.push({
              value: option.value,
              name: option.name,
              operations: []
            });
            resourceMap.set(option.value, []);
          }
        }

        // Find operations for each resource
        if (prop.name === 'operation' && prop.displayOptions?.show?.resource) {
          const resourceValues = Array.isArray(prop.displayOptions.show.resource)
            ? prop.displayOptions.show.resource
            : [prop.displayOptions.show.resource];

          for (const resourceValue of resourceValues) {
            if (resourceMap.has(resourceValue) && prop.options) {
              const ops = prop.options.map((op: any) => op.value);
              resourceMap.get(resourceValue)!.push(...ops);
            }
          }
        }
      }

      // Update resources with their operations
      for (const resource of resources) {
        if (resourceMap.has(resource.value)) {
          resource.operations = resourceMap.get(resource.value);
        }
      }

      // If no explicit resources, check for common patterns
      if (resources.length === 0) {
        // Some nodes don't have explicit resource fields
        const implicitResources = this.extractImplicitResources(properties);
        resources.push(...implicitResources);
      }
    } catch (error) {
      logger.warn(`Failed to extract resources for ${nodeType}:`, error);
    }

    // Cache and return
    this.resourceCache.set(cacheKey, { resources, timestamp: Date.now() });
    return resources;
  }

  /**
   * Extract implicit resources from node properties
   */
  private extractImplicitResources(properties: any[]): any[] {
    const resources: any[] = [];

    // Look for properties that suggest resources
    for (const prop of properties) {
      if (prop.name === 'operation' && prop.options) {
        // If there's no explicit resource field, operations might imply resources
        const resourceFromOps = this.inferResourceFromOperations(prop.options);
        if (resourceFromOps) {
          resources.push({
            value: resourceFromOps,
            name: resourceFromOps.charAt(0).toUpperCase() + resourceFromOps.slice(1),
            operations: prop.options.map((op: any) => op.value)
          });
        }
      }
    }

    return resources;
  }

  /**
   * Infer resource type from operations
   */
  private inferResourceFromOperations(operations: any[]): string | null {
    // Common patterns in operation names that suggest resources
    const patterns = [
      { keywords: ['file', 'upload', 'download'], resource: 'file' },
      { keywords: ['folder', 'directory'], resource: 'folder' },
      { keywords: ['message', 'send', 'reply'], resource: 'message' },
      { keywords: ['channel', 'broadcast'], resource: 'channel' },
      { keywords: ['user', 'member'], resource: 'user' },
      { keywords: ['table', 'row', 'column'], resource: 'table' },
      { keywords: ['document', 'doc'], resource: 'document' },
    ];

    for (const pattern of patterns) {
      for (const op of operations) {
        const opName = (op.value || op).toLowerCase();
        if (pattern.keywords.some(keyword => opName.includes(keyword))) {
          return pattern.resource;
        }
      }
    }

    return null;
  }

  /**
   * Get patterns for a specific node type
   */
  private getNodePatterns(nodeType: string): ResourcePattern[] {
    const patterns: ResourcePattern[] = [];

    // Add node-specific patterns
    if (nodeType.includes('googleDrive')) {
      patterns.push(...(this.commonPatterns.get('googleDrive') || []));
    } else if (nodeType.includes('slack')) {
      patterns.push(...(this.commonPatterns.get('slack') || []));
    } else if (nodeType.includes('postgres') || nodeType.includes('mysql') || nodeType.includes('mongodb')) {
      patterns.push(...(this.commonPatterns.get('database') || []));
    } else if (nodeType.includes('googleSheets')) {
      patterns.push(...(this.commonPatterns.get('googleSheets') || []));
    } else if (nodeType.includes('gmail') || nodeType.includes('email')) {
      patterns.push(...(this.commonPatterns.get('email') || []));
    }

    // Always add generic patterns
    patterns.push(...(this.commonPatterns.get('generic') || []));

    return patterns;
  }

  /**
   * Convert to singular form (simple heuristic)
   */
  private toSingular(word: string): string {
    if (word.endsWith('ies')) {
      return word.slice(0, -3) + 'y';
    } else if (word.endsWith('es')) {
      return word.slice(0, -2);
    } else if (word.endsWith('s') && !word.endsWith('ss')) {
      return word.slice(0, -1);
    }
    return word;
  }

  /**
   * Convert to plural form (simple heuristic)
   */
  private toPlural(word: string): string {
    if (word.endsWith('y') && !['ay', 'ey', 'iy', 'oy', 'uy'].includes(word.slice(-2))) {
      return word.slice(0, -1) + 'ies';
    } else if (word.endsWith('s') || word.endsWith('x') || word.endsWith('z') ||
               word.endsWith('ch') || word.endsWith('sh')) {
      return word + 'es';
    } else {
      return word + 's';
    }
  }

  /**
   * Calculate similarity between two strings using Levenshtein distance
   */
  private calculateSimilarity(str1: string, str2: string): number {
    const s1 = str1.toLowerCase();
    const s2 = str2.toLowerCase();

    // Exact match
    if (s1 === s2) return 1.0;

    // One is substring of the other
    if (s1.includes(s2) || s2.includes(s1)) {
      const ratio = Math.min(s1.length, s2.length) / Math.max(s1.length, s2.length);
      return Math.max(ResourceSimilarityService.CONFIDENCE_THRESHOLDS.MIN_SUBSTRING, ratio);
    }

    // Calculate Levenshtein distance
    const distance = this.levenshteinDistance(s1, s2);
    const maxLength = Math.max(s1.length, s2.length);

    // Convert distance to similarity
    let similarity = 1 - (distance / maxLength);

    // Boost confidence for single character typos and transpositions in short words
    if (distance === 1 && maxLength <= 5) {
      similarity = Math.max(similarity, 0.75);
    } else if (distance === 2 && maxLength <= 5) {
      // Boost for transpositions (e.g., "flie" -> "file")
      similarity = Math.max(similarity, 0.72);
    }

    return similarity;
  }

  /**
   * Calculate Levenshtein distance between two strings
   */
  private levenshteinDistance(str1: string, str2: string): number {
    const m = str1.length;
    const n = str2.length;
    const dp: number[][] = Array(m + 1).fill(null).map(() => Array(n + 1).fill(0));

    for (let i = 0; i <= m; i++) dp[i][0] = i;
    for (let j = 0; j <= n; j++) dp[0][j] = j;

    for (let i = 1; i <= m; i++) {
      for (let j = 1; j <= n; j++) {
        if (str1[i - 1] === str2[j - 1]) {
          dp[i][j] = dp[i - 1][j - 1];
        } else {
          dp[i][j] = Math.min(
            dp[i - 1][j] + 1,    // deletion
            dp[i][j - 1] + 1,    // insertion
            dp[i - 1][j - 1] + 1 // substitution
          );
        }
      }
    }

    return dp[m][n];
  }

  /**
   * Generate a human-readable reason for the similarity
   * @param confidence - Similarity confidence score
   * @param invalid - The invalid resource string
   * @param valid - The valid resource string
   * @returns Human-readable explanation of the similarity
   */
  private getSimilarityReason(confidence: number, invalid: string, valid: string): string {
    const { VERY_HIGH, HIGH, MEDIUM } = ResourceSimilarityService.CONFIDENCE_THRESHOLDS;

    if (confidence >= VERY_HIGH) {
      return 'Almost exact match - likely a typo';
    } else if (confidence >= HIGH) {
      return 'Very similar - common variation';
    } else if (confidence >= MEDIUM) {
      return 'Similar resource name';
    } else if (invalid.includes(valid) || valid.includes(invalid)) {
      return 'Partial match';
    } else {
      return 'Possibly related resource';
    }
  }

  /**
   * Clear caches
   */
  clearCache(): void {
    this.resourceCache.clear();
    this.suggestionCache.clear();
  }
}
```

--------------------------------------------------------------------------------
/tests/unit/parsers/property-extractor.test.ts:
--------------------------------------------------------------------------------

```typescript
import { describe, it, expect, beforeEach } from 'vitest';
import { PropertyExtractor } from '@/parsers/property-extractor';
import {
  programmaticNodeFactory,
  declarativeNodeFactory,
  versionedNodeClassFactory,
  versionedNodeTypeClassFactory,
  nodeClassFactory,
  propertyFactory,
  stringPropertyFactory,
  numberPropertyFactory,
  booleanPropertyFactory,
  optionsPropertyFactory,
  collectionPropertyFactory,
  nestedPropertyFactory,
  resourcePropertyFactory,
  operationPropertyFactory,
  aiToolNodeFactory
} from '@tests/fixtures/factories/parser-node.factory';

describe('PropertyExtractor', () => {
  let extractor: PropertyExtractor;

  beforeEach(() => {
    extractor = new PropertyExtractor();
  });

  describe('extractProperties', () => {
    it('should extract properties from programmatic node', () => {
      const nodeDefinition = programmaticNodeFactory.build();
      const NodeClass = nodeClassFactory.build({ description: nodeDefinition });
      
      const properties = extractor.extractProperties(NodeClass as any);
      
      expect(properties).toHaveLength(nodeDefinition.properties.length);
      expect(properties).toEqual(expect.arrayContaining(
        nodeDefinition.properties.map(prop => expect.objectContaining({
          displayName: prop.displayName,
          name: prop.name,
          type: prop.type,
          default: prop.default
        }))
      ));
    });

    it('should extract properties from versioned node latest version', () => {
      const versionedDef = versionedNodeClassFactory.build();
      const NodeClass = class {
        nodeVersions = versionedDef.nodeVersions;
        baseDescription = versionedDef.baseDescription;
      };
      
      const properties = extractor.extractProperties(NodeClass as any);
      
      // Should get properties from version 2 (latest)
      expect(properties).toHaveLength(versionedDef.nodeVersions![2].description.properties.length);
    });

    it('should extract properties from instance with nodeVersions', () => {
      const NodeClass = class {
        description = { name: 'test' };
        constructor() {
          (this as any).nodeVersions = {
            1: {
              description: {
                properties: [propertyFactory.build({ name: 'v1prop' })]
              }
            },
            2: {
              description: {
                properties: [
                  propertyFactory.build({ name: 'v2prop1' }),
                  propertyFactory.build({ name: 'v2prop2' })
                ]
              }
            }
          };
        }
      };
      
      const properties = extractor.extractProperties(NodeClass as any);
      
      expect(properties).toHaveLength(2);
      expect(properties[0].name).toBe('v2prop1');
      expect(properties[1].name).toBe('v2prop2');
    });

    it('should normalize properties to consistent structure', () => {
      const rawProperties = [
        {
          displayName: 'Field 1',
          name: 'field1',
          type: 'string',
          default: 'value',
          description: 'Test field',
          required: true,
          displayOptions: { show: { resource: ['user'] } },
          typeOptions: { multipleValues: true },
          noDataExpression: false,
          extraField: 'should be removed'
        }
      ];
      
      const NodeClass = nodeClassFactory.build({
        description: { 
          name: 'test',
          properties: rawProperties 
        }
      });
      
      const properties = extractor.extractProperties(NodeClass as any);
      
      expect(properties[0]).toEqual({
        displayName: 'Field 1',
        name: 'field1',
        type: 'string',
        default: 'value',
        description: 'Test field',
        options: undefined,
        required: true,
        displayOptions: { show: { resource: ['user'] } },
        typeOptions: { multipleValues: true },
        noDataExpression: false
      });
      
      expect(properties[0]).not.toHaveProperty('extraField');
    });

    it('should handle nodes without properties', () => {
      const NodeClass = nodeClassFactory.build({
        description: {
          name: 'test',
          displayName: 'Test'
          // No properties field
        }
      });
      
      const properties = extractor.extractProperties(NodeClass as any);
      
      expect(properties).toEqual([]);
    });

    it('should handle failed instantiation', () => {
      const NodeClass = class {
        static description = {
          name: 'test',
          properties: [propertyFactory.build()]
        };
        constructor() {
          throw new Error('Cannot instantiate');
        }
      };
      
      const properties = extractor.extractProperties(NodeClass as any);
      
      expect(properties).toHaveLength(1); // Should get static description property
    });

    it('should extract from baseDescription when main description is missing', () => {
      const NodeClass = class {
        baseDescription = {
          properties: [
            stringPropertyFactory.build({ name: 'baseProp' })
          ]
        };
      };
      
      const properties = extractor.extractProperties(NodeClass as any);
      
      expect(properties).toHaveLength(1);
      expect(properties[0].name).toBe('baseProp');
    });

    it('should handle complex nested properties', () => {
      const nestedProp = nestedPropertyFactory.build();
      const NodeClass = nodeClassFactory.build({
        description: {
          name: 'test',
          properties: [nestedProp]
        }
      });
      
      const properties = extractor.extractProperties(NodeClass as any);
      
      expect(properties).toHaveLength(1);
      expect(properties[0].type).toBe('collection');
      expect(properties[0].options).toBeDefined();
    });

    it('should handle non-function node classes', () => {
      const nodeInstance = {
        description: {
          properties: [propertyFactory.build()]
        }
      };

      const properties = extractor.extractProperties(nodeInstance as any);

      expect(properties).toHaveLength(1);
    });
  });

  describe('extractOperations', () => {
    it('should extract operations from declarative node routing', () => {
      const nodeDefinition = declarativeNodeFactory.build();
      const NodeClass = nodeClassFactory.build({ description: nodeDefinition });
      
      const operations = extractor.extractOperations(NodeClass as any);
      
      // Declarative node has 2 resources with 2 operations each = 4 total
      expect(operations.length).toBe(4);
      
      // Check that we have operations for each resource
      const userOps = operations.filter(op => op.resource === 'user');
      const postOps = operations.filter(op => op.resource === 'post');
      
      expect(userOps.length).toBe(2); // Create and Get
      expect(postOps.length).toBe(2); // Create and List
      
      // Verify operation structure
      expect(userOps[0]).toMatchObject({
        resource: 'user',
        operation: expect.any(String),
        name: expect.any(String),
        action: expect.any(String)
      });
    });

    it('should extract operations when node has programmatic properties', () => {
      const operationProp = operationPropertyFactory.build();
      const NodeClass = nodeClassFactory.build({
        description: {
          name: 'test',
          properties: [operationProp]
        }
      });
      
      const operations = extractor.extractOperations(NodeClass as any);
      
      expect(operations.length).toBe(operationProp.options!.length);
      operations.forEach((op, idx) => {
        expect(op).toMatchObject({
          operation: operationProp.options![idx].value,
          name: operationProp.options![idx].name,
          description: operationProp.options![idx].description
        });
      });
    });

    it('should extract operations when routing.operations structure exists', () => {
      const NodeClass = nodeClassFactory.build({
        description: {
          name: 'test',
          routing: {
            operations: {
              create: { displayName: 'Create Item' },
              update: { displayName: 'Update Item' },
              delete: { displayName: 'Delete Item' }
            }
          }
        }
      });
      
      const operations = extractor.extractOperations(NodeClass as any);
      
      // routing.operations is not currently extracted by the property extractor
      // It only extracts from routing.request structure
      expect(operations).toHaveLength(0);
    });

    it('should handle operations when programmatic nodes have resource-based structure', () => {
      const resourceProp = resourcePropertyFactory.build();
      const operationProp = {
        displayName: 'Operation',
        name: 'operation',
        type: 'options',
        displayOptions: {
          show: {
            resource: ['user', 'post']
          }
        },
        options: [
          { name: 'Create', value: 'create', action: 'Create item' },
          { name: 'Delete', value: 'delete', action: 'Delete item' }
        ]
      };
      
      const NodeClass = nodeClassFactory.build({
        description: {
          name: 'test',
          properties: [resourceProp, operationProp]
        }
      });
      
      const operations = extractor.extractOperations(NodeClass as any);
      
      // PropertyExtractor only extracts operations, not resources
      // It should find the operation property and extract its options
      expect(operations).toHaveLength(operationProp.options.length);
      expect(operations[0]).toMatchObject({
        operation: 'create',
        name: 'Create',
        description: undefined // action field is not mapped to description
      });
      expect(operations[1]).toMatchObject({
        operation: 'delete',
        name: 'Delete',
        description: undefined
      });
    });

    it('should return empty array when node has no operations', () => {
      const NodeClass = nodeClassFactory.build({
        description: {
          name: 'test',
          properties: [stringPropertyFactory.build()]
        }
      });
      
      const operations = extractor.extractOperations(NodeClass as any);
      
      expect(operations).toEqual([]);
    });

    it('should extract operations when node has version structure', () => {
      const NodeClass = class {
        nodeVersions = {
          1: {
            description: {
              properties: []
            }
          },
          2: {
            description: {
              routing: {
                request: {
                  resource: {
                    options: [
                      { name: 'User', value: 'user' }
                    ]
                  },
                  operation: {
                    options: {
                      user: [
                        { name: 'Get', value: 'get', action: 'Get a user' }
                      ]
                    }
                  }
                }
              }
            }
          }
        };
      };
      
      const operations = extractor.extractOperations(NodeClass as any);
      
      expect(operations).toHaveLength(1);
      expect(operations[0]).toMatchObject({
        resource: 'user',
        operation: 'get',
        name: 'User - Get',
        action: 'Get a user'
      });
    });

    it('should handle extraction when property is named action instead of operation', () => {
      const actionProp = {
        displayName: 'Action',
        name: 'action',
        type: 'options',
        options: [
          { name: 'Send', value: 'send' },
          { name: 'Receive', value: 'receive' }
        ]
      };
      
      const NodeClass = nodeClassFactory.build({
        description: {
          name: 'test',
          properties: [actionProp]
        }
      });
      
      const operations = extractor.extractOperations(NodeClass as any);
      
      expect(operations).toHaveLength(2);
      expect(operations[0].operation).toBe('send');
    });
  });

  describe('detectAIToolCapability', () => {
    it('should detect AI capability when usableAsTool property is true', () => {
      const NodeClass = nodeClassFactory.build({
        description: {
          name: 'test',
          usableAsTool: true
        }
      });
      
      const isAITool = extractor.detectAIToolCapability(NodeClass as any);
      
      expect(isAITool).toBe(true);
    });

    it('should detect AI capability when actions contain usableAsTool', () => {
      const NodeClass = nodeClassFactory.build({
        description: {
          name: 'test',
          actions: [
            { name: 'action1', usableAsTool: false },
            { name: 'action2', usableAsTool: true }
          ]
        }
      });
      
      const isAITool = extractor.detectAIToolCapability(NodeClass as any);
      
      expect(isAITool).toBe(true);
    });

    it('should detect AI capability when versioned node has usableAsTool', () => {
      const NodeClass = {
        nodeVersions: {
          1: {
            description: { usableAsTool: false }
          },
          2: {
            description: { usableAsTool: true }
          }
        }
      };
      
      const isAITool = extractor.detectAIToolCapability(NodeClass as any);
      
      expect(isAITool).toBe(true);
    });

    it('should detect AI capability when node name contains AI-related terms', () => {
      const aiNodeNames = ['openai', 'anthropic', 'huggingface', 'cohere', 'myai'];
      
      aiNodeNames.forEach(name => {
        const NodeClass = nodeClassFactory.build({
          description: { name }
        });
        
        const isAITool = extractor.detectAIToolCapability(NodeClass as any);
        
        expect(isAITool).toBe(true);
      });
    });

    it('should return false when node is not AI-related', () => {
      const NodeClass = nodeClassFactory.build({
        description: {
          name: 'slack',
          usableAsTool: false
        }
      });
      
      const isAITool = extractor.detectAIToolCapability(NodeClass as any);
      
      expect(isAITool).toBe(false);
    });

    it('should return false when node has no description', () => {
      const NodeClass = class {};
      
      const isAITool = extractor.detectAIToolCapability(NodeClass as any);
      
      expect(isAITool).toBe(false);
    });
  });

  describe('extractCredentials', () => {
    it('should extract credentials when node description contains them', () => {
      const credentials = [
        { name: 'apiKey', required: true },
        { name: 'oauth2', required: false }
      ];
      
      const NodeClass = nodeClassFactory.build({
        description: {
          name: 'test',
          credentials
        }
      });
      
      const extracted = extractor.extractCredentials(NodeClass as any);
      
      expect(extracted).toEqual(credentials);
    });

    it('should extract credentials when node has version structure', () => {
      const NodeClass = class {
        nodeVersions = {
          1: {
            description: {
              credentials: [{ name: 'basic', required: true }]
            }
          },
          2: {
            description: {
              credentials: [
                { name: 'oauth2', required: true },
                { name: 'apiKey', required: false }
              ]
            }
          }
        };
      };
      
      const credentials = extractor.extractCredentials(NodeClass as any);
      
      expect(credentials).toHaveLength(2);
      expect(credentials[0].name).toBe('oauth2');
      expect(credentials[1].name).toBe('apiKey');
    });

    it('should return empty array when node has no credentials', () => {
      const NodeClass = nodeClassFactory.build({
        description: {
          name: 'test'
          // No credentials field
        }
      });
      
      const credentials = extractor.extractCredentials(NodeClass as any);
      
      expect(credentials).toEqual([]);
    });

    it('should extract credentials when only baseDescription has them', () => {
      const NodeClass = class {
        baseDescription = {
          credentials: [{ name: 'token', required: true }]
        };
      };
      
      const credentials = extractor.extractCredentials(NodeClass as any);
      
      expect(credentials).toHaveLength(1);
      expect(credentials[0].name).toBe('token');
    });

    it('should extract credentials when they are defined at instance level', () => {
      const NodeClass = class {
        constructor() {
          (this as any).description = {
            credentials: [
              { name: 'jwt', required: true }
            ]
          };
        }
      };
      
      const credentials = extractor.extractCredentials(NodeClass as any);
      
      expect(credentials).toHaveLength(1);
      expect(credentials[0].name).toBe('jwt');
    });

    it('should return empty array when instantiation fails', () => {
      const NodeClass = class {
        constructor() {
          throw new Error('Cannot instantiate');
        }
      };
      
      const credentials = extractor.extractCredentials(NodeClass as any);
      
      expect(credentials).toEqual([]);
    });
  });

  describe('edge cases', () => {
    it('should handle extraction when properties are deeply nested', () => {
      const deepProperty = {
        displayName: 'Deep Options',
        name: 'deepOptions',
        type: 'collection',
        options: [
          {
            displayName: 'Level 1',
            name: 'level1',
            type: 'collection',
            options: [
              {
                displayName: 'Level 2',
                name: 'level2',
                type: 'collection',
                options: [
                  stringPropertyFactory.build({ name: 'deepValue' })
                ]
              }
            ]
          }
        ]
      };
      
      const NodeClass = nodeClassFactory.build({
        description: {
          name: 'test',
          properties: [deepProperty]
        }
      });
      
      const properties = extractor.extractProperties(NodeClass as any);
      
      expect(properties).toHaveLength(1);
      expect(properties[0].name).toBe('deepOptions');
      expect(properties[0].options[0].options[0].options).toBeDefined();
    });

    it('should not throw when node structure has circular references', () => {
      const NodeClass = class {
        description: any = { name: 'test' };
        constructor() {
          this.description.properties = [
            {
              name: 'prop1',
              type: 'string',
              parentRef: this.description // Circular reference
            }
          ];
        }
      };
      
      // Should not throw or hang
      const properties = extractor.extractProperties(NodeClass as any);
      
      expect(properties).toBeDefined();
    });

    it('should extract from all sources when multiple operation types exist', () => {
      const NodeClass = nodeClassFactory.build({
        description: {
          name: 'test',
          routing: {
            request: {
              resource: {
                options: [{ name: 'Resource1', value: 'res1' }]
              }
            },
            operations: {
              custom: { displayName: 'Custom Op' }
            }
          },
          properties: [
            operationPropertyFactory.build()
          ]
        }
      });
      
      const operations = extractor.extractOperations(NodeClass as any);
      
      // Should extract from all sources
      expect(operations.length).toBeGreaterThan(1);
    });
  });
});
```

--------------------------------------------------------------------------------
/tests/integration/database/node-repository.test.ts:
--------------------------------------------------------------------------------

```typescript
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import Database from 'better-sqlite3';
import { NodeRepository } from '../../../src/database/node-repository';
import { DatabaseAdapter } from '../../../src/database/database-adapter';
import { TestDatabase, TestDataGenerator, MOCK_NODES, createTestDatabaseAdapter } from './test-utils';
import { ParsedNode } from '../../../src/parsers/node-parser';

describe('NodeRepository Integration Tests', () => {
  let testDb: TestDatabase;
  let db: Database.Database;
  let repository: NodeRepository;
  let adapter: DatabaseAdapter;

  beforeEach(async () => {
    testDb = new TestDatabase({ mode: 'memory' });
    db = await testDb.initialize();
    adapter = createTestDatabaseAdapter(db);
    repository = new NodeRepository(adapter);
  });

  afterEach(async () => {
    await testDb.cleanup();
  });

  describe('saveNode', () => {
    it('should save single node successfully', () => {
      const node = createParsedNode(MOCK_NODES.webhook);
      repository.saveNode(node);

      const saved = repository.getNode(node.nodeType);
      expect(saved).toBeTruthy();
      expect(saved.nodeType).toBe(node.nodeType);
      expect(saved.displayName).toBe(node.displayName);
    });

    it('should update existing nodes', () => {
      const node = createParsedNode(MOCK_NODES.webhook);
      
      // Save initial version
      repository.saveNode(node);
      
      // Update and save again
      const updated = { ...node, displayName: 'Updated Webhook' };
      repository.saveNode(updated);

      const saved = repository.getNode(node.nodeType);
      expect(saved?.displayName).toBe('Updated Webhook');
      
      // Should not create duplicate
      const count = repository.getNodeCount();
      expect(count).toBe(1);
    });

    it('should handle nodes with complex properties', () => {
      const complexNode: ParsedNode = {
        nodeType: 'n8n-nodes-base.complex',
        packageName: 'n8n-nodes-base',
        displayName: 'Complex Node',
        description: 'A complex node with many properties',
        category: 'automation',
        style: 'programmatic',
        isAITool: false,
        isTrigger: false,
        isWebhook: false,
        isVersioned: true,
        version: '1',
        documentation: 'Complex node documentation',
        properties: [
          {
            displayName: 'Resource',
            name: 'resource',
            type: 'options',
            options: [
              { name: 'User', value: 'user' },
              { name: 'Post', value: 'post' }
            ],
            default: 'user'
          },
          {
            displayName: 'Operation',
            name: 'operation',
            type: 'options',
            displayOptions: {
              show: {
                resource: ['user']
              }
            },
            options: [
              { name: 'Create', value: 'create' },
              { name: 'Get', value: 'get' }
            ]
          }
        ],
        operations: [
          { resource: 'user', operation: 'create' },
          { resource: 'user', operation: 'get' }
        ],
        credentials: [
          {
            name: 'httpBasicAuth',
            required: false
          }
        ]
      };

      repository.saveNode(complexNode);
      
      const saved = repository.getNode(complexNode.nodeType);
      expect(saved).toBeTruthy();
      expect(saved.properties).toHaveLength(2);
      expect(saved.credentials).toHaveLength(1);
      expect(saved.operations).toHaveLength(2);
    });

    it('should handle very large nodes', () => {
      const largeNode: ParsedNode = {
        nodeType: 'n8n-nodes-base.large',
        packageName: 'n8n-nodes-base',
        displayName: 'Large Node',
        description: 'A very large node',
        category: 'automation',
        style: 'programmatic',
        isAITool: false,
        isTrigger: false,
        isWebhook: false,
        isVersioned: true,
        version: '1',
        properties: Array.from({ length: 100 }, (_, i) => ({
          displayName: `Property ${i}`,
          name: `prop${i}`,
          type: 'string',
          default: ''
        })),
        operations: [],
        credentials: []
      };

      repository.saveNode(largeNode);
      
      const saved = repository.getNode(largeNode.nodeType);
      expect(saved?.properties).toHaveLength(100);
    });
  });

  describe('getNode', () => {
    beforeEach(() => {
      repository.saveNode(createParsedNode(MOCK_NODES.webhook));
      repository.saveNode(createParsedNode(MOCK_NODES.httpRequest));
    });

    it('should retrieve node by type', () => {
      const node = repository.getNode('n8n-nodes-base.webhook');
      expect(node).toBeTruthy();
      expect(node.displayName).toBe('Webhook');
      expect(node.nodeType).toBe('n8n-nodes-base.webhook');
      expect(node.package).toBe('n8n-nodes-base');
    });

    it('should return null for non-existent node', () => {
      const node = repository.getNode('n8n-nodes-base.nonExistent');
      expect(node).toBeNull();
    });

    it('should handle special characters in node types', () => {
      const specialNode: ParsedNode = {
        nodeType: 'n8n-nodes-base.special-chars_v2.node',
        packageName: 'n8n-nodes-base',
        displayName: 'Special Node',
        description: 'Node with special characters',
        category: 'automation',
        style: 'programmatic',
        isAITool: false,
        isTrigger: false,
        isWebhook: false,
        isVersioned: true,
        version: '2',
        properties: [],
        operations: [],
        credentials: []
      };
      
      repository.saveNode(specialNode);
      const retrieved = repository.getNode(specialNode.nodeType);
      expect(retrieved).toBeTruthy();
    });
  });

  describe('getAllNodes', () => {
    it('should return empty array when no nodes', () => {
      const nodes = repository.getAllNodes();
      expect(nodes).toHaveLength(0);
    });

    it('should return all nodes with limit', () => {
      const nodes = Array.from({ length: 20 }, (_, i) => 
        createParsedNode({
          ...MOCK_NODES.webhook,
          nodeType: `n8n-nodes-base.node${i}`,
          displayName: `Node ${i}`
        })
      );
      
      nodes.forEach(node => repository.saveNode(node));

      const retrieved = repository.getAllNodes(10);
      expect(retrieved).toHaveLength(10);
    });

    it('should return all nodes without limit', () => {
      const nodes = Array.from({ length: 20 }, (_, i) => 
        createParsedNode({
          ...MOCK_NODES.webhook,
          nodeType: `n8n-nodes-base.node${i}`,
          displayName: `Node ${i}`
        })
      );
      
      nodes.forEach(node => repository.saveNode(node));

      const retrieved = repository.getAllNodes();
      expect(retrieved).toHaveLength(20);
    });

    it('should handle very large result sets efficiently', () => {
      const nodes = Array.from({ length: 1000 }, (_, i) => 
        createParsedNode({
          ...MOCK_NODES.webhook,
          nodeType: `n8n-nodes-base.node${i}`,
          displayName: `Node ${i}`
        })
      );
      
      const insertMany = db.transaction((nodes: ParsedNode[]) => {
        nodes.forEach(node => repository.saveNode(node));
      });

      const start = Date.now();
      insertMany(nodes);
      const duration = Date.now() - start;

      expect(duration).toBeLessThan(1000); // Should complete in under 1 second

      const retrieved = repository.getAllNodes();
      expect(retrieved).toHaveLength(1000);
    });
  });

  describe('getNodesByPackage', () => {
    beforeEach(() => {
      const nodes = [
        createParsedNode({ 
          ...MOCK_NODES.webhook,
          nodeType: 'n8n-nodes-base.node1',
          packageName: 'n8n-nodes-base'
        }),
        createParsedNode({ 
          ...MOCK_NODES.webhook,
          nodeType: 'n8n-nodes-base.node2',
          packageName: 'n8n-nodes-base' 
        }),
        createParsedNode({ 
          ...MOCK_NODES.webhook,
          nodeType: '@n8n/n8n-nodes-langchain.node3',
          packageName: '@n8n/n8n-nodes-langchain' 
        })
      ];
      nodes.forEach(node => repository.saveNode(node));
    });

    it('should filter nodes by package', () => {
      const baseNodes = repository.getNodesByPackage('n8n-nodes-base');
      expect(baseNodes).toHaveLength(2);

      const langchainNodes = repository.getNodesByPackage('@n8n/n8n-nodes-langchain');
      expect(langchainNodes).toHaveLength(1);
    });

    it('should return empty array for non-existent package', () => {
      const nodes = repository.getNodesByPackage('non-existent-package');
      expect(nodes).toHaveLength(0);
    });
  });

  describe('getNodesByCategory', () => {
    beforeEach(() => {
      const nodes = [
        createParsedNode({ 
          ...MOCK_NODES.webhook,
          nodeType: 'n8n-nodes-base.webhook',
          category: 'trigger'
        }),
        createParsedNode({ 
          ...MOCK_NODES.webhook,
          nodeType: 'n8n-nodes-base.schedule',
          displayName: 'Schedule',
          category: 'trigger'
        }),
        createParsedNode({ 
          ...MOCK_NODES.httpRequest,
          nodeType: 'n8n-nodes-base.httpRequest',
          category: 'automation'
        })
      ];
      nodes.forEach(node => repository.saveNode(node));
    });

    it('should filter nodes by category', () => {
      const triggers = repository.getNodesByCategory('trigger');
      expect(triggers).toHaveLength(2);
      expect(triggers.every(n => n.category === 'trigger')).toBe(true);

      const automation = repository.getNodesByCategory('automation');
      expect(automation).toHaveLength(1);
      expect(automation[0].category).toBe('automation');
    });
  });

  describe('searchNodes', () => {
    beforeEach(() => {
      const nodes = [
        createParsedNode({
          ...MOCK_NODES.webhook,
          description: 'Starts the workflow when webhook is called'
        }),
        createParsedNode({
          ...MOCK_NODES.httpRequest,
          description: 'Makes HTTP requests to external APIs'
        }),
        createParsedNode({
          nodeType: 'n8n-nodes-base.emailSend',
          packageName: 'n8n-nodes-base',
          displayName: 'Send Email',
          description: 'Sends emails via SMTP protocol',
          category: 'communication',
          developmentStyle: 'programmatic',
          isAITool: false,
          isTrigger: false,
          isWebhook: false,
          isVersioned: true,
          version: '1',
          properties: [],
          operations: [],
          credentials: []
        })
      ];
      nodes.forEach(node => repository.saveNode(node));
    });

    it('should search by node type', () => {
      const results = repository.searchNodes('webhook');
      expect(results).toHaveLength(1);
      expect(results[0].nodeType).toBe('n8n-nodes-base.webhook');
    });

    it('should search by display name', () => {
      const results = repository.searchNodes('Send Email');
      expect(results).toHaveLength(1);
      expect(results[0].nodeType).toBe('n8n-nodes-base.emailSend');
    });

    it('should search by description', () => {
      const results = repository.searchNodes('SMTP');
      expect(results).toHaveLength(1);
      expect(results[0].nodeType).toBe('n8n-nodes-base.emailSend');
    });

    it('should handle OR mode (default)', () => {
      const results = repository.searchNodes('webhook email', 'OR');
      expect(results).toHaveLength(2);
      const nodeTypes = results.map(r => r.nodeType);
      expect(nodeTypes).toContain('n8n-nodes-base.webhook');
      expect(nodeTypes).toContain('n8n-nodes-base.emailSend');
    });

    it('should handle AND mode', () => {
      const results = repository.searchNodes('HTTP request', 'AND');
      expect(results).toHaveLength(1);
      expect(results[0].nodeType).toBe('n8n-nodes-base.httpRequest');
    });

    it('should handle FUZZY mode', () => {
      const results = repository.searchNodes('HTT', 'FUZZY');
      expect(results).toHaveLength(1);
      expect(results[0].nodeType).toBe('n8n-nodes-base.httpRequest');
    });

    it('should handle case-insensitive search', () => {
      const results = repository.searchNodes('WEBHOOK');
      expect(results).toHaveLength(1);
      expect(results[0].nodeType).toBe('n8n-nodes-base.webhook');
    });

    it('should return empty array for no matches', () => {
      const results = repository.searchNodes('nonexistent');
      expect(results).toHaveLength(0);
    });

    it('should respect limit parameter', () => {
      // Add more nodes
      const nodes = Array.from({ length: 10 }, (_, i) => 
        createParsedNode({
          ...MOCK_NODES.webhook,
          nodeType: `n8n-nodes-base.test${i}`,
          displayName: `Test Node ${i}`,
          description: 'Test description'
        })
      );
      nodes.forEach(node => repository.saveNode(node));

      const results = repository.searchNodes('test', 'OR', 5);
      expect(results).toHaveLength(5);
    });
  });

  describe('getAITools', () => {
    it('should return only AI tool nodes', () => {
      const nodes = [
        createParsedNode({ 
          ...MOCK_NODES.webhook,
          nodeType: 'n8n-nodes-base.webhook',
          isAITool: false
        }),
        createParsedNode({ 
          ...MOCK_NODES.webhook,
          nodeType: '@n8n/n8n-nodes-langchain.agent',
          displayName: 'AI Agent',
          packageName: '@n8n/n8n-nodes-langchain',
          isAITool: true
        }),
        createParsedNode({ 
          ...MOCK_NODES.webhook,
          nodeType: '@n8n/n8n-nodes-langchain.tool',
          displayName: 'AI Tool',
          packageName: '@n8n/n8n-nodes-langchain',
          isAITool: true
        })
      ];
      
      nodes.forEach(node => repository.saveNode(node));

      const aiTools = repository.getAITools();
      expect(aiTools).toHaveLength(2);
      expect(aiTools.every(node => node.package.includes('langchain'))).toBe(true);
      expect(aiTools[0].displayName).toBe('AI Agent');
      expect(aiTools[1].displayName).toBe('AI Tool');
    });
  });

  describe('getNodeCount', () => {
    it('should return correct node count', () => {
      expect(repository.getNodeCount()).toBe(0);

      repository.saveNode(createParsedNode(MOCK_NODES.webhook));
      expect(repository.getNodeCount()).toBe(1);

      repository.saveNode(createParsedNode(MOCK_NODES.httpRequest));
      expect(repository.getNodeCount()).toBe(2);
    });
  });

  describe('searchNodeProperties', () => {
    beforeEach(() => {
      const node: ParsedNode = {
        nodeType: 'n8n-nodes-base.complex',
        packageName: 'n8n-nodes-base',
        displayName: 'Complex Node',
        description: 'A complex node',
        category: 'automation',
        style: 'programmatic',
        isAITool: false,
        isTrigger: false,
        isWebhook: false,
        isVersioned: true,
        version: '1',
        properties: [
          {
            displayName: 'Authentication',
            name: 'authentication',
            type: 'options',
            options: [
              { name: 'Basic', value: 'basic' },
              { name: 'OAuth2', value: 'oauth2' }
            ]
          },
          {
            displayName: 'Headers',
            name: 'headers',
            type: 'collection',
            default: {},
            options: [
              {
                displayName: 'Header',
                name: 'header',
                type: 'string'
              }
            ]
          }
        ],
        operations: [],
        credentials: []
      };
      repository.saveNode(node);
    });

    it('should find properties by name', () => {
      const results = repository.searchNodeProperties('n8n-nodes-base.complex', 'auth');
      expect(results.length).toBeGreaterThan(0);
      expect(results.some(r => r.path.includes('authentication'))).toBe(true);
    });

    it('should find nested properties', () => {
      const results = repository.searchNodeProperties('n8n-nodes-base.complex', 'header');
      expect(results.length).toBeGreaterThan(0);
    });

    it('should return empty array for non-existent node', () => {
      const results = repository.searchNodeProperties('non-existent', 'test');
      expect(results).toHaveLength(0);
    });
  });

  describe('Transaction handling', () => {
    it('should handle errors gracefully', () => {
      // Test with a node that violates database constraints
      const invalidNode = {
        nodeType: '', // Empty string should violate PRIMARY KEY constraint
        packageName: null, // NULL should violate NOT NULL constraint
        displayName: null, // NULL should violate NOT NULL constraint
        description: '',
        category: 'automation',
        style: 'programmatic',
        isAITool: false,
        isTrigger: false,
        isWebhook: false,
        isVersioned: false,
        version: '1',
        properties: [],
        operations: [],
        credentials: []
      } as any;

      expect(() => {
        repository.saveNode(invalidNode);
      }).toThrow();

      // Repository should still be functional
      const count = repository.getNodeCount();
      expect(count).toBe(0);
    });

    it('should handle concurrent saves', () => {
      const node = createParsedNode(MOCK_NODES.webhook);
      
      // Simulate concurrent saves of the same node with different display names
      const promises = Array.from({ length: 10 }, (_, i) => {
        const updatedNode = {
          ...node,
          displayName: `Display ${i}`
        };
        return Promise.resolve(repository.saveNode(updatedNode));
      });

      Promise.all(promises);

      // Should have only one node
      const count = repository.getNodeCount();
      expect(count).toBe(1);
      
      // Should have the last update
      const saved = repository.getNode(node.nodeType);
      expect(saved).toBeTruthy();
    });
  });

  describe('Performance characteristics', () => {
    it('should handle bulk operations efficiently', () => {
      const nodeCount = 1000;
      const nodes = Array.from({ length: nodeCount }, (_, i) => 
        createParsedNode({
          ...MOCK_NODES.webhook,
          nodeType: `n8n-nodes-base.node${i}`,
          displayName: `Node ${i}`,
          description: `Description for node ${i}`
        })
      );

      const insertMany = db.transaction((nodes: ParsedNode[]) => {
        nodes.forEach(node => repository.saveNode(node));
      });

      const start = Date.now();
      insertMany(nodes);
      const saveDuration = Date.now() - start;

      expect(saveDuration).toBeLessThan(1000); // Should complete in under 1 second

      // Test search performance
      const searchStart = Date.now();
      const results = repository.searchNodes('node', 'OR', 100);
      const searchDuration = Date.now() - searchStart;

      expect(searchDuration).toBeLessThan(50); // Search should be fast
      expect(results.length).toBe(100); // Respects limit
    });
  });
});

// Helper function to create ParsedNode from test data
function createParsedNode(data: any): ParsedNode {
  return {
    nodeType: data.nodeType,
    packageName: data.packageName,
    displayName: data.displayName,
    description: data.description || '',
    category: data.category || 'automation',
    style: data.developmentStyle || 'programmatic',
    isAITool: data.isAITool || false,
    isTrigger: data.isTrigger || false,
    isWebhook: data.isWebhook || false,
    isVersioned: data.isVersioned !== undefined ? data.isVersioned : true,
    version: data.version || '1',
    documentation: data.documentation || null,
    properties: data.properties || [],
    operations: data.operations || [],
    credentials: data.credentials || []
  };
}
```

--------------------------------------------------------------------------------
/src/mcp/tool-docs/guides/ai-agents-guide.ts:
--------------------------------------------------------------------------------

```typescript
import { ToolDocumentation } from '../types';

export const aiAgentsGuide: ToolDocumentation = {
  name: 'ai_agents_guide',
  category: 'guides',
  essentials: {
    description: 'Comprehensive guide to building AI Agent workflows in n8n. Covers architecture, connections, tools, validation, and best practices for production AI systems.',
    keyParameters: [],
    example: 'Use tools_documentation({topic: "ai_agents_guide"}) to access this guide',
    performance: 'N/A - Documentation only',
    tips: [
      'Start with Chat Trigger → AI Agent → Language Model pattern',
      'Always connect language model BEFORE enabling AI Agent',
      'Use proper toolDescription for all AI tools (15+ characters)',
      'Validate workflows with n8n_validate_workflow before deployment',
      'Use includeExamples=true when searching for AI nodes',
      'Check FINAL_AI_VALIDATION_SPEC.md for detailed requirements'
    ]
  },
  full: {
    description: `# Complete Guide to AI Agents in n8n

This comprehensive guide covers everything you need to build production-ready AI Agent workflows in n8n.

## Table of Contents
1. [AI Agent Architecture](#architecture)
2. [Essential Connection Types](#connections)
3. [Building Your First AI Agent](#first-agent)
4. [AI Tools Deep Dive](#tools)
5. [Advanced Patterns](#advanced)
6. [Validation & Best Practices](#validation)
7. [Troubleshooting](#troubleshooting)

---

## 1. AI Agent Architecture {#architecture}

### Core Components

An n8n AI Agent workflow typically consists of:

1. **Chat Trigger**: Entry point for user interactions
   - Webhook-based or manual trigger
   - Supports streaming responses (responseMode)
   - Passes user message to AI Agent

2. **AI Agent**: The orchestrator
   - Manages conversation flow
   - Decides when to use tools
   - Iterates until task is complete
   - Supports fallback models (v2.1+)

3. **Language Model**: The AI brain
   - OpenAI GPT-4, Claude, Gemini, etc.
   - Connected via ai_languageModel port
   - Can have primary + fallback for reliability

4. **Tools**: AI Agent's capabilities
   - HTTP Request, Code, Vector Store, etc.
   - Connected via ai_tool port
   - Each tool needs clear toolDescription

5. **Optional Components**:
   - Memory (conversation history)
   - Output Parser (structured responses)
   - Vector Store (knowledge retrieval)

### Connection Flow

**CRITICAL**: AI connections flow TO the consumer (reversed from standard n8n):

\`\`\`
Standard n8n:  [Source] --main--> [Target]
AI pattern:    [Language Model] --ai_languageModel--> [AI Agent]
               [HTTP Tool] --ai_tool--> [AI Agent]
\`\`\`

This is why you use \`sourceOutput: "ai_languageModel"\` when connecting components.

---

## 2. Essential Connection Types {#connections}

### The 8 AI Connection Types

1. **ai_languageModel**
   - FROM: OpenAI Chat Model, Anthropic, Google Gemini, etc.
   - TO: AI Agent, Basic LLM Chain
   - REQUIRED: Every AI Agent needs 1-2 language models
   - Example: \`{type: "addConnection", source: "OpenAI", target: "AI Agent", sourceOutput: "ai_languageModel"}\`

2. **ai_tool**
   - FROM: Any tool node (HTTP Request Tool, Code Tool, etc.)
   - TO: AI Agent
   - REQUIRED: At least 1 tool recommended
   - Example: \`{type: "addConnection", source: "HTTP Request Tool", target: "AI Agent", sourceOutput: "ai_tool"}\`

3. **ai_memory**
   - FROM: Window Buffer Memory, Conversation Summary, etc.
   - TO: AI Agent
   - OPTIONAL: 0-1 memory system
   - Enables conversation history tracking

4. **ai_outputParser**
   - FROM: Structured Output Parser, JSON Parser, etc.
   - TO: AI Agent
   - OPTIONAL: For structured responses
   - Must set hasOutputParser=true on AI Agent

5. **ai_embedding**
   - FROM: Embeddings OpenAI, Embeddings Google, etc.
   - TO: Vector Store (Pinecone, In-Memory, etc.)
   - REQUIRED: For vector-based retrieval

6. **ai_vectorStore**
   - FROM: Vector Store node
   - TO: Vector Store Tool
   - REQUIRED: For retrieval-augmented generation (RAG)

7. **ai_document**
   - FROM: Document Loader, Default Data Loader
   - TO: Vector Store
   - REQUIRED: Provides data for vector storage

8. **ai_textSplitter**
   - FROM: Text Splitter nodes
   - TO: Document processing chains
   - OPTIONAL: Chunk large documents

### Connection Examples

\`\`\`typescript
// Basic AI Agent setup
n8n_update_partial_workflow({
  id: "workflow_id",
  operations: [
    // Connect language model (REQUIRED)
    {
      type: "addConnection",
      source: "OpenAI Chat Model",
      target: "AI Agent",
      sourceOutput: "ai_languageModel"
    },
    // Connect tools
    {
      type: "addConnection",
      source: "HTTP Request Tool",
      target: "AI Agent",
      sourceOutput: "ai_tool"
    },
    {
      type: "addConnection",
      source: "Code Tool",
      target: "AI Agent",
      sourceOutput: "ai_tool"
    },
    // Add memory (optional)
    {
      type: "addConnection",
      source: "Window Buffer Memory",
      target: "AI Agent",
      sourceOutput: "ai_memory"
    }
  ]
})
\`\`\`

---

## 3. Building Your First AI Agent {#first-agent}

### Step-by-Step Tutorial

#### Step 1: Create Chat Trigger

Use \`n8n_create_workflow\` or manually create a workflow with:

\`\`\`typescript
{
  name: "My First AI Agent",
  nodes: [
    {
      id: "chat_trigger",
      name: "Chat Trigger",
      type: "@n8n/n8n-nodes-langchain.chatTrigger",
      position: [100, 100],
      parameters: {
        options: {
          responseMode: "lastNode"  // or "streaming" for real-time
        }
      }
    }
  ],
  connections: {}
}
\`\`\`

#### Step 2: Add Language Model

\`\`\`typescript
n8n_update_partial_workflow({
  id: "workflow_id",
  operations: [
    {
      type: "addNode",
      node: {
        name: "OpenAI Chat Model",
        type: "@n8n/n8n-nodes-langchain.lmChatOpenAi",
        position: [300, 50],
        parameters: {
          model: "gpt-4",
          temperature: 0.7
        }
      }
    }
  ]
})
\`\`\`

#### Step 3: Add AI Agent

\`\`\`typescript
n8n_update_partial_workflow({
  id: "workflow_id",
  operations: [
    {
      type: "addNode",
      node: {
        name: "AI Agent",
        type: "@n8n/n8n-nodes-langchain.agent",
        position: [300, 150],
        parameters: {
          promptType: "auto",
          systemMessage: "You are a helpful assistant. Be concise and accurate."
        }
      }
    }
  ]
})
\`\`\`

#### Step 4: Connect Components

\`\`\`typescript
n8n_update_partial_workflow({
  id: "workflow_id",
  operations: [
    // Chat Trigger → AI Agent (main connection)
    {
      type: "addConnection",
      source: "Chat Trigger",
      target: "AI Agent"
    },
    // Language Model → AI Agent (AI connection)
    {
      type: "addConnection",
      source: "OpenAI Chat Model",
      target: "AI Agent",
      sourceOutput: "ai_languageModel"
    }
  ]
})
\`\`\`

#### Step 5: Validate

\`\`\`typescript
n8n_validate_workflow({id: "workflow_id"})
\`\`\`

---

## 4. AI Tools Deep Dive {#tools}

### Tool Types and When to Use Them

#### 1. HTTP Request Tool
**Use when**: AI needs to call external APIs

**Critical Requirements**:
- \`toolDescription\`: Clear, 15+ character description
- \`url\`: API endpoint (can include placeholders)
- \`placeholderDefinitions\`: Define all {placeholders}
- Proper authentication if needed

**Example**:
\`\`\`typescript
{
  type: "addNode",
  node: {
    name: "GitHub Issues Tool",
    type: "@n8n/n8n-nodes-langchain.toolHttpRequest",
    position: [500, 100],
    parameters: {
      method: "POST",
      url: "https://api.github.com/repos/{owner}/{repo}/issues",
      toolDescription: "Create GitHub issues. Requires owner (username), repo (repository name), title, and body.",
      placeholderDefinitions: {
        values: [
          {name: "owner", description: "Repository owner username"},
          {name: "repo", description: "Repository name"},
          {name: "title", description: "Issue title"},
          {name: "body", description: "Issue description"}
        ]
      },
      sendBody: true,
      jsonBody: "={{ { title: $json.title, body: $json.body } }}"
    }
  }
}
\`\`\`

#### 2. Code Tool
**Use when**: AI needs to run custom logic

**Critical Requirements**:
- \`name\`: Function name (alphanumeric + underscore)
- \`description\`: 10+ character explanation
- \`code\`: JavaScript or Python code
- \`inputSchema\`: Define expected inputs (recommended)

**Example**:
\`\`\`typescript
{
  type: "addNode",
  node: {
    name: "Calculate Shipping",
    type: "@n8n/n8n-nodes-langchain.toolCode",
    position: [500, 200],
    parameters: {
      name: "calculate_shipping",
      description: "Calculate shipping cost based on weight (kg) and distance (km)",
      language: "javaScript",
      code: "const cost = 5 + ($input.weight * 2) + ($input.distance * 0.1); return { cost };",
      specifyInputSchema: true,
      inputSchema: "{ \\"type\\": \\"object\\", \\"properties\\": { \\"weight\\": { \\"type\\": \\"number\\" }, \\"distance\\": { \\"type\\": \\"number\\" } } }"
    }
  }
}
\`\`\`

#### 3. Vector Store Tool
**Use when**: AI needs to search knowledge base

**Setup**: Requires Vector Store + Embeddings + Documents

**Example**:
\`\`\`typescript
// Step 1: Create Vector Store with embeddings and documents
n8n_update_partial_workflow({
  operations: [
    {type: "addConnection", source: "Embeddings OpenAI", target: "Pinecone", sourceOutput: "ai_embedding"},
    {type: "addConnection", source: "Document Loader", target: "Pinecone", sourceOutput: "ai_document"}
  ]
})

// Step 2: Connect Vector Store to Vector Store Tool
n8n_update_partial_workflow({
  operations: [
    {type: "addConnection", source: "Pinecone", target: "Vector Store Tool", sourceOutput: "ai_vectorStore"}
  ]
})

// Step 3: Connect tool to AI Agent
n8n_update_partial_workflow({
  operations: [
    {type: "addConnection", source: "Vector Store Tool", target: "AI Agent", sourceOutput: "ai_tool"}
  ]
})
\`\`\`

#### 4. AI Agent Tool (Sub-Agents)
**Use when**: Need specialized expertise

**Example**: Research specialist sub-agent
\`\`\`typescript
{
  type: "addNode",
  node: {
    name: "Research Specialist",
    type: "@n8n/n8n-nodes-langchain.agentTool",
    position: [500, 300],
    parameters: {
      name: "research_specialist",
      description: "Expert researcher that searches multiple sources and synthesizes information. Use for detailed research tasks.",
      systemMessage: "You are a research specialist. Search thoroughly, cite sources, and provide comprehensive analysis."
    }
  }
}
\`\`\`

#### 5. MCP Client Tool
**Use when**: Need to use Model Context Protocol servers

**Example**: Filesystem access
\`\`\`typescript
{
  type: "addNode",
  node: {
    name: "Filesystem Tool",
    type: "@n8n/n8n-nodes-langchain.mcpClientTool",
    position: [500, 400],
    parameters: {
      description: "Access file system to read files, list directories, and search content",
      mcpServer: {
        transport: "stdio",
        command: "npx",
        args: ["-y", "@modelcontextprotocol/server-filesystem", "/allowed/path"]
      },
      tool: "read_file"
    }
  }
}
\`\`\`

---

## 5. Advanced Patterns {#advanced}

### Pattern 1: Streaming Responses

For real-time user experience:

\`\`\`typescript
// Set Chat Trigger to streaming mode
{
  parameters: {
    options: {
      responseMode: "streaming"
    }
  }
}

// CRITICAL: AI Agent must NOT have main output connections in streaming mode
// Responses stream back through Chat Trigger automatically
\`\`\`

**Validation will fail if**:
- Chat Trigger has streaming but target is not AI Agent
- AI Agent in streaming mode has main output connections

### Pattern 2: Fallback Language Models

For production reliability (requires AI Agent v2.1+):

\`\`\`typescript
n8n_update_partial_workflow({
  operations: [
    // Primary model
    {
      type: "addConnection",
      source: "OpenAI GPT-4",
      target: "AI Agent",
      sourceOutput: "ai_languageModel",
      targetIndex: 0
    },
    // Fallback model
    {
      type: "addConnection",
      source: "Anthropic Claude",
      target: "AI Agent",
      sourceOutput: "ai_languageModel",
      targetIndex: 1
    }
  ]
})

// Enable fallback on AI Agent
{
  type: "updateNode",
  nodeName: "AI Agent",
  updates: {
    "parameters.needsFallback": true
  }
}
\`\`\`

### Pattern 3: RAG (Retrieval-Augmented Generation)

Complete knowledge base setup:

\`\`\`typescript
// 1. Load documents
{type: "addConnection", source: "PDF Loader", target: "Text Splitter", sourceOutput: "ai_document"}

// 2. Split and embed
{type: "addConnection", source: "Text Splitter", target: "Vector Store"}
{type: "addConnection", source: "Embeddings", target: "Vector Store", sourceOutput: "ai_embedding"}

// 3. Create search tool
{type: "addConnection", source: "Vector Store", target: "Vector Store Tool", sourceOutput: "ai_vectorStore"}

// 4. Give tool to agent
{type: "addConnection", source: "Vector Store Tool", target: "AI Agent", sourceOutput: "ai_tool"}
\`\`\`

### Pattern 4: Multi-Agent Systems

Specialized sub-agents for complex tasks:

\`\`\`typescript
// Create sub-agents with specific expertise
[
  {name: "research_agent", description: "Deep research specialist"},
  {name: "data_analyst", description: "Data analysis expert"},
  {name: "writer_agent", description: "Content writing specialist"}
].forEach(agent => {
  // Add as AI Agent Tool to main coordinator agent
  {
    type: "addConnection",
    source: agent.name,
    target: "Coordinator Agent",
    sourceOutput: "ai_tool"
  }
})
\`\`\`

---

## 6. Validation & Best Practices {#validation}

### Always Validate Before Deployment

\`\`\`typescript
const result = n8n_validate_workflow({id: "workflow_id"})

if (!result.valid) {
  console.log("Errors:", result.errors)
  console.log("Warnings:", result.warnings)
  console.log("Suggestions:", result.suggestions)
}
\`\`\`

### Common Validation Errors

1. **MISSING_LANGUAGE_MODEL**
   - Problem: AI Agent has no ai_languageModel connection
   - Fix: Connect a language model before creating AI Agent

2. **MISSING_TOOL_DESCRIPTION**
   - Problem: HTTP Request Tool has no toolDescription
   - Fix: Add clear description (15+ characters)

3. **STREAMING_WITH_MAIN_OUTPUT**
   - Problem: AI Agent in streaming mode has outgoing main connections
   - Fix: Remove main connections when using streaming

4. **FALLBACK_MISSING_SECOND_MODEL**
   - Problem: needsFallback=true but only 1 language model
   - Fix: Add second language model or disable needsFallback

### Best Practices Checklist

✅ **Before Creating AI Agent**:
- [ ] Language model is connected first
- [ ] At least one tool is prepared (or will be added)
- [ ] System message is thoughtful and specific

✅ **For Each Tool**:
- [ ] Has toolDescription/description (15+ characters)
- [ ] toolDescription explains WHEN to use the tool
- [ ] All required parameters are configured
- [ ] Credentials are set up if needed

✅ **For Production**:
- [ ] Workflow validated with n8n_validate_workflow
- [ ] Tested with real user queries
- [ ] Fallback model configured for reliability
- [ ] Error handling in place
- [ ] maxIterations set appropriately (default 10, max 50)

---

## 7. Troubleshooting {#troubleshooting}

### Problem: "AI Agent has no language model"

**Cause**: Connection created AFTER AI Agent or using wrong sourceOutput

**Solution**:
\`\`\`typescript
n8n_update_partial_workflow({
  operations: [
    {
      type: "addConnection",
      source: "OpenAI Chat Model",
      target: "AI Agent",
      sourceOutput: "ai_languageModel"  // ← CRITICAL
    }
  ]
})
\`\`\`

### Problem: "Tool has no description"

**Cause**: HTTP Request Tool or Code Tool missing toolDescription/description

**Solution**:
\`\`\`typescript
{
  type: "updateNode",
  nodeName: "HTTP Request Tool",
  updates: {
    "parameters.toolDescription": "Call weather API to get current conditions for a city"
  }
}
\`\`\`

### Problem: "Streaming mode not working"

**Causes**:
1. Chat Trigger not set to streaming
2. AI Agent has main output connections
3. Target of Chat Trigger is not AI Agent

**Solution**:
\`\`\`typescript
// 1. Set Chat Trigger to streaming
{
  type: "updateNode",
  nodeName: "Chat Trigger",
  updates: {
    "parameters.options.responseMode": "streaming"
  }
}

// 2. Remove AI Agent main outputs
{
  type: "removeConnection",
  source: "AI Agent",
  target: "Any Output Node"
}
\`\`\`

### Problem: "Agent keeps looping"

**Cause**: Tool not returning proper response or agent stuck in reasoning loop

**Solutions**:
1. Set maxIterations lower: \`"parameters.maxIterations": 5\`
2. Improve tool descriptions to be more specific
3. Add system message guidance: "Use tools efficiently, don't repeat actions"

---

## Quick Reference

### Essential Tools

| Tool | Purpose | Key Parameters |
|------|---------|----------------|
| HTTP Request Tool | API calls | toolDescription, url, placeholders |
| Code Tool | Custom logic | name, description, code, inputSchema |
| Vector Store Tool | Knowledge search | description, topK |
| AI Agent Tool | Sub-agents | name, description, systemMessage |
| MCP Client Tool | MCP protocol | description, mcpServer, tool |

### Connection Quick Codes

\`\`\`typescript
// Language Model → AI Agent
sourceOutput: "ai_languageModel"

// Tool → AI Agent
sourceOutput: "ai_tool"

// Memory → AI Agent
sourceOutput: "ai_memory"

// Parser → AI Agent
sourceOutput: "ai_outputParser"

// Embeddings → Vector Store
sourceOutput: "ai_embedding"

// Vector Store → Vector Store Tool
sourceOutput: "ai_vectorStore"
\`\`\`

### Validation Command

\`\`\`typescript
n8n_validate_workflow({id: "workflow_id"})
\`\`\`

---

## Related Resources

- **FINAL_AI_VALIDATION_SPEC.md**: Complete validation rules
- **n8n_update_partial_workflow**: Workflow modification tool
- **search_nodes({query: "AI", includeExamples: true})**: Find AI nodes with examples
- **get_node_essentials({nodeType: "...", includeExamples: true})**: Node details with examples

---

*This guide is part of the n8n-mcp documentation system. For questions or issues, refer to the validation spec or use tools_documentation() for specific topics.*`,
    parameters: {},
    returns: 'Complete AI Agents guide with architecture, patterns, validation, and troubleshooting',
    examples: [
      'tools_documentation({topic: "ai_agents_guide"}) - Full guide',
      'tools_documentation({topic: "ai_agents_guide", depth: "essentials"}) - Quick reference',
      'When user asks about AI Agents, Chat Trigger, or building AI workflows → Point to this guide'
    ],
    useCases: [
      'Learning AI Agent architecture in n8n',
      'Understanding AI connection types and patterns',
      'Building first AI Agent workflow step-by-step',
      'Implementing advanced patterns (streaming, fallback, RAG, multi-agent)',
      'Troubleshooting AI workflow issues',
      'Validating AI workflows before deployment',
      'Quick reference for connection types and tools'
    ],
    performance: 'N/A - Static documentation',
    bestPractices: [
      'Reference this guide when users ask about AI Agents',
      'Point to specific sections based on user needs',
      'Combine with search_nodes(includeExamples=true) for working examples',
      'Validate workflows after following guide instructions',
      'Use FINAL_AI_VALIDATION_SPEC.md for detailed requirements'
    ],
    pitfalls: [
      'This is a guide, not an executable tool',
      'Always validate workflows after making changes',
      'AI connections require sourceOutput parameter',
      'Streaming mode has specific constraints',
      'Some features require specific AI Agent versions (v2.1+ for fallback)'
    ],
    relatedTools: [
      'n8n_create_workflow',
      'n8n_update_partial_workflow',
      'n8n_validate_workflow',
      'search_nodes',
      'get_node_essentials',
      'list_ai_tools'
    ]
  }
};

```

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

```typescript
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { NodeRepository } from '@/database/node-repository';
import { DatabaseAdapter, PreparedStatement, RunResult } from '@/database/database-adapter';

// Mock DatabaseAdapter for testing the new operation methods
class MockDatabaseAdapter implements DatabaseAdapter {
  private statements = new Map<string, MockPreparedStatement>();
  private mockNodes = new Map<string, any>();

  prepare = vi.fn((sql: string) => {
    if (!this.statements.has(sql)) {
      this.statements.set(sql, new MockPreparedStatement(sql, this.mockNodes));
    }
    return this.statements.get(sql)!;
  });

  exec = vi.fn();
  close = vi.fn();
  pragma = vi.fn();
  transaction = vi.fn((fn: () => any) => fn());
  checkFTS5Support = vi.fn(() => true);
  inTransaction = false;

  // Test helper to set mock data
  _setMockNode(nodeType: string, value: any) {
    this.mockNodes.set(nodeType, value);
  }
}

class MockPreparedStatement implements PreparedStatement {
  run = vi.fn((...params: any[]): RunResult => ({ changes: 1, lastInsertRowid: 1 }));
  get = vi.fn();
  all = vi.fn(() => []);
  iterate = vi.fn();
  pluck = vi.fn(() => this);
  expand = vi.fn(() => this);
  raw = vi.fn(() => this);
  columns = vi.fn(() => []);
  bind = vi.fn(() => this);

  constructor(private sql: string, private mockNodes: Map<string, any>) {
    // Configure get() to return node data
    if (sql.includes('SELECT * FROM nodes WHERE node_type = ?')) {
      this.get = vi.fn((nodeType: string) => this.mockNodes.get(nodeType));
    }

    // Configure all() for getAllNodes
    if (sql.includes('SELECT * FROM nodes ORDER BY display_name')) {
      this.all = vi.fn(() => Array.from(this.mockNodes.values()));
    }
  }
}

describe('NodeRepository - Operations and Resources', () => {
  let repository: NodeRepository;
  let mockAdapter: MockDatabaseAdapter;

  beforeEach(() => {
    mockAdapter = new MockDatabaseAdapter();
    repository = new NodeRepository(mockAdapter);
  });

  describe('getNodeOperations', () => {
    it('should extract operations from array format', () => {
      const mockNode = {
        node_type: 'nodes-base.httpRequest',
        display_name: 'HTTP Request',
        operations: JSON.stringify([
          { name: 'get', displayName: 'GET' },
          { name: 'post', displayName: 'POST' }
        ]),
        properties_schema: '[]',
        credentials_required: '[]'
      };

      mockAdapter._setMockNode('nodes-base.httpRequest', mockNode);

      const operations = repository.getNodeOperations('nodes-base.httpRequest');

      expect(operations).toEqual([
        { name: 'get', displayName: 'GET' },
        { name: 'post', displayName: 'POST' }
      ]);
    });

    it('should extract operations from object format grouped by resource', () => {
      const mockNode = {
        node_type: 'nodes-base.slack',
        display_name: 'Slack',
        operations: JSON.stringify({
          message: [
            { name: 'send', displayName: 'Send Message' },
            { name: 'update', displayName: 'Update Message' }
          ],
          channel: [
            { name: 'create', displayName: 'Create Channel' },
            { name: 'archive', displayName: 'Archive Channel' }
          ]
        }),
        properties_schema: '[]',
        credentials_required: '[]'
      };

      mockAdapter._setMockNode('nodes-base.slack', mockNode);

      const allOperations = repository.getNodeOperations('nodes-base.slack');
      const messageOperations = repository.getNodeOperations('nodes-base.slack', 'message');

      expect(allOperations).toHaveLength(4);
      expect(messageOperations).toEqual([
        { name: 'send', displayName: 'Send Message' },
        { name: 'update', displayName: 'Update Message' }
      ]);
    });

    it('should extract operations from properties with operation field', () => {
      const mockNode = {
        node_type: 'nodes-base.googleSheets',
        display_name: 'Google Sheets',
        operations: '[]',
        properties_schema: JSON.stringify([
          {
            name: 'resource',
            type: 'options',
            options: [{ name: 'sheet', displayName: 'Sheet' }]
          },
          {
            name: 'operation',
            type: 'options',
            displayOptions: {
              show: {
                resource: ['sheet']
              }
            },
            options: [
              { name: 'append', displayName: 'Append Row' },
              { name: 'read', displayName: 'Read Rows' }
            ]
          }
        ]),
        credentials_required: '[]'
      };

      mockAdapter._setMockNode('nodes-base.googleSheets', mockNode);

      const operations = repository.getNodeOperations('nodes-base.googleSheets');

      expect(operations).toEqual([
        { name: 'append', displayName: 'Append Row' },
        { name: 'read', displayName: 'Read Rows' }
      ]);
    });

    it('should filter operations by resource when specified', () => {
      const mockNode = {
        node_type: 'nodes-base.googleSheets',
        display_name: 'Google Sheets',
        operations: '[]',
        properties_schema: JSON.stringify([
          {
            name: 'operation',
            type: 'options',
            displayOptions: {
              show: {
                resource: ['sheet']
              }
            },
            options: [
              { name: 'append', displayName: 'Append Row' }
            ]
          },
          {
            name: 'operation',
            type: 'options',
            displayOptions: {
              show: {
                resource: ['cell']
              }
            },
            options: [
              { name: 'update', displayName: 'Update Cell' }
            ]
          }
        ]),
        credentials_required: '[]'
      };

      mockAdapter._setMockNode('nodes-base.googleSheets', mockNode);

      const sheetOperations = repository.getNodeOperations('nodes-base.googleSheets', 'sheet');
      const cellOperations = repository.getNodeOperations('nodes-base.googleSheets', 'cell');

      expect(sheetOperations).toEqual([{ name: 'append', displayName: 'Append Row' }]);
      expect(cellOperations).toEqual([{ name: 'update', displayName: 'Update Cell' }]);
    });

    it('should return empty array for non-existent node', () => {
      const operations = repository.getNodeOperations('nodes-base.nonexistent');
      expect(operations).toEqual([]);
    });

    it('should handle nodes without operations', () => {
      const mockNode = {
        node_type: 'nodes-base.simple',
        display_name: 'Simple Node',
        operations: '[]',
        properties_schema: '[]',
        credentials_required: '[]'
      };

      mockAdapter._setMockNode('nodes-base.simple', mockNode);

      const operations = repository.getNodeOperations('nodes-base.simple');
      expect(operations).toEqual([]);
    });

    it('should handle malformed operations JSON gracefully', () => {
      const mockNode = {
        node_type: 'nodes-base.broken',
        display_name: 'Broken Node',
        operations: '{invalid json}',
        properties_schema: '[]',
        credentials_required: '[]'
      };

      mockAdapter._setMockNode('nodes-base.broken', mockNode);

      const operations = repository.getNodeOperations('nodes-base.broken');
      expect(operations).toEqual([]);
    });
  });

  describe('getNodeResources', () => {
    it('should extract resources from properties', () => {
      const mockNode = {
        node_type: 'nodes-base.slack',
        display_name: 'Slack',
        operations: '[]',
        properties_schema: JSON.stringify([
          {
            name: 'resource',
            type: 'options',
            options: [
              { name: 'message', displayName: 'Message' },
              { name: 'channel', displayName: 'Channel' },
              { name: 'user', displayName: 'User' }
            ]
          }
        ]),
        credentials_required: '[]'
      };

      mockAdapter._setMockNode('nodes-base.slack', mockNode);

      const resources = repository.getNodeResources('nodes-base.slack');

      expect(resources).toEqual([
        { name: 'message', displayName: 'Message' },
        { name: 'channel', displayName: 'Channel' },
        { name: 'user', displayName: 'User' }
      ]);
    });

    it('should return empty array for node without resources', () => {
      const mockNode = {
        node_type: 'nodes-base.simple',
        display_name: 'Simple Node',
        operations: '[]',
        properties_schema: JSON.stringify([
          { name: 'url', type: 'string' }
        ]),
        credentials_required: '[]'
      };

      mockAdapter._setMockNode('nodes-base.simple', mockNode);

      const resources = repository.getNodeResources('nodes-base.simple');
      expect(resources).toEqual([]);
    });

    it('should return empty array for non-existent node', () => {
      const resources = repository.getNodeResources('nodes-base.nonexistent');
      expect(resources).toEqual([]);
    });

    it('should handle multiple resource properties', () => {
      const mockNode = {
        node_type: 'nodes-base.multi',
        display_name: 'Multi Resource Node',
        operations: '[]',
        properties_schema: JSON.stringify([
          {
            name: 'resource',
            type: 'options',
            options: [{ name: 'type1', displayName: 'Type 1' }]
          },
          {
            name: 'resource',
            type: 'options',
            options: [{ name: 'type2', displayName: 'Type 2' }]
          }
        ]),
        credentials_required: '[]'
      };

      mockAdapter._setMockNode('nodes-base.multi', mockNode);

      const resources = repository.getNodeResources('nodes-base.multi');

      expect(resources).toEqual([
        { name: 'type1', displayName: 'Type 1' },
        { name: 'type2', displayName: 'Type 2' }
      ]);
    });
  });

  describe('getOperationsForResource', () => {
    it('should return operations for specific resource', () => {
      const mockNode = {
        node_type: 'nodes-base.slack',
        display_name: 'Slack',
        operations: '[]',
        properties_schema: JSON.stringify([
          {
            name: 'operation',
            type: 'options',
            displayOptions: {
              show: {
                resource: ['message']
              }
            },
            options: [
              { name: 'send', displayName: 'Send Message' },
              { name: 'update', displayName: 'Update Message' }
            ]
          },
          {
            name: 'operation',
            type: 'options',
            displayOptions: {
              show: {
                resource: ['channel']
              }
            },
            options: [
              { name: 'create', displayName: 'Create Channel' }
            ]
          }
        ]),
        credentials_required: '[]'
      };

      mockAdapter._setMockNode('nodes-base.slack', mockNode);

      const messageOps = repository.getOperationsForResource('nodes-base.slack', 'message');
      const channelOps = repository.getOperationsForResource('nodes-base.slack', 'channel');
      const nonExistentOps = repository.getOperationsForResource('nodes-base.slack', 'nonexistent');

      expect(messageOps).toEqual([
        { name: 'send', displayName: 'Send Message' },
        { name: 'update', displayName: 'Update Message' }
      ]);
      expect(channelOps).toEqual([
        { name: 'create', displayName: 'Create Channel' }
      ]);
      expect(nonExistentOps).toEqual([]);
    });

    it('should handle array format for resource display options', () => {
      const mockNode = {
        node_type: 'nodes-base.multi',
        display_name: 'Multi Node',
        operations: '[]',
        properties_schema: JSON.stringify([
          {
            name: 'operation',
            type: 'options',
            displayOptions: {
              show: {
                resource: ['message', 'channel'] // Array format
              }
            },
            options: [
              { name: 'list', displayName: 'List Items' }
            ]
          }
        ]),
        credentials_required: '[]'
      };

      mockAdapter._setMockNode('nodes-base.multi', mockNode);

      const messageOps = repository.getOperationsForResource('nodes-base.multi', 'message');
      const channelOps = repository.getOperationsForResource('nodes-base.multi', 'channel');
      const otherOps = repository.getOperationsForResource('nodes-base.multi', 'other');

      expect(messageOps).toEqual([{ name: 'list', displayName: 'List Items' }]);
      expect(channelOps).toEqual([{ name: 'list', displayName: 'List Items' }]);
      expect(otherOps).toEqual([]);
    });

    it('should return empty array for non-existent node', () => {
      const operations = repository.getOperationsForResource('nodes-base.nonexistent', 'message');
      expect(operations).toEqual([]);
    });

    it('should handle string format for single resource', () => {
      const mockNode = {
        node_type: 'nodes-base.single',
        display_name: 'Single Node',
        operations: '[]',
        properties_schema: JSON.stringify([
          {
            name: 'operation',
            type: 'options',
            displayOptions: {
              show: {
                resource: 'document' // String format
              }
            },
            options: [
              { name: 'create', displayName: 'Create Document' }
            ]
          }
        ]),
        credentials_required: '[]'
      };

      mockAdapter._setMockNode('nodes-base.single', mockNode);

      const operations = repository.getOperationsForResource('nodes-base.single', 'document');
      expect(operations).toEqual([{ name: 'create', displayName: 'Create Document' }]);
    });
  });

  describe('getAllOperations', () => {
    it('should collect operations from all nodes', () => {
      const mockNodes = [
        {
          node_type: 'nodes-base.httpRequest',
          display_name: 'HTTP Request',
          operations: JSON.stringify([{ name: 'execute' }]),
          properties_schema: '[]',
          credentials_required: '[]'
        },
        {
          node_type: 'nodes-base.slack',
          display_name: 'Slack',
          operations: JSON.stringify([{ name: 'send' }]),
          properties_schema: '[]',
          credentials_required: '[]'
        },
        {
          node_type: 'nodes-base.empty',
          display_name: 'Empty Node',
          operations: '[]',
          properties_schema: '[]',
          credentials_required: '[]'
        }
      ];

      mockNodes.forEach(node => {
        mockAdapter._setMockNode(node.node_type, node);
      });

      const allOperations = repository.getAllOperations();

      expect(allOperations.size).toBe(2); // Only nodes with operations
      expect(allOperations.get('nodes-base.httpRequest')).toEqual([{ name: 'execute' }]);
      expect(allOperations.get('nodes-base.slack')).toEqual([{ name: 'send' }]);
      expect(allOperations.has('nodes-base.empty')).toBe(false);
    });

    it('should handle empty node list', () => {
      const allOperations = repository.getAllOperations();
      expect(allOperations.size).toBe(0);
    });
  });

  describe('getAllResources', () => {
    it('should collect resources from all nodes', () => {
      const mockNodes = [
        {
          node_type: 'nodes-base.slack',
          display_name: 'Slack',
          operations: '[]',
          properties_schema: JSON.stringify([
            {
              name: 'resource',
              options: [{ name: 'message' }, { name: 'channel' }]
            }
          ]),
          credentials_required: '[]'
        },
        {
          node_type: 'nodes-base.sheets',
          display_name: 'Google Sheets',
          operations: '[]',
          properties_schema: JSON.stringify([
            {
              name: 'resource',
              options: [{ name: 'sheet' }]
            }
          ]),
          credentials_required: '[]'
        },
        {
          node_type: 'nodes-base.simple',
          display_name: 'Simple Node',
          operations: '[]',
          properties_schema: '[]', // No resources
          credentials_required: '[]'
        }
      ];

      mockNodes.forEach(node => {
        mockAdapter._setMockNode(node.node_type, node);
      });

      const allResources = repository.getAllResources();

      expect(allResources.size).toBe(2); // Only nodes with resources
      expect(allResources.get('nodes-base.slack')).toEqual([
        { name: 'message' },
        { name: 'channel' }
      ]);
      expect(allResources.get('nodes-base.sheets')).toEqual([{ name: 'sheet' }]);
      expect(allResources.has('nodes-base.simple')).toBe(false);
    });

    it('should handle empty node list', () => {
      const allResources = repository.getAllResources();
      expect(allResources.size).toBe(0);
    });
  });

  describe('edge cases and error handling', () => {
    it('should handle null or undefined properties gracefully', () => {
      const mockNode = {
        node_type: 'nodes-base.null',
        display_name: 'Null Node',
        operations: null,
        properties_schema: null,
        credentials_required: null
      };

      mockAdapter._setMockNode('nodes-base.null', mockNode);

      const operations = repository.getNodeOperations('nodes-base.null');
      const resources = repository.getNodeResources('nodes-base.null');

      expect(operations).toEqual([]);
      expect(resources).toEqual([]);
    });

    it('should handle complex nested operation properties', () => {
      const mockNode = {
        node_type: 'nodes-base.complex',
        display_name: 'Complex Node',
        operations: '[]',
        properties_schema: JSON.stringify([
          {
            name: 'operation',
            type: 'options',
            displayOptions: {
              show: {
                resource: ['message'],
                mode: ['advanced']
              }
            },
            options: [
              { name: 'complexOperation', displayName: 'Complex Operation' }
            ]
          }
        ]),
        credentials_required: '[]'
      };

      mockAdapter._setMockNode('nodes-base.complex', mockNode);

      const operations = repository.getNodeOperations('nodes-base.complex');
      expect(operations).toEqual([{ name: 'complexOperation', displayName: 'Complex Operation' }]);
    });

    it('should handle operations with mixed data types', () => {
      const mockNode = {
        node_type: 'nodes-base.mixed',
        display_name: 'Mixed Node',
        operations: JSON.stringify({
          string_operation: 'invalid', // Should be array
          valid_operations: [{ name: 'valid' }],
          nested_object: { inner: [{ name: 'nested' }] }
        }),
        properties_schema: '[]',
        credentials_required: '[]'
      };

      mockAdapter._setMockNode('nodes-base.mixed', mockNode);

      const operations = repository.getNodeOperations('nodes-base.mixed');
      expect(operations).toEqual([{ name: 'valid' }]); // Only valid array operations
    });

    it('should handle very deeply nested properties', () => {
      const deepProperties = [
        {
          name: 'resource',
          options: [{ name: 'deep', displayName: 'Deep Resource' }],
          nested: {
            level1: {
              level2: {
                operations: [{ name: 'deep_operation' }]
              }
            }
          }
        }
      ];

      const mockNode = {
        node_type: 'nodes-base.deep',
        display_name: 'Deep Node',
        operations: '[]',
        properties_schema: JSON.stringify(deepProperties),
        credentials_required: '[]'
      };

      mockAdapter._setMockNode('nodes-base.deep', mockNode);

      const resources = repository.getNodeResources('nodes-base.deep');
      expect(resources).toEqual([{ name: 'deep', displayName: 'Deep Resource' }]);
    });
  });
});
```

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

```typescript
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { TemplateRepository } from '../../../src/templates/template-repository';
import { DatabaseAdapter, PreparedStatement, RunResult } from '../../../src/database/database-adapter';

// Mock logger
vi.mock('../../../src/utils/logger', () => ({
  logger: {
    info: vi.fn(),
    warn: vi.fn(),
    error: vi.fn(),
    debug: vi.fn()
  }
}));

// Mock template sanitizer
vi.mock('../../../src/utils/template-sanitizer', () => {
  class MockTemplateSanitizer {
    sanitizeWorkflow = vi.fn((workflow) => ({ sanitized: workflow, wasModified: false }));
    detectTokens = vi.fn(() => []);
  }
  
  return {
    TemplateSanitizer: MockTemplateSanitizer
  };
});

// Create mock database adapter
class MockDatabaseAdapter implements DatabaseAdapter {
  private statements = new Map<string, MockPreparedStatement>();
  private execCalls: string[] = [];
  private _fts5Support = true;
  
  prepare = vi.fn((sql: string) => {
    if (!this.statements.has(sql)) {
      this.statements.set(sql, new MockPreparedStatement(sql));
    }
    return this.statements.get(sql)!;
  });
  
  exec = vi.fn((sql: string) => {
    this.execCalls.push(sql);
  });
  close = vi.fn();
  pragma = vi.fn();
  transaction = vi.fn((fn: () => any) => fn());
  checkFTS5Support = vi.fn(() => this._fts5Support);
  inTransaction = false;
  
  // Test helpers
  _setFTS5Support(supported: boolean) {
    this._fts5Support = supported;
  }
  
  _getStatement(sql: string) {
    return this.statements.get(sql);
  }
  
  _getExecCalls() {
    return this.execCalls;
  }
  
  _clearExecCalls() {
    this.execCalls = [];
  }
}

class MockPreparedStatement implements PreparedStatement {
  public mockResults: any[] = [];
  public capturedParams: any[][] = [];
  
  run = vi.fn((...params: any[]): RunResult => {
    this.capturedParams.push(params);
    return { changes: 1, lastInsertRowid: 1 };
  });
  
  get = vi.fn((...params: any[]) => {
    this.capturedParams.push(params);
    return this.mockResults[0] || null;
  });
  
  all = vi.fn((...params: any[]) => {
    this.capturedParams.push(params);
    return this.mockResults;
  });
  
  iterate = vi.fn();
  pluck = vi.fn(() => this);
  expand = vi.fn(() => this);
  raw = vi.fn(() => this);
  columns = vi.fn(() => []);
  bind = vi.fn(() => this);
  
  constructor(private sql: string) {}
  
  // Test helpers
  _setMockResults(results: any[]) {
    this.mockResults = results;
  }
  
  _getCapturedParams() {
    return this.capturedParams;
  }
}

describe('TemplateRepository - Security Tests', () => {
  let repository: TemplateRepository;
  let mockAdapter: MockDatabaseAdapter;
  
  beforeEach(() => {
    vi.clearAllMocks();
    mockAdapter = new MockDatabaseAdapter();
    repository = new TemplateRepository(mockAdapter);
  });
  
  describe('SQL Injection Prevention', () => {
    describe('searchTemplatesByMetadata', () => {
      it('should prevent SQL injection in category parameter', () => {
        const maliciousCategory = "'; DROP TABLE templates; --";
        
        const stmt = new MockPreparedStatement('');
        stmt._setMockResults([]);
        mockAdapter.prepare = vi.fn().mockReturnValue(stmt);
        
        repository.searchTemplatesByMetadata({
          category: maliciousCategory}, 10, 0);
        
        // Should use parameterized queries, not inject SQL
        const capturedParams = stmt._getCapturedParams();
        expect(capturedParams.length).toBeGreaterThan(0);
        // The parameter should be the sanitized version (JSON.stringify then slice to remove quotes)
        const expectedParam = JSON.stringify(maliciousCategory).slice(1, -1);
        // capturedParams[0] is the first call's parameters array
        expect(capturedParams[0][0]).toBe(expectedParam);
        
        // Verify the SQL doesn't contain the malicious content directly
        const prepareCall = mockAdapter.prepare.mock.calls[0][0];
        expect(prepareCall).not.toContain('DROP TABLE');
        expect(prepareCall).toContain("json_extract(metadata_json, '$.categories') LIKE '%' || ? || '%'");
      });

      it('should prevent SQL injection in requiredService parameter', () => {
        const maliciousService = "'; UNION SELECT * FROM sqlite_master; --";
        
        const stmt = new MockPreparedStatement('');
        stmt._setMockResults([]);
        mockAdapter.prepare = vi.fn().mockReturnValue(stmt);
        
        repository.searchTemplatesByMetadata({
          requiredService: maliciousService}, 10, 0);
        
        const capturedParams = stmt._getCapturedParams();
        const expectedParam = JSON.stringify(maliciousService).slice(1, -1);
        // capturedParams[0] is the first call's parameters array
        expect(capturedParams[0][0]).toBe(expectedParam);
        
        const prepareCall = mockAdapter.prepare.mock.calls[0][0];
        expect(prepareCall).not.toContain('UNION SELECT');
        expect(prepareCall).toContain("json_extract(metadata_json, '$.required_services') LIKE '%' || ? || '%'");
      });

      it('should prevent SQL injection in targetAudience parameter', () => {
        const maliciousAudience = "administrators'; DELETE FROM templates WHERE '1'='1";
        
        const stmt = new MockPreparedStatement('');
        stmt._setMockResults([]);
        mockAdapter.prepare = vi.fn().mockReturnValue(stmt);
        
        repository.searchTemplatesByMetadata({
          targetAudience: maliciousAudience}, 10, 0);
        
        const capturedParams = stmt._getCapturedParams();
        const expectedParam = JSON.stringify(maliciousAudience).slice(1, -1);
        // capturedParams[0] is the first call's parameters array
        expect(capturedParams[0][0]).toBe(expectedParam);
        
        const prepareCall = mockAdapter.prepare.mock.calls[0][0];
        expect(prepareCall).not.toContain('DELETE FROM');
        expect(prepareCall).toContain("json_extract(metadata_json, '$.target_audience') LIKE '%' || ? || '%'");
      });

      it('should safely handle special characters in parameters', () => {
        const specialChars = "test'with\"quotes\\and%wildcards_and[brackets]";
        
        const stmt = new MockPreparedStatement('');
        stmt._setMockResults([]);
        mockAdapter.prepare = vi.fn().mockReturnValue(stmt);
        
        repository.searchTemplatesByMetadata({
          category: specialChars}, 10, 0);
        
        const capturedParams = stmt._getCapturedParams();
        const expectedParam = JSON.stringify(specialChars).slice(1, -1);
        // capturedParams[0] is the first call's parameters array
        expect(capturedParams[0][0]).toBe(expectedParam);
        
        // Should use parameterized query
        const prepareCall = mockAdapter.prepare.mock.calls[0][0];
        expect(prepareCall).toContain("json_extract(metadata_json, '$.categories') LIKE '%' || ? || '%'");
      });

      it('should prevent injection through numeric parameters', () => {
        const stmt = new MockPreparedStatement('');
        stmt._setMockResults([]);
        mockAdapter.prepare = vi.fn().mockReturnValue(stmt);
        
        // Try to inject through numeric parameters
        repository.searchTemplatesByMetadata({maxSetupMinutes: 999999999, // Large number
          minSetupMinutes: -999999999 // Negative number
        }, 10, 0);
        
        const capturedParams = stmt._getCapturedParams();
        // capturedParams[0] is the first call's parameters array
        expect(capturedParams[0]).toContain(999999999);
        expect(capturedParams[0]).toContain(-999999999);
        
        // Should use CAST and parameterized queries
        const prepareCall = mockAdapter.prepare.mock.calls[0][0];
        expect(prepareCall).toContain('CAST(json_extract(metadata_json, \'$.estimated_setup_minutes\') AS INTEGER)');
      });
    });

    describe('getMetadataSearchCount', () => {
      it('should use parameterized queries for count operations', () => {
        const maliciousCategory = "'; DROP TABLE templates; SELECT COUNT(*) FROM sqlite_master WHERE name LIKE '%";
        
        const stmt = new MockPreparedStatement('');
        stmt._setMockResults([{ count: 0 }]);
        mockAdapter.prepare = vi.fn().mockReturnValue(stmt);
        
        repository.getMetadataSearchCount({
          category: maliciousCategory
        });
        
        const capturedParams = stmt._getCapturedParams();
        const expectedParam = JSON.stringify(maliciousCategory).slice(1, -1);
        // capturedParams[0] is the first call's parameters array
        expect(capturedParams[0][0]).toBe(expectedParam);
        
        const prepareCall = mockAdapter.prepare.mock.calls[0][0];
        expect(prepareCall).not.toContain('DROP TABLE');
        expect(prepareCall).toContain('SELECT COUNT(*) as count FROM templates');
      });
    });

    describe('updateTemplateMetadata', () => {
      it('should safely handle metadata with special characters', () => {
        const maliciousMetadata = {
          categories: ["automation'; DROP TABLE templates; --"],
          complexity: "simple",
          use_cases: ['SQL injection"test'],
          estimated_setup_minutes: 30,
          required_services: ['api"with\\"quotes'],
          key_features: ["feature's test"],
          target_audience: ['developers\\administrators']
        };
        
        const stmt = new MockPreparedStatement('');
        mockAdapter.prepare = vi.fn().mockReturnValue(stmt);
        
        repository.updateTemplateMetadata(123, maliciousMetadata);
        
        const capturedParams = stmt._getCapturedParams();
        expect(capturedParams[0][0]).toBe(JSON.stringify(maliciousMetadata));
        expect(capturedParams[0][1]).toBe(123);
        
        // Should use parameterized UPDATE
        const prepareCall = mockAdapter.prepare.mock.calls[0][0];
        expect(prepareCall).toContain('UPDATE templates');
        expect(prepareCall).toContain('metadata_json = ?');
        expect(prepareCall).toContain('WHERE id = ?');
        expect(prepareCall).not.toContain('DROP TABLE');
      });
    });

    describe('batchUpdateMetadata', () => {
      it('should safely handle batch updates with malicious data', () => {
        const maliciousData = new Map();
        maliciousData.set(1, { categories: ["'; DROP TABLE templates; --"] });
        maliciousData.set(2, { categories: ["normal category"] });
        
        const stmt = new MockPreparedStatement('');
        mockAdapter.prepare = vi.fn().mockReturnValue(stmt);
        
        repository.batchUpdateMetadata(maliciousData);
        
        const capturedParams = stmt._getCapturedParams();
        expect(capturedParams).toHaveLength(2);
        
        // Both calls should be parameterized
        const firstJson = capturedParams[0][0];
        const secondJson = capturedParams[1][0];
        expect(firstJson).toContain("'; DROP TABLE templates; --"); // Should be JSON-encoded
        expect(capturedParams[0][1]).toBe(1);
        expect(secondJson).toContain('normal category');
        expect(capturedParams[1][1]).toBe(2);
      });
    });
  });

  describe('JSON Extraction Security', () => {
    it('should safely extract categories from JSON', () => {
      const stmt = new MockPreparedStatement('');
      stmt._setMockResults([]);
      mockAdapter.prepare = vi.fn().mockReturnValue(stmt);
      
      repository.getUniqueCategories();
      
      const prepareCall = mockAdapter.prepare.mock.calls[0][0];
      expect(prepareCall).toContain('json_each(metadata_json, \'$.categories\')');
      expect(prepareCall).not.toContain('eval(');
      expect(prepareCall).not.toContain('exec(');
    });

    it('should safely extract target audiences from JSON', () => {
      const stmt = new MockPreparedStatement('');
      stmt._setMockResults([]);
      mockAdapter.prepare = vi.fn().mockReturnValue(stmt);
      
      repository.getUniqueTargetAudiences();
      
      const prepareCall = mockAdapter.prepare.mock.calls[0][0];
      expect(prepareCall).toContain('json_each(metadata_json, \'$.target_audience\')');
      expect(prepareCall).not.toContain('eval(');
    });

    it('should safely handle complex JSON structures', () => {
      const stmt = new MockPreparedStatement('');
      stmt._setMockResults([]);
      mockAdapter.prepare = vi.fn().mockReturnValue(stmt);
      
      repository.getTemplatesByCategory('test');
      
      const prepareCall = mockAdapter.prepare.mock.calls[0][0];
      expect(prepareCall).toContain("json_extract(metadata_json, '$.categories') LIKE '%' || ? || '%'");
      
      const capturedParams = stmt._getCapturedParams();
      // Check if parameters were captured
      expect(capturedParams.length).toBeGreaterThan(0);
      // Find the parameter that contains 'test'
      const testParam = capturedParams[0].find((p: any) => typeof p === 'string' && p.includes('test'));
      expect(testParam).toBe('test');
    });
  });

  describe('Input Validation and Sanitization', () => {
    it('should handle null and undefined parameters safely', () => {
      const stmt = new MockPreparedStatement('');
      stmt._setMockResults([]);
      mockAdapter.prepare = vi.fn().mockReturnValue(stmt);
      
      repository.searchTemplatesByMetadata({
        category: undefined as any,
        complexity: null as any}, 10, 0);
      
      // Should not break and should exclude undefined/null filters
      const prepareCall = mockAdapter.prepare.mock.calls[0][0];
      expect(prepareCall).toContain('metadata_json IS NOT NULL');
      expect(prepareCall).not.toContain('undefined');
      expect(prepareCall).not.toContain('null');
    });

    it('should handle empty string parameters', () => {
      const stmt = new MockPreparedStatement('');
      stmt._setMockResults([]);
      mockAdapter.prepare = vi.fn().mockReturnValue(stmt);
      
      repository.searchTemplatesByMetadata({
        category: '',
        requiredService: '',
        targetAudience: ''}, 10, 0);
      
      // Empty strings should still be processed (might be valid searches)
      const capturedParams = stmt._getCapturedParams();
      const expectedParam = JSON.stringify("").slice(1, -1); // Results in empty string
      // Check if parameters were captured
      expect(capturedParams.length).toBeGreaterThan(0);
      // Check if empty string parameters are present
      const hasEmptyString = capturedParams[0].includes(expectedParam);
      expect(hasEmptyString).toBe(true);
    });

    it('should validate numeric ranges', () => {
      const stmt = new MockPreparedStatement('');
      stmt._setMockResults([]);
      mockAdapter.prepare = vi.fn().mockReturnValue(stmt);
      
      repository.searchTemplatesByMetadata({
        maxSetupMinutes: Number.MAX_SAFE_INTEGER,
        minSetupMinutes: Number.MIN_SAFE_INTEGER}, 10, 0);
      
      // Should handle extreme values without breaking
      const capturedParams = stmt._getCapturedParams();
      expect(capturedParams[0]).toContain(Number.MAX_SAFE_INTEGER);
      expect(capturedParams[0]).toContain(Number.MIN_SAFE_INTEGER);
    });

    it('should handle Unicode and international characters', () => {
      const unicodeCategory = '自動化'; // Japanese for "automation"
      const emojiAudience = '👩‍💻 developers';
      
      const stmt = new MockPreparedStatement('');
      stmt._setMockResults([]);
      mockAdapter.prepare = vi.fn().mockReturnValue(stmt);
      
      repository.searchTemplatesByMetadata({
        category: unicodeCategory,
        targetAudience: emojiAudience}, 10, 0);
      
      const capturedParams = stmt._getCapturedParams();
      const expectedCategoryParam = JSON.stringify(unicodeCategory).slice(1, -1);
      const expectedAudienceParam = JSON.stringify(emojiAudience).slice(1, -1);
      // capturedParams[0] is the first call's parameters array
      expect(capturedParams[0][0]).toBe(expectedCategoryParam);
      expect(capturedParams[0][1]).toBe(expectedAudienceParam);
    });
  });

  describe('Database Schema Security', () => {
    it('should use proper column names without injection', () => {
      const stmt = new MockPreparedStatement('');
      stmt._setMockResults([]);
      mockAdapter.prepare = vi.fn().mockReturnValue(stmt);
      
      repository.searchTemplatesByMetadata({
        category: 'test'}, 10, 0);
      
      const prepareCall = mockAdapter.prepare.mock.calls[0][0];
      
      // Should reference proper column names
      expect(prepareCall).toContain('metadata_json');
      expect(prepareCall).toContain('templates');
      
      // Should not contain dynamic column names that could be injected
      expect(prepareCall).not.toMatch(/SELECT \* FROM \w+;/);
      expect(prepareCall).not.toContain('information_schema');
      expect(prepareCall).not.toContain('sqlite_master');
    });

    it('should use proper JSON path syntax', () => {
      const stmt = new MockPreparedStatement('');
      stmt._setMockResults([]);
      mockAdapter.prepare = vi.fn().mockReturnValue(stmt);
      
      repository.getUniqueCategories();
      
      const prepareCall = mockAdapter.prepare.mock.calls[0][0];
      
      // Should use safe JSON path syntax
      expect(prepareCall).toContain('$.categories');
      expect(prepareCall).not.toContain('$[');
      expect(prepareCall).not.toContain('eval(');
    });
  });

  describe('Transaction Safety', () => {
    it('should handle transaction rollback on metadata update errors', () => {
      const stmt = new MockPreparedStatement('');
      stmt.run = vi.fn().mockImplementation(() => {
        throw new Error('Database error');
      });
      mockAdapter.prepare = vi.fn().mockReturnValue(stmt);
      
      const maliciousData = new Map();
      maliciousData.set(1, { categories: ["'; DROP TABLE templates; --"] });
      
      expect(() => {
        repository.batchUpdateMetadata(maliciousData);
      }).toThrow('Database error');
      
      // The error is thrown when running the statement, not during transaction setup
      // So we just verify that the error was thrown correctly
    });
  });

  describe('Error Message Security', () => {
    it('should not expose sensitive information in error messages', () => {
      const stmt = new MockPreparedStatement('');
      stmt.get = vi.fn().mockImplementation(() => {
        throw new Error('SQLITE_ERROR: syntax error near "DROP TABLE"');
      });
      mockAdapter.prepare = vi.fn().mockReturnValue(stmt);
      
      expect(() => {
        repository.getMetadataSearchCount({
          category: "'; DROP TABLE templates; --"
        });
      }).toThrow(); // Should throw, but not expose SQL details
    });
  });

  describe('Performance and DoS Protection', () => {
    it('should handle large limit values safely', () => {
      const stmt = new MockPreparedStatement('');
      stmt._setMockResults([]);
      mockAdapter.prepare = vi.fn().mockReturnValue(stmt);
      
      repository.searchTemplatesByMetadata({}, 999999999, 0); // Very large limit
      
      const capturedParams = stmt._getCapturedParams();
      // Check if parameters were captured
      expect(capturedParams.length).toBeGreaterThan(0);
      // Check if the large limit value is present (might be capped)
      const hasLargeLimit = capturedParams[0].includes(999999999) || capturedParams[0].includes(20);
      expect(hasLargeLimit).toBe(true);
      
      // Should still work but might be limited by database constraints
      expect(mockAdapter.prepare).toHaveBeenCalled();
    });

    it('should handle very long string parameters', () => {
      const veryLongString = 'a'.repeat(100000); // 100KB string
      
      const stmt = new MockPreparedStatement('');
      stmt._setMockResults([]);
      mockAdapter.prepare = vi.fn().mockReturnValue(stmt);
      
      repository.searchTemplatesByMetadata({
        category: veryLongString}, 10, 0);
      
      const capturedParams = stmt._getCapturedParams();
      expect(capturedParams[0][0]).toContain(veryLongString);
      
      // Should handle without breaking
      expect(mockAdapter.prepare).toHaveBeenCalled();
    });
  });
});
```
Page 21/46FirstPrevNextLast