Conversation
Mearman
force-pushed
the
feat/100-percent-mutation-xls-codec
branch
3 times, most recently
from
September 14, 2026 07:20
43400af to
482999a
Compare
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
force-pushed
the
feat/100-percent-mutation-xls-codec
branch
from
September 14, 2026 08:07
482999a to
7cd0ce5
Compare
…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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds 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 disablecomment exists anywhere in this package at any point (grep -rn "Stryker disable" packages/xls-codec/srcreturns 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 withArray.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 wraparoundifadjustments 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-branchingMath.min/Math.maxclamp (numerically verified against the original across the full domain any real caller passes), matching the same unification techniquergbToHsl's ownsformula 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 atry/catcharound the reads it existed to protect (using this package's sharedrecoverFromFormatErrorclassifier), and two deadcursor.skipcalls 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 distinguishspidMax/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)..toEqualin 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: extractedrecoverFromFormatError, the one classification every per-record recovery boundary in this package draws around its own try/catch (BiffFormatErrordegrades, anything else rethrows) -- centralising a pattern duplicated ~15 times across 8 files into one directly-tested helper, plus exact-message and error-name assertions forreadRecords' 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 thefIsCF12guard fires for its own reason rather than coincidentally agreeing with an unrelated missing-target branch; a mocked-dependency test proving the sharedrecoverFromFormatErrorcall site actually rethrows a genuine bug.src/container.ts: deleted a redundantCompoundFileFormatErrorspecial case that produced byte-identical output to the generic catch-all beneath it; exact-message andreadCompoundFile-spy tests distinguishing each guard from a coincidentally-identical fallback.src/biff/substreams.ts: exact-message assertions for both "no preceding record" errors and bothreadBofDocumentTypeerrors; the missing BOF-prefix-length boundary case (exactly 4 bytes must be accepted, not rejected).src/workbook/comment-writer.ts: narrowedwriteSheetCommentsto aCommentedCelltype proven (via ahasCommentguard) 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'scbRunsvalue, and the exactMAX_OBJECT_IDboundary.src/workbook/comments.ts: direct unit tests forreadObjPictFmlaStorageId's own sub-record walk (an unrelated sub-record, a nonzerocbFmla, the reserved trailing zero marker) that no round trip had exercised; overrun assertions forreadTxoText'scbFmla/cbRunsskips; deleted a switch statement's redundantbreakin 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 theRangeError-to-BiffFormatErrortranslation is scoped toRangeErrorspecifically; 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 redundantsafeParsesuccess guard (a failed parse's own result object carries nodataproperty at all, soresult.dataalready reads asundefinedon that branch); a foreign-label test isolated from the separate objectKind/document presence check; direct assertions thatsourcePath/tempPathare 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 andbreakThresholdhas not been raised -- both wait until a full run genuinely reports 100% with zero disable comments.