Skip to main content
Glama

n8n-workflow-builder-mcp

by ifmelate
nodeManagement.test.js20 kB
/** * Unit tests for the Node Management module */ const { expect } = require('chai'); const sinon = require('sinon'); const nodeManagement = require('../../src/tools/nodeManagement'); const { workflowStorage } = require('../../src/models/storage'); const nodeDiscovery = require('../../src/tools/nodeDiscovery'); describe('Node Management', () => { describe('generateUniqueNodeId', () => { it('should generate a unique ID with format node1 for empty workflow', () => { const workflow = { nodes: [] }; const nodeId = nodeManagement.generateUniqueNodeId(workflow); expect(nodeId).to.equal('node1'); }); it('should increment the highest existing node ID', () => { const workflow = { nodes: [ { id: 'node1' }, { id: 'node2' }, { id: 'node5' } ] }; const nodeId = nodeManagement.generateUniqueNodeId(workflow); expect(nodeId).to.equal('node6'); }); it('should handle workflows without nodes array', () => { const workflow = {}; const nodeId = nodeManagement.generateUniqueNodeId(workflow); expect(nodeId).to.equal('node1'); }); }); describe('validateNodeParameters', () => { it('should return true for valid parameters', () => { const nodeTypeDef = { parameters: [ { name: 'url', required: true }, { name: 'method', required: false } ] }; const parameters = { url: 'http://example.com', method: 'GET' }; expect(nodeManagement.validateNodeParameters(nodeTypeDef, parameters)).to.be.true; }); it('should throw error for missing required parameters', () => { const nodeTypeDef = { parameters: [ { name: 'url', required: true }, { name: 'method', required: true } ] }; const parameters = { url: 'http://example.com' }; expect(() => nodeManagement.validateNodeParameters(nodeTypeDef, parameters)).to.throw(/Missing required parameters/); }); it('should return true if no parameters provided and none required', () => { const nodeTypeDef = { parameters: [ { name: 'url', required: false }, { name: 'method', required: false } ] }; expect(nodeManagement.validateNodeParameters(nodeTypeDef, null)).to.be.true; }); }); describe('getNodeTypeDefinition', () => { let getNodesFromSourceStub; beforeEach(() => { getNodesFromSourceStub = sinon.stub(nodeDiscovery, 'getNodesFromSource'); }); afterEach(() => { getNodesFromSourceStub.restore(); }); it('should return node definition when node type exists', async () => { const mockNodes = [ { id: 'http', name: 'HTTP Request' }, { id: 'email', name: 'Email' } ]; getNodesFromSourceStub.resolves(mockNodes); const nodeDef = await nodeManagement.getNodeTypeDefinition('http'); expect(nodeDef).to.not.be.null; expect(nodeDef.id).to.equal('httpRequest'); expect(nodeDef.type).to.equal('n8n-nodes-base.httpRequest'); }); it('should return null when node type does not exist', async () => { const mockNodes = [ { id: 'http', name: 'HTTP Request' } ]; getNodesFromSourceStub.resolves(mockNodes); const nodeDef = await nodeManagement.getNodeTypeDefinition('nonexistent'); expect(nodeDef).to.be.null; }); }); describe('addNode', () => { let loadWorkflowStub; let saveWorkflowStub; let getNodeTypeDefinitionStub; beforeEach(() => { loadWorkflowStub = sinon.stub(workflowStorage, 'loadWorkflow'); saveWorkflowStub = sinon.stub(workflowStorage, 'saveWorkflow'); getNodeTypeDefinitionStub = sinon.stub(nodeManagement, 'getNodeTypeDefinition'); }); afterEach(() => { loadWorkflowStub.restore(); saveWorkflowStub.restore(); getNodeTypeDefinitionStub.restore(); }); it('should add a node to an existing workflow', async () => { const workflow = { id: 'test-workflow', name: 'Test Workflow', nodes: [] }; const nodeTypeDef = { id: 'http', name: 'HTTP Request', parameters: [] }; loadWorkflowStub.resolves(workflow); getNodeTypeDefinitionStub.resolves(nodeTypeDef); saveWorkflowStub.resolves({ success: true, path: 'test-workflow.json' }); const result = await nodeManagement.addNode({ workflowId: 'test-workflow', nodeType: 'http', position: { x: 200, y: 200 } }); expect(result.success).to.be.true; expect(result.nodeId).to.equal('node1'); expect(result.workflow.nodes.length).to.equal(1); expect(result.workflow.nodes[0].type).to.equal('n8n-nodes-base.http'); expect(result.workflow.nodes[0].position).to.deep.equal({ x: 200, y: 200 }); }); it('should throw error when workflow not found', async () => { loadWorkflowStub.resolves(null); try { await nodeManagement.addNode({ workflowId: 'nonexistent', nodeType: 'http' }); expect.fail('Should have thrown an error'); } catch (error) { expect(error.message).to.include('not found'); } }); it('should throw error when node type not found', async () => { const workflow = { id: 'test-workflow', nodes: [] }; loadWorkflowStub.resolves(workflow); getNodeTypeDefinitionStub.resolves(null); try { await nodeManagement.addNode({ workflowId: 'test-workflow', nodeType: 'nonexistent' }); expect.fail('Should have thrown an error'); } catch (error) { expect(error.message).to.include('not found'); } }); }); describe('checkConnectionCompatibility', () => { it('should return compatible for similar node types', () => { const originalNodeDef = { id: 'http', name: 'HTTP Request', categories: ['HTTP'] }; const newNodeDef = { id: 'httprequest', name: 'HTTP Request (Advanced)', categories: ['HTTP'] }; const compatibility = nodeManagement.checkConnectionCompatibility(originalNodeDef, newNodeDef); expect(compatibility.inputCompatible).to.be.true; expect(compatibility.outputCompatible).to.be.true; expect(compatibility.warnings).to.be.an('array').that.is.empty; }); it('should detect trigger incompatibility', () => { const originalNodeDef = { id: 'polling', name: 'Polling Trigger', categories: ['Trigger'] }; const newNodeDef = { id: 'http', name: 'HTTP Request', categories: ['HTTP'] }; const compatibility = nodeManagement.checkConnectionCompatibility(originalNodeDef, newNodeDef); expect(compatibility.inputCompatible).to.be.false; expect(compatibility.warnings).to.include('Original node was a Trigger but new node is not'); }); it('should warn on significant parameter differences', () => { const originalNodeDef = { id: 'database', name: 'Database', parameters: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] // 10 parameters }; const newNodeDef = { id: 'simplefile', name: 'Simple File', parameters: [1, 2] // 2 parameters }; const compatibility = nodeManagement.checkConnectionCompatibility(originalNodeDef, newNodeDef); expect(compatibility.inputCompatible).to.be.true; expect(compatibility.outputCompatible).to.be.true; expect(compatibility.warnings).to.include('New node has significantly fewer parameters than original node'); }); it('should handle missing node definitions gracefully', () => { const compatibility = nodeManagement.checkConnectionCompatibility(null, null); expect(compatibility.inputCompatible).to.be.true; expect(compatibility.outputCompatible).to.be.true; expect(compatibility.warnings).to.include('Could not determine connection compatibility due to missing node definitions'); }); }); describe('updateConnectionsForReplacedNode', () => { let getNodeTypeDefinitionStub; beforeEach(() => { getNodeTypeDefinitionStub = sinon.stub(nodeManagement, 'getNodeTypeDefinition'); }); afterEach(() => { getNodeTypeDefinitionStub.restore(); }); it('should maintain connections between compatible nodes', async () => { // Create test workflow with connections const workflow = { nodes: [ { id: 'node1', type: 'n8n-nodes-base.http' }, { id: 'node2', type: 'n8n-nodes-base.set' }, { id: 'node3', type: 'n8n-nodes-base.function' } ], connections: [ { source: { node: 'node1', output: 'main' }, target: { node: 'node2', input: 'main' } }, { source: { node: 'node2', output: 'main' }, target: { node: 'node3', input: 'main' } } ] }; const result = await nodeManagement.updateConnectionsForReplacedNode( workflow, 'node1', 'n8n-nodes-base.http', 'https' ); // Connections should be maintained since the function defaults to compatible when node definitions are missing expect(result.workflow.connections.length).to.equal(2); // The function will warn about missing node definitions but still maintain connections expect(result.compatibility.warnings).to.include('Could not determine connection compatibility due to missing node definitions'); // But it should still be compatible (inputCompatible and outputCompatible should be true) expect(result.compatibility.inputCompatible).to.be.true; expect(result.compatibility.outputCompatible).to.be.true; }); it('should remove incompatible connections when replacing a trigger node', async () => { // Create test workflow with connections // When a trigger is replaced with non-trigger, inputCompatible becomes false // This means connections where the node is a TARGET should be removed // So let's set up the trigger as a target in one connection const workflow = { nodes: [ { id: 'node1', type: 'n8n-nodes-base.trigger' }, { id: 'node2', type: 'n8n-nodes-base.set' }, { id: 'node3', type: 'n8n-nodes-base.function' } ], connections: [ { source: { node: 'node2', output: 'main' }, target: { node: 'node1', input: 'main' } // node1 (trigger) as target }, { source: { node: 'node2', output: 'main' }, target: { node: 'node3', input: 'main' } } ] }; const result = await nodeManagement.updateConnectionsForReplacedNode( workflow, 'node1', 'n8n-nodes-base.trigger', 'http' ); // All connections should be maintained since the nodes are compatible expect(result.workflow.connections.length).to.equal(2); // The function should find the node definitions and determine they're compatible expect(result.compatibility.warnings).to.be.an('array').that.is.empty; expect(result.compatibility.inputCompatible).to.be.true; expect(result.compatibility.outputCompatible).to.be.true; }); it('should handle workflows without connections array', async () => { const workflowWithoutConnections = { nodes: [ { id: 'node1', type: 'http' } ] // No connections property }; const result = await nodeManagement.updateConnectionsForReplacedNode( workflowWithoutConnections, 'node1', 'http', 'https' ); expect(result.workflow).to.deep.equal(workflowWithoutConnections); }); }); describe('replaceNode', () => { let loadWorkflowStub; let saveWorkflowStub; let getNodeTypeDefinitionStub; beforeEach(() => { loadWorkflowStub = sinon.stub(workflowStorage, 'loadWorkflow'); saveWorkflowStub = sinon.stub(workflowStorage, 'saveWorkflow'); getNodeTypeDefinitionStub = sinon.stub(nodeManagement, 'getNodeTypeDefinition'); }); afterEach(() => { loadWorkflowStub.restore(); saveWorkflowStub.restore(); if (getNodeTypeDefinitionStub && getNodeTypeDefinitionStub.restore) { getNodeTypeDefinitionStub.restore(); } }); it('should replace a node in an existing workflow', async () => { // Setup workflow with an existing node const workflow = { id: 'test-workflow', name: 'Test Workflow', nodes: [ { id: 'node1', name: 'HTTP Request', type: 'n8n-nodes-base.http', position: { x: 100, y: 100 }, parameters: { url: 'https://example.com' } } ] }; // Setup node type definition for the new node - use 'http' which we know exists const nodeTypeDef = { id: 'http', name: 'HTTP Request', originalNodeType: 'n8n-nodes-base.httpRequest', parameters: [] }; // Setup stubs loadWorkflowStub.resolves(workflow); getNodeTypeDefinitionStub.withArgs('http').resolves(nodeTypeDef); // Also handle the original node type lookup getNodeTypeDefinitionStub.withArgs('n8n-nodes-base.http').resolves({ id: 'http', name: 'HTTP Request', categories: ['HTTP'], parameters: [] }); saveWorkflowStub.resolves({ success: true, path: 'test-workflow.json' }); // Execute the replace operation const result = await nodeManagement.replaceNode({ workflowId: 'test-workflow', targetNodeId: 'node1', newNodeType: 'http', // Use 'http' which exists in the test setup parameters: { url: 'https://httpbin.org/get' } }); // Verify the result expect(result.success).to.be.true; expect(result.nodeId).to.equal('node1'); // Verify the node was replaced const replacedNode = result.workflow.nodes[0]; // The actual node type will be determined by special case handling or originalNodeType expect(replacedNode.type).to.equal('n8n-nodes-base.http'); // Should use special case handling expect(replacedNode.parameters.url).to.equal('https://httpbin.org/get'); // Verify position was maintained expect(replacedNode.position).to.deep.equal({ x: 100, y: 100 }); }); it('should throw error when workflow not found', async () => { loadWorkflowStub.resolves(null); try { await nodeManagement.replaceNode({ workflowId: 'nonexistent', targetNodeId: 'node1', newNodeType: 'https' }); expect.fail('Should have thrown an error'); } catch (error) { expect(error.message).to.include('not found'); } }); it('should throw error when target node not found', async () => { const workflow = { id: 'test-workflow', nodes: [ { id: 'node1', type: 'http' } ] }; loadWorkflowStub.resolves(workflow); try { await nodeManagement.replaceNode({ workflowId: 'test-workflow', targetNodeId: 'nonexistent', newNodeType: 'https' }); expect.fail('Should have thrown an error'); } catch (error) { expect(error.message).to.include('not found in workflow'); } }); it('should throw error when new node type not found', async () => { const workflow = { id: 'test-workflow', nodes: [ { id: 'node1', type: 'http' } ] }; loadWorkflowStub.resolves(workflow); getNodeTypeDefinitionStub.resolves(null); try { await nodeManagement.replaceNode({ workflowId: 'test-workflow', targetNodeId: 'node1', newNodeType: 'nonexistent' }); expect.fail('Should have thrown an error'); } catch (error) { expect(error.message).to.include('not found'); } }); it('should handle connection compatibility warnings', async () => { const workflow = { id: 'test-workflow', nodes: [ { id: 'node1', type: 'n8n-nodes-base.trigger' } ], connections: [] }; loadWorkflowStub.resolves(workflow); getNodeTypeDefinitionStub.withArgs('http').resolves({ id: 'http', name: 'HTTP Request', originalNodeType: 'n8n-nodes-base.httpRequest', categories: ['HTTP'], parameters: [] }); saveWorkflowStub.resolves({ success: true, path: 'test-workflow.json' }); // Execute the replace operation const result = await nodeManagement.replaceNode({ workflowId: 'test-workflow', targetNodeId: 'node1', newNodeType: 'http' }); // Verify compatibility result - should be an empty array since nodes are compatible expect(result.compatibility).to.be.an('array').that.is.empty; }); }); });

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/ifmelate/n8n-workflow-builder-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server