Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
99 commits
Select commit Hold shift + click to select a range
4182302
test(document-cli): cover the previously-untested runtime helpers
Mearman Sep 13, 2026
db36a3b
test(document-cli): cover the tui state module's format/screen predic…
Mearman Sep 13, 2026
39fdab0
fix(document-cli): use a real FontSubstitution reason in the fallback…
Mearman Sep 13, 2026
221fbd2
test(document-cli): cover the formats command and the shared CLI opti…
Mearman Sep 13, 2026
fe56c43
test(document-cli): cover the SQL result-set table renderer
Mearman Sep 13, 2026
cbe4fe9
fix(document-cli): remove exit-code branches the fallthrough already …
Mearman Sep 13, 2026
47b0cda
test(document-cli): cover the tui metadata dispatcher's per-format cases
Mearman Sep 13, 2026
2984636
test(document-cli): cover the tui's own odb report render pipeline
Mearman Sep 13, 2026
6fb7205
test(document-cli): cover the shared colour, text-field, and slide-ta…
Mearman Sep 13, 2026
5a8d035
test(document-cli): cover the vector-field parsers and formula presets
Mearman Sep 13, 2026
009cda9
test(document-cli): cover the ods editor's shared cell/sheet helpers
Mearman Sep 13, 2026
3880a1f
test(document-cli): cover the pdf and odb editor screens' shared helpers
Mearman Sep 13, 2026
316b59c
test(document-cli): cover the global key table and the shared navigat…
Mearman Sep 13, 2026
97611a9
test(document-cli): cover describeError and the async save-action wra…
Mearman Sep 13, 2026
d8139d4
test(document-cli): cover the generic convert command's own dispatch
Mearman Sep 13, 2026
6770009
test(document-cli): cover the odm-to-pdf command end to end
Mearman Sep 13, 2026
50b8f83
test(document-cli): cover the xls sheet-list and spreadsheet-grid scr…
Mearman Sep 13, 2026
aafe8f8
test(document-cli): cover the ppt slide-list and slide-detail screens
Mearman Sep 13, 2026
b218904
test(document-cli): cover the command palette's parsing and dispatch
Mearman Sep 13, 2026
cb6f7d1
test(document-cli): cover the confirm/help/diagnostics/error/search
Mearman Sep 13, 2026
d41a564
test(document-cli): widen the status-line TTL-expiry wait buffer
Mearman Sep 13, 2026
1539c9b
chore(document-cli): run mutation testing at concurrency 1
Mearman Sep 13, 2026
b16e21c
Revert "chore(document-cli): run mutation testing at concurrency 1"
Mearman Sep 13, 2026
9964c72
test(document-cli): cover the abort-signal passthrough into readFile
Mearman Sep 13, 2026
60b64fc
test(document-cli): cover TextField's own focus-gated Escape handling…
Mearman Sep 13, 2026
20ecd3e
test(document-cli): assert the diagnostics panel's own cyan selection…
Mearman Sep 13, 2026
250b94b
test(document-cli): cover parseHexColorInput's own trim, not just val…
Mearman Sep 13, 2026
b56f0b1
test(document-cli): cover rotation-field selection colour and the odp…
Mearman Sep 13, 2026
e3942c4
test(document-cli): propagate a genuine bug through cli-main-sea's ow…
Mearman Sep 13, 2026
e0077bf
test(document-cli): cover createProgram's description, version, and e…
Mearman Sep 13, 2026
cdc899f
refactor(document-cli): extract ErrorDetail's detail node so it is di…
Mearman Sep 13, 2026
6dab352
test(document-cli): cover the doc/markdown/odt body-list format guards
Mearman Sep 13, 2026
751c70d
fix(document-cli): type the doc/markdown/odt guard-test harness retur…
Mearman Sep 13, 2026
e8cff7b
refactor(document-cli): drop parseOptionalNumberField's dead blank check
Mearman Sep 13, 2026
fbf4daa
test(document-cli): cover OdbHarness's own loading gate and effect deps
Mearman Sep 13, 2026
22a03c2
test(document-cli): cover resolveSlideTable's table-kind filter and c…
Mearman Sep 13, 2026
7d018c0
refactor(document-cli): move FieldWizard's out-of-range guard before …
Mearman Sep 13, 2026
46d17d5
test(document-cli): cover AppStateProvider's cwd prop and the outside…
Mearman Sep 13, 2026
3c6601b
test(document-cli): cover RunEditorScreen's own commit and cancel dis…
Mearman Sep 14, 2026
810130f
test(document-cli): cover outline's whitespace collapse and textless-…
Mearman Sep 14, 2026
8ff5637
refactor(document-cli): extract the list-row selection colour into a …
Mearman Sep 14, 2026
a63c7e4
test(document-cli): cover odb-structure's plural, nesting, and omitte…
Mearman Sep 14, 2026
f65749a
test(document-cli): cover footnote type, table-cell recursion, and nu…
Mearman Sep 14, 2026
f3955e0
test(document-cli): cover convert's csv/svg option wiring and command…
Mearman Sep 14, 2026
cf0f020
test(document-cli): cover metadata's no-metadata sentinel and error path
Mearman Sep 14, 2026
358d46e
test(document-cli): assert every registered option's own help text
Mearman Sep 14, 2026
0eb68a7
refactor(document-cli): drop dead already-aborted guards from combine…
Mearman Sep 14, 2026
10324ac
test(document-cli): widen settle()'s effect-flush wait against conten…
Mearman Sep 14, 2026
67fcbc0
test(document-cli): cover fonts' style suffix and unresolved-format e…
Mearman Sep 14, 2026
11a9082
test(document-cli): cover resolveTargetFormat and formatError directly
Mearman Sep 14, 2026
636e9e2
test(document-cli): assert convert's own exact usage-error messages
Mearman Sep 14, 2026
b71df97
test(document-cli): cover odm-to-pdf's own chapter/destination edges
Mearman Sep 14, 2026
3baf41a
test(document-cli): cover set-metadata's own error paths and help text
Mearman Sep 14, 2026
308e6b8
fix(document-cli): restore the unit project's testTimeout under Stryker
Mearman Sep 14, 2026
5fedbba
test(document-cli): cover outline's own error paths and help text
Mearman Sep 14, 2026
e08a194
fix(document-cli): drop the redundant '-' guard in inferFormatFromExt…
Mearman Sep 14, 2026
c0385fa
test(document-cli): prove the empty/data:/scheme guards actually run
Mearman Sep 14, 2026
981f644
test(document-cli): cover docx-extras's own error path and signal thr…
Mearman Sep 14, 2026
0014808
test(document-cli): accept agreeing --out and positional destinations
Mearman Sep 14, 2026
c579bcc
test(document-cli): open a real .odb through openDocumentAtPath
Mearman Sep 14, 2026
e3b5f1b
test(document-cli): cover open-document's unrecognised-extension and …
Mearman Sep 14, 2026
bce4db6
test(document-cli): prove renderOdbReportTo threads its report name t…
Mearman Sep 14, 2026
d2c8546
test(document-cli): prove metadataFor dispatches to its own case's re…
Mearman Sep 14, 2026
37fe8e0
test(document-cli): isolate loadProvidedFonts's own abort-signal wiring
Mearman Sep 14, 2026
7bcb189
test(document-cli): type callback mocks as void-returning
Mearman Sep 14, 2026
d7e20d2
test(document-cli): cover the reducer's close/overlay/undo-cap/merge-…
Mearman Sep 14, 2026
6faae95
refactor(document-cli): collapse pushSnapshot's undo-cap branch into …
Mearman Sep 14, 2026
9469138
fix(document-cli): allow doc/xls/ppt documents to undo through their …
Mearman Sep 14, 2026
475454c
test(document-cli): cover PDF item actions and every reopenEditable f…
Mearman Sep 14, 2026
81ac620
test(document-cli): cover odb-to-xlsx, odb-to-csv, and odb-tables
Mearman Sep 14, 2026
afc361b
test(document-cli): cover pdf-inspect's default and --json report modes
Mearman Sep 14, 2026
16d8751
test(document-cli): cover from-package's output-conflict, csv/svg tar…
Mearman Sep 14, 2026
b87cb55
test(document-cli): cover cli-main's bare/tui/command dispatch
Mearman Sep 14, 2026
0c72145
test(document-cli): cover ADD_LIST_ITEM's docx/markdown paragraph-cop…
Mearman Sep 14, 2026
7e75a6a
test(document-cli): cover odm-to-pdf's --out flag and font-substituti…
Mearman Sep 14, 2026
567661c
refactor(document-cli): rebuild formula presets per-call instead of a…
Mearman Sep 15, 2026
d507d9d
test(document-cli): cover legacy binary and flowable formats in the f…
Mearman Sep 15, 2026
ad06cc8
test(document-cli): cover the ooxml-fixture test-support builders
Mearman Sep 15, 2026
8341421
build(document-cli): serialise mutation test runs and extend the dry-…
Mearman Sep 15, 2026
1416dc4
refactor(document-cli): rebuild the format-to-extension table per call
Mearman Sep 15, 2026
c6b2804
fix(document-cli): let Esc dismiss the slide-table-detail screen when…
Mearman Sep 15, 2026
a4d800d
test(document-cli): cover slide-table-detail's missing-table and edge…
Mearman Sep 15, 2026
d89fa8c
test(document-cli): cover paragraph-detail's own three guard fallbacks
Mearman Sep 15, 2026
5278f02
test(document-cli): cover UNDO's own per-format read-only warning
Mearman Sep 15, 2026
f0c3b55
test(document-cli): cover the odg page-vs-slide wording branch and re…
Mearman Sep 15, 2026
dd10a56
ci: resolve mutation-testing's --affected against the PR base, not th…
Mearman Sep 15, 2026
162f748
test(document-cli): cover mergePptxTableCells boundaries and the doc-…
Mearman Sep 15, 2026
3fd60ca
test(document-cli): assert exact status text for four out-of-range an…
Mearman Sep 15, 2026
3535468
test(document-cli): cover every remaining PDF field-edit wrong-kind b…
Mearman Sep 15, 2026
c156487
test(document-cli): cover INSERT_ODT_FORMULA's tree rebuild and wrong…
Mearman Sep 15, 2026
d498f18
test(document-cli): cover the odg ellipse/line/path vector-add cases
Mearman Sep 15, 2026
1e4d5ad
test(document-cli): cover CANCEL_QUIT and SAVE_ERROR
Mearman Sep 15, 2026
e931be3
test(document-cli): cover withShape's own odg page-vs-slide wording
Mearman Sep 15, 2026
816a56e
test(document-cli): cover the standalone status/stack/run actions
Mearman Sep 15, 2026
2ed76bf
test(document-cli): cover SET_TABLE_CELL_TEXT's happy path and both g…
Mearman Sep 15, 2026
edb9fef
test(document-cli): cover ADD_PDF_TEXT and INSERT_DOCX_FORMULA
Mearman Sep 15, 2026
39c7762
test(document-cli): cover six previously untested slide/page/shape ac…
Mearman Sep 15, 2026
5507a84
refactor(document-cli): drop the redundant markdown branch from APPEN…
Mearman Sep 15, 2026
eeba2cb
test(document-cli): close mutation-coverage gaps across reducer.ts's …
Mearman Sep 15, 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
2 changes: 2 additions & 0 deletions .github/workflows/mutation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ permissions:
env:
# Mirrors ci.yml's own TURBO_FLAGS exactly: a PR scopes to the packages it actually touches (and their dependents, via turbo's own dependency-aware --affected), main runs the whole workspace.
TURBO_FLAGS: ${{ github.event_name == 'pull_request' && '--affected' || '' }}
# Mirrors ci.yml's own TURBO_SCM_BASE exactly, and is required for --affected to mean anything on a pull_request: actions/checkout's detached-HEAD checkout has no local branch named `main` (only `origin/main`), so without an explicit base turbo cannot resolve the literal `main` ref it falls back to, warns "unable to detect git range, assuming all files have changed", and silently treats every package as affected -- confirmed directly (2026-09-15: a pull_request run's own Plan job logged that exact fallback and planned all 23 workspace packages into 8 shards for a PR whose diff touched a single package).
TURBO_SCM_BASE: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || '' }}

jobs:
plan:
Expand Down
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");
});
});
162 changes: 162 additions & 0 deletions packages/document-cli/src/cli-main.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import {
EXIT_INPUT_ERROR,
EXIT_SUCCESS,
EXIT_USAGE_ERROR,
} from "./runtime/exit-codes";
import * as programModule from "./program";
import { main } from "./cli-main";

// runTui itself (a real Ink render against a real terminal) is exercised by src/tui/*.test.tsx -- this file's own subject is cli-main.ts's dispatch logic around it: which of the three paths (bare invocation, an explicit 'tui' token, or an ordinary registered command) main() takes, how each computes the TUI's own startPath, TTY-gating, and how launchTui's own success/failure maps to an exit code. runTui is mocked throughout so no real Ink instance is ever rendered here.
const runTuiMock =
vi.fn<(options: { readonly startPath?: string }) => Promise<void>>();
vi.mock("./tui/index.js", () => ({
runTui: (options: { readonly startPath?: string }) => runTuiMock(options),
}));

describe("main", () => {
const originalArgv = process.argv;
const originalExitCode = process.exitCode;
const originalIsTTY = process.stdout.isTTY;
let stdoutSpy: ReturnType<typeof vi.spyOn>;
let stderrSpy: ReturnType<typeof vi.spyOn>;

beforeEach(() => {
runTuiMock.mockReset();
runTuiMock.mockResolvedValue(undefined);
stderrSpy = vi
.spyOn(process.stderr, "write")
.mockImplementation(() => true);
stdoutSpy = vi
.spyOn(process.stdout, "write")
.mockImplementation(() => true);
});

afterEach(() => {
process.argv = originalArgv;
process.exitCode = originalExitCode;
process.stdout.isTTY = originalIsTTY;
process.removeAllListeners("SIGINT");
vi.restoreAllMocks();
});

it("launches the TUI on a bare invocation when stdout is a TTY, with no start path", async () => {
process.stdout.isTTY = true;
process.argv = ["node", "document-cli"];

await main();

expect(runTuiMock).toHaveBeenCalledTimes(1);
expect(runTuiMock).toHaveBeenCalledWith(
expect.objectContaining({ startPath: undefined }),
);
expect(process.exitCode).toBe(EXIT_SUCCESS);
});

it("shows help and exits successfully on a bare invocation when stdout is not a TTY, without launching the TUI", async () => {
process.stdout.isTTY = false;
process.argv = ["node", "document-cli"];

await main();

expect(runTuiMock).not.toHaveBeenCalled();
expect(process.exitCode).toBe(EXIT_SUCCESS);
expect(stdoutSpy).toHaveBeenCalledWith(
expect.stringContaining("document-cli"),
);
});

it("refuses an explicit 'tui' invocation with a usage error when stdout is not a TTY", async () => {
process.stdout.isTTY = false;
process.argv = ["node", "document-cli", "tui"];

await main();

expect(runTuiMock).not.toHaveBeenCalled();
expect(process.exitCode).toBe(EXIT_USAGE_ERROR);
expect(stderrSpy).toHaveBeenCalledWith(
expect.stringContaining("requires an interactive terminal"),
);
});

it("launches the TUI for an explicit 'tui' invocation with a TTY, resolving the start path from the first non-flag argument", async () => {
process.stdout.isTTY = true;
process.argv = ["node", "document-cli", "tui", "--foo", "somefile.docx"];

await main();

expect(runTuiMock).toHaveBeenCalledWith(
expect.objectContaining({ startPath: "somefile.docx" }),
);
expect(process.exitCode).toBe(EXIT_SUCCESS);
});

it("launches the TUI for an explicit 'tui' invocation with no file argument, leaving the start path undefined", async () => {
process.stdout.isTTY = true;
process.argv = ["node", "document-cli", "tui"];

await main();

expect(runTuiMock).toHaveBeenCalledWith(
expect.objectContaining({ startPath: undefined }),
);
});

it("reports EXIT_INPUT_ERROR and the formatted error when runTui itself rejects with a framework-level failure", async () => {
process.stdout.isTTY = true;
process.argv = ["node", "document-cli"];
runTuiMock.mockRejectedValue(new Error("ink blew up"));

await main();

expect(process.exitCode).toBe(EXIT_INPUT_ERROR);
expect(stderrSpy).toHaveBeenCalledWith(
expect.stringContaining("ink blew up"),
);
});

it("dispatches an ordinary registered command through the assembled program rather than the TUI", async () => {
process.argv = ["node", "document-cli", "--help"];

await main();

expect(runTuiMock).not.toHaveBeenCalled();
expect(stdoutSpy).toHaveBeenCalledWith(
expect.stringContaining("Commands:"),
);
});

it("registers a 'tui [file]' subcommand on the assembled program that also launches the TUI", async () => {
process.argv = ["node", "document-cli", "tui-registration-probe"];
// dispatchToken is neither undefined nor "tui", so main() takes the else branch that registers 'tui [file]' on a fresh createProgram() result before parsing -- calling createProgram() directly afterwards, as this test does below, would build a SEPARATE program without that registration. Spy on it instead so this test observes the exact program instance main() itself builds and registers against.
const createProgramSpy = vi.spyOn(programModule, "createProgram");
await main();
const registeredProgram = createProgramSpy.mock.results[0]?.value as
ReturnType<typeof programModule.createProgram> | undefined;
if (registeredProgram === undefined) {
throw new Error("expected main() to have called createProgram()");
}

await registeredProgram.parseAsync([
"node",
"document-cli",
"tui",
"registered-file.docx",
]);

expect(runTuiMock).toHaveBeenCalledWith(
expect.objectContaining({ startPath: "registered-file.docx" }),
);
});

it("propagates a non-CommanderError bug from a registered action instead of swallowing it", async () => {
const brokenProgram = programModule.createProgram();
brokenProgram.command("boom").action(() => {
throw new Error("boom");
});
vi.spyOn(programModule, "createProgram").mockReturnValue(brokenProgram);
process.argv = ["node", "document-cli", "boom"];

await expect(main()).rejects.toThrow("boom");
});
});
Loading