Some checks failed
CD / Deploy to Test (push) Blocked by required conditions
CD / Smoke tests against Test (push) Blocked by required conditions
CD / Promote to Int (push) Blocked by required conditions
CD / Build and push images (push) Failing after 27m51s
CI / Lint, typecheck, test (push) Successful in 3m11s
CI / Auth e2e pack (push) Successful in 4m0s
CI / Import/export fidelity gate (push) Failing after 36s
CI / Build container images (push) Has been skipped
Make the "structure-true best effort" fidelity contract (ADR 0009) an objective, pipeline-gated suite so "best effort" cannot erode silently. - New CI job "Import/export fidelity gate" (.gitea/workflows/ci.yml) runs the corpus suites against the pinned sidecar images the stages use (pandoc/core:3.6, gotenberg/gotenberg:8), started via docker run and reached over the host gateway. Small and separate so it stays well under five minutes; the suites self-skip in the main checks job (no sidecars). - Export fidelity: fixtures/export corpus + gen-export-fixtures.mjs + export.fidelity.test.ts — exports Markdown to docx/odt through the real pinned pandoc and reads it back, snapshotting the round trip so a writer drift (ours or a version bump) fails the gate. - PDF smoke: pdf.fidelity.test.ts renders a page through real Gotenberg and asserts the extracted text and a sane page count (pdf-parse, dev-only). - Fidelity contract doc: fixtures/README.md defines "corpus green = fidelity acceptable" and the fixture-first bug process; per-corpus READMEs updated. Because the snapshots are byte-exact and generated with the pinned tools, bumping a sidecar without regenerating shifts the output and fails the suite (AC3). The import corpus (#63) is folded into the same gate. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EwZ4jR4KFAPvpjWevfUGX1
74 lines
3.0 KiB
TypeScript
74 lines
3.0 KiB
TypeScript
import { readFileSync } from 'node:fs';
|
|
import { join } from 'node:path';
|
|
|
|
import { beforeAll, describe, expect, it, TestContext } from 'vitest';
|
|
|
|
import { AppConfig } from '../config/app-config.service';
|
|
|
|
import { markdownForDocument } from './export-markdown';
|
|
import { PandocServerConverter } from './pandoc.converter';
|
|
|
|
/**
|
|
* Export fidelity regression (issue #69, ADR 0009): exports the committed
|
|
* Markdown corpus to `.docx`/`.odt` through the real pinned pandoc and reads
|
|
* each document back, asserting the round-trip Markdown matches its snapshot.
|
|
* This gates the export writer's structural fidelity — a pandoc version bump
|
|
* that shifts the office output surfaces as a snapshot diff to review, not a
|
|
* silent regression. Needs a reachable pandoc sidecar (`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 runner's cwd is `apps/api`; the corpus lives at the repo root.
|
|
const FIXTURES = join(process.cwd(), '../../fixtures/export');
|
|
// Same reader options as the import corpus, so both directions normalise alike.
|
|
const MARKDOWN_FORMAT = 'gfm-implicit_figures-raw_html';
|
|
|
|
const converter = new PandocServerConverter({ env: { PANDOC_URL } } as unknown as AppConfig);
|
|
|
|
let reachable = false;
|
|
|
|
const CORPUS = ['article', 'formatting'];
|
|
const FORMATS = ['docx', 'odt'] as const;
|
|
|
|
/** Export `markdown` to an office document, then read it back to Markdown —
|
|
* exactly the ExportService write path (gfm → ext, standalone) followed by a
|
|
* re-read (ext → gfm). */
|
|
async function roundTrip(markdown: string, ext: 'docx' | 'odt'): Promise<string> {
|
|
const document = await converter.convert({
|
|
from: 'gfm',
|
|
to: ext,
|
|
input: Buffer.from(markdown, 'utf8'),
|
|
standalone: true,
|
|
});
|
|
const back = await converter.convert({
|
|
from: ext,
|
|
to: MARKDOWN_FORMAT,
|
|
input: document.output,
|
|
standalone: false,
|
|
wrap: 'none',
|
|
});
|
|
return back.output.toString('utf8');
|
|
}
|
|
|
|
describe('export fidelity corpus (real pandoc, issue #69)', () => {
|
|
beforeAll(async () => {
|
|
reachable = await converter.reachable().catch(() => false);
|
|
});
|
|
|
|
for (const name of CORPUS) {
|
|
for (const ext of FORMATS) {
|
|
it(`round-trips ${name}.md through ${ext} to its expected Markdown`, async (ctx: TestContext) => {
|
|
if (!reachable) ctx.skip();
|
|
const source = readFileSync(join(FIXTURES, `${name}.md`), 'utf8');
|
|
// The export transform (flatten wikilinks, inline images) runs first; for
|
|
// this text corpus it is an identity, so the snapshot isolates pandoc's
|
|
// office-writer fidelity — the part that drifts on a version bump.
|
|
const document = markdownForDocument(source, new Map());
|
|
const expected = readFileSync(join(FIXTURES, `${name}.${ext}.expected.md`), 'utf8');
|
|
expect(await roundTrip(document, ext)).toBe(expected);
|
|
});
|
|
}
|
|
}
|
|
});
|