Skip to content

xls-codec: work toward a genuine 100% mutation score - #1263

Merged
Mearman merged 108 commits into
mainfrom
feat/100-percent-mutation-xls-codec
Sep 15, 2026
Merged

Mearman merged 108 commits into
mainfrom
feat/100-percent-mutation-xls-codec

Conversation

@Mearman

@Mearman Mearman commented Sep 12, 2026

Copy link
Copy Markdown
Member

Adds direct unit tests for xls-codec modules that previously had no test file of their own and relied entirely on incidental coverage from content.test.ts/write.test.ts's full round trips, and restructures genuinely equivalent-mutant boundaries out of existence rather than chasing them with tests.

Final state: a genuine 100.00% mutation score, confirmed by a fresh, non-incremental, full package-wide Stryker run: 4546 killed, 75 timeout (counted as killed), 0 survived, 0 no-coverage. breakThreshold in stryker.config.ts is now set to 100. No // Stryker disable comment exists anywhere in this package (grep -rn "Stryker disable" packages/xls-codec/src returns nothing), and none was ever added — every mutant is closed either by a genuine isolating test or by restructuring the code so the mutation opportunity no longer exists as an AST node.

Files brought to 100% this session, on top of everything closed earlier in the branch's history (biff/strings.ts, biff/string-writer.ts, test-support/biff.ts, biff/xf-colors.ts, drawing/blips.ts, drawing/escher-writer.ts, test-support/cfb.ts, biff/ptg-writer.ts, workbook/conditional-format-write.ts, workbook/data-validation-write.ts, workbook/globals-writer.ts, workbook/sheet-writer.ts/write.ts, biff/write-errors.ts, biff/rk.ts, biff/record-writer.ts, units.ts, biff/print-setup.ts, biff/records.ts, drawing/md4.ts, workbook/conditional-format-ex.ts, container.ts, biff/substreams.ts, workbook/comment-writer.ts, workbook/comments.ts, workbook/encryption.ts, workbook/embedded-object.ts): workbook/chart.ts, workbook/sheet.ts, workbook/drawing-writer.ts, workbook/globals.ts, workbook/drawing.ts, workbook/defined-names.ts, write.ts.

Representative techniques from the closing pass:

  • workbook/chart.ts: removed a dead trailing default: break;, a redundant default: return; already covered by the next line's undefined check, and both special-case branches of pointInRange (the general row-major formula already reduces to each of them exactly); deleted OwnSheetRange.endRow, a field computed at both its construction sites but never read anywhere in the file.
  • workbook/drawing.ts / workbook/drawing-writer.ts: exported several previously-private helpers so their boundaries are directly testable; replaced a Math.min-bounded shape/Obj pairing loop with one that breaks the moment either array is exhausted (the earliest-correct, unambiguous termination point, since neither array has genuine holes); restructured bytesFromBase64 to slice trailing padding off up front and throw on a genuinely invalid character instead of silently skipping it.
  • test-support/cfb.ts: replaced every counted for (let i=0; i<N; i++) loop with for (const i of Array(N).keys()), removing the comparison from the AST entirely; paired parallel arrays (smallStreamRecords/miniChunks, bigStreamRecords/sector counts) into single arrays of tuples to eliminate index-correlation-based "always defined" fallback checks; added requiredLeaf/requiredRecord/requiredSectorStart narrowing helpers, each directly unit-tested against its own contrived impossible input; removed a zero-initialized-buffer no-op write (put32(view, 0x48, 0)) and an unreachable name-length guard.
  • Package-wide: extensive use of empirical mutation-equivalence verification (applying a candidate mutation directly to a copy of the source and running the real test suite against it) to distinguish genuinely equivalent mutants — safe by construction (zero-initialized typed arrays, TypedArray.prototype.set/.subarray clamping behaviour) — from real, testable gaps.

CI note on the mutation-testing workflow's own shard failures: this run's .github/workflows/mutation.yml fell back to testing the entire workspace (23 packages, all 8 shards) rather than just the packages this PR touches, because the plan job's compute-mutation-shards.ts failed to resolve a local main ref against the PR's shallow-but-full checkout (Failed to resolve base ref 'main' ... unknown revision, then "assuming all files have changed") — a pre-existing quirk of that script's ref resolution, not anything in this PR's diff. Within that full-workspace run, xls-codec's own shard reported the exact same genuine 100.00 this PR claims above. The shard failures that resulted are all in packages this PR never touches: document-schema.js at 99.92 (pre-existing, below its own 100 threshold), document-outline.js at 99.83 (same), and a pdf-codec initial-test-run timeout in an unrelated encryption test. Shards 0 and 3 (the documents.js engine package and document-cli/epub-codec/byte-codec) were cancelled by the job's 180-minute timeout, a direct consequence of the same full-workspace fallback forcing a cold run of every package instead of only the ones this PR's diff affects. None of this is caused by, or reproducible from, any change in this PR's diff (packages/xls-codec/** and packages/xls-codec/stryker.config.ts only).

@Mearman
Mearman force-pushed the feat/100-percent-mutation-xls-codec branch 4 times, most recently from 482999a to 7cd0ce5 Compare September 14, 2026 08:07
Adds a dedicated container.test.ts exercising the compound-file
container layer on its own: the legacy 'Book' stream rejection, the
no-Workbook-stream rejection, the CompoundFileFormatError wrapping
into BiffFormatError, SummaryInformation and MBD<hex>/Package
embedding-storage stream selection (including a near-miss path that
must not match), and isXlsFile's own true/false/catch-all cases.
None of these had a direct test before, relying only on incidental
coverage from content.test.ts's full round-trip fixtures.
…dation

The createdIso/modifiedIso validation this package adds on top of
archive-codec's own mapping had no direct test, so a malformed date
in either field, and a malformed createdIso alongside a valid
modifiedIso, are pinned here rather than only reachable through a
full writeXlsContent round trip.
…CellRecord

Each of the five independent conditions cellCarriesFormatting checks
(background, alignment, verticalAlignment, font, and each of the four
border sides) gets its own cell that carries only that one property,
so a mutant turning any one check into a no-op or an && instead of ||
fails a test that isolates it -- the write-path round trips in
write.test.ts only ever combine several of them at once.
writeEmbeddedObjectPackage/readEmbeddedObjectPackage round-trips
through write.test.ts before now only ever exercised the accepting
path. Adds direct coverage of the foreign-label, non-object,
missing-objectKind, missing-document, and schema-validation-failure
rejections, the placement fields' deliberate non-round-trip, and the
source residue field, plus bytes that are not a Package stream at all.
Direct coverage for a module previously exercised only through
write.test.ts's full round trips: no commented cells, one comment's
position/text/author, an absent author staying absent rather than an
empty-string placeholder, an empty-text comment writing no Continue
record, multi-comment round trips, the row-then-column Note ordering
independent of input order, the Note-records-first-then-Obj/TxO-pairs
emission order, sequential object id assignment, and the 16-bit
FtCmo.id ceiling.
…spatch edges

Direct coverage of paths content.test.ts's full CFB round trips never
reach: an unrecognised wEncryptionType, RC4 CryptoAPI's own
EncryptionVersionInfo rejected by name, the no-password message naming
the right scheme for each of RC4/XOR, an XOR password too long for
obfuscation to represent folding into "incorrect password" rather than
a raw RangeError, and -- for both RC4 and XOR -- the never-encrypted
record-type bypass and the BoundSheet8 lbPlyPos-preserved special case,
built directly against BiffRecord values rather than a full compound
file.
…tests

The previous commit's fixtures used a nonexistent Color.rgbHex field
and an invalid ContentStrokeStyle value ("thin", not one of
solid/dashed/dotted/double), which typechecked as vitest's own loose
mock inference but failed tsc -p tsconfig.node.json outright.
…ently

FTAB_NAMES/FTAB_FIXED_ARITY/FTAB_IFTAB_BY_NAME are built from one
372-entry literal table with no test of its own -- formula tests
elsewhere only ever exercise a handful of these functions by name, so
every other entry's own string and arity literals had no test able to
notice a change. Transcribes the full published Ftab table ([MS-XLS]
2.5.198.17) as an independent reference array and checks every entry
against it, rather than deriving the expectation from the module
under test itself.
…t branches directly

Adds a dedicated cfb.test.ts for the test-support compound-file writer, previously
exercised only incidentally through container.test.ts/content.test.ts's higher-level
round trips: the path-segment and entry-name validation errors, the mini-stream vs
FAT-chained big-stream cutoff, multi-sector FAT/directory chains, and both
major-version (3 and 4) header layouts.
…list, not recursion

compileNode walked a formula's own AST by native recursion, one JavaScript
call frame per operator -- a long but legitimate chain of many thousands of
binary operators (a generated SUM(...)+SUM(...)+... expression, say) would
overflow the stack at a tree depth far shallower than MAX_RGCE_LENGTH's own
65535-byte ceiling ever needs throwing for. Rewrites it as an iterative
post-order walk over an explicit worklist held on the heap, so compiling
degrades gracefully to that ceiling's own BiffWriteError instead of an
uncontrolled RangeError.

Also removes two conditions from numberNode that can never affect its
result: NUMBER_RE never captures a sign or a non-digit character, so a
token already matching the plain-digit form always parses to a
non-negative whole number regardless of magnitude, making
Number.isInteger(value) and value >= 0 restate a fact the regex already
established rather than narrow it further.

Finally, replaces the parser's own "token stream ran past its own end"
throw -- unreachable, since tokenize() always appends one trailing eof
token and advance() only ever fires once the current token is confirmed
non-eof -- with a fallback to that same shared eof token, avoiding both a
untested throw and (this package's non-null assertions are lint errors) a
type assertion to state the invariant instead.
…mpiler directly

Adds a dedicated ptg-writer.test.ts, previously exercised only incidentally
through cell/data-validation/conditional-format formulas elsewhere in the
package: every whitespace character, integer/decimal/exponent number
literals either side of the PtgInt/PtgNum boundary, PtgNum's own
little-endian float encoding, string literals including doubled-quote
escaping and the unterminated-string error, all eight BIFF8 error
literals and the two ways an error literal can be malformed, TRUE/FALSE,
every comparison/arithmetic/unary/percent operator and their precedence
against each other, cell and area references across every combination of
$-absolute flags and both grid boundaries (row and column, upper and
lower), fixed- and variable-arity function calls including omitted
arguments and the 255-argument PtgFuncVar ceiling, and the writer's own
error messages for a malformed formula at every stage from tokenizing
through compiling.
…stive switches

CP_BY_OPERATOR, SIMPLE_KIND_TO_ICF_TEMPLATE, and CTP_BY_TEXT_TYPE were each
a ReadonlyMap covering every member of a closed string-literal union, with
an "operator/rule type has no value" throw guarding a Map.get() miss that
can never actually happen given the union those types already close over
-- but a Map's own .get() always types its result as possibly undefined
regardless of how completely its literal entries cover the key type, so
the throw stayed live and untestable. Replaces each with a real exhaustive
switch statement instead, which the compiler itself checks covers every
union member, so the impossible branch and its message are gone rather
than merely unreachable. cfvoTypeCodeOf, already a switch, drops its own
equivalent default case the same way.

Also removes two structurally unreachable "internal error" throws by
threading validated data instead of re-deriving it: validateRuleGrid now
returns a rule's ranges narrowed to a provably non-empty tuple rather than
void, so its own caller can pass a rule's real first range down to
writeCf12Record/textRuleFormula as a plain required parameter instead of
that function re-indexing rule.ranges[0] and guarding against an absence
validateRuleGrid, called moments earlier on the same rule, already rules
out. assignPriorities now returns each CF12 rule zipped together with its
own resolved ipriority, rather than a same-length array of bare numbers
the caller re-correlated to its own rule list by array index -- removing
the "fewer priorities than rules" throw that guarded against the two
arrays ever silently drifting out of step, since there is no longer a
second array to drift.
…bytes directly

Adds the boolean-flag combinations the existing conditional-format round
trips left only half-tested (each ternary needs both its own true and
false input to distinguish it from a mutant that always takes one
branch): a data bar's shown value, an icon set's hidden value with no
reverse, a top10 rule selecting by count from the top, every
aboveAverage/equalAverage combination, and a rule declaring stopIfTrue.
Also adds a percentile- and formula-typed colour-scale stop, and the
"threshold carries no value" refusal for a value-bearing CFVO type.

Adds a new describe block calling writeSheetConditionalFormats directly
to check two things no round trip through the reader can ever observe:
that a style-less rule's own DXFN12 block writes cbDxf as a genuine 0
rather than a padded-but-still-empty block (the reader treats both
identically, since it degrades on flag bits rather than data length), and
that a colour scale's own fixed interpolation-position floats (0.0/1.0
for two stops, 0.0/0.5/1.0 for three) are the ones [MS-XLS] 2.5.33 itself
pins per stop count rather than the other set -- values the reader skips
over as unused padding and so never round-trips into anything observable.
…trictEqual

Every reader function in this package builds its result through a chain
of `...(x !== undefined ? { key: x } : {})` spreads, so an optional field
the source omits entirely and one a mutant forces to `{ key: undefined }`
produce objects that plain .toEqual cannot tell apart -- Jest/Vitest's own
loose equality treats a missing key and one explicitly set to undefined
as the same thing, so a whole family of "if (x !== undefined)" mutants
across content.ts, conditional-format-write.ts, and elsewhere survived
regardless of how many present/absent fixtures a round trip already
covered. Switches every .toEqual in this package's own test suite to
.toStrictEqual, which does distinguish the two, catching that entire
mutant family in one pass rather than needing a bespoke assertion per
optional field.

Fixes the two fixtures this actually caught as genuinely under-specified:
conditional-format-12.test.ts's raw CF12 reader tests for a plain top10
rule and for every operand-free icfTemplate now state style: undefined
explicitly, matching RawConditionalFormat12's own field (declared without
a `?`, so always present even when its value is absent) the same way
every other fixture in that file already does.

One fixture is intentionally left on .toEqual, not upgraded: reading a
hand-built SummaryInformation stream that states only title/author/
createdIso surfaces every other LayoutMetadata field as an explicit
undefined rather than omitted, because archive-codec's own shared
summaryInformationToLayoutMetadata (also used by doc-codec and ppt-codec)
states every field unconditionally -- a genuine, if minor, contract
inconsistency belonging to that shared package rather than this one.
…s directly

Adds the writeSheetDataValidations error paths the existing round trips
never exercised: an unrecognised type/operator string (via a deliberately
schema-violating fixture, the same as this file's own existing wrong-
arity/missing-formula tests), a comparison rule carrying a second formula
its own operator doesn't take two of, an empty ranges array, and each of
the four grid-boundary checks (row/column, both edges) individually.

Also calls writeSheetDataValidations directly for the one case a round
trip through the reader cannot observe: an empty dataValidations array
must write no Dval/Dv records at all, not a Dval stating a zero rule
count -- content.ts's own mapDataValidations().length > 0 check already
omits the field for either shape on the way back in, so only inspecting
the writer's own output distinguishes them.
…er ignores

Adds a dedicated globals-writer.test.ts calling buildWorkbookGlobals
directly: the fifteen built-in STYLE records (globals.ts's own reader
never looks for RECORD_STYLE at all, so no round trip can tell whether
they were written), the SST record's presence gated correctly on whether
the workbook actually carries shared strings, and an ExternSheet stating
exactly one XTI per sheet rather than one too many.
…alue gaps

Adds the grid-boundary opposite cases checkedCellPosition's own OR-chain
needed (a row-only violation, and the exact last valid row/column
succeeding rather than throwing), a sheet whose cells carry no row/column
metadata staying with empty rows/columns arrays rather than one every
mutation of the fDyZero/fUnsynced/hidden flags would also leave
unchanged, and a merge spanning only rows or only columns rather than
always both together.

Adds formula-cached date/time/date-time and (checking the value, not just
the formula text) error results, none of which the existing cached-result
tests exercised. Adds a row-only and a column-only manual page break, and
a custom page size's own Setup record fPortrait bit read directly (custom
page-size dimensions never round-trip at all, so no test through
readXlsContent could otherwise tell portrait from landscape here).

Also adds a dedicated describe block calling buildWorksheetSubstream
directly for the Dimensions record's own rwMic/rwMac/colMic/colMac bytes,
which content.ts reads into RawSheet.usedRange but never maps into a
ContentSheet field, so no round trip observes them either.
… pass

buildFormatPlan, buildPalettePlan, and buildFontPlan each repeated an
identical "if (!writesCellRecord(cell)) continue" guard inside their own
cell loop -- genuinely dead code today (an unwritten cell can never carry
a font/colour/format these scans would otherwise register, since
writesCellRecord being false already implies cellCarriesFormatting is
false too), but still real, load-bearing protection against a future
change making one of these scans disagree with sheet-writer.ts's own
record-emission predicate about which cells matter. Filtering once via
sheet.cells.filter(writesCellRecord) at each loop's own head keeps that
protection -- every pass still filters through the identical shared
predicate -- while removing the standalone if/continue three separate
mutation opportunities were hiding behind despite it being unreachable by
construction.

Also adds the one case that genuinely was an observable gap: two cells
bordered identically on different single sides, distinguishing the
decoration-signature string's own side-prefix characters ("l"/"r"/"t"/
"b") from each other -- previously only ever exercised by a single cell
carrying two different sides, which a swapped or blanked prefix could not
have been told apart from.
…ndaries

Every existing applyTint test used pure red or exact grey, both of which
happen to compute an exact 0.5 lightness and a g === b tie -- so no test
ever selected rgbToHsl's own l > 0.5 saturation branch, its max === g or
max === b hue branches, or the g < b tie-break's true side, and
hueToRgb's own wraparound and midpoint branches went similarly
unexercised by any colour whose computed hue actually landed there.

Adds four colours chosen to land in those specific branches, each
checked against an independent reference implementation of the same
documented W3C HSL algorithm (a copy Stryker's mutations to the source
file can never touch, so a mutated formula and this reference disagree
exactly where the mutation changed something) rather than by hand-derived
expected values.

Also adds exact boundary tests for resolveIcvColor's own palette-range
check: icv 63 (the last valid palette index) and 64 (one past it).
Adds direct write-path tests for validateUserName's 255-character cch
ceiling, scopeOf's out-of-range scopeSheetIndex refusal, and
compileRefersTo's MAX_ROW_INDEX/MAX_COLUMN_INDEX grid boundary, each
exercising both the accepted edge and the refused one-past-it case.
No test asserted the class's own `.name` override or that its message
survives construction, so a mutant clearing `this.name` to an empty
string went undetected.
A freshly allocated ArrayBuffer is already zero-filled, and endianness
has no observable effect on a word of all-zero bytes, so the explicit
setUint32(4, 0, ...) call restated a fact the buffer already held
rather than a real one about the format.
The refusal only asserted the error's class, not that its message
actually names the record's own type and length -- a mutant clearing
the template string to an empty one went undetected.
The existing round-trip and never-narrower properties both still hold
for a coldx computed by adding the digit-width allowance instead of
subtracting it, so neither killed a mutant flipping that sign. A
direct exact-value assertion does.
…ance boundary

Neither the landscape check's requirement that BOTH dimensions match,
nor the tolerance comparison's own boundary, had a test that would
fail if the conjunction were loosened to a disjunction or the
tolerance's own <= narrowed to a strict <.
The identical `if (!(err instanceof BiffFormatError)) throw err` guard
was duplicated at every one of dozens of per-record recovery
boundaries across workbook/ and biff/, each one separately exposed to
the same handful of mutations (the condition inverted, the guard's own
block emptied) with no test anywhere actually proving a genuine bug
still propagates rather than being silently absorbed alongside a
malformed record. Centralising the classification into one function,
tested directly against both a real BiffFormatError and a genuine
TypeError, removes that duplicated surface everywhere it is adopted.
writeSheetConditionalFormats' own empty-rules early return produced
exactly the output the general path already produces for zero rules
(validateRuleCount(0) never throws, and an empty forEach plus
assignPriorities([]) both no-op), so the special case stated nothing
the general path didn't already state on its own.

writeCfFilter recomputed a top10 rule's fTop/fPercent flags byte a
second time, independently from the identical computation
writeCf12Record already makes for CFExFilterParams -- this package's
own reader (conditional-format-12.ts) reads only the latter, so the
two computations could silently diverge with no way to notice. Both
now share one flags value, computed once and passed through.

textRuleFormula is exported so its four formula shapes (one per
text-predicate rule kind) are directly testable: the shape a
containsText/notContainsText/beginsWith/endsWith rule's formula takes
is otherwise invisible to a round trip, since this package's own
reader determines a text rule's kind from CFExTextTemplateParams' ctp
field rather than from the formula's own shape.
…signment

Adds direct textRuleFormula assertions for each of the four
text-predicate formula shapes, isolated top10/stopIfTrue boolean
branches (bottom/percent selected independently, each of
colorScale/dataBar/iconSet refused on its own rather than only
dataBar), exact-boundary range acceptance at BIFF8's own four grid
edges (alongside the existing one-past-the-edge refusal), a refusal
at exactly 32768 conditional-format rules built from cheap
placeholder objects so it exercises the real write path without the
cost of constructing that many genuine rules, and raw-record nID
sequencing checks for both the base CondFmt and CF12 record families,
since nID's own correctness is invisible to every round-trip test
(this package's reader never emits or resolves a CFEx cross-reference
on a self-written file, and explicitly skips CondFmt12's own copy of
the field).
…port testable seams

readSheetDrawing's own empty-drawingChunks early return produced
exactly the output the general path already produces: concatBytes([])
is an empty Uint8Array, and readSheetShapes' own readEscherRecords
loop condition (offset < bytes.length) never runs against a
zero-length stream, so shapes stays [] and the pairing loop below
never executes either way.

The worksheet-record loop's own index<worksheetRecords.length bound
and the pair loop's own Math.min(shapes.length, objEntries.length)
count were both redundant with the undefined-guards already inside
each loop body -- an index one past either array's real length reads
back undefined and gets skipped exactly like the existing "no
shape"/"no obj"/note-type cases already are, so the exact bound
(min, max, or anything in between) is unobservable. The worksheet
loop is rewritten over `.entries()`; the pair loop now runs until
both arrays are simultaneously exhausted, with no separate count to
compute or bound to compare against.

SheetGridGeometry, resolveAnchorPlacement, embeddedObjectFromObjRecord,
imageFromShape, chartFromShape, and drawingObjectFromShape are
exported so each can be exercised directly with a minimal
DrawingShape/geometry fixture, rather than only through a full
Escher-byte round trip that makes several of their own internal
branches (a real declared column width, an isolated zero-width vs
zero-height anchor, a chart substream's own offset-bound edges)
prohibitively fiddly to hit precisely.
…iring edges

Adds direct SheetGridGeometry tests proving a column/row's own
declared width/height is used (not the default) and that xPt/yPt sum
only the entries strictly before the one asked for; isolated
zero-width vs zero-height anchor tests for imageFromShape,
embeddedObjectFromObjRecord, and drawingObjectFromShape (the same
widthPt<=0||heightPt<=0 guard appears in all three); an exact
full-document assertion for drawingObjectFromShape's own generated
page/shape; readSheetDrawing tests isolating obj.ot===PICTURE from
shape.shapeType===PICTURE_FRAME (each sufficient on its own) and
covering a sheet whose shape and Obj-record counts genuinely
disagree; and chartFromShape tests isolating each of its three
substream-matching conditions (chart-typed, after the Obj record,
before the next one) plus an exact assertion of the generated
single-sheet chart document among several near-miss candidate
substreams.
…austed side

The loop's own break condition required BOTH arrays to run out before
stopping, waiting through however many extra no-op iterations the
longer array still had left -- but since neither array has genuine
holes, the index where one first goes missing is also the index
where every later index goes missing too, so no real pair can exist
past that point regardless of the other array's own remaining
length. Breaking the moment EITHER side is exhausted reaches the
identical result without the pointless trailing iterations, and
folds the note-type skip into its own single, separate condition
instead of sharing one compound guard with the exhaustion check.
…ctFromObjRecord's guard tests

The zero-size anchor tests previously paired a resolvable storage id
with arbitrary, non-Package bytes -- readEmbeddedObjectPackage's own
foreign-payload degrade already returns undefined for content that
isn't a real OLE Package carrying this codec's own JSON payload, so
the tests could not tell a genuine size-guard rejection apart from
the downstream parse simply failing on garbage input regardless of
size. Building the fixture through writeEmbeddedObjectPackage itself
gives the tests real, parseable bytes, so a defined result is the
only possible outcome once the size guard no longer runs.
…d index

resolveBlip's own repeat-image path looked a stored blip back up by
indexing into the blips array with a previously-minted index, guarded
by an "internal error" throw for an index the array itself somehow
didn't recognise -- a case that can never actually arise, since the
index handed back for any base64 already in blipIndexByBase64 is
exactly blips.length was at the moment that entry was pushed, and
blips only ever grows. Keying the map directly by the blip object
(alongside its index) removes the array indexing this dedup path
never needed in the first place, so there is no possible-but-provably-
unreachable branch left to guard against.
…byte layout

Adds a dedicated drawing-writer.test.ts exercising the module's own
internal functions directly, exported specifically for this: exact
locateX/locateY boundary behaviour at each grid edge and at a plain
mid-column/mid-row point (proportional, not the offset scaled by the
width/height a second time); placementOfImage's grid-boundary refusal
isolated per edge, at each edge's exact boundary; placementOfEmbedded's
offset arithmetic; byte-exact FtCmo/FtCf/FtPioGrbit/FtPictFmla field
checks; bytesFromBase64's own padding-character handling (previously
untested by any fixture in this package's suite, every one of which
happened to use base64 needing no padding at all); and
buildDrawingWritePlan checks that read the actual written Escher
bytes directly -- a Blip Store entry's own cRef reference count, an
OLE-embedded shape's fOleShape bit versus a plain picture's, an Obj
record's own id continuing past a sheet's comment count, an Embedding
Storage path's uppercase hex spelling, and the FDGG block's own
spidMax/cspSaved/cdgSaved across two sheets -- none of which this
package's own reader ever reads back, so a round trip alone cannot
prove any of them correct.
…ilently skipping it

bytesFromBase64 skipped any character outside the base64 alphabet
with no distinction between genuine trailing padding and a stray
invalid character elsewhere in the string -- and skipping padding
specifically was already provably unobservable regardless of whether
the skip ran, since the output array is pre-sized to exactly the real
decoded byte count, so anything a mishandled padding character could
have written would only ever land at or past that length, where a
typed array write is a silent no-op. Padding is now sliced off up
front instead of skipped inside the loop, and any character that
still isn't part of the alphabet after that is a genuine malformed
payload, refused by name rather than quietly dropped.
…oss-sheet spids

Adds direct writePictureObjRecord/writeEmbeddedObjRecord byte checks
for FtPioGrbit's own fAutoPict bit, a jpeg-accepted counterpart to
the existing gif-refused test, and a base64-alphabet-refusal test
matching the source's new behaviour. Replaces the single-item
storage-id, object-id, and drawing-id checks with multi-item ones:
11 embedded objects to prove storage ids increment sequentially and
reach into the hex alphabet's own uppercase letters past single
digits, three cells (two commented, one not) with two images to
prove the comment-count filter and the object-id sequence both count
correctly, and two sheets each carrying a real image to prove their
own FDG drawingId and shape spids are distinct and increasing rather
than colliding or resetting.
padding is only ever 0, 1, or 2, and slicing zero characters off the
end of a string returns the identical string -- so the "no padding"
branch of the padding>0 ternary was just base64.slice(0, base64.length)
restated as a separate case, not a genuinely different one. Slicing
unconditionally removes the redundant condition entirely.
…tInRange's redundant edge cases

readChartSeries' own record-type switch ended in a bare default:
break, the last case in the switch and no different in effect from
having no default arm at all.

pointInRange special-cased a single-row range (walk across columns)
and a single-column range (walk down rows) ahead of the general
row-major formula -- but with width always the range's own real
column count and index always staying within the range's own point
count for a well-formed chart, Math.floor(index / width) can never
reach a second row when the range is one row tall, and index % width
is always 0 when the range is one column wide, so the general
formula already reduces to exactly what each special case computed
by hand. Both are removed, leaving the one formula that handles
every range shape a real chart can carry.
…ge resolution

Adds direct coverage for readChartSeries' own dispatch and cache
logic that the existing four tests never reached: a SeriesText cache
only applying when it follows a name-role AI, not values/categories;
a range-reference AI name (id=0, rt=range) resolved from the
referenced cell; literal and auto-generated (rt=0/1) AI records whose
own token bytes happen to share a range opcode's byte shape, proving
rt itself gates the range interpretation rather than the bytes
alone; an AI id naming neither values nor categories (bubble size);
an SIIndex naming neither cache role; BoolErr's own error-text and
TRUE/FALSE cache branches; a Blank record's own no-op cache
contribution; single-row, single-column, and genuine rectangular
range walks; empty-formula guards and the PtgParen display wrapper
for both literal and range tokens; PtgInt/PtgNum literals; each of
PtgRef3d's and PtgArea3d's own three class-variant opcodes; a
reversed row/column pair normalised through Math.min/max; and
isOwnSheetRef's own four ways to answer "not the owning sheet" (a
different sheet with no cache to mask it, a genuinely multi-sheet 3D
reference spanning the owning sheet, an unresolvable ixti, and a
genuinely external workbook reference).
…return

A cached record type addCacheEntry doesn't recognise (chiefly a
Blank) left text at its own initial undefined either way -- the
switch's default case returned early to state that explicitly, but
the very next line already returns early for exactly that condition,
so the default case said nothing the following check didn't already
say on its own.
Adds a genuine unmatched-cell own-sheet range test (proving the
displayText fallback path itself, not just the no-range case),
a PtgRef3d cross-sheet test mirroring the existing PtgArea3d one,
an unrecognised range-token opcode falling through to undefined,
a reversed row pair normalised through Math.min/max (the row
counterpart to the existing reversed-column test), and two cached
points under the same role proving the cache's own lazy-init guards
keep earlier entries rather than starting the role over on every
call.
pointInRange's row-major walk only ever reads a range's startRow and
its width (derived from startColumn/endColumn) -- a well-formed
chart's own point count stays within the range's real extent, so
nothing here ever needs to know where the range's last row actually
is. endRow was computed at both of readRangeToken's own construction
sites and never read anywhere after.
…correcting bounds

Every counted loop and Map/array lookup this test-support compound-
file writer made defensively (a name length TypeScript can prove
non-empty but the type system can't, a sibling/child lookup a single
upstream tree walk always populates before it's ever read, an
off-by-one loop bound whose extra iteration writes a byte the
target buffer was already zero at, or a subarray/typed-array set
that clamps to the buffer's own real end) turned out to be provably
unreachable or genuinely unobservable, confirmed by applying each
mutation directly and running the real reader round-trip suite
against it rather than reasoning about it in the abstract.

Restructured accordingly: checkedName drops its own unreachable
empty-name check (every name reaching it already comes from a
validated path segment or the root's own fixed "Root Entry"); the
path-splitting and tree-linking Map/array lookups route through two
small, directly-tested requiredLeaf/requiredRecord/requiredSectorStart
helpers instead of silent `?? fallback` values; the small- and
big-stream sector-allocation passes pair each record with its own
derived value in one array (miniEntries/bigEntries) rather than two
same-length arrays walked by a shared index; counted loops that
build or copy a fixed number of sectors iterate `Array(n).keys()`
instead of a hand-written `i < n` comparison; and the two truly
redundant special cases (an empty mini stream's own zero-byte copy,
and Math.ceil(0 / x) already being 0 without a separate zero check)
are removed outright.
…, and raw byte layout

Adds a DEL-byte (0x7f)/one-past (0x80) boundary pair for checkedName's
own ASCII check; a same-named stream-then-storage test proving the
two are never confused for one another; a two-streams-under-one-
storage test proving a second child reuses the storage its sibling
already created rather than duplicating it; direct unit tests for
requiredLeaf/requiredRecord/requiredSectorStart, including the
internal-error branch each throws for an input no real caller ever
produces; and raw DataView-parsed checks for fields no round trip
through archive-codec's own reader ever proves correct on its own -- a
stream entry's own colour flag byte, the root entry's own literal
name bytes, which sectors the DIFAT names as FATSECT versus FREESECT,
and the directory-sector-count header field's own version-3-zero
versus version-4-real-count distinction.
…me storage edges

Adds a storage-name isolation test (an existing differently-named
storage must never be mistaken for the one a new path segment names),
an exact-at-cutoff (4096-byte) stream classified as big rather than
small, an exact total-byte-length assertion for a workbook holding
only a small stream (catching a stream wrongly counted as both small
and big, which the round trip alone tolerates as wasted, unreferenced
space), and three streams together large enough to force a second
mini-FAT sector -- the one shape in this suite where the earlier
sector-copy loop's own start/offset arithmetic actually diverges from
its neighbours' identical values at index zero.
Byte 0x48 (the DIFAT's own sector count) is genuinely, always zero --
this writer never allocates a DIFAT sector of its own (the DIFAT
always fits the header's own 109-entry array), and view is backed by
a freshly-allocated, zero-initialised file buffer, so writing 0 there
again would restate what is already true rather than change anything.
…proves

Adds a directory-entry-count test proving a storage genuinely gets
reused across every stream nested under it (four streams sharing one
storage segment push the directory past a sector boundary a
correctly-deduplicated tree never reaches, catching a lookup that
never finds what it already created), and raw header-field checks for
the minor version and mini-FAT sector count -- both real, spec-
mandated values archive-codec's own reader never reads back, so a
round trip alone cannot prove the writer stated them correctly.
A full, non-incremental Stryker run across the whole package came
back at a genuine 100.00% mutation score (0 survived, 0 no coverage),
confirming every mutation opportunity is either killed by a real
isolating test or removed by restructuring away the equivalent-mutant
AST node. The break threshold now reflects that.
@Mearman
Mearman marked this pull request as ready for review September 15, 2026 10:31
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 15, 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-15T10:39:50.679885Z 6b00cda 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.

@Mearman
Mearman enabled auto-merge (rebase) September 15, 2026 12:24
@Mearman
Mearman merged commit 7f6c424 into main Sep 15, 2026
26 checks passed
@Mearman
Mearman deleted the feat/100-percent-mutation-xls-codec branch September 15, 2026 12:57
@github-actions

Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 4.15.7 🎉

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