Files
SnapOtter/packages/doc-engine/tests/pandoc-kills.test.ts
T

133 lines
5.2 KiB
TypeScript
Raw Normal View History

import { EventEmitter } from "node:events";
import { afterEach, describe, expect, it, vi } from "vitest";
// pandoc.ts caches version detection in module-level state, so each case loads a
// fresh module with its own mocked child_process. These target the mutants the
// first suite left alive: the exact spawnSync/spawn option objects, the
// `status !== 0 || !r.stdout` fallback (each operand isolated), the
// `/pandoc\s+(\d+)/` major-version regex, and the settle-once guard in runPandoc.
async function loadWithVersion(status: number | null, stdout: string) {
vi.resetModules();
const spawnSync = vi.fn(() => ({ status, stdout }));
const spawn = vi.fn();
vi.doMock("node:child_process", () => ({ spawnSync, spawn }));
const mod = await import("../src/pandoc.js");
return { mod, spawnSync, spawn };
}
function selfContainedFlags(mod: {
buildPandocArgs: (i: string, o: string, opts: object) => string[];
}) {
// Strip the fixed prefix so only the version-derived flags remain.
return mod.buildPandocArgs("in.md", "out.html", { selfContained: true }).slice(4);
}
describe("pandoc version-detection spawnSync options", () => {
afterEach(() => vi.resetModules());
it("probes `pandoc --version` with the exact encoding and stdio options", async () => {
const { mod, spawnSync } = await loadWithVersion(0, "pandoc 3.1.2\n");
selfContainedFlags(mod);
expect(spawnSync).toHaveBeenCalledWith("pandoc", ["--version"], {
encoding: "utf8",
stdio: ["ignore", "pipe", "ignore"],
});
});
});
describe("pandoc selfContainedArgs version branch", () => {
afterEach(() => vi.resetModules());
it("major 3 yields --embed-resources --standalone", async () => {
const { mod } = await loadWithVersion(0, "pandoc 3.1.2\n");
expect(selfContainedFlags(mod)).toEqual(["--embed-resources", "--standalone"]);
});
it("major 2 yields --self-contained", async () => {
const { mod } = await loadWithVersion(0, "pandoc 2.17.1.1\n");
expect(selfContainedFlags(mod)).toEqual(["--self-contained"]);
});
it("a two-digit major (10) still takes the >=3 path (kills \\d+ -> \\d)", async () => {
const { mod } = await loadWithVersion(0, "pandoc 10.0.1\n");
expect(selfContainedFlags(mod)).toEqual(["--embed-resources", "--standalone"]);
});
it("output with no version token defaults to major 2 -> --self-contained", async () => {
const { mod } = await loadWithVersion(0, "some unexpected banner\n");
expect(selfContainedFlags(mod)).toEqual(["--self-contained"]);
});
it("nonzero status falls back to --self-contained even with a 3.x banner (kills the status operand)", async () => {
// status !== 0 is true, stdout is truthy: only the || short-circuit reaches the fallback.
const { mod } = await loadWithVersion(1, "pandoc 3.1.2\n");
expect(selfContainedFlags(mod)).toEqual(["--self-contained"]);
});
it("empty stdout with status 0 falls back to --self-contained (kills the !stdout operand)", async () => {
// status !== 0 is false, !stdout is true: the other || operand must reach the fallback.
const { mod } = await loadWithVersion(0, "");
expect(selfContainedFlags(mod)).toEqual(["--self-contained"]);
});
});
class FakeChild extends EventEmitter {
stderr = new EventEmitter();
kill = vi.fn();
}
describe("pandoc runPandoc spawn options and settle-once", () => {
afterEach(() => vi.resetModules());
it("spawns with stdio ignore/pipe/pipe", async () => {
vi.resetModules();
const child = new FakeChild();
const spawn = vi.fn(() => child);
vi.doMock("node:child_process", () => ({
spawn,
spawnSync: vi.fn(() => ({ status: 0, stdout: "pandoc 3.1.2" })),
}));
const mod = await import("../src/pandoc.js");
const p = mod.runPandoc("in.md", "out.html");
child.emit("close", 0, null);
await expect(p).resolves.toBeUndefined();
expect(spawn).toHaveBeenCalledWith(expect.anything(), expect.anything(), {
stdio: ["ignore", "pipe", "pipe"],
});
});
it("settles once: a close after an error does not flip the result", async () => {
vi.resetModules();
const child = new FakeChild();
const spawn = vi.fn(() => child);
vi.doMock("node:child_process", () => ({
spawn,
spawnSync: vi.fn(() => ({ status: 0, stdout: "pandoc 3" })),
}));
const mod = await import("../src/pandoc.js");
const p = mod.runPandoc("in.md", "out.html");
const boom = new Error("spawn error");
child.emit("error", boom);
// A late close(0) must be ignored because `settled` is already true; if the
// settled guard is inverted, this would try to resolve an already-rejected promise.
child.emit("close", 0, null);
await expect(p).rejects.toBe(boom);
});
it("rejects with the exit code and stderr tail on nonzero close", async () => {
vi.resetModules();
const child = new FakeChild();
const spawn = vi.fn(() => child);
vi.doMock("node:child_process", () => ({
spawn,
spawnSync: vi.fn(() => ({ status: 0, stdout: "pandoc 3" })),
}));
const mod = await import("../src/pandoc.js");
const p = mod.runPandoc("in.md", "out.html");
child.stderr.emit("data", Buffer.from("boom detail"));
child.emit("close", 3, null);
await expect(p).rejects.toThrow(/pandoc exited 3: .*boom detail/);
});
});