mirror of
https://github.com/czlonkowski/n8n-mcp.git
synced 2026-01-30 06:22:04 +00:00
* perf: optimize workflow tool responses for token efficiency (v2.29.0)
Reduce response sizes by 75-90% for 4 workflow management tools:
- n8n_update_partial_workflow: Returns {id, name, active, operationsApplied}
- n8n_create_workflow: Returns {id, name, active, nodeCount}
- n8n_update_full_workflow: Returns {id, name, active, nodeCount}
- n8n_delete_workflow: Returns {id, name, deleted: true}
AI agents can use n8n_get_workflow with mode 'structure' if they need
to verify the current workflow state after operations.
Conceived by Romuald Członkowski - https://www.aiadvisors.pl/en
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: update tests and add nodeCount to partial update response
- Fix handleCreateWorkflow test to expect minimal response
- Fix handleDeleteWorkflow test to expect minimal response
- Add nodeCount to n8n_update_partial_workflow response for consistency
- Update documentation and CHANGELOG
Conceived by Romuald Członkowski - https://www.aiadvisors.pl/en
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: update handlers-workflow-diff tests for minimal response
Update 3 more tests that expected full workflow in response:
- should apply diff operations successfully
- should activate workflow after successful update
- should deactivate workflow after successful update
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: update integration tests to use minimal response format
Integration tests now verify minimal response format and use
client.getWorkflow() to fetch actual workflow state for verification.
Conceived by Romuald Czlonkowski - www.aiadvisors.pl/en
* fix: update create/update workflow integration tests for minimal response
Integration tests now verify minimal response and use client.getWorkflow()
to fetch actual workflow state for detailed verification.
Conceived by Romuald Czlonkowski - www.aiadvisors.pl/en
* fix: add type assertions to fix TypeScript errors in tests
Conceived by Romuald Czlonkowski - www.aiadvisors.pl/en
---------
Co-authored-by: Romuald Członkowski <romualdczlonkowski@MacBook-Pro-Romuald.local>
Co-authored-by: Claude <noreply@anthropic.com>
1154 lines
37 KiB
TypeScript
1154 lines
37 KiB
TypeScript
/**
|
|
* 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, getMcpRepository } from '../utils/mcp-context';
|
|
import { InstanceContext } from '../../../../src/types/instance-context';
|
|
import { NodeRepository } from '../../../../src/database/node-repository';
|
|
import { handleUpdatePartialWorkflow } from '../../../../src/mcp/handlers-workflow-diff';
|
|
|
|
describe('Integration: handleUpdatePartialWorkflow', () => {
|
|
let context: TestContext;
|
|
let client: N8nApiClient;
|
|
let mcpContext: InstanceContext;
|
|
let repository: NodeRepository;
|
|
|
|
beforeEach(async () => {
|
|
context = createTestContext();
|
|
client = getTestN8nClient();
|
|
mcpContext = createMcpContext();
|
|
repository = await getMcpRepository();
|
|
});
|
|
|
|
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 and connect it to maintain workflow validity
|
|
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'
|
|
}
|
|
]
|
|
}
|
|
}
|
|
}
|
|
},
|
|
{
|
|
type: 'addConnection',
|
|
source: 'Webhook',
|
|
target: 'Set',
|
|
sourcePort: 'main',
|
|
targetPort: 'main'
|
|
}
|
|
]
|
|
},
|
|
repository,
|
|
mcpContext
|
|
);
|
|
|
|
expect(response.success).toBe(true);
|
|
// Response now returns minimal data - verify with a follow-up get
|
|
const responseData = response.data as any;
|
|
expect(responseData.id).toBe(created.id);
|
|
expect(responseData.nodeCount).toBe(2);
|
|
|
|
// Fetch actual workflow to verify changes
|
|
const updated = await client.getWorkflow(created.id);
|
|
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: {}
|
|
}
|
|
}
|
|
]
|
|
},
|
|
repository,
|
|
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'
|
|
}
|
|
]
|
|
},
|
|
repository,
|
|
mcpContext
|
|
);
|
|
|
|
expect(response.success).toBe(true);
|
|
// Response now returns minimal data - verify with a follow-up get
|
|
const responseData = response.data as any;
|
|
expect(responseData.id).toBe(created.id);
|
|
expect(responseData.nodeCount).toBe(1);
|
|
|
|
// Fetch actual workflow to verify changes
|
|
const updated = await client.getWorkflow(created.id);
|
|
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'
|
|
}
|
|
]
|
|
},
|
|
repository,
|
|
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'
|
|
}
|
|
}
|
|
]
|
|
},
|
|
repository,
|
|
mcpContext
|
|
);
|
|
|
|
expect(response.success).toBe(true);
|
|
// Response now returns minimal data - verify with a follow-up get
|
|
expect((response.data as any).id).toBe(created.id);
|
|
|
|
// Fetch actual workflow to verify changes
|
|
const updated = await client.getWorkflow(created.id);
|
|
const webhookNode = updated.nodes.find((n: any) => n.name === 'Webhook');
|
|
expect(webhookNode).toBeDefined();
|
|
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'
|
|
}
|
|
}
|
|
]
|
|
},
|
|
repository,
|
|
mcpContext
|
|
);
|
|
|
|
expect(response.success).toBe(true);
|
|
// Response now returns minimal data - verify with a follow-up get
|
|
expect((response.data as any).id).toBe(created.id);
|
|
|
|
// Fetch actual workflow to verify changes
|
|
const updated = await client.getWorkflow(created.id);
|
|
const webhookNode = updated.nodes.find((n: any) => n.name === 'Webhook');
|
|
expect(webhookNode).toBeDefined();
|
|
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
|
|
}
|
|
]
|
|
},
|
|
repository,
|
|
mcpContext
|
|
);
|
|
|
|
expect(response.success).toBe(true);
|
|
// Response now returns minimal data - verify with a follow-up get
|
|
expect((response.data as any).id).toBe(created.id);
|
|
|
|
// Fetch actual workflow to verify changes
|
|
const updated = await client.getWorkflow(created.id);
|
|
const webhookNode = updated.nodes.find((n: any) => n.name === 'Webhook');
|
|
expect(webhookNode).toBeDefined();
|
|
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'
|
|
}
|
|
]
|
|
},
|
|
repository,
|
|
mcpContext
|
|
);
|
|
|
|
expect(response.success).toBe(true);
|
|
// Response now returns minimal data - verify with a follow-up get
|
|
expect((response.data as any).id).toBe(created.id);
|
|
|
|
// Fetch actual workflow to verify changes
|
|
const updated = await client.getWorkflow(created.id);
|
|
const webhookNode = updated.nodes.find((n: any) => n.name === 'Webhook');
|
|
expect(webhookNode).toBeDefined();
|
|
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' }]
|
|
},
|
|
repository,
|
|
mcpContext
|
|
);
|
|
|
|
// Then enable it
|
|
const response = await handleUpdatePartialWorkflow(
|
|
{
|
|
id: created.id,
|
|
operations: [
|
|
{
|
|
type: 'enableNode',
|
|
nodeName: 'Webhook'
|
|
}
|
|
]
|
|
},
|
|
repository,
|
|
mcpContext
|
|
);
|
|
|
|
expect(response.success).toBe(true);
|
|
// Response now returns minimal data - verify with a follow-up get
|
|
expect((response.data as any).id).toBe(created.id);
|
|
|
|
// Fetch actual workflow to verify changes
|
|
const updated = await client.getWorkflow(created.id);
|
|
const webhookNode = updated.nodes.find((n: any) => n.name === 'Webhook');
|
|
expect(webhookNode).toBeDefined();
|
|
// After enabling, disabled should be false or undefined (both mean enabled)
|
|
expect(webhookNode!.disabled).toBeFalsy();
|
|
});
|
|
});
|
|
});
|
|
|
|
// ======================================================================
|
|
// 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'
|
|
}
|
|
]
|
|
},
|
|
repository,
|
|
mcpContext
|
|
);
|
|
|
|
expect(response.success).toBe(true);
|
|
// Response now returns minimal data - verify with a follow-up get
|
|
expect((response.data as any).id).toBe(created.id);
|
|
|
|
// Fetch actual workflow to verify changes
|
|
const updated = await client.getWorkflow(created.id);
|
|
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
|
|
}
|
|
]
|
|
},
|
|
repository,
|
|
mcpContext
|
|
);
|
|
|
|
expect(response.success).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('removeConnection', () => {
|
|
it('should reject removal of last connection (creates invalid workflow)', 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);
|
|
|
|
// Try to remove the only connection - should be rejected (leaves 2 nodes with no connections)
|
|
const response = await handleUpdatePartialWorkflow(
|
|
{
|
|
id: created.id,
|
|
operations: [
|
|
{
|
|
type: 'removeConnection',
|
|
source: 'Webhook',
|
|
target: 'HTTP Request',
|
|
sourcePort: 'main',
|
|
targetPort: 'main'
|
|
}
|
|
]
|
|
},
|
|
repository,
|
|
mcpContext
|
|
);
|
|
|
|
// Should fail validation - multi-node workflow needs connections
|
|
expect(response.success).toBe(false);
|
|
expect(response.error).toContain('Workflow validation failed');
|
|
});
|
|
|
|
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
|
|
}
|
|
]
|
|
},
|
|
repository,
|
|
mcpContext
|
|
);
|
|
|
|
// Should succeed because ignoreErrors is true
|
|
expect(response.success).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('replaceConnections', () => {
|
|
it('should reject replacing with empty connections (creates invalid workflow)', 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);
|
|
|
|
// Try to replace with empty connections - should be rejected (leaves 2 nodes with no connections)
|
|
const response = await handleUpdatePartialWorkflow(
|
|
{
|
|
id: created.id,
|
|
operations: [
|
|
{
|
|
type: 'replaceConnections',
|
|
connections: {}
|
|
}
|
|
]
|
|
},
|
|
repository,
|
|
mcpContext
|
|
);
|
|
|
|
// Should fail validation - multi-node workflow needs connections
|
|
expect(response.success).toBe(false);
|
|
expect(response.error).toContain('Workflow validation failed');
|
|
});
|
|
});
|
|
|
|
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' }]
|
|
},
|
|
repository,
|
|
mcpContext
|
|
);
|
|
|
|
// Clean stale connections in dry run
|
|
const response = await handleUpdatePartialWorkflow(
|
|
{
|
|
id: created.id,
|
|
operations: [
|
|
{
|
|
type: 'cleanStaleConnections',
|
|
dryRun: true
|
|
}
|
|
],
|
|
validateOnly: true
|
|
},
|
|
repository,
|
|
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'
|
|
}
|
|
}
|
|
]
|
|
},
|
|
repository,
|
|
mcpContext
|
|
);
|
|
|
|
expect(response.success).toBe(true);
|
|
// Response now returns minimal data - verify with a follow-up get
|
|
expect((response.data as any).id).toBe(created.id);
|
|
|
|
// Fetch actual workflow to verify changes
|
|
const updated = await client.getWorkflow(created.id);
|
|
// Note: n8n API may not return all settings in response
|
|
// The operation should succeed even if settings aren't reflected in the response
|
|
expect(updated.settings).toBeDefined();
|
|
});
|
|
});
|
|
|
|
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
|
|
}
|
|
]
|
|
},
|
|
repository,
|
|
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'
|
|
}
|
|
]
|
|
},
|
|
repository,
|
|
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'
|
|
}
|
|
]
|
|
},
|
|
repository,
|
|
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')
|
|
}
|
|
]
|
|
},
|
|
repository,
|
|
mcpContext
|
|
);
|
|
|
|
expect(response.success).toBe(true);
|
|
// Response now returns minimal data - verify with a follow-up get
|
|
const responseData = response.data as any;
|
|
expect(responseData.id).toBe(created.id);
|
|
expect(responseData.nodeCount).toBe(2);
|
|
|
|
// Fetch actual workflow to verify changes
|
|
const updated = await client.getWorkflow(created.id);
|
|
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
|
|
},
|
|
repository,
|
|
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
|
|
},
|
|
repository,
|
|
mcpContext
|
|
);
|
|
|
|
// Should succeed with partial results
|
|
expect(response.success).toBe(true);
|
|
expect(response.details?.applied).toBeDefined();
|
|
expect(response.details?.failed).toBeDefined();
|
|
});
|
|
});
|
|
|
|
// ======================================================================
|
|
// WORKFLOW STRUCTURE VALIDATION (prevents corrupted workflows)
|
|
// ======================================================================
|
|
|
|
describe('Workflow Structure Validation', () => {
|
|
it('should reject removal of all connections in multi-node workflow', async () => {
|
|
// Create workflow with 2 nodes and 1 connection
|
|
const workflow = {
|
|
...SIMPLE_HTTP_WORKFLOW,
|
|
name: createTestWorkflowName('Partial - Reject Empty 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);
|
|
|
|
// Try to remove the only connection - should be rejected
|
|
const response = await handleUpdatePartialWorkflow(
|
|
{
|
|
id: created.id,
|
|
operations: [
|
|
{
|
|
type: 'removeConnection',
|
|
source: 'Webhook',
|
|
target: 'HTTP Request',
|
|
sourcePort: 'main',
|
|
targetPort: 'main'
|
|
}
|
|
]
|
|
},
|
|
repository,
|
|
mcpContext
|
|
);
|
|
|
|
// Should fail validation
|
|
expect(response.success).toBe(false);
|
|
expect(response.error).toContain('Workflow validation failed');
|
|
expect(response.details?.errors).toBeDefined();
|
|
expect(Array.isArray(response.details?.errors)).toBe(true);
|
|
expect((response.details?.errors as string[])[0]).toContain('no connections');
|
|
});
|
|
|
|
it('should reject removal of all nodes except one non-webhook node', async () => {
|
|
// Create workflow with 4 nodes: Webhook, Set 1, Set 2, Merge
|
|
const workflow = {
|
|
...MULTI_NODE_WORKFLOW,
|
|
name: createTestWorkflowName('Partial - Reject Single Non-Webhook'),
|
|
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 remove all nodes except Merge node (non-webhook) - should be rejected
|
|
const response = await handleUpdatePartialWorkflow(
|
|
{
|
|
id: created.id,
|
|
operations: [
|
|
{
|
|
type: 'removeNode',
|
|
nodeName: 'Webhook'
|
|
},
|
|
{
|
|
type: 'removeNode',
|
|
nodeName: 'Set 1'
|
|
},
|
|
{
|
|
type: 'removeNode',
|
|
nodeName: 'Set 2'
|
|
}
|
|
]
|
|
},
|
|
repository,
|
|
mcpContext
|
|
);
|
|
|
|
// Should fail validation
|
|
expect(response.success).toBe(false);
|
|
expect(response.error).toContain('Workflow validation failed');
|
|
expect(response.details?.errors).toBeDefined();
|
|
expect(Array.isArray(response.details?.errors)).toBe(true);
|
|
expect((response.details?.errors as string[])[0]).toContain('Single non-webhook node');
|
|
});
|
|
|
|
it('should allow valid partial updates that maintain workflow integrity', async () => {
|
|
// Create workflow with 4 nodes
|
|
const workflow = {
|
|
...MULTI_NODE_WORKFLOW,
|
|
name: createTestWorkflowName('Partial - Valid Update'),
|
|
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);
|
|
|
|
// Valid update: add a node and connect it
|
|
const response = await handleUpdatePartialWorkflow(
|
|
{
|
|
id: created.id,
|
|
operations: [
|
|
{
|
|
type: 'addNode',
|
|
node: {
|
|
name: 'Process Data',
|
|
type: 'n8n-nodes-base.set',
|
|
typeVersion: 3.4,
|
|
position: [850, 300],
|
|
parameters: {
|
|
assignments: {
|
|
assignments: []
|
|
}
|
|
}
|
|
}
|
|
},
|
|
{
|
|
type: 'addConnection',
|
|
source: 'Merge',
|
|
target: 'Process Data',
|
|
sourcePort: 'main',
|
|
targetPort: 'main'
|
|
}
|
|
]
|
|
},
|
|
repository,
|
|
mcpContext
|
|
);
|
|
|
|
// Should succeed
|
|
expect(response.success).toBe(true);
|
|
// Response now returns minimal data - verify with a follow-up get
|
|
const responseData = response.data as any;
|
|
expect(responseData.id).toBe(created.id);
|
|
expect(responseData.nodeCount).toBe(5); // Original 4 + 1 new
|
|
|
|
// Fetch actual workflow to verify changes
|
|
const updated = await client.getWorkflow(created.id);
|
|
expect(updated.nodes).toHaveLength(5); // Original 4 + 1 new
|
|
expect(updated.nodes.find((n: any) => n.name === 'Process Data')).toBeDefined();
|
|
});
|
|
|
|
it('should reject adding node without connecting it (disconnected node)', async () => {
|
|
// Create workflow with 2 connected nodes
|
|
const workflow = {
|
|
...SIMPLE_HTTP_WORKFLOW,
|
|
name: createTestWorkflowName('Partial - Reject Disconnected 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);
|
|
|
|
// Try to add a third node WITHOUT connecting it - should be rejected
|
|
const response = await handleUpdatePartialWorkflow(
|
|
{
|
|
id: created.id,
|
|
operations: [
|
|
{
|
|
type: 'addNode',
|
|
node: {
|
|
name: 'Disconnected Set',
|
|
type: 'n8n-nodes-base.set',
|
|
typeVersion: 3.4,
|
|
position: [800, 300],
|
|
parameters: {
|
|
assignments: {
|
|
assignments: []
|
|
}
|
|
}
|
|
}
|
|
// Note: No connection operation - this creates a disconnected node
|
|
}
|
|
]
|
|
},
|
|
repository,
|
|
mcpContext
|
|
);
|
|
|
|
// Should fail validation - disconnected node detected
|
|
expect(response.success).toBe(false);
|
|
expect(response.error).toContain('Workflow validation failed');
|
|
expect(response.details?.errors).toBeDefined();
|
|
expect(Array.isArray(response.details?.errors)).toBe(true);
|
|
const errorMessage = (response.details?.errors as string[])[0];
|
|
expect(errorMessage).toContain('Disconnected nodes detected');
|
|
expect(errorMessage).toContain('Disconnected Set');
|
|
});
|
|
});
|
|
});
|