import assert from 'node:assert/strict'; import { createHash } from 'node:crypto'; import { existsSync, readFileSync } from 'node:fs'; import { createRequire } from 'node:module'; import { resolve } from 'node:path'; import test from 'node:test'; import { buildInspectionActPdf } from '../../src/inspection-reports/inspection-act-pdf-builder'; import { buildInspectionReportWord } from '../../src/inspection-reports/inspection-report-word-builder'; import sharp from 'sharp'; import { renderableInspectionImage } from '../../src/inspection-reports/inspection-document-images'; const digest = (data: Buffer) => createHash('sha256').update(data).digest('hex'); const photo = readFileSync(resolve(process.cwd(), 'assets/logo-mendoza.png')); const locked = { act: { code: 'ACT-TEST-01', occurredAt: '2026-09-14T18:30:00Z', summary: 'Se inspeccionó un tanque.', inspection: { code: 'INSP-TEST-01' } }, responsible: { fullName: 'Responsable de prueba', documentNumber: '12345678' }, inventories: [{ id: 'asset-1', code: 'CAM-TEST-01', name: 'Tanque de prueba' }], findings: [{ id: 'finding-1', assetId: 'asset-1', code: 'ACT-TEST-H01', title: 'Pérdida', description: 'Se constató una pérdida visible.', severity: 3 }], }; const sealed = { lockedSnapshot: locked, finalSha256: 'a'.repeat(64), lockedSha256: 'b'.repeat(64), signatures: [], seal: { serverSealedAt: '2026-09-15T18:30:00Z' } }; const evidence = { id: 'photo-1', findingId: 'finding-1', sha256: digest(photo), buffer: photo }; test('F6.9 consolidated Acta is a hashed PDF 1.4 with actual evidence embedded', async () => { const without = await buildInspectionActPdf(sealed); const withEvidence = await buildInspectionActPdf(sealed, [evidence]); assert.ok(withEvidence.buffer.subarray(0, 8).equals(Buffer.from('%PDF-1.4'))); assert.equal(withEvidence.sha256, digest(withEvidence.buffer)); assert.ok(withEvidence.buffer.length > without.buffer.length + 3_000); assert.notEqual(withEvidence.sha256, without.sha256); }); test('F6.9 technical Informe embeds evidence and its institutional source hash', () => { const input = { code: 'INF-TEST-01', title: 'Informe de prueba', generatedAt: new Date('2026-09-15'), frozenSha256: 'c'.repeat(64), frozenSnapshot: { sealedAct: sealed, source: { actCode: locked.act.code } } }; const plain = buildInspectionReportWord(input); const withEvidence = buildInspectionReportWord({ ...input, photos: [evidence] }); assert.equal(withEvidence.sha256, digest(withEvidence.buffer)); assert.ok(withEvidence.buffer.subarray(0, 4).equals(Buffer.from('PK\x03\x04'))); assert.ok(withEvidence.buffer.includes(Buffer.from('word/media/photo-1.png'))); assert.ok(withEvidence.buffer.includes(photo)); assert.ok(withEvidence.buffer.includes(Buffer.from('ACT-TEST-H01'))); assert.ok(withEvidence.buffer.includes(Buffer.from('SHA-256 de la fuente del Informe'))); assert.ok(withEvidence.buffer.length > plain.buffer.length + photo.length); assert.ok(!withEvidence.buffer.includes(Buffer.from('[Completar'))); }); test('F6.9 keeps prior sealed PDF and prior company responses available for audit', () => { const pdf = readFileSync(resolve(process.cwd(), 'src/inspection-reports/inspection-act-pdf.service.ts'), 'utf8'); const controller = readFileSync(resolve(process.cwd(), 'src/act-administration/act-administration.controller.ts'), 'utf8'); assert.match(pdf, /inspection_act_consolidated_pdf_revisions/); const word = readFileSync(resolve(process.cwd(), 'src/inspection-reports/inspection-report-word.service.ts'), 'utf8'); assert.match(word, /inspection_report_consolidated_word_revisions/); assert.match(pdf, /ON CONFLICT \(act_id,template_version\) DO NOTHING/); assert.match(controller, /responseContent\(responseId\)/); assert.doesNotMatch(controller, /@Post\('responses'\)/); }); test('F6.9 renders WebP field photographs in both documents without changing the source hash', async () => { const original = await sharp(photo).webp().toBuffer(); const originalSha = digest(original); const display = await renderableInspectionImage(original); assert.ok(display.subarray(0, 8).equals(Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]))); assert.equal(digest(original), originalSha); const pdf = await buildInspectionActPdf(sealed, [{ ...evidence, sha256: originalSha, buffer: display }]); assert.ok(pdf.buffer.subarray(0, 8).equals(Buffer.from('%PDF-1.4'))); const word = buildInspectionReportWord({ code: 'INF-WEBP', title: 'Informe', generatedAt: new Date(), frozenSha256: 'c'.repeat(64), frozenSnapshot: { sealedAct: sealed }, photos: [{ ...evidence, sha256: originalSha, buffer: display }] }); assert.ok(word.buffer.includes(display)); assert.ok(word.buffer.includes(Buffer.from(originalSha))); }); test('F6.9 built CommonJS runtime renders signed PDF and WebP evidence', async () => { const builtPdf = resolve(process.cwd(), 'dist/inspection-reports/inspection-act-pdf-builder.js'); const builtImage = resolve(process.cwd(), 'dist/inspection-reports/inspection-document-images.js'); if (!existsSync(builtPdf) || !existsSync(builtImage)) return; const requireCjs = createRequire(resolve(process.cwd(), 'package.json')); const runtimePdf = requireCjs(builtPdf) as { buildInspectionActPdf: typeof buildInspectionActPdf }; const runtimeImage = requireCjs(builtImage) as { renderableInspectionImage: typeof renderableInspectionImage }; const webp = await sharp(photo).webp().toBuffer(); const rendered = await runtimeImage.renderableInspectionImage(webp); const pdf = await runtimePdf.buildInspectionActPdf(sealed, [{ ...evidence, buffer: rendered, sha256: digest(webp) }]); assert.ok(pdf.buffer.subarray(0, 8).equals(Buffer.from('%PDF-1.4'))); }); test('F6.9 revision migration archives both first document versions before serving the corrected template', () => { const migration = readFileSync(resolve(process.cwd(), 'src/database/migrations/1790135400000-f6-9-consolidated-document-revisions.ts'), 'utf8'); assert.match(migration, /SELECT act_id,1,stored_name,original_name,size_bytes,sha256,generated_at/); assert.match(migration, /SELECT report_id,1,stored_name,original_name,size_bytes,sha256,generated_at/); assert.match(migration, /PRIMARY KEY \(act_id,template_version\)/); assert.match(migration, /PRIMARY KEY \(report_id,template_version\)/); }); test('Acta ignores standalone field photos while the technical Informe remains independent', async () => { const other = await sharp(photo).resize(75).png().toBuffer(); const unlinked = { id: 'photo-other-asset', assetId: 'asset-2', title: 'Otra instalación', sha256: digest(other), buffer: other }; const withUnlinked = await buildInspectionActPdf(sealed, [evidence, unlinked]); const linkedOnly = await buildInspectionActPdf(sealed, [evidence]); assert.ok(Math.abs(withUnlinked.buffer.length - linkedOnly.buffer.length) < 500); const word = buildInspectionReportWord({ code: 'INF-OTHER', title: 'Informe', generatedAt: new Date(), frozenSha256: 'c'.repeat(64), frozenSnapshot: { sealedAct: sealed }, photos: [evidence, unlinked] }); assert.ok(word.buffer.includes(Buffer.from('OTRAS INSTALACIONES INSPECCIONADAS'))); assert.ok(word.buffer.includes(Buffer.from('word/media/photo-2.png'))); assert.ok(word.buffer.includes(other)); });