diff --git a/tests/integration/n8n-api/workflows/update-partial-workflow.test.ts b/tests/integration/n8n-api/workflows/update-partial-workflow.test.ts new file mode 100644 index 0000000..898ba75 --- /dev/null +++ b/tests/integration/n8n-api/workflows/update-partial-workflow.test.ts @@ -0,0 +1,866 @@ +/** + * Integration Tests: handleUpdatePartialWorkflow + * + * Tests diff-based partial workflow updates against a real n8n instance. + * Covers all 15 operation types: node operations (6), connection operations (5), + * and metadata operations (4). + */ + +import { describe, it, expect, beforeEach, afterEach, afterAll } from 'vitest'; +import { createTestContext, TestContext, createTestWorkflowName } from '../utils/test-context'; +import { getTestN8nClient } from '../utils/n8n-client'; +import { N8nApiClient } from '../../../../src/services/n8n-api-client'; +import { SIMPLE_WEBHOOK_WORKFLOW, SIMPLE_HTTP_WORKFLOW, MULTI_NODE_WORKFLOW } from '../utils/fixtures'; +import { cleanupOrphanedWorkflows } from '../utils/cleanup-helpers'; +import { createMcpContext } from '../utils/mcp-context'; +import { InstanceContext } from '../../../../src/types/instance-context'; +import { handleUpdatePartialWorkflow } from '../../../../src/mcp/handlers-workflow-diff'; + +describe('Integration: handleUpdatePartialWorkflow', () => { + let context: TestContext; + let client: N8nApiClient; + let mcpContext: InstanceContext; + + beforeEach(() => { + context = createTestContext(); + client = getTestN8nClient(); + mcpContext = createMcpContext(); + }); + + afterEach(async () => { + await context.cleanup(); + }); + + afterAll(async () => { + if (!process.env.CI) { + await cleanupOrphanedWorkflows(); + } + }); + + // ====================================================================== + // NODE OPERATIONS (6 operations) + // ====================================================================== + + describe('Node Operations', () => { + describe('addNode', () => { + it('should add a new node to workflow', async () => { + // Create simple workflow + const workflow = { + ...SIMPLE_WEBHOOK_WORKFLOW, + name: createTestWorkflowName('Partial - Add Node'), + tags: ['mcp-integration-test'] + }; + + const created = await client.createWorkflow(workflow); + expect(created.id).toBeTruthy(); + if (!created.id) throw new Error('Workflow ID is missing'); + context.trackWorkflow(created.id); + + // Add a Set node + const response = await handleUpdatePartialWorkflow( + { + id: created.id, + operations: [ + { + type: 'addNode', + node: { + name: 'Set', + type: 'n8n-nodes-base.set', + typeVersion: 3.4, + position: [450, 300], + parameters: { + assignments: { + assignments: [ + { + id: 'assign-1', + name: 'test', + value: 'value', + type: 'string' + } + ] + } + } + } + } + ] + }, + mcpContext + ); + + expect(response.success).toBe(true); + const updated = response.data as any; + expect(updated.nodes).toHaveLength(2); + expect(updated.nodes.find((n: any) => n.name === 'Set')).toBeDefined(); + }); + + it('should return error for duplicate node name', async () => { + const workflow = { + ...SIMPLE_WEBHOOK_WORKFLOW, + name: createTestWorkflowName('Partial - Duplicate Node Name'), + tags: ['mcp-integration-test'] + }; + + const created = await client.createWorkflow(workflow); + expect(created.id).toBeTruthy(); + if (!created.id) throw new Error('Workflow ID is missing'); + context.trackWorkflow(created.id); + + // Try to add node with same name as existing + const response = await handleUpdatePartialWorkflow( + { + id: created.id, + operations: [ + { + type: 'addNode', + node: { + name: 'Webhook', // Duplicate name + type: 'n8n-nodes-base.set', + typeVersion: 3.4, + position: [450, 300], + parameters: {} + } + } + ] + }, + mcpContext + ); + + expect(response.success).toBe(false); + expect(response.error).toBeDefined(); + }); + }); + + describe('removeNode', () => { + it('should remove node by name', async () => { + const workflow = { + ...SIMPLE_HTTP_WORKFLOW, + name: createTestWorkflowName('Partial - Remove Node'), + tags: ['mcp-integration-test'] + }; + + const created = await client.createWorkflow(workflow); + expect(created.id).toBeTruthy(); + if (!created.id) throw new Error('Workflow ID is missing'); + context.trackWorkflow(created.id); + + // Remove HTTP Request node by name + const response = await handleUpdatePartialWorkflow( + { + id: created.id, + operations: [ + { + type: 'removeNode', + nodeName: 'HTTP Request' + } + ] + }, + mcpContext + ); + + expect(response.success).toBe(true); + const updated = response.data as any; + expect(updated.nodes).toHaveLength(1); + expect(updated.nodes.find((n: any) => n.name === 'HTTP Request')).toBeUndefined(); + }); + + it('should return error for non-existent node', async () => { + const workflow = { + ...SIMPLE_WEBHOOK_WORKFLOW, + name: createTestWorkflowName('Partial - Remove Non-existent'), + tags: ['mcp-integration-test'] + }; + + const created = await client.createWorkflow(workflow); + expect(created.id).toBeTruthy(); + if (!created.id) throw new Error('Workflow ID is missing'); + context.trackWorkflow(created.id); + + const response = await handleUpdatePartialWorkflow( + { + id: created.id, + operations: [ + { + type: 'removeNode', + nodeName: 'NonExistentNode' + } + ] + }, + mcpContext + ); + + expect(response.success).toBe(false); + }); + }); + + describe('updateNode', () => { + it('should update node parameters', async () => { + const workflow = { + ...SIMPLE_WEBHOOK_WORKFLOW, + name: createTestWorkflowName('Partial - Update Node'), + tags: ['mcp-integration-test'] + }; + + const created = await client.createWorkflow(workflow); + expect(created.id).toBeTruthy(); + if (!created.id) throw new Error('Workflow ID is missing'); + context.trackWorkflow(created.id); + + // Update webhook path + const response = await handleUpdatePartialWorkflow( + { + id: created.id, + operations: [ + { + type: 'updateNode', + nodeName: 'Webhook', + updates: { + 'parameters.path': 'updated-path' + } + } + ] + }, + mcpContext + ); + + expect(response.success).toBe(true); + const updated = response.data as any; + const webhookNode = updated.nodes.find((n: any) => n.name === 'Webhook'); + expect(webhookNode.parameters.path).toBe('updated-path'); + }); + + it('should update nested parameters', async () => { + const workflow = { + ...SIMPLE_WEBHOOK_WORKFLOW, + name: createTestWorkflowName('Partial - Update Nested'), + tags: ['mcp-integration-test'] + }; + + const created = await client.createWorkflow(workflow); + expect(created.id).toBeTruthy(); + if (!created.id) throw new Error('Workflow ID is missing'); + context.trackWorkflow(created.id); + + const response = await handleUpdatePartialWorkflow( + { + id: created.id, + operations: [ + { + type: 'updateNode', + nodeName: 'Webhook', + updates: { + 'parameters.httpMethod': 'POST', + 'parameters.path': 'new-path' + } + } + ] + }, + mcpContext + ); + + expect(response.success).toBe(true); + const updated = response.data as any; + const webhookNode = updated.nodes.find((n: any) => n.name === 'Webhook'); + expect(webhookNode.parameters.httpMethod).toBe('POST'); + expect(webhookNode.parameters.path).toBe('new-path'); + }); + }); + + describe('moveNode', () => { + it('should move node to new position', async () => { + const workflow = { + ...SIMPLE_WEBHOOK_WORKFLOW, + name: createTestWorkflowName('Partial - Move Node'), + tags: ['mcp-integration-test'] + }; + + const created = await client.createWorkflow(workflow); + expect(created.id).toBeTruthy(); + if (!created.id) throw new Error('Workflow ID is missing'); + context.trackWorkflow(created.id); + + const newPosition: [number, number] = [500, 500]; + + const response = await handleUpdatePartialWorkflow( + { + id: created.id, + operations: [ + { + type: 'moveNode', + nodeName: 'Webhook', + position: newPosition + } + ] + }, + mcpContext + ); + + expect(response.success).toBe(true); + const updated = response.data as any; + const webhookNode = updated.nodes.find((n: any) => n.name === 'Webhook'); + expect(webhookNode.position).toEqual(newPosition); + }); + }); + + describe('enableNode / disableNode', () => { + it('should disable a node', async () => { + const workflow = { + ...SIMPLE_WEBHOOK_WORKFLOW, + name: createTestWorkflowName('Partial - Disable Node'), + tags: ['mcp-integration-test'] + }; + + const created = await client.createWorkflow(workflow); + expect(created.id).toBeTruthy(); + if (!created.id) throw new Error('Workflow ID is missing'); + context.trackWorkflow(created.id); + + const response = await handleUpdatePartialWorkflow( + { + id: created.id, + operations: [ + { + type: 'disableNode', + nodeName: 'Webhook' + } + ] + }, + mcpContext + ); + + expect(response.success).toBe(true); + const updated = response.data as any; + const webhookNode = updated.nodes.find((n: any) => n.name === 'Webhook'); + expect(webhookNode.disabled).toBe(true); + }); + + it('should enable a disabled node', async () => { + const workflow = { + ...SIMPLE_WEBHOOK_WORKFLOW, + name: createTestWorkflowName('Partial - Enable Node'), + tags: ['mcp-integration-test'] + }; + + const created = await client.createWorkflow(workflow); + expect(created.id).toBeTruthy(); + if (!created.id) throw new Error('Workflow ID is missing'); + context.trackWorkflow(created.id); + + // First disable the node + await handleUpdatePartialWorkflow( + { + id: created.id, + operations: [{ type: 'disableNode', nodeName: 'Webhook' }] + }, + mcpContext + ); + + // Then enable it + const response = await handleUpdatePartialWorkflow( + { + id: created.id, + operations: [ + { + type: 'enableNode', + nodeName: 'Webhook' + } + ] + }, + mcpContext + ); + + expect(response.success).toBe(true); + const updated = response.data as any; + const webhookNode = updated.nodes.find((n: any) => n.name === 'Webhook'); + expect(webhookNode.disabled).toBeUndefined(); + }); + }); + }); + + // ====================================================================== + // CONNECTION OPERATIONS (5 operations) + // ====================================================================== + + describe('Connection Operations', () => { + describe('addConnection', () => { + it('should add connection between nodes', async () => { + // Start with workflow without connections + const workflow = { + ...SIMPLE_HTTP_WORKFLOW, + name: createTestWorkflowName('Partial - Add Connection'), + tags: ['mcp-integration-test'], + connections: {} // Start with no connections + }; + + const created = await client.createWorkflow(workflow); + expect(created.id).toBeTruthy(); + if (!created.id) throw new Error('Workflow ID is missing'); + context.trackWorkflow(created.id); + + // Add connection + const response = await handleUpdatePartialWorkflow( + { + id: created.id, + operations: [ + { + type: 'addConnection', + source: 'Webhook', + target: 'HTTP Request' + } + ] + }, + mcpContext + ); + + expect(response.success).toBe(true); + const updated = response.data as any; + expect(updated.connections).toBeDefined(); + expect(updated.connections.Webhook).toBeDefined(); + }); + + it('should add connection with custom ports', async () => { + const workflow = { + ...SIMPLE_HTTP_WORKFLOW, + name: createTestWorkflowName('Partial - Add Connection Ports'), + tags: ['mcp-integration-test'], + connections: {} + }; + + const created = await client.createWorkflow(workflow); + expect(created.id).toBeTruthy(); + if (!created.id) throw new Error('Workflow ID is missing'); + context.trackWorkflow(created.id); + + const response = await handleUpdatePartialWorkflow( + { + id: created.id, + operations: [ + { + type: 'addConnection', + source: 'Webhook', + target: 'HTTP Request', + sourceOutput: 'main', + targetInput: 'main', + sourceIndex: 0, + targetIndex: 0 + } + ] + }, + mcpContext + ); + + expect(response.success).toBe(true); + }); + }); + + describe('removeConnection', () => { + it('should remove connection between nodes', async () => { + const workflow = { + ...SIMPLE_HTTP_WORKFLOW, + name: createTestWorkflowName('Partial - Remove Connection'), + tags: ['mcp-integration-test'] + }; + + const created = await client.createWorkflow(workflow); + expect(created.id).toBeTruthy(); + if (!created.id) throw new Error('Workflow ID is missing'); + context.trackWorkflow(created.id); + + const response = await handleUpdatePartialWorkflow( + { + id: created.id, + operations: [ + { + type: 'removeConnection', + source: 'Webhook', + target: 'HTTP Request' + } + ] + }, + mcpContext + ); + + expect(response.success).toBe(true); + const updated = response.data as any; + expect(Object.keys(updated.connections || {})).toHaveLength(0); + }); + + it('should ignore error for non-existent connection with ignoreErrors flag', async () => { + const workflow = { + ...SIMPLE_WEBHOOK_WORKFLOW, + name: createTestWorkflowName('Partial - Remove Connection Ignore'), + tags: ['mcp-integration-test'] + }; + + const created = await client.createWorkflow(workflow); + expect(created.id).toBeTruthy(); + if (!created.id) throw new Error('Workflow ID is missing'); + context.trackWorkflow(created.id); + + const response = await handleUpdatePartialWorkflow( + { + id: created.id, + operations: [ + { + type: 'removeConnection', + source: 'Webhook', + target: 'NonExistent', + ignoreErrors: true + } + ] + }, + mcpContext + ); + + // Should succeed because ignoreErrors is true + expect(response.success).toBe(true); + }); + }); + + describe('replaceConnections', () => { + it('should replace all connections', async () => { + const workflow = { + ...SIMPLE_HTTP_WORKFLOW, + name: createTestWorkflowName('Partial - Replace Connections'), + tags: ['mcp-integration-test'] + }; + + const created = await client.createWorkflow(workflow); + expect(created.id).toBeTruthy(); + if (!created.id) throw new Error('Workflow ID is missing'); + context.trackWorkflow(created.id); + + // Replace with empty connections + const response = await handleUpdatePartialWorkflow( + { + id: created.id, + operations: [ + { + type: 'replaceConnections', + connections: {} + } + ] + }, + mcpContext + ); + + expect(response.success).toBe(true); + const updated = response.data as any; + expect(Object.keys(updated.connections || {})).toHaveLength(0); + }); + }); + + describe('cleanStaleConnections', () => { + it('should remove stale connections in dry run mode', async () => { + const workflow = { + ...SIMPLE_HTTP_WORKFLOW, + name: createTestWorkflowName('Partial - Clean Stale Dry Run'), + tags: ['mcp-integration-test'] + }; + + const created = await client.createWorkflow(workflow); + expect(created.id).toBeTruthy(); + if (!created.id) throw new Error('Workflow ID is missing'); + context.trackWorkflow(created.id); + + // Remove HTTP Request node to create stale connection + await handleUpdatePartialWorkflow( + { + id: created.id, + operations: [{ type: 'removeNode', nodeName: 'HTTP Request' }] + }, + mcpContext + ); + + // Clean stale connections in dry run + const response = await handleUpdatePartialWorkflow( + { + id: created.id, + operations: [ + { + type: 'cleanStaleConnections', + dryRun: true + } + ], + validateOnly: true + }, + mcpContext + ); + + expect(response.success).toBe(true); + }); + }); + }); + + // ====================================================================== + // METADATA OPERATIONS (4 operations) + // ====================================================================== + + describe('Metadata Operations', () => { + describe('updateSettings', () => { + it('should update workflow settings', async () => { + const workflow = { + ...SIMPLE_WEBHOOK_WORKFLOW, + name: createTestWorkflowName('Partial - Update Settings'), + tags: ['mcp-integration-test'] + }; + + const created = await client.createWorkflow(workflow); + expect(created.id).toBeTruthy(); + if (!created.id) throw new Error('Workflow ID is missing'); + context.trackWorkflow(created.id); + + const response = await handleUpdatePartialWorkflow( + { + id: created.id, + operations: [ + { + type: 'updateSettings', + settings: { + timezone: 'America/New_York', + executionOrder: 'v1' + } + } + ] + }, + mcpContext + ); + + expect(response.success).toBe(true); + const updated = response.data as any; + expect(updated.settings?.timezone).toBe('America/New_York'); + }); + }); + + describe('updateName', () => { + it('should update workflow name', async () => { + const workflow = { + ...SIMPLE_WEBHOOK_WORKFLOW, + name: createTestWorkflowName('Partial - Update Name Original'), + tags: ['mcp-integration-test'] + }; + + const created = await client.createWorkflow(workflow); + expect(created.id).toBeTruthy(); + if (!created.id) throw new Error('Workflow ID is missing'); + context.trackWorkflow(created.id); + + const newName = createTestWorkflowName('Partial - Update Name Modified'); + + const response = await handleUpdatePartialWorkflow( + { + id: created.id, + operations: [ + { + type: 'updateName', + name: newName + } + ] + }, + mcpContext + ); + + expect(response.success).toBe(true); + const updated = response.data as any; + expect(updated.name).toBe(newName); + }); + }); + + describe('addTag / removeTag', () => { + it('should add tag to workflow', async () => { + const workflow = { + ...SIMPLE_WEBHOOK_WORKFLOW, + name: createTestWorkflowName('Partial - Add Tag'), + tags: ['mcp-integration-test'] + }; + + const created = await client.createWorkflow(workflow); + expect(created.id).toBeTruthy(); + if (!created.id) throw new Error('Workflow ID is missing'); + context.trackWorkflow(created.id); + + const response = await handleUpdatePartialWorkflow( + { + id: created.id, + operations: [ + { + type: 'addTag', + tag: 'new-tag' + } + ] + }, + mcpContext + ); + + expect(response.success).toBe(true); + const updated = response.data as any; + + // Note: n8n API tag behavior may vary + if (updated.tags) { + expect(updated.tags).toContain('new-tag'); + } + }); + + it('should remove tag from workflow', async () => { + const workflow = { + ...SIMPLE_WEBHOOK_WORKFLOW, + name: createTestWorkflowName('Partial - Remove Tag'), + tags: ['mcp-integration-test', 'to-remove'] + }; + + const created = await client.createWorkflow(workflow); + expect(created.id).toBeTruthy(); + if (!created.id) throw new Error('Workflow ID is missing'); + context.trackWorkflow(created.id); + + const response = await handleUpdatePartialWorkflow( + { + id: created.id, + operations: [ + { + type: 'removeTag', + tag: 'to-remove' + } + ] + }, + mcpContext + ); + + expect(response.success).toBe(true); + const updated = response.data as any; + + if (updated.tags) { + expect(updated.tags).not.toContain('to-remove'); + } + }); + }); + }); + + // ====================================================================== + // ADVANCED SCENARIOS + // ====================================================================== + + describe('Advanced Scenarios', () => { + it('should apply multiple operations in sequence', async () => { + const workflow = { + ...SIMPLE_WEBHOOK_WORKFLOW, + name: createTestWorkflowName('Partial - Multiple Ops'), + tags: ['mcp-integration-test'] + }; + + const created = await client.createWorkflow(workflow); + expect(created.id).toBeTruthy(); + if (!created.id) throw new Error('Workflow ID is missing'); + context.trackWorkflow(created.id); + + const response = await handleUpdatePartialWorkflow( + { + id: created.id, + operations: [ + { + type: 'addNode', + node: { + name: 'Set', + type: 'n8n-nodes-base.set', + typeVersion: 3.4, + position: [450, 300], + parameters: { + assignments: { assignments: [] } + } + } + }, + { + type: 'addConnection', + source: 'Webhook', + target: 'Set' + }, + { + type: 'updateName', + name: createTestWorkflowName('Partial - Multiple Ops Updated') + } + ] + }, + mcpContext + ); + + expect(response.success).toBe(true); + const updated = response.data as any; + expect(updated.nodes).toHaveLength(2); + expect(updated.connections.Webhook).toBeDefined(); + }); + + it('should validate operations without applying (validateOnly mode)', async () => { + const workflow = { + ...SIMPLE_WEBHOOK_WORKFLOW, + name: createTestWorkflowName('Partial - Validate Only'), + tags: ['mcp-integration-test'] + }; + + const created = await client.createWorkflow(workflow); + expect(created.id).toBeTruthy(); + if (!created.id) throw new Error('Workflow ID is missing'); + context.trackWorkflow(created.id); + + const response = await handleUpdatePartialWorkflow( + { + id: created.id, + operations: [ + { + type: 'updateName', + name: 'New Name' + } + ], + validateOnly: true + }, + mcpContext + ); + + expect(response.success).toBe(true); + expect(response.data).toHaveProperty('valid', true); + + // Verify workflow was NOT actually updated + const current = await client.getWorkflow(created.id); + expect(current.name).not.toBe('New Name'); + }); + + it('should handle continueOnError mode with partial failures', async () => { + const workflow = { + ...SIMPLE_WEBHOOK_WORKFLOW, + name: createTestWorkflowName('Partial - Continue On Error'), + tags: ['mcp-integration-test'] + }; + + const created = await client.createWorkflow(workflow); + expect(created.id).toBeTruthy(); + if (!created.id) throw new Error('Workflow ID is missing'); + context.trackWorkflow(created.id); + + // Mix valid and invalid operations + const response = await handleUpdatePartialWorkflow( + { + id: created.id, + operations: [ + { + type: 'updateName', + name: createTestWorkflowName('Partial - Continue On Error Updated') + }, + { + type: 'removeNode', + nodeName: 'NonExistentNode' // This will fail + }, + { + type: 'addTag', + tag: 'new-tag' + } + ], + continueOnError: true + }, + mcpContext + ); + + // Should succeed with partial results + expect(response.success).toBe(true); + expect(response.details?.applied).toBeDefined(); + expect(response.details?.failed).toBeDefined(); + }); + }); +}); diff --git a/tests/integration/n8n-api/workflows/update-workflow.test.ts b/tests/integration/n8n-api/workflows/update-workflow.test.ts new file mode 100644 index 0000000..7910eed --- /dev/null +++ b/tests/integration/n8n-api/workflows/update-workflow.test.ts @@ -0,0 +1,396 @@ +/** + * Integration Tests: handleUpdateWorkflow + * + * Tests full workflow updates against a real n8n instance. + * Covers various update scenarios including nodes, connections, settings, and tags. + */ + +import { describe, it, expect, beforeEach, afterEach, afterAll } from 'vitest'; +import { createTestContext, TestContext, createTestWorkflowName } from '../utils/test-context'; +import { getTestN8nClient } from '../utils/n8n-client'; +import { N8nApiClient } from '../../../../src/services/n8n-api-client'; +import { SIMPLE_WEBHOOK_WORKFLOW, SIMPLE_HTTP_WORKFLOW } from '../utils/fixtures'; +import { cleanupOrphanedWorkflows } from '../utils/cleanup-helpers'; +import { createMcpContext } from '../utils/mcp-context'; +import { InstanceContext } from '../../../../src/types/instance-context'; +import { handleUpdateWorkflow } from '../../../../src/mcp/handlers-n8n-manager'; + +describe('Integration: handleUpdateWorkflow', () => { + let context: TestContext; + let client: N8nApiClient; + let mcpContext: InstanceContext; + + beforeEach(() => { + context = createTestContext(); + client = getTestN8nClient(); + mcpContext = createMcpContext(); + }); + + afterEach(async () => { + await context.cleanup(); + }); + + afterAll(async () => { + if (!process.env.CI) { + await cleanupOrphanedWorkflows(); + } + }); + + // ====================================================================== + // Full Workflow Replacement + // ====================================================================== + + describe('Full Workflow Replacement', () => { + it('should replace entire workflow with new nodes and connections', async () => { + // Create initial simple workflow + const initialWorkflow = { + ...SIMPLE_WEBHOOK_WORKFLOW, + name: createTestWorkflowName('Update - Full Replacement'), + tags: ['mcp-integration-test'] + }; + + const created = await client.createWorkflow(initialWorkflow); + expect(created.id).toBeTruthy(); + if (!created.id) throw new Error('Workflow ID is missing'); + context.trackWorkflow(created.id); + + // Replace with HTTP workflow (completely different structure) + const replacement = { + ...SIMPLE_HTTP_WORKFLOW, + name: createTestWorkflowName('Update - Full Replacement (Updated)'), + tags: ['mcp-integration-test', 'updated'] + }; + + // Update using MCP handler + const response = await handleUpdateWorkflow( + { + id: created.id, + name: replacement.name, + nodes: replacement.nodes, + connections: replacement.connections, + tags: replacement.tags + }, + mcpContext + ); + + // Verify MCP response + expect(response.success).toBe(true); + expect(response.data).toBeDefined(); + + const updated = response.data as any; + expect(updated.id).toBe(created.id); + expect(updated.name).toBe(replacement.name); + expect(updated.nodes).toHaveLength(2); // HTTP workflow has 2 nodes + expect(updated.tags).toContain('updated'); + }); + }); + + // ====================================================================== + // Update Nodes + // ====================================================================== + + describe('Update Nodes', () => { + it('should update workflow nodes while preserving other properties', async () => { + // Create workflow + const workflow = { + ...SIMPLE_WEBHOOK_WORKFLOW, + name: createTestWorkflowName('Update - Nodes Only'), + tags: ['mcp-integration-test'] + }; + + const created = await client.createWorkflow(workflow); + expect(created.id).toBeTruthy(); + if (!created.id) throw new Error('Workflow ID is missing'); + context.trackWorkflow(created.id); + + // Update nodes - add a second node + const updatedNodes = [ + ...workflow.nodes!, + { + id: 'set-1', + name: 'Set', + type: 'n8n-nodes-base.set', + typeVersion: 3.4, + position: [450, 300] as [number, number], + parameters: { + assignments: { + assignments: [ + { + id: 'assign-1', + name: 'test', + value: 'value', + type: 'string' + } + ] + } + } + } + ]; + + const updatedConnections = { + Webhook: { + main: [[{ node: 'Set', type: 'main' as const, index: 0 }]] + } + }; + + // Update using MCP handler + const response = await handleUpdateWorkflow( + { + id: created.id, + nodes: updatedNodes, + connections: updatedConnections + }, + mcpContext + ); + + expect(response.success).toBe(true); + const updated = response.data as any; + expect(updated.nodes).toHaveLength(2); + expect(updated.nodes.find((n: any) => n.name === 'Set')).toBeDefined(); + }); + }); + + // ====================================================================== + // Update Connections + // ====================================================================== + + describe('Update Connections', () => { + it('should update workflow connections', async () => { + // Create HTTP workflow + const workflow = { + ...SIMPLE_HTTP_WORKFLOW, + name: createTestWorkflowName('Update - Connections'), + tags: ['mcp-integration-test'] + }; + + const created = await client.createWorkflow(workflow); + expect(created.id).toBeTruthy(); + if (!created.id) throw new Error('Workflow ID is missing'); + context.trackWorkflow(created.id); + + // Remove connections (disconnect nodes) + const response = await handleUpdateWorkflow( + { + id: created.id, + connections: {} + }, + mcpContext + ); + + expect(response.success).toBe(true); + const updated = response.data as any; + expect(Object.keys(updated.connections || {})).toHaveLength(0); + }); + }); + + // ====================================================================== + // Update Settings + // ====================================================================== + + describe('Update Settings', () => { + it('should update workflow settings without affecting nodes', async () => { + // Create workflow + const workflow = { + ...SIMPLE_WEBHOOK_WORKFLOW, + name: createTestWorkflowName('Update - Settings'), + tags: ['mcp-integration-test'] + }; + + const created = await client.createWorkflow(workflow); + expect(created.id).toBeTruthy(); + if (!created.id) throw new Error('Workflow ID is missing'); + context.trackWorkflow(created.id); + + // Update settings + const response = await handleUpdateWorkflow( + { + id: created.id, + settings: { + executionOrder: 'v1' as const, + timezone: 'Europe/London' + } + }, + mcpContext + ); + + expect(response.success).toBe(true); + const updated = response.data as any; + expect(updated.settings?.timezone).toBe('Europe/London'); + expect(updated.nodes).toHaveLength(1); // Nodes unchanged + }); + }); + + // ====================================================================== + // Update Tags + // ====================================================================== + + describe('Update Tags', () => { + it('should update workflow tags', async () => { + // Create workflow + const workflow = { + ...SIMPLE_WEBHOOK_WORKFLOW, + name: createTestWorkflowName('Update - Tags'), + tags: ['mcp-integration-test', 'original'] + }; + + const created = await client.createWorkflow(workflow); + expect(created.id).toBeTruthy(); + if (!created.id) throw new Error('Workflow ID is missing'); + context.trackWorkflow(created.id); + + // Update tags + const response = await handleUpdateWorkflow( + { + id: created.id, + tags: ['mcp-integration-test', 'updated', 'modified'] + }, + mcpContext + ); + + expect(response.success).toBe(true); + const updated = response.data as any; + + // Note: n8n API tag behavior may vary + if (updated.tags) { + expect(updated.tags).toContain('updated'); + } + }); + }); + + // ====================================================================== + // Validation Errors + // ====================================================================== + + describe('Validation Errors', () => { + it('should return error for invalid node types', async () => { + // Create workflow + const workflow = { + ...SIMPLE_WEBHOOK_WORKFLOW, + name: createTestWorkflowName('Update - Invalid Node Type'), + tags: ['mcp-integration-test'] + }; + + const created = await client.createWorkflow(workflow); + expect(created.id).toBeTruthy(); + if (!created.id) throw new Error('Workflow ID is missing'); + context.trackWorkflow(created.id); + + // Try to update with invalid node type + const response = await handleUpdateWorkflow( + { + id: created.id, + nodes: [ + { + id: 'invalid-1', + name: 'Invalid', + type: 'invalid-node-type', + typeVersion: 1, + position: [250, 300], + parameters: {} + } + ], + connections: {} + }, + mcpContext + ); + + // Validation should fail + expect(response.success).toBe(false); + expect(response.error).toBeDefined(); + }); + + it('should return error for non-existent workflow ID', async () => { + const response = await handleUpdateWorkflow( + { + id: '99999999', + name: 'Should Fail' + }, + mcpContext + ); + + expect(response.success).toBe(false); + expect(response.error).toBeDefined(); + }); + }); + + // ====================================================================== + // Update Name Only + // ====================================================================== + + describe('Update Name', () => { + it('should update workflow name without affecting structure', async () => { + // Create workflow + const workflow = { + ...SIMPLE_WEBHOOK_WORKFLOW, + name: createTestWorkflowName('Update - Name Original'), + tags: ['mcp-integration-test'] + }; + + const created = await client.createWorkflow(workflow); + expect(created.id).toBeTruthy(); + if (!created.id) throw new Error('Workflow ID is missing'); + context.trackWorkflow(created.id); + + const newName = createTestWorkflowName('Update - Name Modified'); + + // Update name only + const response = await handleUpdateWorkflow( + { + id: created.id, + name: newName + }, + mcpContext + ); + + expect(response.success).toBe(true); + const updated = response.data as any; + expect(updated.name).toBe(newName); + expect(updated.nodes).toHaveLength(1); // Structure unchanged + }); + }); + + // ====================================================================== + // Multiple Properties Update + // ====================================================================== + + describe('Multiple Properties', () => { + it('should update name, tags, and settings together', async () => { + // Create workflow + const workflow = { + ...SIMPLE_WEBHOOK_WORKFLOW, + name: createTestWorkflowName('Update - Multiple Props'), + tags: ['mcp-integration-test'] + }; + + const created = await client.createWorkflow(workflow); + expect(created.id).toBeTruthy(); + if (!created.id) throw new Error('Workflow ID is missing'); + context.trackWorkflow(created.id); + + const newName = createTestWorkflowName('Update - Multiple Props (Modified)'); + + // Update multiple properties + const response = await handleUpdateWorkflow( + { + id: created.id, + name: newName, + tags: ['mcp-integration-test', 'multi-update'], + settings: { + executionOrder: 'v1' as const, + timezone: 'America/New_York' + } + }, + mcpContext + ); + + expect(response.success).toBe(true); + const updated = response.data as any; + expect(updated.name).toBe(newName); + expect(updated.settings?.timezone).toBe('America/New_York'); + + if (updated.tags) { + expect(updated.tags).toContain('multi-update'); + } + }); + }); +});