Files
SnapOtter/packages/doc-engine/tests/python-docs.test.ts
T
SnapOtterandGitHub 301e6eb01a test: coverage campaign and mutation testing across five packages (#628)
Coverage 83.6 to 87.36% lines, 81.63 to 84.14% branches. Mutation testing across five packages: image-engine 85, media-engine 92, doc-engine 87, shared+enterprise 86, apps/api security and jobs slice. Runs all five lanes weekly. Fixes the silently-broken mutation CI (babel pin), a redact-pdf envelope-shape test bug, an untested enterprise license valid-signature path, and an audit test that only exercised a hand-copied reproduction. Test and config only, no product code changes beyond the babel pin and one test-only oidc export. Full suite: 16,712 pass, 0 fail.
2026-07-24 17:36:57 +08:00

367 lines
14 KiB
TypeScript

import type { SignPlacement } from "@snapotter/shared";
import { beforeEach, describe, expect, it, vi } from "vitest";
/**
* python-docs.ts wraps runDocsScript (from @snapotter/ai) around the doc_* Python
* scripts. We mock only runDocsScript and use the real @snapotter/shared SafeError
* so parseDocsJson's error type is exercised faithfully.
*/
const runDocsScript = vi.fn<(script: string, args: unknown, opts?: unknown) => Promise<string>>();
vi.mock("@snapotter/ai", () => ({
runDocsScript: (script: string, args: unknown, opts?: unknown) =>
runDocsScript(script, args, opts),
}));
function firstCall() {
return runDocsScript.mock.calls[0];
}
beforeEach(() => {
runDocsScript.mockReset();
});
describe("pdfPageCountPy", () => {
it("calls doc_pagecount with the path and returns the page count", async () => {
runDocsScript.mockResolvedValueOnce('{"pages": 7}');
const n = await import("../src/python-docs.js").then((m) => m.pdfPageCountPy("/abs/doc.pdf"));
expect(n).toBe(7);
expect(firstCall()).toEqual(["doc_pagecount", { path: "/abs/doc.pdf" }, undefined]);
});
it("throws the reported error message", async () => {
runDocsScript.mockResolvedValueOnce('{"error": "corrupt xref"}');
await expect(
import("../src/python-docs.js").then((m) => m.pdfPageCountPy("/x.pdf")),
).rejects.toThrow("doc_pagecount failed: corrupt xref");
});
it("throws when pages is not a number", async () => {
runDocsScript.mockResolvedValueOnce('{"pages": "seven"}');
await expect(
import("../src/python-docs.js").then((m) => m.pdfPageCountPy("/x.pdf")),
).rejects.toThrow("doc_pagecount failed:");
});
it("returns 0 pages faithfully", async () => {
runDocsScript.mockResolvedValueOnce('{"pages": 0}');
await expect(
import("../src/python-docs.js").then((m) => m.pdfPageCountPy("/x.pdf")),
).resolves.toBe(0);
});
});
describe("parseDocsJson (via pdfFlattenPy non-JSON)", () => {
it("resolves on a valid ok:true payload", async () => {
runDocsScript.mockResolvedValueOnce('{"ok": true}');
await expect(
import("../src/python-docs.js").then((m) => m.pdfFlattenPy("/in.pdf", "/out.pdf")),
).resolves.toBeUndefined();
});
it("passes path + out to doc_flatten", async () => {
runDocsScript.mockResolvedValueOnce('{"ok": true}');
await import("../src/python-docs.js").then((m) => m.pdfFlattenPy("/in.pdf", "/out.pdf"));
expect(firstCall()).toEqual(["doc_flatten", { path: "/in.pdf", out: "/out.pdf" }, undefined]);
});
it("wraps non-JSON stdout in a SafeError with kind bug, script code, and raw cause", async () => {
runDocsScript.mockResolvedValueOnce("Traceback (most recent call last): boom");
try {
await import("../src/python-docs.js").then((m) => m.pdfFlattenPy("/in.pdf", "/out.pdf"));
throw new Error("should have thrown");
} catch (e) {
const err = e as {
message: string;
name: string;
kind?: string;
code?: string;
cause?: Error;
};
expect(err.name).toBe("SafeError");
expect(err.message).toBe("Document tool returned non-JSON output");
expect(err.kind).toBe("bug");
expect(err.code).toBe("doc_flatten");
expect(err.cause?.message).toContain("doc_flatten stdout (first 200 chars):");
expect(err.cause?.message).toContain("Traceback");
}
});
it("truncates the raw stdout to 200 chars in the SafeError cause", async () => {
const huge = `X${"y".repeat(500)}`; // not JSON
runDocsScript.mockResolvedValueOnce(huge);
try {
await import("../src/python-docs.js").then((m) => m.pdfFlattenPy("/in.pdf", "/out.pdf"));
throw new Error("should have thrown");
} catch (e) {
const cause = (e as { cause?: Error }).cause;
const snippet = cause?.message.split("chars): ")[1] ?? "";
expect(snippet.length).toBe(200);
}
});
it("surfaces a script-reported error (JSON with error field) as a plain Error", async () => {
runDocsScript.mockResolvedValueOnce('{"error": "flatten blew up"}');
await expect(
import("../src/python-docs.js").then((m) => m.pdfFlattenPy("/in.pdf", "/out.pdf")),
).rejects.toThrow("doc_flatten failed: flatten blew up");
});
});
describe("pdfScrubProducerPy", () => {
it("calls doc_scrub_meta with path + out and resolves on ok", async () => {
runDocsScript.mockResolvedValueOnce('{"ok": true}');
await import("../src/python-docs.js").then((m) => m.pdfScrubProducerPy("/in.pdf", "/out.pdf"));
expect(firstCall()).toEqual([
"doc_scrub_meta",
{ path: "/in.pdf", out: "/out.pdf" },
undefined,
]);
});
it("throws the reported error", async () => {
runDocsScript.mockResolvedValueOnce('{"error": "scrub failed"}');
await expect(
import("../src/python-docs.js").then((m) => m.pdfScrubProducerPy("/in.pdf", "/out.pdf")),
).rejects.toThrow("doc_scrub_meta failed: scrub failed");
});
});
describe("pdfRedactPy", () => {
it("passes terms + caseSensitive and returns the found count", async () => {
runDocsScript.mockResolvedValueOnce('{"found": 3, "verified": true}');
const res = await import("../src/python-docs.js").then((m) =>
m.pdfRedactPy("/in.pdf", "/out.pdf", ["ssn", "dob"], true),
);
expect(res).toEqual({ found: 3 });
expect(firstCall()).toEqual([
"doc_redact",
{ path: "/in.pdf", out: "/out.pdf", terms: ["ssn", "dob"], caseSensitive: true },
undefined,
]);
});
it("forwards caseSensitive: false verbatim", async () => {
runDocsScript.mockResolvedValueOnce('{"found": 0}');
await import("../src/python-docs.js").then((m) =>
m.pdfRedactPy("/in.pdf", "/out.pdf", ["x"], false),
);
expect((firstCall()[1] as { caseSensitive: boolean }).caseSensitive).toBe(false);
});
it("returns found: 0 when nothing matched", async () => {
runDocsScript.mockResolvedValueOnce('{"found": 0, "verified": true}');
await expect(
import("../src/python-docs.js").then((m) =>
m.pdfRedactPy("/in.pdf", "/out.pdf", ["x"], true),
),
).resolves.toEqual({ found: 0 });
});
it("throws the reported error", async () => {
runDocsScript.mockResolvedValueOnce('{"error": "redaction verify failed"}');
await expect(
import("../src/python-docs.js").then((m) =>
m.pdfRedactPy("/in.pdf", "/out.pdf", ["x"], true),
),
).rejects.toThrow("doc_redact failed: redaction verify failed");
});
it("throws when found is missing but no error field is present", async () => {
runDocsScript.mockResolvedValueOnce('{"verified": true}');
await expect(
import("../src/python-docs.js").then((m) =>
m.pdfRedactPy("/in.pdf", "/out.pdf", ["x"], true),
),
).rejects.toThrow("doc_redact failed:");
});
});
describe("pdfTextPy", () => {
it("passes path + out and returns chars + hasText", async () => {
runDocsScript.mockResolvedValueOnce('{"chars": 128, "hasText": true}');
const res = await import("../src/python-docs.js").then((m) =>
m.pdfTextPy("/in.pdf", "/out.txt"),
);
expect(res).toEqual({ chars: 128, hasText: true });
expect(firstCall()).toEqual(["doc_text", { path: "/in.pdf", out: "/out.txt" }, undefined]);
});
it("respects an explicit hasText:false even with chars > 0", async () => {
runDocsScript.mockResolvedValueOnce('{"chars": 50, "hasText": false}');
await expect(
import("../src/python-docs.js").then((m) => m.pdfTextPy("/in.pdf", "/out.txt")),
).resolves.toEqual({ chars: 50, hasText: false });
});
it("derives hasText=true from chars>0 when hasText is absent", async () => {
runDocsScript.mockResolvedValueOnce('{"chars": 5}');
await expect(
import("../src/python-docs.js").then((m) => m.pdfTextPy("/in.pdf", "/out.txt")),
).resolves.toEqual({ chars: 5, hasText: true });
});
it("derives hasText=false from chars===0 when hasText is absent", async () => {
runDocsScript.mockResolvedValueOnce('{"chars": 0}');
await expect(
import("../src/python-docs.js").then((m) => m.pdfTextPy("/in.pdf", "/out.txt")),
).resolves.toEqual({ chars: 0, hasText: false });
});
it("throws the reported error", async () => {
runDocsScript.mockResolvedValueOnce('{"error": "text extract failed"}');
await expect(
import("../src/python-docs.js").then((m) => m.pdfTextPy("/in.pdf", "/out.txt")),
).rejects.toThrow("doc_text failed: text extract failed");
});
it("throws when chars is not a number and no error field is present", async () => {
runDocsScript.mockResolvedValueOnce('{"hasText": true}');
await expect(
import("../src/python-docs.js").then((m) => m.pdfTextPy("/in.pdf", "/out.txt")),
).rejects.toThrow("doc_text failed:");
});
});
describe("pdfToWordPy", () => {
it("calls doc_to_word with a 5-minute timeout", async () => {
runDocsScript.mockResolvedValueOnce('{"ok": true}');
await import("../src/python-docs.js").then((m) => m.pdfToWordPy("/in.pdf", "/out.docx"));
expect(firstCall()).toEqual([
"doc_to_word",
{ path: "/in.pdf", out: "/out.docx" },
{ timeoutMs: 300_000 },
]);
});
it("throws the reported error", async () => {
runDocsScript.mockResolvedValueOnce('{"error": "pdf2docx failed"}');
await expect(
import("../src/python-docs.js").then((m) => m.pdfToWordPy("/in.pdf", "/out.docx")),
).rejects.toThrow("doc_to_word failed: pdf2docx failed");
});
});
describe("pdfMetadataGetPy", () => {
it("calls doc_metadata mode:get and returns the metadata object", async () => {
runDocsScript.mockResolvedValueOnce('{"metadata": {"Title": "Report", "Author": "Otter"}}');
const meta = await import("../src/python-docs.js").then((m) => m.pdfMetadataGetPy("/in.pdf"));
expect(meta).toEqual({ Title: "Report", Author: "Otter" });
expect(firstCall()).toEqual(["doc_metadata", { path: "/in.pdf", mode: "get" }, undefined]);
});
it("throws the reported error", async () => {
runDocsScript.mockResolvedValueOnce('{"error": "docinfo read failed"}');
await expect(
import("../src/python-docs.js").then((m) => m.pdfMetadataGetPy("/in.pdf")),
).rejects.toThrow("doc_metadata get failed: docinfo read failed");
});
it("throws when metadata is missing/not an object", async () => {
runDocsScript.mockResolvedValueOnce('{"metadata": "not-an-object"}');
await expect(
import("../src/python-docs.js").then((m) => m.pdfMetadataGetPy("/in.pdf")),
).rejects.toThrow("doc_metadata get failed:");
});
});
describe("pdfMetadataSetPy", () => {
it("calls doc_metadata mode:set with the metadata payload", async () => {
runDocsScript.mockResolvedValueOnce('{"ok": true}');
await import("../src/python-docs.js").then((m) =>
m.pdfMetadataSetPy("/in.pdf", "/out.pdf", { Title: "New Title" }),
);
expect(firstCall()).toEqual([
"doc_metadata",
{ path: "/in.pdf", out: "/out.pdf", mode: "set", metadata: { Title: "New Title" } },
undefined,
]);
});
it("throws the reported error", async () => {
runDocsScript.mockResolvedValueOnce('{"error": "docinfo write failed"}');
await expect(
import("../src/python-docs.js").then((m) =>
m.pdfMetadataSetPy("/in.pdf", "/out.pdf", { Title: "X" }),
),
).rejects.toThrow("doc_metadata set failed: docinfo write failed");
});
});
describe("htmlToPdfPy", () => {
it("calls doc_html_pdf with the mode and a 2-minute timeout", async () => {
runDocsScript.mockResolvedValueOnce('{"ok": true}');
await import("../src/python-docs.js").then((m) =>
m.htmlToPdfPy("/in.html", "/out.pdf", "html"),
);
expect(firstCall()).toEqual([
"doc_html_pdf",
{ path: "/in.html", out: "/out.pdf", mode: "html" },
{ timeoutMs: 120_000 },
]);
});
it("forwards the markdown mode verbatim", async () => {
runDocsScript.mockResolvedValueOnce('{"ok": true}');
await import("../src/python-docs.js").then((m) =>
m.htmlToPdfPy("/in.md", "/out.pdf", "markdown"),
);
expect((firstCall()[1] as { mode: string }).mode).toBe("markdown");
});
it("throws the reported error", async () => {
runDocsScript.mockResolvedValueOnce('{"error": "weasyprint failed"}');
await expect(
import("../src/python-docs.js").then((m) => m.htmlToPdfPy("/in.html", "/out.pdf", "html")),
).rejects.toThrow("doc_html_pdf failed: weasyprint failed");
});
});
describe("pdfSignPy", () => {
const placements: SignPlacement[] = [{ sig: 0, page: 1, x: 0.1, y: 0.2, w: 0.3, h: 0.4 }];
it("uses input/output keys and passes signatures + placements, returning placed", async () => {
runDocsScript.mockResolvedValueOnce('{"placed": 2, "ok": true}');
const res = await import("../src/python-docs.js").then((m) =>
m.pdfSignPy("/in.pdf", "/out.pdf", ["sig0.png", "sig1.png"], placements),
);
expect(res).toEqual({ placed: 2 });
expect(firstCall()).toEqual([
"doc_sign",
{
input: "/in.pdf",
output: "/out.pdf",
signatures: ["sig0.png", "sig1.png"],
placements,
},
undefined,
]);
});
it("returns placed: 0 faithfully", async () => {
runDocsScript.mockResolvedValueOnce('{"placed": 0}');
await expect(
import("../src/python-docs.js").then((m) =>
m.pdfSignPy("/in.pdf", "/out.pdf", ["s.png"], placements),
),
).resolves.toEqual({ placed: 0 });
});
it("throws the reported error", async () => {
runDocsScript.mockResolvedValueOnce('{"error": "sign failed"}');
await expect(
import("../src/python-docs.js").then((m) =>
m.pdfSignPy("/in.pdf", "/out.pdf", ["s.png"], placements),
),
).rejects.toThrow("doc_sign failed: sign failed");
});
it("throws when placed is missing but no error field is present", async () => {
runDocsScript.mockResolvedValueOnce('{"ok": true}');
await expect(
import("../src/python-docs.js").then((m) =>
m.pdfSignPy("/in.pdf", "/out.pdf", ["s.png"], placements),
),
).rejects.toThrow("doc_sign failed:");
});
});