Skip to content

Commit 84a69cd

Browse files
authored
Fix: one failing exporter should not abort the others (#754)
## Problem When one exporter's `exportTransactions` throws, the entire export pipeline aborts: 1. Per-exporter catch block re-throws the error 2. `Promise.all(exportPromises)` rejects fail-fast 3. Other still-running exporters are orphaned 4. `log.summary()` never runs 5. `EXPORT_PROCESS_END` event never fires 6. `yarn scrape` exits non-zero A single exporter hitting a runtime error prevents all other exporters from completing, even though their work is independent. ## Fix 1. **Remove the `throw e`** in the per-exporter catch block. The outcome is already recorded via `successCount`/`failedCount` counters and the `EXPORTER_ERROR` event — re-throwing adds nothing but the fail-fast behavior. 2. **Switch `Promise.all` → `Promise.allSettled`** so an error outside the try/catch still cannot abort other exporters or skip the summary. Added unit tests covering both invariants: - one exporter throwing does not prevent the others from running - the function resolves (does not reject) with a partial result ## Verification - `yarn test:main` → 45 passed - `yarn typecheck:main` → clean - `yarn lint` → clean
2 parents 33dcc8b + cf91fe6 commit 84a69cd

2 files changed

Lines changed: 85 additions & 2 deletions

File tree

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { type EventPublisher } from '@/backend/eventEmitters/EventEmitter';
2+
import { beforeEach, describe, expect, test, vi } from 'vitest';
3+
4+
// Mock the outputVendors module so we can inject controlled exporters.
5+
vi.mock('@/backend/export/outputVendors', () => ({
6+
default: [],
7+
}));
8+
9+
import outputVendors from '@/backend/export/outputVendors';
10+
import { createTransactionsInExternalVendors } from './exportTransactions';
11+
12+
const noopEventPublisher: EventPublisher = {
13+
emit: vi.fn().mockResolvedValue(undefined),
14+
};
15+
16+
function makeExporter(name: string, behavior: 'success' | 'fail', exported = 1) {
17+
return {
18+
name,
19+
init: vi.fn().mockResolvedValue(undefined),
20+
exportTransactions: vi.fn().mockImplementation(async () => {
21+
if (behavior === 'fail') {
22+
throw new Error(`${name} blew up`);
23+
}
24+
return { exportedTransactionsNum: exported };
25+
}),
26+
};
27+
}
28+
29+
function setExporters(list: unknown[]) {
30+
// Mutate the mocked array in place so the module-level import in
31+
// exportTransactions.ts sees the new values.
32+
(outputVendors as unknown as unknown[]).length = 0;
33+
(outputVendors as unknown as unknown[]).push(...list);
34+
}
35+
36+
describe('createTransactionsInExternalVendors', () => {
37+
beforeEach(() => {
38+
vi.clearAllMocks();
39+
});
40+
41+
test('one exporter failing does not prevent other exporters from running', async () => {
42+
const failing = makeExporter('csv', 'fail');
43+
const succeeding = makeExporter('ynab', 'success', 5);
44+
setExporters([failing, succeeding]);
45+
46+
const config = {
47+
csv: { active: true },
48+
ynab: { active: true },
49+
} as never;
50+
51+
const result = await createTransactionsInExternalVendors(
52+
config,
53+
{ companyA: [] as never[] },
54+
new Date('2025-01-01'),
55+
noopEventPublisher,
56+
);
57+
58+
// The successful exporter should have run to completion even though csv threw.
59+
expect(succeeding.exportTransactions).toHaveBeenCalledTimes(1);
60+
expect(failing.exportTransactions).toHaveBeenCalledTimes(1);
61+
expect(result).toHaveProperty('ynab');
62+
expect(result).not.toHaveProperty('csv');
63+
});
64+
65+
test('does not reject when an exporter throws (promise resolves with partial result)', async () => {
66+
const failing = makeExporter('csv', 'fail');
67+
const succeeding = makeExporter('ynab', 'success', 3);
68+
setExporters([failing, succeeding]);
69+
70+
const config = {
71+
csv: { active: true },
72+
ynab: { active: true },
73+
} as never;
74+
75+
await expect(
76+
createTransactionsInExternalVendors(
77+
config,
78+
{ companyA: [] as never[] },
79+
new Date('2025-01-01'),
80+
noopEventPublisher,
81+
),
82+
).resolves.toBeDefined();
83+
});
84+
});

packages/main/src/backend/export/exportTransactions.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,11 +106,10 @@ export async function createTransactionsInExternalVendors(
106106
...baseEvent,
107107
}),
108108
);
109-
throw e;
110109
}
111110
});
112111

113-
await Promise.all(exportPromises);
112+
await Promise.allSettled(exportPromises);
114113

115114
const result = failedCount === 0 ? 'success' : successCount === 0 ? 'failed' : 'partial';
116115
log.summary(result, {

0 commit comments

Comments
 (0)