Skip to content

test(ooxml.js): work toward a 100% mutation score - #1259

Draft
Mearman wants to merge 102 commits into
mainfrom
feat/100-percent-mutation-ooxml.js
Draft

Mearman wants to merge 102 commits into
mainfrom
feat/100-percent-mutation-ooxml.js

Conversation

@Mearman

@Mearman Mearman commented Sep 12, 2026

Copy link
Copy Markdown
Member

Part of the workspace-wide effort to bring every package's Stryker mutation score to a genuine 100% with zero disable comments (see the sibling PRs already merged for byte-codec, excel-number-format, document-compute.js, pdf-raster-cpu).

Measured baseline for ooxml.js: 65.05% of 7126 valid mutants (killed 4604, timeout 31, survived 2081, no-coverage 410).

This PR is a work in progress. It has landed, in order:

  • The package's smaller, previously-untested foundation modules: base64, XML build/parse, the XmlNode structural guard, package classification, image signature sniffing.
  • A batch of small-to-medium gaps across xlsx/docx/pptx typed modules: shading's unrecognised-fill-kind guard, figure-caption association's image gate and run-join separator, numbering's non-canonical sort keys and undefined-level handling, threaded-comment id sequencing, table-relationship type filtering, the embedded-object decode's root-entry precedence and Package-stream-by-name lookup, and pptx inherit's relationship-type filtering, placeholder idx/type fallback, and run-property/level-clamp coverage.
  • Full, direct structural coverage for the embedded-object test fixture builder (test-support/cfb.ts), including byte-level assertions for header fields archive-codec's own reader deliberately never cross-checks, and a fixture sized to the builder's own one-FAT-sector boundary.
  • xlsx/defined-names.ts and xlsx/data-validation.ts brought to 100% from zero direct coverage, plus a handful of redundant-guard removals proven equivalent by hand-mutation (a name-then-type check that already excluded an absent name, a no-op ternary branch, an indexOf/slice split removing a possibly-undefined array index, a letters regex made redundant by columnLettersToIndex's own character-class check, and a print-area builder rewritten to construct its dollared reference directly from row/column indices instead of formatting then re-parsing a plain reference through a regex).
  • xlsx/print-settings.ts brought to 100%: margin/break/scale coverage plus one redundant scale-presence guard removed (Number(undefined) is NaN, already caught by the isFinite check beside it).
  • docx/numbering.ts, pptx/chart.ts, and pptx/diagram.ts brought to 100%: direct unit coverage for readChartTable/readChartResidue and readDiagramText/readDiagramResidue (previously exercised only indirectly through full xlsx/pptx round trips), plus the numbering override's no-w:val case.
  • docx/styles.ts brought to 100%: style-cascade type discrimination (a same-styleId style of the wrong type, a default-style flag on the wrong type), the default paragraph style's own w:pPr merge, strike's basedOn inheritance, majorAscii/minorAscii alongside their HAnsi spellings, an unrecognised asciiTheme resolving to no font, a bare w:u with no w:val, w:ind/@w:start as w:left's fallback, "distribute"/"atLeast" alongside their siblings, a themeTint byte with stray characters before or after its two hex digits, plus one redundant guard removed (readToggle's absent-value check, subsumed by the three inequality comparisons beside it).
  • pptx/reading-order.ts from 74.63% to 89.47%: an exact axis-ratio tie breaking to rows, overlapping shapes whose primary/secondary sort keys disagree, a row needing its own internal column cut once split out, extentAlong's true-span computation, and two provably-redundant guards removed (a shapes.length<=1 early return the algorithm's own fallback already makes a no-op, and a columns.groups.length>1 check implied by the ratio comparison already being positive). Three touching-boundary mutants in splitOnGap remain genuinely difficult to distinguish through the exported function's output alone and are left for a follow-up pass.

Current measured score: 72.05% of the package's valid mutants (up from the 68.17% this PR previously reported, and the 65.05% original baseline). Zero Stryker disable comments anywhere in the package (grep -rn "Stryker disable" src/ returns no matches).

Files now at a genuine 100% mutation score in this PR: util/base64.ts, xml/build.ts, xml/parse.ts, model/node.ts, typed/util.ts, typed/xlsx/util.ts, typed/xlsx/defined-names.ts, typed/xlsx/data-validation.ts, typed/xlsx/print-settings.ts, typed/docx/numbering.ts, typed/docx/shading.ts, typed/docx/styles.ts, typed/pptx/chart.ts, typed/pptx/diagram.ts, typed/pptx/inherit.ts, typed/xlsx/serial.ts, typed/xlsx/definitions.ts, typed/xlsx/rule-residue.ts, typed/xlsx/comments-write.ts, typed/xlsx/shared-strings.ts, typed/xlsx/sqref.ts, typed/xlsx/units.ts, typed/shared/units.ts, typed/embedded.ts, typed/document-tree.ts, typed/figure-captions.ts, plus image/sniff.ts and package-io/read.ts/write.ts.

Every fix is either a real test proving a genuine behavioural difference, or a small refactor removing code whose mutation was verified equivalent by hand (mutating the source directly and confirming the existing suite still passed before the fix, and failed after it) -- never a Stryker disable comment.

Substantial work remains: the package's largest modules are still far below 100% -- typed/docx/write.ts (57.81%, 2124 lines), typed/docx/read.ts (75.98%, 1896 lines), typed/xlsx/build.ts (33.58%, 1235 lines), typed/pptx/read.ts (77.23%, 1048 lines), typed/xlsx/conditional-format.ts (62.09%), typed/xlsx/styles.ts (68.83%), typed/xlsx/content.ts (57.00%), typed/xlsx/drawings-write.ts (40.82%), typed/xlsx/drawings.ts (65.13%), typed/xlsx/comments.ts (75.00%), typed/docx/constructs.ts (80.77%), typed/pptx/reading-order.ts (89.47%, close), typed/shared/color.ts (66.42%), typed/shared/drawingml.ts (76.88%), typed/shared/metadata.ts (76.00%), typed/compact.ts (47.95%), typed/xlsx.ts (60.00%), typed/xlsx/definitions-write.ts (54.00%), and test-support/embedded.ts (65.00%). stryker.config.ts's breakThreshold is left at its original measured-baseline value (63) rather than raised, since the package is not close to the genuine 100% that value would need to reflect.

Left as a draft while this continues.

@Mearman
Mearman force-pushed the feat/100-percent-mutation-ooxml.js branch 5 times, most recently from 3c46d9b to 856c7dc Compare September 16, 2026 01:00
…code buffer sizing

Adds direct coverage for bytesToBase64/base64ToBytes across every
input-length remainder (0, 1, 2 bytes past a full 3-byte group), the
invalid-base64 throw for each of the two positions a malformed
character can occupy in a 4-character group, and whitespace stripping
before decode.

base64ToBytes now builds its output as a plain number[] converted via
Uint8Array.from rather than pre-sizing a Uint8Array from a `len * 3 /
4` estimate: that estimate is only ever an upper bound, so any formula
that never under-counts is behaviourally identical to any other once
the result is trimmed to its real length -- removing the sizing
arithmetic as an AST node rather than leaving an unobservable estimate
for a mutation to hide behind.
…lder scaffolding

Adds direct coverage for buildXml across every XmlNode variant (text,
comment, cdata, pi, declaration, attribute-less and attributed
elements, nested children, multiple root nodes) and for
assertBuiltString's own throw, extracted from buildXml so the "did the
builder return a string" guard is directly testable with a non-string
literal rather than left uncovered forever (XMLBuilder, given this
module's fixed options, never actually returns anything else).

Simplifies two spots verified directly against fast-xml-parser to be
unobservable: a processing instruction's and a declaration's own child
array is never rendered by the builder under this configuration (`{
"?custom": [{ "#text": "x" }] }` and `{ "?custom": [] }` build to the
byte-identical `<?custom?>`), so neither carries a value the builder
ever reads; and an element's own `:@` attributes object is set
unconditionally rather than gated on whether any attribute exists,
since an empty `:@": {}` builds identically to the key being absent
and parseAttributes already reads both back to the same empty array.
Exports and directly unit-tests every one of parseXml's own structural
guards and error paths (isRecord, isUnknownArray, asString, parseNodes,
parseNode, parseAttributes, scalarText) against synthetic
fast-xml-parser-shaped input: a node that is not an object, a node
with no tag key or more than one, an attribute value or scalar-text
wrapper of the wrong shape. Real fast-xml-parser output never produces
these malformed shapes, so none of these branches was ever exercised
through parseXml's own public entry point alone.
…variant

Adds direct coverage for isXmlNode's own structural guard across
non-record inputs (null, an array, a primitive -- each a distinct
branch of typeof/null/Array.isArray that real Zod-validated input
never separately exercises), every XmlNode variant's own required
fields, malformed attribute entries, and a recursive check that a
child element's own children are validated the same way rather than
only its own direct fields.
…edundant bounds check

Adds direct coverage, via packageFromEntries's own xml/binary
classification, for a UTF-8 BOM prefix (alone and combined with
leading whitespace), every individual whitespace byte the format
permits, a run of several in a row, an all-whitespace part with no
non-whitespace byte at all, and a part whose first three bytes only
partially match the BOM (isolating each of the three signature bytes'
own necessity) -- none of which any existing test exercised.

Drops looksLikeXml's own `bytes.length >= 3` BOM guard: it is provably
redundant given how out-of-range Uint8Array indexing behaves -- an
index at or past a real array's own length always reads `undefined`,
which can never equal a real BOM byte, so a short array already fails
the byte-by-byte comparison on its own. The main scan loop is likewise
rebounded on `bytes[i] !== undefined` rather than a separately tracked
`i < bytes.length`, for the identical reason.
…ant bounds check

Adds direct coverage for sniffImageFormat across every recognised
signature (PNG, JPEG, both GIF header versions), near-miss prefixes
that diverge partway through or on the final byte, and SVG detection
by its own XML-prolog and bare-root-tag spellings, leading whitespace
before either, and the 1024-byte sniff window's own boundary (a real
'<svg' tag placed well past the window must not be found there).

Drops startsWith's own `bytes.length < signature.length` guard: it is
provably redundant given how out-of-range Uint8Array indexing behaves
-- an index at or past a real array's own length always reads
`undefined`, which can never equal a real signature byte, so a shorter
array already fails the byte-by-byte comparison on its own.
Adds direct unit coverage for relsPathFor (a slash-free part path, and
a nested one where only the LAST slash may split it) and
resolveRelTarget (a package-rooted target, a relative target against
both an empty and a real subject directory, a '../' segment popping
the enclosing directory, a '.' segment, and a doubled-slash empty
segment) -- neither function was reachable from any existing test
except through a much larger relationship-resolution fixture that
never varied these specific shapes.
… redundant date checks

Adds direct coverage for serialToIsoTime/serialToIsoDateTime's own
non-finite and negative-serial rejections, and for
utcMsOfCalendarDate's own year/month rollover rejections -- including
a day value large enough to roll a whole leap year forward, the one
shape that makes the year check's own necessity observable (the
public isoDateToSerial entry point never passes a day outside 0-99,
which alone never triggers it).

isoDateOfDayCount now switches on the sign of the offset from the
phantom leap day rather than pairing an equality check (excluding day
60 itself) with a separate `<` comparison against the identical
threshold: with 60 excluded by the `0` case, the remaining two cases
are Math.sign's only other outputs, leaving no inequality boundary for
a mutation to hide behind.

utcMsOfCalendarDate drops its own third, day-level equality check:
Date.UTC(year, month-1, day) maps onto exactly one real calendar date,
so whenever a re-read year and month both already match what was
asked for, day is necessarily inside that month's own valid range and
is therefore already forced to match too (confirmed by exhaustive
search over every realistic year/month/day combination) -- a third
check here could only ever restate a fact the first two already
guarantee.
…space split

Adds direct coverage for parseSqref (absent/empty input, a single bare
cell, a real span, several ranges, a malformed token skipped among
well-formed ones), formatSqrefRange (bare cell vs. row-only vs.
column-only vs. full spans), and formatSqref's own join -- none of
which this shared helper had a dedicated test file for at all.

Simplifies the token split from `/\s+/` to `/\s/`: splitting on each
individual whitespace character rather than a run of them only ever
inserts extra empty strings between adjacent whitespace characters,
which the loop's own `token === ""` skip already discards, so both
forms produce the identical final token list regardless of how many
consecutive whitespace characters separate two ranges.
… directly

Adds a dedicated test file for the xlsx rule-residue helpers: capturing
zero, some, and every attribute as unmanaged, and reading residue back
for an absent source, a wrong-format source, a source that fails to
parse as exactly one element, and one whose tag mismatches the
expected rule kind -- none of which had direct coverage before.
Adds a dedicated test file: an absent sharedStrings part reads back as
exactly an empty array (not a placeholder value), multi-run <si>
entries concatenate in document order, and SharedStringTable assigns
sequential indices while deduplicating a value interned twice.
…re no tables

A plain property read cannot distinguish a genuinely absent key from
one spread on with an explicit undefined value -- both read back as
undefined. Adds an Object.hasOwn check alongside the existing
toBeUndefined() assertion so readXlsx's own conditional spread is
actually exercised, not just its value.
… directly

Adds a dedicated test file: a sheet with no table relationship at all
reads no definitions, a non-table relationship among several is
skipped in favour of the genuine table one, and a table part missing
its own name or ref attribute is skipped rather than promoted with a
missing field.
…at all

Adds a case where neither of an image's own neighbours is a paragraph
(two more images either side), which no existing fixture in this file
exercised -- every prior case had at least one paragraph candidate,
matching or not.
…r patterns' own absent key

Adds a "none" w:fill and a "none" w:color case (only "auto" was
previously exercised for either), and strengthens the existing
single-colour pattern tests with an Object.hasOwn check: a plain
toEqual cannot distinguish an omitted foregroundColor/backgroundColor
key from one spread on with an explicit undefined value, so a genuinely
one-sided pattern read needs the stricter check to prove the other
key is truly absent.
…nt, and reply linkage

Adds a dedicated test file: threadedCommentId's own uppercase-hex
formatting (a counter of 10 exercises the digit-vs-letter distinction
0-9 alone cannot), sequential ids increasing across two separately
commented cells (not just within one thread), a reply immediately
following its own root with the root's real id as parentId while the
root itself carries none, and the threaded-comments root's own
declared namespace.

Exports threadedCommentId, previously module-private, purely for this
direct coverage.
… a dead type-narrowing check

Adds a test at exactly the half-point tolerance boundary (not just
comfortably inside it), and four tests each isolating one dimension's
own necessity in pageSizeToPaperSizeCode's Letter/A4 checks (a width
match with a mismatched height, and vice versa, for both page sizes) --
none of which any existing test distinguished from the other.

parseUniversalMeasureToPt no longer runs an `amountRaw === undefined ||
unit === undefined` check after a successful regex match: neither of
UNIVERSAL_MEASURE_RE's two capture groups is optional (neither has a
trailing `?`), so a successful match always populates both --
TypeScript's own RegExpExecArray typing just cannot express that a
specific pattern's own groups are mandatory. Non-null assertions state
that directly instead of a runtime check no real regex match can ever
fail.
…and numeric level ordering

Adds a w:startOverride whose own ilvl names a level the base
abstractNum never defined (must be skipped, not fabricated), a
declared-namespace assertion for the built w:numbering root, and a
level ordering case proving ilvl sorts numerically ('10' after '2'),
none of which the existing round-trip-only fixtures distinguished from
a passing but coincidentally-correct result.
readEmbeddedOoxmlPayload's outer catch swallows a wrongly-detected
flavour's own read failure exactly as gracefully as a genuinely
undetected one, so testing hasDocxBody/detectFlavour only through that
public entry point cannot tell "correctly found no flavour" apart from
"wrongly matched one, then threw reading it" -- both produce the same
undefined result. Exports both functions and adds direct coverage: a
w:body present/absent, and each of the three entry-part flavours
detected (or none) independent of the read that would follow.
…cell

ContentSheetCellSchema requires displayText, absent from the plain
number-cell literals comments-write.test.ts built by hand -- caught by
tsconfig.node.json's own typecheck (which includes test files, unlike
the base tsconfig.json a plain tsc run checks). Introduces a
numberCell helper that always sets it alongside the numeric value.
…tes.length

Uint8Array.prototype.subarray already clamps its end argument to the
array's own length, so requesting SVG_SNIFF_WINDOW bytes from a
shorter buffer already yields exactly the bytes that exist -- the
Math.min was never observably different from omitting it.
…hape

A value shaped exactly like a valid element (tag/attributes/children
all present) under an unrecognised type name must still fall through
to the final `return false` -- nothing previously drove the value
into the "element" arm by an unrelated type name alone.
…wards

A comment thread with one reply, followed by a second cell's own
comment, needs the second root's id to continue at 2 -- a reply-loop
increment that ran backwards would instead collide it with the first
cell's own root id.
A distractor relationship whose type is not the table relationship
type, but whose target happens to be a genuinely well-formed table
element (name and ref both present), must still be skipped -- the
existing distractor test's target failed the name/ref check anyway,
so it could not by itself distinguish the type guard from an absent
one.
… guard

When indexOf finds no 'T', the date half slices to length
iso.length - 1 and the time half to the whole iso.length characters.
ISO_DATE_PATTERN and ISO_TIME_PATTERN are anchored to exactly 10 and 8
characters respectively, so matching both at once would need
iso.length to be both 11 and 8 -- impossible. With no separator, at
least one half always fails to parse, so the existing undefined
fallthrough already covers it with no separate check needed.
insertConstructMarkers's own "extents.length === 0" early return produces
the same array content the main loop already builds for an empty extent
list, and isBlockScopedHalf's trailing calc no longer needs its
"lastContentIndex === -1" shortcut: position is guaranteed non-negative by
the guard above it, so "position > lastContentIndex" already evaluates true
on its own whenever lastContentIndex is -1. Both readCheckboxState and
readOnOff drop the identical "val === undefined ||" shortcut for the same
reason -- undefined already satisfies every one of the three !== checks
that follow it.
…pairing gaps

Adds direct unit coverage for indexParagraphContent's content-bearing
classification, isBlockScopedHalf's leading/trailing edge cases via
synthetic ParagraphContentIndex objects, runRangeMarkerExtents' malformed
start/end pairings and out-of-order run positions, compareExtents'
startIndex-over-order sort priority for crossing extents, and every
w:/w14: spelling fallback across readContentControlDescriptor and
readFormControlDescriptor's checkbox, dropdown, and gallery reading.
…tests

The two boundary tests introduced in the prior commit compared a half's
runPosition rather than its actual array position (index.elements.indexOf),
so both silently exercised the wrong slots and left the </= boundary
mutants on isBlockScopedHalf's leading/trailing calc alive. Pads the
elements array with filler so each half lands at the exact index the test
means to probe, and pins the other half's own block-scope verdict via an
unrelated, mutant-stable condition so the pair's AND genuinely hinges on
the one boundary under test.
…checks

Number.isInteger(min)/(max)/(r) is always true or NaN given each value's
own Number.parseInt provenance, and min >= 1 (or r >= 1) already rejects
NaN unaided, so the isInteger guards were checking exactly what the
numeric bounds already reject. A "max >= min" guard on a declared column
range is equally unnecessary: columnWidthPt's own lookup only ever matches
a range via "index >= min && index <= max", which an inverted range can
never satisfy for any index, so admitting one unguarded is exactly as
inert as rejecting it. Introduces parseIntAttr to read min/max/r directly
as NaN-when-absent, replacing the "attr(..) ?? \"\"" placeholder
Number.parseInt needed only to satisfy its own string parameter -- every
string that could stand in for "absent" parses to NaN just the same, so
the placeholder's own text was never an observable choice.
…ck ternaries

Number(undefined) is already NaN, and every one of these ternaries fed
that NaN straight into an isFinite check that already degrades it to the
same fallback (0, or DEFAULT_ROW_HEIGHT_PT) an explicit NaN branch would
produce -- readAnchorChild's own "empty string" arm is the same story,
since Number("") is 0, itself already finite and thus already the
function's own fallback value. parseIntAttr's identical-shaped ternary
stays: Number.parseInt requires a genuine string argument, so the
"undefined" branch there is load-bearing for the type system even though
it is provably behaviourally equivalent to the value parsing would
already produce.
…As sizing

Adds synthetic-package tests for xlsx drawing-anchor geometry: a malformed
column range (min below 1) falling back to the default width, a covering
range's own declared width winning over a wider range with no width at all,
a real sheetFormatPr defaultRowHeight overriding the built-in default, a
declared row's own height taking precedence over that default, a malformed
row (r below 1, or an unparseable ht) falling back to the default height,
and editAs defaulting to twoCell (to-marker sizing) versus reading an
explicit oneCell (own transform-extent sizing).

Also names the payload sheet from the graphic frame's own xdr:cNvPr/@name
in the existing chart graphic frame test, rather than leaving it implicit.
…gaps

Adds synthetic-package tests for the chart-graphic-frame reading path
that the picture-anchor fixtures never exercised: a graphicData whose
uri names something other than a chart, a graphic frame with no
xdr:cNvPr at all, one whose cNvPr carries no name attribute, a
worksheet whose rels list an unrelated relationship type before the
real drawing one, and a picture-only drawing asserting embeddedObjects
stays absent. Also adds marker-field tests distinguishing a genuinely
nonzero rowOff from colOff and a numeric marker value from a non-text
sibling node, a column-range test proving a range never applies below
its own declared min, and an absoluteAnchor position landing exactly
on a column boundary.

Drops the now-provably-redundant "r >= 1" guard on declared row
heights: rowHeightPt is a direct Map.get on the caller's own index,
never a range test, so a malformed row lands at a key no legitimate
query can ever reach, unlike the analogous column-range check this
guard was modelled on. Reads editAs directly against "oneCell" rather
than through an intermediate default, since twoCell and an absent
attribute are already indistinguishable to that comparison. Documents
emptyWorksheet's own tag as unobservable to its sole caller. Rewrites
chartCells to read a table cell's single run directly instead of
joining a general multi-block, multi-run shape neither this file's
only producer (labelCell) nor any real chart cache ever populates with
more than one of either.
…ixed package-scaffolding XML

buildDrawing's zero offsets, rect preset, and distT/B/L/R attributes, and the
fixed _rels/.rels relationships, [Content_Types].xml Default/Override
entries, and styles.xml docDefaults/Normal scaffolding were never asserted
against their literal values: readDocxContent doesn't read most of them
back, so a round-trip assertion alone can't catch a mutated literal. These
tests parse the written XML directly and check every fixed attribute value.
…tion gaps

Adds direct synthetic-package coverage for sheetFormatDefaultRowHeightPt,
readColumns, and readRows: default row height fallback, 1-based min/r lower
bounds, the 1-based-to-0-based index subtraction, and hidden flags.

Covers deriveDisplayText/resolveNumericValue's exact per-kind displayText
output (dateTime, percentage, false boolean, symbol-only currency),
readCellValue's boolean parsing and NaN handling, and merged-range
colSpan/rowSpan arithmetic anchored away from row/column 0 so subtraction
and addition mutants actually diverge.

Adds a hasOwn() helper and uses it wherever a test needs to prove a key is
genuinely absent from a ContentSheetCell/ContentSheet, since toBeUndefined()
cannot distinguish an absent key from one explicitly assigned undefined.

Removes redundant guards whose branches the surrounding NaN-fallback
arithmetic already collapses to the identical result
(sheetFormatDefaultRowHeightPt's raw-undefined check, readColumns'
widthRaw-defined check, readRows' htRaw-undefined check), and the two
early-return size checks in applyCellComments/applyCellResidueRules, whose
absence only skips pointless work over an empty collection rather than
changing any observable output.

Documents two remaining genuinely irreducible equivalent mutants (the
sqref-split regex's + quantifier, and fallbackEmptyWorksheet's own tag
string, matching drawings.ts's identically-shaped case) with the exact
reasoning that makes them unobservable through this module's own contract.
Removes applyCellResidueRules' own empty-firstToken disjunct:
parseRangeReference('') already returns undefined rather than
throwing, so the check was a redundant special case of the
undefined branch beside it, which stays load-bearing on its own.

Asserts displayText, not just value, for a true boolean cell,
closing the one real remaining gap in deriveDisplayText.

Documents deriveDisplayText's own "empty" case as a structurally
required but genuinely unreachable switch arm: ContentCellValue's
type still includes "empty" as a member, so the case must stay for
the function to type-check as returning string unconditionally,
even though neither of its two real call sites can ever pass one.
signature mutation gaps

Adds direct unit coverage for every optional-field presence check
in the read side (font/fill/border/alignment key absence, not just
value, proven with hasOwn rather than toBeUndefined), for the exact
val-string behaviour of readFontToggle/readFontUnderline, for a
non-integer numFmtId/sizePt leaving the code/sizePt unresolvable,
and for colorFromElement's own validation (invalid hex, a too-short
rgb attribute).

Adds write-side tests proving every one of CellFormatTable's own
signature segments (each font/fill/border/alignment flag) actually
distinguishes two otherwise-identical entries, rather than trusting
that the signature strings alone don't collapse two different
inputs onto the same interned index; and that a font/fill/border
interned twice under different number formats still caches to a
single declared entry.

Removes readFontTableEntry's own redundant szVal-undefined guard:
Number(undefined) is NaN, so an absent <sz val> already falls
through the Number.isFinite check below to the same "no sizePt"
result this guard would have selected directly.

Documents two remaining genuinely irreducible equivalent mutants in
colorFromElement (the raw.length >= 6 vs > 6 boundary, and the hex
regex's own anchors) with the exact reasoning that makes them
unobservable given hex's own fixed construction.
Adds a real regression test for the border style ?? "solid"
fallback: an edge with no style stated and one explicitly styled
"solid" now assert to the same borderId, proving the fallback
actually merges two representations of the identical visible
border rather than only avoiding a crash.

Adds tests for fontFamily/sizePt/colour genuinely differing from
or absent against the baseline, size/fontFamily alone distinguishing
two interned fonts, an empty borders object not colliding with a
real one, and internFill's own default branch throwing for a fill
kind this discriminated union has no member for.

Removes readBorderEdge's own redundant "none" special case:
"none" is not a key XLSX_BORDER_STYLE declares, so it already
falls through the resolved-undefined check below to the identical
result this check would have returned directly.

Documents the remaining genuinely irreducible equivalent mutants in
the read-side and write-side non-integer-numFmtId guards (each
redundant with a sibling guard on the only real call path) and in
every internal-only, never-exposed signature-building segment
(font/fill/border/alignment dedup keys), where no consistent
relabelling or placeholder substitution can ever create a real
collision given the actual domain of values each field carries.
actually observe their own mutant

Both replacement tests compared an empty-borders {} decoration
against a real one, but an empty {} decoration's own outer
signature already coincides with EMPTY_DECORATION's (the loop over
its zero edges appends nothing), so it hits the seeded
cellFormat-level cache before internFormat/internBorder is ever
called at all -- neither test could ever have observed a change to
internBorder's own per-edge signature building or to the outer
alignment-presence check, regardless of mutation.

Verified directly, per this project's own equivalence-claim
convention: applying each mutation by hand and running the affected
test confirmed it passed unchanged either way, before rewriting it.

The style ?? "solid" fallback now compares two SAME-number-format
interns (an implicit-style edge against an explicit "solid" one),
which genuinely forces reuse of the outer cellFormat cache and so
exercises signatureOfDecoration's own fallback, not
internBorder's separately-correct borderToXlsxStyle handling of
the same case.

The edge-segment test now compares two distinct REAL borders (both
of which genuinely reach internBorder) rather than an empty one
against a real one, so a collapsed per-edge segment is observable
as a wrongly-shared borderId.
Both cases were entirely unreached by any existing test -- every
border test so far exercised only the dashed/solid weight-bucketing
branches, leaving the two fixed-token cases genuinely uncovered
rather than merely untested for a specific input.
This module had no dedicated test file at all: its only coverage
came from content.test.ts/build.test.ts round trips through
readXlsxContent, whose reader never inspects an OOXML element's
exact tag or attribute spelling, only its structural shape -- so a
round trip could never tell a real element name from a mutated one
apart.

Asserts the full xdr:oneCellAnchor/xdr:pic/xdr:graphicFrame shape
for both a picture and a chart anchor, the drawing/chart namespace
declarations, the relationship part's own Id/Type/Target triples,
the chart XML declaration and c:chartSpace root, every c:ser/
c:barChart/c:catAx/c:valAx element and its fixed axis ids, the
series/category range arithmetic against a non-trivial category
count (so the +1 in each range's own upper bound, and the +1 from
column index to letters, are both observable), object-id and
relationship-id counters advancing across multiple images, media/
chart numbering advancing across two calls sharing one counters
instance (the shared-across-sheets contract DrawingCounters' own
doc comment states), and every one of this module's five thrown
error paths (svg image, non-chart object, a missing anchor field,
a non-spreadsheet document, a spreadsheet document with no sheet).
Moved the shared picture+chart buildSheetDrawing() call from the
describe body into beforeEach. Stryker's per-test coverage
instrumentation attributes a line's execution to whichever test is
running when that line executes, and a describe body runs during
test collection, before any it() has started -- a call made there
is invisible to that attribution, so Stryker silently ran some
other, less precise test against these mutants instead of this
file's own assertions.

Verified directly: an L62 StringLiteral mutant on "xdr:col" showed
Survived in a real scoped run despite the exact assertion in this
file catching it when the same mutation was applied by hand and
run locally, until the call moved into beforeEach.
…arse-cell gaps

Adds a two-chart test proving the object-id counter genuinely
advances (2 then 3) for charts, not just for the two-image case
already covered, and a sparse-chart-cells test proving a missing
series-name/value cell reads back through chartSeriesFromDocument's
own cellAt fallback as an empty string, matching the empty label a
missing point already gets on the read side.
This module's only prior coverage came from two real-producer
fixtures (cellIs, colorScale) and a round-trip suite covering every
rule family's own value shape, neither of which exercised the
module's own exact XML vocabulary directly: an attribute name
mutation on the write side and the matching read-side lookup can
cancel each other out in a round trip, and toEqual-based value
checks cannot distinguish a key genuinely absent from one assigned
undefined.

Covers, on the read side: the wrapper-level sqref gate, priority/
stopIfTrue/source residue capture, every cfvo type token including
formula/percentile (previously entirely uncovered), the colorScale
cfvo/color count-mismatch rejection, dataBar/iconSet's own
true-default showValue convention and reverse flag, every dxf
residue passthrough branch (font/fill/numFmt/alignment/border/
protection, including a font or fill kept whole when no colour was
captured from it), cellIs formula2 restricted to between/
notBetween, top10's rank<=0 rejection and percent/bottom presence,
aboveAverage's own true-default and stdDev<=0 rejection, and the
colorScale/iconSet type discriminants.

Covers, on the write side: formula/formula2 element count, top10/
aboveAverage/dataBar/iconSet's own attribute presence (each written
only when explicitly set, never restating a default), the
colorScale element's cfvo-before-color ordering, and
buildConditionalFormattingElements' own range-based grouping and
gap-filling priority assignment (an unpriorised rule never reuses
an already-claimed explicit priority).
…sidue gaps

Covers every isSheetRuleOperator member distinctly (not just
between/greaterThan), cellIs formula2 for notBetween alongside
between, the absent-timePeriod rejection, the colorScale cfvo/
color count boundary at exactly 2 and exactly 3 stops on both
sides, residualAttributesFor's own expectedTag gate (an unmanaged
cfRule attribute genuinely restored on write), rangeSetKey's field
separator (two ranges that would collide under naive concatenation
without it), and a full round trip of every dxf residue kind at
once (font+color, fill+patternFill+bgColor, numFmt, alignment,
border, protection) through DxfTable.intern.

Adds the missing "omitted entirely" half of several write-side
attribute-presence tests (top10 percent, aboveAverage/equalAverage/
stdDev, dataBar showValue, iconSet reverse/showValue) that only
asserted the explicit-true/false case, never that the attribute is
genuinely absent -- not merely unasserted -- when nothing was set.
… underline/strike tokens

Adds a minimal, layout/master-free slide package builder for
isolating a single shape's own paragraph/run properties, and uses
it to cover: the widescreen-default fallback when p:sldSz carries
no cx (previously untested against a real, differently-sized
sldSz, so the default and a genuine explicit size were
indistinguishable), every algn token (l/ctr/r/just/justLow, plus
an unrecognised token falling through to no alignment), and the
exact none/noStrike tokens for underline and strikethrough
alongside their positive and absent-attribute cases.
re-measured floor

Re-measures the package-wide score after closing the mutation gaps
in content.ts, styles.ts, drawings-write.ts (now a genuine 100%),
conditional-format.ts, and pptx/read.ts: 84.44% of 6891 valid
mutants, up from the original 64.55% baseline the threshold of 63
reflected. States plainly which modules remain the real next
targets (xlsx/build.ts, docx/write.ts, docx/read.ts), so the number
reads as a measured floor rather than a ceiling.
@Mearman
Mearman force-pushed the feat/100-percent-mutation-ooxml.js branch from 856c7dc to e59d726 Compare September 16, 2026 05:53
Adds direct assertions for the XML declaration prolog, every
[Content_Types].xml Override across a document exercising comments,
images, charts, and tables, the fixed _rels/.rels and
xl/_rels/workbook.xml.rels relationships, xl/workbook.xml's sheetId
and r:id numbering, and xl/sharedStrings.xml's count/uniqueCount and
xml:space attribute.

Closes the Print_Titles derivation gap where repeatRows and
repeatColumns were always set together, so mutating the || between
them to && never changed the observable output; adds cases with
each set alone and with neither set.
Asserts the exact fixed scaffolding buildStylesPart writes for a
document needing no number formats: the single default font, the
two reserved fills, the one reserved border, and the default
cellStyleXfs/cellXfs/cellStyles entries, none apply*-flagged.

Adds a font carrying bold, italic, strike, and underline together,
proving each toggle writes its own element independently; a border
with only its top edge set, proving the per-edge branch runs
independently for each of the four edges rather than uniformly; a
cell with verticalAlignment 'top', the one branch neither 'middle'
nor the default omission exercises; and pattern fills with only a
foreground or only a background colour.

Asserts every docProps/core.xml and docProps/app.xml field,
including subject, modifiedIso, and creator, and the case where
metadata carries none of them and keywords is an empty array.
…and merge output exactly

Adds cases where a sheet's dimension is extended solely by its
columns array or solely by its rows array (no cells at all), and a
case where a column/row entry reaches past the last cell, proving
computeDimension takes the genuine max across all three sources
rather than letting one silently dominate.

Asserts buildColsElement writes hidden with no width attribute, and
width/customWidth with no hidden attribute, as two independent
column declarations rather than always pairing the two.

Proves buildSheetDataElement sorts both rows and, within a row,
cells into ascending order regardless of input order, and that a
row with no matching ContentSheetRow entry carries only its own r
attribute.

Proves buildMergeCellsElement treats colSpan and rowSpan as
independent merge triggers, and writes no <mergeCells> at all when
every cell's span is 1 or absent.

Covers buildCellElement's decoration ternary for alignment-only
cells, the exact <f>/<v> child order and missing t attribute for a
formula's numeric result, and the no-formula case writing no <f> at
all; renderString's formula-result branch for an unparseable
temporal value; and buildSheetPrElement's fitToPage reflecting
whether the sheet actually declares fitToPages.
… breaks exactly

Asserts ptToInches's genuine points-to-inches conversion for both
the common 72pt case and non-72pt margins, and the fixed 0.3in
header/footer margin.

Covers buildPageSetupElement's paperSize/paperWidth-paperHeight
branch for a standard versus a custom page size, the landscape and
portrait orientation branches, the default scale/fitToWidth/
fitToHeight when neither scalePercent nor fitToPages is declared,
and their declared values when present.

Covers buildBreaksElements writing row breaks and column breaks
independently of each other, with the exact id/min/max/man
attributes, and writing neither element when manualBreaks is
undefined or both its arrays are empty.

Asserts buildWorksheetPart writes cols/mergeCells/drawing/
tableParts together for a sheet carrying every optional feature and
none of them for a plain sheet, plus the worksheet root's own
xmlns/xmlns:r and buildWorksheetRelsPart's Relationships root.
…onship numbering exactly

Proves a table definitions entry attaches tableParts and its own
xl/tables/tableN.xml only to the sheet it names, and that an
unrelated second sheet gets neither the table nor its own rels
part at all, closing the gap where the 'table.sheet !== sheet.name'
skip was never exercised by a genuinely non-matching sheet.

Asserts worksheet relationship ids are assigned sequentially
(rId1/rId2/rId3) across comments, drawing, and table relationships
on the same sheet, in that order.

Proves usedImageFormats collects every distinct format a sheet's
images actually use (png and jpeg together), not just the first,
and declares no Default extension for a format never used.

Adds a negative assertion that a plain document with neither a
chart nor a table writes no /xl/charts/ or /xl/tables/ Override at
all, closing the gap where an initial-empty-array mutant seeding a
bogus part name went undetected by toContainEqual-only assertions.
…content

The existing "derives the reserved print names" test checked the
definedName's name and localSheetId but never its own text, so a
mutant dropping the range text entirely went unnoticed.
…ryker reporting anomaly

xlsx/build.ts now measures 69.3-69.5% of its own valid mutants
under Stryker's scoped mutation run, up from 32.83% before the
structural coverage added in this session's earlier commits,
confirmed reproducible across three independent runs including one
at concurrency 1.

Documents a genuine tool-measurement anomaly found while verifying
that improvement: several mutants Stryker's own reporter marks
survived were directly disproven as equivalent by manually applying
the exact mutation and running the identical vitest configuration
Stryker's own runner uses, which fails the relevant tests every
time. The package breakThreshold is left unchanged, since raising
it needs a fresh full-package measurement once docx/write.ts and
docx/read.ts, the package's two remaining large modules, have also
been closed.
Narrow worksheetRels[0] with an explicit undefined check before passing
it to attr(), instead of an unsound index access typed as
XmlElement | undefined, and drop the readonly modifier from
pkgWithBreaks's manualBreaks parameter to match
ContentSheetPrintSettingsSchema's own mutable array type, which
readonly arrays cannot satisfy.
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