Skip to content

test(document-cli): work toward a genuine 100% mutation score - #1268

Draft
Mearman wants to merge 97 commits into
mainfrom
feat/100-percent-mutation-document-cli
Draft

Mearman wants to merge 97 commits into
mainfrom
feat/100-percent-mutation-document-cli

Conversation

@Mearman

@Mearman Mearman commented Sep 13, 2026

Copy link
Copy Markdown
Member

Adds unit tests for document-cli covering command dispatch (convert, odm-to-pdf), error description helpers, the async save-action wrapper, the global key table, the shared navigation-input hook, and shared PDF/ODB editor screen helpers.

Current state: work in progress toward a genuine 100% Stryker mutation score for this package (breakThreshold currently 32, derived from a 33.33% baseline of 6241 valid mutants). Not yet at 100%; more survived/no-coverage mutants remain to be addressed before breakThreshold can be raised further.

@Mearman
Mearman force-pushed the feat/100-percent-mutation-document-cli branch 2 times, most recently from 93330af to be0379d Compare September 14, 2026 18:46
createRuntimeSignal, readInput/writeOutput/resolveDefaultOutputPath,
loadProvidedFonts, the diagnostic/font-substitution reporters and
their raw-callback adapters, and the filesystem markdown image
resolver had no direct unit tests at all -- every branch in them
(SIGINT vs timeout precedence, stdin abort/non-buffer-chunk handling,
json/quiet interaction in both the reporter and the summary line, the
data:/scheme-URL/empty-destination guards) was reachable only through
whatever an integration-level command test happened to exercise, if
anything.
…ates

isEditableFormat/isWritableFormat and their document-level wrappers,
selectionKeyFor's per-screen-kind key derivation, selectedIndexFor's
absent-vs-zero distinction, currentScreen's empty-stack invariant,
rootScreenForFormat's per-format routing, and anyOverlayOpen's flag
enumeration had no direct unit tests -- every screen-component test
that happened to exercise one of these functions only ever hit
whichever branch its own scenario needed.
…-message test

FontSubstitution.reason is a closed union of "missing-face" |
"vendored-substitute" (document-schema.js's own font-port types) --
"style-fallback" was never a valid value and only typechecked because
the surrounding object literal had not yet been checked against it.
…on registrars

The formats command's own table/JSON output and trailing not-covered
line, and options.ts's addOutOption/addTimeoutOption/addJsonOption/
addQuietOption/addVerboseOption/addDumpPackageOption/addFontOptions/
addDelimiterOption/addSheetOption/addPageOption registrars, had no
direct tests -- each option's flag string, short alias, default value,
and coercion function were only ever reachable through whichever
command happened to apply that helper and whichever flag a downstream
test happened to pass.
formatSqlResultSetTable had no direct test: column-width derivation
(header vs longest cell), the two-space gap, trailing-padding trim,
the singular/plural row-count summary, and every ContentCellValue
kind's own display text were only reachable through a real .odb
fixture driven via the odb-query command.
…covers

The HsqldbSql*Error branch and the PdfEncryptedError/PdfParseError
branch each returned EXIT_INPUT_ERROR, identically to the function's
own final fallthrough -- every mutation of either condition still
produced the same exit code, since removing the branch entirely
changes nothing observable. Folded both into the fallthrough's own
comment instead of leaving an unkillable instanceof check standing,
and added the missing test for UnsupportedFontSourceFormatError's own
real EXIT_USAGE_ERROR branch plus direct tests for the three Hsqldb
SQL error classes now reached only through that fallthrough.
metadataFor's own switch had no direct test: every editor-backed
format's readXContent(doc.editor.toPackage()) call, doc/xls/ppt's and
pdf's direct doc.editor.metadata/doc.layout.metadata reads, every
read-only-preview format's identical doc.layout.metadata read, and
odb's own no-metadata-concept throw were only reachable through
whichever screen test happened to open that particular format. Also
adds a direct test for detectFormat, the one-line named seam over
inferFormatFromExtension.
renderOdbReportTo had no direct test: the destination-extension
dispatch to docx/odt/pdf, the unknown-report-name failure naming the
available reports, the extension-outside-docx/odt/pdf rejection, and
fontFiles threading through to the pdf branch alone were only
reachable through the odb-render-report CLI command's own tests,
which exercise a structurally similar but separate code path
(commands/odb.ts), not this TUI-side pipeline.
…ble helpers

layoutColorToHex/isValidHexColorInput/parseHexColorInput,
truncatePreview/parsePositiveIntField/parseNonNegativeIntField/
parseNumberField, and resolveSlideTable/slideTableCellText/
summarizeSlideTables had no direct tests -- byte-padding, the
zero-vs-negative-vs-fallback boundaries each numeric field parser
draws differently, the empty-preview marker, and the pptx/odp content
dispatch (plus its out-of-range slide/table-index cases) were only
reachable through whichever screen-component test happened to render
a matching scenario.
parseColorField/parseStrokeField/defaultTriangleSubpaths and the six
FORMULA_PRESETS entries had no direct tests -- the component-count and
finite-value guards each parser draws, and every MathML string literal
and tree shape in the presets, were only reachable through whichever
odg/pptx/docx screen test happened to type a matching value.
odsDocument, resolveSheet, sheetExtent's floor/derive-from-cells/
derive-from-declared-rows-columns branches, cellKey, cellLookup,
rawEditableText's per-kind rendering, inferKind's boolean/number/
string classification, and buildCellValue's per-kind parse-or-reject
logic had no direct tests -- each was only reachable through whichever
combination a spreadsheet-grid or cell-detail screen test happened to
drive.
requirePdfDocument/isEditablePdfDocument, formatSize/formatPt/
formatColor/formatStroke, parseRequiredColorField's fallback,
parseFontWeight/parseFontStyle, parseOptionalNumberField's blank-to-
undefined behaviour, defaultTriangleLayoutSubpaths, inferImageFormat,
and requireOdbDocument had no direct tests -- each was only reachable
through whichever pdf/xlsx/csv/svg/rtf/wpd/epub or odb screen test
happened to exercise a matching branch.
…ion-input hook

GLOBAL_KEYS' own literal binding list, and useNavigationInput's
clamped up/down/page/home/end movement, its Enter/Right/l select
(including the itemCount === 0 no-op), its Escape/Left/h back call,
and its optional onAppend branch had no direct tests -- reachable
before only through whichever screen-component test happened to send
a matching key sequence to whichever list screen it was testing.
…pper

describeError's Error-vs-non-Error branches and saveOpenDocumentAction's
SAVE_SUCCESS/SAVE_ERROR mapping (including the error message actually
naming the destination path) had no direct tests -- only reachable
before through whichever app-shell-level scenario a screen test
happened to drive as far as an actual save attempt.
runGenericConvert's .odm/.odb rejection (each naming its real
alternative command), the unresolvable-source and unresolvable-target
failures, and --to winning over the output path's own extension had
no direct test -- only the csv/svg selection-flag threading through
this same command was previously exercised.
commands/odm.ts had no test at all: chapter resolution via
--chapters-dir (matched by href basename) and via an explicit
--chapter href=file override, the unresolved-chapter failure naming
both flags, the malformed --chapter InvalidArgumentError, the
conflicting-destination usage error, and the --json result summary
were all unreachable. Adds a minimal hand-authored .odm fixture
(test-support/odm-fixture.ts, mirroring documents.js's own internal
odm test-support) since no .odm writer exists anywhere in this
ecosystem to build one with.
…eens

Adds ink-testing-library coverage for XlsSheetListScreen (default sheet
render, add-sheet prompt including the blank-name warning and Escape
cancel, and pushing the spreadsheetGrid screen) and
XlsSpreadsheetGridScreen (hjkl/arrow cursor movement with origin
clamping, double-letter column addressing past Z, editing an existing
versus an empty cell, cancelling an edit, popping the screen on Escape,
the compact non-empty-cells toggle, and the missing-sheet guard).
Adds ink-testing-library coverage for PptSlideListScreen (the empty
presentation message, adding a slide via ADD_SLIDE, and pushing
slideDetail on Enter) and PptSlideDetailScreen (shape geometry/text
rows, the empty-shape placeholder, the trailing notes row and its
default/populated text, the shape text editor's commit and cancel
paths, the "n" notes hotkey and selecting the notes row directly, the
add-text-box field wizard's default-accepting happy path and its
Escape cancel, the missing-slide guard, and popping the screen on
Escape while browsing).
Adds ink-testing-library coverage for CommandPalette: the full command
list and its live filtering, exact-name-over-prefix resolution,
unknown-command warnings, and every :save/:saveas/:export/:new/:open/
:close/:undo/:view-source/:help/:quit branch, including real
round-trips through the filesystem (save, export to PDF, open) and
their failure paths. A harness mirrors app.tsx's own overlay wiring
(mount only while the overlay is open, reopen on ":") so each command
starts from the palette's real closed-to-open lifecycle rather than a
permanently mounted instance.
overlays and the status line

Adds ink-testing-library coverage for six previously untested shared
components: ConfirmDialog (every y/Y/Enter/n/N/Esc branch, and no-op on
an unrelated key), HelpOverlay (renders every GLOBAL_KEYS row, closes
on Esc/?/Enter), DiagnosticsPanel (page-scoped vs plain diagnostic
formatting, the empty-list message, dismissing the selected entry,
closing the panel), ErrorDetail (message plus optional detail line,
dismiss on Esc/Enter, the empty-box case with no error set), and
SearchOverlay (live query updates, keep-on-submit vs clear-on-cancel,
starting pre-filled). StatusLine gets its own statusColour helper
exported for a direct unit test (ink strips ANSI colour codes from a
non-TTY render, so a rendered frame can never distinguish colours) plus
real-time-driven coverage of its transient-status TTL: an info/warning
status clears itself after TRANSIENT_STATUS_TTL_MS (now exported so the
test derives its wait from the real constant) and an error status never
does.
The prior 6-second buffer over the transient-status TTL occasionally
missed the frame update under heavy concurrent CPU load on this shared
machine, since the effect's real setTimeout can fire well past its
nominal delay when the process is starved of CPU time. Widen the
margin so the wait comfortably outlasts realistic scheduling delay.
Multiple concurrent checker/test-runner processes were crashing with
SIGSEGV under this shared machine's own heavy concurrent load. Pinning
concurrency to 1 trades throughput for a run that actually completes.
loadProvidedFonts, readInput, and renderOdbReportTo each forward
options.signal into node:fs/promises' readFile, but nothing exercised
the signal actually reaching that call: an already-aborted controller
now causes each of them to reject instead of silently reading the file
to completion.
… and its local query echo

TextField only wires its Escape-to-cancel useInput handler while
isFocused is true; nothing exercised the unfocused case, where Escape
must reach neither onCancel nor anything else. SearchOverlay separately
mirrors every keystroke into its own local `query` state (rendered by
its child TextField) as well as into dispatched state -- only the
dispatched half had a covering assertion, leaving the local echo free
to silently stop updating without any test noticing.
… colour and its --help text

DiagnosticsPanel colours only the selected row's Text cyan; nothing
distinguished that from every row (or none) getting the same treatment.
The formats command's own --help text was likewise never read back
anywhere, leaving its description and --json option strings free to
change without any test noticing.
…idation's

isValidHexColorInput's internal trim already covered the validation
half; a leading/trailing-whitespace hex string previously reached
rgbHexToColor untrimmed once past validation, relying on trim only
being read, never proven load-bearing at the point that actually
matters.
…/pptx slide-list guard throws

RotationField's non-editing row colours itself cyan only while
selected, mirroring the same pattern already covered elsewhere in this
suite. OdpSlideListScreen and PptxSlideListScreen each guard against
rendering with the wrong (or no) open document, a throw ink's own
render() swallows entirely via its no-op onUncaughtError callback --
renderToString propagates it instead, so it is the only way to prove
these guards actually fire.
…own live-view editors

UNDO's own format switch still treated doc/xls/ppt as the read-only,
no-live-editor formats they used to be, so a document opened as one of
these accumulated real undo snapshots via mutate() on every edit but
could never pop one back off -- UNDO reported "read-only, so it has no
history to undo" even immediately after a genuine, undoable edit. These
three formats gained real live-view editors and their own reopenEditable
case in the same change that widened EditableOpenDocument to include
them; the UNDO exclusion list was never updated to match.
…ormat branch

Adds reducer-level coverage for the PDF ADD_PDF_ELLIPSE/LINE/PATH/IMAGE/
LINK actions, field edits on every non-text PDF item kind (rect, ellipse,
line, path, image, link) including their own wrong-kind warning paths,
the remaining SET_PDF_TEXT_* fields and the underline toggle's on/off
round trip, and reopenEditable's pptx/odt/odp/ods/odg/doc/xls/ppt undo
branches -- previously only docx and pdf were exercised there.
These three commands had no test coverage at all: default and explicit
output path resolution, the --out flag, the conflicting-destination
usage error (including the equal-values non-conflict case), --table
selection and its not-found error, and odb-tables' plain-text and
--json report forms.
Only --full had any coverage. Adds the plain-text report (page count
singular/plural, the per-page item-kind histogram and its empty-page
omission, the metadata and images-by-format sections and their
all-empty omission), the --json summary shape, and the non-PDF-input
error path.
…gets, and version-gate errors

Adds coverage for the positional/--out conflict and agreement checks,
the --out flag on its own, a genuine SchemaVersionMismatchError (a
document-tree.schema.json dump pinned to a major other than the
installed one, distinct from the rename/demotion tombstones already
covered), invalid-UTF-8 input bytes, and the csv/svg target branches
with --delimiter/--page threaded through, plus --json/--quiet output.
cli-main.ts had no test coverage at all. Adds the bare-invocation TTY
gate (launches the TUI vs. shows help), the explicit 'tui' token's own
TTY refusal and start-path resolution (skipping leading flag tokens),
launchTui's own error-to-exit-code mapping, and both the registered
'tui [file]' subcommand and the CommanderError-vs-genuine-bug split in
the ordinary command dispatch path.
…y branch

The odt branch (a real OdtList) already had its own describe block;
this action's other branch -- appending a new paragraph and copying the
anchor paragraph's own list membership, the docx/markdown path with no
separate list object -- had no dispatch anywhere, including its two
warning paths (no paragraph at blockIndex, and an anchor not part of a
list).
…on reporting

--out on its own (only the positional-vs-flag conflict was covered
before) and the reportFontSubstitution branch (a Calibri-styled chapter
substituted to the vendored carlito face under
--report-font-substitutions, and silent without it), mirroring the
identical scenario convert-fonts.test.ts already covers for the other
<format>-to-pdf commands.
… module constant

Stryker's per-test coverage instrumentation attributes a module-scope
constant's one-time initialisation to whichever test happens to trigger
the very first import of the module across the whole suite, since module
caching means every later importer just reads the already-built array.
A mutant in the literal was therefore only ever re-verified against that
unrelated first-importing test, never against formula-presets.test.ts's
own assertions. Rebuilding the array inside a function makes each call
site's own execution the thing coverage attributes to, so this file's
exhaustive tests are what actually re-run against a mutant.
…ormat table

isDocumentFormat, inferFormatFromExtension, and formatToExtension were
only exercised for the earlier office/markup formats; wpd, doc, xls,
ppt, and epub had no case asserting their own membership, extension
inference, or round-trip extension.
xmlDeclaration, txt, and el had no direct test of their own; every
existing use only exercised them indirectly through the fixtures that
call them, leaving each builder's own default-argument and attribute-
ordering behaviour unasserted.
…run timeout

document-cli's TUI suite spawns and manages child processes and pseudo-
terminals; running mutants at the default concurrency let sibling
workers race over the same terminal/process resources and produced
flaky, environment-dependent failures unrelated to the mutated code.
Concurrency of 1 and a longer dry-run timeout make a run reproducible
at the cost of wall-clock time.
FORMAT_TO_EXTENSION was a module-scope object literal, so Stryker's
per-test coverage instrumentation attributed its one-time initialisation
to whichever test happened to trigger the first import of format.ts in
the whole suite, never to the tests that actually call formatToExtension
with a given format. A mutated extension value therefore survived even
though formatToExtension's own exhaustive test asserts on every entry.
Rebuilding the table inside a function called on every lookup makes each
call site's own execution the thing coverage attributes to.
… its table is gone

The useInput handler returned before reaching any key check whenever
resolveSlideTable came back undefined, so the fallback view's own
"press Esc to go back" text was a lie: no key, Escape included, ever
did anything. Esc now still pops the screen in that state; every
other key remains a no-op since there is no grid left to act on.
…-clamp paths

Adds coverage for branches no existing test touched: the
table-no-longer-exists fallback (and that Esc, uniquely, still works
there), cursor clamping at every edge of the grid via individual
h/j/k/l presses, and committing a pending merge with Enter rather
than a second 'm'.
Nothing exercised the outside-screen guard, the no-open-document
guard, or the out-of-range blockIndex guard -- all three early
returns above the screen's real render path were dead as far as the
suite could tell.
UNDO's read-only branch names one of seven formats (odb/xlsx/csv/svg/
rtf/wpd/epub) in its warning text, but nothing dispatched UNDO against
any of them -- every existing undo test opened an editable format
instead, leaving the whole seven-way format check and its templated
message unexercised.
…maining format/kind guards

Covers withShape's "page" wording for an odg missing-shape warning, previously only exercised
via the pptx "slide" branch, each of the nine formats OPEN_FILE_SUCCESS names individually in
its read-only-PDF-preview note, and the wrong-kind guards for SET_PDF_INTERNAL_LINK_DESTINATION
and SET_PDF_INTERNAL_LINK_FRAME.
…e literal 'main'

actions/checkout leaves a detached HEAD with no local main branch (only origin/main), so
turbo's --affected fell back to "assume all files have changed" on every pull_request run,
sharding the entire 23-package workspace regardless of how narrow the actual diff was.
Setting TURBO_SCM_BASE to the PR's base commit sha, mirroring ci.yml's own working setup,
lets turbo resolve the real diff range instead of guessing.
…format union arms

Adds exact-boundary tests for mergePptxTableCells's row/column overrun checks (a merge
landing exactly on the table's last row/column must succeed, not throw), direct hMerge/
vMerge attribute assertions distinguishing the row-loop and column-loop merge flags from
each other, and loop-boundary checks proving cells past rowSpan/colSpan are never touched.

Also covers wrongDocument's "no document" wording when nothing is open at all, and the
doc-format arm of wordprocessingDocument/styledWordprocessingDocument, which nothing else
in this suite previously exercised through APPEND_PARAGRAPH or TOGGLE_RUN_BOLD.
…d wrong-doc-type warnings

Pins the literal warning text for the missing-paragraph-index, missing-page-index, and
markdown-wrong-document-type branches so a StringLiteral mutant on any of these messages
fails the assertion instead of surviving on severity alone.
…ranch

withPdfItemMatching's kindLabel string is a separate literal at every call site, so the
existing FRAME/FILL/one-per-kind wrong-kind tests only killed the mutant at their own call
site. Adds a data-driven case per action that had none (text's own text/position/color/
width/underline setters, rect's stroke, ellipse's frame/stroke, all three line setters,
path's fill/stroke, image's frame/source, and link's frame), each asserting the exact
kindLabel substring so a StringLiteral mutant on any of them fails the assertion.
…-doc guard

mutableMathMlNode had no coverage at all before this: an element/text node is rebuilt as a
fresh mutable tree so document-schema.js's mutable MathMlNode type-checks with no cast, and
the four kinds MathML content never carries (cdata/comment/declaration/pi) collapse to the
documented empty stand-in each schema variant requires. Reads the round-tripped formula back
through readOdtContent/formulaOfBlock to assert on the real embedded package, plus a second
case for the wrong-document-format warning.
The odg branch's own inner switch (addRect/addEllipse/addLine/addPath) had only ever been
reached via ADD_RECT; each case is a separate switch-statement mutant, so nothing proved
removing the ellipse/line/path cases would still pass. Adds each kind through the page's
own vectors() accessor, mirroring the equivalent odp coverage already in this file.
CANCEL_QUIT had no coverage at all -- its own confirmQuit-overlay-false write and
isExiting-stays-false behaviour were unproven, and CONFIRM_QUIT's sibling write to the same
overlay field was reached but never asserted. SAVE_ERROR likewise had no test: it now asserts
the exact error message surfaces as status text with hasUnsavedChanges left untouched.
withWideShape's identical ternary was already covered via SET_SHAPE_TEXT, but withShape is a
separate function with its own copy of the same literal, reached only by SET_SHAPE_ROTATION.
Adds the missing odg case so removing withShape's own page/slide branch fails too.
OPEN_FILE_ERROR, SAVE_AS_REQUEST, SET_SEARCH_QUERY, CLEAR_STATUS, DISMISS_ERROR_DETAIL and
SET_RUN_TEXT had no coverage anywhere in the suite -- each is a plain state-field write with
no format branching, so one direct dispatch per action proves the write.
…uards

Nothing in the suite ever dispatched SET_TABLE_CELL_TEXT before this: the missing-table and
missing-cell warnings, and the actual cell-text write itself (verified through a real
readDocxContent round trip), all had zero coverage.
ADD_PDF_TEXT (add a second real text item, round-tripped through toBytes()/openPdf()) and
INSERT_DOCX_FORMULA (both the written-OMML happy path, read back through readDocxContent's
own embedded-formula splice, and the no-OMML-content warning branch) had no coverage
anywhere in the suite before this, plus INSERT_DOCX_FORMULA's missing-paragraph and
wrong-document-format guards.
…tions

None of ADD_SLIDE, ADD_PAGE, ADD_TEXTBOX, ADD_IMAGE, SET_SHAPE_FRAME or
SET_SHEET_PRINT_SETTINGS had any coverage anywhere in the suite. Adds a real happy-path
dispatch per action on pptx and/or odg, plus the missing-page/slide and wrong-document-format
guards for the two dual-branch actions (ADD_TEXTBOX, ADD_IMAGE).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant