import { readFileSync } from 'node:fs'; import { join } from 'node:path'; import { markdownToDoc } from '@dorfteich/shared'; import { beforeAll, describe, expect, it, TestContext } from 'vitest'; import { AppConfig } from '../config/app-config.service'; import { convertImportedDocument } from './import.service'; import { CONVERSION_TIMEOUT_MS, PandocServerConverter } from './pandoc.converter'; /** * Import fidelity regression (issue #63, ADR 0009): runs the real two-pass * pandoc conversion over the committed `.docx`/`.odt` corpus and asserts each * produces its expected Markdown. Needs a reachable pandoc sidecar (the pinned * `pandoc/core:3.6`, so output matches the snapshots) — each test skips itself * when none is configured, and CI starts one and points `PANDOC_URL` at it. */ const PANDOC_URL = process.env.PANDOC_URL ?? 'http://localhost:3030'; // The test runner's cwd is `apps/api`; the corpus lives at the repo root. const FIXTURES = join(process.cwd(), '../../fixtures/import'); const converter = new PandocServerConverter({ env: { PANDOC_URL }, } as unknown as AppConfig); let reachable = false; /** Normalise embedded image `data:` URIs to the stable token the snapshots use * (the base64 payload is volatile and not what we are pinning). */ function normalize(markdown: string): string { return markdown.replace( /data:image\/[a-zA-Z0-9.+-]+;base64,[A-Za-z0-9+/=]+/g, 'data:embedded-image', ); } const CORPUS = ['article.docx', 'article.odt', 'formatting.docx', 'formatting.odt']; describe('import fixture corpus (real pandoc, issue #63)', () => { beforeAll(async () => { reachable = await converter.reachable().catch(() => false); }); for (const fixture of CORPUS) { it(`converts ${fixture} to its expected Markdown`, async (ctx: TestContext) => { if (!reachable) ctx.skip(); const format = fixture.endsWith('.odt') ? 'odt' : 'docx'; const document = readFileSync(join(FIXTURES, fixture)); const expected = readFileSync(join(FIXTURES, `${fixture}.expected.md`), 'utf8'); const markdown = await convertImportedDocument(converter, format, document); expect(normalize(markdown)).toBe(expected); // The Markdown must also parse into a valid editor document (no schema // surprises from real-world structure). expect(() => markdownToDoc(markdown)).not.toThrow(); }); } it('imports a 50-page document within the conversion timeout', async (ctx: TestContext) => { if (!reachable) ctx.skip(); // Build a ~50-page document by repeating a page of structured content, then // convert it to docx once and time the import conversion of that document. const onePage = '# Section\n\n' + 'A paragraph of survey notes about the pond. '.repeat(20) + '\n\n'; const large = Array.from({ length: 50 }, () => onePage).join('\n---\n\n'); const docx = await converter.convert({ from: 'gfm', to: 'docx', input: Buffer.from(large) }); const started = Date.now(); const markdown = await convertImportedDocument(converter, 'docx', docx.output); const elapsed = Date.now() - started; expect(markdown.length).toBeGreaterThan(1000); // Comfortably inside the documented 60 s per-conversion ceiling (ADR 0009). expect(elapsed).toBeLessThan(CONVERSION_TIMEOUT_MS); }); });