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:
Romuald Członkowski
2025-10-22 17:58:13 +02:00
committed by GitHub
parent 7300957d13
commit c76ffd9fb1
9 changed files with 1054 additions and 35 deletions

View 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;
}