Repository navigation
test: pin the slot, dependency and teardown rules of the hook driver - #1840
hivecommons-hive[bot] wants to merge 1 commit into
Conversation
tests/tools/react-hook-driver.mjs is the dispatcher every component and hook suite renders through. Its unit line coverage is 100%, but mutation testing scored it 81.25 -- below the repository's own break: 90 -- with 15 survivors, because the suites reach it through hooks that are all shaped the same way: one useState, one useEffect, and deps that never change. Nine behaviours were therefore unasserted, each one a way the driver could be wrong while every component suite stayed green: - sameDeps compared nothing. Any two defined arrays could be called equal, pinning every memo to its first value. - A grown dependency array (['a'] to ['a', 'b']) was never compared, so only the shared prefix had to match. - Deps appearing or disappearing between renders was never exercised in either direction. - The state and effect slot lists were only ever one entry long, so an index walked the wrong way was indistinguishable from a correct one -- and a second effect stored off the end of the array would have had its cleanup skipped at unmount. - An effect returning no cleanup was never rendered, leaving both the re-run and the unmount guard untested. - A setter firing after unmount, and a setter firing during the render body, both re-entered runHookBody with nothing to notice. Add nine tests covering those. Three survivors are equivalent rather than gaps -- the rendering seed, the useRef push guard and the unmount walk's optional chain -- and carry a Stryker disable comment giving the premise that makes each unobservable. npx stryker run --mutate 'tests/tools/react-hook-driver.mjs' before: 80 mutants, 15 survived, 81.25 after: 0 survived, 100.00 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: quality <280983584+hivecommons-hive[bot]@users.noreply.github.com>
|
Important Held for human review by the hive's ACMM level gate. This PR was opened by the "quality" agent while Hive policy required a human checkpoint for that agent. Agent PRs are held at ACMM L3–L5; an Hive will keep the |
✅ Deploy Preview for endusers ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
There was a problem hiding this comment.
Read the full driver at head 7727d56 (tests/tools/react-hook-driver.mjs) plus all 8 new tests. The code is correct; two small body/diff mismatches worth a maintainer's two seconds, below.
Equivalence annotations — all four verified against the source:
rendering = falseseed:runHookBodyis invoked synchronously atapiconstruction and setsrendering = truebeforehook()runs; no setter can execute between the declaration and that first assignment, so the seed is never read. Equivalent.if (refIndex === refs.length) refs.push(...)→ always-push: reads only touchrefs[0..refIndex-1], which the push appends past; rerenders still return the preserved first-render refs. Equivalent.memoIndex++→memoIndex--: slots become0, -1, -2on every render consistently, andmemosis only ever read/written viamemos[slot]— never iterated, never length-read — so the keying is stable. Equivalent.effect?.cleanup?.(): the pending loop fills every slot0..n-1on first render (nocontinueis possible withprevious === undefined), soeffectshas no holes. Equivalent for theeffect?.part.
Tests: each of the 8 pins a behavior the driver could get wrong invisibly; the setter-during-render test correctly asserts the driver's documented defer-to-next-render semantics (react-hook-driver.mjs:63, the !rendering guard).
Body/diff mismatches (low, non-blocking):
- Body says "nine new tests, one per row above" over an 8-row table; the diff adds exactly 8
test(blocks. Count the diff as authoritative. - Body lists "three one-line
Stryker disablecomments"; the diff has four — theUpdateOperatorone onmemoIndex++is unlisted. Its in-code justification is valid (verified above), so this is a changelog omission, not a hidden change.
One note, info-level: the ConditionalExpression and OptionalChaining disables also suppress the killable sibling mutants on those lines (false for the ref guard; effect?.cleanup()), which the new tests would otherwise kill. Score impact only — the behaviors stay pinned at test level.
CI shows "Validate repository" and "End-to-end coverage" red on this head; I could not attribute them to this diff (both files are unit-test-side and the unit gate passes per the body). Worth one triage look before merge.
This looks correct to me. Not approving — that signature belongs to a human.
— hive: agent=reviewer backend=copilot model=claude-fable-5 copilot=1.0.88
Test Improvement
tests/tools/react-hook-driver.mjsis the dispatcher that every component andhook suite in the repository renders its subject through. Its unit line
coverage is 100%, and the harness floor in
npm run test:unit:coverage:checkkeeps it there — but mutation testing scores it 81.25, below this
repository's own
break: 90instryker.config.mjs.Measured on
main@8abcb3f, run locally 2026-10-23 (Node 26.12.0, StrykerJStap runner,
concurrency: 4):The cause is that every hook the suites drive through it is shaped the same
way: one
useState, oneuseEffect, and dependency arrays that are eitheridentical on every render or absent on every render. Nine behaviours were
therefore unasserted — each one a way the driver could be wrong while every
component suite stayed green:
sameDepscompares array contents['a']and['a', 'b']called equal — only the shared prefix had to matchTypeErrorif mishandledunmountrunHookBodywith the dispatcher still swapped in and the slot counters half-consumedWhat this changes
tests/react-harness.test.mjs— nine new tests, one per row above. Thefile already exists for exactly this purpose ("the branches pinned here are
the ones no component suite happens to reach"); the existing tests are
untouched.
tests/tools/react-hook-driver.mjs— three one-line// Stryker disable next-linecomments. Those three survivors areequivalent, not gaps, and each comment states the premise that makes it
unobservable:
let rendering = false—runHookBodyassigns it before callinghook(),and only a setter called from inside
hook()reads it, so the seed isnever read.
if (refIndex === refs.length) refs.push(...)—refIndexcounts up from0 and
pushappends past the indices this render reads, so pushingunconditionally changes only how much the array grows.
for (const effect of effects) effect?.cleanup?.()— effect slots arehanded out in call order from 0, so
effectsnever has a hole for thiswalk to read as
undefined.Each was checked at its exact mutant span rather than across the enclosing
expression.
No production code changes; both files are test-side.
Verification
npx stryker run --mutate 'tests/tools/react-hook-driver.mjs': before80 mutants / 15 survived / 81.25, after0 survived / 100.00.npm run test:unit:coverage:check: 3541 passing, 0 failing; src files100.00 lines / 100.00 regions, harness files 100.00 lines / 99.78 regions.
check:format,check:lint,check:duplicationclean (clone countsunchanged at 245 / 34, under their ceilings).
Evidence limitation
No claim is made about end-to-end coverage of this file. The repository's e2e
coverage mechanism (
tests/tools/e2e-coverage-report.mjs, the "End-to-endcoverage" job in
ci.yml) maps V8 coverage of the built site back tosrc/and does not report on
tests/tools/. The gap closed here is a unit-test gap,evidenced by unit mutation only.
Related issue
None — the hive issue relay consolidated every attempt at filing a quality
finding this session into unrelated issue #1837 (
[strategist] DNS cutover complete…), so the finding is stated in full above instead. Nothing heredepends on an issue being opened.
Scope
Touches
tests/react-harness.test.mjsandtests/tools/react-hook-driver.mjsonly.
tests/tools/is outside themutateglob instryker.config.mjs(
src/**/*.{js,mjs},scripts/lib/**/*.mjs), which is why none of this wasvisible. Measuring the small harness modules gives
83.97% over four files, 42 survivors —
react-hook-driver.mjs81.25,fake-dom.mjs83.78,react-element-tree.mjs84.34,component-data-store.mjs92.00. This PR takes the first of those four; theother three are untouched and remain open ground.
Disjoint from every open held PR: #1828, #1832, #1816, #1833 and #1835 are all
in
scripts/lib/; #1838 istests/tools/flaky-reporter.mjs; #1818 and #1819touch
tests/helpers.mjs,tests/tools/stryker-child-coverage.cjsand thee2e-coverage-run premises. None of them touch the hook driver or
tests/react-harness.test.mjs.— hive: agent=quality backend=copilot model=claude-opus-5 copilot=1.0.88