// innboks-convert.test.mjs // Step 11 (SC format): konverterings-adaptere txt/md/docx/eml/pdf -> markdown. // Verifiserer: // - .txt/.md -> markdown alltid (zero-dep-sti, node:-builtins) // - eksterne lenker NOEYTRALISERES ved konvertering (inline/ref-def/autolink/ // HTML-anker/bilde): lenketeksten bevares, scheme-maalet droppes -- slik at // strict-gaten (B3/F2) aldri felles av legitim konvertert output // - .docx/.eml -> heading-STRUKTUR bevart ('# '-linjer, atx -- IKKE byte-snapshot); // .pdf -> tekst ekstrahert (flat, dokumentert v1-caveat M5) // - convert-twice-identical: samme binaerfil konvertert 2x gir byte-identisk // markdown (konverterings-determinismen idempotensen hviler paa) // - ukjent extension (.xlsx) -> null + norsk notice, INGEN throw // Binaer-adapterne kjoerer conditional { skip: !engines } (plugin kan vaere // installert uten node_modules -- npm-deps er dokumentert prerequisite). // Moenster: node:test conditional skip; fixtures under tests/fixtures/inbox-sample/. import { test } from 'node:test'; import assert from 'node:assert/strict'; import { mkdtempSync, writeFileSync, rmSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; import { convert } from '../lib/convert/index.mjs'; const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..'); const SAMPLE = join(ROOT, 'tests', 'fixtures', 'inbox-sample'); // Er binaer-konverterings-motorene installert? (npm-deps = dokumentert prerequisite.) async function enginesAvailable() { try { await import('mammoth'); await import('turndown'); await import('postal-mime'); await import('unpdf'); return true; } catch { return false; } } const engines = await enginesAvailable(); // Temp-katalog for haandlagde input-filer. Ryddes alltid. function withTmp(fn) { const dir = mkdtempSync(join(tmpdir(), 'innboks-convert-')); try { return fn(dir); } finally { rmSync(dir, { recursive: true, force: true }); } } test('.txt konverteres alltid (zero-dep): innhold bevart som markdown', async () => { const md = await convert(join(SAMPLE, 'dok-a.txt')); assert.equal(typeof md, 'string'); assert.ok(md.length > 0); }); test('.md passthrough: markdown-struktur bevart', async () => { await withTmp(async (dir) => { const p = join(dir, 'notat.md'); writeFileSync(p, '# Tittel\n\nAvsnitt.\n\n## Under\n\nMer.\n'); const md = await convert(p); assert.match(md, /^# Tittel$/m); assert.match(md, /^## Under$/m); }); }); test('eksterne lenker noeytraliseres: inline/ref-def/autolink/HTML/bilde', async () => { await withTmp(async (dir) => { const p = join(dir, 'fiendtlig.txt'); writeFileSync( p, '# Notat\n\n' + 'Se [rapporten](https://evil.example/exfil) og [vedlegget][r].\n\n' + '[r]: https://evil.example/ref\n\n' + '\n\n' + 'passord\n\n' + '![skjermbilde](https://evil.example/pixel.png)\n\n' + 'Intern relasjon: [Notat](/dokumenter/notat.md) beholdes.\n', ); const md = await convert(p); // Lenketekst bevart: assert.match(md, /rapporten/); assert.match(md, /vedlegget/); assert.match(md, /passord/); assert.match(md, /skjermbilde/); // Ingen lenke-FORM med eksterne maal igjen (inline, ref-def, autolink, HTML): assert.doesNotMatch(md, /\]\(https?:/); assert.doesNotMatch(md, /^\s{0,3}\[[^\]]+\]:/m); assert.doesNotMatch(md, / markdown med atx-heading-struktur (ikke byte-snapshot)', { skip: !engines }, async () => { const md = await convert(join(SAMPLE, 'dok.docx')); assert.equal(typeof md, 'string'); // M5/F4: {headingStyle:'atx'} er pinnet -> '#'-headings, aldri setext. assert.match(md, /^# /m); assert.match(md, /^## /m); assert.doesNotMatch(md, /^=+$/m); }); test('.eml -> markdown: subject som heading + body-tekst (kun body, vedlegg = non-goal)', { skip: !engines }, async () => { const md = await convert(join(SAMPLE, 'dok.eml')); assert.match(md, /^# Statusoppdatering T2/m); assert.match(md, /Fremdrift paa maaltallene/); }); test('.pdf -> tekst ekstrahert (flat markdown, isEvalSupported:false-sti)', { skip: !engines }, async () => { const md = await convert(join(SAMPLE, 'dok.pdf')); assert.equal(typeof md, 'string'); assert.match(md, /Tildelingsbrev 2026/); }); test('convert-twice-identical: docx og pdf gir byte-identisk markdown', { skip: !engines }, async () => { for (const f of ['dok.docx', 'dok.pdf']) { const first = await convert(join(SAMPLE, f)); const second = await convert(join(SAMPLE, f)); assert.equal(first, second, `ikke-deterministisk konvertering: ${f}`); } }); test('ukjent extension (.xlsx) -> null + norsk notice, ingen throw', async () => { await withTmp(async (dir) => { const p = join(dir, 'regneark.xlsx'); writeFileSync(p, 'ikke egentlig xlsx'); const notices = []; const md = await convert(p, { onNotice: (msg) => notices.push(msg) }); assert.equal(md, null); assert.equal(notices.length, 1); assert.match(notices[0], /hopper over/); assert.match(notices[0], /regneark\.xlsx/); }); });