Skip to content

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

Draft
Mearman wants to merge 70 commits into
mainfrom
feat/100-percent-mutation-xls-codec
Draft

Mearman wants to merge 70 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 several genuinely equivalent-mutant boundaries out of existence rather than chasing them with tests.

Work continues on this branch toward a genuine, disable-comment-free 100%; this PR stays draft until that is reached and re-verified with a full run.

Baseline: 68.92% of 4166 valid mutants (breakThreshold still 66, unchanged from the baseline derivation until a genuine 100% is verified).

Current state (fresh, non-incremental full run): 86.53% (490 survived + 159 no-coverage remaining of 4819 valid mutants). No // Stryker disable comment exists anywhere in this package at any point (grep -rn "Stryker disable" packages/xls-codec/src returns nothing) and none should ever be 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 now at a genuine 100% this session (on top of everything the earlier progress list below already closed): biff/strings.ts, biff/string-writer.ts, test-support/biff.ts, biff/xf-colors.ts, drawing/blips.ts, drawing/escher-writer.ts (the last of these previously had no test file of its own at all).

Representative fixes from this pass, illustrating the "restructure, don't chase" approach the standing rules require:

  • biff/strings.ts/biff/string-writer.ts/test-support/biff.ts/drawing/blips.ts: a manually bounds-checked chunking loop (for (let i = 0; i < n; i += CHUNK)) whose final boundary iteration is a provable no-op is replaced with Array.from({ length: Math.ceil(n / CHUNK) }, ...), removing the redundant comparison instead of trying to test an unobservable off-by-one.
  • biff/xf-colors.ts: hslToRgb's four-piece hue curve and its two wraparound if adjustments were each continuous at their own boundary by construction (adjacent pieces were chosen to agree exactly where they meet), so a </<= mutation there could only ever disagree with itself about which of two identical values to return. Restated as one non-branching Math.min/Math.max clamp (numerically verified against the original across the full domain any real caller passes), matching the same unification technique rgbToHsl's own s formula already used for an analogous boundary.
  • drawing/blips.ts: two genuinely redundant guards deleted outright (an empty-input check and a past-the-end check, both already handled identically by downstream code), a length pre-check replaced with a try/catch around the reads it existed to protect (using this package's shared recoverFromFormatError classifier), and two dead cursor.skip calls removed (their own position was never read again).
  • drawing/escher-writer.ts: added a dedicated test file, since it had none — only ever exercised incidentally through single-shape/single-drawing round trips that never varied count enough to distinguish spidMax/cspSaved's own arithmetic from a subtly wrong version of the same formula.

Progress so far this session (from the 68.92% baseline, carried forward from earlier in this branch's history):

  • src/test-support/cfb.ts: dedicated cfb.test.ts covering the compound-file writer's validation and sector-layout branches directly.
  • src/biff/ptg-writer.ts: full dedicated ptg-writer.test.ts (tokenizer/parser/compiler, ~144 mutants addressed); refactored compileNode to an iterative worklist (removing a real stack-overflow risk on long formulas) and eliminated two provably-unreachable defensive branches by restructuring rather than testing around them.
  • src/workbook/conditional-format-write.ts: replaced three unreachable Map-lookup guards with exhaustive switches the compiler itself proves complete; threaded validated data (a rule's first range, each CF12 entry's resolved priority) instead of re-deriving it with a redundant undefined-check; added round-trip and direct-byte-level tests for every remaining gap (~85 mutants addressed).
  • Package-wide: switched every .toEqual in this package's test suite to .toStrictEqual, killing a whole family of "optional field written as {key: undefined} instead of omitted" mutants across content.ts and elsewhere in one pass; fixed the two conditional-format-12.test.ts fixtures this newly caught as genuinely under-specified.
  • src/workbook/data-validation-write.ts: added the missing error-path and grid-boundary tests, plus a direct writer-level test for the one case no round trip can observe (an empty dataValidations array must write no Dval/Dv records at all).
  • src/workbook/globals-writer.ts: dedicated globals-writer.test.ts for records this package's own reader never reads back (STYLE, conditional SST, ExternSheet's own XTI count).
  • src/workbook/sheet-writer.ts / src/write.ts: grid-boundary, merge-span, and formula-cached-value gaps; a direct Dimensions-record test; a Setup record fPortrait direct test; removed three more unreachable-but-documented defensive guards by filtering once per scan pass instead of repeating an unreachable per-cell check three times.
  • src/biff/write-errors.ts, src/biff/rk.ts, src/biff/record-writer.ts, src/units.ts, src/biff/print-setup.ts: individually verified message/name/exact-formula/boundary assertions; rk.ts's never-written low dword deleted as a provably redundant write against an already-zero-filled buffer.
  • src/biff/records.ts: extracted recoverFromFormatError, the one classification every per-record recovery boundary in this package draws around its own try/catch (BiffFormatError degrades, anything else rethrows) -- centralising a pattern duplicated ~15 times across 8 files into one directly-tested helper, plus exact-message and error-name assertions for readRecords' own thrown errors.
  • src/drawing/md4.ts: a 56-byte test vector (independently computed via OpenSSL's legacy MD4 provider) covering the one padding-length window RFC 1320's own A.5 vectors never land on; typed the block-word buffer as a 16-element tuple to remove a now-genuinely-unreachable internal-error guard; deleted the length field's never-observably-different high 32 bits.
  • src/workbook/conditional-format-ex.ts: a test proving the fIsCF12 guard fires for its own reason rather than coincidentally agreeing with an unrelated missing-target branch; a mocked-dependency test proving the shared recoverFromFormatError call site actually rethrows a genuine bug.
  • src/container.ts: deleted a redundant CompoundFileFormatError special case that produced byte-identical output to the generic catch-all beneath it; exact-message and readCompoundFile-spy tests distinguishing each guard from a coincidentally-identical fallback.
  • src/biff/substreams.ts: exact-message assertions for both "no preceding record" errors and both readBofDocumentType errors; the missing BOF-prefix-length boundary case (exactly 4 bytes must be accepted, not rejected).
  • src/workbook/comment-writer.ts: narrowed writeSheetComments to a CommentedCell type proven (via a hasComment guard) to carry a comment, removing an "internal error" guard nothing could trigger without a bug in the caller; assertions on FtNts's genuine randomness, TxO's cbRuns value, and the exact MAX_OBJECT_ID boundary.
  • src/workbook/comments.ts: direct unit tests for readObjPictFmlaStorageId's own sub-record walk (an unrelated sub-record, a nonzero cbFmla, the reserved trailing zero marker) that no round trip had exercised; overrun assertions for readTxoText's cbFmla/cbRuns skips; deleted a switch statement's redundant break in its own last case.
  • src/workbook/encryption.ts: isolated each half of two disjunctive checks (the RC4 version check, the XOR key/verifier check) that every existing test had previously varied together; an engineered RC4 verifier-hash byte collision proving the comparison is a genuine every-byte match, not "at least one byte agrees"; a mocked-dependency test proving the RangeError-to-BiffFormatError translation is scoped to RangeError specifically; deleted a redundant length check (md5's digest and the decrypted verifier hash are both always exactly 16 bytes by construction).
  • src/workbook/embedded-object.ts: deleted a redundant safeParse success guard (a failed parse's own result object carries no data property at all, so result.data already reads as undefined on that branch); a foreign-label test isolated from the separate objectKind/document presence check; direct assertions that sourcePath/tempPath are genuinely empty strings.

Honest state of the remaining gap: the fresh full run above leaves 490 survived + 159 no-coverage mutants across the following files, none of them yet touched this session, roughly ordered by remaining count: workbook/chart.ts (71), workbook/sheet.ts (61), workbook/drawing-writer.ts (58), test-support/cfb.ts (57), workbook/globals.ts (56), workbook/conditional-format-write.ts (55), workbook/drawing.ts (53), write.ts (47), workbook/defined-names.ts (45), biff/ptg.ts (42), workbook/sheet-writer.ts (39), content.ts (36), workbook/conditional-format.ts (29). This PR is not ready for a final review pass and breakThreshold has not been raised -- both wait until a full run genuinely reports 100% with zero disable comments.

@Mearman
Mearman force-pushed the feat/100-percent-mutation-xls-codec branch 3 times, most recently from 43400af to 482999a Compare September 14, 2026 07:20
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.
…erFromFormatError

Every one of these catch blocks restated the identical
`if (!(err instanceof BiffFormatError)) throw err` guard by hand; each
now defers to the one, directly tested classifier instead, removing
the duplicated if/throw as a separate mutation target at each site.

data-validation.ts's own readDvParsedFormula also drops its early
`cce === 0` return: parseFormulaText already resolves a zero-length
rgce to undefined on its own (an empty token stream never pushes onto
its own operand stack, so the final "exactly one operand left" check
already fails), so the guard restated a fact the callee already
established rather than a real branch in this function's own behaviour.
…sses

Every public method (hasMore, remainingInBlock, blockPosition,
nextByte) already calls settle() as its own first step, so a fresh
cursor needs no separate normalisation pass before its first use, and
remainingTotal() needed no second settle() of its own once it reads
remainingInBlock() (which already settles) before slicing the blocks
that follow rather than after.

remainingTotal() itself no longer walks a manual index loop: slicing
and reducing the blocks past the current one removes the off-by-one
comparison that loop carried, which was already unobservable in its
return value (an out-of-range index only ever added zero to the
total) but still sat there as an untestable mutation target.

nextByte() now folds its two undefined checks (no block left at all,
or -- unreachable given settle()'s own invariant, but not something
noUncheckedIndexedAccess's own typing can see -- an in-range block
with nothing at this offset) into the one case an optional-chained
lookup already distinguishes, rather than restating an unreachable
second copy of the same throw. take()'s own per-byte copy loop, which
can never actually run out once its upfront remainingTotal() check
has passed, borrows u8's already-exercised context label instead of
building an independent, permanently unobservable template literal of
its own.
…etic skip guards

runCount and extendedSize are both read from a u16/i32 and can never be negative, so their
">0" guards only ever chose between an unconditional cursor.skip(0) (already a no-op) and
the identical zero-byte skip -- there is no third, negative-count case left for the guard
to distinguish. Removing both conditionals leaves the two skip calls unconditional, and the
new tests assert the cursor lands correctly on a trailing sentinel byte in the run/phonetic/
neither cases and prove readXLUnicodeString batches its own chunked String.fromCharCode
assembly correctly past the chunk boundary.
…ht/fontName refusal messages

The one-unit-per-name-character overshoot a broken fontNameBytes loop could produce shifts
every font name's record length by the same two bytes, so a length DELTA between two names
can never distinguish correct output from that bug -- only a single name's absolute total
record length can. Also adds a height-0 exception test (heightTwips 0 is let through despite
sitting outside dyHeight's own 20-8191 range) and exact-message regex assertions for both
refusal cases, plus a contentFontOf describe block covering the same-icv/differing-icv colour
resolution split.
…dding branch

max is always at least MAX_SHORT_STRING_LENGTH (255) across this module's own three call
sites, so by the time the length check throws at all, text is always well past the 40
characters the message embeds -- there was never a shorter-text case left to choose a whole-
text embedding over a truncated one. The new tests pin each shape's own name and the exact
40-character truncation into the thrown message, since a repeated-character overflow string
can't otherwise distinguish a truncated slice from the untruncated text by content alone.
…eadLbl/parsePrintAreas

readLbl's cch/highByte bail and repeatBandsOf's whole-sheet exclusion were only ever tested
alongside inputs whose later fields happened to make the bail unobservable either way (an
empty area list, or a band shape only one branch could match) -- adding a raw Lbl builder
lets a test put a genuinely well-formed area behind the guard being probed, so bypassing the
guard produces a different, observable result instead of the same empty one. Also covers
PtgRef3d and PtgMemArea in their array-class spelling, and the "abandon the whole name, not
a partial one" rule for an unrecognised token following an otherwise-valid area.
… Math.sign, not a threshold

isoDateOfDayCount already excludes days === 60 (the phantom leap day) before choosing which
epoch origin the remaining count is measured from, so a plain `days < 60` and `days <= 60`
classify every day count this line can still see identically -- the one value they would
ever disagree on is excluded above. Comparing Math.sign(days - 60) against the specific
value -1, rather than testing an inequality against the same threshold the exclusion already
covers, makes BELOW and ABOVE genuinely swappable rather than merely restatable. Also adds
the missing non-finite/negative-serial refusals for serialToIsoTime and serialToIsoDateTime,
pins every thrown message's exact wording, and covers isoDateToSerial's own days === 0
boundary (the epoch date itself, which must not be refused the way a date before it is).
… cover firstChild directly

readEscherRecords' three length-overrun refusals were only ever checked with a bare
toThrow(), which stays green whether the check that produces the message fires or is
skipped entirely -- pinning the exact wording each throws forces the guard, and the
arithmetic and comparison it's built from, to actually run. firstChild had no test of its
own at all despite being an exported sibling of childrenOfType/findDescendant, and
findDescendant's own early-return only ever ran against a single matching container, which
cannot tell "return once a match is found" apart from "return after checking the first
container regardless" -- a second, genuinely empty sibling checked first closes that gap.
…ck and pib's dead fComplex test

readEscherRecords already returns no records at all for a zero-length stream, so the
DgContainer search two lines below already takes the same "no shapes" path a genuinely
non-empty but DgContainer-less stream does -- the dedicated length check duplicated that
outcome rather than producing a different one. readPibProperty's own fComplex check was
similarly dead: `opid === FOPT_OPID_PIB` already pins opid to a value whose own fComplex bit
is clear, so the second half of that && could never see the case it looked like it was
testing for. Also validates a malformed Opt table's own entry count up front (an exact
multiple of one FOPTE entry's size) rather than recovering from whatever BlockCursor throws
reading past it, removing the need for a catch clause the only error it ever produced could
reach. New tests isolate the DgContainer search from other top-level containers, the group
filter from same-recType atoms and same-kind-wrong-recType containers, and the ClientAnchor
and Opt-table length checks each from a well-formed sibling shape.
…d from a same-shaped bypass

The DgContainer search and readPibProperty's own opid check each had one half of their
own && condition (kind, and opid equality respectively) that no existing fixture could
prove was load-bearing on its own: a container of the wrong recType and a single-entry
Opt table both happened to make the OTHER half of the check do all the real work. An atom
carrying DgContainer's own recType, and a well-formed FOPTE entry naming a different
property, each isolate the half a fixture without them couldn't.
…and CFColor's unreachable skip

Nothing readCf12's own try block can throw is ever anything but a BiffFormatError --
every cursor read is one of BlockCursor's own u8/u16/u32/take, and parseDxfStyle already
catches and swallows its own BiffFormatError internally rather than letting one escape --
so recoverFromFormatError's instanceof check and rethrow branch had no second error kind
left to distinguish, and the catch now just returns undefined directly. readCfColor's own
"consumed so the cursor stays positioned for whatever follows" skip was equally dead: both
of its callers (readCfGradient, readCfDatabar) return undefined themselves the moment they
see an unresolved colour, never reading from that cursor again, so there was never a
"whatever follows" left to position it for. Also isolates readCfvo's own undefined-formula
guard, the cInterpCurve/cGradientCurve mismatch and range checks, the rgce2/fmlaActive
skips, the ct 0x02 dispatch from a same-shaped ct 0x01 record, and a malformed cbFilter from
one that merely looks unread -- each previously covered only by fixtures where the guard
being probed and its neighbours all agreed, so a bypass produced the identical outcome a
correct refusal already gave.
…in unterminated-string check

tokenize() pushed an explicit EOF_TOKEN at the end of every token array, but
FormulaParser.peek() already falls back to that identical shared constant the moment
`position + offset` steps past whatever real tokens tokenize() found -- the push and the
fallback were two routes to the same value, so the push is gone and the fallback is now the
only one. The unterminated-string check had the same kind of redundancy in a different
shape: charAt()'s forgiving out-of-range "" return meant a `cursor >= text.length` vs `>`
boundary only ever delayed the same eventual throw by one harmless iteration, never changing
what got thrown -- switched to bracket indexing, whose `undefined` past the end is a value a
real character can never equal, so running past the string is now what actually triggers the
throw rather than a length comparison beside it. Also merges compileParent's "binary" and
"unary" cases, whose bodies were already byte-for-byte identical, and adds tests isolating
every operator-loop guard's own token-type check (a quoted string reading "+"/"&"/etc. is the
one input whose type disagrees with its text), the error-literal regex's own start-of-token
anchoring, and the cell-reference regexes' own quantifier and anchor ranges -- each previously
covered only by inputs where the type or shorter/narrower alternative read identically.
…from its trailing one

"A1B2" (killing the combined pattern's missing trailing anchor) still ends in a digit, so
it cannot also prove the plain-column check's own leading anchor is load-bearing: without
it, [A-Za-z]{1,3}$ would still match just the final letter run of a word ending in one,
ignoring everything before it. "A1B" -- ending in a letter, unlike "A1B2" -- is the case
that needs the leading anchor specifically to be rejected.
…rop redundant range bounds

hslToRgb's own s === 0 shortcut duplicated what the general formula already computes: when
s is genuinely 0, q and p both reduce to l regardless of which branch computes q, collapsing
every hueToRgb call to l anyway. resolveIcvColor's two range checks each carried a lower
bound plain array indexing already makes unobservable (a negative index reads back
undefined the same way an explicit `>= 0` guard's failure would) or that the FIRST check's
own failure already establishes (PALETTE_BASE_ICV equals FIXED_COLOR_TABLE.length exactly).
resolveBorderEdge's and resolveFillBackground's own BORDER_STYLE_NONE/FILL_PATTERN_NONE
guards were the same shape: neither table has an entry for that value either, so the general
"not a recognised token" fallback already produced the identical undefined. Also restates the
HSL saturation formula and applyTint's own tint-sign branch each as a genuinely two-valued
choice (Math.min/Math.sign) rather than a threshold a mutation could shift without changing
either branch's own computed value at the one point it would matter, and pins the achromatic
shortcut that remains (rgbToHsl's own max === min, needed to avoid a real 0/0 division at
pure black or white) with a test only pure black/white can distinguish from dead code.
…ound

DEFAULT_PALETTE_TABLE and a real Palette record are both always exactly
PALETTE_ENTRY_COUNT entries long ([MS-XLS] 2.4.188's own ccv field is validated to that
count on read), so an icv past that range already reads back undefined from the plain
array lookup on its own -- the explicit upper-bound check was refusing nothing the lookup
itself didn't already refuse.
…eout

Building and writing that many comment records genuinely takes longer
than vitest's default test timeout allows, independent of machine
load, so Stryker's own dry run (which re-executes the suite under
instrumentation overhead) could fail this test outright rather than
merely running it slowly.
…ith Array.from

readCharacters' manual CHUNK loop and encodeCharacters' high-byte scan
both bounds-checked an index against a length where the boundary
iteration is a provable no-op, so a `<` to `<=` mutation produced no
observable difference. Deriving the chunk/element count from
Math.ceil/Array.from removes the redundant boundary entirely instead
of leaving it for a test to (unsuccessfully) chase, and adds direct
boundary tests for encodeCharacters' own 0xFF high-byte threshold.
…packing

encodeCharacters' high-byte scan, ftNts's fixed byte layout, and
cellXfTrailer's alc/alcV packing were only ever exercised incidentally
by whatever reader/writer test happened to call them, so a genuine bug
in this fixture builder could silently corrupt every test built on it
without failing anything itself. Adds direct tests for each, and
replaces the same redundant index-bounded scan loop strings.ts and
string-writer.ts already had with the identical Array.from form.
hueToRgb's four branches and its two wraparound adjustments were each
individually continuous at their own shared boundary (by the same
piecewise-construction argument rgbToHsl's own s formula above already
documents), so a boundary comparison mutant there could only ever
disagree with itself about which of two identical values to return.
Math.min(tt, 2/3 - tt) clamped to [0, 1] is the same four-piece curve
as one non-branching trapezoid, verified numerically against the
original across the full domain any real caller passes; q's own l <
0.5 ternary collapses the same way rgbToHsl's s formula already does,
via l + s * Math.min(l, 1 - l). The wraparound's own leading `t % 1`
was additionally just redundant: mod-1 addition distributes over the
following + 1 regardless of whether t was reduced first, for any t.
…blips.ts

readBlipStore's own empty-input guard and readBseImage's "past the end
of the BSE" guard were both provably redundant: readEscherRecords
already returns no records for a zero-length stream, and a subarray
starting past its own end already yields an empty slice, so both
converge on the identical result the guards existed to special-case.
readBseImage's own too-short-to-parse guard is replaced with a
try/catch around the cursor reads it was pre-checking for, using the
package's shared recoverFromFormatError classifier -- the cursor's own
bounds-checked reads already throw at precisely the byte where
truncation bites, a tighter boundary than restating BSE_FIXED_SIZE (a
length that is never itself reachable-but-still-too-short) a second
time. Two of readBseImage's own cursor.skip calls (unused2/unused3)
were dead code: embeddedStart is computed from BSE_FIXED_SIZE and
cbName alone, never from the cursor's position, so nothing reads
through it again after cbName. bytesToBase64's chunking loop gets the
same Array.from-with-Math.ceil treatment biff/strings.ts's readCharacters
and test-support/biff.ts's encodeCharacters already have, for the
identical no-op-boundary-iteration reason.

Adds direct tests for the drawing-group container lookup's own kind-and-recType
conjunction (a decoy of each wrong shape ahead of the real one), the
truncated-BSE-entry recovery path (including a mocked non-format-error
bug that must still propagate), the exact UID-plus-tag header boundary,
a JPEG_B-specific positive case, an unrecognised recType that would
otherwise share a valid JPEG UID count, and a multi-chunk base64
round-trip proving the chunk-boundary arithmetic and join separator.
drawing/escher-writer.ts had no test file of its own and was only ever
exercised incidentally through workbook/drawing-writer.ts's own
single-shape round trips, which never varied drawing count, blip
presence, or shape count enough to distinguish its own spidMax/cspSaved
arithmetic from a subtly wrong version of the same formula. Covers
writeDrawingGroupBytes' FDGG fields and Blip Store presence across
zero, one, and several drawings/blips, writeSheetDrawingBytes' own
patriarch-then-shapes id allocation, the rgbUid a real BSE derives
byte for byte from md4 of its own file bytes, and the internal-error
guard for a corrupted digest length. hexToBytes' own loop gets the
same Array.from treatment as the identical redundant-tail-iteration
pattern already fixed in biff/strings.ts and drawing/blips.ts.
…nal-format.ts

Removes three early-return guards (parseDxfStyle's dxfBytes.length===0
check, readCf's rgce1.length===0 check, and its rgce2.length>0 ternary)
that were behaviourally redundant: an empty byte array already drives
the surrounding cursor reads or parseFormulaText call to the identical
fallback the guard was hand-coding, so the guard was an
unreachable-in-practice branch rather than a real decision.

Adds direct unit coverage for the DXFN flag combinations no existing
test exercised: the fixed-length DXFNumIFmt form (as opposed to the
ambiguous DXFNumUsr one), the unmodelled DXFALC/DXFBdr skip blocks,
the icvFore boundary (negative, zero, and the 32767 default-colour
sentinel), a non-empty dxf carrying none of the optional blocks, a
ct 0x02 formula condition paired with an otherwise-valid cp (so the
ct guard itself is what excludes it), and the three catch blocks'
own genuine-bug-propagation behaviour (a non-BiffFormatError still
throws rather than being swallowed as a malformed record).
Removes two dead-code paths content.ts's own mutation-testing pass
surfaced: applyCellComments' comments.size===0 early return (an empty
comments map already makes the loop below a no-op on its own) and its
byPosition.set(key, materialised) call (comments is a Map, so a key
can never recur across the loop's own remaining iterations, meaning
the entry it writes is never read back). Also removes displayTextOf's
unreachable default case -- ContentCellValueSchema's ten kinds are
already exhaustively handled by the switch above it, so the default
was a second, redundant copy of the "empty" case's own return.

Adds direct unit and round-trip coverage for the print-settings,
conditional-format, data-validation, row/column, and merge-cell
boundary conditions no existing test isolated: a fit-to-page sheet
with only the width count at the spec's own auto value, a scalePercent
of exactly zero, a BoundSheet8 lbPlyPos landing on a non-worksheet
substream, a workbook stream with no records at all, a dxf whose fill
pattern is FLSNULL (a real, present block that still resolves to no
style), a valType-0 Dv record's genuinely absent formula1, a hidden
column with no usable width, an out-of-range cell format index, a
border-only and a vertical-alignment-only blank cell, each alignment
axis confirmed absent on the other axis's own round trip, a false
boolean cell's own displayText, and a degenerate 1x1 MergeCells range
alongside same-row/same-column merge-anchor neighbours.
…n cellIs shape

toStrictEqual against the whole discriminated-union entry, not a
chained ?.style property access ContentSheetConditionalFormatSchema's
own union has no single shared 'style' field for.
@Mearman
Mearman force-pushed the feat/100-percent-mutation-xls-codec branch from 482999a to 7cd0ce5 Compare September 14, 2026 08:07
Mearman and others added 6 commits September 14, 2026 09:15
…eet-writer.ts boundaries

alignmentOf assigns horizontal/vertical unconditionally instead of
behind its own redundant "if !== undefined" guard: mapCell, its only
caller, already re-checks each field against undefined before ever
copying it onto the ContentSheetCell it builds, so the guard could
never produce an observable difference -- only choose between two
intermediate objects mapCell already treats identically.

Adds direct coverage for readSheet's dataValidations/conditionalFormats
omission when a sheet declares neither, mapColumns dropping a column
that states no usable width and no hidden flag, and sheet-writer.ts's
own boundary and array-emptiness checks: Row's colMic/colMac across
several cells and for a declared-but-empty row, MergeCells omitted
for ordinary and explicitly-1x1 cells, CalcCount's real iteration
limit, the 256-column ColInfo ceiling, a stated column's own hidden
flag staying clear, Setup's inactive-scale sentinel winning over a
stray scalePercent once fitToPages is stated, empty page-break arrays
writing no record, comment/merge record absence, cell and row write
ordering, and writeCellValueRecord's own internal-error guard actually
firing on a genuine disagreement with written-cells.ts's predicate.
Removes sheet-writer.ts's own commentedCells.length>0 guard: unlike
the sibling merges guard right above it (which genuinely must skip an
empty case, since writeMergeCellsRecord still emits a real zero-count
record for one), writeSheetComments already returns an empty array
for an empty input, so the guard could only ever decide between
calling a function that does nothing and not calling it.

Adds direct coverage for every remaining ptg.ts survivor: wrapBelow's
own left-operand precedence comparison exercised without a PtgParen
token forcing the same output another way, PtgUminus/PtgPercent/
PtgParen/PtgAdd each aborting on a starved stack with a trailing
token proving the abort is immediate rather than a coincidental
fallthrough, both directions of the relative-row/column wraparound
boundary (not just the one already covered), and the PtgAttr subtype
family's own no-op/unsupported/CHOOSE branches.

Also strengthens several message-only BiffWriteError assertions from
a bare class check to the actual thrown text, and adds direct
coverage for sheet-writer.ts's own remaining boundaries: the exact
256-column and page-break-index ceilings, ascending sort order for
declared page breaks, a formula's own cached error/empty/string
result paths (refusing an undefined error code and an empty-kind
value, writing the trailing String record a string result needs),
and the worksheet substream's own trailing EOF record.
Adds direct coverage for applyFunctionCall's own arity-starved abort
(with a trailing token proving the abort is immediate, matching the
established pattern for every other stack-starved abort in this
file), the PtgAttr subtype family's remaining no-op members (Semi/
BaxcelA/BaxcelB/Space/SpaceSemi), an unsupported subtype outside this
reader's vocabulary, and PtgAttrChoose's own dedicated abort.

Also fixes a masking gap in the existing unresolved-3D-reference test:
without a trailing token after the failing reference, an incorrectly
non-aborting mutant reached the identical "undefined" result via the
unrelated stack-not-exactly-one-operand fallthrough check instead of
the ixti-resolution guard the test meant to exercise.

Adds a genuine-bug-propagation test for readArrayLiteralText's own
catch block, mocking BlockCursor.prototype.u8 (shared by both cursors
parseFormulaText walks at once) to fail only on its second call --
the array reader's own leading columns-count read, not the outer
loop's opcode read -- confirming a non-BiffFormatError still throws
rather than being silently absorbed as a malformed rgcb.
- document-rest@1.4.11 (patch)
…ir own generic fallback

readArrayElementText's SERAR_NIL case and parseFormulaText's PtgAttrChoose
branch each duplicated the identical undefined result their surrounding
default/fallback path already produces for every other unrecognised case,
since readArrayLiteralText aborts the whole array literal on the very
first element that resolves to undefined regardless of why, and the
PtgAttr subtype else-if chain already returns undefined for every subtype
it doesn't name. Neither guard changed any observable behaviour, so both
are removed in favour of the fallback they duplicated.
…overage lines

Every stack-starved-abort test in this file needs a trailing token to
distinguish a genuine abort from a coincidental empty-stack fallthrough,
but the exact shape of that trailing token depends on what the guard's
own mutant does to the stack: a mutant that returns early without
touching the stack is only caught by a bare trailing operand, while a
mutant that falls through and pushes a malformed-but-defined entry needs
that entry combined with a trailing operand via a real operator before
the final stack.length===1 check stops masking the difference again.
applyFunctionCall's own arity guard needed both forms as separate tests
since its two possible mutants (return-early vs fall-through) fail in
those two different ways; the 3D reference and 3D area reference ixti
checks needed the operator form since their own mutants push resolveSheetLabel's
literal "undefined" text rather than leaving the stack short.

Also adds direct coverage for PtgConcat (format and starved-abort),
PtgMissArg filling an omitted optional argument, a FALSE boolean array
element, an embedded single quote doubling inside a quoted sheet name,
and a trailing token on the PtgExp vocabulary-abort test, none of which
this file exercised before.
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.

2 participants