test(document-cli): work toward a genuine 100% mutation score - #1268
Draft
Mearman wants to merge 55 commits into
Draft
test(document-cli): work toward a genuine 100% mutation score#1268Mearman wants to merge 55 commits into
Mearman wants to merge 55 commits into
Conversation
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
force-pushed
the
feat/100-percent-mutation-document-cli
branch
from
September 13, 2026 20:51
6514f98 to
adf00cf
Compare
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.
This reverts commit 50493de.
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.