feat: implement transferWorkflow operation in n8n_update_partial_workflow (#644) (#649)

Add transferWorkflow diff operation to move workflows between n8n projects:
- TransferWorkflowOperation type with destinationProjectId field
- WorkflowDiffEngine validates and tracks transfer intent
- Handler calls PUT /workflows/{id}/transfer after update
- N8nApiClient.transferWorkflow() method
- Zod schema validates destinationProjectId is non-empty
- Tool description and documentation updated
- inferIntentFromOperations case for transfer

Also fixes two pre-existing bugs found during review:
- continueOnError path now properly extracts/propagates activation flags
- Debug log in updateConnectionReferences shows correct old name

Based on work by @djakielski in PR #645.


Conceived by Romuald Członkowski - https://www.aiadvisors.pl/en
This commit is contained in:
Romuald Członkowski
2026-03-20 17:50:00 +01:00
committed by GitHub
parent 14962a39b6
commit 47a1cb135d
30 changed files with 582 additions and 37 deletions

View File

@@ -1250,6 +1250,56 @@ describe('N8nApiClient', () => {
});
});
describe('transferWorkflow', () => {
beforeEach(() => {
client = new N8nApiClient(defaultConfig);
});
it('should transfer workflow successfully via PUT', async () => {
mockAxiosInstance.put.mockResolvedValue({ data: undefined });
await client.transferWorkflow('123', 'project-456');
expect(mockAxiosInstance.put).toHaveBeenCalledWith(
'/workflows/123/transfer',
{ destinationProjectId: 'project-456' }
);
});
it('should throw N8nNotFoundError on 404', async () => {
const error = {
message: 'Request failed',
response: { status: 404, data: { message: 'Workflow not found' } }
};
await mockAxiosInstance.simulateError('put', error);
try {
await client.transferWorkflow('123', 'project-456');
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 throw appropriate error on 403 forbidden', async () => {
const error = {
message: 'Request failed',
response: { status: 403, data: { message: 'Forbidden' } }
};
await mockAxiosInstance.simulateError('put', error);
try {
await client.transferWorkflow('123', 'project-456');
expect.fail('Should have thrown an error');
} catch (err) {
expect(err).toBeInstanceOf(N8nApiError);
expect((err as N8nApiError).statusCode).toBe(403);
}
});
});
describe('interceptors', () => {
let requestInterceptor: any;
let responseInterceptor: any;
@@ -1317,4 +1367,4 @@ describe('N8nApiClient', () => {
expect(result.message).toBe('Bad request');
});
});
});
});