import test from 'node:test'; import assert from 'node:assert/strict'; import { DossierExportGate, dossierExportFailure, prepareDossierAttachments, } from '../../resources/js/lib/dossier-export-state.ts'; import { createProjectDossierReport, generateProjectDossierDocx, createProjectDossierPrintHtml, generateProjectDossierPackage, } from '../../resources/js/lib/project-dossier-report.ts'; void test('concurrent export is ignored; failed generation unlocks one retry without mutating inputs', async () => { const gate = new DossierExportGate(); const draft = { project_title: 'SINTÉTICO QA', project_summary: 'Pressupostos declarados; não é resultado fiscal.', }; const saved = JSON.stringify(draft); let release; let starts = 0; const first = gate.run(async () => { starts++; await new Promise((resolve) => { release = resolve; }); throw new Error('Library failure'); }); assert.equal( await gate.run(async () => { starts++; }), false, ); release(); await assert.rejects(first, /Library failure/); assert.equal( await gate.run(async () => { starts++; }), true, ); assert.equal(starts, 2); assert.equal(JSON.stringify(draft), saved); }); void test('all operations show Portuguese recovery text while retaining technical details in diagnostics', () => { for (const operation of ['word', 'package', 'pdf', 'csv', 'attachments']) { const error = new Error('English internals / private stack'); const logs = []; const text = dossierExportFailure(operation, error, (...args) => logs.push(args), ); assert.match(text, /Não foi possível/); assert.match(text, /foram mantidos.*Tente novamente/); assert.doesNotMatch(text, /English internals|private stack/); assert.equal(logs.length, 1); assert.equal(logs[0][1].cause, error); assert.ok(text.includes(logs[0][1].reference)); } }); void test('attachment read failure and size refusal preserve existing selection; retry deduplicates bytes and keeps captions', async () => { const file = new File(['SINTÉTICO QA'], 'qa.txt', { type: 'text/plain' }); const current = [{ file, include: false, caption: 'Legenda conservada' }]; const broken = new File(['another'], 'broken.txt'); broken.arrayBuffer = async () => { throw new Error('OS read failed'); }; await assert.rejects( prepareDossierAttachments(current, [broken]), /OS read failed/, ); assert.equal(current.length, 1); const tooLarge = new File([new Uint8Array(16 * 1024 * 1024)], 'large.txt'); const refused = await prepareDossierAttachments(current, [tooLarge]); assert.equal(refused.attachments, current); assert.match(refused.problem, /15 MB/); const result = await prepareDossierAttachments(current, [ new File(['SINTÉTICO QA'], 'qa.txt'), new File(['distinct'], 'qa.txt'), ]); assert.equal(result.duplicateCount, 1); assert.equal(result.attachments.length, 2); assert.equal(result.attachments[0], current[0]); assert.equal(result.attachments[0].caption, 'Legenda conservada'); assert.equal(result.attachments[0].include, false); }); void test('actual Word PDF and ZIP generators recover after a transient attachment read failure; archive has one original', async () => { const model = createProjectDossierReport( { project_title: 'QA recuperação' }, { selectedSections: new Set(['summary', 'annexes']) }, ); const file = new File(['Texto QA'], 'qa.txt', { type: 'text/plain' }); const original = file.text.bind(file); let fail = false; file.text = async () => { if (fail) { fail = false; throw new Error('Transient attachment read failure'); } return original(); }; const attachments = [{ file, include: true, caption: '' }]; for (const generate of [ () => generateProjectDossierDocx(model, attachments, true), () => createProjectDossierPrintHtml(model, attachments, true), ]) { fail = true; await assert.rejects(generate, /Transient attachment/); assert.ok(await generate()); assert.equal(attachments.length, 1); } const word = await generateProjectDossierDocx(model, [], false); let broken = true; const { DossierExportGate: Gate } = await import('../../resources/js/lib/dossier-export-state.ts'); const gate = new Gate(); await assert.rejects( gate.run(async () => { if (broken) throw new Error('ZIP write fault'); }), /ZIP write fault/, ); broken = false; let zip; await gate.run(async () => { zip = await generateProjectDossierPackage(word, attachments, true); }); const { default: JSZip } = await import('jszip'); const archive = await JSZip.loadAsync(await zip.arrayBuffer()); assert.deepEqual( Object.keys(archive.files).filter( (name) => name.startsWith('Anexos/') && !archive.files[name].dir, ), ['Anexos/qa.txt'], ); });