Skip to content

test(odf.js): work toward a 100% Stryker mutation score - #1260

Merged
Mearman merged 99 commits into
mainfrom
feat/100-percent-mutation-odf.js
Sep 16, 2026
Merged

Mearman merged 99 commits into
mainfrom
feat/100-percent-mutation-odf.js

Conversation

@Mearman

@Mearman Mearman commented Sep 12, 2026

Copy link
Copy Markdown
Member

Works through odf.js's mutation survivors from its own first CI-measured baseline (71.31% of 8442 valid mutants, timeout share 1.3%). This is the largest package under this effort so far by a wide margin, so this PR has landed real, verified progress across many batches rather than a single finished 100% run.

Batches so far have added or extended direct unit coverage across most of the package: the ods writer's cell-run/cell-value boundaries and every canonical* helper, typed/shared/canonicalise.ts's field-by-field branches (plus removing two proven-dead closeListPlan calls), typed/shared/units.ts, metadata.ts, list.ts, expression.ts and forms.ts (each previously untested), test-support/zip.ts and document-tree.ts, xml/build.ts and xml/parse.ts, image/sniff.ts, util/base64.ts, the a1.ts column/cursor helpers, typed/shared/text.ts, border.ts, package-io/scaffold.ts, typed/shared/transform.ts, metadata.ts, ooo1/ns.ts, typed/draw/embedded-write.ts, typed/odp/read.ts, typed/shared/cascade.ts, masterpage.ts, typed/odb/subdocument.ts, typed/odg/read.ts, typed/odm/write.ts and read.ts, package-io/write.ts, styles/serialize.ts, typed/formula/read.ts and write.ts, util/base64.ts's remaining survivors, styles/span.ts, styles/registry.ts, typed/odg/write.ts, typed/draw/embedded.ts, typed/odb/report.ts (100%), typed/ods/conditional-format.ts (100%) and data-validation.ts (100%), typed/shared/constructs.ts, typed/draw/shapes.ts, ooo1/transform.ts (100%), typed/ods's used-range/data-validation/sheet-embedded-object-frame edge cases, typed/draw/write-vectors.ts, ooo1/properties.ts, model/node.ts, typed/draw/write-shapes.ts, typed/odb/write.ts's XML-structure coverage, and isXmlNode.

A full CI-measured run of this PR's own branch now confirms 91.40% of 8302 valid mutants (7476 killed, 587 survived, 127 no-coverage, 112 timeout), up from the 71.31% baseline. breakThreshold is raised to 89 (the derived floor for this measured score), still provisional.

Remaining work, tracked honestly rather than hidden: 714 mutants (survived + no-coverage) remain, concentrated in typed/shared/paragraph.ts (~124), typed/ods/write.ts (~118), typed/odt/write.ts (~94), typed/ods/read.ts (~84), typed/draw/shapes.ts (~70), typed/odt/read.ts (~61), typed/odp/write.ts (~53), typed/shared/path.ts (~39), typed/draw/write-shapes.ts (~29), xml/parse.ts (~21), typed/shared/table.ts (~18), and a handful of smaller files (typed/shared/canonicalise.ts, styles/properties.ts).

No Stryker disable comments anywhere in this package (verified by grep before every commit and again before this update).

@Mearman
Mearman force-pushed the feat/100-percent-mutation-odf.js branch 7 times, most recently from d3614a8 to da5f83d Compare September 14, 2026 20:02
…boundaries

isBareNewlineRun's own nine-field AND-chain (deciding whether a "\n" run
is a genuine text:p paragraph break or a formatted text:line-break) had
no direct coverage at all: existing tests only exercised the "all fields
absent" and "bold + hyperlink together" shapes, leaving every other
field's own undefined-check untested. Adds one test per field plus the
bare-newline case.

formatOdfDuration's regex and fraction branches, writeCellValueAttributes'
boolean/currency/exactValue branches, and cellSourceRuns' empty-displayText
fallback get the same direct coverage.

Exports canonicalColor/canonicalCellFill/canonicalRun/canonicalCellValue/
canonicalCell/canonicalCells/canonicalColumns/canonicalRows/
canonicalSheetImage/canonicalImages/canonicalPrintSettings/
canonicalDataValidations/canonicalConditionalFormatStyle/
canonicalConditionalFormats and pins each directly against a literal
expected value: normaliseOdsContent applies every one of these to BOTH
sides of the write-round-trip suite's own equality check (the real
reader's output and the original document, each normalised the same
way), so a mutation confined to one of these helpers changes both sides
in lockstep and was invisible to that comparison -- only a direct,
one-sided assertion can observe it.
canonicalCellFill's own pattern-fill fixtures used "gray50", which is
not a member of ContentCellPatternTypeSchema (the enum spells Excel's
own fixed-density greys as "mediumGray"/"darkGray"/"lightGray" through
"gray0625", never a percentage-style name). canonicalPrintSettings'
fitToPages/repeatRows/repeatColumns fixtures used field names from a
different schema shape (widthPages/heightPages, startRow/endRow,
startColumn/endColumn) rather than the real {width, height} and
{start, end} shapes ContentSheetPrintSettingsSchema actually declares.
…r-kind fill fields

regularPolygonSubpath/fixedPresetSubpath's own trig and midpoint arithmetic
had only a vertex-count assertion (it.each over diamond/triangle/pentagon/
hexagon/octagon), never the actual coordinates, so every arithmetic operator
in the angle/radius formulas could flip with nothing to catch it. Adds exact
coordinates for isosceles-triangle, right-triangle, and a full six-point
hexagon check against the same trig formula the source uses.

roundedRectSubpath's own eight segments (four straight edges, four cubic
corner arcs) were pinned only at the start point; the other seven segments'
own w-radiusPt/h-radiusPt/radiusPt-k/radiusPt+k arithmetic had no coverage
at all. Adds the full segment-by-segment coordinate check, plus the two
boundary cases (readRoundRectangleRadiusPt degrading to a plain rect for a
zero-width viewBox and for a resolved radius of zero) neither had a test.

fillPattern/fillOpacity's own "carry through only when defined" pair is
duplicated identically across readDrawRectVector, readDrawEllipseVector,
readDrawPathVector, and readCustomShapeVector, but every existing test
exercised it through draw:rect alone -- the other three vector kinds'
own copies were untested. Adds one fillOpacity case per remaining kind.
…op two dead closeListPlan calls

canonicalParagraph/canonicalCell/canonicalTable/canonicalMetadata/
canonicalImage are applied identically to both sides of every
odt/odp/odg/draw-vector writer's own round-trip equality check, so a
mutation confined to one of them changes both sides in lockstep and
was invisible to that comparison -- exactly the odf.js/typed/ods/
write.ts precedent this mirrors for the odt/odp/odg-facing sibling
module. Adds direct, one-sided unit coverage for every field-by-field
branch, the colSpan/rowSpan covered-grid loop's own off-by-one
boundaries, and the per-cell list-run scoping canonicalTable threads
through a shared ListPlanState.

Also removes two closeListPlan(listState) calls (canonicalTable's own
leading call, and the one immediately after canonicalCell inside the
per-cell loop): both are unconditionally overwritten by a later
closeListPlan before anything could ever observe their own effect --
the leading call by the per-cell close that precedes every cell's
canonicalCell invocation (including the first), and the post-cell
call by either the next cell's own leading close or, for the last
cell, the table's own trailing close. Neither was reachable by any
test because neither can ever change behaviour; deleting them removes
the mutation opportunity rather than chasing an equivalent mutant.

Simplifies canonicalParagraph's own protected-boundary set for
segmentOdfParagraphRunsMapped: the merge loop only ever tests
protectedBoundaries.has(index) for index in [0, runs.length), so a
stated {0, runs.length} pair contributed nothing to either branch's
outcome (index 0 never merges regardless, having no preceding group
yet; index === runs.length is never reached by that loop). The false
branch now passes an empty Set with no array literal to mutate; the
true branch keeps only the construct extents' own interior
boundaries, which are the one real source of boundary positions the
merge decision ever consults.
…ial directly

parseOdfAngleDeg had no test coverage at all -- deg/grad/rad conversion,
the bare-number-defaults-to-degrees case, and the malformed-input path
were all unexercised. Adds direct coverage for every branch, including
the grad/rad conversion constants.

Exports and directly tests isLengthUnit and expandExponential rather
than only reaching them through parseOdfLength/formatOdfNumber: both
are pure predicates/formatters whose own internal branches (the six-way
unit check, expandExponential's pointIndex <= 0 / >= digits.length
boundaries) were only ever exercised with values the caller's own
regex had already narrowed, leaving several branches equivalent in
practice. Direct string-input tests pin the exact pointIndex boundary
cases (0 exactly, digits.length exactly, strictly between, negative)
that were previously unreachable through formatOdfLength's own
floating-point call sites.

Replaces expandExponential's and parseOdfAngleDeg's own
sign/integerDigits/exponent === undefined re-checks with a plain
comment plus non-null assertions: each of those regex groups has no
`?` quantifier of its own (only the alternation inside them does), so
none can ever actually be undefined once the enclosing match is
non-null -- the re-check was dead code no input could reach, not a
guard against a real failure mode.
…t.ts

list.ts had no test file of its own at all: resolveOdfListKind,
mintOdfListNumId, buildOdfListStyle, writeOdfList, listKindOf,
canonicalNumId, planListMembership/closeListPlan, and
readOdfListParagraphs were exercised only incidentally through odt/odp
round-trip fixtures, which never happened to hit several of their own
branches (a level-1-only style-kind check, the nested-list
write-side's own level-jump/dedent handling, the run-boundary sentinel
planListMembership mints for a membership with no incoming numId).

Adds a dedicated suite covering every exported function directly:
each of the three list-kind resolutions (ordered/bullet/bullet-via-
image) and their negative cases, the ten-level style builder's own
per-level indent arithmetic, writeOdfList's nesting/dedent/level-clamp
behaviour, and the list-plan run-boundary semantics closeListPlan and
a changed incoming numId both need to preserve.
…ression.ts

skipExpression/takeExpression (the balanced-paren/brace/quote-aware
expression splitter both typed/ods/data-validation.ts's table:condition
reading and typed/ods/conditional-format.ts's calcext:condition reading
share) had no test file of its own -- both readers exercised it only
through their own real-world condition-string fixtures, which never
happened to cover brace nesting, single-quoted strings, an unterminated
quoted string running to the end of the text, or a whitespace-only
span correctly yielding undefined rather than an empty string.
…floor

A full run after this batch's own survivor kills (canonicalise.ts,
units.ts, metadata.ts, list.ts, expression.ts) measures 75.72% of 8444
valid mutants, up from the 71.31% baseline this threshold was
originally derived from. Still provisional -- the bulk of this
package's own files carry their own unkilled mutants and have not
been touched yet.
…ckage's XML sniff

hasUtf8Bom's compound bytes.length >= 3 && bytes[0] === 0xef && ...
condition mutated to several sub-clause "true"/"false" variants that
survived: through looksLikeXml alone, a wrongly-detected BOM and a
correctly-rejected one routinely land on the same XML/binary verdict
downstream (a too-short array ends the scan at the same byte either
way), so no test built only on the final classification could
distinguish them. Extracting it into its own exported, directly
tested function pins every boundary (a too-short array, each byte
individually mismatched) against its actual boolean return value
instead.

Also replaces the four-way whitespace disjunction (b === 0x20 ||
b === 0x09 || ...) with a Set.has() check, and the manual
index/while loop with a for...of over bytes.subarray(start):
both removed a class of survivable sub-expression and loop-boundary
mutants outright rather than chasing them through indirect
byte-array engineering, since a byte either belongs to the
whitespace set or doesn't and a subarray iteration carries no
explicit index comparison to mutate.
…ms.ts

readOdfFormDefinitions and readOdfFormControlConstructs had no dedicated
test file at all, only indirect exercise through odt/odb round-trip
fixtures that never varied every optional attribute, control-tag
mapping, or checkbox/listbox branch independently. Adds direct coverage
for every branch: each optional field on a form definition and on a
control (present and absent), nested form:form as subForm vs control,
form:properties/text-node exclusion from both control and subForm
scanning, every CONTROL_TYPE_BY_TAG mapping, the unmapped-tag richText
degrade with whole-element residue, current-value/value precedence,
checkbox/radio's current-state-derived checked field (and its absence
on every other control type), listbox option label/value precedence
and the neither-present skip, and per-control form:properties residue.

Switches this file's toEqual assertions on absence to toStrictEqual:
toEqual treats an explicitly-set `field: undefined` the same as the
field being absent, so an `if (x !== undefined) descriptor.field = x`
mutated to `if (true)` was invisible to a toEqual comparison even
though it changes the object's own own-property set.

Raises odf.js's mutation break threshold to the re-measured floor now
that typed/shared/forms.ts has zero survived and zero no-coverage
mutants of its own.
…eaders

Adds direct unit coverage for readUint16LE/readUint32LE/localFileHeaderNames/
assertMimetypeEntryLayout: truncated-input throws (including negative-offset
isolation of the first missing byte), exact-boundary reads, multi-entry
offset arithmetic through a non-zero extra field and compressed size, and
each of assertMimetypeEntryLayout's six field checks.

Also replaces the per-byte "b0 === undefined || b1 === undefined || ..."
guards in both readers with a single [offset, offset + byteCount) range
check. The per-byte form could never be killed in full: a real Uint8Array's
undefined region is always a contiguous prefix or suffix, so no input can
isolate an interior byte (b1 of 4, say) as the sole missing one, leaving
that comparison an unreachable, equivalent mutant. The range check has no
interior case to isolate.
…lpers

Adds direct unit coverage for the five kind-narrowing functions
(wordprocessingPackage/presentationPackage/spreadsheetPackage/
drawingPackage/formulaPackage), each checked both on a matching-kind
package (returns it unchanged, no throw) and a wrong-kind one (throws
the exact "expected a ... package, got ..." message) -- neither path
was ever exercised by odf.js's own reader suites, which only ever hand
these functions a correctly-kinded result.

Also covers assertPackageRoundTrip's three checks individually, each
isolated so exactly one fails while the other two still pass: a
schema-invalid tree via a malformed `fonts` field (schema-checked but
read by neither flattenTree nor factorStyles, which carries an
existing value through verbatim rather than recomputing it); a tree
that flattens to something other than the given content; and a tree
carrying an extra, unreferenced styles-table entry that a fresh mint
of its own flattened content would not reproduce.
Pins buildXml's pi/declaration/element/text/comment/cdata node mapping
directly, including the empty-array shape for pi and declaration nodes
that fast-xml-parser's ordered builder reads only from a node's own
":@" attributes rather than its array value, and the throw path when
the underlying XMLBuilder does not return a string.

Extends the package's existing no-deprecated exemption for the
deprecated XMLBuilder class from build.ts to its own test file, which
necessarily references the identical class to reach BUILDER's shared
prototype.
Factors the pi/declaration cases' shared "ignored by fast-xml-builder"
array literal into one constant typed as the empty tuple `readonly
[]`, turning a content mutation there into a type error rather than a
silent, unobservable survivor (fast-xml-builder never reads either
node shape's own array value).

Exports toOrderedNode so a test can pin the exact intermediate
ordered-node shape directly, in particular that an attribute-less
element's object carries no ":@" key at all rather than one holding
an empty object, a distinction the built XML string never renders
differently, so no test on buildXml's own output could observe it.
Covers PNG/JPEG/GIF87a/GIF89a magic-byte detection, the too-short and
empty-input cases, and SVG sniffing from either an XML prolog or a
bare root tag with leading whitespace, including the deliberate
window-size cap that lets a root element sitting past the first
kilobyte go undetected rather than scanning an unboundedly large file.

Removes startsWith's separate "bytes too short" guard: an
out-of-bounds Uint8Array read is undefined, which never strictly
equals a real signature byte, so the comparison loop already returns
false for a too-short input on its own, the guard produced no outcome
the loop didn't already produce.
Pins bytesToBase64 and base64ToBytes against the classic Wikipedia
"Man"/"Many hands..." progressive vectors, one per length mod 4 so
every padding branch is exercised both true and false, plus the
whitespace-stripping clean-up regex, the invalid-padding-position
throw, and the fixed-size scratch buffer silently bounding a
malformed, non-4-multiple-length decode rather than growing to fit it.
…text

columnLettersToIndex had no direct test at all: adds cases for a valid
uppercase reference and the three invalid shapes (lowercase, a
trailing digit, empty) that must return undefined rather than
delegate to the schema helper.

Sharpens TableCursor's repeat-count error assertions from a generic
/positive integer/ pattern match to the caller name itself
(TableCursor.nextCell / TableCursor.nextRow), since the generic
pattern alone can't tell the two call sites' own error text apart.
… mixed case

document-schema.js's own columnLettersToIndex uppercases its input
before validating, so it alone can't distinguish "aA" or "Aa" from
"AA"; each of odf.js's own ^ and $ anchors, if dropped, would let
exactly one of those two mixed-case strings reach the schema helper
undetected instead of being rejected up front.
Adds negative, non-canonical-spelling (leading zero), and exact-zero
text:c cases to getOdfSpaceCount's own guard, each isolating one of
its three disjuncts; adds a child-carrying bookmark/marker case to
measureOdfNodeLength and decodeOdfText, since an empty-children marker
can't tell "recursed into nothing" apart from "never recursed".

Drops the space-run scanner's redundant `end < text.length` bound: an
out-of-range string index is undefined, which is never `=== SPACE`, so
the comparison loop already stops there on its own.
…old.ts

Pins createOdfPackage's exact XML declaration, office:version stamping
(both the given version and the DEFAULT_ODF_VERSION default), the body
element nesting inside office:body, and the mimetype part it writes.

Covers both of odfPartContainer's throw paths directly: a part path
that resolves to a non-XML part, and an XML part with no container
matching the requested tag, alongside the already-implicit success
path returning a real container.
…der.ts

Covers parseBorderEdge's whitespace tolerance (leading/trailing, and a
run of several spaces collapsing to one separator), its wrong-token-
count/unparseable-length/unparseable-colour/zero-or-negative-width
rejections, the none/hidden marker, an unmapped style token leaving
style unset, and formatBorderEdge's own solid-style default.
Pins resolveOdfListKind's undefined-style-name short-circuit against a
package carrying a real, matchable, ordered list-style whose own
style:name attribute is absent (so attrValue coincidentally also
resolves to undefined), and extends the existing "only a level-1
child counts" case from the ordered path to the bullet and image
paths, each previously untested.

Pins buildOdfListStyle's ordered branch to also carry the indent
properties every level already carries on the bullet branch, and adds
a readOdfListParagraphs case where an item child's tag is neither
text:p/text:h nor text:list, carrying its own text:list-item/text:p
descendants specifically so a wrongly-permissive recursion would
surface them.

Drops writeOdfList's redundant tag check on an existing host: every
element this function ever pushes onto an "enclosing" list's children
is already a text:list-item via its own construction, so an element
found there carries no other tag to distinguish from it.
Adds cases for rotate() called with no argument, an unmodelled
function's own args never being parsed as translate's just because
they happen to look like valid lengths, and a run of several spaces
between translate's two arguments collapsing to one separator.

Drops parseOdfTransform's redundant name/argsRaw undefined guard:
FUNCTION_PATTERN's two capture groups are both plain, non-optional
captures, so a successful match always populates both. Replaces the
split-then-filter empty-string removal with an explicit empty-argsRaw
check, the only case split can actually misbehave on given
FUNCTION_PATTERN's own surrounding whitespace trim.
… write-side paths

readCellStyleDecoration had no direct test coverage at all for
verticalAlignment, alignment, or multi-element cascading/accumulation,
and the entire write side (tableColumnStyle, tableRowStyle,
tableCellStyle, writeCellBlocks, writeOdfTable) had none either --
every mutant in that half of the file was either Survived or
NoCoverage. Adds direct tests for both.

Also simplifies readRepeatCount: Number.parseInt always returns an
integer or NaN, and NaN compared with > is always false like every
other NaN comparison, so the separate Number.isInteger guard was
checking something > 0 already covers on its own -- removing it drops
an unkillable mutation opportunity (isInteger is true whenever parsed
isn't NaN, and false only when parsed is NaN, so no reachable state
distinguishes && from ||) rather than leaving a redundant condition
for a test to fake-cover.
canonicalRun had no dedicated tests at all -- every canonicalParagraph
test exercised it only through a single-field RUN fixture, so none of
its seven per-field guards (bold/italic/underline/strike/fontFamily/
sizePt/color/hyperlink) or the canonicalColor delegation ever had a
test proving that field actually passes through. Adds one test per
field plus an all-fields-at-once case proving none clobbers another.
writeOdb's own suite was entirely round-trip tests against real
fixtures (write then read back, compare to the original inventory),
which cannot observe a mutation that changes what gets WRITTEN in a
way the reader's own inverse tolerates or the fixture never exercises
-- the same failure mode typed/shared/canonicalise.ts's own top-of-
file note describes for write/read pairs in general.

Adds direct, one-sided assertions against the raw written XML for:
the two connection-type names in the no-url error message, the four
optional child elements staying entirely absent when every list is
empty, db:as-template/db:escape-processing each writing the exact
true/false the caller stated (and nothing when absent), table names
writing in order, and a connection's url landing verbatim in
db:connection-resource regardless of connection type.
…-vectors.ts

writeDrawVector/canonicalDrawVector had no dedicated test file at all
-- only indirect exercise through typed/odg/write.test.ts's own
round-trip suite, which cannot observe a mutation that changes what
gets WRITTEN in a way the reader's own inverse tolerates or the
round-trip fixtures never happen to exercise (the same failure mode
typed/shared/canonicalise.ts's own top-of-file note describes).

Adds direct, one-sided assertions against the raw written XML for
vectorGraphicStyleName's fill/fillRule/stroke branches (including the
two refusal paths: a dotted stroke style and a non-positive stroke
width), zIndexAttrs' resolved-vs-fallback paint order, each of the
four writeDrawVector element shapes (including the path element's two
refusal paths: no subpaths, and a non-positive frame dimension), and
writeDrawVectors' own baseIndex-plus-position arithmetic -- plus
direct coverage of canonicalDrawVector's own per-field canonicalising
rules (rotationDeg-zero collapse, color quantisation, absent-stroke-
style defaulting, fillRule/subpath carrying, and per-kind field
identity).
nextNoteOrdinal/nextAnnotationOrdinal each only ever had a single
unnamed note/annotation in any existing test, so the counter's own
post-increment could run backwards (or not advance at all) without
any test noticing -- the minted name a SINGLE call produces is
identical either way. Adds a two-construct case for each counter,
asserting the second unnamed note/annotation gets a genuinely
higher-numbered name than the first, not the same name reused or a
decreasing one.
This module (the OpenOffice.org 1.x style:properties splitter/merger)
had no dedicated test file at all -- every reader that touches it
only exercises it indirectly through a whole-document round trip.

Adds direct tests for propertyTypesForContainer's own routing
branches (container-tag lookup, style:style/style:default-style
family lookup, and each of its three undefined-returning cases),
splitStyleProperties' first-match-wins routing and its fallback to
the first candidate for an unrecognised attribute, its own per-
candidate element emission (including omitting an empty candidate),
its non-element-child skip, its child-element routing, both
OpenOffice.org compound-attribute expansions (style:text-underline
and style:text-crossing-out, each with a listed and an unlisted
value), the fo:keep-with-next boolean-to-keyword rewrite, and
mergeStyleProperties' found/not-found result and its multi-child
concatenation and rest-preservation behaviour.
isXmlNode is a hand-written recursive structural guard (z.custom's
own validator, since a genuinely recursive schema collapses to
unknown under z.lazy in this pinned zod version) with no direct
tests at all -- every place it runs is exercised only as a side
effect of parsing a real XML document, which never constructs the
malformed shapes a mutant here would need a test to distinguish.

Covers every branch: non-object/unrecognised-type rejection, each of
text/cdata/comment/declaration/pi/element's own required-field
checks, an element's own attributes/children array-type and per-item
validity checks, and two levels of recursive descent (a valid deeply
nested tree, and one whose innermost grandchild is malformed).
parseXml had no direct unit tests at all -- every reader test in this
package exercises it only indirectly, through a whole XML document
string, which never isolates a single node kind's own mapping.

Adds direct assertions, each against a minimal real XML string, for
every reachable parseNode branch: a bare element, an element's text
content, a comment, a CDATA section, the leading declaration and its
attributes, a non-xml processing instruction, attribute parsing and
ordering, nested-element recursion with mixed element/text children,
and that entities and whitespace are carried through unencoded and
untrimmed (this package's own lossless-round-trip contract).
…-shapes.ts

Every function here (planShapeContent, frameGeometryAttrs, odfZIndexOf,
writeDrawFrame, canonicalDrawShape, writeDrawShapes) was previously only
exercised indirectly through odp/write.test.ts and odg/write.test.ts's own
whole-document round-trip suites, which cannot observe a mutation that
changes what gets written in a way the reader's own inverse tolerates.
… validateManifest

Extracts parseFileEntryElements as the single place that walks a
manifest:manifest root's manifest:file-entry children and enforces the
required manifest:full-path/manifest:media-type attributes.
validateManifest's own encryption-data scan now reuses it instead of
independently re-walking and re-validating the same elements, so its
fullPath is a plain string rather than needing its own redundant
"already surfaced above" undefined check.

Also drops two other redundant conditions found the same way:
resolvePartMediaType's extension === "" fallback (mediaTypeForExtension
never has "" as a key, so skipping the lookup when there is no dot at
all was always equivalent, and does it without the empty-string
placeholder ever risking a nameless part being read as one named
literally after its own extension), and validateManifest's fullPath ===
"/" check (a bare "/" already satisfies its own endsWith("/") check).
…ceholder

parseLineDecoration's strike call site passed a hardcoded (undefined,
"") companion-B pair to represent "no second companion attribute" --
a placeholder no test could ever observe, since strike genuinely has
only one companion (type). companionB is now a single, properly
optional { value, onValue } parameter, so a call site with no second
companion (strike) omits it entirely instead of feeding the function a
value that doesn't correspond to any real ODF attribute.

Also simplifies parsePercentageMultiplier: PERCENTAGE_PATTERN anchors
the numeric portion between ^ and a trailing "%$", so a successful
match's numeric text is always exactly the input with its last
character removed -- reading it back out of a capture group needed a
second, provably-always-true undefined check the type system can't
see through on its own.
…s edge cases

Adds direct coverage for propertyTypesForContainer ignoring a
recognised style:family attribute on a tag that isn't style:style or
default-style, splitStyleProperties throwing on an empty property-type
candidate list, and lineThroughStyleOf's "double-line"/"thick-line"
expansions alongside the values already covered.
…eduping

computeUsedRange had no direct test coverage for six of its independent
grid-extension sources (columns/rows/images/embeddedObjects/data-validation
ranges/printSettings), so mutants deleting any one of those loop bodies or
calls went undetected as long as the sheet's cells alone already drove the
grid large enough. Adds one test per source, each with an otherwise-empty
cells array, proving that source alone extends the correct axis and never
the other.

Also covers internContentValidation's dedup/naming (two rules with
identical written content share one definition; three distinct rules mint
val1/val2/val3 in encounter order; allowBlank left unset and allowBlank
explicitly true dedupe to the same definition since both mean the same
written content) and writeValidationMessage's no-message and multi-line
body cases.
…optional fields

Both tests used toEqual, which treats an explicit key set to undefined
as equal to the key being absent entirely. A mutated optional-field
assignment flipped to always-true produces exactly that shape, since
assigning an undefined value still adds the key.

Switching to toStrictEqual, plus a new canonicalCell case pinning that
none of its eight optional fields appear at all when none are stated,
makes the assertions actually distinguish field-present from
field-absent the way the two tests already claimed to.
… block

The same masking toEqual already had for canonicalRun/canonicalCell
applies to every conditional-spread field in canonicalConditionalFormats
and its siblings: a mutant that flips a "field !== undefined" guard to
always-true still spreads in a key whose value is the underlying
undefined, and toEqual treats that key as equal to the key being absent.
Bulk-converting every toEqual in this describe block to toStrictEqual
across canonicalCellFill/canonicalDataValidations/canonicalConditionalFormatStyle/
canonicalConditionalFormats makes each "only when present" assertion in
this file actually enforce that, not just the fields already spot-checked
directly.
…sition

writeRowCells's own cell-vs-repeat branch checked cellByPosition and
imagesByPosition but never objectsByPosition, so a position anchoring
only an embedded object (no real cell, no image) fell through to the
covered/repeat branch and the object's own draw:frame was never
written at all -- silently dropping the whole embedded object, despite
computeUsedRange's own comment already stating the intent that "an
anchor past the content grid has to materialise that cell exactly as
an image anchor does". Adds the missing objects check alongside the
existing cell/images ones.

Also covers the sequential counters writeSheetImageFrame,
writeSheetEmbeddedObjectFrame, and writeSheet mint per instance:
Pictures/imageN.png paths and draw:z-index across multiple images,
Object N directories across multiple embedded objects, and a distinct
per-sheet table:style-name across multiple sheets.
…index

The multi-image test only proved nextZIndex increments through
writeSheetImageFrame; writeSheetEmbeddedObjectFrame draws from the same
counter via its own separate nextZIndex++ call, which had no coverage
of its own. Extends the existing multi-object test to also assert the
two objects' draw:z-index values are 0 and 1, not both 0.
Two mutants survived because every existing malformed-input test happened
to give the same isXmlNode result under both the real and mutated guard:
a bogus type with no other fields fails the downstream tag/attributes
check either way, and a primitive with no matching property naturally
returns undefined for value.type regardless of which typeof check ran.
Added a bogus-type object shaped exactly like a valid element (only the
type check itself can reject it), a function value carrying valid
text-node properties (typeof narrows to "function", not "object", so
only that distinction rejects it), and a declaration attribute with a
non-string name but a valid string value (isolating the name check from
the value check the existing malformed-attribute test already covers).
…rvivors

Three mutants survived on write-vectors.ts: an explicit "solid" stroke
style was never exercised separately from an absent style (both take the
same code path today, so only the explicit value distinguishes the OR's
own second branch), a path frame with a height of exactly zero was never
tested (only a negative height was, which the >= mutant still rejects the
same way), and canonicalDrawVector's rotationDeg-omission was asserted
with toBeUndefined(), which reads identically whether the key is absent
or present with value undefined -- switched to not.toHaveProperty to
distinguish the two.
Four mutants survived on typed/odb/write.ts: xlink:type="simple" on both
the connection resource and a component writes a value the reader never
checks at all, so no round-trip test could ever observe it changing to
an empty string -- added a direct assertion against the raw written
attribute. Separately, a caller-supplied version threaded through both
the ?? fallback and the syncManifest call, but every existing test
either omitted the option entirely or never checked office:version or
manifest:version against it, so options.version && DEFAULT_ODF_VERSION
and syncManifest(pkg, {}) both silently discarded the caller's own
non-default value without any test noticing -- added a case supplying
"1.2" and asserting it lands on both.
…vors

canonicalRun and canonicalMetadata each guard optional fields with
"if (x !== undefined) canonical.x = x.value", and every existing test
compared with toEqual, which ignores an explicit undefined-valued
property -- so a mutant forcing any one guard to "if (true)" (setting
that field to undefined unconditionally instead of leaving the key
absent) read as equal to the field-omitted expectation and survived.
Switched every canonicalRun assertion and canonicalMetadata's own
all-fields-absent case to toStrictEqual, which does distinguish an
absent key from one explicitly set to undefined. Same fix for a table
row's heightPt.

canonicalTable's own covering-cell loop had two further gaps: nothing
tested that a cell already known covered never lets its OWN stated
colSpan/rowSpan mark further cells covered (the covering pass is meant
to run only for the anchor, never for a cell already inside another
anchor's reach), and the rowSpan=2 test only checked the covered
column, never that an UNcovered column beyond it survives untouched.
Restructured the covering loop itself to exclude the anchor's own
position structurally (splitting into a first-row tail and the
remaining rows, rather than a runtime "unless this is the anchor"
check every iteration paid for) rather than leave a mutation
opportunity Stryker could flip to reach it; the two remaining survivors
on that restructured loop's own start bounds are genuinely irreducible
equivalent mutants, documented in place -- row/column indices are
always non-negative in real input, so a "- 1" start only ever adds
covered.add() calls for either a fictional negative-index key no real
cell can occupy or the anchor's own key (already established never to
be looked up again once its own isCovered check has run), and cells
are visited exactly once each in one left-to-right, top-to-bottom pass.

Also switched the "closes the list plan after the whole table" test to
a genuinely distinguishing scenario: the original used two DIFFERENT
raw numIds either side of the table boundary, which always mints a
fresh run regardless of whether the state was actually closed. The new
version reuses the identical raw numId on both sides, which can only
canonicalise to different numIds if the trailing close genuinely ran.
…loor

A genuine cold full-package run (incremental cache deleted first) found
91.10% of 8201 valid mutants, replacing the earlier provisional 74
threshold with 89, derived by this file's own stated rule. 612 survivors
and 127 no-coverage mutants remain across many files -- still far from
100%, so the threshold stays a conservative floor rather than a target.
@Mearman
Mearman force-pushed the feat/100-percent-mutation-odf.js branch from 62ed1b9 to 5634b16 Compare September 16, 2026 00:58
…asurement

The prior comment recorded a local pre-push run's figures (91.10%, 612 survived).
The workflow's own shard-2 run for this branch measured 91.40% of 8302 valid mutants
(587 survived, 127 no-coverage, 112 timeout, 7476 killed), the same derivation still
landing on a break threshold of 89. Reorders the concentrated-in file list to match
the CI-measured survivor+no-coverage weight per file.
@Mearman
Mearman marked this pull request as ready for review September 16, 2026 03:17
@Mearman
Mearman merged commit 4a4dcf6 into main Sep 16, 2026
26 checks passed
@Mearman
Mearman deleted the feat/100-percent-mutation-odf.js branch September 16, 2026 03:18
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 16, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
🔒 Security Review Completed 2026-09-16T03:23:44.475564Z d41caf6 Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@github-actions

Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 7.25.5 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant