mirror of
https://github.com/czlonkowski/n8n-mcp.git
synced 2026-03-17 16:03:08 +00:00
fix: sticky notes validation - eliminate false positives in workflow updates (#350)
Fixed critical bug where sticky notes (UI-only annotation nodes) incorrectly triggered "disconnected node" validation errors when updating workflows via MCP tools (n8n_update_partial_workflow, n8n_update_full_workflow). Problem: - Workflows with sticky notes failed validation with "Node is disconnected" errors - n8n-validation.ts lacked sticky note exclusion logic - workflow-validator.ts had correct logic but as private method - Code duplication led to divergent behavior Solution: 1. Created shared utility module (src/utils/node-classification.ts) - isStickyNote(): Identifies all sticky note type variations - isTriggerNode(): Identifies trigger nodes - isNonExecutableNode(): Identifies UI-only nodes - requiresIncomingConnection(): Determines connection requirements 2. Updated n8n-validation.ts to use shared utilities - Fixed disconnected nodes check to skip non-executable nodes - Added validation for workflows with only sticky notes - Fixed multi-node connection check to exclude sticky notes 3. Updated workflow-validator.ts to use shared utilities - Removed private isStickyNote() method (8 locations) - Eliminated code duplication Testing: - Created comprehensive test suites (54 new tests, 100% coverage) - Tested with n8n-mcp-tester agent using real n8n instance - All test scenarios passed including regression tests - Validated against real workflows with sticky notes Impact: - Sticky notes no longer block workflow updates - Matches n8n UI behavior exactly - Zero regressions in existing validation - All MCP workflow tools now work correctly with annotated workflows Files Changed: - NEW: src/utils/node-classification.ts - NEW: tests/unit/utils/node-classification.test.ts (44 tests) - NEW: tests/unit/services/n8n-validation-sticky-notes.test.ts (10 tests) - MODIFIED: src/services/n8n-validation.ts (lines 198-259) - MODIFIED: src/services/workflow-validator.ts (8 locations) - MODIFIED: tests/unit/validation-fixes.test.ts - MODIFIED: CHANGELOG.md (v2.20.8 entry) - MODIFIED: package.json (version bump to 2.20.8) Test Results: - Unit tests: 54 new tests passing, 100% coverage on utilities - Integration tests: All 10 sticky notes validation tests passing - Regression tests: Zero failures in existing test suite - Real-world testing: 4 test workflows validated successfully Conceived by Romuald Członkowski - www.aiadvisors.pl/en
This commit is contained in:
committed by
GitHub
parent
7300957d13
commit
79aff3406b
122
CHANGELOG.md
122
CHANGELOG.md
@@ -7,6 +7,128 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [2.20.8] - 2025-10-22
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
**Sticky Notes Validation - Disconnected Node False Positives**
|
||||
|
||||
Fixed critical bug where sticky notes (UI-only annotation nodes) were incorrectly triggering "disconnected node" validation errors when updating workflows via MCP tools.
|
||||
|
||||
#### Problem
|
||||
- Workflows with sticky notes failed validation with "Node is disconnected" errors
|
||||
- `n8n_update_partial_workflow` and `n8n_update_full_workflow` tools blocked legitimate updates
|
||||
- Example error: "Validation Error: Node '📝 Webhook Trigger' is disconnected"
|
||||
- Sticky notes are UI-only annotations and should never trigger connection validation
|
||||
|
||||
#### Root Cause Analysis
|
||||
|
||||
**Inconsistent Validation Logic:**
|
||||
- `src/services/workflow-validator.ts` correctly excluded sticky notes using private `isStickyNote()` method
|
||||
- `src/services/n8n-validation.ts` lacked sticky note exclusion logic entirely
|
||||
- Code duplication led to divergent behavior between validators
|
||||
|
||||
**Missing Checks in n8n-validation.ts:**
|
||||
```typescript
|
||||
// BEFORE (Broken) - lines 246-257:
|
||||
const webhookTypes = new Set([
|
||||
'n8n-nodes-base.webhook',
|
||||
'n8n-nodes-base.webhookTrigger',
|
||||
'n8n-nodes-base.manualTrigger'
|
||||
]);
|
||||
// Only checked for webhooks, missed sticky notes entirely
|
||||
const disconnectedNodes = workflow.nodes.filter(node => {
|
||||
const isConnected = connectedNodes.has(node.name);
|
||||
const isTrigger = webhookTypes.has(node.type);
|
||||
// ...
|
||||
});
|
||||
```
|
||||
|
||||
#### Fixed
|
||||
|
||||
**1. Created Shared Utility Module** (`src/utils/node-classification.ts`)
|
||||
- Centralized node classification logic to ensure consistency
|
||||
- Four core functions:
|
||||
- `isStickyNote()`: Identifies all sticky note type variations
|
||||
- `isTriggerNode()`: Identifies trigger nodes (webhook, manual, cron, schedule)
|
||||
- `isNonExecutableNode()`: Identifies UI-only nodes
|
||||
- `requiresIncomingConnection()`: Determines if node needs incoming connections
|
||||
|
||||
**2. Updated n8n-validation.ts** (lines 198-259)
|
||||
- Added imports: `import { isNonExecutableNode, isTriggerNode } from '../utils/node-classification'`
|
||||
- Fixed disconnected nodes check to skip non-executable nodes:
|
||||
```typescript
|
||||
// AFTER (Fixed):
|
||||
const disconnectedNodes = workflow.nodes.filter(node => {
|
||||
// Skip non-executable nodes (sticky notes, etc.) - they're UI-only annotations
|
||||
if (isNonExecutableNode(node.type)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const isConnected = connectedNodes.has(node.name);
|
||||
const isTrigger = isTriggerNode(node.type);
|
||||
// ...
|
||||
});
|
||||
```
|
||||
- Added validation for workflows with only sticky notes
|
||||
- Fixed multi-node connection check to exclude sticky notes when counting executable nodes
|
||||
|
||||
**3. Updated workflow-validator.ts** (8 locations)
|
||||
- Removed private `isStickyNote()` method
|
||||
- Replaced all calls with `isNonExecutableNode()` from shared utilities
|
||||
- Eliminates code duplication
|
||||
|
||||
#### Testing
|
||||
|
||||
**New Test Files:**
|
||||
- `tests/unit/utils/node-classification.test.ts`: 44 tests, 100% coverage
|
||||
- Tests all classification functions
|
||||
- Tests all sticky note type variations
|
||||
- Tests trigger node identification
|
||||
- Integration scenarios
|
||||
|
||||
- `tests/unit/services/n8n-validation-sticky-notes.test.ts`: 10 comprehensive tests
|
||||
- Workflows with sticky notes and connected functional nodes
|
||||
- Multiple sticky notes (10+ notes)
|
||||
- All sticky note type variations
|
||||
- Complex real-world scenarios (simulates POST /auth/login workflow)
|
||||
- Detection of truly disconnected functional nodes
|
||||
- Regression tests matching n8n UI behavior
|
||||
|
||||
**Updated Test Files:**
|
||||
- `tests/unit/validation-fixes.test.ts`: Updated terminology to reflect shared utilities
|
||||
|
||||
**Test Results:**
|
||||
- All 54 new tests passing
|
||||
- 100% coverage on node-classification utilities
|
||||
- Zero regressions in existing test suite
|
||||
|
||||
#### Impact
|
||||
|
||||
**Workflow Updates:**
|
||||
- ✅ Sticky notes no longer block workflow updates
|
||||
- ✅ `n8n_update_partial_workflow` works correctly with annotated workflows
|
||||
- ✅ `n8n_update_full_workflow` accepts workflows with documentation notes
|
||||
- ✅ Matches n8n UI behavior exactly
|
||||
|
||||
**Code Quality:**
|
||||
- ✅ Eliminated code duplication between validators
|
||||
- ✅ Centralized node classification logic
|
||||
- ✅ Future node types can be added in one place
|
||||
- ✅ Consistent behavior across all validation paths
|
||||
|
||||
**Node Type Support:**
|
||||
- ✅ Handles all sticky note variations: `n8n-nodes-base.stickyNote`, `nodes-base.stickyNote`, `@n8n/n8n-nodes-base.stickyNote`
|
||||
- ✅ Proper trigger node detection: webhook, webhookTrigger, manualTrigger, cronTrigger, scheduleTrigger
|
||||
- ✅ Correct connection requirements for all node types
|
||||
|
||||
**Backward Compatibility:**
|
||||
- ✅ No breaking changes
|
||||
- ✅ All existing validations continue to work
|
||||
- ✅ API remains unchanged
|
||||
|
||||
Concieved by Romuald Członkowski - [www.aiadvisors.pl/en](https://www.aiadvisors.pl/en)
|
||||
|
||||
## [2.20.7] - 2025-10-22
|
||||
|
||||
### 🔄 Dependencies
|
||||
|
||||
BIN
data/nodes.db
BIN
data/nodes.db
Binary file not shown.
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "n8n-mcp",
|
||||
"version": "2.20.7",
|
||||
"version": "2.20.8",
|
||||
"description": "Integration between n8n workflow automation and Model Context Protocol (MCP)",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { z } from 'zod';
|
||||
import { WorkflowNode, WorkflowConnection, Workflow } from '../types/n8n-api';
|
||||
import { isNonExecutableNode, isTriggerNode } from '../utils/node-classification';
|
||||
|
||||
// Zod schemas for n8n API validation
|
||||
|
||||
@@ -194,6 +195,14 @@ export function validateWorkflowStructure(workflow: Partial<Workflow>): string[]
|
||||
errors.push('Workflow must have at least one node');
|
||||
}
|
||||
|
||||
// Check if workflow has only non-executable nodes (sticky notes)
|
||||
if (workflow.nodes && workflow.nodes.length > 0) {
|
||||
const hasExecutableNodes = workflow.nodes.some(node => !isNonExecutableNode(node.type));
|
||||
if (!hasExecutableNodes) {
|
||||
errors.push('Workflow must have at least one executable node. Sticky notes alone cannot form a valid workflow.');
|
||||
}
|
||||
}
|
||||
|
||||
if (!workflow.connections) {
|
||||
errors.push('Workflow connections are required');
|
||||
}
|
||||
@@ -211,13 +220,15 @@ export function validateWorkflowStructure(workflow: Partial<Workflow>): string[]
|
||||
|
||||
// Check for disconnected nodes in multi-node workflows
|
||||
if (workflow.nodes && workflow.nodes.length > 1 && workflow.connections) {
|
||||
// Filter out non-executable nodes (sticky notes) when counting nodes
|
||||
const executableNodes = workflow.nodes.filter(node => !isNonExecutableNode(node.type));
|
||||
const connectionCount = Object.keys(workflow.connections).length;
|
||||
|
||||
// First check: workflow has no connections at all
|
||||
if (connectionCount === 0) {
|
||||
const nodeNames = workflow.nodes.slice(0, 2).map(n => n.name);
|
||||
// First check: workflow has no connections at all (only check if there are multiple executable nodes)
|
||||
if (connectionCount === 0 && executableNodes.length > 1) {
|
||||
const nodeNames = executableNodes.slice(0, 2).map(n => n.name);
|
||||
errors.push(`Multi-node workflow has no connections between nodes. Add a connection using: {type: 'addConnection', source: '${nodeNames[0]}', target: '${nodeNames[1]}', sourcePort: 'main', targetPort: 'main'}`);
|
||||
} else {
|
||||
} else if (connectionCount > 0 || executableNodes.length > 1) {
|
||||
// Second check: detect disconnected nodes (nodes with no incoming or outgoing connections)
|
||||
const connectedNodes = new Set<string>();
|
||||
|
||||
@@ -236,19 +247,20 @@ export function validateWorkflowStructure(workflow: Partial<Workflow>): string[]
|
||||
}
|
||||
});
|
||||
|
||||
// Find disconnected nodes (excluding webhook triggers which can be source-only)
|
||||
const webhookTypes = new Set([
|
||||
'n8n-nodes-base.webhook',
|
||||
'n8n-nodes-base.webhookTrigger',
|
||||
'n8n-nodes-base.manualTrigger'
|
||||
]);
|
||||
|
||||
// Find disconnected nodes (excluding non-executable nodes and triggers)
|
||||
// Non-executable nodes (sticky notes) are UI-only and don't need connections
|
||||
// Trigger nodes only need outgoing connections
|
||||
const disconnectedNodes = workflow.nodes.filter(node => {
|
||||
const isConnected = connectedNodes.has(node.name);
|
||||
const isWebhookOrTrigger = webhookTypes.has(node.type);
|
||||
// Skip non-executable nodes (sticky notes, etc.) - they're UI-only annotations
|
||||
if (isNonExecutableNode(node.type)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Webhook/trigger nodes only need outgoing connections
|
||||
if (isWebhookOrTrigger) {
|
||||
const isConnected = connectedNodes.has(node.name);
|
||||
const isTrigger = isTriggerNode(node.type);
|
||||
|
||||
// Trigger nodes only need outgoing connections
|
||||
if (isTrigger) {
|
||||
return !workflow.connections?.[node.name]; // Disconnected if no outgoing connections
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import { NodeSimilarityService, NodeSuggestion } from './node-similarity-service
|
||||
import { NodeTypeNormalizer } from '../utils/node-type-normalizer';
|
||||
import { Logger } from '../utils/logger';
|
||||
import { validateAISpecificNodes, hasAINodes } from './ai-node-validator';
|
||||
import { isNonExecutableNode } from '../utils/node-classification';
|
||||
const logger = new Logger({ prefix: '[WorkflowValidator]' });
|
||||
|
||||
interface WorkflowNode {
|
||||
@@ -85,17 +86,8 @@ export class WorkflowValidator {
|
||||
this.similarityService = new NodeSimilarityService(nodeRepository);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a node is a Sticky Note or other non-executable node
|
||||
*/
|
||||
private isStickyNote(node: WorkflowNode): boolean {
|
||||
const stickyNoteTypes = [
|
||||
'n8n-nodes-base.stickyNote',
|
||||
'nodes-base.stickyNote',
|
||||
'@n8n/n8n-nodes-base.stickyNote'
|
||||
];
|
||||
return stickyNoteTypes.includes(node.type);
|
||||
}
|
||||
// Note: isStickyNote logic moved to shared utility: src/utils/node-classification.ts
|
||||
// Use isNonExecutableNode(node.type) instead
|
||||
|
||||
/**
|
||||
* Validate a complete workflow
|
||||
@@ -146,7 +138,7 @@ export class WorkflowValidator {
|
||||
}
|
||||
|
||||
// Update statistics after null check (exclude sticky notes from counts)
|
||||
const executableNodes = Array.isArray(workflow.nodes) ? workflow.nodes.filter(n => !this.isStickyNote(n)) : [];
|
||||
const executableNodes = Array.isArray(workflow.nodes) ? workflow.nodes.filter(n => !isNonExecutableNode(n.type)) : [];
|
||||
result.statistics.totalNodes = executableNodes.length;
|
||||
result.statistics.enabledNodes = executableNodes.filter(n => !n.disabled).length;
|
||||
|
||||
@@ -356,7 +348,7 @@ export class WorkflowValidator {
|
||||
profile: string
|
||||
): Promise<void> {
|
||||
for (const node of workflow.nodes) {
|
||||
if (node.disabled || this.isStickyNote(node)) continue;
|
||||
if (node.disabled || isNonExecutableNode(node.type)) continue;
|
||||
|
||||
try {
|
||||
// Validate node name length
|
||||
@@ -632,7 +624,7 @@ export class WorkflowValidator {
|
||||
|
||||
// Check for orphaned nodes (exclude sticky notes)
|
||||
for (const node of workflow.nodes) {
|
||||
if (node.disabled || this.isStickyNote(node)) continue;
|
||||
if (node.disabled || isNonExecutableNode(node.type)) continue;
|
||||
|
||||
const normalizedType = NodeTypeNormalizer.normalizeToFullForm(node.type);
|
||||
const isTrigger = normalizedType.toLowerCase().includes('trigger') ||
|
||||
@@ -877,7 +869,7 @@ export class WorkflowValidator {
|
||||
|
||||
// Build node type map (exclude sticky notes)
|
||||
workflow.nodes.forEach(node => {
|
||||
if (!this.isStickyNote(node)) {
|
||||
if (!isNonExecutableNode(node.type)) {
|
||||
nodeTypeMap.set(node.name, node.type);
|
||||
}
|
||||
});
|
||||
@@ -945,7 +937,7 @@ export class WorkflowValidator {
|
||||
|
||||
// Check from all executable nodes (exclude sticky notes)
|
||||
for (const node of workflow.nodes) {
|
||||
if (!this.isStickyNote(node) && !visited.has(node.name)) {
|
||||
if (!isNonExecutableNode(node.type) && !visited.has(node.name)) {
|
||||
if (hasCycleDFS(node.name)) return true;
|
||||
}
|
||||
}
|
||||
@@ -964,7 +956,7 @@ export class WorkflowValidator {
|
||||
const nodeNames = workflow.nodes.map(n => n.name);
|
||||
|
||||
for (const node of workflow.nodes) {
|
||||
if (node.disabled || this.isStickyNote(node)) continue;
|
||||
if (node.disabled || isNonExecutableNode(node.type)) continue;
|
||||
|
||||
// Skip expression validation for langchain nodes
|
||||
// They have AI-specific validators and different expression rules
|
||||
@@ -1111,7 +1103,7 @@ export class WorkflowValidator {
|
||||
|
||||
// Check node-level error handling properties for ALL executable nodes
|
||||
for (const node of workflow.nodes) {
|
||||
if (!this.isStickyNote(node)) {
|
||||
if (!isNonExecutableNode(node.type)) {
|
||||
this.checkNodeErrorHandling(node, workflow, result);
|
||||
}
|
||||
}
|
||||
|
||||
121
src/utils/node-classification.ts
Normal file
121
src/utils/node-classification.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
/**
|
||||
* Node Classification Utilities
|
||||
*
|
||||
* Provides shared classification logic for workflow nodes.
|
||||
* Used by validators to consistently identify node types across the codebase.
|
||||
*
|
||||
* This module centralizes node type classification to ensure consistent behavior
|
||||
* between WorkflowValidator and n8n-validation.ts, preventing bugs like sticky
|
||||
* notes being incorrectly flagged as disconnected nodes.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Check if a node type is a sticky note (documentation-only node)
|
||||
*
|
||||
* Sticky notes are UI-only annotation nodes that:
|
||||
* - Do not participate in workflow execution
|
||||
* - Never have connections (by design)
|
||||
* - Should be excluded from connection validation
|
||||
* - Serve purely as visual documentation in the workflow canvas
|
||||
*
|
||||
* Example sticky note types:
|
||||
* - 'n8n-nodes-base.stickyNote' (standard format)
|
||||
* - 'nodes-base.stickyNote' (normalized format)
|
||||
* - '@n8n/n8n-nodes-base.stickyNote' (scoped format)
|
||||
*
|
||||
* @param nodeType - The node type to check (e.g., 'n8n-nodes-base.stickyNote')
|
||||
* @returns true if the node is a sticky note, false otherwise
|
||||
*/
|
||||
export function isStickyNote(nodeType: string): boolean {
|
||||
const stickyNoteTypes = [
|
||||
'n8n-nodes-base.stickyNote',
|
||||
'nodes-base.stickyNote',
|
||||
'@n8n/n8n-nodes-base.stickyNote'
|
||||
];
|
||||
return stickyNoteTypes.includes(nodeType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a node type is a trigger node
|
||||
*
|
||||
* Trigger nodes:
|
||||
* - Start workflow execution
|
||||
* - Only need outgoing connections (no incoming connections required)
|
||||
* - Include webhooks, manual triggers, schedule triggers, etc.
|
||||
* - Are the entry points for workflow execution
|
||||
*
|
||||
* Examples:
|
||||
* - Webhooks: Listen for HTTP requests
|
||||
* - Manual triggers: Started manually by user
|
||||
* - Schedule/Cron triggers: Run on a schedule
|
||||
*
|
||||
* @param nodeType - The node type to check
|
||||
* @returns true if the node is a trigger, false otherwise
|
||||
*/
|
||||
export function isTriggerNode(nodeType: string): boolean {
|
||||
const triggerTypes = [
|
||||
'n8n-nodes-base.webhook',
|
||||
'n8n-nodes-base.webhookTrigger',
|
||||
'n8n-nodes-base.manualTrigger',
|
||||
'n8n-nodes-base.cronTrigger',
|
||||
'n8n-nodes-base.scheduleTrigger'
|
||||
];
|
||||
return triggerTypes.includes(nodeType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a node type is non-executable (UI-only)
|
||||
*
|
||||
* Non-executable nodes:
|
||||
* - Do not participate in workflow execution
|
||||
* - Serve documentation/annotation purposes only
|
||||
* - Should be excluded from all execution-related validation
|
||||
* - Should be excluded from statistics like "total executable nodes"
|
||||
* - Should be excluded from connection validation
|
||||
*
|
||||
* Currently includes: sticky notes
|
||||
*
|
||||
* Future: May include other annotation/comment nodes if n8n adds them
|
||||
*
|
||||
* @param nodeType - The node type to check
|
||||
* @returns true if the node is non-executable, false otherwise
|
||||
*/
|
||||
export function isNonExecutableNode(nodeType: string): boolean {
|
||||
return isStickyNote(nodeType);
|
||||
// Future: Add other non-executable node types here
|
||||
// Example: || isCommentNode(nodeType) || isAnnotationNode(nodeType)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a node type requires incoming connections
|
||||
*
|
||||
* Most nodes require at least one incoming connection to receive data,
|
||||
* but there are two categories of exceptions:
|
||||
*
|
||||
* 1. Trigger nodes: Only need outgoing connections
|
||||
* - They start workflow execution
|
||||
* - They generate their own data
|
||||
* - Examples: webhook, manualTrigger, scheduleTrigger
|
||||
*
|
||||
* 2. Non-executable nodes: Don't need any connections
|
||||
* - They are UI-only annotations
|
||||
* - They don't participate in execution
|
||||
* - Examples: stickyNote
|
||||
*
|
||||
* @param nodeType - The node type to check
|
||||
* @returns true if the node requires incoming connections, false otherwise
|
||||
*/
|
||||
export function requiresIncomingConnection(nodeType: string): boolean {
|
||||
// Non-executable nodes don't need any connections
|
||||
if (isNonExecutableNode(nodeType)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Trigger nodes only need outgoing connections
|
||||
if (isTriggerNode(nodeType)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Regular nodes need incoming connections
|
||||
return true;
|
||||
}
|
||||
532
tests/unit/services/n8n-validation-sticky-notes.test.ts
Normal file
532
tests/unit/services/n8n-validation-sticky-notes.test.ts
Normal file
@@ -0,0 +1,532 @@
|
||||
import { describe, test, expect } from 'vitest';
|
||||
import { validateWorkflowStructure } from '@/services/n8n-validation';
|
||||
import type { Workflow } from '@/types/n8n-api';
|
||||
|
||||
describe('n8n-validation - Sticky Notes Bug Fix', () => {
|
||||
describe('sticky notes should be excluded from disconnected nodes validation', () => {
|
||||
test('should allow workflow with sticky notes and connected functional nodes', () => {
|
||||
const workflow: Partial<Workflow> = {
|
||||
name: 'Test Workflow',
|
||||
nodes: [
|
||||
{
|
||||
id: '1',
|
||||
name: 'Webhook',
|
||||
type: 'n8n-nodes-base.webhook',
|
||||
typeVersion: 1,
|
||||
position: [250, 300],
|
||||
parameters: { path: '/test' }
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'HTTP Request',
|
||||
type: 'n8n-nodes-base.httpRequest',
|
||||
typeVersion: 3,
|
||||
position: [450, 300],
|
||||
parameters: {}
|
||||
},
|
||||
{
|
||||
id: 'sticky1',
|
||||
name: 'Documentation Note',
|
||||
type: 'n8n-nodes-base.stickyNote',
|
||||
typeVersion: 1,
|
||||
position: [250, 100],
|
||||
parameters: { content: 'This is a documentation note' }
|
||||
}
|
||||
],
|
||||
connections: {
|
||||
'Webhook': {
|
||||
main: [[{ node: 'HTTP Request', type: 'main', index: 0 }]]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const errors = validateWorkflowStructure(workflow);
|
||||
|
||||
// Should have no errors - sticky note should be ignored
|
||||
expect(errors).toEqual([]);
|
||||
});
|
||||
|
||||
test('should handle multiple sticky notes without errors', () => {
|
||||
const workflow: Partial<Workflow> = {
|
||||
name: 'Documented Workflow',
|
||||
nodes: [
|
||||
{
|
||||
id: '1',
|
||||
name: 'Webhook',
|
||||
type: 'n8n-nodes-base.webhook',
|
||||
typeVersion: 1,
|
||||
position: [250, 300],
|
||||
parameters: { path: '/test' }
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'Process',
|
||||
type: 'n8n-nodes-base.set',
|
||||
typeVersion: 3,
|
||||
position: [450, 300],
|
||||
parameters: {}
|
||||
},
|
||||
// 10 sticky notes for documentation
|
||||
...Array.from({ length: 10 }, (_, i) => ({
|
||||
id: `sticky${i}`,
|
||||
name: `📝 Note ${i}`,
|
||||
type: 'n8n-nodes-base.stickyNote',
|
||||
typeVersion: 1,
|
||||
position: [100 + i * 50, 100] as [number, number],
|
||||
parameters: { content: `Documentation note ${i}` }
|
||||
}))
|
||||
],
|
||||
connections: {
|
||||
'Webhook': {
|
||||
main: [[{ node: 'Process', type: 'main', index: 0 }]]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const errors = validateWorkflowStructure(workflow);
|
||||
expect(errors).toEqual([]);
|
||||
});
|
||||
|
||||
test('should handle all sticky note type variations', () => {
|
||||
const stickyTypes = [
|
||||
'n8n-nodes-base.stickyNote',
|
||||
'nodes-base.stickyNote',
|
||||
'@n8n/n8n-nodes-base.stickyNote'
|
||||
];
|
||||
|
||||
stickyTypes.forEach((stickyType, index) => {
|
||||
const workflow: Partial<Workflow> = {
|
||||
name: 'Test Workflow',
|
||||
nodes: [
|
||||
{
|
||||
id: '1',
|
||||
name: 'Webhook',
|
||||
type: 'n8n-nodes-base.webhook',
|
||||
typeVersion: 1,
|
||||
position: [250, 300],
|
||||
parameters: { path: '/test' }
|
||||
},
|
||||
{
|
||||
id: `sticky${index}`,
|
||||
name: `Note ${index}`,
|
||||
type: stickyType,
|
||||
typeVersion: 1,
|
||||
position: [250, 100],
|
||||
parameters: { content: `Note ${index}` }
|
||||
}
|
||||
],
|
||||
connections: {}
|
||||
};
|
||||
|
||||
const errors = validateWorkflowStructure(workflow);
|
||||
|
||||
// Sticky note should be ignored regardless of type variation
|
||||
expect(errors.every(e => !e.includes(`Note ${index}`))).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
test('should handle complex workflow with multiple sticky notes (real-world scenario)', () => {
|
||||
// Simulates workflow like "POST /auth/login" with 4 sticky notes
|
||||
const workflow: Partial<Workflow> = {
|
||||
name: 'POST /auth/login',
|
||||
nodes: [
|
||||
{
|
||||
id: 'webhook1',
|
||||
name: 'Webhook Trigger',
|
||||
type: 'n8n-nodes-base.webhook',
|
||||
typeVersion: 1,
|
||||
position: [250, 300],
|
||||
parameters: { path: '/auth/login', httpMethod: 'POST' }
|
||||
},
|
||||
{
|
||||
id: 'http1',
|
||||
name: 'Authenticate',
|
||||
type: 'n8n-nodes-base.httpRequest',
|
||||
typeVersion: 3,
|
||||
position: [450, 300],
|
||||
parameters: {}
|
||||
},
|
||||
{
|
||||
id: 'respond1',
|
||||
name: 'Return Success',
|
||||
type: 'n8n-nodes-base.respondToWebhook',
|
||||
typeVersion: 1,
|
||||
position: [650, 250],
|
||||
parameters: {}
|
||||
},
|
||||
{
|
||||
id: 'respond2',
|
||||
name: 'Return Error',
|
||||
type: 'n8n-nodes-base.respondToWebhook',
|
||||
typeVersion: 1,
|
||||
position: [650, 350],
|
||||
parameters: {}
|
||||
},
|
||||
// 4 sticky notes for documentation
|
||||
{
|
||||
id: 'sticky1',
|
||||
name: '📝 Webhook Trigger',
|
||||
type: 'n8n-nodes-base.stickyNote',
|
||||
typeVersion: 1,
|
||||
position: [250, 150],
|
||||
parameters: { content: 'Receives login request' }
|
||||
},
|
||||
{
|
||||
id: 'sticky2',
|
||||
name: '📝 Authenticate with Supabase',
|
||||
type: 'n8n-nodes-base.stickyNote',
|
||||
typeVersion: 1,
|
||||
position: [450, 150],
|
||||
parameters: { content: 'Validates credentials' }
|
||||
},
|
||||
{
|
||||
id: 'sticky3',
|
||||
name: '📝 Return Tokens',
|
||||
type: 'n8n-nodes-base.stickyNote',
|
||||
typeVersion: 1,
|
||||
position: [650, 150],
|
||||
parameters: { content: 'Returns access and refresh tokens' }
|
||||
},
|
||||
{
|
||||
id: 'sticky4',
|
||||
name: '📝 Return Error',
|
||||
type: 'n8n-nodes-base.stickyNote',
|
||||
typeVersion: 1,
|
||||
position: [650, 450],
|
||||
parameters: { content: 'Returns error message' }
|
||||
}
|
||||
],
|
||||
connections: {
|
||||
'Webhook Trigger': {
|
||||
main: [[{ node: 'Authenticate', type: 'main', index: 0 }]]
|
||||
},
|
||||
'Authenticate': {
|
||||
main: [
|
||||
[{ node: 'Return Success', type: 'main', index: 0 }],
|
||||
[{ node: 'Return Error', type: 'main', index: 0 }]
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const errors = validateWorkflowStructure(workflow);
|
||||
|
||||
// Should have no errors - all sticky notes should be ignored
|
||||
expect(errors).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('validation should still detect truly disconnected functional nodes', () => {
|
||||
test('should detect disconnected HTTP node but ignore sticky note', () => {
|
||||
const workflow: Partial<Workflow> = {
|
||||
name: 'Test Workflow',
|
||||
nodes: [
|
||||
{
|
||||
id: '1',
|
||||
name: 'Webhook',
|
||||
type: 'n8n-nodes-base.webhook',
|
||||
typeVersion: 1,
|
||||
position: [250, 300],
|
||||
parameters: { path: '/test' }
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'Disconnected HTTP',
|
||||
type: 'n8n-nodes-base.httpRequest',
|
||||
typeVersion: 3,
|
||||
position: [450, 300],
|
||||
parameters: {}
|
||||
},
|
||||
{
|
||||
id: 'sticky1',
|
||||
name: 'Sticky Note',
|
||||
type: 'n8n-nodes-base.stickyNote',
|
||||
typeVersion: 1,
|
||||
position: [250, 100],
|
||||
parameters: { content: 'Note' }
|
||||
}
|
||||
],
|
||||
connections: {} // No connections
|
||||
};
|
||||
|
||||
const errors = validateWorkflowStructure(workflow);
|
||||
|
||||
// Should error on HTTP node, but NOT on sticky note
|
||||
expect(errors.length).toBeGreaterThan(0);
|
||||
const disconnectedError = errors.find(e => e.includes('Disconnected'));
|
||||
expect(disconnectedError).toBeDefined();
|
||||
expect(disconnectedError).toContain('Disconnected HTTP');
|
||||
expect(disconnectedError).not.toContain('Sticky Note');
|
||||
});
|
||||
|
||||
test('should detect multiple disconnected functional nodes but ignore sticky notes', () => {
|
||||
const workflow: Partial<Workflow> = {
|
||||
name: 'Test Workflow',
|
||||
nodes: [
|
||||
{
|
||||
id: '1',
|
||||
name: 'Webhook',
|
||||
type: 'n8n-nodes-base.webhook',
|
||||
typeVersion: 1,
|
||||
position: [250, 300],
|
||||
parameters: { path: '/test' }
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'Disconnected HTTP',
|
||||
type: 'n8n-nodes-base.httpRequest',
|
||||
typeVersion: 3,
|
||||
position: [450, 300],
|
||||
parameters: {}
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: 'Disconnected Set',
|
||||
type: 'n8n-nodes-base.set',
|
||||
typeVersion: 3,
|
||||
position: [650, 300],
|
||||
parameters: {}
|
||||
},
|
||||
// Multiple sticky notes that should be ignored
|
||||
{
|
||||
id: 'sticky1',
|
||||
name: 'Note 1',
|
||||
type: 'n8n-nodes-base.stickyNote',
|
||||
typeVersion: 1,
|
||||
position: [250, 100],
|
||||
parameters: { content: 'Note 1' }
|
||||
},
|
||||
{
|
||||
id: 'sticky2',
|
||||
name: 'Note 2',
|
||||
type: 'n8n-nodes-base.stickyNote',
|
||||
typeVersion: 1,
|
||||
position: [450, 100],
|
||||
parameters: { content: 'Note 2' }
|
||||
}
|
||||
],
|
||||
connections: {} // No connections
|
||||
};
|
||||
|
||||
const errors = validateWorkflowStructure(workflow);
|
||||
|
||||
// Should error because there are no connections
|
||||
// When there are NO connections, validation shows "Multi-node workflow has no connections"
|
||||
// This is the expected behavior - it suggests connecting any two executable nodes
|
||||
expect(errors.length).toBeGreaterThan(0);
|
||||
const connectionError = errors.find(e => e.includes('no connections') || e.includes('Disconnected'));
|
||||
expect(connectionError).toBeDefined();
|
||||
// Error should NOT mention sticky notes
|
||||
expect(connectionError).not.toContain('Note 1');
|
||||
expect(connectionError).not.toContain('Note 2');
|
||||
});
|
||||
|
||||
test('should allow sticky notes but still validate functional node connections', () => {
|
||||
const workflow: Partial<Workflow> = {
|
||||
name: 'Test Workflow',
|
||||
nodes: [
|
||||
{
|
||||
id: '1',
|
||||
name: 'Webhook',
|
||||
type: 'n8n-nodes-base.webhook',
|
||||
typeVersion: 1,
|
||||
position: [250, 300],
|
||||
parameters: { path: '/test' }
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'Connected HTTP',
|
||||
type: 'n8n-nodes-base.httpRequest',
|
||||
typeVersion: 3,
|
||||
position: [450, 300],
|
||||
parameters: {}
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: 'Disconnected Set',
|
||||
type: 'n8n-nodes-base.set',
|
||||
typeVersion: 3,
|
||||
position: [650, 300],
|
||||
parameters: {}
|
||||
},
|
||||
{
|
||||
id: 'sticky1',
|
||||
name: 'Sticky Note',
|
||||
type: 'n8n-nodes-base.stickyNote',
|
||||
typeVersion: 1,
|
||||
position: [250, 100],
|
||||
parameters: { content: 'Note' }
|
||||
}
|
||||
],
|
||||
connections: {
|
||||
'Webhook': {
|
||||
main: [[{ node: 'Connected HTTP', type: 'main', index: 0 }]]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const errors = validateWorkflowStructure(workflow);
|
||||
|
||||
// Should error only on disconnected Set node
|
||||
expect(errors.length).toBeGreaterThan(0);
|
||||
const disconnectedError = errors.find(e => e.includes('Disconnected'));
|
||||
expect(disconnectedError).toBeDefined();
|
||||
expect(disconnectedError).toContain('Disconnected Set');
|
||||
expect(disconnectedError).not.toContain('Connected HTTP');
|
||||
expect(disconnectedError).not.toContain('Sticky Note');
|
||||
});
|
||||
});
|
||||
|
||||
describe('regression tests - ensure sticky notes work like in n8n UI', () => {
|
||||
test('single webhook with sticky notes should be valid (matches n8n UI behavior)', () => {
|
||||
const workflow: Partial<Workflow> = {
|
||||
name: 'Webhook Only with Notes',
|
||||
nodes: [
|
||||
{
|
||||
id: '1',
|
||||
name: 'Webhook',
|
||||
type: 'n8n-nodes-base.webhook',
|
||||
typeVersion: 1,
|
||||
position: [250, 300],
|
||||
parameters: { path: '/test' }
|
||||
},
|
||||
{
|
||||
id: 'sticky1',
|
||||
name: 'Usage Instructions',
|
||||
type: 'n8n-nodes-base.stickyNote',
|
||||
typeVersion: 1,
|
||||
position: [250, 100],
|
||||
parameters: { content: 'Call this webhook to trigger the workflow' }
|
||||
}
|
||||
],
|
||||
connections: {}
|
||||
};
|
||||
|
||||
const errors = validateWorkflowStructure(workflow);
|
||||
|
||||
// Webhook-only workflows are valid in n8n
|
||||
// Sticky notes should not affect this
|
||||
expect(errors).toEqual([]);
|
||||
});
|
||||
|
||||
test('workflow with only sticky notes should be invalid (no executable nodes)', () => {
|
||||
const workflow: Partial<Workflow> = {
|
||||
name: 'Only Notes',
|
||||
nodes: [
|
||||
{
|
||||
id: 'sticky1',
|
||||
name: 'Note 1',
|
||||
type: 'n8n-nodes-base.stickyNote',
|
||||
typeVersion: 1,
|
||||
position: [250, 100],
|
||||
parameters: { content: 'Note 1' }
|
||||
},
|
||||
{
|
||||
id: 'sticky2',
|
||||
name: 'Note 2',
|
||||
type: 'n8n-nodes-base.stickyNote',
|
||||
typeVersion: 1,
|
||||
position: [450, 100],
|
||||
parameters: { content: 'Note 2' }
|
||||
}
|
||||
],
|
||||
connections: {}
|
||||
};
|
||||
|
||||
const errors = validateWorkflowStructure(workflow);
|
||||
|
||||
// Should fail because there are no executable nodes
|
||||
expect(errors.length).toBeGreaterThan(0);
|
||||
expect(errors.some(e => e.includes('at least one executable node'))).toBe(true);
|
||||
});
|
||||
|
||||
test('complex production workflow structure should validate correctly', () => {
|
||||
// Tests a realistic production workflow structure
|
||||
const workflow: Partial<Workflow> = {
|
||||
name: 'Production API Endpoint',
|
||||
nodes: [
|
||||
// Functional nodes
|
||||
{
|
||||
id: 'webhook1',
|
||||
name: 'API Webhook',
|
||||
type: 'n8n-nodes-base.webhook',
|
||||
typeVersion: 1,
|
||||
position: [250, 300],
|
||||
parameters: { path: '/api/endpoint' }
|
||||
},
|
||||
{
|
||||
id: 'validate1',
|
||||
name: 'Validate Input',
|
||||
type: 'n8n-nodes-base.code',
|
||||
typeVersion: 2,
|
||||
position: [450, 300],
|
||||
parameters: {}
|
||||
},
|
||||
{
|
||||
id: 'branch1',
|
||||
name: 'Check Valid',
|
||||
type: 'n8n-nodes-base.if',
|
||||
typeVersion: 2,
|
||||
position: [650, 300],
|
||||
parameters: {}
|
||||
},
|
||||
{
|
||||
id: 'process1',
|
||||
name: 'Process Request',
|
||||
type: 'n8n-nodes-base.httpRequest',
|
||||
typeVersion: 3,
|
||||
position: [850, 250],
|
||||
parameters: {}
|
||||
},
|
||||
{
|
||||
id: 'success1',
|
||||
name: 'Return Success',
|
||||
type: 'n8n-nodes-base.respondToWebhook',
|
||||
typeVersion: 1,
|
||||
position: [1050, 250],
|
||||
parameters: {}
|
||||
},
|
||||
{
|
||||
id: 'error1',
|
||||
name: 'Return Error',
|
||||
type: 'n8n-nodes-base.respondToWebhook',
|
||||
typeVersion: 1,
|
||||
position: [850, 350],
|
||||
parameters: {}
|
||||
},
|
||||
// Documentation sticky notes (11 notes like in real workflow)
|
||||
...Array.from({ length: 11 }, (_, i) => ({
|
||||
id: `sticky${i}`,
|
||||
name: `📝 Documentation ${i}`,
|
||||
type: 'n8n-nodes-base.stickyNote',
|
||||
typeVersion: 1,
|
||||
position: [250 + i * 100, 100] as [number, number],
|
||||
parameters: { content: `Documentation section ${i}` }
|
||||
}))
|
||||
],
|
||||
connections: {
|
||||
'API Webhook': {
|
||||
main: [[{ node: 'Validate Input', type: 'main', index: 0 }]]
|
||||
},
|
||||
'Validate Input': {
|
||||
main: [[{ node: 'Check Valid', type: 'main', index: 0 }]]
|
||||
},
|
||||
'Check Valid': {
|
||||
main: [
|
||||
[{ node: 'Process Request', type: 'main', index: 0 }],
|
||||
[{ node: 'Return Error', type: 'main', index: 0 }]
|
||||
]
|
||||
},
|
||||
'Process Request': {
|
||||
main: [[{ node: 'Return Success', type: 'main', index: 0 }]]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const errors = validateWorkflowStructure(workflow);
|
||||
|
||||
// Should be valid - all functional nodes connected, sticky notes ignored
|
||||
expect(errors).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
240
tests/unit/utils/node-classification.test.ts
Normal file
240
tests/unit/utils/node-classification.test.ts
Normal file
@@ -0,0 +1,240 @@
|
||||
import { describe, test, expect } from 'vitest';
|
||||
import {
|
||||
isStickyNote,
|
||||
isTriggerNode,
|
||||
isNonExecutableNode,
|
||||
requiresIncomingConnection
|
||||
} from '@/utils/node-classification';
|
||||
|
||||
describe('Node Classification Utilities', () => {
|
||||
describe('isStickyNote', () => {
|
||||
test('should identify standard sticky note type', () => {
|
||||
expect(isStickyNote('n8n-nodes-base.stickyNote')).toBe(true);
|
||||
});
|
||||
|
||||
test('should identify normalized sticky note type', () => {
|
||||
expect(isStickyNote('nodes-base.stickyNote')).toBe(true);
|
||||
});
|
||||
|
||||
test('should identify scoped sticky note type', () => {
|
||||
expect(isStickyNote('@n8n/n8n-nodes-base.stickyNote')).toBe(true);
|
||||
});
|
||||
|
||||
test('should return false for webhook node', () => {
|
||||
expect(isStickyNote('n8n-nodes-base.webhook')).toBe(false);
|
||||
});
|
||||
|
||||
test('should return false for HTTP request node', () => {
|
||||
expect(isStickyNote('n8n-nodes-base.httpRequest')).toBe(false);
|
||||
});
|
||||
|
||||
test('should return false for manual trigger node', () => {
|
||||
expect(isStickyNote('n8n-nodes-base.manualTrigger')).toBe(false);
|
||||
});
|
||||
|
||||
test('should return false for Set node', () => {
|
||||
expect(isStickyNote('n8n-nodes-base.set')).toBe(false);
|
||||
});
|
||||
|
||||
test('should return false for empty string', () => {
|
||||
expect(isStickyNote('')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isTriggerNode', () => {
|
||||
test('should identify webhook trigger', () => {
|
||||
expect(isTriggerNode('n8n-nodes-base.webhook')).toBe(true);
|
||||
});
|
||||
|
||||
test('should identify webhook trigger variant', () => {
|
||||
expect(isTriggerNode('n8n-nodes-base.webhookTrigger')).toBe(true);
|
||||
});
|
||||
|
||||
test('should identify manual trigger', () => {
|
||||
expect(isTriggerNode('n8n-nodes-base.manualTrigger')).toBe(true);
|
||||
});
|
||||
|
||||
test('should identify cron trigger', () => {
|
||||
expect(isTriggerNode('n8n-nodes-base.cronTrigger')).toBe(true);
|
||||
});
|
||||
|
||||
test('should identify schedule trigger', () => {
|
||||
expect(isTriggerNode('n8n-nodes-base.scheduleTrigger')).toBe(true);
|
||||
});
|
||||
|
||||
test('should return false for HTTP request node', () => {
|
||||
expect(isTriggerNode('n8n-nodes-base.httpRequest')).toBe(false);
|
||||
});
|
||||
|
||||
test('should return false for Set node', () => {
|
||||
expect(isTriggerNode('n8n-nodes-base.set')).toBe(false);
|
||||
});
|
||||
|
||||
test('should return false for sticky note', () => {
|
||||
expect(isTriggerNode('n8n-nodes-base.stickyNote')).toBe(false);
|
||||
});
|
||||
|
||||
test('should return false for empty string', () => {
|
||||
expect(isTriggerNode('')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isNonExecutableNode', () => {
|
||||
test('should identify sticky note as non-executable', () => {
|
||||
expect(isNonExecutableNode('n8n-nodes-base.stickyNote')).toBe(true);
|
||||
});
|
||||
|
||||
test('should identify all sticky note variations as non-executable', () => {
|
||||
expect(isNonExecutableNode('nodes-base.stickyNote')).toBe(true);
|
||||
expect(isNonExecutableNode('@n8n/n8n-nodes-base.stickyNote')).toBe(true);
|
||||
});
|
||||
|
||||
test('should return false for webhook trigger', () => {
|
||||
expect(isNonExecutableNode('n8n-nodes-base.webhook')).toBe(false);
|
||||
});
|
||||
|
||||
test('should return false for HTTP request node', () => {
|
||||
expect(isNonExecutableNode('n8n-nodes-base.httpRequest')).toBe(false);
|
||||
});
|
||||
|
||||
test('should return false for Set node', () => {
|
||||
expect(isNonExecutableNode('n8n-nodes-base.set')).toBe(false);
|
||||
});
|
||||
|
||||
test('should return false for manual trigger', () => {
|
||||
expect(isNonExecutableNode('n8n-nodes-base.manualTrigger')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('requiresIncomingConnection', () => {
|
||||
describe('non-executable nodes (should not require connections)', () => {
|
||||
test('should return false for sticky note', () => {
|
||||
expect(requiresIncomingConnection('n8n-nodes-base.stickyNote')).toBe(false);
|
||||
});
|
||||
|
||||
test('should return false for all sticky note variations', () => {
|
||||
expect(requiresIncomingConnection('nodes-base.stickyNote')).toBe(false);
|
||||
expect(requiresIncomingConnection('@n8n/n8n-nodes-base.stickyNote')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('trigger nodes (should not require incoming connections)', () => {
|
||||
test('should return false for webhook', () => {
|
||||
expect(requiresIncomingConnection('n8n-nodes-base.webhook')).toBe(false);
|
||||
});
|
||||
|
||||
test('should return false for webhook trigger', () => {
|
||||
expect(requiresIncomingConnection('n8n-nodes-base.webhookTrigger')).toBe(false);
|
||||
});
|
||||
|
||||
test('should return false for manual trigger', () => {
|
||||
expect(requiresIncomingConnection('n8n-nodes-base.manualTrigger')).toBe(false);
|
||||
});
|
||||
|
||||
test('should return false for cron trigger', () => {
|
||||
expect(requiresIncomingConnection('n8n-nodes-base.cronTrigger')).toBe(false);
|
||||
});
|
||||
|
||||
test('should return false for schedule trigger', () => {
|
||||
expect(requiresIncomingConnection('n8n-nodes-base.scheduleTrigger')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('regular nodes (should require incoming connections)', () => {
|
||||
test('should return true for HTTP request node', () => {
|
||||
expect(requiresIncomingConnection('n8n-nodes-base.httpRequest')).toBe(true);
|
||||
});
|
||||
|
||||
test('should return true for Set node', () => {
|
||||
expect(requiresIncomingConnection('n8n-nodes-base.set')).toBe(true);
|
||||
});
|
||||
|
||||
test('should return true for Code node', () => {
|
||||
expect(requiresIncomingConnection('n8n-nodes-base.code')).toBe(true);
|
||||
});
|
||||
|
||||
test('should return true for Function node', () => {
|
||||
expect(requiresIncomingConnection('n8n-nodes-base.function')).toBe(true);
|
||||
});
|
||||
|
||||
test('should return true for IF node', () => {
|
||||
expect(requiresIncomingConnection('n8n-nodes-base.if')).toBe(true);
|
||||
});
|
||||
|
||||
test('should return true for Switch node', () => {
|
||||
expect(requiresIncomingConnection('n8n-nodes-base.switch')).toBe(true);
|
||||
});
|
||||
|
||||
test('should return true for Respond to Webhook node', () => {
|
||||
expect(requiresIncomingConnection('n8n-nodes-base.respondToWebhook')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('edge cases', () => {
|
||||
test('should return true for unknown node types (conservative approach)', () => {
|
||||
expect(requiresIncomingConnection('unknown-package.unknownNode')).toBe(true);
|
||||
});
|
||||
|
||||
test('should return true for empty string', () => {
|
||||
expect(requiresIncomingConnection('')).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('integration scenarios', () => {
|
||||
test('sticky notes should be non-executable and not require connections', () => {
|
||||
const stickyType = 'n8n-nodes-base.stickyNote';
|
||||
expect(isNonExecutableNode(stickyType)).toBe(true);
|
||||
expect(requiresIncomingConnection(stickyType)).toBe(false);
|
||||
expect(isStickyNote(stickyType)).toBe(true);
|
||||
expect(isTriggerNode(stickyType)).toBe(false);
|
||||
});
|
||||
|
||||
test('webhook nodes should be triggers and not require incoming connections', () => {
|
||||
const webhookType = 'n8n-nodes-base.webhook';
|
||||
expect(isTriggerNode(webhookType)).toBe(true);
|
||||
expect(requiresIncomingConnection(webhookType)).toBe(false);
|
||||
expect(isNonExecutableNode(webhookType)).toBe(false);
|
||||
expect(isStickyNote(webhookType)).toBe(false);
|
||||
});
|
||||
|
||||
test('regular nodes should require incoming connections', () => {
|
||||
const httpType = 'n8n-nodes-base.httpRequest';
|
||||
expect(requiresIncomingConnection(httpType)).toBe(true);
|
||||
expect(isNonExecutableNode(httpType)).toBe(false);
|
||||
expect(isTriggerNode(httpType)).toBe(false);
|
||||
expect(isStickyNote(httpType)).toBe(false);
|
||||
});
|
||||
|
||||
test('all trigger types should not require incoming connections', () => {
|
||||
const triggerTypes = [
|
||||
'n8n-nodes-base.webhook',
|
||||
'n8n-nodes-base.webhookTrigger',
|
||||
'n8n-nodes-base.manualTrigger',
|
||||
'n8n-nodes-base.cronTrigger',
|
||||
'n8n-nodes-base.scheduleTrigger'
|
||||
];
|
||||
|
||||
triggerTypes.forEach(type => {
|
||||
expect(isTriggerNode(type)).toBe(true);
|
||||
expect(requiresIncomingConnection(type)).toBe(false);
|
||||
expect(isNonExecutableNode(type)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
test('all sticky note variations should be non-executable', () => {
|
||||
const stickyTypes = [
|
||||
'n8n-nodes-base.stickyNote',
|
||||
'nodes-base.stickyNote',
|
||||
'@n8n/n8n-nodes-base.stickyNote'
|
||||
];
|
||||
|
||||
stickyTypes.forEach(type => {
|
||||
expect(isStickyNote(type)).toBe(true);
|
||||
expect(isNonExecutableNode(type)).toBe(true);
|
||||
expect(requiresIncomingConnection(type)).toBe(false);
|
||||
expect(isTriggerNode(type)).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -206,7 +206,7 @@ describe('Validation System Fixes', () => {
|
||||
const result = await workflowValidator.validateWorkflow(workflow);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.statistics.totalNodes).toBe(1); // Only webhook, sticky note excluded
|
||||
expect(result.statistics.totalNodes).toBe(1); // Only webhook, non-executable nodes excluded
|
||||
expect(result.statistics.enabledNodes).toBe(1);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user