Skip to content

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

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

test(document-cli): work toward a genuine 100% mutation score#1268
Mearman wants to merge 55 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.

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.
@Mearman
Mearman force-pushed the feat/100-percent-mutation-document-cli branch from 6514f98 to adf00cf Compare September 13, 2026 20:51
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.
…n dispatch

parseAsync's catch only rethrows a non-CommanderError; nothing proved
that rethrow actually happens, so an accidental swallow of a real
action bug would have gone unnoticed.
…xit-code mapping

Its --help description, --version output, and the exitOverride's
zero-vs-nonzero exitCode branch (EXIT_SUCCESS vs EXIT_USAGE_ERROR) were
all read only incidentally by other commands' own tests, never
asserted here where they're actually set.
…rectly assertable

ink renders an empty <Text>{undefined}</Text> identically to omitting
the node outright, so a rendered-frame assertion can never distinguish
"correctly omitted for no detail" from "always rendered, just empty
this time." Extracting the ternary into its own function makes that
distinction assertable on the return value directly. The Escape/Enter
dismiss handler is also now proven to ignore every other key.
Each screen's own mismatched-format message (naming the actual open
format, or 'no open document' when none is) had never been rendered by
any test in this suite.
…n as optional

Each Harness function conditionally returns undefined before the
document finishes opening, which the declared ReactElement return type
didn't allow.
Number.parseFloat already skips leading whitespace per spec, and a
blank or whitespace-only string parses to NaN either way, which
Number.isFinite already rejects -- the prior trim-and-length-check
branch never changed the return value for any input, so no test could
observe it.
Nothing captured the placeholder frame before the seeding effect runs,
and the effect's dependency array (tables/forms/reports/path) had
never been proven load-bearing against a genuine prop change on
rerender.
…ontent-kind guard

The blocks.filter predicate had never been proven to actually filter,
since every existing fixture's sole shape was a table with no sibling
paragraph blocks to wrongly include. The guard against readPptxContent
/readOdpContent ever resolving to a non-presentation ContentDocument
needed its own isolated, mocked file, since faking that return shape
is file-wide once documents.js is mocked.
…its state

The guard already proves field is defined by the time draft's initial
state is set, so the separate initialField-may-be-undefined fallback
was dead: stepIndex starts at the same index field itself reads on
that first render, so initialField and field were always identical or
both undefined, and the empty-fields case throws before draft's value
could ever be observed either way.
…-provider guards

Nothing proved cwd actually reaches createInitialState rather than
being dropped, or that useAppState/useAppDispatch genuinely throw
(with their own real messages) when called outside a provider.
…patch

Neither onCommit's SET_RUN_TEXT+POP_SCREEN pair nor onCancel's
POP_SCREEN had ever been exercised against a real open document, so an
accidentally emptied handler would have gone unnoticed.
…leaf bracket rendering

singleLineText's regex/trim pair had only ever seen already-clean,
single-spaced text, and no leaf had ever produced empty text to
exercise the '[kind]' bracket fallback -- an image with no alt text,
and the paragraph hosting it, both do.
…tested helper

Every ListView-driven screen inlined its own isSelected ? "cyan" : undefined
ternary for its selected row's colour, but ink's colour output collapses to
plain text under this suite's non-TTY test runner, so a render test could
never observe which colour string reached a Text component's colour prop.
Extracting it into list-view.tsx's new selectedColor(isSelected) makes it a
plain value a unit test can assert on directly, and removes two existing
tests (DiagnosticsPanel, RotationField) that asserted on a raw ANSI escape
code ink-testing-library never emits and were failing as a result.

Also covers ListView's own windowing and selection-index logic directly:
the viewport-clamping arithmetic that keeps a scrolled window from
overrunning the list's own end, and the isSelected flag threaded through
to each renderItem call.
…d-field edges

The one real .odb fixture always has exactly one form/report, a control
implementation on every control, a data source on every definition, and no
band with a genuinely empty attribute set -- so describeOdbForm/describeOdbReport's
singular-vs-plural branches, formControlLines/formDefinitionLines's several
field-omitted branches, and reportGroupLines's own zero-attribute case had
never run the other way. Covered with hand-built OdbForm/OdbReport/
OdbFormControl/OdbFormDefinition values, still pure data with no bytes or
I/O, specifically to reach those edges the one fixture's own shape cannot.
…mbering restart

None of these paths had ever been exercised: a footnote carrying a
type, partText's own walk into a table cell's paragraphs, two runs
joined within one paragraph, or a numbering level naming its own
restart target. Also drops numberingSection's redundant numeric sort
-- Object.keys already enumerates canonical-integer string keys in
ascending numeric order per ECMA-262, so the sort never changed the
result for any input.
… descriptions

Adds direct assertions against registerConversionCommands' registered
Command objects: each explicit per-pair command's own description
string, the --delimiter/--sheet/--page conditionals keyed on source
and target format, and the generic convert command's own description,
shared flags, and --to option text.
Adds a dedicated mocked-reader test file for the two branches no real
fixture reaches cleanly -- readDocumentMetadata returning no fields at
all (a fresh createDocx() always stamps created/modified, so the empty
LayoutMetadata path was unreachable through a real document) and the
reader throwing, proving the [metadata] prefix, non-verbose error
formatting, and exit-code mapping. Also asserts the command's own
description and --json option text, and the [metadata] prefix on the
existing unresolved-source-format case.
Adds one exact-text assertion per flag registered by commands/options.ts,
covering the description string mutation testing flagged as unobserved
by every existing behavioural test in this file.
…Signals

combineSignals' only call site always passes two freshly constructed
AbortControllers, so the `if (signal.aborted)` pre-check on each input
guarded a state neither can ever be in at that point -- dead defensive
code for a general-purpose combinator this module has no other caller
of. Also drops the redundant `{ once: true }` listener option: an
AbortSignal's own "abort" event is defined to fire at most once per
signal regardless.

Adds the SIGINT-side coverage the guard removal exposed as missing:
the interrupt error's own message, the combined signal aborting via
SIGINT when a timeout was also configured, and that the timeout timer
is actually unref'd.
…tion

100ms reliably let Ink's effect/listener flush on an otherwise-idle
machine but was repeatedly observed to lose the race once the host was
running this monorepo's full multi-package test suite under heavy
concurrent load, while the same test passed reliably run in isolation
-- proving the flake was starvation-induced, not a genuine race in the
component under test. 300ms costs nothing on the normal fast path and
meaningfully reduces exposure to the slow one.
…rror

Adds a dedicated mocked-extractor test file for the human-readable
report's own style-suffix rendering -- bold, italic, both, and neither
-- which no real embedded-font fixture in fonts.test.ts carries (every
fixture there embeds a plain regular face), plus the unrecognised-
extension branch runFonts short-circuits on before ever reaching
extractSourceFontsForFormat. Also asserts the command's own description
and --json option text.
Both are pure functions with every branch exercised only indirectly,
and weakly, through the CLI commands that call them. Testing them
directly pins resolveTargetFormat's four distinct outcomes (explicit
--to, --out's extension, an unresolvable destination, an unrecognised
extension) and formatError's verbose/non-verbose and has-stack/no-stack
combinations by their own exact output.
The existing target-unresolvable and source-unresolvable cases only
checked a loose substring, leaving buildConversionAction's own
conflicting-destinations message and both of resolveTargetFormat's
usage-error strings unobserved. Pins each to its exact full-line
output and adds the two cases -- an unrecognised output extension and
an unrecognised --to value -- neither previously exercised through the
generic convert command at all.
Adds the --chapters-dir-given-but-basename-missing case createResolveSubDocument's
own existsSync check guards (previously only the chaptersDir-undefined
path was exercised), pins the conflicting-destinations message to its
exact [odm-to-pdf]-prefixed text, and asserts the command's own
description and every registered option's help text.
Adds the conflicting-destinations, unresolvable-target, and
unresolvable-source cases -- none previously exercised through this
command -- each pinned to its exact [set-metadata]-prefixed message,
plus the command's own description, its addHelpText "after" content
(read via outputHelp() through a real --help run, since
Command#helpInformation() alone omits addHelpText content), and every
registered option's help text.
vitest.mutation.config.ts replaces vitest.config.ts's entire `test`
block rather than merging into it, which silently dropped the "unit"
project's own 10s testTimeout along with everything else it didn't
explicitly restate -- Stryker's dry run was running every test under
vitest's bare 5000ms default instead. Several Ink TUI tests drive
dozens of settle() calls in one test (a keypress-per-iteration loop),
and that total already exceeds 5000ms even at settle()'s own previous,
tighter wait -- a real regression this exposed once EFFECT_SETTLE_MS
widened: "XlsSpreadsheetGridScreen renders a double-letter column
address once the cursor passes column Z" now runs ~8.2s, comfortably
under 10s but well past vitest's own unconfigured default.
Adds the unresolvable-source, unrecognised-from, stdin-with-no-from,
and genuine-read-failure cases, pinning each to its exact
[outline]-prefixed message, plus the command's own description and
every option's help text. Cleans up the SIGINT listener each real
runOutline() call leaves behind (createRuntimeSignal registers one and
never removes it) so this file's now-larger real-invocation count
stays under Node's default MaxListeners and doesn't print a warning
into the captured stderr some of these tests assert is empty.
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