Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
04c8def
test(document-cli): cover the previously-untested runtime helpers
Mearman Sep 13, 2026
f2efefe
test(document-cli): cover the tui state module's format/screen predic…
Mearman Sep 13, 2026
73883c6
fix(document-cli): use a real FontSubstitution reason in the fallback…
Mearman Sep 13, 2026
0067f30
test(document-cli): cover the formats command and the shared CLI opti…
Mearman Sep 13, 2026
f3c3635
test(document-cli): cover the SQL result-set table renderer
Mearman Sep 13, 2026
46f533a
fix(document-cli): remove exit-code branches the fallthrough already …
Mearman Sep 13, 2026
8633fe2
test(document-cli): cover the tui metadata dispatcher's per-format cases
Mearman Sep 13, 2026
8a594ef
test(document-cli): cover the tui's own odb report render pipeline
Mearman Sep 13, 2026
fca2b29
test(document-cli): cover the shared colour, text-field, and slide-ta…
Mearman Sep 13, 2026
c236bbb
test(document-cli): cover the vector-field parsers and formula presets
Mearman Sep 13, 2026
e51af12
test(document-cli): cover the ods editor's shared cell/sheet helpers
Mearman Sep 13, 2026
dab5b18
test(document-cli): cover the pdf and odb editor screens' shared helpers
Mearman Sep 13, 2026
b1a76fc
test(document-cli): cover the global key table and the shared navigat…
Mearman Sep 13, 2026
3018cdf
test(document-cli): cover describeError and the async save-action wra…
Mearman Sep 13, 2026
3f5d8c6
test(document-cli): cover the generic convert command's own dispatch
Mearman Sep 13, 2026
5bcd188
test(document-cli): cover the odm-to-pdf command end to end
Mearman Sep 13, 2026
9199862
test(document-cli): cover the xls sheet-list and spreadsheet-grid scr…
Mearman Sep 13, 2026
3923972
test(document-cli): cover the ppt slide-list and slide-detail screens
Mearman Sep 13, 2026
95d3fb7
test(document-cli): cover the command palette's parsing and dispatch
Mearman Sep 13, 2026
adf00cf
test(document-cli): cover the confirm/help/diagnostics/error/search
Mearman Sep 13, 2026
8eb8ae5
test(document-cli): widen the status-line TTL-expiry wait buffer
Mearman Sep 13, 2026
50493de
chore(document-cli): run mutation testing at concurrency 1
Mearman Sep 13, 2026
8335e0b
Revert "chore(document-cli): run mutation testing at concurrency 1"
Mearman Sep 13, 2026
a227b55
test(document-cli): cover the abort-signal passthrough into readFile
Mearman Sep 13, 2026
c76aed5
test(document-cli): cover TextField's own focus-gated Escape handling…
Mearman Sep 13, 2026
ebd74a6
test(document-cli): assert the diagnostics panel's own cyan selection…
Mearman Sep 13, 2026
5ae96ef
test(document-cli): cover parseHexColorInput's own trim, not just val…
Mearman Sep 13, 2026
67d998a
test(document-cli): cover rotation-field selection colour and the odp…
Mearman Sep 13, 2026
4140238
test(document-cli): propagate a genuine bug through cli-main-sea's ow…
Mearman Sep 13, 2026
14d3e50
test(document-cli): cover createProgram's description, version, and e…
Mearman Sep 13, 2026
2ac85d7
refactor(document-cli): extract ErrorDetail's detail node so it is di…
Mearman Sep 13, 2026
e533a14
test(document-cli): cover the doc/markdown/odt body-list format guards
Mearman Sep 13, 2026
863f5e3
fix(document-cli): type the doc/markdown/odt guard-test harness retur…
Mearman Sep 13, 2026
bd60a17
refactor(document-cli): drop parseOptionalNumberField's dead blank check
Mearman Sep 13, 2026
a35ac8f
test(document-cli): cover OdbHarness's own loading gate and effect deps
Mearman Sep 13, 2026
fa0cbf3
test(document-cli): cover resolveSlideTable's table-kind filter and c…
Mearman Sep 13, 2026
1198ecb
refactor(document-cli): move FieldWizard's out-of-range guard before …
Mearman Sep 13, 2026
b4e48e7
test(document-cli): cover AppStateProvider's cwd prop and the outside…
Mearman Sep 13, 2026
d75d382
test(document-cli): cover RunEditorScreen's own commit and cancel dis…
Mearman Sep 14, 2026
9b5b922
test(document-cli): cover outline's whitespace collapse and textless-…
Mearman Sep 14, 2026
ad0b81e
refactor(document-cli): extract the list-row selection colour into a …
Mearman Sep 14, 2026
3d1c2a4
test(document-cli): cover odb-structure's plural, nesting, and omitte…
Mearman Sep 14, 2026
f76d970
test(document-cli): cover footnote type, table-cell recursion, and nu…
Mearman Sep 14, 2026
1d632b0
test(document-cli): cover convert's csv/svg option wiring and command…
Mearman Sep 14, 2026
c4915f1
test(document-cli): cover metadata's no-metadata sentinel and error path
Mearman Sep 14, 2026
63c354e
test(document-cli): assert every registered option's own help text
Mearman Sep 14, 2026
6c7b332
refactor(document-cli): drop dead already-aborted guards from combine…
Mearman Sep 14, 2026
44043d7
test(document-cli): widen settle()'s effect-flush wait against conten…
Mearman Sep 14, 2026
117554e
test(document-cli): cover fonts' style suffix and unresolved-format e…
Mearman Sep 14, 2026
0c944c0
test(document-cli): cover resolveTargetFormat and formatError directly
Mearman Sep 14, 2026
5d8cdce
test(document-cli): assert convert's own exact usage-error messages
Mearman Sep 14, 2026
7c58bcc
test(document-cli): cover odm-to-pdf's own chapter/destination edges
Mearman Sep 14, 2026
7e70809
test(document-cli): cover set-metadata's own error paths and help text
Mearman Sep 14, 2026
53175e1
fix(document-cli): restore the unit project's testTimeout under Stryker
Mearman Sep 14, 2026
93330af
test(document-cli): cover outline's own error paths and help text
Mearman Sep 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions packages/document-cli/src/cli-main-sea.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { EXIT_SUCCESS, EXIT_USAGE_ERROR } from "./runtime/exit-codes";
import { main } from "./cli-main-sea";
import * as programModule from "./program";

// Only the TUI-free dispatch this module owns is exercised here -- every real command's own behaviour is already covered by document-cli's own command-level tests and its full test/smoke.test.mjs (spawning the real dist/cli.js), which src/cli-main.ts's identical `createProgram().parseAsync()` call already reaches. This file exists to prove the one thing genuinely different about the SEA dispatch: no TUI subcommand, and an explicit `tui` invocation refused with a clear message rather than silently doing nothing.
describe("main", () => {
Expand Down Expand Up @@ -49,4 +50,15 @@ describe("main", () => {
expect.stringContaining("Commands:"),
);
});

it("propagates a non-CommanderError bug instead of swallowing it", async () => {
// Every registered action already catches and maps its own errors into a CommanderError (see this module's own comment); a plain Error surfacing here means a genuine, unexpected bug in an action, which must reach the caller rather than being silently absorbed alongside the expected --help/--version CommanderError case.
const brokenProgram = programModule.createProgram();
brokenProgram.command("boom").action(() => {
throw new Error("boom");
});
vi.spyOn(programModule, "createProgram").mockReturnValue(brokenProgram);
process.argv = ["node", "sea-entry.js", "boom"];
await expect(main()).rejects.toThrow("boom");
});
});
238 changes: 238 additions & 0 deletions packages/document-cli/src/commands/convert.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
import { mkdtemp, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { createDocx } from "documents.js";
import {
afterAll,
afterEach,
beforeAll,
describe,
expect,
it,
vi,
} from "vitest";
import { createProgram } from "../program";
import { EXIT_SUCCESS, EXIT_USAGE_ERROR } from "../runtime/exit-codes";

let workspace: string;
let savedExitCode: typeof process.exitCode;

interface CapturedRun {
readonly exitCode: typeof process.exitCode;
readonly stderr: string;
}

async function runCli(args: readonly string[]): Promise<CapturedRun> {
const stderrChunks: string[] = [];
const stderrSpy = vi
.spyOn(process.stderr, "write")
.mockImplementation((chunk) => {
stderrChunks.push(
typeof chunk === "string" ? chunk : new TextDecoder().decode(chunk),
);
return true;
});
const stdoutSpy = vi
.spyOn(process.stdout, "write")
.mockImplementation(() => true);
try {
await createProgram().parseAsync(["node", "document-cli", ...args]);
} finally {
stderrSpy.mockRestore();
stdoutSpy.mockRestore();
}
return { exitCode: process.exitCode, stderr: stderrChunks.join("") };
}

beforeAll(async () => {
workspace = await mkdtemp(join(tmpdir(), "document-cli-convert-"));
const docx = createDocx();
docx.body.appendParagraph().appendRun({ text: "hello" });
await writeFile(join(workspace, "input.docx"), docx.toBytes());
});

afterAll(async () => {
await rm(workspace, { recursive: true, force: true });
});

afterEach(() => {
process.exitCode = savedExitCode;
});

beforeAll(() => {
savedExitCode = process.exitCode;
});

describe("convert", () => {
it("converts a real docx to pdf via the generic command, inferring both formats", async () => {
const output = join(workspace, "generic-output.pdf");
const { exitCode } = await runCli([
"convert",
join(workspace, "input.docx"),
output,
]);
expect(exitCode).toBe(EXIT_SUCCESS);
});

it("rejects a .odm input, naming odm-to-pdf as the alternative", async () => {
const { exitCode, stderr } = await runCli([
"convert",
join(workspace, "book.odm"),
join(workspace, "out.pdf"),
]);
expect(exitCode).toBe(EXIT_USAGE_ERROR);
expect(stderr).toContain("'.odm' master documents are not supported");
expect(stderr).toContain("odm-to-pdf");
});

it("rejects a .odb input, naming the odb-specific commands as the alternative", async () => {
const { exitCode, stderr } = await runCli([
"convert",
join(workspace, "database.odb"),
join(workspace, "out.pdf"),
]);
expect(exitCode).toBe(EXIT_USAGE_ERROR);
expect(stderr).toContain("'.odb' embedded databases are not supported");
expect(stderr).toContain("odb-to-csv");
expect(stderr).toContain("odb-to-xlsx");
expect(stderr).toContain("odb-tables");
});

it("fails clearly when the source format cannot be inferred from the input extension", async () => {
const { exitCode, stderr } = await runCli([
"convert",
join(workspace, "mystery.xyz"),
join(workspace, "out.pdf"),
]);
expect(exitCode).toBe(EXIT_USAGE_ERROR);
expect(stderr).toContain("cannot infer a source format from");
expect(stderr).toContain("mystery.xyz");
});

it("fails clearly when the target format cannot be resolved at all", async () => {
const { exitCode, stderr } = await runCli([
"convert",
join(workspace, "input.docx"),
]);
expect(exitCode).toBe(EXIT_USAGE_ERROR);
expect(stderr).toBe(
"convert: cannot infer a target format -- pass an output path with a recognised extension, --out with one, or --to <format>\n",
);
});

it("fails clearly when the output path's own extension is not a recognised format", async () => {
const { exitCode, stderr } = await runCli([
"convert",
join(workspace, "input.docx"),
join(workspace, "out.mystery"),
]);
expect(exitCode).toBe(EXIT_USAGE_ERROR);
expect(stderr).toBe(
`convert: cannot infer a target format from '${join(workspace, "out.mystery")}'; pass --to <format> instead\n`,
);
});

it("rejects an unrecognised --to format, naming it and every known format", async () => {
const { exitCode, stderr } = await runCli([
"convert",
join(workspace, "input.docx"),
join(workspace, "out.pdf"),
"--to",
"not-a-format",
]);
expect(exitCode).toBe(EXIT_USAGE_ERROR);
expect(stderr).toBe(
"convert: unknown --to format 'not-a-format'; expected one of docx, pptx, xlsx, odt, odp, ods, odg, svg, odf, csv, markdown, rtf, wpd, doc, xls, ppt, epub, pdf\n",
);
});

it("rejects a positional output and a conflicting --out, naming both", async () => {
const { exitCode, stderr } = await runCli([
"convert",
join(workspace, "input.docx"),
"positional.pdf",
"--out",
"different.pdf",
]);
expect(exitCode).toBe(EXIT_USAGE_ERROR);
expect(stderr).toBe(
"[docx-to-pdf] conflicting output destinations: positional 'positional.pdf' and --out 'different.pdf'\n",
);
});

it("prefers --to over the output path's own extension for the target format", async () => {
const output = join(workspace, "explicit-to.pdf");
const { exitCode } = await runCli([
"convert",
join(workspace, "input.docx"),
output,
"--to",
"pdf",
]);
expect(exitCode).toBe(EXIT_SUCCESS);
});
});

describe("docx-to-pdf", () => {
it("registers the explicit per-pair command and runs a real conversion", async () => {
const output = join(workspace, "explicit-output.pdf");
const { exitCode } = await runCli([
"docx-to-pdf",
join(workspace, "input.docx"),
output,
]);
expect(exitCode).toBe(EXIT_SUCCESS);
});
});

describe("registerConversionCommands option wiring", () => {
const program = createProgram();
const byName = (name: string) =>
program.commands.find((command) => command.name() === name);
const hasOption = (
command: ReturnType<typeof byName>,
long: string,
): boolean => (command?.options ?? []).some((option) => option.long === long);

it("describes each explicit per-pair command by its own source and target", () => {
expect(byName("docx-to-pdf")?.description()).toBe(
"convert a docx document to pdf",
);
expect(byName("pdf-to-docx")?.description()).toBe(
"convert a pdf document to docx",
);
});

it("adds --delimiter only to a command whose source or target is csv", () => {
expect(hasOption(byName("docx-to-csv"), "--delimiter")).toBe(true);
expect(hasOption(byName("csv-to-docx"), "--delimiter")).toBe(true);
expect(hasOption(byName("docx-to-pdf"), "--delimiter")).toBe(false);
});

it("adds --sheet only to a command whose target is csv", () => {
expect(hasOption(byName("docx-to-csv"), "--sheet")).toBe(true);
expect(hasOption(byName("csv-to-docx"), "--sheet")).toBe(false);
expect(hasOption(byName("docx-to-pdf"), "--sheet")).toBe(false);
});

it("adds --page only to a command whose target is svg", () => {
expect(hasOption(byName("docx-to-svg"), "--page")).toBe(true);
expect(hasOption(byName("docx-to-csv"), "--page")).toBe(false);
expect(hasOption(byName("docx-to-pdf"), "--page")).toBe(false);
});

it("describes the generic convert command and registers its shared and --to options", () => {
const generic = byName("convert");
expect(generic?.description()).toBe(
"convert between any two supported document formats, inferring source/target from file extensions where possible",
);
expect(hasOption(generic, "--json")).toBe(true);
expect(hasOption(generic, "--dump-package")).toBe(true);
expect(hasOption(generic, "--sheet")).toBe(true);
expect(hasOption(generic, "--page")).toBe(true);
const toOption = generic?.options.find((option) => option.long === "--to");
expect(toOption?.description).toContain(
"target format when it cannot be inferred from the output path",
);
});
});
Loading