mirror of
https://github.com/czlonkowski/n8n-mcp.git
synced 2026-01-30 06:22:04 +00:00
Fixed validation bug where mcpTrigger nodes were incorrectly flagged as "disconnected nodes" when using n8n_update_partial_workflow or n8n_update_full_workflow. This blocked ALL updates to MCP server workflows. Changes: - Extended validateWorkflowStructure() to check all 7 connection types (main, error, ai_tool, ai_languageModel, ai_memory, ai_embedding, ai_vectorStore) - Updated trigger node validation to accept either outgoing OR inbound connections - Added 7 new tests covering all AI connection types Fixes #503 Conceived by Romuald Członkowski - https://www.aiadvisors.pl/en 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Romuald Członkowski <romualdczlonkowski@MacBook-Pro-Romuald.local> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
committed by
GitHub
parent
d60182eeb8
commit
705d31c35e
21
CHANGELOG.md
21
CHANGELOG.md
@@ -7,6 +7,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [2.31.1] - 2025-12-23
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
**mcpTrigger Nodes No Longer Incorrectly Flagged as "Disconnected" (Issue #503)**
|
||||||
|
|
||||||
|
Fixed a validation bug where `mcpTrigger` nodes were incorrectly flagged as "disconnected nodes" when using `n8n_update_partial_workflow` or `n8n_update_full_workflow`. This blocked ALL updates to MCP server workflows.
|
||||||
|
|
||||||
|
**Root Cause:**
|
||||||
|
The `validateWorkflowStructure()` function only checked `main` connections when building the connected nodes set, ignoring AI connection types (`ai_tool`, `ai_languageModel`, `ai_memory`, `ai_embedding`, `ai_vectorStore`). Additionally, trigger nodes were only checked for outgoing connections, but `mcpTrigger` only receives inbound `ai_tool` connections.
|
||||||
|
|
||||||
|
**Changes:**
|
||||||
|
- Extended connection validation to check all 7 connection types (main, error, ai_tool, ai_languageModel, ai_memory, ai_embedding, ai_vectorStore)
|
||||||
|
- Updated trigger node validation to accept either outgoing OR inbound connections
|
||||||
|
- Added 7 new tests covering all AI connection types
|
||||||
|
|
||||||
|
**Impact:**
|
||||||
|
- MCP server workflows can now be updated, renamed, and deactivated normally
|
||||||
|
- All `n8n_update_*` operations work correctly for AI workflows
|
||||||
|
- No breaking changes for existing workflows
|
||||||
|
|
||||||
## [2.31.0] - 2025-12-23
|
## [2.31.0] - 2025-12-23
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
2
dist/services/n8n-validation.d.ts.map
vendored
2
dist/services/n8n-validation.d.ts.map
vendored
@@ -1 +1 @@
|
|||||||
{"version":3,"file":"n8n-validation.d.ts","sourceRoot":"","sources":["../../src/services/n8n-validation.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,YAAY,EAAE,kBAAkB,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAM9E,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAiB7B,CAAC;AAkBH,eAAO,MAAM,wBAAwB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAUpC,CAAC;AAEF,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAWjC,CAAC;AAGH,eAAO,MAAM,uBAAuB;;;;;;CAMnC,CAAC;AAGF,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,OAAO,GAAG,YAAY,CAEhE;AAED,wBAAgB,2BAA2B,CAAC,WAAW,EAAE,OAAO,GAAG,kBAAkB,CAEpF;AAED,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,OAAO,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAElG;AAGD,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,OAAO,CAAC,QAAQ,CAAC,GAAG,OAAO,CAAC,QAAQ,CAAC,CAsBrF;AAiBD,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAoE5E;AAGD,wBAAgB,yBAAyB,CAAC,QAAQ,EAAE,OAAO,CAAC,QAAQ,CAAC,GAAG,MAAM,EAAE,CAiP/E;AAGD,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAK7D;AAMD,wBAAgB,+BAA+B,CAAC,IAAI,EAAE,YAAY,GAAG,MAAM,EAAE,CA+F5E;AAMD,wBAAgB,yBAAyB,CAAC,QAAQ,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CA0D/E;AAGD,wBAAgB,aAAa,CAAC,QAAQ,EAAE,QAAQ,GAAG,MAAM,GAAG,IAAI,CAmB/D;AAGD,wBAAgB,2BAA2B,IAAI,MAAM,CA6CpD;AAGD,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CAmBpE"}
|
{"version":3,"file":"n8n-validation.d.ts","sourceRoot":"","sources":["../../src/services/n8n-validation.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,YAAY,EAAE,kBAAkB,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAM9E,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAiB7B,CAAC;AAkBH,eAAO,MAAM,wBAAwB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAUpC,CAAC;AAEF,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAWjC,CAAC;AAGH,eAAO,MAAM,uBAAuB;;;;;;CAMnC,CAAC;AAGF,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,OAAO,GAAG,YAAY,CAEhE;AAED,wBAAgB,2BAA2B,CAAC,WAAW,EAAE,OAAO,GAAG,kBAAkB,CAEpF;AAED,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,OAAO,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAElG;AAGD,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,OAAO,CAAC,QAAQ,CAAC,GAAG,OAAO,CAAC,QAAQ,CAAC,CAsBrF;AAiBD,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAoE5E;AAGD,wBAAgB,yBAAyB,CAAC,QAAQ,EAAE,OAAO,CAAC,QAAQ,CAAC,GAAG,MAAM,EAAE,CA6P/E;AAGD,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAK7D;AAMD,wBAAgB,+BAA+B,CAAC,IAAI,EAAE,YAAY,GAAG,MAAM,EAAE,CA+F5E;AAMD,wBAAgB,yBAAyB,CAAC,QAAQ,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CA0D/E;AAGD,wBAAgB,aAAa,CAAC,QAAQ,EAAE,QAAQ,GAAG,MAAM,GAAG,IAAI,CAmB/D;AAGD,wBAAgB,2BAA2B,IAAI,MAAM,CA6CpD;AAGD,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CAmBpE"}
|
||||||
28
dist/services/n8n-validation.js
vendored
28
dist/services/n8n-validation.js
vendored
@@ -152,17 +152,23 @@ function validateWorkflowStructure(workflow) {
|
|||||||
}
|
}
|
||||||
else if (connectionCount > 0 || executableNodes.length > 1) {
|
else if (connectionCount > 0 || executableNodes.length > 1) {
|
||||||
const connectedNodes = new Set();
|
const connectedNodes = new Set();
|
||||||
|
const ALL_CONNECTION_TYPES = ['main', 'error', 'ai_tool', 'ai_languageModel', 'ai_memory', 'ai_embedding', 'ai_vectorStore'];
|
||||||
Object.entries(workflow.connections).forEach(([sourceName, connection]) => {
|
Object.entries(workflow.connections).forEach(([sourceName, connection]) => {
|
||||||
connectedNodes.add(sourceName);
|
connectedNodes.add(sourceName);
|
||||||
if (connection.main && Array.isArray(connection.main)) {
|
ALL_CONNECTION_TYPES.forEach(connType => {
|
||||||
connection.main.forEach((outputs) => {
|
const connData = connection[connType];
|
||||||
if (Array.isArray(outputs)) {
|
if (connData && Array.isArray(connData)) {
|
||||||
outputs.forEach((target) => {
|
connData.forEach((outputs) => {
|
||||||
connectedNodes.add(target.node);
|
if (Array.isArray(outputs)) {
|
||||||
});
|
outputs.forEach((target) => {
|
||||||
}
|
if (target?.node) {
|
||||||
});
|
connectedNodes.add(target.node);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
const disconnectedNodes = workflow.nodes.filter(node => {
|
const disconnectedNodes = workflow.nodes.filter(node => {
|
||||||
if ((0, node_classification_1.isNonExecutableNode)(node.type)) {
|
if ((0, node_classification_1.isNonExecutableNode)(node.type)) {
|
||||||
@@ -171,7 +177,9 @@ function validateWorkflowStructure(workflow) {
|
|||||||
const isConnected = connectedNodes.has(node.name);
|
const isConnected = connectedNodes.has(node.name);
|
||||||
const isNodeTrigger = (0, node_type_utils_1.isTriggerNode)(node.type);
|
const isNodeTrigger = (0, node_type_utils_1.isTriggerNode)(node.type);
|
||||||
if (isNodeTrigger) {
|
if (isNodeTrigger) {
|
||||||
return !workflow.connections?.[node.name];
|
const hasOutgoingConnections = !!workflow.connections?.[node.name];
|
||||||
|
const hasInboundConnections = isConnected;
|
||||||
|
return !hasOutgoingConnections && !hasInboundConnections;
|
||||||
}
|
}
|
||||||
return !isConnected;
|
return !isConnected;
|
||||||
});
|
});
|
||||||
|
|||||||
2
dist/services/n8n-validation.js.map
vendored
2
dist/services/n8n-validation.js.map
vendored
File diff suppressed because one or more lines are too long
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "n8n-mcp",
|
"name": "n8n-mcp",
|
||||||
"version": "2.31.0",
|
"version": "2.31.1",
|
||||||
"description": "Integration between n8n workflow automation and Model Context Protocol (MCP)",
|
"description": "Integration between n8n workflow automation and Model Context Protocol (MCP)",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"types": "dist/index.d.ts",
|
"types": "dist/index.d.ts",
|
||||||
|
|||||||
@@ -248,23 +248,32 @@ export function validateWorkflowStructure(workflow: Partial<Workflow>): string[]
|
|||||||
const connectedNodes = new Set<string>();
|
const connectedNodes = new Set<string>();
|
||||||
|
|
||||||
// Collect all nodes that appear in connections (as source or target)
|
// Collect all nodes that appear in connections (as source or target)
|
||||||
|
// Check ALL connection types, not just 'main' - AI workflows use ai_tool, ai_languageModel, etc.
|
||||||
|
const ALL_CONNECTION_TYPES = ['main', 'error', 'ai_tool', 'ai_languageModel', 'ai_memory', 'ai_embedding', 'ai_vectorStore'] as const;
|
||||||
|
|
||||||
Object.entries(workflow.connections).forEach(([sourceName, connection]) => {
|
Object.entries(workflow.connections).forEach(([sourceName, connection]) => {
|
||||||
connectedNodes.add(sourceName); // Node has outgoing connection
|
connectedNodes.add(sourceName); // Node has outgoing connection
|
||||||
|
|
||||||
if (connection.main && Array.isArray(connection.main)) {
|
// Check all connection types for target nodes
|
||||||
connection.main.forEach((outputs) => {
|
ALL_CONNECTION_TYPES.forEach(connType => {
|
||||||
if (Array.isArray(outputs)) {
|
const connData = (connection as Record<string, unknown>)[connType];
|
||||||
outputs.forEach((target) => {
|
if (connData && Array.isArray(connData)) {
|
||||||
connectedNodes.add(target.node); // Node has incoming connection
|
connData.forEach((outputs) => {
|
||||||
});
|
if (Array.isArray(outputs)) {
|
||||||
}
|
outputs.forEach((target: { node: string }) => {
|
||||||
});
|
if (target?.node) {
|
||||||
}
|
connectedNodes.add(target.node); // Node has incoming connection
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Find disconnected nodes (excluding non-executable nodes and triggers)
|
// Find disconnected nodes (excluding non-executable nodes and triggers)
|
||||||
// Non-executable nodes (sticky notes) are UI-only and don't need connections
|
// Non-executable nodes (sticky notes) are UI-only and don't need connections
|
||||||
// Trigger nodes only need outgoing connections
|
// Trigger nodes need either outgoing connections OR inbound AI connections (for mcpTrigger)
|
||||||
const disconnectedNodes = workflow.nodes.filter(node => {
|
const disconnectedNodes = workflow.nodes.filter(node => {
|
||||||
// Skip non-executable nodes (sticky notes, etc.) - they're UI-only annotations
|
// Skip non-executable nodes (sticky notes, etc.) - they're UI-only annotations
|
||||||
if (isNonExecutableNode(node.type)) {
|
if (isNonExecutableNode(node.type)) {
|
||||||
@@ -274,9 +283,12 @@ export function validateWorkflowStructure(workflow: Partial<Workflow>): string[]
|
|||||||
const isConnected = connectedNodes.has(node.name);
|
const isConnected = connectedNodes.has(node.name);
|
||||||
const isNodeTrigger = isTriggerNode(node.type);
|
const isNodeTrigger = isTriggerNode(node.type);
|
||||||
|
|
||||||
// Trigger nodes only need outgoing connections
|
// Trigger nodes need outgoing connections OR inbound connections (for mcpTrigger)
|
||||||
|
// mcpTrigger is special: it has "trigger" in its name but only receives inbound ai_tool connections
|
||||||
if (isNodeTrigger) {
|
if (isNodeTrigger) {
|
||||||
return !workflow.connections?.[node.name]; // Disconnected if no outgoing connections
|
const hasOutgoingConnections = !!workflow.connections?.[node.name];
|
||||||
|
const hasInboundConnections = isConnected;
|
||||||
|
return !hasOutgoingConnections && !hasInboundConnections; // Disconnected if NEITHER
|
||||||
}
|
}
|
||||||
|
|
||||||
// Regular nodes need at least one connection (incoming or outgoing)
|
// Regular nodes need at least one connection (incoming or outgoing)
|
||||||
|
|||||||
@@ -884,6 +884,260 @@ describe('n8n-validation', () => {
|
|||||||
const errors = validateWorkflowStructure(workflow);
|
const errors = validateWorkflowStructure(workflow);
|
||||||
expect(errors.some(e => e.includes('Invalid connections'))).toBe(true);
|
expect(errors.some(e => e.includes('Invalid connections'))).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Issue #503: mcpTrigger nodes should not be flagged as disconnected
|
||||||
|
describe('AI connection types (Issue #503)', () => {
|
||||||
|
it('should NOT flag mcpTrigger as disconnected when it has ai_tool inbound connections', () => {
|
||||||
|
const workflow = {
|
||||||
|
name: 'MCP Server Workflow',
|
||||||
|
nodes: [
|
||||||
|
{
|
||||||
|
id: 'mcp-server',
|
||||||
|
name: 'MCP Server',
|
||||||
|
type: '@n8n/n8n-nodes-langchain.mcpTrigger',
|
||||||
|
typeVersion: 1,
|
||||||
|
position: [500, 300] as [number, number],
|
||||||
|
parameters: {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'tool-1',
|
||||||
|
name: 'Get Weather Tool',
|
||||||
|
type: '@n8n/n8n-nodes-langchain.toolWorkflow',
|
||||||
|
typeVersion: 1.3,
|
||||||
|
position: [300, 200] as [number, number],
|
||||||
|
parameters: {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'tool-2',
|
||||||
|
name: 'Search Tool',
|
||||||
|
type: '@n8n/n8n-nodes-langchain.toolWorkflow',
|
||||||
|
typeVersion: 1.3,
|
||||||
|
position: [300, 400] as [number, number],
|
||||||
|
parameters: {},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
connections: {
|
||||||
|
'Get Weather Tool': {
|
||||||
|
ai_tool: [[{ node: 'MCP Server', type: 'ai_tool', index: 0 }]],
|
||||||
|
},
|
||||||
|
'Search Tool': {
|
||||||
|
ai_tool: [[{ node: 'MCP Server', type: 'ai_tool', index: 0 }]],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const errors = validateWorkflowStructure(workflow);
|
||||||
|
const disconnectedErrors = errors.filter(e => e.includes('Disconnected'));
|
||||||
|
expect(disconnectedErrors).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should NOT flag nodes as disconnected when connected via ai_languageModel', () => {
|
||||||
|
const workflow = {
|
||||||
|
name: 'AI Agent Workflow',
|
||||||
|
nodes: [
|
||||||
|
{
|
||||||
|
id: 'agent-1',
|
||||||
|
name: 'AI Agent',
|
||||||
|
type: '@n8n/n8n-nodes-langchain.agent',
|
||||||
|
typeVersion: 1.6,
|
||||||
|
position: [500, 300] as [number, number],
|
||||||
|
parameters: {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'llm-1',
|
||||||
|
name: 'OpenAI Model',
|
||||||
|
type: '@n8n/n8n-nodes-langchain.lmChatOpenAi',
|
||||||
|
typeVersion: 1,
|
||||||
|
position: [300, 300] as [number, number],
|
||||||
|
parameters: {},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
connections: {
|
||||||
|
'OpenAI Model': {
|
||||||
|
ai_languageModel: [[{ node: 'AI Agent', type: 'ai_languageModel', index: 0 }]],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const errors = validateWorkflowStructure(workflow);
|
||||||
|
const disconnectedErrors = errors.filter(e => e.includes('Disconnected'));
|
||||||
|
expect(disconnectedErrors).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should NOT flag nodes as disconnected when connected via ai_memory', () => {
|
||||||
|
const workflow = {
|
||||||
|
name: 'AI Memory Workflow',
|
||||||
|
nodes: [
|
||||||
|
{
|
||||||
|
id: 'agent-1',
|
||||||
|
name: 'AI Agent',
|
||||||
|
type: '@n8n/n8n-nodes-langchain.agent',
|
||||||
|
typeVersion: 1.6,
|
||||||
|
position: [500, 300] as [number, number],
|
||||||
|
parameters: {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'memory-1',
|
||||||
|
name: 'Buffer Memory',
|
||||||
|
type: '@n8n/n8n-nodes-langchain.memoryBufferWindow',
|
||||||
|
typeVersion: 1,
|
||||||
|
position: [300, 400] as [number, number],
|
||||||
|
parameters: {},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
connections: {
|
||||||
|
'Buffer Memory': {
|
||||||
|
ai_memory: [[{ node: 'AI Agent', type: 'ai_memory', index: 0 }]],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const errors = validateWorkflowStructure(workflow);
|
||||||
|
const disconnectedErrors = errors.filter(e => e.includes('Disconnected'));
|
||||||
|
expect(disconnectedErrors).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should NOT flag nodes as disconnected when connected via ai_embedding', () => {
|
||||||
|
const workflow = {
|
||||||
|
name: 'Vector Store Workflow',
|
||||||
|
nodes: [
|
||||||
|
{
|
||||||
|
id: 'vs-1',
|
||||||
|
name: 'Vector Store',
|
||||||
|
type: '@n8n/n8n-nodes-langchain.vectorStorePinecone',
|
||||||
|
typeVersion: 1,
|
||||||
|
position: [500, 300] as [number, number],
|
||||||
|
parameters: {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'embed-1',
|
||||||
|
name: 'OpenAI Embeddings',
|
||||||
|
type: '@n8n/n8n-nodes-langchain.embeddingsOpenAi',
|
||||||
|
typeVersion: 1,
|
||||||
|
position: [300, 300] as [number, number],
|
||||||
|
parameters: {},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
connections: {
|
||||||
|
'OpenAI Embeddings': {
|
||||||
|
ai_embedding: [[{ node: 'Vector Store', type: 'ai_embedding', index: 0 }]],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const errors = validateWorkflowStructure(workflow);
|
||||||
|
const disconnectedErrors = errors.filter(e => e.includes('Disconnected'));
|
||||||
|
expect(disconnectedErrors).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should NOT flag nodes as disconnected when connected via ai_vectorStore', () => {
|
||||||
|
const workflow = {
|
||||||
|
name: 'Retriever Workflow',
|
||||||
|
nodes: [
|
||||||
|
{
|
||||||
|
id: 'retriever-1',
|
||||||
|
name: 'Vector Store Retriever',
|
||||||
|
type: '@n8n/n8n-nodes-langchain.retrieverVectorStore',
|
||||||
|
typeVersion: 1,
|
||||||
|
position: [500, 300] as [number, number],
|
||||||
|
parameters: {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'vs-1',
|
||||||
|
name: 'Pinecone Store',
|
||||||
|
type: '@n8n/n8n-nodes-langchain.vectorStorePinecone',
|
||||||
|
typeVersion: 1,
|
||||||
|
position: [300, 300] as [number, number],
|
||||||
|
parameters: {},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
connections: {
|
||||||
|
'Pinecone Store': {
|
||||||
|
ai_vectorStore: [[{ node: 'Vector Store Retriever', type: 'ai_vectorStore', index: 0 }]],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const errors = validateWorkflowStructure(workflow);
|
||||||
|
const disconnectedErrors = errors.filter(e => e.includes('Disconnected'));
|
||||||
|
expect(disconnectedErrors).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should NOT flag nodes as disconnected when connected via error output', () => {
|
||||||
|
const workflow = {
|
||||||
|
name: 'Error Handling Workflow',
|
||||||
|
nodes: [
|
||||||
|
{
|
||||||
|
id: 'http-1',
|
||||||
|
name: 'HTTP Request',
|
||||||
|
type: 'n8n-nodes-base.httpRequest',
|
||||||
|
typeVersion: 4.2,
|
||||||
|
position: [300, 300] as [number, number],
|
||||||
|
parameters: {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'set-1',
|
||||||
|
name: 'Handle Error',
|
||||||
|
type: 'n8n-nodes-base.set',
|
||||||
|
typeVersion: 3.4,
|
||||||
|
position: [500, 400] as [number, number],
|
||||||
|
parameters: {},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
connections: {
|
||||||
|
'HTTP Request': {
|
||||||
|
error: [[{ node: 'Handle Error', type: 'error', index: 0 }]],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const errors = validateWorkflowStructure(workflow);
|
||||||
|
const disconnectedErrors = errors.filter(e => e.includes('Disconnected'));
|
||||||
|
expect(disconnectedErrors).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should still flag truly disconnected nodes in AI workflows', () => {
|
||||||
|
const workflow = {
|
||||||
|
name: 'AI Workflow with Disconnected Node',
|
||||||
|
nodes: [
|
||||||
|
{
|
||||||
|
id: 'agent-1',
|
||||||
|
name: 'AI Agent',
|
||||||
|
type: '@n8n/n8n-nodes-langchain.agent',
|
||||||
|
typeVersion: 1.6,
|
||||||
|
position: [500, 300] as [number, number],
|
||||||
|
parameters: {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'llm-1',
|
||||||
|
name: 'OpenAI Model',
|
||||||
|
type: '@n8n/n8n-nodes-langchain.lmChatOpenAi',
|
||||||
|
typeVersion: 1,
|
||||||
|
position: [300, 300] as [number, number],
|
||||||
|
parameters: {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'disconnected-1',
|
||||||
|
name: 'Disconnected Set',
|
||||||
|
type: 'n8n-nodes-base.set',
|
||||||
|
typeVersion: 3.4,
|
||||||
|
position: [700, 300] as [number, number],
|
||||||
|
parameters: {},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
connections: {
|
||||||
|
'OpenAI Model': {
|
||||||
|
ai_languageModel: [[{ node: 'AI Agent', type: 'ai_languageModel', index: 0 }]],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const errors = validateWorkflowStructure(workflow);
|
||||||
|
const disconnectedErrors = errors.filter(e => e.includes('Disconnected'));
|
||||||
|
expect(disconnectedErrors.length).toBeGreaterThan(0);
|
||||||
|
expect(disconnectedErrors[0]).toContain('Disconnected Set');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('hasWebhookTrigger', () => {
|
describe('hasWebhookTrigger', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user