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

# Directory Structure

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

# Files

--------------------------------------------------------------------------------
/scripts/test-release-automation.js:
--------------------------------------------------------------------------------

```javascript
  1 | #!/usr/bin/env node
  2 | 
  3 | /**
  4 |  * Test script for release automation
  5 |  * Validates the release workflow components locally
  6 |  */
  7 | 
  8 | const fs = require('fs');
  9 | const path = require('path');
 10 | const { execSync } = require('child_process');
 11 | 
 12 | // Color codes for output
 13 | const colors = {
 14 |   reset: '\x1b[0m',
 15 |   red: '\x1b[31m',
 16 |   green: '\x1b[32m',
 17 |   yellow: '\x1b[33m',
 18 |   blue: '\x1b[34m',
 19 |   magenta: '\x1b[35m',
 20 |   cyan: '\x1b[36m'
 21 | };
 22 | 
 23 | function log(message, color = 'reset') {
 24 |   console.log(`${colors[color]}${message}${colors.reset}`);
 25 | }
 26 | 
 27 | function header(title) {
 28 |   log(`\n${'='.repeat(60)}`, 'cyan');
 29 |   log(`🧪 ${title}`, 'cyan');
 30 |   log(`${'='.repeat(60)}`, 'cyan');
 31 | }
 32 | 
 33 | function section(title) {
 34 |   log(`\n📋 ${title}`, 'blue');
 35 |   log(`${'-'.repeat(40)}`, 'blue');
 36 | }
 37 | 
 38 | function success(message) {
 39 |   log(`✅ ${message}`, 'green');
 40 | }
 41 | 
 42 | function warning(message) {
 43 |   log(`⚠️  ${message}`, 'yellow');
 44 | }
 45 | 
 46 | function error(message) {
 47 |   log(`❌ ${message}`, 'red');
 48 | }
 49 | 
 50 | function info(message) {
 51 |   log(`ℹ️  ${message}`, 'blue');
 52 | }
 53 | 
 54 | class ReleaseAutomationTester {
 55 |   constructor() {
 56 |     this.rootDir = path.resolve(__dirname, '..');
 57 |     this.errors = [];
 58 |     this.warnings = [];
 59 |   }
 60 | 
 61 |   /**
 62 |    * Test if required files exist
 63 |    */
 64 |   testFileExistence() {
 65 |     section('Testing File Existence');
 66 |     
 67 |     const requiredFiles = [
 68 |       'package.json',
 69 |       'package.runtime.json',
 70 |       'docs/CHANGELOG.md',
 71 |       '.github/workflows/release.yml',
 72 |       'scripts/sync-runtime-version.js',
 73 |       'scripts/publish-npm.sh'
 74 |     ];
 75 | 
 76 |     for (const file of requiredFiles) {
 77 |       const filePath = path.join(this.rootDir, file);
 78 |       if (fs.existsSync(filePath)) {
 79 |         success(`Found: ${file}`);
 80 |       } else {
 81 |         error(`Missing: ${file}`);
 82 |         this.errors.push(`Missing required file: ${file}`);
 83 |       }
 84 |     }
 85 |   }
 86 | 
 87 |   /**
 88 |    * Test version detection logic
 89 |    */
 90 |   testVersionDetection() {
 91 |     section('Testing Version Detection');
 92 |     
 93 |     try {
 94 |       const packageJson = require(path.join(this.rootDir, 'package.json'));
 95 |       const runtimeJson = require(path.join(this.rootDir, 'package.runtime.json'));
 96 |       
 97 |       success(`Package.json version: ${packageJson.version}`);
 98 |       success(`Runtime package version: ${runtimeJson.version}`);
 99 |       
100 |       if (packageJson.version === runtimeJson.version) {
101 |         success('Version sync: Both versions match');
102 |       } else {
103 |         warning('Version sync: Versions do not match - run sync:runtime-version');
104 |         this.warnings.push('Package versions are not synchronized');
105 |       }
106 |       
107 |       // Test semantic version format
108 |       const semverRegex = /^\d+\.\d+\.\d+(?:-[\w\.-]+)?(?:\+[\w\.-]+)?$/;
109 |       if (semverRegex.test(packageJson.version)) {
110 |         success(`Version format: Valid semantic version (${packageJson.version})`);
111 |       } else {
112 |         error(`Version format: Invalid semantic version (${packageJson.version})`);
113 |         this.errors.push('Invalid semantic version format');
114 |       }
115 |       
116 |     } catch (err) {
117 |       error(`Version detection failed: ${err.message}`);
118 |       this.errors.push(`Version detection error: ${err.message}`);
119 |     }
120 |   }
121 | 
122 |   /**
123 |    * Test changelog parsing
124 |    */
125 |   testChangelogParsing() {
126 |     section('Testing Changelog Parsing');
127 |     
128 |     try {
129 |       const changelogPath = path.join(this.rootDir, 'docs/CHANGELOG.md');
130 |       
131 |       if (!fs.existsSync(changelogPath)) {
132 |         error('Changelog file not found');
133 |         this.errors.push('Missing changelog file');
134 |         return;
135 |       }
136 |       
137 |       const changelogContent = fs.readFileSync(changelogPath, 'utf8');
138 |       const packageJson = require(path.join(this.rootDir, 'package.json'));
139 |       const currentVersion = packageJson.version;
140 |       
141 |       // Check if current version exists in changelog
142 |       const versionRegex = new RegExp(`^## \\[${currentVersion.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\]`, 'm');
143 |       
144 |       if (versionRegex.test(changelogContent)) {
145 |         success(`Changelog entry found for version ${currentVersion}`);
146 |         
147 |         // Test extraction logic (simplified version of the GitHub Actions script)
148 |         const lines = changelogContent.split('\n');
149 |         let startIndex = -1;
150 |         let endIndex = -1;
151 |         
152 |         for (let i = 0; i < lines.length; i++) {
153 |           if (versionRegex.test(lines[i])) {
154 |             startIndex = i;
155 |             break;
156 |           }
157 |         }
158 |         
159 |         if (startIndex !== -1) {
160 |           // Find the end of this version's section
161 |           for (let i = startIndex + 1; i < lines.length; i++) {
162 |             if (lines[i].startsWith('## [') && !lines[i].includes('Unreleased')) {
163 |               endIndex = i;
164 |               break;
165 |             }
166 |           }
167 |           
168 |           if (endIndex === -1) {
169 |             endIndex = lines.length;
170 |           }
171 |           
172 |           const sectionLines = lines.slice(startIndex + 1, endIndex);
173 |           const contentLines = sectionLines.filter(line => line.trim() !== '');
174 |           
175 |           if (contentLines.length > 0) {
176 |             success(`Changelog content extracted: ${contentLines.length} lines`);
177 |             info(`Preview: ${contentLines[0].substring(0, 100)}...`);
178 |           } else {
179 |             warning('Changelog section appears to be empty');
180 |             this.warnings.push(`Empty changelog section for version ${currentVersion}`);
181 |           }
182 |         }
183 |         
184 |       } else {
185 |         warning(`No changelog entry found for current version ${currentVersion}`);
186 |         this.warnings.push(`Missing changelog entry for version ${currentVersion}`);
187 |       }
188 |       
189 |       // Check changelog format
190 |       if (changelogContent.includes('## [Unreleased]')) {
191 |         success('Changelog format: Contains Unreleased section');
192 |       } else {
193 |         warning('Changelog format: Missing Unreleased section');
194 |       }
195 |       
196 |       if (changelogContent.includes('Keep a Changelog')) {
197 |         success('Changelog format: Follows Keep a Changelog format');
198 |       } else {
199 |         warning('Changelog format: Does not reference Keep a Changelog');
200 |       }
201 |       
202 |     } catch (err) {
203 |       error(`Changelog parsing failed: ${err.message}`);
204 |       this.errors.push(`Changelog parsing error: ${err.message}`);
205 |     }
206 |   }
207 | 
208 |   /**
209 |    * Test build process
210 |    */
211 |   testBuildProcess() {
212 |     section('Testing Build Process');
213 |     
214 |     try {
215 |       // Check if dist directory exists
216 |       const distPath = path.join(this.rootDir, 'dist');
217 |       if (fs.existsSync(distPath)) {
218 |         success('Build output: dist directory exists');
219 |         
220 |         // Check for key build files
221 |         const keyFiles = [
222 |           'dist/index.js',
223 |           'dist/mcp/index.js',
224 |           'dist/mcp/server.js'
225 |         ];
226 |         
227 |         for (const file of keyFiles) {
228 |           const filePath = path.join(this.rootDir, file);
229 |           if (fs.existsSync(filePath)) {
230 |             success(`Build file: ${file} exists`);
231 |           } else {
232 |             warning(`Build file: ${file} missing - run 'npm run build'`);
233 |             this.warnings.push(`Missing build file: ${file}`);
234 |           }
235 |         }
236 |         
237 |       } else {
238 |         warning('Build output: dist directory missing - run "npm run build"');
239 |         this.warnings.push('Missing build output');
240 |       }
241 |       
242 |       // Check database
243 |       const dbPath = path.join(this.rootDir, 'data/nodes.db');
244 |       if (fs.existsSync(dbPath)) {
245 |         const stats = fs.statSync(dbPath);
246 |         success(`Database: nodes.db exists (${Math.round(stats.size / 1024 / 1024)}MB)`);
247 |       } else {
248 |         warning('Database: nodes.db missing - run "npm run rebuild"');
249 |         this.warnings.push('Missing database file');
250 |       }
251 |       
252 |     } catch (err) {
253 |       error(`Build process test failed: ${err.message}`);
254 |       this.errors.push(`Build process error: ${err.message}`);
255 |     }
256 |   }
257 | 
258 |   /**
259 |    * Test npm publish preparation
260 |    */
261 |   testNpmPublishPrep() {
262 |     section('Testing NPM Publish Preparation');
263 |     
264 |     try {
265 |       const packageJson = require(path.join(this.rootDir, 'package.json'));
266 |       const runtimeJson = require(path.join(this.rootDir, 'package.runtime.json'));
267 |       
268 |       // Check package.json fields
269 |       const requiredFields = ['name', 'version', 'description', 'main', 'bin'];
270 |       for (const field of requiredFields) {
271 |         if (packageJson[field]) {
272 |           success(`Package field: ${field} is present`);
273 |         } else {
274 |           error(`Package field: ${field} is missing`);
275 |           this.errors.push(`Missing package.json field: ${field}`);
276 |         }
277 |       }
278 |       
279 |       // Check runtime dependencies
280 |       if (runtimeJson.dependencies) {
281 |         const depCount = Object.keys(runtimeJson.dependencies).length;
282 |         success(`Runtime dependencies: ${depCount} packages`);
283 |         
284 |         // List key dependencies
285 |         const keyDeps = ['@modelcontextprotocol/sdk', 'express', 'sql.js'];
286 |         for (const dep of keyDeps) {
287 |           if (runtimeJson.dependencies[dep]) {
288 |             success(`Key dependency: ${dep} (${runtimeJson.dependencies[dep]})`);
289 |           } else {
290 |             warning(`Key dependency: ${dep} is missing`);
291 |             this.warnings.push(`Missing key dependency: ${dep}`);
292 |           }
293 |         }
294 |         
295 |       } else {
296 |         error('Runtime package has no dependencies');
297 |         this.errors.push('Missing runtime dependencies');
298 |       }
299 |       
300 |       // Check files array
301 |       if (packageJson.files && Array.isArray(packageJson.files)) {
302 |         success(`Package files: ${packageJson.files.length} patterns specified`);
303 |         info(`Files: ${packageJson.files.join(', ')}`);
304 |       } else {
305 |         warning('Package files: No files array specified');
306 |         this.warnings.push('No files array in package.json');
307 |       }
308 |       
309 |     } catch (err) {
310 |       error(`NPM publish prep test failed: ${err.message}`);
311 |       this.errors.push(`NPM publish prep error: ${err.message}`);
312 |     }
313 |   }
314 | 
315 |   /**
316 |    * Test Docker configuration
317 |    */
318 |   testDockerConfig() {
319 |     section('Testing Docker Configuration');
320 |     
321 |     try {
322 |       const dockerfiles = ['Dockerfile', 'Dockerfile.railway'];
323 |       
324 |       for (const dockerfile of dockerfiles) {
325 |         const dockerfilePath = path.join(this.rootDir, dockerfile);
326 |         if (fs.existsSync(dockerfilePath)) {
327 |           success(`Dockerfile: ${dockerfile} exists`);
328 |           
329 |           const content = fs.readFileSync(dockerfilePath, 'utf8');
330 |           
331 |           // Check for key instructions
332 |           if (content.includes('FROM node:')) {
333 |             success(`${dockerfile}: Uses Node.js base image`);
334 |           } else {
335 |             warning(`${dockerfile}: Does not use standard Node.js base image`);
336 |           }
337 |           
338 |           if (content.includes('COPY dist')) {
339 |             success(`${dockerfile}: Copies build output`);
340 |           } else {
341 |             warning(`${dockerfile}: May not copy build output correctly`);
342 |           }
343 |           
344 |         } else {
345 |           warning(`Dockerfile: ${dockerfile} not found`);
346 |           this.warnings.push(`Missing Dockerfile: ${dockerfile}`);
347 |         }
348 |       }
349 |       
350 |       // Check docker-compose files
351 |       const composeFiles = ['docker-compose.yml', 'docker-compose.n8n.yml'];
352 |       for (const composeFile of composeFiles) {
353 |         const composePath = path.join(this.rootDir, composeFile);
354 |         if (fs.existsSync(composePath)) {
355 |           success(`Docker Compose: ${composeFile} exists`);
356 |         } else {
357 |           info(`Docker Compose: ${composeFile} not found (optional)`);
358 |         }
359 |       }
360 |       
361 |     } catch (err) {
362 |       error(`Docker config test failed: ${err.message}`);
363 |       this.errors.push(`Docker config error: ${err.message}`);
364 |     }
365 |   }
366 | 
367 |   /**
368 |    * Test workflow file syntax
369 |    */
370 |   testWorkflowSyntax() {
371 |     section('Testing Workflow Syntax');
372 |     
373 |     try {
374 |       const workflowPath = path.join(this.rootDir, '.github/workflows/release.yml');
375 |       
376 |       if (!fs.existsSync(workflowPath)) {
377 |         error('Release workflow file not found');
378 |         this.errors.push('Missing release workflow file');
379 |         return;
380 |       }
381 |       
382 |       const workflowContent = fs.readFileSync(workflowPath, 'utf8');
383 |       
384 |       // Basic YAML structure checks
385 |       if (workflowContent.includes('name: Automated Release')) {
386 |         success('Workflow: Has correct name');
387 |       } else {
388 |         warning('Workflow: Name may be incorrect');
389 |       }
390 |       
391 |       if (workflowContent.includes('on:') && workflowContent.includes('push:')) {
392 |         success('Workflow: Has push trigger');
393 |       } else {
394 |         error('Workflow: Missing push trigger');
395 |         this.errors.push('Workflow missing push trigger');
396 |       }
397 |       
398 |       if (workflowContent.includes('branches: [main]')) {
399 |         success('Workflow: Configured for main branch');
400 |       } else {
401 |         warning('Workflow: May not be configured for main branch');
402 |       }
403 |       
404 |       // Check for required jobs
405 |       const requiredJobs = [
406 |         'detect-version-change',
407 |         'extract-changelog',
408 |         'create-release',
409 |         'publish-npm',
410 |         'build-docker'
411 |       ];
412 |       
413 |       for (const job of requiredJobs) {
414 |         if (workflowContent.includes(`${job}:`)) {
415 |           success(`Workflow job: ${job} defined`);
416 |         } else {
417 |           error(`Workflow job: ${job} missing`);
418 |           this.errors.push(`Missing workflow job: ${job}`);
419 |         }
420 |       }
421 |       
422 |       // Check for secrets usage
423 |       if (workflowContent.includes('${{ secrets.NPM_TOKEN }}')) {
424 |         success('Workflow: NPM_TOKEN secret configured');
425 |       } else {
426 |         warning('Workflow: NPM_TOKEN secret may be missing');
427 |         this.warnings.push('NPM_TOKEN secret may need to be configured');
428 |       }
429 |       
430 |       if (workflowContent.includes('${{ secrets.GITHUB_TOKEN }}')) {
431 |         success('Workflow: GITHUB_TOKEN secret configured');
432 |       } else {
433 |         warning('Workflow: GITHUB_TOKEN secret may be missing');
434 |       }
435 |       
436 |     } catch (err) {
437 |       error(`Workflow syntax test failed: ${err.message}`);
438 |       this.errors.push(`Workflow syntax error: ${err.message}`);
439 |     }
440 |   }
441 | 
442 |   /**
443 |    * Test environment and dependencies
444 |    */
445 |   testEnvironment() {
446 |     section('Testing Environment');
447 |     
448 |     try {
449 |       // Check Node.js version
450 |       const nodeVersion = process.version;
451 |       success(`Node.js version: ${nodeVersion}`);
452 |       
453 |       // Check if npm is available
454 |       try {
455 |         const npmVersion = execSync('npm --version', { encoding: 'utf8', stdio: 'pipe' }).trim();
456 |         success(`NPM version: ${npmVersion}`);
457 |       } catch (err) {
458 |         error('NPM not available');
459 |         this.errors.push('NPM not available');
460 |       }
461 |       
462 |       // Check if git is available
463 |       try {
464 |         const gitVersion = execSync('git --version', { encoding: 'utf8', stdio: 'pipe' }).trim();
465 |         success(`Git available: ${gitVersion}`);
466 |       } catch (err) {
467 |         error('Git not available');
468 |         this.errors.push('Git not available');
469 |       }
470 |       
471 |       // Check if we're in a git repository
472 |       try {
473 |         execSync('git rev-parse --git-dir', { stdio: 'pipe' });
474 |         success('Git repository: Detected');
475 |         
476 |         // Check current branch
477 |         try {
478 |           const branch = execSync('git branch --show-current', { encoding: 'utf8', stdio: 'pipe' }).trim();
479 |           info(`Current branch: ${branch}`);
480 |         } catch (err) {
481 |           info('Could not determine current branch');
482 |         }
483 |         
484 |       } catch (err) {
485 |         warning('Not in a git repository');
486 |         this.warnings.push('Not in a git repository');
487 |       }
488 |       
489 |     } catch (err) {
490 |       error(`Environment test failed: ${err.message}`);
491 |       this.errors.push(`Environment error: ${err.message}`);
492 |     }
493 |   }
494 | 
495 |   /**
496 |    * Run all tests
497 |    */
498 |   async runAllTests() {
499 |     header('Release Automation Test Suite');
500 |     
501 |     info('Testing release automation components...');
502 |     
503 |     this.testFileExistence();
504 |     this.testVersionDetection();
505 |     this.testChangelogParsing();
506 |     this.testBuildProcess();
507 |     this.testNpmPublishPrep();
508 |     this.testDockerConfig();
509 |     this.testWorkflowSyntax();
510 |     this.testEnvironment();
511 |     
512 |     // Summary
513 |     header('Test Summary');
514 |     
515 |     if (this.errors.length === 0 && this.warnings.length === 0) {
516 |       log('🎉 All tests passed! Release automation is ready.', 'green');
517 |     } else {
518 |       if (this.errors.length > 0) {
519 |         log(`\n❌ ${this.errors.length} Error(s):`, 'red');
520 |         this.errors.forEach(err => log(`   • ${err}`, 'red'));
521 |       }
522 |       
523 |       if (this.warnings.length > 0) {
524 |         log(`\n⚠️  ${this.warnings.length} Warning(s):`, 'yellow');
525 |         this.warnings.forEach(warn => log(`   • ${warn}`, 'yellow'));
526 |       }
527 |       
528 |       if (this.errors.length > 0) {
529 |         log('\n🔧 Please fix the errors before running the release workflow.', 'red');
530 |         process.exit(1);
531 |       } else {
532 |         log('\n✅ No critical errors found. Warnings should be reviewed but won\'t prevent releases.', 'yellow');
533 |       }
534 |     }
535 |     
536 |     // Next steps
537 |     log('\n📋 Next Steps:', 'cyan');
538 |     log('1. Ensure all secrets are configured in GitHub repository settings:', 'cyan');
539 |     log('   • NPM_TOKEN (required for npm publishing)', 'cyan');
540 |     log('   • GITHUB_TOKEN (automatically available)', 'cyan');
541 |     log('\n2. To trigger a release:', 'cyan');
542 |     log('   • Update version in package.json', 'cyan');
543 |     log('   • Update changelog in docs/CHANGELOG.md', 'cyan');
544 |     log('   • Commit and push to main branch', 'cyan');
545 |     log('\n3. Monitor the release workflow in GitHub Actions', 'cyan');
546 |     
547 |     return this.errors.length === 0;
548 |   }
549 | }
550 | 
551 | // Run the tests
552 | if (require.main === module) {
553 |   const tester = new ReleaseAutomationTester();
554 |   tester.runAllTests().catch(err => {
555 |     console.error('Test suite failed:', err);
556 |     process.exit(1);
557 |   });
558 | }
559 | 
560 | module.exports = ReleaseAutomationTester;
```

--------------------------------------------------------------------------------
/tests/integration/mcp-protocol/error-handling.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, it, expect, beforeEach, afterEach } from 'vitest';
  2 | import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js';
  3 | import { Client } from '@modelcontextprotocol/sdk/client/index.js';
  4 | import { TestableN8NMCPServer } from './test-helpers';
  5 | 
  6 | describe('MCP Error Handling', () => {
  7 |   let mcpServer: TestableN8NMCPServer;
  8 |   let client: Client;
  9 | 
 10 |   beforeEach(async () => {
 11 |     mcpServer = new TestableN8NMCPServer();
 12 |     await mcpServer.initialize();
 13 |     
 14 |     const [serverTransport, clientTransport] = InMemoryTransport.createLinkedPair();
 15 |     await mcpServer.connectToTransport(serverTransport);
 16 |     
 17 |     client = new Client({
 18 |       name: 'test-client',
 19 |       version: '1.0.0'
 20 |     }, {
 21 |       capabilities: {}
 22 |     });
 23 |     
 24 |     await client.connect(clientTransport);
 25 |   });
 26 | 
 27 |   afterEach(async () => {
 28 |     await client.close();
 29 |     await mcpServer.close();
 30 |   });
 31 | 
 32 |   describe('JSON-RPC Error Codes', () => {
 33 |     it('should handle invalid request (parse error)', async () => {
 34 |       // The MCP SDK handles parsing, so we test with invalid method instead
 35 |       try {
 36 |         await (client as any).request({
 37 |           method: '',  // Empty method
 38 |           params: {}
 39 |         });
 40 |         expect.fail('Should have thrown an error');
 41 |       } catch (error: any) {
 42 |         expect(error).toBeDefined();
 43 |       }
 44 |     });
 45 | 
 46 |     it('should handle method not found', async () => {
 47 |       try {
 48 |         await (client as any).request({
 49 |           method: 'nonexistent/method',
 50 |           params: {}
 51 |         });
 52 |         expect.fail('Should have thrown an error');
 53 |       } catch (error: any) {
 54 |         expect(error).toBeDefined();
 55 |         expect(error.message).toContain('not found');
 56 |       }
 57 |     });
 58 | 
 59 |     it('should handle invalid params', async () => {
 60 |       try {
 61 |         // Missing required parameter
 62 |         await client.callTool({ name: 'get_node_info', arguments: {} });
 63 |         expect.fail('Should have thrown an error');
 64 |       } catch (error: any) {
 65 |         expect(error).toBeDefined();
 66 |         // The error now properly validates required parameters
 67 |         expect(error.message).toContain("Missing required parameters");
 68 |       }
 69 |     });
 70 | 
 71 |     it('should handle internal errors gracefully', async () => {
 72 |       try {
 73 |         // Invalid node type format should cause internal processing error
 74 |         await client.callTool({ name: 'get_node_info', arguments: {
 75 |           nodeType: 'completely-invalid-format-$$$$'
 76 |         } });
 77 |         expect.fail('Should have thrown an error');
 78 |       } catch (error: any) {
 79 |         expect(error).toBeDefined();
 80 |         expect(error.message).toContain('not found');
 81 |       }
 82 |     });
 83 |   });
 84 | 
 85 |   describe('Tool-Specific Errors', () => {
 86 |     describe('Node Discovery Errors', () => {
 87 |       it('should handle invalid category filter', async () => {
 88 |         const response = await client.callTool({ name: 'list_nodes', arguments: {
 89 |           category: 'invalid_category'
 90 |         } });
 91 | 
 92 |         // Should return empty array, not error
 93 |         const result = JSON.parse((response as any).content[0].text);
 94 |         expect(result).toHaveProperty('nodes');
 95 |         expect(Array.isArray(result.nodes)).toBe(true);
 96 |         expect(result.nodes).toHaveLength(0);
 97 |       });
 98 | 
 99 |       it('should handle invalid search mode', async () => {
100 |         try {
101 |           await client.callTool({ name: 'search_nodes', arguments: {
102 |             query: 'test',
103 |             mode: 'INVALID_MODE' as any
104 |           } });
105 |           expect.fail('Should have thrown an error');
106 |         } catch (error: any) {
107 |           expect(error).toBeDefined();
108 |         }
109 |       });
110 | 
111 |       it('should handle empty search query', async () => {
112 |         try {
113 |           await client.callTool({ name: 'search_nodes', arguments: {
114 |             query: ''
115 |           } });
116 |           expect.fail('Should have thrown an error');
117 |         } catch (error: any) {
118 |           expect(error).toBeDefined();
119 |           expect(error.message).toContain("search_nodes: Validation failed:");
120 |           expect(error.message).toContain("query: query cannot be empty");
121 |         }
122 |       });
123 | 
124 |       it('should handle non-existent node types', async () => {
125 |         try {
126 |           await client.callTool({ name: 'get_node_info', arguments: {
127 |             nodeType: 'nodes-base.thisDoesNotExist'
128 |           } });
129 |           expect.fail('Should have thrown an error');
130 |         } catch (error: any) {
131 |           expect(error).toBeDefined();
132 |           expect(error.message).toContain('not found');
133 |         }
134 |       });
135 |     });
136 | 
137 |     describe('Validation Errors', () => {
138 |       it('should handle invalid validation profile', async () => {
139 |         try {
140 |           await client.callTool({ name: 'validate_node_operation', arguments: {
141 |             nodeType: 'nodes-base.httpRequest',
142 |             config: { method: 'GET', url: 'https://api.example.com' },
143 |             profile: 'invalid_profile' as any
144 |           } });
145 |           expect.fail('Should have thrown an error');
146 |         } catch (error: any) {
147 |           expect(error).toBeDefined();
148 |         }
149 |       });
150 | 
151 |       it('should handle malformed workflow structure', async () => {
152 |         try {
153 |           await client.callTool({ name: 'validate_workflow', arguments: {
154 |             workflow: {
155 |               // Missing required 'nodes' array
156 |               connections: {}
157 |             }
158 |           } });
159 |           expect.fail('Should have thrown an error');
160 |         } catch (error: any) {
161 |           expect(error).toBeDefined();
162 |           expect(error.message).toContain("validate_workflow: Validation failed:");
163 |           expect(error.message).toContain("workflow.nodes: workflow.nodes is required");
164 |         }
165 |       });
166 | 
167 |       it('should handle circular workflow references', async () => {
168 |         const workflow = {
169 |           nodes: [
170 |             {
171 |               id: '1',
172 |               name: 'Node1',
173 |               type: 'nodes-base.noOp',
174 |               typeVersion: 1,
175 |               position: [0, 0],
176 |               parameters: {}
177 |             },
178 |             {
179 |               id: '2',
180 |               name: 'Node2',
181 |               type: 'nodes-base.noOp',
182 |               typeVersion: 1,
183 |               position: [250, 0],
184 |               parameters: {}
185 |             }
186 |           ],
187 |           connections: {
188 |             'Node1': {
189 |               'main': [[{ node: 'Node2', type: 'main', index: 0 }]]
190 |             },
191 |             'Node2': {
192 |               'main': [[{ node: 'Node1', type: 'main', index: 0 }]]
193 |             }
194 |           }
195 |         };
196 | 
197 |         const response = await client.callTool({ name: 'validate_workflow', arguments: {
198 |           workflow
199 |         } });
200 | 
201 |         const validation = JSON.parse((response as any).content[0].text);
202 |         expect(validation.warnings).toBeDefined();
203 |       });
204 |     });
205 | 
206 |     describe('Documentation Errors', () => {
207 |       it('should handle non-existent documentation topics', async () => {
208 |         const response = await client.callTool({ name: 'tools_documentation', arguments: {
209 |           topic: 'completely_fake_tool'
210 |         } });
211 | 
212 |         expect((response as any).content[0].text).toContain('not found');
213 |       });
214 | 
215 |       it('should handle invalid depth parameter', async () => {
216 |         try {
217 |           await client.callTool({ name: 'tools_documentation', arguments: {
218 |             depth: 'invalid_depth' as any
219 |           } });
220 |           expect.fail('Should have thrown an error');
221 |         } catch (error: any) {
222 |           expect(error).toBeDefined();
223 |         }
224 |       });
225 |     });
226 |   });
227 | 
228 |   describe('Large Payload Handling', () => {
229 |     it('should handle large node info requests', async () => {
230 |       // HTTP Request node has extensive properties
231 |       const response = await client.callTool({ name: 'get_node_info', arguments: {
232 |         nodeType: 'nodes-base.httpRequest'
233 |       } });
234 | 
235 |       expect((response as any).content[0].text.length).toBeGreaterThan(10000);
236 |       
237 |       // Should be valid JSON
238 |       const nodeInfo = JSON.parse((response as any).content[0].text);
239 |       expect(nodeInfo).toHaveProperty('properties');
240 |     });
241 | 
242 |     it('should handle large workflow validation', async () => {
243 |       // Create a large workflow
244 |       const nodes = [];
245 |       const connections: any = {};
246 | 
247 |       for (let i = 0; i < 50; i++) {
248 |         const nodeName = `Node${i}`;
249 |         nodes.push({
250 |           id: String(i),
251 |           name: nodeName,
252 |           type: 'nodes-base.noOp',
253 |           typeVersion: 1,
254 |           position: [i * 100, 0],
255 |           parameters: {}
256 |         });
257 | 
258 |         if (i > 0) {
259 |           const prevNode = `Node${i - 1}`;
260 |           connections[prevNode] = {
261 |             'main': [[{ node: nodeName, type: 'main', index: 0 }]]
262 |           };
263 |         }
264 |       }
265 | 
266 |       const response = await client.callTool({ name: 'validate_workflow', arguments: {
267 |         workflow: { nodes, connections }
268 |       } });
269 | 
270 |       const validation = JSON.parse((response as any).content[0].text);
271 |       expect(validation).toHaveProperty('valid');
272 |     });
273 | 
274 |     it('should handle many concurrent requests', async () => {
275 |       const requestCount = 50;
276 |       const promises = [];
277 | 
278 |       for (let i = 0; i < requestCount; i++) {
279 |         promises.push(
280 |           client.callTool({ name: 'list_nodes', arguments: {
281 |             limit: 1,
282 |             category: i % 2 === 0 ? 'trigger' : 'transform'
283 |           } })
284 |         );
285 |       }
286 | 
287 |       const responses = await Promise.all(promises);
288 |       expect(responses).toHaveLength(requestCount);
289 |     });
290 |   });
291 | 
292 |   describe('Invalid JSON Handling', () => {
293 |     it('should handle invalid JSON in tool parameters', async () => {
294 |       try {
295 |         // Config should be an object, not a string
296 |         await client.callTool({ name: 'validate_node_operation', arguments: {
297 |           nodeType: 'nodes-base.httpRequest',
298 |           config: 'invalid json string' as any
299 |         } });
300 |         expect.fail('Should have thrown an error');
301 |       } catch (error: any) {
302 |         expect(error).toBeDefined();
303 |       }
304 |     });
305 | 
306 |     it('should handle malformed workflow JSON', async () => {
307 |       try {
308 |         await client.callTool({ name: 'validate_workflow', arguments: {
309 |           workflow: 'not a valid workflow object' as any
310 |         } });
311 |         expect.fail('Should have thrown an error');
312 |       } catch (error: any) {
313 |         expect(error).toBeDefined();
314 |       }
315 |     });
316 |   });
317 | 
318 |   describe('Timeout Scenarios', () => {
319 |     it('should handle rapid sequential requests', async () => {
320 |       const start = Date.now();
321 |       
322 |       for (let i = 0; i < 20; i++) {
323 |         await client.callTool({ name: 'get_database_statistics', arguments: {} });
324 |       }
325 | 
326 |       const duration = Date.now() - start;
327 |       
328 |       // Should complete reasonably quickly (under 5 seconds)
329 |       expect(duration).toBeLessThan(5000);
330 |     });
331 | 
332 |     it('should handle long-running operations', async () => {
333 |       // Search with complex query that requires more processing
334 |       const response = await client.callTool({ name: 'search_nodes', arguments: {
335 |         query: 'a b c d e f g h i j k l m n o p q r s t u v w x y z',
336 |         mode: 'AND'
337 |       } });
338 | 
339 |       expect(response).toBeDefined();
340 |     });
341 |   });
342 | 
343 |   describe('Memory Pressure', () => {
344 |     it('should handle multiple large responses', async () => {
345 |       const promises = [];
346 | 
347 |       // Request multiple large node infos
348 |       const largeNodes = [
349 |         'nodes-base.httpRequest',
350 |         'nodes-base.postgres',
351 |         'nodes-base.googleSheets',
352 |         'nodes-base.slack',
353 |         'nodes-base.gmail'
354 |       ];
355 | 
356 |       for (const nodeType of largeNodes) {
357 |         promises.push(
358 |           client.callTool({ name: 'get_node_info', arguments: { nodeType } })
359 |             .catch(() => null) // Some might not exist
360 |         );
361 |       }
362 | 
363 |       const responses = await Promise.all(promises);
364 |       const validResponses = responses.filter(r => r !== null);
365 |       
366 |       expect(validResponses.length).toBeGreaterThan(0);
367 |     });
368 | 
369 |     it('should handle workflow with many nodes', async () => {
370 |       const nodeCount = 100;
371 |       const nodes = [];
372 | 
373 |       for (let i = 0; i < nodeCount; i++) {
374 |         nodes.push({
375 |           id: String(i),
376 |           name: `Node${i}`,
377 |           type: 'nodes-base.noOp',
378 |           typeVersion: 1,
379 |           position: [i * 50, Math.floor(i / 10) * 100],
380 |           parameters: {
381 |             // Add some data to increase memory usage
382 |             data: `This is some test data for node ${i}`.repeat(10)
383 |           }
384 |         });
385 |       }
386 | 
387 |       const response = await client.callTool({ name: 'validate_workflow', arguments: {
388 |         workflow: {
389 |           nodes,
390 |           connections: {}
391 |         }
392 |       } });
393 | 
394 |       const validation = JSON.parse((response as any).content[0].text);
395 |       expect(validation).toHaveProperty('valid');
396 |     });
397 |   });
398 | 
399 |   describe('Error Recovery', () => {
400 |     it('should continue working after errors', async () => {
401 |       // Cause an error
402 |       try {
403 |         await client.callTool({ name: 'get_node_info', arguments: {
404 |           nodeType: 'invalid'
405 |         } });
406 |       } catch (error) {
407 |         // Expected
408 |       }
409 | 
410 |       // Should still work
411 |       const response = await client.callTool({ name: 'list_nodes', arguments: { limit: 1 } });
412 |       expect(response).toBeDefined();
413 |     });
414 | 
415 |     it('should handle mixed success and failure', async () => {
416 |       const promises = [
417 |         client.callTool({ name: 'list_nodes', arguments: { limit: 5 } }),
418 |         client.callTool({ name: 'get_node_info', arguments: { nodeType: 'invalid' } }).catch(e => ({ error: e })),
419 |         client.callTool({ name: 'get_database_statistics', arguments: {} }),
420 |         client.callTool({ name: 'search_nodes', arguments: { query: '' } }).catch(e => ({ error: e })),
421 |         client.callTool({ name: 'list_ai_tools', arguments: {} })
422 |       ];
423 | 
424 |       const results = await Promise.all(promises);
425 |       
426 |       // Some should succeed, some should fail
427 |       const successes = results.filter(r => !('error' in r));
428 |       const failures = results.filter(r => 'error' in r);
429 |       
430 |       expect(successes.length).toBeGreaterThan(0);
431 |       expect(failures.length).toBeGreaterThan(0);
432 |     });
433 |   });
434 | 
435 |   describe('Edge Cases', () => {
436 |     it('should handle empty responses gracefully', async () => {
437 |       const response = await client.callTool({ name: 'list_nodes', arguments: {
438 |         category: 'nonexistent_category'
439 |       } });
440 | 
441 |       const result = JSON.parse((response as any).content[0].text);
442 |       expect(result).toHaveProperty('nodes');
443 |       expect(Array.isArray(result.nodes)).toBe(true);
444 |       expect(result.nodes).toHaveLength(0);
445 |     });
446 | 
447 |     it('should handle special characters in parameters', async () => {
448 |       const response = await client.callTool({ name: 'search_nodes', arguments: {
449 |         query: 'test!@#$%^&*()_+-=[]{}|;\':",./<>?'
450 |       } });
451 | 
452 |       // Should return results or empty array, not error
453 |       const result = JSON.parse((response as any).content[0].text);
454 |       expect(result).toHaveProperty('results');
455 |       expect(Array.isArray(result.results)).toBe(true);
456 |     });
457 | 
458 |     it('should handle unicode in parameters', async () => {
459 |       const response = await client.callTool({ name: 'search_nodes', arguments: {
460 |         query: 'test 测试 тест परीक्षण'
461 |       } });
462 | 
463 |       const result = JSON.parse((response as any).content[0].text);
464 |       expect(result).toHaveProperty('results');
465 |       expect(Array.isArray(result.results)).toBe(true);
466 |     });
467 | 
468 |     it('should handle null and undefined gracefully', async () => {
469 |       // Most tools should handle missing optional params
470 |       const response = await client.callTool({ name: 'list_nodes', arguments: {
471 |         limit: undefined as any,
472 |         category: null as any
473 |       } });
474 | 
475 |       const result = JSON.parse((response as any).content[0].text);
476 |       expect(result).toHaveProperty('nodes');
477 |       expect(Array.isArray(result.nodes)).toBe(true);
478 |     });
479 |   });
480 | 
481 |   describe('Error Message Quality', () => {
482 |     it('should provide helpful error messages', async () => {
483 |       try {
484 |         // Use a truly invalid node type
485 |         await client.callTool({ name: 'get_node_info', arguments: {
486 |           nodeType: 'invalid-node-type-that-does-not-exist'
487 |         } });
488 |         expect.fail('Should have thrown an error');
489 |       } catch (error: any) {
490 |         expect(error.message).toBeDefined();
491 |         expect(error.message.length).toBeGreaterThan(10);
492 |         // Should mention the issue
493 |         expect(error.message.toLowerCase()).toMatch(/not found|invalid|missing/);
494 |       }
495 |     });
496 | 
497 |     it('should indicate missing required parameters', async () => {
498 |       try {
499 |         await client.callTool({ name: 'search_nodes', arguments: {} });
500 |         expect.fail('Should have thrown an error');
501 |       } catch (error: any) {
502 |         expect(error).toBeDefined();
503 |         // The error now properly validates required parameters
504 |         expect(error.message).toContain("search_nodes: Validation failed:");
505 |         expect(error.message).toContain("query: query is required");
506 |       }
507 |     });
508 | 
509 |     it('should provide context for validation errors', async () => {
510 |       const response = await client.callTool({ name: 'validate_node_operation', arguments: {
511 |         nodeType: 'nodes-base.httpRequest',
512 |         config: {
513 |           // Missing required fields
514 |           method: 'INVALID_METHOD'
515 |         }
516 |       } });
517 | 
518 |       const validation = JSON.parse((response as any).content[0].text);
519 |       expect(validation.valid).toBe(false);
520 |       expect(validation.errors).toBeDefined();
521 |       expect(Array.isArray(validation.errors)).toBe(true);
522 |       expect(validation.errors.length).toBeGreaterThan(0);
523 |       if (validation.errors.length > 0) {
524 |         expect(validation.errors[0].message).toBeDefined();
525 |         // Field property might not exist on all error types
526 |         if (validation.errors[0].field !== undefined) {
527 |           expect(validation.errors[0].field).toBeDefined();
528 |         }
529 |       }
530 |     });
531 |   });
532 | });
```

--------------------------------------------------------------------------------
/src/scripts/fetch-templates.ts:
--------------------------------------------------------------------------------

```typescript
  1 | #!/usr/bin/env node
  2 | import { createDatabaseAdapter } from '../database/database-adapter';
  3 | import { TemplateService } from '../templates/template-service';
  4 | import * as fs from 'fs';
  5 | import * as path from 'path';
  6 | import * as zlib from 'zlib';
  7 | import * as dotenv from 'dotenv';
  8 | import type { MetadataRequest } from '../templates/metadata-generator';
  9 | 
 10 | // Load environment variables
 11 | dotenv.config();
 12 | 
 13 | /**
 14 |  * Extract node configurations from a template workflow
 15 |  */
 16 | function extractNodeConfigs(
 17 |   templateId: number,
 18 |   templateName: string,
 19 |   templateViews: number,
 20 |   workflowCompressed: string,
 21 |   metadata: any
 22 | ): Array<{
 23 |   node_type: string;
 24 |   template_id: number;
 25 |   template_name: string;
 26 |   template_views: number;
 27 |   node_name: string;
 28 |   parameters_json: string;
 29 |   credentials_json: string | null;
 30 |   has_credentials: number;
 31 |   has_expressions: number;
 32 |   complexity: string;
 33 |   use_cases: string;
 34 | }> {
 35 |   try {
 36 |     // Decompress workflow
 37 |     const decompressed = zlib.gunzipSync(Buffer.from(workflowCompressed, 'base64'));
 38 |     const workflow = JSON.parse(decompressed.toString('utf-8'));
 39 | 
 40 |     const configs: any[] = [];
 41 | 
 42 |     for (const node of workflow.nodes || []) {
 43 |       // Skip UI-only nodes (sticky notes, etc.)
 44 |       if (node.type.includes('stickyNote') || !node.parameters) {
 45 |         continue;
 46 |       }
 47 | 
 48 |       configs.push({
 49 |         node_type: node.type,
 50 |         template_id: templateId,
 51 |         template_name: templateName,
 52 |         template_views: templateViews,
 53 |         node_name: node.name,
 54 |         parameters_json: JSON.stringify(node.parameters),
 55 |         credentials_json: node.credentials ? JSON.stringify(node.credentials) : null,
 56 |         has_credentials: node.credentials ? 1 : 0,
 57 |         has_expressions: detectExpressions(node.parameters) ? 1 : 0,
 58 |         complexity: metadata?.complexity || 'medium',
 59 |         use_cases: JSON.stringify(metadata?.use_cases || [])
 60 |       });
 61 |     }
 62 | 
 63 |     return configs;
 64 |   } catch (error) {
 65 |     console.error(`Error extracting configs from template ${templateId}:`, error);
 66 |     return [];
 67 |   }
 68 | }
 69 | 
 70 | /**
 71 |  * Detect n8n expressions in parameters
 72 |  */
 73 | function detectExpressions(params: any): boolean {
 74 |   if (!params) return false;
 75 |   const json = JSON.stringify(params);
 76 |   return json.includes('={{') || json.includes('$json') || json.includes('$node');
 77 | }
 78 | 
 79 | /**
 80 |  * Insert extracted configs into database and rank them
 81 |  */
 82 | function insertAndRankConfigs(db: any, configs: any[]) {
 83 |   if (configs.length === 0) {
 84 |     console.log('No configs to insert');
 85 |     return;
 86 |   }
 87 | 
 88 |   // Clear old configs for these templates
 89 |   const templateIds = [...new Set(configs.map(c => c.template_id))];
 90 |   const placeholders = templateIds.map(() => '?').join(',');
 91 |   db.prepare(`DELETE FROM template_node_configs WHERE template_id IN (${placeholders})`).run(...templateIds);
 92 | 
 93 |   // Insert new configs
 94 |   const insertStmt = db.prepare(`
 95 |     INSERT INTO template_node_configs (
 96 |       node_type, template_id, template_name, template_views,
 97 |       node_name, parameters_json, credentials_json,
 98 |       has_credentials, has_expressions, complexity, use_cases
 99 |     ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
100 |   `);
101 | 
102 |   for (const config of configs) {
103 |     insertStmt.run(
104 |       config.node_type,
105 |       config.template_id,
106 |       config.template_name,
107 |       config.template_views,
108 |       config.node_name,
109 |       config.parameters_json,
110 |       config.credentials_json,
111 |       config.has_credentials,
112 |       config.has_expressions,
113 |       config.complexity,
114 |       config.use_cases
115 |     );
116 |   }
117 | 
118 |   // Rank configs per node_type by template popularity
119 |   db.exec(`
120 |     UPDATE template_node_configs
121 |     SET rank = (
122 |       SELECT COUNT(*) + 1
123 |       FROM template_node_configs AS t2
124 |       WHERE t2.node_type = template_node_configs.node_type
125 |         AND t2.template_views > template_node_configs.template_views
126 |     )
127 |   `);
128 | 
129 |   // Keep only top 10 per node_type
130 |   db.exec(`
131 |     DELETE FROM template_node_configs
132 |     WHERE id NOT IN (
133 |       SELECT id FROM template_node_configs
134 |       WHERE rank <= 10
135 |       ORDER BY node_type, rank
136 |     )
137 |   `);
138 | 
139 |   console.log(`✅ Extracted and ranked ${configs.length} node configurations`);
140 | }
141 | 
142 | /**
143 |  * Extract node configurations from existing templates
144 |  */
145 | async function extractTemplateConfigs(db: any, service: TemplateService) {
146 |   console.log('📦 Extracting node configurations from templates...');
147 |   const repository = (service as any).repository;
148 |   const allTemplates = repository.getAllTemplates();
149 | 
150 |   const allConfigs: any[] = [];
151 |   let configsExtracted = 0;
152 | 
153 |   for (const template of allTemplates) {
154 |     if (template.workflow_json_compressed) {
155 |       const metadata = template.metadata_json ? JSON.parse(template.metadata_json) : null;
156 |       const configs = extractNodeConfigs(
157 |         template.id,
158 |         template.name,
159 |         template.views,
160 |         template.workflow_json_compressed,
161 |         metadata
162 |       );
163 |       allConfigs.push(...configs);
164 |       configsExtracted += configs.length;
165 |     }
166 |   }
167 | 
168 |   if (allConfigs.length > 0) {
169 |     insertAndRankConfigs(db, allConfigs);
170 | 
171 |     // Show stats
172 |     const configStats = db.prepare(`
173 |       SELECT
174 |         COUNT(DISTINCT node_type) as node_types,
175 |         COUNT(*) as total_configs,
176 |         AVG(rank) as avg_rank
177 |       FROM template_node_configs
178 |     `).get() as any;
179 | 
180 |     console.log(`📊 Node config stats:`);
181 |     console.log(`   - Unique node types: ${configStats.node_types}`);
182 |     console.log(`   - Total configs stored: ${configStats.total_configs}`);
183 |     console.log(`   - Average rank: ${configStats.avg_rank?.toFixed(1) || 'N/A'}`);
184 |   } else {
185 |     console.log('⚠️  No node configurations extracted');
186 |   }
187 | }
188 | 
189 | async function fetchTemplates(
190 |   mode: 'rebuild' | 'update' = 'rebuild',
191 |   generateMetadata: boolean = false,
192 |   metadataOnly: boolean = false,
193 |   extractOnly: boolean = false
194 | ) {
195 |   // If extract-only mode, skip template fetching and only extract configs
196 |   if (extractOnly) {
197 |     console.log('📦 Extract-only mode: Extracting node configurations from existing templates...\n');
198 | 
199 |     const db = await createDatabaseAdapter('./data/nodes.db');
200 | 
201 |     // Ensure template_node_configs table exists
202 |     try {
203 |       const tableExists = db.prepare(`
204 |         SELECT name FROM sqlite_master
205 |         WHERE type='table' AND name='template_node_configs'
206 |       `).get();
207 | 
208 |       if (!tableExists) {
209 |         console.log('📋 Creating template_node_configs table...');
210 |         const migrationPath = path.join(__dirname, '../../src/database/migrations/add-template-node-configs.sql');
211 |         const migration = fs.readFileSync(migrationPath, 'utf8');
212 |         db.exec(migration);
213 |         console.log('✅ Table created successfully\n');
214 |       }
215 |     } catch (error) {
216 |       console.error('❌ Error checking/creating template_node_configs table:', error);
217 |       if ('close' in db && typeof db.close === 'function') {
218 |         db.close();
219 |       }
220 |       process.exit(1);
221 |     }
222 | 
223 |     const service = new TemplateService(db);
224 | 
225 |     await extractTemplateConfigs(db, service);
226 | 
227 |     if ('close' in db && typeof db.close === 'function') {
228 |       db.close();
229 |     }
230 |     return;
231 |   }
232 | 
233 |   // If metadata-only mode, skip template fetching entirely
234 |   if (metadataOnly) {
235 |     console.log('🤖 Metadata-only mode: Generating metadata for existing templates...\n');
236 | 
237 |     if (!process.env.OPENAI_API_KEY) {
238 |       console.error('❌ OPENAI_API_KEY not set in environment');
239 |       process.exit(1);
240 |     }
241 | 
242 |     const db = await createDatabaseAdapter('./data/nodes.db');
243 |     const service = new TemplateService(db);
244 | 
245 |     await generateTemplateMetadata(db, service);
246 | 
247 |     if ('close' in db && typeof db.close === 'function') {
248 |       db.close();
249 |     }
250 |     return;
251 |   }
252 |   
253 |   const modeEmoji = mode === 'rebuild' ? '🔄' : '⬆️';
254 |   const modeText = mode === 'rebuild' ? 'Rebuilding' : 'Updating';
255 |   console.log(`${modeEmoji} ${modeText} n8n workflow templates...\n`);
256 |   
257 |   if (generateMetadata) {
258 |     console.log('🤖 Metadata generation enabled (using OpenAI)\n');
259 |   }
260 |   
261 |   // Ensure data directory exists
262 |   const dataDir = './data';
263 |   if (!fs.existsSync(dataDir)) {
264 |     fs.mkdirSync(dataDir, { recursive: true });
265 |   }
266 |   
267 |   // Initialize database
268 |   const db = await createDatabaseAdapter('./data/nodes.db');
269 |   
270 |   // Handle database schema based on mode
271 |   if (mode === 'rebuild') {
272 |     try {
273 |       // Drop existing tables in rebuild mode
274 |       db.exec('DROP TABLE IF EXISTS templates');
275 |       db.exec('DROP TABLE IF EXISTS templates_fts');
276 |       console.log('🗑️  Dropped existing templates tables (rebuild mode)\n');
277 |       
278 |       // Apply fresh schema
279 |       const schema = fs.readFileSync(path.join(__dirname, '../../src/database/schema.sql'), 'utf8');
280 |       db.exec(schema);
281 |       console.log('📋 Applied database schema\n');
282 |     } catch (error) {
283 |       console.error('❌ Error setting up database schema:', error);
284 |       throw error;
285 |     }
286 |   } else {
287 |     console.log('📊 Update mode: Keeping existing templates and schema\n');
288 |     
289 |     // In update mode, only ensure new columns exist (for migration)
290 |     try {
291 |       // Check if metadata columns exist, add them if not (migration support)
292 |       const columns = db.prepare("PRAGMA table_info(templates)").all() as any[];
293 |       const hasMetadataColumn = columns.some((col: any) => col.name === 'metadata_json');
294 |       
295 |       if (!hasMetadataColumn) {
296 |         console.log('📋 Adding metadata columns to existing schema...');
297 |         db.exec(`
298 |           ALTER TABLE templates ADD COLUMN metadata_json TEXT;
299 |           ALTER TABLE templates ADD COLUMN metadata_generated_at DATETIME;
300 |         `);
301 |         console.log('✅ Metadata columns added\n');
302 |       }
303 |     } catch (error) {
304 |       // Columns might already exist, that's fine
305 |       console.log('📋 Schema is up to date\n');
306 |     }
307 |   }
308 |   
309 |   // FTS5 initialization is handled by TemplateRepository
310 |   // No need to duplicate the logic here
311 |   
312 |   // Create service
313 |   const service = new TemplateService(db);
314 |   
315 |   // Progress tracking
316 |   let lastMessage = '';
317 |   const startTime = Date.now();
318 |   
319 |   try {
320 |     await service.fetchAndUpdateTemplates((message, current, total) => {
321 |       // Clear previous line
322 |       if (lastMessage) {
323 |         process.stdout.write('\r' + ' '.repeat(lastMessage.length) + '\r');
324 |       }
325 |       
326 |       const progress = total > 0 ? Math.round((current / total) * 100) : 0;
327 |       lastMessage = `📊 ${message}: ${current}/${total} (${progress}%)`;
328 |       process.stdout.write(lastMessage);
329 |     }, mode);  // Pass the mode parameter!
330 |     
331 |     console.log('\n'); // New line after progress
332 |     
333 |     // Get stats
334 |     const stats = await service.getTemplateStats();
335 |     const elapsed = Math.round((Date.now() - startTime) / 1000);
336 |     
337 |     console.log('✅ Template fetch complete!\n');
338 |     console.log('📈 Statistics:');
339 |     console.log(`   - Total templates: ${stats.totalTemplates}`);
340 |     console.log(`   - Average views: ${stats.averageViews}`);
341 |     console.log(`   - Time elapsed: ${elapsed} seconds`);
342 |     console.log('\n🔝 Top used nodes:');
343 |     
344 |     stats.topUsedNodes.forEach((node: any, index: number) => {
345 |       console.log(`   ${index + 1}. ${node.node} (${node.count} templates)`);
346 |     });
347 | 
348 |     // Extract node configurations from templates
349 |     console.log('');
350 |     await extractTemplateConfigs(db, service);
351 | 
352 |     // Generate metadata if requested
353 |     if (generateMetadata && process.env.OPENAI_API_KEY) {
354 |       console.log('\n🤖 Generating metadata for templates...');
355 |       await generateTemplateMetadata(db, service);
356 |     } else if (generateMetadata && !process.env.OPENAI_API_KEY) {
357 |       console.log('\n⚠️  Metadata generation requested but OPENAI_API_KEY not set');
358 |     }
359 | 
360 |   } catch (error) {
361 |     console.error('\n❌ Error fetching templates:', error);
362 |     process.exit(1);
363 |   }
364 |   
365 |   // Close database
366 |   if ('close' in db && typeof db.close === 'function') {
367 |     db.close();
368 |   }
369 | }
370 | 
371 | // Generate metadata for templates using OpenAI
372 | async function generateTemplateMetadata(db: any, service: TemplateService) {
373 |   try {
374 |     const { BatchProcessor } = await import('../templates/batch-processor');
375 |     const repository = (service as any).repository;
376 |     
377 |     // Get templates without metadata (0 = no limit)
378 |     const limit = parseInt(process.env.METADATA_LIMIT || '0');
379 |     const templatesWithoutMetadata = limit > 0 
380 |       ? repository.getTemplatesWithoutMetadata(limit)
381 |       : repository.getTemplatesWithoutMetadata(999999); // Get all
382 |     
383 |     if (templatesWithoutMetadata.length === 0) {
384 |       console.log('✅ All templates already have metadata');
385 |       return;
386 |     }
387 |     
388 |     console.log(`Found ${templatesWithoutMetadata.length} templates without metadata`);
389 |     
390 |     // Create batch processor
391 |     const batchSize = parseInt(process.env.OPENAI_BATCH_SIZE || '50');
392 |     console.log(`Processing in batches of ${batchSize} templates each`);
393 |     
394 |     // Warn if batch size is very large
395 |     if (batchSize > 100) {
396 |       console.log(`⚠️  Large batch size (${batchSize}) may take longer to process`);
397 |       console.log(`   Consider using OPENAI_BATCH_SIZE=50 for faster results`);
398 |     }
399 |     
400 |     const processor = new BatchProcessor({
401 |       apiKey: process.env.OPENAI_API_KEY!,
402 |       model: process.env.OPENAI_MODEL || 'gpt-4o-mini',
403 |       batchSize: batchSize,
404 |       outputDir: './temp/batch'
405 |     });
406 |     
407 |     // Prepare metadata requests
408 |     const requests: MetadataRequest[] = templatesWithoutMetadata.map((t: any) => {
409 |       let workflow = undefined;
410 |       try {
411 |         if (t.workflow_json_compressed) {
412 |           const decompressed = zlib.gunzipSync(Buffer.from(t.workflow_json_compressed, 'base64'));
413 |           workflow = JSON.parse(decompressed.toString());
414 |         } else if (t.workflow_json) {
415 |           workflow = JSON.parse(t.workflow_json);
416 |         }
417 |       } catch (error) {
418 |         console.warn(`Failed to parse workflow for template ${t.id}:`, error);
419 |       }
420 | 
421 |       // Parse nodes_used safely
422 |       let nodes: string[] = [];
423 |       try {
424 |         if (t.nodes_used) {
425 |           nodes = JSON.parse(t.nodes_used);
426 |           // Ensure it's an array
427 |           if (!Array.isArray(nodes)) {
428 |             console.warn(`Template ${t.id} has invalid nodes_used (not an array), using empty array`);
429 |             nodes = [];
430 |           }
431 |         }
432 |       } catch (error) {
433 |         console.warn(`Failed to parse nodes_used for template ${t.id}:`, error);
434 |         nodes = [];
435 |       }
436 | 
437 |       return {
438 |         templateId: t.id,
439 |         name: t.name,
440 |         description: t.description,
441 |         nodes: nodes,
442 |         workflow
443 |       };
444 |     });
445 |     
446 |     // Process in batches
447 |     const results = await processor.processTemplates(requests, (message, current, total) => {
448 |       process.stdout.write(`\r📊 ${message}: ${current}/${total}`);
449 |     });
450 |     
451 |     console.log('\n');
452 |     
453 |     // Update database with metadata
454 |     const metadataMap = new Map();
455 |     for (const [templateId, result] of results) {
456 |       if (!result.error) {
457 |         metadataMap.set(templateId, result.metadata);
458 |       }
459 |     }
460 |     
461 |     if (metadataMap.size > 0) {
462 |       repository.batchUpdateMetadata(metadataMap);
463 |       console.log(`✅ Updated metadata for ${metadataMap.size} templates`);
464 |     }
465 |     
466 |     // Show stats
467 |     const stats = repository.getMetadataStats();
468 |     console.log('\n📈 Metadata Statistics:');
469 |     console.log(`   - Total templates: ${stats.total}`);
470 |     console.log(`   - With metadata: ${stats.withMetadata}`);
471 |     console.log(`   - Without metadata: ${stats.withoutMetadata}`);
472 |     console.log(`   - Outdated (>30 days): ${stats.outdated}`);
473 |   } catch (error) {
474 |     console.error('\n❌ Error generating metadata:', error);
475 |   }
476 | }
477 | 
478 | // Parse command line arguments
479 | function parseArgs(): { mode: 'rebuild' | 'update', generateMetadata: boolean, metadataOnly: boolean, extractOnly: boolean } {
480 |   const args = process.argv.slice(2);
481 | 
482 |   let mode: 'rebuild' | 'update' = 'rebuild';
483 |   let generateMetadata = false;
484 |   let metadataOnly = false;
485 |   let extractOnly = false;
486 | 
487 |   // Check for --mode flag
488 |   const modeIndex = args.findIndex(arg => arg.startsWith('--mode'));
489 |   if (modeIndex !== -1) {
490 |     const modeArg = args[modeIndex];
491 |     const modeValue = modeArg.includes('=') ? modeArg.split('=')[1] : args[modeIndex + 1];
492 | 
493 |     if (modeValue === 'update') {
494 |       mode = 'update';
495 |     }
496 |   }
497 | 
498 |   // Check for --update flag as shorthand
499 |   if (args.includes('--update')) {
500 |     mode = 'update';
501 |   }
502 | 
503 |   // Check for --generate-metadata flag
504 |   if (args.includes('--generate-metadata') || args.includes('--metadata')) {
505 |     generateMetadata = true;
506 |   }
507 | 
508 |   // Check for --metadata-only flag
509 |   if (args.includes('--metadata-only')) {
510 |     metadataOnly = true;
511 |   }
512 | 
513 |   // Check for --extract-only flag
514 |   if (args.includes('--extract-only') || args.includes('--extract')) {
515 |     extractOnly = true;
516 |   }
517 | 
518 |   // Show help if requested
519 |   if (args.includes('--help') || args.includes('-h')) {
520 |     console.log('Usage: npm run fetch:templates [options]\n');
521 |     console.log('Options:');
522 |     console.log('  --mode=rebuild|update  Rebuild from scratch or update existing (default: rebuild)');
523 |     console.log('  --update               Shorthand for --mode=update');
524 |     console.log('  --generate-metadata    Generate AI metadata after fetching templates');
525 |     console.log('  --metadata             Shorthand for --generate-metadata');
526 |     console.log('  --metadata-only        Only generate metadata, skip template fetching');
527 |     console.log('  --extract-only         Only extract node configs, skip template fetching');
528 |     console.log('  --extract              Shorthand for --extract-only');
529 |     console.log('  --help, -h             Show this help message');
530 |     process.exit(0);
531 |   }
532 | 
533 |   return { mode, generateMetadata, metadataOnly, extractOnly };
534 | }
535 | 
536 | // Run if called directly
537 | if (require.main === module) {
538 |   const { mode, generateMetadata, metadataOnly, extractOnly } = parseArgs();
539 |   fetchTemplates(mode, generateMetadata, metadataOnly, extractOnly).catch(console.error);
540 | }
541 | 
542 | export { fetchTemplates };
```

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

```typescript
  1 | import { describe, it, expect, beforeEach, vi } from 'vitest';
  2 | import { NodeRepository } from '@/database/node-repository';
  3 | import { DatabaseAdapter } from '@/database/database-adapter';
  4 | import { ParsedNode } from '@/parsers/node-parser';
  5 | 
  6 | describe('NodeRepository - Outputs Handling', () => {
  7 |   let repository: NodeRepository;
  8 |   let mockDb: DatabaseAdapter;
  9 |   let mockStatement: any;
 10 | 
 11 |   beforeEach(() => {
 12 |     mockStatement = {
 13 |       run: vi.fn(),
 14 |       get: vi.fn(),
 15 |       all: vi.fn()
 16 |     };
 17 | 
 18 |     mockDb = {
 19 |       prepare: vi.fn().mockReturnValue(mockStatement),
 20 |       transaction: vi.fn(),
 21 |       exec: vi.fn(),
 22 |       close: vi.fn(),
 23 |       pragma: vi.fn()
 24 |     } as any;
 25 | 
 26 |     repository = new NodeRepository(mockDb);
 27 |   });
 28 | 
 29 |   describe('saveNode with outputs', () => {
 30 |     it('should save node with outputs and outputNames correctly', () => {
 31 |       const outputs = [
 32 |         { displayName: 'Done', description: 'Final results when loop completes' },
 33 |         { displayName: 'Loop', description: 'Current batch data during iteration' }
 34 |       ];
 35 |       const outputNames = ['done', 'loop'];
 36 | 
 37 |       const node: ParsedNode = {
 38 |         style: 'programmatic',
 39 |         nodeType: 'nodes-base.splitInBatches',
 40 |         displayName: 'Split In Batches',
 41 |         description: 'Split data into batches',
 42 |         category: 'transform',
 43 |         properties: [],
 44 |         credentials: [],
 45 |         isAITool: false,
 46 |         isTrigger: false,
 47 |         isWebhook: false,
 48 |         operations: [],
 49 |         version: '3',
 50 |         isVersioned: false,
 51 |         packageName: 'n8n-nodes-base',
 52 |         outputs,
 53 |         outputNames
 54 |       };
 55 | 
 56 |       repository.saveNode(node);
 57 | 
 58 |       expect(mockDb.prepare).toHaveBeenCalledWith(`
 59 |       INSERT OR REPLACE INTO nodes (
 60 |         node_type, package_name, display_name, description,
 61 |         category, development_style, is_ai_tool, is_trigger,
 62 |         is_webhook, is_versioned, version, documentation,
 63 |         properties_schema, operations, credentials_required,
 64 |         outputs, output_names
 65 |       ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
 66 |     `);
 67 | 
 68 |       expect(mockStatement.run).toHaveBeenCalledWith(
 69 |         'nodes-base.splitInBatches',
 70 |         'n8n-nodes-base',
 71 |         'Split In Batches',
 72 |         'Split data into batches',
 73 |         'transform',
 74 |         'programmatic',
 75 |         0, // false
 76 |         0, // false
 77 |         0, // false
 78 |         0, // false
 79 |         '3',
 80 |         null, // documentation
 81 |         JSON.stringify([], null, 2), // properties
 82 |         JSON.stringify([], null, 2), // operations
 83 |         JSON.stringify([], null, 2), // credentials
 84 |         JSON.stringify(outputs, null, 2), // outputs
 85 |         JSON.stringify(outputNames, null, 2) // output_names
 86 |       );
 87 |     });
 88 | 
 89 |     it('should save node with only outputs (no outputNames)', () => {
 90 |       const outputs = [
 91 |         { displayName: 'True', description: 'Items that match condition' },
 92 |         { displayName: 'False', description: 'Items that do not match condition' }
 93 |       ];
 94 | 
 95 |       const node: ParsedNode = {
 96 |         style: 'programmatic',
 97 |         nodeType: 'nodes-base.if',
 98 |         displayName: 'IF',
 99 |         description: 'Route items based on conditions',
100 |         category: 'transform',
101 |         properties: [],
102 |         credentials: [],
103 |         isAITool: false,
104 |         isTrigger: false,
105 |         isWebhook: false,
106 |         operations: [],
107 |         version: '2',
108 |         isVersioned: false,
109 |         packageName: 'n8n-nodes-base',
110 |         outputs
111 |         // no outputNames
112 |       };
113 | 
114 |       repository.saveNode(node);
115 | 
116 |       const callArgs = mockStatement.run.mock.calls[0];
117 |       expect(callArgs[15]).toBe(JSON.stringify(outputs, null, 2)); // outputs
118 |       expect(callArgs[16]).toBe(null); // output_names should be null
119 |     });
120 | 
121 |     it('should save node with only outputNames (no outputs)', () => {
122 |       const outputNames = ['main', 'error'];
123 | 
124 |       const node: ParsedNode = {
125 |         style: 'programmatic',
126 |         nodeType: 'nodes-base.customNode',
127 |         displayName: 'Custom Node',
128 |         description: 'Custom node with output names only',
129 |         category: 'transform',
130 |         properties: [],
131 |         credentials: [],
132 |         isAITool: false,
133 |         isTrigger: false,
134 |         isWebhook: false,
135 |         operations: [],
136 |         version: '1',
137 |         isVersioned: false,
138 |         packageName: 'n8n-nodes-base',
139 |         outputNames
140 |         // no outputs
141 |       };
142 | 
143 |       repository.saveNode(node);
144 | 
145 |       const callArgs = mockStatement.run.mock.calls[0];
146 |       expect(callArgs[15]).toBe(null); // outputs should be null
147 |       expect(callArgs[16]).toBe(JSON.stringify(outputNames, null, 2)); // output_names
148 |     });
149 | 
150 |     it('should save node without outputs or outputNames', () => {
151 |       const node: ParsedNode = {
152 |         style: 'programmatic',
153 |         nodeType: 'nodes-base.httpRequest',
154 |         displayName: 'HTTP Request',
155 |         description: 'Make HTTP requests',
156 |         category: 'input',
157 |         properties: [],
158 |         credentials: [],
159 |         isAITool: false,
160 |         isTrigger: false,
161 |         isWebhook: false,
162 |         operations: [],
163 |         version: '4',
164 |         isVersioned: false,
165 |         packageName: 'n8n-nodes-base'
166 |         // no outputs or outputNames
167 |       };
168 | 
169 |       repository.saveNode(node);
170 | 
171 |       const callArgs = mockStatement.run.mock.calls[0];
172 |       expect(callArgs[15]).toBe(null); // outputs should be null
173 |       expect(callArgs[16]).toBe(null); // output_names should be null
174 |     });
175 | 
176 |     it('should handle empty outputs and outputNames arrays', () => {
177 |       const node: ParsedNode = {
178 |         style: 'programmatic',
179 |         nodeType: 'nodes-base.emptyNode',
180 |         displayName: 'Empty Node',
181 |         description: 'Node with empty outputs',
182 |         category: 'misc',
183 |         properties: [],
184 |         credentials: [],
185 |         isAITool: false,
186 |         isTrigger: false,
187 |         isWebhook: false,
188 |         operations: [],
189 |         version: '1',
190 |         isVersioned: false,
191 |         packageName: 'n8n-nodes-base',
192 |         outputs: [],
193 |         outputNames: []
194 |       };
195 | 
196 |       repository.saveNode(node);
197 | 
198 |       const callArgs = mockStatement.run.mock.calls[0];
199 |       expect(callArgs[15]).toBe(JSON.stringify([], null, 2)); // outputs
200 |       expect(callArgs[16]).toBe(JSON.stringify([], null, 2)); // output_names
201 |     });
202 |   });
203 | 
204 |   describe('getNode with outputs', () => {
205 |     it('should retrieve node with outputs and outputNames correctly', () => {
206 |       const outputs = [
207 |         { displayName: 'Done', description: 'Final results when loop completes' },
208 |         { displayName: 'Loop', description: 'Current batch data during iteration' }
209 |       ];
210 |       const outputNames = ['done', 'loop'];
211 | 
212 |       const mockRow = {
213 |         node_type: 'nodes-base.splitInBatches',
214 |         display_name: 'Split In Batches',
215 |         description: 'Split data into batches',
216 |         category: 'transform',
217 |         development_style: 'programmatic',
218 |         package_name: 'n8n-nodes-base',
219 |         is_ai_tool: 0,
220 |         is_trigger: 0,
221 |         is_webhook: 0,
222 |         is_versioned: 0,
223 |         version: '3',
224 |         properties_schema: JSON.stringify([]),
225 |         operations: JSON.stringify([]),
226 |         credentials_required: JSON.stringify([]),
227 |         documentation: null,
228 |         outputs: JSON.stringify(outputs),
229 |         output_names: JSON.stringify(outputNames)
230 |       };
231 | 
232 |       mockStatement.get.mockReturnValue(mockRow);
233 | 
234 |       const result = repository.getNode('nodes-base.splitInBatches');
235 | 
236 |       expect(result).toEqual({
237 |         nodeType: 'nodes-base.splitInBatches',
238 |         displayName: 'Split In Batches',
239 |         description: 'Split data into batches',
240 |         category: 'transform',
241 |         developmentStyle: 'programmatic',
242 |         package: 'n8n-nodes-base',
243 |         isAITool: false,
244 |         isTrigger: false,
245 |         isWebhook: false,
246 |         isVersioned: false,
247 |         version: '3',
248 |         properties: [],
249 |         operations: [],
250 |         credentials: [],
251 |         hasDocumentation: false,
252 |         outputs,
253 |         outputNames
254 |       });
255 |     });
256 | 
257 |     it('should retrieve node with only outputs (null outputNames)', () => {
258 |       const outputs = [
259 |         { displayName: 'True', description: 'Items that match condition' }
260 |       ];
261 | 
262 |       const mockRow = {
263 |         node_type: 'nodes-base.if',
264 |         display_name: 'IF',
265 |         description: 'Route items',
266 |         category: 'transform',
267 |         development_style: 'programmatic',
268 |         package_name: 'n8n-nodes-base',
269 |         is_ai_tool: 0,
270 |         is_trigger: 0,
271 |         is_webhook: 0,
272 |         is_versioned: 0,
273 |         version: '2',
274 |         properties_schema: JSON.stringify([]),
275 |         operations: JSON.stringify([]),
276 |         credentials_required: JSON.stringify([]),
277 |         documentation: null,
278 |         outputs: JSON.stringify(outputs),
279 |         output_names: null
280 |       };
281 | 
282 |       mockStatement.get.mockReturnValue(mockRow);
283 | 
284 |       const result = repository.getNode('nodes-base.if');
285 | 
286 |       expect(result.outputs).toEqual(outputs);
287 |       expect(result.outputNames).toBe(null);
288 |     });
289 | 
290 |     it('should retrieve node with only outputNames (null outputs)', () => {
291 |       const outputNames = ['main'];
292 | 
293 |       const mockRow = {
294 |         node_type: 'nodes-base.customNode',
295 |         display_name: 'Custom Node',
296 |         description: 'Custom node',
297 |         category: 'misc',
298 |         development_style: 'programmatic',
299 |         package_name: 'n8n-nodes-base',
300 |         is_ai_tool: 0,
301 |         is_trigger: 0,
302 |         is_webhook: 0,
303 |         is_versioned: 0,
304 |         version: '1',
305 |         properties_schema: JSON.stringify([]),
306 |         operations: JSON.stringify([]),
307 |         credentials_required: JSON.stringify([]),
308 |         documentation: null,
309 |         outputs: null,
310 |         output_names: JSON.stringify(outputNames)
311 |       };
312 | 
313 |       mockStatement.get.mockReturnValue(mockRow);
314 | 
315 |       const result = repository.getNode('nodes-base.customNode');
316 | 
317 |       expect(result.outputs).toBe(null);
318 |       expect(result.outputNames).toEqual(outputNames);
319 |     });
320 | 
321 |     it('should retrieve node without outputs or outputNames', () => {
322 |       const mockRow = {
323 |         node_type: 'nodes-base.httpRequest',
324 |         display_name: 'HTTP Request',
325 |         description: 'Make HTTP requests',
326 |         category: 'input',
327 |         development_style: 'programmatic',
328 |         package_name: 'n8n-nodes-base',
329 |         is_ai_tool: 0,
330 |         is_trigger: 0,
331 |         is_webhook: 0,
332 |         is_versioned: 0,
333 |         version: '4',
334 |         properties_schema: JSON.stringify([]),
335 |         operations: JSON.stringify([]),
336 |         credentials_required: JSON.stringify([]),
337 |         documentation: null,
338 |         outputs: null,
339 |         output_names: null
340 |       };
341 | 
342 |       mockStatement.get.mockReturnValue(mockRow);
343 | 
344 |       const result = repository.getNode('nodes-base.httpRequest');
345 | 
346 |       expect(result.outputs).toBe(null);
347 |       expect(result.outputNames).toBe(null);
348 |     });
349 | 
350 |     it('should handle malformed JSON gracefully', () => {
351 |       const mockRow = {
352 |         node_type: 'nodes-base.malformed',
353 |         display_name: 'Malformed Node',
354 |         description: 'Node with malformed JSON',
355 |         category: 'misc',
356 |         development_style: 'programmatic',
357 |         package_name: 'n8n-nodes-base',
358 |         is_ai_tool: 0,
359 |         is_trigger: 0,
360 |         is_webhook: 0,
361 |         is_versioned: 0,
362 |         version: '1',
363 |         properties_schema: JSON.stringify([]),
364 |         operations: JSON.stringify([]),
365 |         credentials_required: JSON.stringify([]),
366 |         documentation: null,
367 |         outputs: '{invalid json}',
368 |         output_names: '[invalid, json'
369 |       };
370 | 
371 |       mockStatement.get.mockReturnValue(mockRow);
372 | 
373 |       const result = repository.getNode('nodes-base.malformed');
374 | 
375 |       // Should use default values when JSON parsing fails
376 |       expect(result.outputs).toBe(null);
377 |       expect(result.outputNames).toBe(null);
378 |     });
379 | 
380 |     it('should return null for non-existent node', () => {
381 |       mockStatement.get.mockReturnValue(null);
382 | 
383 |       const result = repository.getNode('nodes-base.nonExistent');
384 | 
385 |       expect(result).toBe(null);
386 |     });
387 | 
388 |     it('should handle SplitInBatches counterintuitive output order correctly', () => {
389 |       // Test that the output order is preserved: done=0, loop=1
390 |       const outputs = [
391 |         { displayName: 'Done', description: 'Final results when loop completes', index: 0 },
392 |         { displayName: 'Loop', description: 'Current batch data during iteration', index: 1 }
393 |       ];
394 |       const outputNames = ['done', 'loop'];
395 | 
396 |       const mockRow = {
397 |         node_type: 'nodes-base.splitInBatches',
398 |         display_name: 'Split In Batches',
399 |         description: 'Split data into batches',
400 |         category: 'transform',
401 |         development_style: 'programmatic',
402 |         package_name: 'n8n-nodes-base',
403 |         is_ai_tool: 0,
404 |         is_trigger: 0,
405 |         is_webhook: 0,
406 |         is_versioned: 0,
407 |         version: '3',
408 |         properties_schema: JSON.stringify([]),
409 |         operations: JSON.stringify([]),
410 |         credentials_required: JSON.stringify([]),
411 |         documentation: null,
412 |         outputs: JSON.stringify(outputs),
413 |         output_names: JSON.stringify(outputNames)
414 |       };
415 | 
416 |       mockStatement.get.mockReturnValue(mockRow);
417 | 
418 |       const result = repository.getNode('nodes-base.splitInBatches');
419 | 
420 |       // Verify order is preserved
421 |       expect(result.outputs[0].displayName).toBe('Done');
422 |       expect(result.outputs[1].displayName).toBe('Loop');
423 |       expect(result.outputNames[0]).toBe('done');
424 |       expect(result.outputNames[1]).toBe('loop');
425 |     });
426 |   });
427 | 
428 |   describe('parseNodeRow with outputs', () => {
429 |     it('should parse node row with outputs correctly using parseNodeRow', () => {
430 |       const outputs = [{ displayName: 'Output' }];
431 |       const outputNames = ['main'];
432 | 
433 |       const mockRow = {
434 |         node_type: 'nodes-base.test',
435 |         display_name: 'Test',
436 |         description: 'Test node',
437 |         category: 'misc',
438 |         development_style: 'programmatic',
439 |         package_name: 'n8n-nodes-base',
440 |         is_ai_tool: 0,
441 |         is_trigger: 0,
442 |         is_webhook: 0,
443 |         is_versioned: 0,
444 |         version: '1',
445 |         properties_schema: JSON.stringify([]),
446 |         operations: JSON.stringify([]),
447 |         credentials_required: JSON.stringify([]),
448 |         documentation: null,
449 |         outputs: JSON.stringify(outputs),
450 |         output_names: JSON.stringify(outputNames)
451 |       };
452 | 
453 |       mockStatement.all.mockReturnValue([mockRow]);
454 | 
455 |       const results = repository.getAllNodes(1);
456 | 
457 |       expect(results[0].outputs).toEqual(outputs);
458 |       expect(results[0].outputNames).toEqual(outputNames);
459 |     });
460 | 
461 |     it('should handle empty string as null for outputs', () => {
462 |       const mockRow = {
463 |         node_type: 'nodes-base.empty',
464 |         display_name: 'Empty',
465 |         description: 'Empty node',
466 |         category: 'misc',
467 |         development_style: 'programmatic',
468 |         package_name: 'n8n-nodes-base',
469 |         is_ai_tool: 0,
470 |         is_trigger: 0,
471 |         is_webhook: 0,
472 |         is_versioned: 0,
473 |         version: '1',
474 |         properties_schema: JSON.stringify([]),
475 |         operations: JSON.stringify([]),
476 |         credentials_required: JSON.stringify([]),
477 |         documentation: null,
478 |         outputs: '', // empty string
479 |         output_names: '' // empty string
480 |       };
481 | 
482 |       mockStatement.all.mockReturnValue([mockRow]);
483 | 
484 |       const results = repository.getAllNodes(1);
485 | 
486 |       // Empty strings should be treated as null since they fail JSON parsing
487 |       expect(results[0].outputs).toBe(null);
488 |       expect(results[0].outputNames).toBe(null);
489 |     });
490 |   });
491 | 
492 |   describe('complex output structures', () => {
493 |     it('should handle complex output objects with metadata', () => {
494 |       const complexOutputs = [
495 |         {
496 |           displayName: 'Done',
497 |           name: 'done',
498 |           type: 'main',
499 |           hint: 'Receives the final data after all batches have been processed',
500 |           description: 'Final results when loop completes',
501 |           index: 0
502 |         },
503 |         {
504 |           displayName: 'Loop',
505 |           name: 'loop',
506 |           type: 'main', 
507 |           hint: 'Receives the current batch data during each iteration',
508 |           description: 'Current batch data during iteration',
509 |           index: 1
510 |         }
511 |       ];
512 | 
513 |       const node: ParsedNode = {
514 |         style: 'programmatic',
515 |         nodeType: 'nodes-base.splitInBatches',
516 |         displayName: 'Split In Batches',
517 |         description: 'Split data into batches',
518 |         category: 'transform',
519 |         properties: [],
520 |         credentials: [],
521 |         isAITool: false,
522 |         isTrigger: false,
523 |         isWebhook: false,
524 |         operations: [],
525 |         version: '3',
526 |         isVersioned: false,
527 |         packageName: 'n8n-nodes-base',
528 |         outputs: complexOutputs,
529 |         outputNames: ['done', 'loop']
530 |       };
531 | 
532 |       repository.saveNode(node);
533 | 
534 |       // Simulate retrieval
535 |       const mockRow = {
536 |         node_type: 'nodes-base.splitInBatches',
537 |         display_name: 'Split In Batches',
538 |         description: 'Split data into batches',
539 |         category: 'transform',
540 |         development_style: 'programmatic',
541 |         package_name: 'n8n-nodes-base',
542 |         is_ai_tool: 0,
543 |         is_trigger: 0,
544 |         is_webhook: 0,
545 |         is_versioned: 0,
546 |         version: '3',
547 |         properties_schema: JSON.stringify([]),
548 |         operations: JSON.stringify([]),
549 |         credentials_required: JSON.stringify([]),
550 |         documentation: null,
551 |         outputs: JSON.stringify(complexOutputs),
552 |         output_names: JSON.stringify(['done', 'loop'])
553 |       };
554 | 
555 |       mockStatement.get.mockReturnValue(mockRow);
556 | 
557 |       const result = repository.getNode('nodes-base.splitInBatches');
558 | 
559 |       expect(result.outputs).toEqual(complexOutputs);
560 |       expect(result.outputs[0]).toMatchObject({
561 |         displayName: 'Done',
562 |         name: 'done',
563 |         type: 'main',
564 |         hint: 'Receives the final data after all batches have been processed'
565 |       });
566 |     });
567 |   });
568 | });
```

--------------------------------------------------------------------------------
/tests/unit/services/execution-processor.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Execution Processor Service Tests
  3 |  *
  4 |  * Comprehensive test coverage for execution filtering and processing
  5 |  */
  6 | 
  7 | import { describe, it, expect } from 'vitest';
  8 | import {
  9 |   generatePreview,
 10 |   filterExecutionData,
 11 |   processExecution,
 12 | } from '../../../src/services/execution-processor';
 13 | import {
 14 |   Execution,
 15 |   ExecutionStatus,
 16 |   ExecutionFilterOptions,
 17 | } from '../../../src/types/n8n-api';
 18 | 
 19 | /**
 20 |  * Test data factories
 21 |  */
 22 | 
 23 | function createMockExecution(options: {
 24 |   id?: string;
 25 |   status?: ExecutionStatus;
 26 |   nodeData?: Record<string, any>;
 27 |   hasError?: boolean;
 28 | }): Execution {
 29 |   const { id = 'test-exec-1', status = ExecutionStatus.SUCCESS, nodeData = {}, hasError = false } = options;
 30 | 
 31 |   return {
 32 |     id,
 33 |     workflowId: 'workflow-1',
 34 |     status,
 35 |     mode: 'manual',
 36 |     finished: true,
 37 |     startedAt: '2024-01-01T10:00:00.000Z',
 38 |     stoppedAt: '2024-01-01T10:00:05.000Z',
 39 |     data: {
 40 |       resultData: {
 41 |         runData: nodeData,
 42 |         error: hasError ? { message: 'Test error' } : undefined,
 43 |       },
 44 |     },
 45 |   };
 46 | }
 47 | 
 48 | function createNodeData(itemCount: number, includeError = false) {
 49 |   const items = Array.from({ length: itemCount }, (_, i) => ({
 50 |     json: {
 51 |       id: i + 1,
 52 |       name: `Item ${i + 1}`,
 53 |       value: Math.random() * 100,
 54 |       nested: {
 55 |         field1: `value${i}`,
 56 |         field2: true,
 57 |       },
 58 |     },
 59 |   }));
 60 | 
 61 |   return [
 62 |     {
 63 |       startTime: Date.now(),
 64 |       executionTime: 123,
 65 |       data: {
 66 |         main: [items],
 67 |       },
 68 |       error: includeError ? { message: 'Node error' } : undefined,
 69 |     },
 70 |   ];
 71 | }
 72 | 
 73 | /**
 74 |  * Preview Mode Tests
 75 |  */
 76 | describe('ExecutionProcessor - Preview Mode', () => {
 77 |   it('should generate preview for empty execution', () => {
 78 |     const execution = createMockExecution({ nodeData: {} });
 79 |     const { preview, recommendation } = generatePreview(execution);
 80 | 
 81 |     expect(preview.totalNodes).toBe(0);
 82 |     expect(preview.executedNodes).toBe(0);
 83 |     expect(preview.estimatedSizeKB).toBe(0);
 84 |     expect(recommendation.canFetchFull).toBe(true);
 85 |     expect(recommendation.suggestedMode).toBe('full'); // Empty execution is safe to fetch in full
 86 |   });
 87 | 
 88 |   it('should generate preview with accurate item counts', () => {
 89 |     const execution = createMockExecution({
 90 |       nodeData: {
 91 |         'HTTP Request': createNodeData(50),
 92 |         'Filter': createNodeData(12),
 93 |       },
 94 |     });
 95 | 
 96 |     const { preview } = generatePreview(execution);
 97 | 
 98 |     expect(preview.totalNodes).toBe(2);
 99 |     expect(preview.executedNodes).toBe(2);
100 |     expect(preview.nodes['HTTP Request'].itemCounts.output).toBe(50);
101 |     expect(preview.nodes['Filter'].itemCounts.output).toBe(12);
102 |   });
103 | 
104 |   it('should extract data structure from nodes', () => {
105 |     const execution = createMockExecution({
106 |       nodeData: {
107 |         'HTTP Request': createNodeData(5),
108 |       },
109 |     });
110 | 
111 |     const { preview } = generatePreview(execution);
112 |     const structure = preview.nodes['HTTP Request'].dataStructure;
113 | 
114 |     expect(structure).toHaveProperty('json');
115 |     expect(structure.json).toHaveProperty('id');
116 |     expect(structure.json).toHaveProperty('name');
117 |     expect(structure.json).toHaveProperty('nested');
118 |     expect(structure.json.id).toBe('number');
119 |     expect(structure.json.name).toBe('string');
120 |     expect(typeof structure.json.nested).toBe('object');
121 |   });
122 | 
123 |   it('should estimate data size', () => {
124 |     const execution = createMockExecution({
125 |       nodeData: {
126 |         'HTTP Request': createNodeData(50),
127 |       },
128 |     });
129 | 
130 |     const { preview } = generatePreview(execution);
131 | 
132 |     expect(preview.estimatedSizeKB).toBeGreaterThan(0);
133 |     expect(preview.nodes['HTTP Request'].estimatedSizeKB).toBeGreaterThan(0);
134 |   });
135 | 
136 |   it('should detect error status in nodes', () => {
137 |     const execution = createMockExecution({
138 |       nodeData: {
139 |         'HTTP Request': createNodeData(5, true),
140 |       },
141 |     });
142 | 
143 |     const { preview } = generatePreview(execution);
144 | 
145 |     expect(preview.nodes['HTTP Request'].status).toBe('error');
146 |     expect(preview.nodes['HTTP Request'].error).toBeDefined();
147 |   });
148 | 
149 |   it('should recommend full mode for small datasets', () => {
150 |     const execution = createMockExecution({
151 |       nodeData: {
152 |         'HTTP Request': createNodeData(5),
153 |       },
154 |     });
155 | 
156 |     const { recommendation } = generatePreview(execution);
157 | 
158 |     expect(recommendation.canFetchFull).toBe(true);
159 |     expect(recommendation.suggestedMode).toBe('full');
160 |   });
161 | 
162 |   it('should recommend filtered mode for large datasets', () => {
163 |     const execution = createMockExecution({
164 |       nodeData: {
165 |         'HTTP Request': createNodeData(100),
166 |       },
167 |     });
168 | 
169 |     const { recommendation } = generatePreview(execution);
170 | 
171 |     expect(recommendation.canFetchFull).toBe(false);
172 |     expect(recommendation.suggestedMode).toBe('filtered');
173 |     expect(recommendation.suggestedItemsLimit).toBeGreaterThan(0);
174 |   });
175 | 
176 |   it('should recommend summary mode for moderate datasets', () => {
177 |     const execution = createMockExecution({
178 |       nodeData: {
179 |         'HTTP Request': createNodeData(30),
180 |       },
181 |     });
182 | 
183 |     const { recommendation } = generatePreview(execution);
184 | 
185 |     expect(recommendation.canFetchFull).toBe(false);
186 |     expect(recommendation.suggestedMode).toBe('summary');
187 |   });
188 | });
189 | 
190 | /**
191 |  * Filtering Mode Tests
192 |  */
193 | describe('ExecutionProcessor - Filtering', () => {
194 |   it('should filter by node names', () => {
195 |     const execution = createMockExecution({
196 |       nodeData: {
197 |         'HTTP Request': createNodeData(10),
198 |         'Filter': createNodeData(5),
199 |         'Set': createNodeData(3),
200 |       },
201 |     });
202 | 
203 |     const options: ExecutionFilterOptions = {
204 |       mode: 'filtered',
205 |       nodeNames: ['HTTP Request', 'Filter'],
206 |     };
207 | 
208 |     const result = filterExecutionData(execution, options);
209 | 
210 |     expect(result.nodes).toHaveProperty('HTTP Request');
211 |     expect(result.nodes).toHaveProperty('Filter');
212 |     expect(result.nodes).not.toHaveProperty('Set');
213 |     expect(result.summary?.executedNodes).toBe(2);
214 |   });
215 | 
216 |   it('should handle non-existent node names gracefully', () => {
217 |     const execution = createMockExecution({
218 |       nodeData: {
219 |         'HTTP Request': createNodeData(10),
220 |       },
221 |     });
222 | 
223 |     const options: ExecutionFilterOptions = {
224 |       mode: 'filtered',
225 |       nodeNames: ['NonExistent'],
226 |     };
227 | 
228 |     const result = filterExecutionData(execution, options);
229 | 
230 |     expect(Object.keys(result.nodes || {})).toHaveLength(0);
231 |     expect(result.summary?.executedNodes).toBe(0);
232 |   });
233 | 
234 |   it('should limit items to 0 (structure only)', () => {
235 |     const execution = createMockExecution({
236 |       nodeData: {
237 |         'HTTP Request': createNodeData(50),
238 |       },
239 |     });
240 | 
241 |     const options: ExecutionFilterOptions = {
242 |       mode: 'filtered',
243 |       itemsLimit: 0,
244 |     };
245 | 
246 |     const result = filterExecutionData(execution, options);
247 |     const nodeData = result.nodes?.['HTTP Request'];
248 | 
249 |     expect(nodeData?.data?.metadata.itemsShown).toBe(0);
250 |     expect(nodeData?.data?.metadata.truncated).toBe(true);
251 |     expect(nodeData?.data?.metadata.totalItems).toBe(50);
252 | 
253 |     // Check that we have structure but no actual values
254 |     const output = nodeData?.data?.output?.[0]?.[0];
255 |     expect(output).toBeDefined();
256 |     expect(typeof output).toBe('object');
257 |   });
258 | 
259 |   it('should limit items to 2 (default)', () => {
260 |     const execution = createMockExecution({
261 |       nodeData: {
262 |         'HTTP Request': createNodeData(50),
263 |       },
264 |     });
265 | 
266 |     const options: ExecutionFilterOptions = {
267 |       mode: 'summary',
268 |     };
269 | 
270 |     const result = filterExecutionData(execution, options);
271 |     const nodeData = result.nodes?.['HTTP Request'];
272 | 
273 |     expect(nodeData?.data?.metadata.itemsShown).toBe(2);
274 |     expect(nodeData?.data?.metadata.totalItems).toBe(50);
275 |     expect(nodeData?.data?.metadata.truncated).toBe(true);
276 |     expect(nodeData?.data?.output?.[0]).toHaveLength(2);
277 |   });
278 | 
279 |   it('should limit items to custom value', () => {
280 |     const execution = createMockExecution({
281 |       nodeData: {
282 |         'HTTP Request': createNodeData(50),
283 |       },
284 |     });
285 | 
286 |     const options: ExecutionFilterOptions = {
287 |       mode: 'filtered',
288 |       itemsLimit: 5,
289 |     };
290 | 
291 |     const result = filterExecutionData(execution, options);
292 |     const nodeData = result.nodes?.['HTTP Request'];
293 | 
294 |     expect(nodeData?.data?.metadata.itemsShown).toBe(5);
295 |     expect(nodeData?.data?.metadata.truncated).toBe(true);
296 |     expect(nodeData?.data?.output?.[0]).toHaveLength(5);
297 |   });
298 | 
299 |   it('should not truncate when itemsLimit is -1 (unlimited)', () => {
300 |     const execution = createMockExecution({
301 |       nodeData: {
302 |         'HTTP Request': createNodeData(50),
303 |       },
304 |     });
305 | 
306 |     const options: ExecutionFilterOptions = {
307 |       mode: 'filtered',
308 |       itemsLimit: -1,
309 |     };
310 | 
311 |     const result = filterExecutionData(execution, options);
312 |     const nodeData = result.nodes?.['HTTP Request'];
313 | 
314 |     expect(nodeData?.data?.metadata.itemsShown).toBe(50);
315 |     expect(nodeData?.data?.metadata.totalItems).toBe(50);
316 |     expect(nodeData?.data?.metadata.truncated).toBe(false);
317 |   });
318 | 
319 |   it('should not truncate when items are less than limit', () => {
320 |     const execution = createMockExecution({
321 |       nodeData: {
322 |         'HTTP Request': createNodeData(3),
323 |       },
324 |     });
325 | 
326 |     const options: ExecutionFilterOptions = {
327 |       mode: 'filtered',
328 |       itemsLimit: 5,
329 |     };
330 | 
331 |     const result = filterExecutionData(execution, options);
332 |     const nodeData = result.nodes?.['HTTP Request'];
333 | 
334 |     expect(nodeData?.data?.metadata.itemsShown).toBe(3);
335 |     expect(nodeData?.data?.metadata.truncated).toBe(false);
336 |   });
337 | 
338 |   it('should include input data when requested', () => {
339 |     const execution = createMockExecution({
340 |       nodeData: {
341 |         'HTTP Request': [
342 |           {
343 |             startTime: Date.now(),
344 |             executionTime: 100,
345 |             inputData: [[{ json: { input: 'test' } }]],
346 |             data: {
347 |               main: [[{ json: { output: 'result' } }]],
348 |             },
349 |           },
350 |         ],
351 |       },
352 |     });
353 | 
354 |     const options: ExecutionFilterOptions = {
355 |       mode: 'filtered',
356 |       includeInputData: true,
357 |     };
358 | 
359 |     const result = filterExecutionData(execution, options);
360 |     const nodeData = result.nodes?.['HTTP Request'];
361 | 
362 |     expect(nodeData?.data?.input).toBeDefined();
363 |     expect(nodeData?.data?.input?.[0]?.[0]?.json?.input).toBe('test');
364 |   });
365 | 
366 |   it('should not include input data by default', () => {
367 |     const execution = createMockExecution({
368 |       nodeData: {
369 |         'HTTP Request': [
370 |           {
371 |             startTime: Date.now(),
372 |             executionTime: 100,
373 |             inputData: [[{ json: { input: 'test' } }]],
374 |             data: {
375 |               main: [[{ json: { output: 'result' } }]],
376 |             },
377 |           },
378 |         ],
379 |       },
380 |     });
381 | 
382 |     const options: ExecutionFilterOptions = {
383 |       mode: 'filtered',
384 |     };
385 | 
386 |     const result = filterExecutionData(execution, options);
387 |     const nodeData = result.nodes?.['HTTP Request'];
388 | 
389 |     expect(nodeData?.data?.input).toBeUndefined();
390 |   });
391 | });
392 | 
393 | /**
394 |  * Mode Tests
395 |  */
396 | describe('ExecutionProcessor - Modes', () => {
397 |   it('should handle preview mode', () => {
398 |     const execution = createMockExecution({
399 |       nodeData: {
400 |         'HTTP Request': createNodeData(50),
401 |       },
402 |     });
403 | 
404 |     const result = filterExecutionData(execution, { mode: 'preview' });
405 | 
406 |     expect(result.mode).toBe('preview');
407 |     expect(result.preview).toBeDefined();
408 |     expect(result.recommendation).toBeDefined();
409 |     expect(result.nodes).toBeUndefined();
410 |   });
411 | 
412 |   it('should handle summary mode', () => {
413 |     const execution = createMockExecution({
414 |       nodeData: {
415 |         'HTTP Request': createNodeData(50),
416 |       },
417 |     });
418 | 
419 |     const result = filterExecutionData(execution, { mode: 'summary' });
420 | 
421 |     expect(result.mode).toBe('summary');
422 |     expect(result.summary).toBeDefined();
423 |     expect(result.nodes).toBeDefined();
424 |     expect(result.nodes?.['HTTP Request']?.data?.metadata.itemsShown).toBe(2);
425 |   });
426 | 
427 |   it('should handle filtered mode', () => {
428 |     const execution = createMockExecution({
429 |       nodeData: {
430 |         'HTTP Request': createNodeData(50),
431 |       },
432 |     });
433 | 
434 |     const result = filterExecutionData(execution, {
435 |       mode: 'filtered',
436 |       itemsLimit: 5,
437 |     });
438 | 
439 |     expect(result.mode).toBe('filtered');
440 |     expect(result.summary).toBeDefined();
441 |     expect(result.nodes?.['HTTP Request']?.data?.metadata.itemsShown).toBe(5);
442 |   });
443 | 
444 |   it('should handle full mode', () => {
445 |     const execution = createMockExecution({
446 |       nodeData: {
447 |         'HTTP Request': createNodeData(50),
448 |       },
449 |     });
450 | 
451 |     const result = filterExecutionData(execution, { mode: 'full' });
452 | 
453 |     expect(result.mode).toBe('full');
454 |     expect(result.nodes?.['HTTP Request']?.data?.metadata.itemsShown).toBe(50);
455 |     expect(result.nodes?.['HTTP Request']?.data?.metadata.truncated).toBe(false);
456 |   });
457 | });
458 | 
459 | /**
460 |  * Edge Cases
461 |  */
462 | describe('ExecutionProcessor - Edge Cases', () => {
463 |   it('should handle execution with no data', () => {
464 |     const execution: Execution = {
465 |       id: 'test-1',
466 |       workflowId: 'workflow-1',
467 |       status: ExecutionStatus.SUCCESS,
468 |       mode: 'manual',
469 |       finished: true,
470 |       startedAt: '2024-01-01T10:00:00.000Z',
471 |       stoppedAt: '2024-01-01T10:00:05.000Z',
472 |     };
473 | 
474 |     const result = filterExecutionData(execution, { mode: 'summary' });
475 | 
476 |     expect(result.summary?.totalNodes).toBe(0);
477 |     expect(result.summary?.executedNodes).toBe(0);
478 |   });
479 | 
480 |   it('should handle execution with error', () => {
481 |     const execution = createMockExecution({
482 |       nodeData: {
483 |         'HTTP Request': createNodeData(5),
484 |       },
485 |       hasError: true,
486 |     });
487 | 
488 |     const result = filterExecutionData(execution, { mode: 'summary' });
489 | 
490 |     expect(result.error).toBeDefined();
491 |   });
492 | 
493 |   it('should handle empty node data arrays', () => {
494 |     const execution = createMockExecution({
495 |       nodeData: {
496 |         'HTTP Request': [],
497 |       },
498 |     });
499 | 
500 |     const result = filterExecutionData(execution, { mode: 'summary' });
501 | 
502 |     expect(result.nodes?.['HTTP Request']).toBeDefined();
503 |     expect(result.nodes?.['HTTP Request'].itemsOutput).toBe(0);
504 |   });
505 | 
506 |   it('should handle nested data structures', () => {
507 |     const execution = createMockExecution({
508 |       nodeData: {
509 |         'HTTP Request': [
510 |           {
511 |             startTime: Date.now(),
512 |             executionTime: 100,
513 |             data: {
514 |               main: [[{
515 |                 json: {
516 |                   deeply: {
517 |                     nested: {
518 |                       structure: {
519 |                         value: 'test',
520 |                         array: [1, 2, 3],
521 |                       },
522 |                     },
523 |                   },
524 |                 },
525 |               }]],
526 |             },
527 |           },
528 |         ],
529 |       },
530 |     });
531 | 
532 |     const { preview } = generatePreview(execution);
533 |     const structure = preview.nodes['HTTP Request'].dataStructure;
534 | 
535 |     expect(structure.json.deeply).toBeDefined();
536 |     expect(typeof structure.json.deeply).toBe('object');
537 |   });
538 | 
539 |   it('should calculate duration correctly', () => {
540 |     const execution = createMockExecution({
541 |       nodeData: {
542 |         'HTTP Request': createNodeData(5),
543 |       },
544 |     });
545 | 
546 |     const result = filterExecutionData(execution, { mode: 'summary' });
547 | 
548 |     expect(result.duration).toBe(5000); // 5 seconds
549 |   });
550 | 
551 |   it('should handle execution without stop time', () => {
552 |     const execution: Execution = {
553 |       id: 'test-1',
554 |       workflowId: 'workflow-1',
555 |       status: ExecutionStatus.WAITING,
556 |       mode: 'manual',
557 |       finished: false,
558 |       startedAt: '2024-01-01T10:00:00.000Z',
559 |       data: {
560 |         resultData: {
561 |           runData: {},
562 |         },
563 |       },
564 |     };
565 | 
566 |     const result = filterExecutionData(execution, { mode: 'summary' });
567 | 
568 |     expect(result.duration).toBeUndefined();
569 |     expect(result.finished).toBe(false);
570 |   });
571 | });
572 | 
573 | /**
574 |  * processExecution Tests
575 |  */
576 | describe('ExecutionProcessor - processExecution', () => {
577 |   it('should return original execution when no options provided', () => {
578 |     const execution = createMockExecution({
579 |       nodeData: {
580 |         'HTTP Request': createNodeData(5),
581 |       },
582 |     });
583 | 
584 |     const result = processExecution(execution, {});
585 | 
586 |     expect(result).toBe(execution);
587 |   });
588 | 
589 |   it('should process when mode is specified', () => {
590 |     const execution = createMockExecution({
591 |       nodeData: {
592 |         'HTTP Request': createNodeData(5),
593 |       },
594 |     });
595 | 
596 |     const result = processExecution(execution, { mode: 'preview' });
597 | 
598 |     expect(result).not.toBe(execution);
599 |     expect((result as any).mode).toBe('preview');
600 |   });
601 | 
602 |   it('should process when filtering options are provided', () => {
603 |     const execution = createMockExecution({
604 |       nodeData: {
605 |         'HTTP Request': createNodeData(5),
606 |         'Filter': createNodeData(3),
607 |       },
608 |     });
609 | 
610 |     const result = processExecution(execution, { nodeNames: ['HTTP Request'] });
611 | 
612 |     expect(result).not.toBe(execution);
613 |     expect((result as any).nodes).toHaveProperty('HTTP Request');
614 |     expect((result as any).nodes).not.toHaveProperty('Filter');
615 |   });
616 | });
617 | 
618 | /**
619 |  * Summary Statistics Tests
620 |  */
621 | describe('ExecutionProcessor - Summary Statistics', () => {
622 |   it('should calculate hasMoreData correctly', () => {
623 |     const execution = createMockExecution({
624 |       nodeData: {
625 |         'HTTP Request': createNodeData(50),
626 |       },
627 |     });
628 | 
629 |     const result = filterExecutionData(execution, {
630 |       mode: 'summary',
631 |       itemsLimit: 2,
632 |     });
633 | 
634 |     expect(result.summary?.hasMoreData).toBe(true);
635 |   });
636 | 
637 |   it('should set hasMoreData to false when all data is included', () => {
638 |     const execution = createMockExecution({
639 |       nodeData: {
640 |         'HTTP Request': createNodeData(2),
641 |       },
642 |     });
643 | 
644 |     const result = filterExecutionData(execution, {
645 |       mode: 'summary',
646 |       itemsLimit: 5,
647 |     });
648 | 
649 |     expect(result.summary?.hasMoreData).toBe(false);
650 |   });
651 | 
652 |   it('should count total items correctly across multiple nodes', () => {
653 |     const execution = createMockExecution({
654 |       nodeData: {
655 |         'HTTP Request': createNodeData(10),
656 |         'Filter': createNodeData(5),
657 |         'Set': createNodeData(3),
658 |       },
659 |     });
660 | 
661 |     const result = filterExecutionData(execution, { mode: 'summary' });
662 | 
663 |     expect(result.summary?.totalItems).toBe(18);
664 |   });
665 | });
666 | 
```

--------------------------------------------------------------------------------
/tests/integration/database/template-node-configs.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, it, expect, beforeEach, afterEach } from 'vitest';
  2 | import Database from 'better-sqlite3';
  3 | import { DatabaseAdapter, createDatabaseAdapter } from '../../../src/database/database-adapter';
  4 | import fs from 'fs';
  5 | import path from 'path';
  6 | 
  7 | /**
  8 |  * Integration tests for template_node_configs table
  9 |  * Testing database schema, migrations, and data operations
 10 |  */
 11 | 
 12 | describe('Template Node Configs Database Integration', () => {
 13 |   let db: DatabaseAdapter;
 14 |   let dbPath: string;
 15 | 
 16 |   beforeEach(async () => {
 17 |     // Create temporary database
 18 |     dbPath = ':memory:';
 19 |     db = await createDatabaseAdapter(dbPath);
 20 | 
 21 |     // Apply schema
 22 |     const schemaPath = path.join(__dirname, '../../../src/database/schema.sql');
 23 |     const schema = fs.readFileSync(schemaPath, 'utf-8');
 24 |     db.exec(schema);
 25 | 
 26 |     // Apply migration
 27 |     const migrationPath = path.join(__dirname, '../../../src/database/migrations/add-template-node-configs.sql');
 28 |     const migration = fs.readFileSync(migrationPath, 'utf-8');
 29 |     db.exec(migration);
 30 | 
 31 |     // Insert test templates with id 1-1000 to satisfy foreign key constraints
 32 |     // Tests insert configs with various template_id values, so we pre-create many templates
 33 |     const stmt = db.prepare(`
 34 |       INSERT INTO templates (
 35 |         id, workflow_id, name, description, views,
 36 |         nodes_used, created_at, updated_at
 37 |       ) VALUES (?, ?, ?, ?, ?, ?, datetime('now'), datetime('now'))
 38 |     `);
 39 |     for (let i = 1; i <= 1000; i++) {
 40 |       stmt.run(i, i, `Test Template ${i}`, 'Test template for node configs', 100, '[]');
 41 |     }
 42 |   });
 43 | 
 44 |   afterEach(() => {
 45 |     if ('close' in db && typeof db.close === 'function') {
 46 |       db.close();
 47 |     }
 48 |   });
 49 | 
 50 |   describe('Schema Validation', () => {
 51 |     it('should create template_node_configs table', () => {
 52 |       const tableExists = db.prepare(`
 53 |         SELECT name FROM sqlite_master
 54 |         WHERE type='table' AND name='template_node_configs'
 55 |       `).get();
 56 | 
 57 |       expect(tableExists).toBeDefined();
 58 |       expect(tableExists).toHaveProperty('name', 'template_node_configs');
 59 |     });
 60 | 
 61 |     it('should have all required columns', () => {
 62 |       const columns = db.prepare(`PRAGMA table_info(template_node_configs)`).all() as any[];
 63 | 
 64 |       const columnNames = columns.map(col => col.name);
 65 |       expect(columnNames).toContain('id');
 66 |       expect(columnNames).toContain('node_type');
 67 |       expect(columnNames).toContain('template_id');
 68 |       expect(columnNames).toContain('template_name');
 69 |       expect(columnNames).toContain('template_views');
 70 |       expect(columnNames).toContain('node_name');
 71 |       expect(columnNames).toContain('parameters_json');
 72 |       expect(columnNames).toContain('credentials_json');
 73 |       expect(columnNames).toContain('has_credentials');
 74 |       expect(columnNames).toContain('has_expressions');
 75 |       expect(columnNames).toContain('complexity');
 76 |       expect(columnNames).toContain('use_cases');
 77 |       expect(columnNames).toContain('rank');
 78 |       expect(columnNames).toContain('created_at');
 79 |     });
 80 | 
 81 |     it('should have correct column types and constraints', () => {
 82 |       const columns = db.prepare(`PRAGMA table_info(template_node_configs)`).all() as any[];
 83 | 
 84 |       const idColumn = columns.find(col => col.name === 'id');
 85 |       expect(idColumn.pk).toBe(1); // Primary key
 86 | 
 87 |       const nodeTypeColumn = columns.find(col => col.name === 'node_type');
 88 |       expect(nodeTypeColumn.notnull).toBe(1); // NOT NULL
 89 | 
 90 |       const parametersJsonColumn = columns.find(col => col.name === 'parameters_json');
 91 |       expect(parametersJsonColumn.notnull).toBe(1); // NOT NULL
 92 |     });
 93 | 
 94 |     it('should have complexity CHECK constraint', () => {
 95 |       // Try to insert invalid complexity
 96 |       expect(() => {
 97 |         db.prepare(`
 98 |           INSERT INTO template_node_configs (
 99 |             node_type, template_id, template_name, template_views,
100 |             node_name, parameters_json, complexity
101 |           ) VALUES (?, ?, ?, ?, ?, ?, ?)
102 |         `).run(
103 |           'n8n-nodes-base.test',
104 |           1,
105 |           'Test Template',
106 |           100,
107 |           'Test Node',
108 |           '{}',
109 |           'invalid' // Should fail CHECK constraint
110 |         );
111 |       }).toThrow();
112 |     });
113 | 
114 |     it('should accept valid complexity values', () => {
115 |       const validComplexities = ['simple', 'medium', 'complex'];
116 | 
117 |       validComplexities.forEach((complexity, index) => {
118 |         expect(() => {
119 |           db.prepare(`
120 |             INSERT INTO template_node_configs (
121 |               node_type, template_id, template_name, template_views,
122 |               node_name, parameters_json, complexity
123 |             ) VALUES (?, ?, ?, ?, ?, ?, ?)
124 |           `).run(
125 |             'n8n-nodes-base.test',
126 |             index + 1,
127 |             'Test Template',
128 |             100,
129 |             'Test Node',
130 |             '{}',
131 |             complexity
132 |           );
133 |         }).not.toThrow();
134 |       });
135 | 
136 |       const count = db.prepare('SELECT COUNT(*) as count FROM template_node_configs').get() as any;
137 |       expect(count.count).toBe(3);
138 |     });
139 |   });
140 | 
141 |   describe('Indexes', () => {
142 |     it('should create idx_config_node_type_rank index', () => {
143 |       const indexes = db.prepare(`
144 |         SELECT name FROM sqlite_master
145 |         WHERE type='index' AND tbl_name='template_node_configs'
146 |       `).all() as any[];
147 | 
148 |       const indexNames = indexes.map(idx => idx.name);
149 |       expect(indexNames).toContain('idx_config_node_type_rank');
150 |     });
151 | 
152 |     it('should create idx_config_complexity index', () => {
153 |       const indexes = db.prepare(`
154 |         SELECT name FROM sqlite_master
155 |         WHERE type='index' AND tbl_name='template_node_configs'
156 |       `).all() as any[];
157 | 
158 |       const indexNames = indexes.map(idx => idx.name);
159 |       expect(indexNames).toContain('idx_config_complexity');
160 |     });
161 | 
162 |     it('should create idx_config_auth index', () => {
163 |       const indexes = db.prepare(`
164 |         SELECT name FROM sqlite_master
165 |         WHERE type='index' AND tbl_name='template_node_configs'
166 |       `).all() as any[];
167 | 
168 |       const indexNames = indexes.map(idx => idx.name);
169 |       expect(indexNames).toContain('idx_config_auth');
170 |     });
171 |   });
172 | 
173 |   describe('View: ranked_node_configs', () => {
174 |     it('should create ranked_node_configs view', () => {
175 |       const viewExists = db.prepare(`
176 |         SELECT name FROM sqlite_master
177 |         WHERE type='view' AND name='ranked_node_configs'
178 |       `).get();
179 | 
180 |       expect(viewExists).toBeDefined();
181 |       expect(viewExists).toHaveProperty('name', 'ranked_node_configs');
182 |     });
183 | 
184 |     it('should return only top 5 ranked configs per node type', () => {
185 |       // Insert 10 configs for same node type with different ranks
186 |       for (let i = 1; i <= 10; i++) {
187 |         db.prepare(`
188 |           INSERT INTO template_node_configs (
189 |             node_type, template_id, template_name, template_views,
190 |             node_name, parameters_json, rank
191 |           ) VALUES (?, ?, ?, ?, ?, ?, ?)
192 |         `).run(
193 |           'n8n-nodes-base.httpRequest',
194 |           i,
195 |           `Template ${i}`,
196 |           1000 - (i * 50), // Decreasing views
197 |           'HTTP Request',
198 |           '{}',
199 |           i // Rank 1-10
200 |         );
201 |       }
202 | 
203 |       const rankedConfigs = db.prepare('SELECT * FROM ranked_node_configs').all() as any[];
204 | 
205 |       // Should only return rank 1-5
206 |       expect(rankedConfigs).toHaveLength(5);
207 |       expect(Math.max(...rankedConfigs.map(c => c.rank))).toBe(5);
208 |       expect(Math.min(...rankedConfigs.map(c => c.rank))).toBe(1);
209 |     });
210 | 
211 |     it('should order by node_type and rank', () => {
212 |       // Insert configs for multiple node types
213 |       const configs = [
214 |         { nodeType: 'n8n-nodes-base.webhook', rank: 2 },
215 |         { nodeType: 'n8n-nodes-base.webhook', rank: 1 },
216 |         { nodeType: 'n8n-nodes-base.httpRequest', rank: 2 },
217 |         { nodeType: 'n8n-nodes-base.httpRequest', rank: 1 },
218 |       ];
219 | 
220 |       configs.forEach((config, index) => {
221 |         db.prepare(`
222 |           INSERT INTO template_node_configs (
223 |             node_type, template_id, template_name, template_views,
224 |             node_name, parameters_json, rank
225 |           ) VALUES (?, ?, ?, ?, ?, ?, ?)
226 |         `).run(
227 |           config.nodeType,
228 |           index + 1,
229 |           `Template ${index}`,
230 |           100,
231 |           'Node',
232 |           '{}',
233 |           config.rank
234 |         );
235 |       });
236 | 
237 |       const rankedConfigs = db.prepare('SELECT * FROM ranked_node_configs ORDER BY node_type, rank').all() as any[];
238 | 
239 |       // First two should be httpRequest rank 1, 2
240 |       expect(rankedConfigs[0].node_type).toBe('n8n-nodes-base.httpRequest');
241 |       expect(rankedConfigs[0].rank).toBe(1);
242 |       expect(rankedConfigs[1].node_type).toBe('n8n-nodes-base.httpRequest');
243 |       expect(rankedConfigs[1].rank).toBe(2);
244 | 
245 |       // Last two should be webhook rank 1, 2
246 |       expect(rankedConfigs[2].node_type).toBe('n8n-nodes-base.webhook');
247 |       expect(rankedConfigs[2].rank).toBe(1);
248 |       expect(rankedConfigs[3].node_type).toBe('n8n-nodes-base.webhook');
249 |       expect(rankedConfigs[3].rank).toBe(2);
250 |     });
251 |   });
252 | 
253 |   describe('Foreign Key Constraints', () => {
254 |     beforeEach(() => {
255 |       // Enable foreign keys
256 |       db.exec('PRAGMA foreign_keys = ON');
257 |       // Note: Templates are already created in the main beforeEach
258 |     });
259 | 
260 |     it('should allow inserting config with valid template_id', () => {
261 |       expect(() => {
262 |         db.prepare(`
263 |           INSERT INTO template_node_configs (
264 |             node_type, template_id, template_name, template_views,
265 |             node_name, parameters_json
266 |           ) VALUES (?, ?, ?, ?, ?, ?)
267 |         `).run(
268 |           'n8n-nodes-base.test',
269 |           1, // Valid template_id
270 |           'Test Template',
271 |           100,
272 |           'Test Node',
273 |           '{}'
274 |         );
275 |       }).not.toThrow();
276 |     });
277 | 
278 |     it('should cascade delete configs when template is deleted', () => {
279 |       // Insert config
280 |       db.prepare(`
281 |         INSERT INTO template_node_configs (
282 |           node_type, template_id, template_name, template_views,
283 |           node_name, parameters_json
284 |         ) VALUES (?, ?, ?, ?, ?, ?)
285 |       `).run(
286 |         'n8n-nodes-base.test',
287 |         1,
288 |         'Test Template',
289 |         100,
290 |         'Test Node',
291 |         '{}'
292 |       );
293 | 
294 |       // Verify config exists
295 |       let configs = db.prepare('SELECT * FROM template_node_configs WHERE template_id = ?').all(1) as any[];
296 |       expect(configs).toHaveLength(1);
297 | 
298 |       // Delete template
299 |       db.prepare('DELETE FROM templates WHERE id = ?').run(1);
300 | 
301 |       // Verify config is deleted (CASCADE)
302 |       configs = db.prepare('SELECT * FROM template_node_configs WHERE template_id = ?').all(1) as any[];
303 |       expect(configs).toHaveLength(0);
304 |     });
305 |   });
306 | 
307 |   describe('Data Operations', () => {
308 |     it('should insert and retrieve config with all fields', () => {
309 |       const testConfig = {
310 |         node_type: 'n8n-nodes-base.webhook',
311 |         template_id: 1,
312 |         template_name: 'Webhook Template',
313 |         template_views: 2000,
314 |         node_name: 'Webhook Trigger',
315 |         parameters_json: JSON.stringify({
316 |           httpMethod: 'POST',
317 |           path: 'webhook-test',
318 |           responseMode: 'lastNode'
319 |         }),
320 |         credentials_json: JSON.stringify({
321 |           webhookAuth: { id: '1', name: 'Webhook Auth' }
322 |         }),
323 |         has_credentials: 1,
324 |         has_expressions: 1,
325 |         complexity: 'medium',
326 |         use_cases: JSON.stringify(['webhook processing', 'automation triggers']),
327 |         rank: 1
328 |       };
329 | 
330 |       db.prepare(`
331 |         INSERT INTO template_node_configs (
332 |           node_type, template_id, template_name, template_views,
333 |           node_name, parameters_json, credentials_json,
334 |           has_credentials, has_expressions, complexity, use_cases, rank
335 |         ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
336 |       `).run(...Object.values(testConfig));
337 | 
338 |       const retrieved = db.prepare('SELECT * FROM template_node_configs WHERE id = 1').get() as any;
339 | 
340 |       expect(retrieved.node_type).toBe(testConfig.node_type);
341 |       expect(retrieved.template_id).toBe(testConfig.template_id);
342 |       expect(retrieved.template_name).toBe(testConfig.template_name);
343 |       expect(retrieved.template_views).toBe(testConfig.template_views);
344 |       expect(retrieved.node_name).toBe(testConfig.node_name);
345 |       expect(retrieved.parameters_json).toBe(testConfig.parameters_json);
346 |       expect(retrieved.credentials_json).toBe(testConfig.credentials_json);
347 |       expect(retrieved.has_credentials).toBe(testConfig.has_credentials);
348 |       expect(retrieved.has_expressions).toBe(testConfig.has_expressions);
349 |       expect(retrieved.complexity).toBe(testConfig.complexity);
350 |       expect(retrieved.use_cases).toBe(testConfig.use_cases);
351 |       expect(retrieved.rank).toBe(testConfig.rank);
352 |       expect(retrieved.created_at).toBeDefined();
353 |     });
354 | 
355 |     it('should handle nullable fields correctly', () => {
356 |       db.prepare(`
357 |         INSERT INTO template_node_configs (
358 |           node_type, template_id, template_name, template_views,
359 |           node_name, parameters_json
360 |         ) VALUES (?, ?, ?, ?, ?, ?)
361 |       `).run(
362 |         'n8n-nodes-base.test',
363 |         1,
364 |         'Test',
365 |         100,
366 |         'Node',
367 |         '{}'
368 |       );
369 | 
370 |       const retrieved = db.prepare('SELECT * FROM template_node_configs WHERE id = 1').get() as any;
371 | 
372 |       expect(retrieved.credentials_json).toBeNull();
373 |       expect(retrieved.has_credentials).toBe(0); // Default value
374 |       expect(retrieved.has_expressions).toBe(0); // Default value
375 |       expect(retrieved.rank).toBe(0); // Default value
376 |     });
377 | 
378 |     it('should update rank values', () => {
379 |       // Insert multiple configs
380 |       for (let i = 1; i <= 3; i++) {
381 |         db.prepare(`
382 |           INSERT INTO template_node_configs (
383 |             node_type, template_id, template_name, template_views,
384 |             node_name, parameters_json, rank
385 |           ) VALUES (?, ?, ?, ?, ?, ?, ?)
386 |         `).run(
387 |           'n8n-nodes-base.test',
388 |           i,
389 |           'Template',
390 |           100,
391 |           'Node',
392 |           '{}',
393 |           0 // Initial rank
394 |         );
395 |       }
396 | 
397 |       // Update ranks
398 |       db.exec(`
399 |         UPDATE template_node_configs
400 |         SET rank = (
401 |           SELECT COUNT(*) + 1
402 |           FROM template_node_configs AS t2
403 |           WHERE t2.node_type = template_node_configs.node_type
404 |             AND t2.template_views > template_node_configs.template_views
405 |         )
406 |       `);
407 | 
408 |       const configs = db.prepare('SELECT * FROM template_node_configs ORDER BY rank').all() as any[];
409 | 
410 |       // All should have same rank (same views)
411 |       expect(configs.every(c => c.rank === 1)).toBe(true);
412 |     });
413 | 
414 |     it('should delete configs with rank > 10', () => {
415 |       // Insert 15 configs with different ranks
416 |       for (let i = 1; i <= 15; i++) {
417 |         db.prepare(`
418 |           INSERT INTO template_node_configs (
419 |             node_type, template_id, template_name, template_views,
420 |             node_name, parameters_json, rank
421 |           ) VALUES (?, ?, ?, ?, ?, ?, ?)
422 |         `).run(
423 |           'n8n-nodes-base.test',
424 |           i,
425 |           'Template',
426 |           100,
427 |           'Node',
428 |           '{}',
429 |           i // Rank 1-15
430 |         );
431 |       }
432 | 
433 |       // Delete configs with rank > 10
434 |       db.exec(`
435 |         DELETE FROM template_node_configs
436 |         WHERE id NOT IN (
437 |           SELECT id FROM template_node_configs
438 |           WHERE rank <= 10
439 |           ORDER BY node_type, rank
440 |         )
441 |       `);
442 | 
443 |       const remaining = db.prepare('SELECT * FROM template_node_configs').all() as any[];
444 | 
445 |       expect(remaining).toHaveLength(10);
446 |       expect(Math.max(...remaining.map(c => c.rank))).toBe(10);
447 |     });
448 |   });
449 | 
450 |   describe('Query Performance', () => {
451 |     beforeEach(() => {
452 |       // Insert 1000 configs for performance testing
453 |       const stmt = db.prepare(`
454 |         INSERT INTO template_node_configs (
455 |           node_type, template_id, template_name, template_views,
456 |           node_name, parameters_json, rank
457 |         ) VALUES (?, ?, ?, ?, ?, ?, ?)
458 |       `);
459 | 
460 |       const nodeTypes = [
461 |         'n8n-nodes-base.httpRequest',
462 |         'n8n-nodes-base.webhook',
463 |         'n8n-nodes-base.slack',
464 |         'n8n-nodes-base.googleSheets',
465 |         'n8n-nodes-base.code'
466 |       ];
467 | 
468 |       for (let i = 1; i <= 1000; i++) {
469 |         const nodeType = nodeTypes[i % nodeTypes.length];
470 |         stmt.run(
471 |           nodeType,
472 |           i,
473 |           `Template ${i}`,
474 |           Math.floor(Math.random() * 10000),
475 |           'Node',
476 |           '{}',
477 |           (i % 10) + 1 // Rank 1-10
478 |         );
479 |       }
480 |     });
481 | 
482 |     it('should query by node_type and rank efficiently', () => {
483 |       const start = Date.now();
484 |       const results = db.prepare(`
485 |         SELECT * FROM template_node_configs
486 |         WHERE node_type = ?
487 |         ORDER BY rank
488 |         LIMIT 3
489 |       `).all('n8n-nodes-base.httpRequest') as any[];
490 |       const duration = Date.now() - start;
491 | 
492 |       expect(results.length).toBeGreaterThan(0);
493 |       expect(duration).toBeLessThan(10); // Should be very fast with index
494 |     });
495 | 
496 |     it('should filter by complexity efficiently', () => {
497 |       // First set some complexity values
498 |       db.exec(`UPDATE template_node_configs SET complexity = 'simple' WHERE id % 3 = 0`);
499 |       db.exec(`UPDATE template_node_configs SET complexity = 'medium' WHERE id % 3 = 1`);
500 |       db.exec(`UPDATE template_node_configs SET complexity = 'complex' WHERE id % 3 = 2`);
501 | 
502 |       const start = Date.now();
503 |       const results = db.prepare(`
504 |         SELECT * FROM template_node_configs
505 |         WHERE node_type = ? AND complexity = ?
506 |         ORDER BY rank
507 |         LIMIT 5
508 |       `).all('n8n-nodes-base.webhook', 'simple') as any[];
509 |       const duration = Date.now() - start;
510 | 
511 |       expect(duration).toBeLessThan(10); // Should be fast with index
512 |     });
513 |   });
514 | 
515 |   describe('Migration Idempotency', () => {
516 |     it('should be safe to run migration multiple times', () => {
517 |       const migrationPath = path.join(__dirname, '../../../src/database/migrations/add-template-node-configs.sql');
518 |       const migration = fs.readFileSync(migrationPath, 'utf-8');
519 | 
520 |       // Run migration again
521 |       expect(() => {
522 |         db.exec(migration);
523 |       }).not.toThrow();
524 | 
525 |       // Table should still exist
526 |       const tableExists = db.prepare(`
527 |         SELECT name FROM sqlite_master
528 |         WHERE type='table' AND name='template_node_configs'
529 |       `).get();
530 | 
531 |       expect(tableExists).toBeDefined();
532 |     });
533 |   });
534 | });
535 | 
```

--------------------------------------------------------------------------------
/tests/unit/services/config-validator-node-specific.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, it, expect, vi, beforeEach } from 'vitest';
  2 | import { ConfigValidator } from '@/services/config-validator';
  3 | import type { ValidationResult, ValidationError, ValidationWarning } from '@/services/config-validator';
  4 | 
  5 | // Mock the database
  6 | vi.mock('better-sqlite3');
  7 | 
  8 | describe('ConfigValidator - Node-Specific Validation', () => {
  9 |   beforeEach(() => {
 10 |     vi.clearAllMocks();
 11 |   });
 12 | 
 13 |   describe('HTTP Request node validation', () => {
 14 |     it('should perform HTTP Request specific validation', () => {
 15 |       const nodeType = 'nodes-base.httpRequest';
 16 |       const config = {
 17 |         method: 'POST',
 18 |         url: 'invalid-url', // Missing protocol
 19 |         sendBody: false
 20 |       };
 21 |       const properties = [
 22 |         { name: 'method', type: 'options' },
 23 |         { name: 'url', type: 'string' },
 24 |         { name: 'sendBody', type: 'boolean' }
 25 |       ];
 26 | 
 27 |       const result = ConfigValidator.validate(nodeType, config, properties);
 28 | 
 29 |       expect(result.valid).toBe(false);
 30 |       expect(result.errors).toHaveLength(1);
 31 |       expect(result.errors[0]).toMatchObject({
 32 |         type: 'invalid_value',
 33 |         property: 'url',
 34 |         message: 'URL must start with http:// or https://'
 35 |       });
 36 |       expect(result.warnings).toHaveLength(1);
 37 |       expect(result.warnings[0]).toMatchObject({
 38 |         type: 'missing_common',
 39 |         property: 'sendBody',
 40 |         message: 'POST requests typically send a body'
 41 |       });
 42 |       expect(result.autofix).toMatchObject({
 43 |         sendBody: true,
 44 |         contentType: 'json'
 45 |       });
 46 |     });
 47 | 
 48 |     it('should validate HTTP Request with authentication in API URLs', () => {
 49 |       const nodeType = 'nodes-base.httpRequest';
 50 |       const config = {
 51 |         method: 'GET',
 52 |         url: 'https://api.github.com/user/repos',
 53 |         authentication: 'none'
 54 |       };
 55 |       const properties = [
 56 |         { name: 'method', type: 'options' },
 57 |         { name: 'url', type: 'string' },
 58 |         { name: 'authentication', type: 'options' }
 59 |       ];
 60 | 
 61 |       const result = ConfigValidator.validate(nodeType, config, properties);
 62 | 
 63 |       expect(result.warnings.some(w => 
 64 |         w.type === 'security' && 
 65 |         w.message.includes('API endpoints typically require authentication')
 66 |       )).toBe(true);
 67 |     });
 68 | 
 69 |     it('should validate JSON in HTTP Request body', () => {
 70 |       const nodeType = 'nodes-base.httpRequest';
 71 |       const config = {
 72 |         method: 'POST',
 73 |         url: 'https://api.example.com',
 74 |         contentType: 'json',
 75 |         body: '{"invalid": json}' // Invalid JSON
 76 |       };
 77 |       const properties = [
 78 |         { name: 'method', type: 'options' },
 79 |         { name: 'url', type: 'string' },
 80 |         { name: 'contentType', type: 'options' },
 81 |         { name: 'body', type: 'string' }
 82 |       ];
 83 | 
 84 |       const result = ConfigValidator.validate(nodeType, config, properties);
 85 | 
 86 |       expect(result.errors.some(e => 
 87 |         e.property === 'body' && 
 88 |         e.message.includes('Invalid JSON')
 89 |       ));
 90 |     });
 91 | 
 92 |     it('should handle webhook-specific validation', () => {
 93 |       const nodeType = 'nodes-base.webhook';
 94 |       const config = {
 95 |         httpMethod: 'GET',
 96 |         path: 'webhook-endpoint' // Missing leading slash
 97 |       };
 98 |       const properties = [
 99 |         { name: 'httpMethod', type: 'options' },
100 |         { name: 'path', type: 'string' }
101 |       ];
102 | 
103 |       const result = ConfigValidator.validate(nodeType, config, properties);
104 | 
105 |       expect(result.warnings.some(w => 
106 |         w.property === 'path' && 
107 |         w.message.includes('should start with /')
108 |       ));
109 |     });
110 |   });
111 | 
112 |   describe('Code node validation', () => {
113 |     it('should validate Code node configurations', () => {
114 |       const nodeType = 'nodes-base.code';
115 |       const config = {
116 |         language: 'javascript',
117 |         jsCode: '' // Empty code
118 |       };
119 |       const properties = [
120 |         { name: 'language', type: 'options' },
121 |         { name: 'jsCode', type: 'string' }
122 |       ];
123 | 
124 |       const result = ConfigValidator.validate(nodeType, config, properties);
125 | 
126 |       expect(result.valid).toBe(false);
127 |       expect(result.errors).toHaveLength(1);
128 |       expect(result.errors[0]).toMatchObject({
129 |         type: 'missing_required',
130 |         property: 'jsCode',
131 |         message: 'Code cannot be empty'
132 |       });
133 |     });
134 | 
135 |     it('should validate JavaScript syntax in Code node', () => {
136 |       const nodeType = 'nodes-base.code';
137 |       const config = {
138 |         language: 'javascript',
139 |         jsCode: `
140 |           const data = { foo: "bar" };
141 |           if (data.foo {  // Missing closing parenthesis
142 |             return [{json: data}];
143 |           }
144 |         `
145 |       };
146 |       const properties = [
147 |         { name: 'language', type: 'options' },
148 |         { name: 'jsCode', type: 'string' }
149 |       ];
150 | 
151 |       const result = ConfigValidator.validate(nodeType, config, properties);
152 | 
153 |       expect(result.errors.some(e => e.message.includes('Unbalanced')));
154 |       expect(result.warnings).toHaveLength(1);
155 |     });
156 | 
157 |     it('should validate n8n-specific patterns in Code node', () => {
158 |       const nodeType = 'nodes-base.code';
159 |       const config = {
160 |         language: 'javascript',
161 |         jsCode: `
162 |           // Process data without returning
163 |           const processedData = items.map(item => ({
164 |             ...item.json,
165 |             processed: true
166 |           }));
167 |           // No output provided
168 |         `
169 |       };
170 |       const properties = [
171 |         { name: 'language', type: 'options' },
172 |         { name: 'jsCode', type: 'string' }
173 |       ];
174 | 
175 |       const result = ConfigValidator.validate(nodeType, config, properties);
176 | 
177 |       // The warning should be about missing return statement
178 |       expect(result.warnings.some(w => w.type === 'missing_common' && w.message.includes('No return statement found'))).toBe(true);
179 |     });
180 | 
181 |     it('should handle empty code in Code node', () => {
182 |       const nodeType = 'nodes-base.code';
183 |       const config = {
184 |         language: 'javascript',
185 |         jsCode: '   \n  \t  \n   ' // Just whitespace
186 |       };
187 |       const properties = [
188 |         { name: 'language', type: 'options' },
189 |         { name: 'jsCode', type: 'string' }
190 |       ];
191 | 
192 |       const result = ConfigValidator.validate(nodeType, config, properties);
193 | 
194 |       expect(result.valid).toBe(false);
195 |       expect(result.errors.some(e => 
196 |         e.type === 'missing_required' && 
197 |         e.message.includes('Code cannot be empty')
198 |       )).toBe(true);
199 |     });
200 | 
201 |     it('should validate complex return patterns in Code node', () => {
202 |       const nodeType = 'nodes-base.code';
203 |       const config = {
204 |         language: 'javascript',
205 |         jsCode: `
206 |           return ["string1", "string2", "string3"];
207 |         `
208 |       };
209 |       const properties = [
210 |         { name: 'language', type: 'options' },
211 |         { name: 'jsCode', type: 'string' }
212 |       ];
213 | 
214 |       const result = ConfigValidator.validate(nodeType, config, properties);
215 | 
216 |       expect(result.warnings.some(w => 
217 |         w.type === 'invalid_value' && 
218 |         w.message.includes('Items must be objects with json property')
219 |       )).toBe(true);
220 |     });
221 | 
222 |     it('should validate Code node with $helpers usage', () => {
223 |       const nodeType = 'nodes-base.code';
224 |       const config = {
225 |         language: 'javascript',
226 |         jsCode: `
227 |           const workflow = $helpers.getWorkflowStaticData();
228 |           workflow.counter = (workflow.counter || 0) + 1;
229 |           return [{json: {count: workflow.counter}}];
230 |         `
231 |       };
232 |       const properties = [
233 |         { name: 'language', type: 'options' },
234 |         { name: 'jsCode', type: 'string' }
235 |       ];
236 | 
237 |       const result = ConfigValidator.validate(nodeType, config, properties);
238 | 
239 |       expect(result.warnings.some(w => 
240 |         w.type === 'best_practice' && 
241 |         w.message.includes('$helpers is only available in Code nodes')
242 |       )).toBe(true);
243 |     });
244 | 
245 |     it('should detect incorrect $helpers.getWorkflowStaticData usage', () => {
246 |       const nodeType = 'nodes-base.code';
247 |       const config = {
248 |         language: 'javascript',
249 |         jsCode: `
250 |           const data = $helpers.getWorkflowStaticData;  // Missing parentheses
251 |           return [{json: {data}}];
252 |         `
253 |       };
254 |       const properties = [
255 |         { name: 'language', type: 'options' },
256 |         { name: 'jsCode', type: 'string' }
257 |       ];
258 | 
259 |       const result = ConfigValidator.validate(nodeType, config, properties);
260 | 
261 |       expect(result.errors.some(e => 
262 |         e.type === 'invalid_value' && 
263 |         e.message.includes('getWorkflowStaticData requires parentheses')
264 |       )).toBe(true);
265 |     });
266 | 
267 |     it('should validate console.log usage', () => {
268 |       const nodeType = 'nodes-base.code';
269 |       const config = {
270 |         language: 'javascript',
271 |         jsCode: `
272 |           console.log('Debug info:', items);
273 |           return items;
274 |         `
275 |       };
276 |       const properties = [
277 |         { name: 'language', type: 'options' },
278 |         { name: 'jsCode', type: 'string' }
279 |       ];
280 | 
281 |       const result = ConfigValidator.validate(nodeType, config, properties);
282 | 
283 |       expect(result.warnings.some(w => 
284 |         w.type === 'best_practice' && 
285 |         w.message.includes('console.log output appears in n8n execution logs')
286 |       )).toBe(true);
287 |     });
288 | 
289 |     it('should validate $json usage warning', () => {
290 |       const nodeType = 'nodes-base.code';
291 |       const config = {
292 |         language: 'javascript',
293 |         jsCode: `
294 |           const data = $json.myField;
295 |           return [{json: {processed: data}}];
296 |         `
297 |       };
298 |       const properties = [
299 |         { name: 'language', type: 'options' },
300 |         { name: 'jsCode', type: 'string' }
301 |       ];
302 | 
303 |       const result = ConfigValidator.validate(nodeType, config, properties);
304 | 
305 |       expect(result.warnings.some(w => 
306 |         w.type === 'best_practice' && 
307 |         w.message.includes('$json only works in "Run Once for Each Item" mode')
308 |       )).toBe(true);
309 |     });
310 | 
311 |     it('should not warn about properties for Code nodes', () => {
312 |       const nodeType = 'nodes-base.code';
313 |       const config = {
314 |         language: 'javascript',
315 |         jsCode: 'return items;',
316 |         unusedProperty: 'this should not generate a warning for Code nodes'
317 |       };
318 |       const properties = [
319 |         { name: 'language', type: 'options' },
320 |         { name: 'jsCode', type: 'string' }
321 |       ];
322 | 
323 |       const result = ConfigValidator.validate(nodeType, config, properties);
324 | 
325 |       // Code nodes should skip the common issues check that warns about unused properties
326 |       expect(result.warnings.some(w => 
327 |         w.type === 'inefficient' && 
328 |         w.property === 'unusedProperty'
329 |       )).toBe(false);
330 |     });
331 | 
332 |     it('should validate crypto module usage', () => {
333 |       const nodeType = 'nodes-base.code';
334 |       const config = {
335 |         language: 'javascript',
336 |         jsCode: `
337 |           const uuid = crypto.randomUUID();
338 |           return [{json: {id: uuid}}];
339 |         `
340 |       };
341 |       const properties = [
342 |         { name: 'language', type: 'options' },
343 |         { name: 'jsCode', type: 'string' }
344 |       ];
345 | 
346 |       const result = ConfigValidator.validate(nodeType, config, properties);
347 | 
348 |       expect(result.warnings.some(w => 
349 |         w.type === 'invalid_value' && 
350 |         w.message.includes('Using crypto without require')
351 |       )).toBe(true);
352 |     });
353 | 
354 |     it('should suggest error handling for complex code', () => {
355 |       const nodeType = 'nodes-base.code';
356 |       const config = {
357 |         language: 'javascript',
358 |         jsCode: `
359 |           const apiUrl = items[0].json.url;
360 |           const response = await fetch(apiUrl);
361 |           const data = await response.json();
362 |           return [{json: data}];
363 |         `
364 |       };
365 |       const properties = [
366 |         { name: 'language', type: 'options' },
367 |         { name: 'jsCode', type: 'string' }
368 |       ];
369 | 
370 |       const result = ConfigValidator.validate(nodeType, config, properties);
371 | 
372 |       expect(result.suggestions.some(s => 
373 |         s.includes('Consider adding error handling')
374 |       ));
375 |     });
376 | 
377 |     it('should suggest error handling for non-trivial code', () => {
378 |       const nodeType = 'nodes-base.code';
379 |       const config = {
380 |         language: 'javascript',
381 |         jsCode: Array(10).fill('const x = 1;').join('\n') + '\nreturn items;'
382 |       };
383 |       const properties = [
384 |         { name: 'language', type: 'options' },
385 |         { name: 'jsCode', type: 'string' }
386 |       ];
387 | 
388 |       const result = ConfigValidator.validate(nodeType, config, properties);
389 | 
390 |       expect(result.suggestions.some(s => s.includes('error handling')));
391 |     });
392 | 
393 |     it('should validate async operations without await', () => {
394 |       const nodeType = 'nodes-base.code';
395 |       const config = {
396 |         language: 'javascript',
397 |         jsCode: `
398 |           const promise = fetch('https://api.example.com');
399 |           return [{json: {data: promise}}];
400 |         `
401 |       };
402 |       const properties = [
403 |         { name: 'language', type: 'options' },
404 |         { name: 'jsCode', type: 'string' }
405 |       ];
406 | 
407 |       const result = ConfigValidator.validate(nodeType, config, properties);
408 | 
409 |       expect(result.warnings.some(w => 
410 |         w.type === 'best_practice' && 
411 |         w.message.includes('Async operation without await')
412 |       )).toBe(true);
413 |     });
414 |   });
415 | 
416 |   describe('Python Code node validation', () => {
417 |     it('should validate Python code syntax', () => {
418 |       const nodeType = 'nodes-base.code';
419 |       const config = {
420 |         language: 'python',
421 |         pythonCode: `
422 | def process_data():
423 |   return [{"json": {"test": True}]  # Missing closing bracket
424 |         `
425 |       };
426 |       const properties = [
427 |         { name: 'language', type: 'options' },
428 |         { name: 'pythonCode', type: 'string' }
429 |       ];
430 | 
431 |       const result = ConfigValidator.validate(nodeType, config, properties);
432 | 
433 |       expect(result.errors.some(e => 
434 |         e.type === 'syntax_error' && 
435 |         e.message.includes('Unmatched bracket')
436 |       )).toBe(true);
437 |     });
438 | 
439 |     it('should detect mixed indentation in Python code', () => {
440 |       const nodeType = 'nodes-base.code';
441 |       const config = {
442 |         language: 'python',
443 |         pythonCode: `
444 | def process():
445 |     x = 1
446 | 	y = 2  # This line uses tabs
447 |     return [{"json": {"x": x, "y": y}}]
448 |         `
449 |       };
450 |       const properties = [
451 |         { name: 'language', type: 'options' },
452 |         { name: 'pythonCode', type: 'string' }
453 |       ];
454 | 
455 |       const result = ConfigValidator.validate(nodeType, config, properties);
456 | 
457 |       expect(result.errors.some(e => 
458 |         e.type === 'syntax_error' && 
459 |         e.message.includes('Mixed indentation')
460 |       )).toBe(true);
461 |     });
462 | 
463 |     it('should warn about incorrect n8n return patterns', () => {
464 |       const nodeType = 'nodes-base.code';
465 |       const config = {
466 |         language: 'python',
467 |         pythonCode: `
468 | result = {"data": "value"}
469 | return result  # Should return array of objects with json key
470 |         `
471 |       };
472 |       const properties = [
473 |         { name: 'language', type: 'options' },
474 |         { name: 'pythonCode', type: 'string' }
475 |       ];
476 | 
477 |       const result = ConfigValidator.validate(nodeType, config, properties);
478 | 
479 |       expect(result.warnings.some(w => 
480 |         w.type === 'invalid_value' && 
481 |         w.message.includes('Must return array of objects with json key')
482 |       )).toBe(true);
483 |     });
484 | 
485 |     it('should warn about using external libraries in Python code', () => {
486 |       const nodeType = 'nodes-base.code';
487 |       const config = {
488 |         language: 'python',
489 |         pythonCode: `
490 |           import pandas as pd
491 |           import requests
492 |           
493 |           df = pd.DataFrame(items)
494 |           response = requests.get('https://api.example.com')
495 |           return [{"json": {"data": response.json()}}]
496 |         `
497 |       };
498 |       const properties = [
499 |         { name: 'language', type: 'options' },
500 |         { name: 'pythonCode', type: 'string' }
501 |       ];
502 | 
503 |       const result = ConfigValidator.validate(nodeType, config, properties);
504 | 
505 |       expect(result.warnings.some(w => 
506 |         w.type === 'invalid_value' && 
507 |         w.message.includes('External libraries not available')
508 |       )).toBe(true);
509 |     });
510 | 
511 |     it('should validate Python code with print statements', () => {
512 |       const nodeType = 'nodes-base.code';
513 |       const config = {
514 |         language: 'python',
515 |         pythonCode: `
516 | print("Debug:", items)
517 | processed = []
518 | for item in items:
519 |     print(f"Processing: {item}")
520 |     processed.append({"json": item["json"]})
521 | return processed
522 |         `
523 |       };
524 |       const properties = [
525 |         { name: 'language', type: 'options' },
526 |         { name: 'pythonCode', type: 'string' }
527 |       ];
528 | 
529 |       const result = ConfigValidator.validate(nodeType, config, properties);
530 | 
531 |       expect(result.warnings.some(w => 
532 |         w.type === 'best_practice' && 
533 |         w.message.includes('print() output appears in n8n execution logs')
534 |       )).toBe(true);
535 |     });
536 |   });
537 | 
538 |   describe('Database node validation', () => {
539 |     it('should validate database query security', () => {
540 |       const nodeType = 'nodes-base.postgres';
541 |       const config = {
542 |         query: 'DELETE FROM users;' // Missing WHERE clause
543 |       };
544 |       const properties = [
545 |         { name: 'query', type: 'string' }
546 |       ];
547 | 
548 |       const result = ConfigValidator.validate(nodeType, config, properties);
549 | 
550 |       expect(result.warnings.some(w => 
551 |         w.type === 'security' && 
552 |         w.message.includes('DELETE query without WHERE clause')
553 |       )).toBe(true);
554 |     });
555 | 
556 |     it('should check for SQL injection vulnerabilities', () => {
557 |       const nodeType = 'nodes-base.mysql';
558 |       const config = {
559 |         query: 'SELECT * FROM users WHERE id = ${userId}'
560 |       };
561 |       const properties = [
562 |         { name: 'query', type: 'string' }
563 |       ];
564 | 
565 |       const result = ConfigValidator.validate(nodeType, config, properties);
566 | 
567 |       expect(result.warnings.some(w => 
568 |         w.type === 'security' && 
569 |         w.message.includes('SQL injection')
570 |       )).toBe(true);
571 |     });
572 | 
573 |     it('should validate SQL SELECT * performance warning', () => {
574 |       const nodeType = 'nodes-base.postgres';
575 |       const config = {
576 |         query: 'SELECT * FROM large_table WHERE status = "active"'
577 |       };
578 |       const properties = [
579 |         { name: 'query', type: 'string' }
580 |       ];
581 | 
582 |       const result = ConfigValidator.validate(nodeType, config, properties);
583 | 
584 |       expect(result.suggestions.some(s => 
585 |         s.includes('Consider selecting specific columns')
586 |       )).toBe(true);
587 |     });
588 |   });
589 | });
```
Page 26/60FirstPrevNextLast