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

@@ -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
}