workflow-diff-node-rename.test.tsโข32.4 kB
/**
* Comprehensive test suite for auto-update connection references on node rename
* Tests Issue #353: Enhancement - Auto-update connection references on node rename
*/
import { describe, it, expect, beforeEach } from 'vitest';
import { WorkflowDiffEngine } from '@/services/workflow-diff-engine';
import { createWorkflow, WorkflowBuilder } from '@tests/utils/builders/workflow.builder';
import {
WorkflowDiffRequest,
UpdateNodeOperation,
AddConnectionOperation,
RemoveConnectionOperation
} from '@/types/workflow-diff';
import { Workflow, WorkflowNode } from '@/types/n8n-api';
describe('WorkflowDiffEngine - Auto-Update Connection References on Node Rename', () => {
let diffEngine: WorkflowDiffEngine;
let baseWorkflow: Workflow;
/**
* Helper to convert ID-based connections to name-based
* (as n8n API expects)
*/
function convertConnectionsToNameBased(workflow: Workflow): void {
const newConnections: any = {};
for (const [nodeId, outputs] of Object.entries(workflow.connections)) {
const node = workflow.nodes.find((n: any) => n.id === nodeId);
if (node) {
newConnections[node.name] = {};
for (const [outputName, connections] of Object.entries(outputs)) {
newConnections[node.name][outputName] = (connections as any[]).map((conns: any) =>
conns.map((conn: any) => {
const targetNode = workflow.nodes.find((n: any) => n.id === conn.node);
return {
...conn,
node: targetNode ? targetNode.name : conn.node
};
})
);
}
}
}
workflow.connections = newConnections;
}
beforeEach(() => {
diffEngine = new WorkflowDiffEngine();
});
describe('Scenario 1: Simple rename with single connection', () => {
beforeEach(() => {
baseWorkflow = createWorkflow('Test Workflow')
.addWebhookNode({ id: 'webhook-1', name: 'Webhook' })
.addHttpRequestNode({ id: 'http-1', name: 'HTTP Request' })
.connect('webhook-1', 'http-1')
.build() as Workflow;
convertConnectionsToNameBased(baseWorkflow);
});
it('should automatically update connection when renaming target node', async () => {
const operation: UpdateNodeOperation = {
type: 'updateNode',
nodeId: 'http-1',
updates: {
name: 'HTTP Request Renamed'
}
};
const request: WorkflowDiffRequest = {
id: 'test-workflow',
operations: [operation]
};
const result = await diffEngine.applyDiff(baseWorkflow, request);
expect(result.success).toBe(true);
expect(result.workflow).toBeDefined();
// Node should be renamed
const renamedNode = result.workflow!.nodes.find((n: WorkflowNode) => n.id === 'http-1');
expect(renamedNode?.name).toBe('HTTP Request Renamed');
// Connection should reference new name
const webhookConnections = result.workflow!.connections['Webhook'];
expect(webhookConnections).toBeDefined();
expect(webhookConnections.main[0][0].node).toBe('HTTP Request Renamed');
});
it('should automatically update connection when renaming source node', async () => {
const operation: UpdateNodeOperation = {
type: 'updateNode',
nodeId: 'webhook-1',
updates: {
name: 'Webhook Renamed'
}
};
const request: WorkflowDiffRequest = {
id: 'test-workflow',
operations: [operation]
};
const result = await diffEngine.applyDiff(baseWorkflow, request);
expect(result.success).toBe(true);
expect(result.workflow).toBeDefined();
// Node should be renamed
const renamedNode = result.workflow!.nodes.find((n: WorkflowNode) => n.id === 'webhook-1');
expect(renamedNode?.name).toBe('Webhook Renamed');
// Connection key should use new name
expect(result.workflow!.connections['Webhook Renamed']).toBeDefined();
expect(result.workflow!.connections['Webhook']).toBeUndefined();
expect(result.workflow!.connections['Webhook Renamed'].main[0][0].node).toBe('HTTP Request');
});
});
describe('Scenario 2: Multiple incoming connections', () => {
beforeEach(() => {
baseWorkflow = createWorkflow('Test Workflow')
.addWebhookNode({ id: 'webhook-1', name: 'Webhook 1' })
.addWebhookNode({ id: 'webhook-2', name: 'Webhook 2' })
.addHttpRequestNode({ id: 'http-1', name: 'HTTP Request' })
.connect('webhook-1', 'http-1')
.connect('webhook-2', 'http-1')
.build() as Workflow;
convertConnectionsToNameBased(baseWorkflow);
});
it('should update all incoming connections when renaming target', async () => {
const operation: UpdateNodeOperation = {
type: 'updateNode',
nodeId: 'http-1',
updates: {
name: 'Merged HTTP Request'
}
};
const request: WorkflowDiffRequest = {
id: 'test-workflow',
operations: [operation]
};
const result = await diffEngine.applyDiff(baseWorkflow, request);
expect(result.success).toBe(true);
expect(result.workflow).toBeDefined();
// Both webhook connections should reference new name
expect(result.workflow!.connections['Webhook 1'].main[0][0].node).toBe('Merged HTTP Request');
expect(result.workflow!.connections['Webhook 2'].main[0][0].node).toBe('Merged HTTP Request');
});
});
describe('Scenario 3: Multiple outgoing connections', () => {
beforeEach(() => {
// Manually create workflow with IF node having two outputs
baseWorkflow = {
id: 'test-workflow',
name: 'Test Workflow',
nodes: [
{
id: 'if-1',
name: 'IF',
type: 'n8n-nodes-base.if',
typeVersion: 2,
position: [0, 0],
parameters: {}
},
{
id: 'http-1',
name: 'HTTP Request 1',
type: 'n8n-nodes-base.httpRequest',
typeVersion: 4.1,
position: [200, 0],
parameters: {}
},
{
id: 'http-2',
name: 'HTTP Request 2',
type: 'n8n-nodes-base.httpRequest',
typeVersion: 4.1,
position: [200, 100],
parameters: {}
}
],
connections: {
'IF': {
main: [
[{ node: 'HTTP Request 1', type: 'main', index: 0 }], // output index 0
[{ node: 'HTTP Request 2', type: 'main', index: 0 }] // output index 1
]
}
}
};
});
it('should update all outgoing connections when renaming source', async () => {
const operation: UpdateNodeOperation = {
type: 'updateNode',
nodeId: 'if-1',
updates: {
name: 'IF Condition'
}
};
const request: WorkflowDiffRequest = {
id: 'test-workflow',
operations: [operation]
};
const result = await diffEngine.applyDiff(baseWorkflow, request);
expect(result.success).toBe(true);
expect(result.workflow).toBeDefined();
// Connection key should be updated
expect(result.workflow!.connections['IF Condition']).toBeDefined();
expect(result.workflow!.connections['IF']).toBeUndefined();
// Both connections should still exist
expect(result.workflow!.connections['IF Condition'].main).toHaveLength(2);
expect(result.workflow!.connections['IF Condition'].main[0][0].node).toBe('HTTP Request 1');
expect(result.workflow!.connections['IF Condition'].main[1][0].node).toBe('HTTP Request 2');
});
});
describe('Scenario 4: IF node branches', () => {
beforeEach(() => {
// Manually create workflow with IF node branches
baseWorkflow = {
id: 'test-workflow',
name: 'Test Workflow',
nodes: [
{
id: 'if-1',
name: 'IF',
type: 'n8n-nodes-base.if',
typeVersion: 2,
position: [0, 0],
parameters: {}
},
{
id: 'http-true',
name: 'HTTP True',
type: 'n8n-nodes-base.httpRequest',
typeVersion: 4.1,
position: [200, 0],
parameters: {}
},
{
id: 'http-false',
name: 'HTTP False',
type: 'n8n-nodes-base.httpRequest',
typeVersion: 4.1,
position: [200, 200],
parameters: {}
}
],
connections: {
'IF': {
main: [
[{ node: 'HTTP True', type: 'main', index: 0 }], // branch=true (index 0)
[{ node: 'HTTP False', type: 'main', index: 0 }] // branch=false (index 1)
]
}
}
};
});
it('should update both branch connections when renaming IF node', async () => {
const operation: UpdateNodeOperation = {
type: 'updateNode',
nodeId: 'if-1',
updates: {
name: 'IF Renamed'
}
};
const request: WorkflowDiffRequest = {
id: 'test-workflow',
operations: [operation]
};
const result = await diffEngine.applyDiff(baseWorkflow, request);
expect(result.success).toBe(true);
expect(result.workflow).toBeDefined();
// Connection key should be updated
expect(result.workflow!.connections['IF Renamed']).toBeDefined();
expect(result.workflow!.connections['IF']).toBeUndefined();
// Both branches should still exist
expect(result.workflow!.connections['IF Renamed'].main).toHaveLength(2);
expect(result.workflow!.connections['IF Renamed'].main[0][0].node).toBe('HTTP True');
expect(result.workflow!.connections['IF Renamed'].main[1][0].node).toBe('HTTP False');
});
it('should update branch target when renaming target node', async () => {
const operation: UpdateNodeOperation = {
type: 'updateNode',
nodeId: 'http-true',
updates: {
name: 'HTTP Success'
}
};
const request: WorkflowDiffRequest = {
id: 'test-workflow',
operations: [operation]
};
const result = await diffEngine.applyDiff(baseWorkflow, request);
expect(result.success).toBe(true);
expect(result.workflow).toBeDefined();
// True branch connection should reference new name
expect(result.workflow!.connections['IF'].main[0][0].node).toBe('HTTP Success');
// False branch should remain unchanged
expect(result.workflow!.connections['IF'].main[1][0].node).toBe('HTTP False');
});
});
describe('Scenario 5: Switch node cases', () => {
beforeEach(() => {
// Manually create workflow with Switch node cases
baseWorkflow = {
id: 'test-workflow',
name: 'Test Workflow',
nodes: [
{
id: 'switch-1',
name: 'Switch',
type: 'n8n-nodes-base.switch',
typeVersion: 3,
position: [0, 0],
parameters: {}
},
{
id: 'http-case0',
name: 'HTTP Case 0',
type: 'n8n-nodes-base.httpRequest',
typeVersion: 4.1,
position: [200, 0],
parameters: {}
},
{
id: 'http-case1',
name: 'HTTP Case 1',
type: 'n8n-nodes-base.httpRequest',
typeVersion: 4.1,
position: [200, 100],
parameters: {}
},
{
id: 'http-case2',
name: 'HTTP Case 2',
type: 'n8n-nodes-base.httpRequest',
typeVersion: 4.1,
position: [200, 200],
parameters: {}
}
],
connections: {
'Switch': {
main: [
[{ node: 'HTTP Case 0', type: 'main', index: 0 }], // case 0
[{ node: 'HTTP Case 1', type: 'main', index: 0 }], // case 1
[{ node: 'HTTP Case 2', type: 'main', index: 0 }] // case 2
]
}
}
};
});
it('should update all case connections when renaming Switch node', async () => {
const operation: UpdateNodeOperation = {
type: 'updateNode',
nodeId: 'switch-1',
updates: {
name: 'Switch Renamed'
}
};
const request: WorkflowDiffRequest = {
id: 'test-workflow',
operations: [operation]
};
const result = await diffEngine.applyDiff(baseWorkflow, request);
expect(result.success).toBe(true);
expect(result.workflow).toBeDefined();
// Connection key should be updated
expect(result.workflow!.connections['Switch Renamed']).toBeDefined();
expect(result.workflow!.connections['Switch']).toBeUndefined();
// All three cases should still exist
expect(result.workflow!.connections['Switch Renamed'].main).toHaveLength(3);
expect(result.workflow!.connections['Switch Renamed'].main[0][0].node).toBe('HTTP Case 0');
expect(result.workflow!.connections['Switch Renamed'].main[1][0].node).toBe('HTTP Case 1');
expect(result.workflow!.connections['Switch Renamed'].main[2][0].node).toBe('HTTP Case 2');
});
it('should update specific case target when renamed', async () => {
const operation: UpdateNodeOperation = {
type: 'updateNode',
nodeId: 'http-case1',
updates: {
name: 'HTTP Middle Case'
}
};
const request: WorkflowDiffRequest = {
id: 'test-workflow',
operations: [operation]
};
const result = await diffEngine.applyDiff(baseWorkflow, request);
expect(result.success).toBe(true);
expect(result.workflow).toBeDefined();
// Case 1 connection should reference new name
expect(result.workflow!.connections['Switch'].main[1][0].node).toBe('HTTP Middle Case');
// Other cases should remain unchanged
expect(result.workflow!.connections['Switch'].main[0][0].node).toBe('HTTP Case 0');
expect(result.workflow!.connections['Switch'].main[2][0].node).toBe('HTTP Case 2');
});
});
describe('Scenario 6: Error connections', () => {
beforeEach(() => {
// Manually create workflow with error connection
baseWorkflow = {
id: 'test-workflow',
name: 'Test Workflow',
nodes: [
{
id: 'http-1',
name: 'HTTP Request',
type: 'n8n-nodes-base.httpRequest',
typeVersion: 4.1,
position: [0, 0],
parameters: {}
},
{
id: 'error-handler',
name: 'Error Handler',
type: 'n8n-nodes-base.code',
typeVersion: 2,
position: [200, 100],
parameters: {}
}
],
connections: {
'HTTP Request': {
error: [
[{ node: 'Error Handler', type: 'main', index: 0 }]
]
}
}
};
});
it('should update error connections when renaming source node', async () => {
const operation: UpdateNodeOperation = {
type: 'updateNode',
nodeId: 'http-1',
updates: {
name: 'HTTP Request Renamed'
}
};
const request: WorkflowDiffRequest = {
id: 'test-workflow',
operations: [operation]
};
const result = await diffEngine.applyDiff(baseWorkflow, request);
expect(result.success).toBe(true);
expect(result.workflow).toBeDefined();
// Error connection should have updated key
expect(result.workflow!.connections['HTTP Request Renamed']).toBeDefined();
expect(result.workflow!.connections['HTTP Request Renamed'].error[0][0].node).toBe('Error Handler');
});
it('should update error connections when renaming target node', async () => {
const operation: UpdateNodeOperation = {
type: 'updateNode',
nodeId: 'error-handler',
updates: {
name: 'Error Logger'
}
};
const request: WorkflowDiffRequest = {
id: 'test-workflow',
operations: [operation]
};
const result = await diffEngine.applyDiff(baseWorkflow, request);
expect(result.success).toBe(true);
expect(result.workflow).toBeDefined();
// Error connection target should be updated
expect(result.workflow!.connections['HTTP Request'].error[0][0].node).toBe('Error Logger');
});
});
describe('Scenario 7: AI tool connections', () => {
beforeEach(() => {
// Manually create workflow with AI tool connection
baseWorkflow = {
id: 'test-workflow',
name: 'Test Workflow',
nodes: [
{
id: 'agent-1',
name: 'AI Agent',
type: '@n8n/n8n-nodes-langchain.agent',
typeVersion: 1,
position: [0, 0],
parameters: {}
},
{
id: 'tool-1',
name: 'HTTP Tool',
type: '@n8n/n8n-nodes-langchain.toolHttpRequest',
typeVersion: 1,
position: [200, 0],
parameters: {}
}
],
connections: {
'AI Agent': {
ai_tool: [
[{ node: 'HTTP Tool', type: 'ai_tool', index: 0 }]
]
}
}
};
});
it('should update AI tool connections when renaming agent', async () => {
const operation: UpdateNodeOperation = {
type: 'updateNode',
nodeId: 'agent-1',
updates: {
name: 'AI Agent Renamed'
}
};
const request: WorkflowDiffRequest = {
id: 'test-workflow',
operations: [operation]
};
const result = await diffEngine.applyDiff(baseWorkflow, request);
expect(result.success).toBe(true);
expect(result.workflow).toBeDefined();
// AI tool connection should have updated key
expect(result.workflow!.connections['AI Agent Renamed']).toBeDefined();
expect(result.workflow!.connections['AI Agent Renamed'].ai_tool[0][0].node).toBe('HTTP Tool');
});
it('should update AI tool connections when renaming tool', async () => {
const operation: UpdateNodeOperation = {
type: 'updateNode',
nodeId: 'tool-1',
updates: {
name: 'API Tool'
}
};
const request: WorkflowDiffRequest = {
id: 'test-workflow',
operations: [operation]
};
const result = await diffEngine.applyDiff(baseWorkflow, request);
expect(result.success).toBe(true);
expect(result.workflow).toBeDefined();
// AI tool connection target should be updated
expect(result.workflow!.connections['AI Agent'].ai_tool[0][0].node).toBe('API Tool');
});
});
describe('Scenario 8: Name collision detection', () => {
beforeEach(() => {
baseWorkflow = createWorkflow('Test Workflow')
.addHttpRequestNode({ id: 'http-1', name: 'HTTP Request 1' })
.addHttpRequestNode({ id: 'http-2', name: 'HTTP Request 2' })
.build() as Workflow;
convertConnectionsToNameBased(baseWorkflow);
});
it('should fail when renaming to an existing node name', async () => {
const operation: UpdateNodeOperation = {
type: 'updateNode',
nodeId: 'http-1',
updates: {
name: 'HTTP Request 2' // Collision!
}
};
const request: WorkflowDiffRequest = {
id: 'test-workflow',
operations: [operation]
};
const result = await diffEngine.applyDiff(baseWorkflow, request);
expect(result.success).toBe(false);
expect(result.errors).toBeDefined();
expect(result.errors![0].message).toContain('already exists');
expect(result.errors![0].message).toContain('HTTP Request 2');
});
it('should allow renaming to same name (no-op)', async () => {
const operation: UpdateNodeOperation = {
type: 'updateNode',
nodeId: 'http-1',
updates: {
name: 'HTTP Request 1' // Same name
}
};
const request: WorkflowDiffRequest = {
id: 'test-workflow',
operations: [operation]
};
const result = await diffEngine.applyDiff(baseWorkflow, request);
expect(result.success).toBe(true);
expect(result.workflow).toBeDefined();
});
});
describe('Scenario 9: Multiple renames in single batch', () => {
beforeEach(() => {
baseWorkflow = createWorkflow('Test Workflow')
.addWebhookNode({ id: 'webhook-1', name: 'Webhook' })
.addHttpRequestNode({ id: 'http-1', name: 'HTTP Request' })
.addSlackNode({ id: 'slack-1', name: 'Slack' })
.connect('webhook-1', 'http-1')
.connect('http-1', 'slack-1')
.build() as Workflow;
convertConnectionsToNameBased(baseWorkflow);
});
it('should handle multiple renames in one batch', async () => {
const operations: UpdateNodeOperation[] = [
{
type: 'updateNode',
nodeId: 'webhook-1',
updates: { name: 'Webhook Trigger' }
},
{
type: 'updateNode',
nodeId: 'http-1',
updates: { name: 'API Call' }
},
{
type: 'updateNode',
nodeId: 'slack-1',
updates: { name: 'Slack Notification' }
}
];
const request: WorkflowDiffRequest = {
id: 'test-workflow',
operations
};
const result = await diffEngine.applyDiff(baseWorkflow, request);
expect(result.success).toBe(true);
expect(result.workflow).toBeDefined();
// All nodes should be renamed
expect(result.workflow!.nodes.find((n: WorkflowNode) => n.id === 'webhook-1')?.name).toBe('Webhook Trigger');
expect(result.workflow!.nodes.find((n: WorkflowNode) => n.id === 'http-1')?.name).toBe('API Call');
expect(result.workflow!.nodes.find((n: WorkflowNode) => n.id === 'slack-1')?.name).toBe('Slack Notification');
// All connections should be updated
expect(result.workflow!.connections['Webhook Trigger']).toBeDefined();
expect(result.workflow!.connections['Webhook Trigger'].main[0][0].node).toBe('API Call');
expect(result.workflow!.connections['API Call']).toBeDefined();
expect(result.workflow!.connections['API Call'].main[0][0].node).toBe('Slack Notification');
});
});
describe('Scenario 10: Chain operations - rename then add/remove connections', () => {
beforeEach(() => {
baseWorkflow = createWorkflow('Test Workflow')
.addWebhookNode({ id: 'webhook-1', name: 'Webhook' })
.addHttpRequestNode({ id: 'http-1', name: 'HTTP Request' })
.addSlackNode({ id: 'slack-1', name: 'Slack' })
.connect('webhook-1', 'http-1')
.build() as Workflow;
convertConnectionsToNameBased(baseWorkflow);
});
it('should handle rename followed by add connection using new name', async () => {
const operations = [
{
type: 'updateNode',
nodeId: 'http-1',
updates: { name: 'API Call' }
} as UpdateNodeOperation,
{
type: 'addConnection',
source: 'API Call', // Using new name
target: 'Slack'
} as AddConnectionOperation
];
const request: WorkflowDiffRequest = {
id: 'test-workflow',
operations
};
const result = await diffEngine.applyDiff(baseWorkflow, request);
expect(result.success).toBe(true);
expect(result.workflow).toBeDefined();
// Connection should exist with new name
expect(result.workflow!.connections['API Call']).toBeDefined();
expect(result.workflow!.connections['API Call'].main[0]).toContainEqual(
expect.objectContaining({ node: 'Slack' })
);
});
it('should handle rename followed by remove connection using new name', async () => {
const operations = [
{
type: 'updateNode',
nodeId: 'webhook-1',
updates: { name: 'Webhook Trigger' }
} as UpdateNodeOperation,
{
type: 'removeConnection',
source: 'Webhook Trigger', // Using new name
target: 'HTTP Request'
} as RemoveConnectionOperation
];
const request: WorkflowDiffRequest = {
id: 'test-workflow',
operations
};
const result = await diffEngine.applyDiff(baseWorkflow, request);
expect(result.success).toBe(true);
expect(result.workflow).toBeDefined();
// Connection should be removed
expect(result.workflow!.connections['Webhook Trigger']).toBeUndefined();
});
});
describe('Scenario 11: validateOnly mode', () => {
beforeEach(() => {
baseWorkflow = createWorkflow('Test Workflow')
.addWebhookNode({ id: 'webhook-1', name: 'Webhook' })
.addHttpRequestNode({ id: 'http-1', name: 'HTTP Request' })
.connect('webhook-1', 'http-1')
.build() as Workflow;
convertConnectionsToNameBased(baseWorkflow);
});
it('should validate rename without applying changes', async () => {
const operation: UpdateNodeOperation = {
type: 'updateNode',
nodeId: 'http-1',
updates: { name: 'HTTP Request Renamed' }
};
const request: WorkflowDiffRequest = {
id: 'test-workflow',
operations: [operation],
validateOnly: true
};
const result = await diffEngine.applyDiff(baseWorkflow, request);
expect(result.success).toBe(true);
expect(result.workflow).toBeUndefined();
// Original workflow should remain unchanged
const httpNode = baseWorkflow.nodes.find((n: WorkflowNode) => n.id === 'http-1');
expect(httpNode?.name).toBe('HTTP Request');
expect(baseWorkflow.connections['Webhook'].main[0][0].node).toBe('HTTP Request');
});
});
describe('Scenario 12: continueOnError mode', () => {
beforeEach(() => {
baseWorkflow = createWorkflow('Test Workflow')
.addWebhookNode({ id: 'webhook-1', name: 'Webhook' })
.addHttpRequestNode({ id: 'http-1', name: 'HTTP Request' })
.addSlackNode({ id: 'slack-1', name: 'Slack' })
.connect('webhook-1', 'http-1')
.connect('http-1', 'slack-1')
.build() as Workflow;
convertConnectionsToNameBased(baseWorkflow);
});
it('should apply successful renames and update connections even with some failures', async () => {
const operations: UpdateNodeOperation[] = [
{
type: 'updateNode',
nodeId: 'webhook-1',
updates: { name: 'Webhook Trigger' }
},
{
type: 'updateNode',
nodeId: 'invalid-id', // This will fail
updates: { name: 'Invalid' }
},
{
type: 'updateNode',
nodeId: 'slack-1',
updates: { name: 'Slack Notification' }
}
];
const request: WorkflowDiffRequest = {
id: 'test-workflow',
operations,
continueOnError: true
};
const result = await diffEngine.applyDiff(baseWorkflow, request);
expect(result.success).toBe(true); // Some operations succeeded
expect(result.errors).toBeDefined();
expect(result.errors!.length).toBe(1); // One failed
// Successful renames should have updated connections
expect(result.workflow!.connections['Webhook Trigger']).toBeDefined();
expect(result.workflow!.connections['HTTP Request'].main[0][0].node).toBe('Slack Notification');
});
});
describe('Scenario 13: Self-connections', () => {
beforeEach(() => {
// Create workflow where a node connects to itself (loop)
baseWorkflow = {
id: 'test-workflow',
name: 'Test Workflow',
nodes: [
{
id: 'loop-1',
name: 'Loop Node',
type: 'n8n-nodes-base.code',
typeVersion: 2,
position: [0, 0],
parameters: {}
}
],
connections: {
'Loop Node': {
main: [
[{ node: 'Loop Node', type: 'main', index: 0 }] // Self-connection
]
}
}
};
});
it('should update self-connections when node is renamed', async () => {
const operation: UpdateNodeOperation = {
type: 'updateNode',
nodeId: 'loop-1',
updates: { name: 'Recursive Loop' }
};
const request: WorkflowDiffRequest = {
id: 'test-workflow',
operations: [operation]
};
const result = await diffEngine.applyDiff(baseWorkflow, request);
expect(result.success).toBe(true);
expect(result.workflow).toBeDefined();
// Both source and target should reference new name
expect(result.workflow!.connections['Recursive Loop']).toBeDefined();
expect(result.workflow!.connections['Recursive Loop'].main[0][0].node).toBe('Recursive Loop');
});
});
describe('Scenario 14: Real-world scenario from Issue #353', () => {
beforeEach(() => {
// Recreate the exact scenario from the issue
baseWorkflow = {
id: 'workflow123',
name: 'POST /patients/:id/approaches',
nodes: [
{
id: 'if-node',
name: 'If',
type: 'n8n-nodes-base.if',
typeVersion: 2,
position: [0, 0],
parameters: {}
},
{
id: '8546d741-1af1-4aa0-bf11-af6c926c0008',
name: 'Return 403 Forbidden1',
type: 'n8n-nodes-base.respondToWebhook',
typeVersion: 1.1,
position: [200, 100],
parameters: {
responseBody: '={{ {"error": "Forbidden"} }}',
options: { responseCode: 403 }
}
},
{
id: 'return-200',
name: 'Return 200 OK',
type: 'n8n-nodes-base.respondToWebhook',
typeVersion: 1.1,
position: [200, 0],
parameters: {
responseBody: '={{ {"success": true} }}',
options: { responseCode: 200 }
}
}
],
connections: {
'If': {
main: [
[{ node: 'Return 200 OK', type: 'main', index: 0 }], // true branch
[{ node: 'Return 403 Forbidden1', type: 'main', index: 0 }] // false branch
]
}
}
};
});
it('should successfully rename node and update connection (exact issue scenario)', async () => {
// The exact operation from the issue
const operation: UpdateNodeOperation = {
type: 'updateNode',
nodeId: '8546d741-1af1-4aa0-bf11-af6c926c0008',
updates: {
name: 'Return 404 Not Found',
parameters: {
responseBody: '={{ {"error": "Not Found"} }}',
options: { responseCode: 404 }
}
}
};
const request: WorkflowDiffRequest = {
id: 'workflow123',
operations: [operation]
};
const result = await diffEngine.applyDiff(baseWorkflow, request);
// This should now succeed (was failing before fix)
expect(result.success).toBe(true);
expect(result.workflow).toBeDefined();
// Node should be renamed
const renamedNode = result.workflow!.nodes.find((n: WorkflowNode) => n.id === '8546d741-1af1-4aa0-bf11-af6c926c0008');
expect(renamedNode?.name).toBe('Return 404 Not Found');
// Parameters should be updated
expect(renamedNode?.parameters.responseBody).toBe('={{ {"error": "Not Found"} }}');
expect(renamedNode?.parameters.options?.responseCode).toBe(404);
// Connection should automatically reference new name
expect(result.workflow!.connections['If'].main[1][0].node).toBe('Return 404 Not Found');
// True branch should remain unchanged
expect(result.workflow!.connections['If'].main[0][0].node).toBe('Return 200 OK');
// No validation errors should occur
expect(result.errors).toBeUndefined();
});
});
});