test: Add comprehensive test coverage for workflow activation/deactivation

Added 25 new tests to improve coverage for workflow activation/deactivation feature:
- 7 tests for handlers-workflow-diff.test.ts (activation/deactivation handler logic)
- 8 tests for workflow-diff-engine.test.ts (validate/apply activate/deactivate operations)
- 10 tests for n8n-api-client.test.ts (API client activation/deactivation methods)

Coverage improvements:
- Branch coverage increased from 77% to 85.58%
- All 3512 tests passing

Tests cover:
- Successful workflow activation/deactivation after updates
- Error handling for activation/deactivation failures
- Validation of activatable trigger nodes (webhook, schedule, etc.)
- Rejection of workflows without activatable triggers
- API client error cases (not found, already active/inactive, server errors)

Conceived by Romuald Członkowski - www.aiadvisors.pl/en
This commit is contained in:
czlonkowski
2025-11-06 23:58:34 +01:00
parent 4d3b8fbc91
commit 3578f2cc31
3 changed files with 733 additions and 5 deletions

View File

@@ -635,5 +635,211 @@ describe('handlers-workflow-diff', () => {
}, },
}); });
}); });
describe('Workflow Activation/Deactivation', () => {
it('should activate workflow after successful update', async () => {
const testWorkflow = createTestWorkflow({ active: false });
const updatedWorkflow = { ...testWorkflow, active: false };
const activatedWorkflow = { ...testWorkflow, active: true };
mockApiClient.getWorkflow.mockResolvedValue(testWorkflow);
mockDiffEngine.applyDiff.mockResolvedValue({
success: true,
workflow: updatedWorkflow,
operationsApplied: 1,
message: 'Success',
errors: [],
shouldActivate: true,
});
mockApiClient.updateWorkflow.mockResolvedValue(updatedWorkflow);
mockApiClient.activateWorkflow = vi.fn().mockResolvedValue(activatedWorkflow);
const result = await handleUpdatePartialWorkflow({
id: 'test-workflow-id',
operations: [{ type: 'activateWorkflow' }],
}, mockRepository);
expect(result.success).toBe(true);
expect(result.data).toEqual(activatedWorkflow);
expect(result.message).toContain('Workflow activated');
expect(result.details?.active).toBe(true);
expect(mockApiClient.activateWorkflow).toHaveBeenCalledWith('test-workflow-id');
});
it('should deactivate workflow after successful update', async () => {
const testWorkflow = createTestWorkflow({ active: true });
const updatedWorkflow = { ...testWorkflow, active: true };
const deactivatedWorkflow = { ...testWorkflow, active: false };
mockApiClient.getWorkflow.mockResolvedValue(testWorkflow);
mockDiffEngine.applyDiff.mockResolvedValue({
success: true,
workflow: updatedWorkflow,
operationsApplied: 1,
message: 'Success',
errors: [],
shouldDeactivate: true,
});
mockApiClient.updateWorkflow.mockResolvedValue(updatedWorkflow);
mockApiClient.deactivateWorkflow = vi.fn().mockResolvedValue(deactivatedWorkflow);
const result = await handleUpdatePartialWorkflow({
id: 'test-workflow-id',
operations: [{ type: 'deactivateWorkflow' }],
}, mockRepository);
expect(result.success).toBe(true);
expect(result.data).toEqual(deactivatedWorkflow);
expect(result.message).toContain('Workflow deactivated');
expect(result.details?.active).toBe(false);
expect(mockApiClient.deactivateWorkflow).toHaveBeenCalledWith('test-workflow-id');
});
it('should handle activation failure after successful update', async () => {
const testWorkflow = createTestWorkflow({ active: false });
const updatedWorkflow = { ...testWorkflow, active: false };
mockApiClient.getWorkflow.mockResolvedValue(testWorkflow);
mockDiffEngine.applyDiff.mockResolvedValue({
success: true,
workflow: updatedWorkflow,
operationsApplied: 1,
message: 'Success',
errors: [],
shouldActivate: true,
});
mockApiClient.updateWorkflow.mockResolvedValue(updatedWorkflow);
mockApiClient.activateWorkflow = vi.fn().mockRejectedValue(new Error('Activation failed: No trigger nodes'));
const result = await handleUpdatePartialWorkflow({
id: 'test-workflow-id',
operations: [{ type: 'activateWorkflow' }],
}, mockRepository);
expect(result.success).toBe(false);
expect(result.error).toBe('Workflow updated successfully but activation failed');
expect(result.details).toEqual({
workflowUpdated: true,
activationError: 'Activation failed: No trigger nodes',
});
});
it('should handle deactivation failure after successful update', async () => {
const testWorkflow = createTestWorkflow({ active: true });
const updatedWorkflow = { ...testWorkflow, active: true };
mockApiClient.getWorkflow.mockResolvedValue(testWorkflow);
mockDiffEngine.applyDiff.mockResolvedValue({
success: true,
workflow: updatedWorkflow,
operationsApplied: 1,
message: 'Success',
errors: [],
shouldDeactivate: true,
});
mockApiClient.updateWorkflow.mockResolvedValue(updatedWorkflow);
mockApiClient.deactivateWorkflow = vi.fn().mockRejectedValue(new Error('Deactivation failed'));
const result = await handleUpdatePartialWorkflow({
id: 'test-workflow-id',
operations: [{ type: 'deactivateWorkflow' }],
}, mockRepository);
expect(result.success).toBe(false);
expect(result.error).toBe('Workflow updated successfully but deactivation failed');
expect(result.details).toEqual({
workflowUpdated: true,
deactivationError: 'Deactivation failed',
});
});
it('should update workflow without activation when shouldActivate is false', async () => {
const testWorkflow = createTestWorkflow({ active: false });
const updatedWorkflow = { ...testWorkflow, active: false };
mockApiClient.getWorkflow.mockResolvedValue(testWorkflow);
mockDiffEngine.applyDiff.mockResolvedValue({
success: true,
workflow: updatedWorkflow,
operationsApplied: 1,
message: 'Success',
errors: [],
shouldActivate: false,
shouldDeactivate: false,
});
mockApiClient.updateWorkflow.mockResolvedValue(updatedWorkflow);
mockApiClient.activateWorkflow = vi.fn();
mockApiClient.deactivateWorkflow = vi.fn();
const result = await handleUpdatePartialWorkflow({
id: 'test-workflow-id',
operations: [{ type: 'updateName', name: 'Updated' }],
}, mockRepository);
expect(result.success).toBe(true);
expect(result.message).not.toContain('activated');
expect(result.message).not.toContain('deactivated');
expect(mockApiClient.activateWorkflow).not.toHaveBeenCalled();
expect(mockApiClient.deactivateWorkflow).not.toHaveBeenCalled();
});
it('should handle non-Error activation failures', async () => {
const testWorkflow = createTestWorkflow({ active: false });
const updatedWorkflow = { ...testWorkflow, active: false };
mockApiClient.getWorkflow.mockResolvedValue(testWorkflow);
mockDiffEngine.applyDiff.mockResolvedValue({
success: true,
workflow: updatedWorkflow,
operationsApplied: 1,
message: 'Success',
errors: [],
shouldActivate: true,
});
mockApiClient.updateWorkflow.mockResolvedValue(updatedWorkflow);
mockApiClient.activateWorkflow = vi.fn().mockRejectedValue('String error');
const result = await handleUpdatePartialWorkflow({
id: 'test-workflow-id',
operations: [{ type: 'activateWorkflow' }],
}, mockRepository);
expect(result.success).toBe(false);
expect(result.error).toBe('Workflow updated successfully but activation failed');
expect(result.details).toEqual({
workflowUpdated: true,
activationError: 'Unknown error',
});
});
it('should handle non-Error deactivation failures', async () => {
const testWorkflow = createTestWorkflow({ active: true });
const updatedWorkflow = { ...testWorkflow, active: true };
mockApiClient.getWorkflow.mockResolvedValue(testWorkflow);
mockDiffEngine.applyDiff.mockResolvedValue({
success: true,
workflow: updatedWorkflow,
operationsApplied: 1,
message: 'Success',
errors: [],
shouldDeactivate: true,
});
mockApiClient.updateWorkflow.mockResolvedValue(updatedWorkflow);
mockApiClient.deactivateWorkflow = vi.fn().mockRejectedValue({ code: 'UNKNOWN' });
const result = await handleUpdatePartialWorkflow({
id: 'test-workflow-id',
operations: [{ type: 'deactivateWorkflow' }],
}, mockRepository);
expect(result.success).toBe(false);
expect(result.error).toBe('Workflow updated successfully but deactivation failed');
expect(result.details).toEqual({
workflowUpdated: true,
deactivationError: 'Unknown error',
});
});
});
}); });
}); });

View File

@@ -362,19 +362,19 @@ describe('N8nApiClient', () => {
it('should delete workflow successfully', async () => { it('should delete workflow successfully', async () => {
mockAxiosInstance.delete.mockResolvedValue({ data: {} }); mockAxiosInstance.delete.mockResolvedValue({ data: {} });
await client.deleteWorkflow('123'); await client.deleteWorkflow('123');
expect(mockAxiosInstance.delete).toHaveBeenCalledWith('/workflows/123'); expect(mockAxiosInstance.delete).toHaveBeenCalledWith('/workflows/123');
}); });
it('should handle deletion error', async () => { it('should handle deletion error', async () => {
const error = { const error = {
message: 'Request failed', message: 'Request failed',
response: { status: 404, data: { message: 'Not found' } } response: { status: 404, data: { message: 'Not found' } }
}; };
await mockAxiosInstance.simulateError('delete', error); await mockAxiosInstance.simulateError('delete', error);
try { try {
await client.deleteWorkflow('123'); await client.deleteWorkflow('123');
expect.fail('Should have thrown an error'); expect.fail('Should have thrown an error');
@@ -386,6 +386,178 @@ describe('N8nApiClient', () => {
}); });
}); });
describe('activateWorkflow', () => {
beforeEach(() => {
client = new N8nApiClient(defaultConfig);
});
it('should activate workflow successfully', async () => {
const workflow = { id: '123', name: 'Test', active: false, nodes: [], connections: {} };
const activatedWorkflow = { ...workflow, active: true };
mockAxiosInstance.post.mockResolvedValue({ data: activatedWorkflow });
const result = await client.activateWorkflow('123');
expect(mockAxiosInstance.post).toHaveBeenCalledWith('/workflows/123/activate');
expect(result).toEqual(activatedWorkflow);
expect(result.active).toBe(true);
});
it('should handle activation error - no trigger nodes', async () => {
const error = {
message: 'Request failed',
response: { status: 400, data: { message: 'Workflow must have at least one trigger node' } }
};
await mockAxiosInstance.simulateError('post', error);
try {
await client.activateWorkflow('123');
expect.fail('Should have thrown an error');
} catch (err) {
expect(err).toBeInstanceOf(N8nValidationError);
expect((err as N8nValidationError).message).toContain('trigger node');
expect((err as N8nValidationError).statusCode).toBe(400);
}
});
it('should handle activation error - workflow not found', async () => {
const error = {
message: 'Request failed',
response: { status: 404, data: { message: 'Workflow not found' } }
};
await mockAxiosInstance.simulateError('post', error);
try {
await client.activateWorkflow('non-existent');
expect.fail('Should have thrown an error');
} catch (err) {
expect(err).toBeInstanceOf(N8nNotFoundError);
expect((err as N8nNotFoundError).message).toContain('not found');
expect((err as N8nNotFoundError).statusCode).toBe(404);
}
});
it('should handle activation error - workflow already active', async () => {
const error = {
message: 'Request failed',
response: { status: 400, data: { message: 'Workflow is already active' } }
};
await mockAxiosInstance.simulateError('post', error);
try {
await client.activateWorkflow('123');
expect.fail('Should have thrown an error');
} catch (err) {
expect(err).toBeInstanceOf(N8nValidationError);
expect((err as N8nValidationError).message).toContain('already active');
expect((err as N8nValidationError).statusCode).toBe(400);
}
});
it('should handle server error during activation', async () => {
const error = {
message: 'Request failed',
response: { status: 500, data: { message: 'Internal server error' } }
};
await mockAxiosInstance.simulateError('post', error);
try {
await client.activateWorkflow('123');
expect.fail('Should have thrown an error');
} catch (err) {
expect(err).toBeInstanceOf(N8nServerError);
expect((err as N8nServerError).message).toBe('Internal server error');
expect((err as N8nServerError).statusCode).toBe(500);
}
});
});
describe('deactivateWorkflow', () => {
beforeEach(() => {
client = new N8nApiClient(defaultConfig);
});
it('should deactivate workflow successfully', async () => {
const workflow = { id: '123', name: 'Test', active: true, nodes: [], connections: {} };
const deactivatedWorkflow = { ...workflow, active: false };
mockAxiosInstance.post.mockResolvedValue({ data: deactivatedWorkflow });
const result = await client.deactivateWorkflow('123');
expect(mockAxiosInstance.post).toHaveBeenCalledWith('/workflows/123/deactivate');
expect(result).toEqual(deactivatedWorkflow);
expect(result.active).toBe(false);
});
it('should handle deactivation error - workflow not found', async () => {
const error = {
message: 'Request failed',
response: { status: 404, data: { message: 'Workflow not found' } }
};
await mockAxiosInstance.simulateError('post', error);
try {
await client.deactivateWorkflow('non-existent');
expect.fail('Should have thrown an error');
} catch (err) {
expect(err).toBeInstanceOf(N8nNotFoundError);
expect((err as N8nNotFoundError).message).toContain('not found');
expect((err as N8nNotFoundError).statusCode).toBe(404);
}
});
it('should handle deactivation error - workflow already inactive', async () => {
const error = {
message: 'Request failed',
response: { status: 400, data: { message: 'Workflow is already inactive' } }
};
await mockAxiosInstance.simulateError('post', error);
try {
await client.deactivateWorkflow('123');
expect.fail('Should have thrown an error');
} catch (err) {
expect(err).toBeInstanceOf(N8nValidationError);
expect((err as N8nValidationError).message).toContain('already inactive');
expect((err as N8nValidationError).statusCode).toBe(400);
}
});
it('should handle server error during deactivation', async () => {
const error = {
message: 'Request failed',
response: { status: 500, data: { message: 'Internal server error' } }
};
await mockAxiosInstance.simulateError('post', error);
try {
await client.deactivateWorkflow('123');
expect.fail('Should have thrown an error');
} catch (err) {
expect(err).toBeInstanceOf(N8nServerError);
expect((err as N8nServerError).message).toBe('Internal server error');
expect((err as N8nServerError).statusCode).toBe(500);
}
});
it('should handle authentication error during deactivation', async () => {
const error = {
message: 'Request failed',
response: { status: 401, data: { message: 'Invalid API key' } }
};
await mockAxiosInstance.simulateError('post', error);
try {
await client.deactivateWorkflow('123');
expect.fail('Should have thrown an error');
} catch (err) {
expect(err).toBeInstanceOf(N8nAuthenticationError);
expect((err as N8nAuthenticationError).message).toBe('Invalid API key');
expect((err as N8nAuthenticationError).statusCode).toBe(401);
}
});
});
describe('listWorkflows', () => { describe('listWorkflows', () => {
beforeEach(() => { beforeEach(() => {
client = new N8nApiClient(defaultConfig); client = new N8nApiClient(defaultConfig);

View File

@@ -4269,4 +4269,354 @@ describe('WorkflowDiffEngine', () => {
expect(result.workflow.connections["When clicking 'Execute workflow'"]).toBeDefined(); expect(result.workflow.connections["When clicking 'Execute workflow'"]).toBeDefined();
}); });
}); });
describe('Workflow Activation/Deactivation Operations', () => {
it('should activate workflow with activatable trigger nodes', async () => {
// Create workflow with webhook trigger (activatable)
const workflowWithTrigger = createWorkflow('Test Workflow')
.addWebhookNode({ id: 'webhook-1', name: 'Webhook Trigger' })
.addHttpRequestNode({ id: 'http-1', name: 'HTTP Request' })
.connect('webhook-1', 'http-1')
.build() as Workflow;
// Fix connections to use node names
const newConnections: any = {};
for (const [nodeId, outputs] of Object.entries(workflowWithTrigger.connections)) {
const node = workflowWithTrigger.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 = workflowWithTrigger.nodes.find((n: any) => n.id === conn.node);
return { ...conn, node: targetNode ? targetNode.name : conn.node };
})
);
}
}
}
workflowWithTrigger.connections = newConnections;
const operation: any = {
type: 'activateWorkflow'
};
const request: WorkflowDiffRequest = {
id: 'test-workflow',
operations: [operation]
};
const result = await diffEngine.applyDiff(workflowWithTrigger, request);
expect(result.success).toBe(true);
expect(result.shouldActivate).toBe(true);
expect((result.workflow as any)._shouldActivate).toBeUndefined(); // Flag should be cleaned up
});
it('should reject activation if no activatable trigger nodes', async () => {
// Create workflow with no trigger nodes at all
const workflowWithoutActivatableTrigger = createWorkflow('Test Workflow')
.addNode({
id: 'set-1',
name: 'Set Node',
type: 'n8n-nodes-base.set',
position: [100, 100],
parameters: {}
})
.addHttpRequestNode({ id: 'http-1', name: 'HTTP Request' })
.connect('set-1', 'http-1')
.build() as Workflow;
// Fix connections to use node names
const newConnections: any = {};
for (const [nodeId, outputs] of Object.entries(workflowWithoutActivatableTrigger.connections)) {
const node = workflowWithoutActivatableTrigger.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 = workflowWithoutActivatableTrigger.nodes.find((n: any) => n.id === conn.node);
return { ...conn, node: targetNode ? targetNode.name : conn.node };
})
);
}
}
}
workflowWithoutActivatableTrigger.connections = newConnections;
const operation: any = {
type: 'activateWorkflow'
};
const request: WorkflowDiffRequest = {
id: 'test-workflow',
operations: [operation]
};
const result = await diffEngine.applyDiff(workflowWithoutActivatableTrigger, request);
expect(result.success).toBe(false);
expect(result.errors).toBeDefined();
expect(result.errors![0].message).toContain('No activatable trigger nodes found');
expect(result.errors![0].message).toContain('executeWorkflowTrigger cannot activate workflows');
});
it('should reject activation if all trigger nodes are disabled', async () => {
// Create workflow with disabled webhook trigger
const workflowWithDisabledTrigger = createWorkflow('Test Workflow')
.addWebhookNode({ id: 'webhook-1', name: 'Webhook Trigger', disabled: true })
.addHttpRequestNode({ id: 'http-1', name: 'HTTP Request' })
.connect('webhook-1', 'http-1')
.build() as Workflow;
// Fix connections to use node names
const newConnections: any = {};
for (const [nodeId, outputs] of Object.entries(workflowWithDisabledTrigger.connections)) {
const node = workflowWithDisabledTrigger.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 = workflowWithDisabledTrigger.nodes.find((n: any) => n.id === conn.node);
return { ...conn, node: targetNode ? targetNode.name : conn.node };
})
);
}
}
}
workflowWithDisabledTrigger.connections = newConnections;
const operation: any = {
type: 'activateWorkflow'
};
const request: WorkflowDiffRequest = {
id: 'test-workflow',
operations: [operation]
};
const result = await diffEngine.applyDiff(workflowWithDisabledTrigger, request);
expect(result.success).toBe(false);
expect(result.errors).toBeDefined();
expect(result.errors![0].message).toContain('No activatable trigger nodes found');
});
it('should activate workflow with schedule trigger', async () => {
// Create workflow with schedule trigger (activatable)
const workflowWithSchedule = createWorkflow('Test Workflow')
.addNode({
id: 'schedule-1',
name: 'Schedule',
type: 'n8n-nodes-base.scheduleTrigger',
position: [100, 100],
parameters: { rule: { interval: [{ field: 'hours', hoursInterval: 1 }] } }
})
.addHttpRequestNode({ id: 'http-1', name: 'HTTP Request' })
.connect('schedule-1', 'http-1')
.build() as Workflow;
// Fix connections
const newConnections: any = {};
for (const [nodeId, outputs] of Object.entries(workflowWithSchedule.connections)) {
const node = workflowWithSchedule.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 = workflowWithSchedule.nodes.find((n: any) => n.id === conn.node);
return { ...conn, node: targetNode ? targetNode.name : conn.node };
})
);
}
}
}
workflowWithSchedule.connections = newConnections;
const operation: any = {
type: 'activateWorkflow'
};
const request: WorkflowDiffRequest = {
id: 'test-workflow',
operations: [operation]
};
const result = await diffEngine.applyDiff(workflowWithSchedule, request);
expect(result.success).toBe(true);
expect(result.shouldActivate).toBe(true);
});
it('should deactivate workflow successfully', async () => {
// Any workflow can be deactivated
const operation: any = {
type: 'deactivateWorkflow'
};
const request: WorkflowDiffRequest = {
id: 'test-workflow',
operations: [operation]
};
const result = await diffEngine.applyDiff(baseWorkflow, request);
expect(result.success).toBe(true);
expect(result.shouldDeactivate).toBe(true);
expect((result.workflow as any)._shouldDeactivate).toBeUndefined(); // Flag should be cleaned up
});
it('should deactivate workflow without trigger nodes', async () => {
// Create workflow without any trigger nodes
const workflowWithoutTrigger = createWorkflow('Test Workflow')
.addHttpRequestNode({ id: 'http-1', name: 'HTTP Request' })
.addNode({
id: 'set-1',
name: 'Set',
type: 'n8n-nodes-base.set',
position: [300, 100],
parameters: {}
})
.connect('http-1', 'set-1')
.build() as Workflow;
// Fix connections
const newConnections: any = {};
for (const [nodeId, outputs] of Object.entries(workflowWithoutTrigger.connections)) {
const node = workflowWithoutTrigger.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 = workflowWithoutTrigger.nodes.find((n: any) => n.id === conn.node);
return { ...conn, node: targetNode ? targetNode.name : conn.node };
})
);
}
}
}
workflowWithoutTrigger.connections = newConnections;
const operation: any = {
type: 'deactivateWorkflow'
};
const request: WorkflowDiffRequest = {
id: 'test-workflow',
operations: [operation]
};
const result = await diffEngine.applyDiff(workflowWithoutTrigger, request);
expect(result.success).toBe(true);
expect(result.shouldDeactivate).toBe(true);
});
it('should combine activation with other operations', async () => {
// Create workflow with webhook trigger
const workflowWithTrigger = createWorkflow('Test Workflow')
.addWebhookNode({ id: 'webhook-1', name: 'Webhook Trigger' })
.addHttpRequestNode({ id: 'http-1', name: 'HTTP Request' })
.connect('webhook-1', 'http-1')
.build() as Workflow;
// Fix connections
const newConnections: any = {};
for (const [nodeId, outputs] of Object.entries(workflowWithTrigger.connections)) {
const node = workflowWithTrigger.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 = workflowWithTrigger.nodes.find((n: any) => n.id === conn.node);
return { ...conn, node: targetNode ? targetNode.name : conn.node };
})
);
}
}
}
workflowWithTrigger.connections = newConnections;
const operations: any[] = [
{
type: 'updateName',
name: 'Updated Workflow Name'
},
{
type: 'addTag',
tag: 'production'
},
{
type: 'activateWorkflow'
}
];
const request: WorkflowDiffRequest = {
id: 'test-workflow',
operations
};
const result = await diffEngine.applyDiff(workflowWithTrigger, request);
expect(result.success).toBe(true);
expect(result.operationsApplied).toBe(3);
expect(result.workflow!.name).toBe('Updated Workflow Name');
expect(result.workflow!.tags).toContain('production');
expect(result.shouldActivate).toBe(true);
});
it('should reject activation if workflow has executeWorkflowTrigger only', async () => {
// Create workflow with executeWorkflowTrigger (not activatable - Issue #351)
const workflowWithExecuteTrigger = createWorkflow('Test Workflow')
.addNode({
id: 'execute-1',
name: 'Execute Workflow Trigger',
type: 'n8n-nodes-base.executeWorkflowTrigger',
position: [100, 100],
parameters: {}
})
.addHttpRequestNode({ id: 'http-1', name: 'HTTP Request' })
.connect('execute-1', 'http-1')
.build() as Workflow;
// Fix connections
const newConnections: any = {};
for (const [nodeId, outputs] of Object.entries(workflowWithExecuteTrigger.connections)) {
const node = workflowWithExecuteTrigger.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 = workflowWithExecuteTrigger.nodes.find((n: any) => n.id === conn.node);
return { ...conn, node: targetNode ? targetNode.name : conn.node };
})
);
}
}
}
workflowWithExecuteTrigger.connections = newConnections;
const operation: any = {
type: 'activateWorkflow'
};
const request: WorkflowDiffRequest = {
id: 'test-workflow',
operations: [operation]
};
const result = await diffEngine.applyDiff(workflowWithExecuteTrigger, request);
expect(result.success).toBe(false);
expect(result.errors).toBeDefined();
expect(result.errors![0].message).toContain('No activatable trigger nodes found');
expect(result.errors![0].message).toContain('executeWorkflowTrigger cannot activate workflows');
});
});
}); });