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:
Romuald Członkowski
2026-03-27 14:22:22 +01:00
committed by GitHub
parent 07bd1d4cc2
commit de2abaf89d
75 changed files with 3718 additions and 21917 deletions

View File

@@ -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);
});
});
});