const fs = require("node:fs"); const path = require("node:path"); const { spawnSync, execFileSync } = require("node:child_process"); function parseArgs(argv) { const out = { fixtures: "/fixtures", outDir: "/tmp/pdf-fixtures-out" }; for (let i = 2; i < argv.length; i++) { const a = argv[i]; if (a === "--fixtures") out.fixtures = argv[++i]; else if (a === "--out") out.outDir = argv[++i]; } return out; } function readManifestSHA() { const p = "/opt/forgejo-pdf/manifest.json"; const b = fs.readFileSync(p, "utf8"); const j = JSON.parse(b); if (!j.manifest_sha || typeof j.manifest_sha !== "string") { throw new Error("manifest.json missing manifest_sha"); } return j.manifest_sha; } function extractKnownUniqueString(md) { const m = md.match(/KNOWN_UNIQUE_STRING:\s*([A-Za-z0-9_\-\.]+)/); return m ? m[1] : null; } function containsMermaid(md) { return /```mermaid[\s\S]*?```/m.test(md); } function run(cmd, args, opts = {}) { const res = spawnSync(cmd, args, { encoding: "utf8", ...opts }); return res; } function main() { const { fixtures, outDir } = parseArgs(process.argv); fs.mkdirSync(outDir, { recursive: true }); const manifestSHA = readManifestSHA(); const pdfConfig = { pdf: { determinism: "strict", timestamp: "commit_time", typography: "professional", mermaid: { strategy: "balanced", caption: false }, orphansWidows: { enforce: true }, footer: { enabled: true } } }; const files = fs .readdirSync(fixtures) .filter((f) => f.endsWith(".md")) .sort(); if (files.length === 0) { throw new Error("no fixture markdown files found"); } for (const f of files) { const mdPath = path.join(fixtures, f); const md = fs.readFileSync(mdPath, "utf8"); const expected = extractKnownUniqueString(md); if (!expected) throw new Error(`fixture missing KNOWN_UNIQUE_STRING: ${f}`); const input = { markdown: md, repoMeta: { owner: "fixture", repo: "forgejo-pdf", path: f, repoID: 1, commitSHA: "0123456789abcdef0123456789abcdef01234567", commitTimeRFC3339: "2020-01-02T03:04:05Z" }, config: pdfConfig, manifestSHA }; const jobDir = fs.mkdtempSync(path.join(outDir, "job-")); const inPath = path.join(jobDir, "input.json"); const outPath = path.join(jobDir, "output.pdf"); fs.writeFileSync(inPath, JSON.stringify(input), "utf8"); const res = run("node", ["src/index.js", "--in", inPath, "--out", outPath], { cwd: "/opt/forgejo-pdf" }); if (res.status !== 0) { throw new Error(`worker failed for ${f}: ${res.stderr.trim() || res.stdout.trim()}`); } const logs = res.stderr .split("\n") .map((l) => l.trim()) .filter(Boolean) .map((l) => { try { return JSON.parse(l); } catch { return null; } }) .filter(Boolean); const done = logs.findLast ? logs.findLast((l) => l.event === "done") : logs.reverse().find((l) => l.event === "done"); if (!done) throw new Error(`missing done log for ${f}`); if (done.blocked_requests !== 0) throw new Error(`blocked_requests != 0 for ${f}`); const hasMermaid = containsMermaid(md); if (hasMermaid && (!Number.isFinite(done.mermaid_count) || done.mermaid_count < 1)) { throw new Error(`expected mermaid_count >= 1 for ${f}`); } if (!hasMermaid && done.mermaid_count !== 0) { throw new Error(`expected mermaid_count == 0 for ${f}`); } execFileSync("qpdf", ["--check", outPath], { stdio: "inherit" }); const text = execFileSync("pdftotext", [outPath, "-"], { encoding: "utf8" }); const normalized = text.replace(/\s+/g, ""); const expectedNorm = expected.replace(/\s+/g, ""); if (!normalized.includes(expectedNorm)) { throw new Error(`pdftotext missing expected marker for ${f}: ${expected}`); } } } main();