test(document-outline.js): reach 100% mutation score - #1257
Merged
Conversation
document-outline.js's graph.test.ts runs an expensive exhaustive LCS-reconciliation sweep that several concurrent Stryker workers running it at once push past its own timeout through contention alone. Rather than lowering the shared default for every package, give packageStrykerConfig a concurrency option so only the package that needs a lower value pays for it.
…or direct testing The high 32 bits of the SHA-256 bit-length suffix only become nonzero once a message reaches 2^32 bits (~512 MiB), an input size no unit test can afford to allocate and hash. Splitting the write into its own writeBitLength function lets a test exercise that arithmetic directly against a synthetic bit length instead.
resolveHeadingGroup, resolveListGroup, and resolveParagraphLeaf each
special-cased an empty style chain to skip resolution. resolveStyleChain
already returns {} for an empty chain rather than throwing, and
applyEntry({}, node) already returns node by the identical reference,
so the special case computed nothing a plain call wouldn't already
produce.
Also export assertResolvedHeadingAnchor and assertResolvedListAnchor
and add direct tests exercising their throw branches: both guard an
invariant (document-schema.js's fill-only style application) that no
input reachable through this module's own construction can violate,
so a real regression test needs to call them directly rather than
relying on a resolution path that can never reach the throw.
…utlineNode Number.isFinite's own type check already returns false for any non-number input, so the preceding typeof value.level !== "number" check tested nothing Number.isFinite didn't already cover.
…ilter+reduce nearestTextCell tracked a running nearest candidate through a manual loop with two separate comparisons (position and region membership). Filtering to the qualifying candidates first and reducing them by position makes the same result reachable without a mutable running best that a mutation test can flip the comparison direction of unnoticed.
…n orderKeyBetween Once !highExhausted holds, position is already known to be within high's own length -- reaching position === high.length while !highExhausted still held would require high to be a strict prefix of low, contradicting the precondition that low sorts strictly before high. The separate position < high.length check therefore never changed the result.
Every parent map entry is created by union() alone, which already guards against mapping a root to itself -- so a chain of parent.get calls always terminates by reaching an unmapped key, never by revisiting an already-seen node. The separate "or equals current" termination check in root()'s two walks tested a condition that could never occur, and the ensure() pre-population step was likewise redundant: an unmapped key already resolves to itself as its own root without ever being written to the map first. Also replace computeSignals' single-row rowRegularity shortcut and boundingRange/boundingBox's manual min/max tracking with the unconditional formula and Math.min/Math.max respectively, since both already reduce to the same result in their shortcut's own case.
…stable helpers Split classifyLeaf's signal computation (computeLeafSignals) from its scoring (classifyFromLeafSignals), and export both alongside every other internal helper in this module (layoutItemBounds, findCut, recursiveXYCut, groupIntoLines, and the rest) purely for direct unit testing -- hand-picking signal values directly is far more precise than reverse-engineering a BoundedItem layout that happens to produce a given avgCells/cellRegularity/xStartRegularity combination. Also replace several manual min/max/loop patterns with Math.min/ Math.max and .entries() iteration, and drop guard branches (recursiveXYCut's degenerate-single-group check, findCut's bands.length checks, isRowAlignedGrid's text-only filter) that findCut/groupIntoLines/reduce already make unreachable on their own.
…oups and nested list stacks Add cases pinning list-stack pop targeting (popping only the deepest of several open groups), a construct group's own content and following siblings landing under the innermost open list item, and a construct group's self-contained heading nesting neither inheriting from nor leaking into the surrounding stack. Also fix a pre-existing fixture bug: a construct group nested inside a listGroup's children must be shape-scoped (ShapeConstructGroupNode), not section-scoped, since document-schema.js 4.1.0 narrowed ListChild to the shape-flow vocabulary.
…rojection addNode/addEdge guarded against overwriting an existing entry, but a second mint producing the identical node id or edge key is always structurally identical content (contentHashV1 is a pure function of that content, and edgeKey's own fields are GraphEdge's whole field set), so the guard only ever wrote back the value already there. decideEntry's decidingEntries cycle marker likewise never needed an explicit delete: the memo check at the top of the method already short-circuits any later call before it could re-read a stale marker. isGroupChild checked both `node` and `children` for presence, but every TreeGroup carries both and every TreeLeaf carries neither, so checking one is exactly as discriminating as checking both. siblingInsertIndex's matches.length > 1 guard is likewise redundant: the single-match case already satisfies the ambiguity check trivially (a one-element Set has size 1, equal to matchedOrderKeys.length). walkPropertyGraph's needsGuard/onPath conditional allocation collapsed to an unconditional Set, since the cost of tracking a CONTAINS-only walk's on-path set (which this function's own module comment proves can never actually suppress anything) is a few unused Set operations, not a behavioural branch worth a separate code path. pathsEqual's separate undefined special-case is likewise redundant: JSON.stringify of undefined already disagrees with JSON.stringify of any real path. Also add direct tests for two previously-untested defensive throws (recordOf's non-record refusal, decideEntry's undefined-table-entry refusal) via deliberately schema-invalid, type-system-bypassing fixtures, matching this file's own established pattern for exercising an invariant no real construction path can violate. Raises graph.test.ts's own exhaustive LCS-reconciliation sweep timeout again (vitest.config.ts): the sweep now also runs a reference- oracle comparison over arbitrary, not-necessarily-subsequence existing wirings, and Stryker's own instrumentation pushes the enlarged sweep past the previous 120s budget.
…LeafText joins Pin outlineLeafText's own cell-block joining behaviour (multiple blocks within one cell join by space, multiple runs within one paragraph join with no separator) and its recursion into a nested table's own multi-block cells, both previously reachable only through single-block/single-run fixtures that couldn't distinguish a space join from a no-separator join.
…ders Every optional-field pattern in test-support/fixtures.ts is exercised twice -- once with the field set (checking the exact key and value land) and once without (checking the key is genuinely absent via "key" in result, not merely undefined). Full-literal builders are checked against their exact expected output via toStrictEqual instead.
Uint32Array silently drops an out-of-range write and returns undefined for an out-of-range read, so a loop bound weakened by one (i <= 64 instead of i < 64) previously wrote past w's own 64-element length with no observable effect. Throwing on an out-of-bounds index turns that boundary into a genuine, catchable failure.
… fixed tuple classifyFromLeafSignals and classifyRegion each sorted a 3-element literal array of scores and then checked scored[0]/scored[1] for undefined before use — a branch no input can ever actually take, since the literal is always exactly 3 entries, but noUncheckedIndexedAccess still typed each lookup as possibly undefined. Typing the array as a fixed 3-tuple instead makes the two destructured elements known-defined at the type level, since Array.prototype.sort's this-typed return preserves the tuple shape through the sort — removing the unreachable-guard branch rather than leaving it as dead code no mutation can distinguish from live behaviour. Also replaces isRowAlignedGrid's equivalent defined-check on `first` with a plain non-null assertion, for the same reason: withLines.length >= 2 is already established immediately above, and there is no way to encode ">= 2" as a type for a dynamically filtered array the way a fixed-length tuple can.
…ts into testable units Extracts the ascending orderKey comparator, duplicated at three sort sites, into one exported orderKeyAscComparator so a single test pins its exact -1/0/1 return values instead of three copies each needing their own coverage. Extracts the repeated try/catch around boundedOrderKey (bisect, then rebalance on OrderKeyBudgetExhaustedError) in reconcileChildren and insertEdge into a shared runOrRebalance helper, and exports it and boundedOrderKey so their own edge cases (the tied-siblings message, the exhaustion/rebalance branch, the unreachable rethrow) get direct unit tests rather than relying on paths that may never exercise them through the public write API alone. Replaces reconcileChildren's fixed-size, pre-filled matchedIndex and matchedByOriginal arrays with Maps keyed by index: neither is ever read by length or iterated as a whole, only looked up by position and checked for presence, so a Map's own has()/get() already say everything a pre-sized array's "still at its initial fill value" did. Replaces its dp table's bare row[index]! reads with a bounds-checked lookup that throws on an out-of-range index, turning a boundary mutation of the backtrack's own loop conditions into an observable failure instead of a silently-absorbed undefined. Replaces anchorFor's separately-bounded for loop with children.entries(), tying the scan's end directly to the array it walks. Extends the exhaustive LCS-reconciliation sweep from existing wirings up to length 3 to length 4: the anti-inflation pass's own multi-occurrence reuse (a bucket holding more than one leftover index for the same id) only becomes observable once existing carries at least two unmatched occurrences of a repeated id plus a third, differently-labelled element to break the trivial full match a homogeneous run gets for free, which needs four existing slots. Adds direct tests for the newly exported helpers (orderKeyAscComparator, runOrRebalance, boundedOrderKey), a project() entry-node ordering test covering multiple policy-extracted table entries, and an insertEdge test proving sibling-position resolution filters by the requested kind rather than a hardcoded one.
deriveNeighbourLabels' nearestTextCell reduce had no test exercising two candidate cells at the identical row/column: nothing in ContentSheetCell's own shape forbids a hand-built array from carrying one, so the reduce's tie-break direction needs a deterministic, tested answer rather than being left to whichever comparison direction happens to compile.
…to a testable function The message-schedule write's own bounds guard never fires under any correct loop bound, so mutating the guard's condition produced a survived mutant with no way to reach it through sha256 itself. Extracting it to a module-level writeScheduleWord, the same pattern writeBitLength above it already follows, lets a direct unit test call it with an out-of-range index and prove the throw fires.
…tants testable Extracts pendingEntryNodes' ascending id comparator into an exported entryIdAscComparator, mirroring orderKeyAscComparator, so its own tie branch (two entries content-hashing to the identical id) gets a direct test pinning stable-sort behaviour instead of relying on a pipeline where genuinely tied ids never occur. Hoists reconcileChildren's dp bounds-check closure to a module-level, exported dpAt: the closure form could never be called with an out-of-range index by any real caller, so the guard's own condition had no test able to reach it; a direct unit test now does. Replaces anchorFor's per-call forward scan (bounded by an explicit `later <= position` comparison) with a single backward precompute over [...children.entries()].reverse(): the position a call site ever anchors is always absent from matchedIndex by construction, making the old comparison's exact boundary unobservable no matter how the loop was written. The precompute needs no such comparison at all, removing the mutation opportunity rather than testing around it. Adds direct tests for reconcileChildren's originalSiblings (sorted by orderKey rather than the edges array's own creation order, and filtered to the owner id and CONTAINS kind specifically, not a decoy sharing only one of the two), for insertEdge's CONTAINS-cycle check being scoped to CONTAINS edges alone (a non-CONTAINS attachment must never run the cycle check, even when it would otherwise close one), and for insertEdge sorting its own siblings by orderKey before resolving a position rather than trusting the edges array's own order.
…llable mutants Drops the redundant `index === 0` guards in findCut's band-gap loop and cellsInLine: both already fall through the very next line's own undefined check (bandStats[-1] and line.items[-1] are always undefined), so the explicit check duplicated logic that was already there, invisible to any test. Rewrites the mixed-margin comparison as `top < second + MIXED_MARGIN` rather than the algebraically equivalent `top - second < MIXED_MARGIN`: with both scores held to [SIGNAL_THRESHOLD, 1], their difference always lands on a coarser floating-point grid than MIXED_MARGIN's own stored value needs, so no achievable score pair can ever make that subtraction hit the margin bit-for-bit. The addition form lets a test construct the top score as literally `second + MIXED_MARGIN`, making the two sides of the comparison bit-identical by construction. Adds a test proving a touching item is merged into the running band (not started as its own), and that this changes the merged band's own scale enough to flip the next gap's cut decision; a test for the exact mixed-margin boundary using the construction above; and a test proving the vertical-tie sort's own x tie-break is a genuine subtraction, not a sum that happens to force the correct swap only when the input is already reversed.
…le mutants Removes DisjointCellSet's own path compression: it only ever changes how many hops a FUTURE find() walks, never any value this class returns, so it was unobservable through the class's own public contract and untestable dead weight, not real functionality worth keeping code around for. Drops the redundant `i === 0` guards in connectedComponents' two adjacency loops: both already fall through the very next line's own `previous !== undefined` check (sorted[-1] is always undefined), so the explicit check duplicated logic that was already there. Rewrites the mixed-margin comparison as `top < second + MIXED_MARGIN` rather than the algebraically equivalent `top - second < MIXED_MARGIN`, the identical fix pdf-regions.ts's own classifyFromLeafSignals needed for the same reason (see that commit): with both scores held to [SIGNAL_THRESHOLD, 1], their difference always lands on a coarser floating-point grid than MIXED_MARGIN's own stored value needs, so the subtraction form can never hit the margin bit-for-bit. Replaces the existing "exactly at the mixed margin" test with one built from that same identity: tableScore and proseScore are each constructed so the comparison's two sides are bit-identical, not merely numerically close.
… survivors Converts reconcileChildren's anchorAt from a pre-sized array to a Map keyed by position: every value is read back exactly once via anchorFor, never through .length or as a whole, so a fixed initial size added nothing a Map's own get() didn't already say, and was itself a mutation surface with no way to observe a weakened size. Rewrites the originalSiblings decoy test to request the same child id TWICE against a single genuine existing edge: reconciliation never deletes an edge just because originalSiblings over-counted it, so a single-occurrence request produces the identical final edge count whether a decoy is wrongly included or correctly excluded. Requesting two occurrences makes a wrongly-included decoy visible, since it lets a phantom "already wired" match suppress the genuinely missing second insertion. Rewrites insertEdge's own sibling-sort test to use a before/after position rather than "end": resolving a named sibling's position against the raw (unsorted) array index, rather than its sorted index, sends boundedOrderKey down its orderKeyAfter branch instead of orderKeyBetween -- a materially different key, unlike "end", whose insert index is siblings.length regardless of sort order and only changes WHICH neighbour is picked, not by how much.
…rator for direct testing recursiveXYCut's own internal per-axis sort already fixes which region lands as `a` versus `b` by the time segmentPdfRegions reaches its final sort, so an end-to-end test can never control which argument the tie- break clause receives -- both a reversed and an already-ascending input end up compared in the identical order, unable to distinguish a real subtraction from a sum that happens to force the correct swap either way for two positive x values. Exporting the comparator lets a test call it directly in both argument orders, proving the tie-break is a genuine subtraction rather than a sum.
…e insertion A pure reordering with one occurrence per id can never distinguish a sorted read from an unsorted one: the anti-inflation pass pairs every unmatched existing edge against an unmatched requested occurrence of the identical id regardless of which index either side carries, so a scrambled-but-balanced originalSiblings still ends every position matched (no edge minted, no edge moved) either way, and reconcileChildren never rewrites an existing edge's own orderKey -- the output graph was bit-for-bit identical whether or not the read was sorted first. Replaces that test with one requesting an id with no existing edge at all, so a genuine insertion happens: an unsorted read pairs the LCS match differently, anchoring the new edge before the existing set's first sibling instead of strictly between its two siblings -- a real, observable difference in the final walked order.
Every mutant now genuinely dies, either killed by a real test or removed by restructuring the code so the mutation opportunity no longer exists, matching the pattern byte-codec, pdf-raster-cpu, and excel-number-format already establish for a package at the literal maximum.
Mearman
marked this pull request as ready for review
September 13, 2026 06:06
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
Mearman
enabled auto-merge (rebase)
September 13, 2026 06:07
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.
Restructures document-outline.js's own source so that every mutation opportunity Stryker finds is either genuinely killed by a real test or removed as an AST node entirely (redundant guards deleted, manual min/max replaced with Math.min/Math.max, manually-bounded loops replaced with .entries() iteration), rather than suppressed with a disable comment.
Also adds a per-package Stryker
concurrencyoverride (stryker.shared.ts) so this package's own expensive exhaustive LCS-reconciliation sweep in graph.test.ts can run at concurrency 1 without slowing every other package's mutation CI.No
// Stryker disablecomments anywhere in this package.