mirror of
https://github.com/czlonkowski/n8n-mcp.git
synced 2026-03-28 21:23:07 +00:00
refactor: streamline test suite — 33 fewer files, 11.9x faster (#670)
* refactor: streamline test suite - cut 33 files, enable parallel execution (11.9x speedup) Remove duplicate, low-value, and fragmented test files while preserving all meaningful coverage. Enable parallel test execution and remove the entire benchmark infrastructure. Key changes: - Consolidate workflow-validator tests (13 files -> 3) - Consolidate config-validator tests (9 files -> 3) - Consolidate telemetry tests (11 files -> 6) - Merge AI validator tests (2 files -> 1) - Remove example/demo test files, mock-testing files, and already-skipped tests - Remove benchmark infrastructure (10 files, CI workflow, 4 npm scripts) - Enable parallel test execution (remove singleThread: true) - Remove retry:2 that was masking flaky tests - Slim CI publish-results job Results: 224 -> 191 test files, 4690 -> 4303 tests, 121K -> 106K lines Local runtime: 319s -> 27s (11.9x speedup) Conceived by Romuald Członkowski - www.aiadvisors.pl/en Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor: absorb config-validator satellite tests into consolidated file The previous commit deleted 4 config-validator satellite files. This properly merges their unique tests into the consolidated config-validator.test.ts, recovering 89 tests that were dropped during the bulk deletion. Deduplicates 5 tests that existed in both the satellite files and the security test file. Conceived by Romuald Członkowski - www.aiadvisors.pl/en Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: delete missed benchmark-pr.yml workflow, fix flaky session test - Remove benchmark-pr.yml that referenced deleted benchmark:ci script - Fix session-persistence round-trip test using timestamps closer to now to avoid edge cases exposed by removing retry:2 Conceived by Romuald Członkowski - www.aiadvisors.pl/en Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: rebuild FTS5 index after database rebuild to prevent stale rowid refs The FTS5 content-synced index could retain phantom rowid references from previous rebuild cycles, causing 'missing row N from content table' errors on MATCH queries. - Add explicit FTS5 rebuild command in rebuild script after all nodes saved - Add FTS5 rebuild in test beforeAll as defense-in-depth - Rebuild nodes.db with consistent FTS5 index Conceived by Romuald Członkowski - www.aiadvisors.pl/en Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: use recent timestamps in all session persistence tests Session round-trip tests used timestamps 5-10 minutes in the past which could fail under CI load when combined with session timeout validation. Use timestamps 30 seconds in the past for all valid-session test data. Conceived by Romuald Członkowski - www.aiadvisors.pl/en Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
committed by
GitHub
parent
07bd1d4cc2
commit
de2abaf89d
@@ -915,4 +915,269 @@ describe('WorkflowValidator - Connection Validation (#620)', () => {
|
||||
expect(warning!.message).toContain('"unmatched" branch has no effect');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Error Output Validation (absorbed from workflow-validator-error-outputs) ──
|
||||
|
||||
describe('Error Output Configuration', () => {
|
||||
it('should detect incorrect configuration - multiple nodes in same array', async () => {
|
||||
const workflow = {
|
||||
nodes: [
|
||||
{ id: '1', name: 'Validate Input', type: 'n8n-nodes-base.set', typeVersion: 3.4, position: [-400, 64], parameters: {} },
|
||||
{ id: '2', name: 'Filter URLs', type: 'n8n-nodes-base.filter', typeVersion: 2.2, position: [-176, 64], parameters: {} },
|
||||
{ id: '3', name: 'Error Response1', type: 'n8n-nodes-base.respondToWebhook', typeVersion: 1.5, position: [-160, 240], parameters: {} },
|
||||
],
|
||||
connections: {
|
||||
'Validate Input': {
|
||||
main: [[
|
||||
{ node: 'Filter URLs', type: 'main', index: 0 },
|
||||
{ node: 'Error Response1', type: 'main', index: 0 },
|
||||
]],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = await validator.validateWorkflow(workflow as any);
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors.some(e =>
|
||||
e.message.includes('Incorrect error output configuration') &&
|
||||
e.message.includes('Error Response1') &&
|
||||
e.message.includes('appear to be error handlers but are in main[0]'),
|
||||
)).toBe(true);
|
||||
const errorMsg = result.errors.find(e => e.message.includes('Incorrect error output configuration'));
|
||||
expect(errorMsg?.message).toContain('INCORRECT (current)');
|
||||
expect(errorMsg?.message).toContain('CORRECT (should be)');
|
||||
});
|
||||
|
||||
it('should validate correct configuration - separate arrays', async () => {
|
||||
const workflow = {
|
||||
nodes: [
|
||||
{ id: '1', name: 'Validate Input', type: 'n8n-nodes-base.set', typeVersion: 3.4, position: [-400, 64], parameters: {}, onError: 'continueErrorOutput' },
|
||||
{ id: '2', name: 'Filter URLs', type: 'n8n-nodes-base.filter', typeVersion: 2.2, position: [-176, 64], parameters: {} },
|
||||
{ id: '3', name: 'Error Response1', type: 'n8n-nodes-base.respondToWebhook', typeVersion: 1.5, position: [-160, 240], parameters: {} },
|
||||
],
|
||||
connections: {
|
||||
'Validate Input': {
|
||||
main: [
|
||||
[{ node: 'Filter URLs', type: 'main', index: 0 }],
|
||||
[{ node: 'Error Response1', type: 'main', index: 0 }],
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = await validator.validateWorkflow(workflow as any);
|
||||
expect(result.errors.some(e => e.message.includes('Incorrect error output configuration'))).toBe(false);
|
||||
});
|
||||
|
||||
it('should detect onError without error connections', async () => {
|
||||
const workflow = {
|
||||
nodes: [
|
||||
{ id: '1', name: 'HTTP Request', type: 'n8n-nodes-base.httpRequest', typeVersion: 4, position: [100, 100], parameters: {}, onError: 'continueErrorOutput' },
|
||||
{ id: '2', name: 'Process Data', type: 'n8n-nodes-base.set', position: [300, 100], parameters: {} },
|
||||
],
|
||||
connections: {
|
||||
'HTTP Request': { main: [[{ node: 'Process Data', type: 'main', index: 0 }]] },
|
||||
},
|
||||
};
|
||||
|
||||
const result = await validator.validateWorkflow(workflow as any);
|
||||
expect(result.errors.some(e =>
|
||||
e.nodeName === 'HTTP Request' &&
|
||||
e.message.includes("has onError: 'continueErrorOutput' but no error output connections"),
|
||||
)).toBe(true);
|
||||
});
|
||||
|
||||
it('should warn about error connections without onError', async () => {
|
||||
const workflow = {
|
||||
nodes: [
|
||||
{ id: '1', name: 'HTTP Request', type: 'n8n-nodes-base.httpRequest', typeVersion: 4, position: [100, 100], parameters: {} },
|
||||
{ id: '2', name: 'Process Data', type: 'n8n-nodes-base.set', position: [300, 100], parameters: {} },
|
||||
{ id: '3', name: 'Error Handler', type: 'n8n-nodes-base.set', position: [300, 300], parameters: {} },
|
||||
],
|
||||
connections: {
|
||||
'HTTP Request': {
|
||||
main: [
|
||||
[{ node: 'Process Data', type: 'main', index: 0 }],
|
||||
[{ node: 'Error Handler', type: 'main', index: 0 }],
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = await validator.validateWorkflow(workflow as any);
|
||||
expect(result.warnings.some(w =>
|
||||
w.nodeName === 'HTTP Request' &&
|
||||
w.message.includes('error output connections in main[1] but missing onError'),
|
||||
)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error Handler Detection', () => {
|
||||
it('should detect error handler nodes by name', async () => {
|
||||
const workflow = {
|
||||
nodes: [
|
||||
{ id: '1', name: 'API Call', type: 'n8n-nodes-base.httpRequest', position: [100, 100], parameters: {} },
|
||||
{ id: '2', name: 'Process Success', type: 'n8n-nodes-base.set', position: [300, 100], parameters: {} },
|
||||
{ id: '3', name: 'Handle Error', type: 'n8n-nodes-base.set', position: [300, 300], parameters: {} },
|
||||
],
|
||||
connections: {
|
||||
'API Call': { main: [[{ node: 'Process Success', type: 'main', index: 0 }, { node: 'Handle Error', type: 'main', index: 0 }]] },
|
||||
},
|
||||
};
|
||||
|
||||
const result = await validator.validateWorkflow(workflow as any);
|
||||
expect(result.errors.some(e => e.message.includes('Handle Error') && e.message.includes('appear to be error handlers'))).toBe(true);
|
||||
});
|
||||
|
||||
it('should detect error handler nodes by type', async () => {
|
||||
const workflow = {
|
||||
nodes: [
|
||||
{ id: '1', name: 'Webhook', type: 'n8n-nodes-base.webhook', position: [100, 100], parameters: {} },
|
||||
{ id: '2', name: 'Process', type: 'n8n-nodes-base.set', position: [300, 100], parameters: {} },
|
||||
{ id: '3', name: 'Respond', type: 'n8n-nodes-base.respondToWebhook', position: [300, 300], parameters: {} },
|
||||
],
|
||||
connections: {
|
||||
'Webhook': { main: [[{ node: 'Process', type: 'main', index: 0 }, { node: 'Respond', type: 'main', index: 0 }]] },
|
||||
},
|
||||
};
|
||||
|
||||
const result = await validator.validateWorkflow(workflow as any);
|
||||
expect(result.errors.some(e => e.message.includes('Respond') && e.message.includes('appear to be error handlers'))).toBe(true);
|
||||
});
|
||||
|
||||
it('should not flag non-error nodes in main[0]', async () => {
|
||||
const workflow = {
|
||||
nodes: [
|
||||
{ id: '1', name: 'Start', type: 'n8n-nodes-base.manualTrigger', position: [100, 100], parameters: {} },
|
||||
{ id: '2', name: 'First Process', type: 'n8n-nodes-base.set', position: [300, 100], parameters: {} },
|
||||
{ id: '3', name: 'Second Process', type: 'n8n-nodes-base.set', position: [300, 200], parameters: {} },
|
||||
],
|
||||
connections: {
|
||||
'Start': { main: [[{ node: 'First Process', type: 'main', index: 0 }, { node: 'Second Process', type: 'main', index: 0 }]] },
|
||||
},
|
||||
};
|
||||
|
||||
const result = await validator.validateWorkflow(workflow as any);
|
||||
expect(result.errors.some(e => e.message.includes('Incorrect error output configuration'))).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Complex Error Patterns', () => {
|
||||
it('should handle multiple error handlers correctly in main[1]', async () => {
|
||||
const workflow = {
|
||||
nodes: [
|
||||
{ id: '1', name: 'HTTP Request', type: 'n8n-nodes-base.httpRequest', position: [100, 100], parameters: {}, onError: 'continueErrorOutput' },
|
||||
{ id: '2', name: 'Process', type: 'n8n-nodes-base.set', position: [300, 100], parameters: {} },
|
||||
{ id: '3', name: 'Log Error', type: 'n8n-nodes-base.set', position: [300, 200], parameters: {} },
|
||||
{ id: '4', name: 'Send Error Email', type: 'n8n-nodes-base.emailSend', position: [300, 300], parameters: {} },
|
||||
],
|
||||
connections: {
|
||||
'HTTP Request': {
|
||||
main: [
|
||||
[{ node: 'Process', type: 'main', index: 0 }],
|
||||
[{ node: 'Log Error', type: 'main', index: 0 }, { node: 'Send Error Email', type: 'main', index: 0 }],
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = await validator.validateWorkflow(workflow as any);
|
||||
expect(result.errors.some(e => e.message.includes('Incorrect error output configuration'))).toBe(false);
|
||||
});
|
||||
|
||||
it('should detect mixed success and error handlers in main[0]', async () => {
|
||||
const workflow = {
|
||||
nodes: [
|
||||
{ id: '1', name: 'API Request', type: 'n8n-nodes-base.httpRequest', position: [100, 100], parameters: {} },
|
||||
{ id: '2', name: 'Transform Data', type: 'n8n-nodes-base.set', position: [300, 100], parameters: {} },
|
||||
{ id: '3', name: 'Store Data', type: 'n8n-nodes-base.set', position: [500, 100], parameters: {} },
|
||||
{ id: '4', name: 'Error Notification', type: 'n8n-nodes-base.emailSend', position: [300, 300], parameters: {} },
|
||||
],
|
||||
connections: {
|
||||
'API Request': {
|
||||
main: [[
|
||||
{ node: 'Transform Data', type: 'main', index: 0 },
|
||||
{ node: 'Store Data', type: 'main', index: 0 },
|
||||
{ node: 'Error Notification', type: 'main', index: 0 },
|
||||
]],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = await validator.validateWorkflow(workflow as any);
|
||||
expect(result.errors.some(e =>
|
||||
e.message.includes('Error Notification') && e.message.includes('appear to be error handlers but are in main[0]'),
|
||||
)).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle nested error handling (error handlers with their own errors)', async () => {
|
||||
const workflow = {
|
||||
nodes: [
|
||||
{ id: '1', name: 'Primary API', type: 'n8n-nodes-base.httpRequest', position: [100, 100], parameters: {}, onError: 'continueErrorOutput' },
|
||||
{ id: '2', name: 'Success Handler', type: 'n8n-nodes-base.set', position: [300, 100], parameters: {} },
|
||||
{ id: '3', name: 'Error Logger', type: 'n8n-nodes-base.httpRequest', position: [300, 200], parameters: {}, onError: 'continueErrorOutput' },
|
||||
{ id: '4', name: 'Fallback Error', type: 'n8n-nodes-base.set', position: [500, 250], parameters: {} },
|
||||
],
|
||||
connections: {
|
||||
'Primary API': { main: [[{ node: 'Success Handler', type: 'main', index: 0 }], [{ node: 'Error Logger', type: 'main', index: 0 }]] },
|
||||
'Error Logger': { main: [[], [{ node: 'Fallback Error', type: 'main', index: 0 }]] },
|
||||
},
|
||||
};
|
||||
|
||||
const result = await validator.validateWorkflow(workflow as any);
|
||||
expect(result.errors.some(e => e.message.includes('Incorrect error output configuration'))).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle workflows with only error outputs (no success path)', async () => {
|
||||
const workflow = {
|
||||
nodes: [
|
||||
{ id: '1', name: 'Risky Operation', type: 'n8n-nodes-base.httpRequest', position: [100, 100], parameters: {}, onError: 'continueErrorOutput' },
|
||||
{ id: '2', name: 'Error Handler Only', type: 'n8n-nodes-base.set', position: [300, 200], parameters: {} },
|
||||
],
|
||||
connections: {
|
||||
'Risky Operation': { main: [[], [{ node: 'Error Handler Only', type: 'main', index: 0 }]] },
|
||||
},
|
||||
};
|
||||
|
||||
const result = await validator.validateWorkflow(workflow as any);
|
||||
expect(result.errors.some(e => e.message.includes('Incorrect error output configuration'))).toBe(false);
|
||||
expect(result.errors.some(e => e.message.includes("has onError: 'continueErrorOutput' but no error output connections"))).toBe(false);
|
||||
});
|
||||
|
||||
it('should not flag legitimate parallel processing nodes', async () => {
|
||||
const workflow = {
|
||||
nodes: [
|
||||
{ id: '1', name: 'Data Source', type: 'n8n-nodes-base.webhook', position: [100, 100], parameters: {} },
|
||||
{ id: '2', name: 'Process A', type: 'n8n-nodes-base.set', position: [300, 50], parameters: {} },
|
||||
{ id: '3', name: 'Process B', type: 'n8n-nodes-base.set', position: [300, 150], parameters: {} },
|
||||
{ id: '4', name: 'Transform Data', type: 'n8n-nodes-base.set', position: [300, 250], parameters: {} },
|
||||
],
|
||||
connections: {
|
||||
'Data Source': { main: [[{ node: 'Process A', type: 'main', index: 0 }, { node: 'Process B', type: 'main', index: 0 }, { node: 'Transform Data', type: 'main', index: 0 }]] },
|
||||
},
|
||||
};
|
||||
|
||||
const result = await validator.validateWorkflow(workflow as any);
|
||||
expect(result.errors.some(e => e.message.includes('Incorrect error output configuration'))).toBe(false);
|
||||
});
|
||||
|
||||
it('should detect all variations of error-related node names', async () => {
|
||||
const workflow = {
|
||||
nodes: [
|
||||
{ id: '1', name: 'Source', type: 'n8n-nodes-base.httpRequest', position: [100, 100], parameters: {} },
|
||||
{ id: '2', name: 'Handle Failure', type: 'n8n-nodes-base.set', position: [300, 100], parameters: {} },
|
||||
{ id: '3', name: 'Catch Exception', type: 'n8n-nodes-base.set', position: [300, 200], parameters: {} },
|
||||
{ id: '4', name: 'Success Path', type: 'n8n-nodes-base.set', position: [500, 100], parameters: {} },
|
||||
],
|
||||
connections: {
|
||||
'Source': { main: [[{ node: 'Handle Failure', type: 'main', index: 0 }, { node: 'Catch Exception', type: 'main', index: 0 }, { node: 'Success Path', type: 'main', index: 0 }]] },
|
||||
},
|
||||
};
|
||||
|
||||
const result = await validator.validateWorkflow(workflow as any);
|
||||
expect(result.errors.some(e =>
|
||||
e.message.includes('Handle Failure') && e.message.includes('Catch Exception') && e.message.includes('appear to be error handlers but are in main[0]'),
|
||||
)).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user