Fix export excel writer crashing the process on a stream write error

exportExcel's CSV/XLS write streams had no 'error' listener, so a write
failure (disk full, export directory becomes inaccessible mid-run — both
realistic for a large CNPJ export) surfaced as an uncaught exception that
crashed the whole export-job.mjs child process, bypassing every
await/try-catch in the caller. The exports row was then never marked
'error' and stayed stuck, and a half-written file could sit at the final
path (no temp+rename, unlike the same pattern already used in
rfb-extractor.mjs/rfb-downloader.mjs).

Streams now track their error via a listener instead of throwing, writes
and stream-end are rejected through the normal promise chain so
export-job.mjs's existing catch/finally handles it, and both files are
written to .tmp paths and only renamed to their final names once both
streams close successfully.

Found via a full-project audit.
This commit is contained in:
Gustavo
2026-07-31 22:13:49 -03:00
parent 0bae79e1aa
commit 3d552b5bc0
2 changed files with 91 additions and 25 deletions
+21
View File
@@ -45,6 +45,27 @@ test('exportExcel usa nome interno e neutraliza formulas no CSV/XLS', async () =
}
});
// Antes, um erro no stream de escrita (disco cheio, pasta inacessivel) virava
// excecao nao capturada e derrubava o processo do job inteiro, sem nunca
// chegar ao catch de export-job.mjs; e o arquivo final podia ficar pela
// metade no lugar do arquivo definitivo. Forca EISDIR criando um diretorio
// no caminho .tmp esperado do CSV, no lugar de um disco cheio de verdade.
test('exportExcel rejeita (nao derruba o processo) quando o stream falha, e nao deixa arquivo pela metade', async () => {
const db = openDb(':memory:');
seedLead(db);
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'pessoal-export-falha-'));
try {
await fs.mkdir(path.join(dir, 'leads-excel.csv.tmp'));
await assert.rejects(() => exportExcel({ db, filtro: {}, dir }));
assert.equal(await fs.stat(path.join(dir, 'leads-excel.csv')).then(() => true, () => false), false);
assert.equal(await fs.stat(path.join(dir, 'leads-excel.xls')).then(() => true, () => false), false);
assert.equal(await fs.stat(path.join(dir, 'leads-excel.xls.tmp')).then(() => true, () => false), false);
} finally {
await fs.rm(dir, { recursive: true, force: true });
db.close();
}
});
test('rota de exportacao rejeita tipos corporativos removidos', async () => {
const db = openDb(':memory:');
seedLead(db);