Repository navigation
ci: extract the source-reach check so the mutation guard can mutate its own guard logic #3835
Description
Activity
Premise holds.
walkNonTestTsFilesfilters.test.tsatscripts/mutation-guards-config.test.ts:68, so the three reach functions can never entermutatewhile they live in a test file. The extraction is the right shape.Three things the PR needs that the issue doesn't cover.
1. The constraint has only one legal answer
mutation-guards.jsoncarries exactly one array:not-mutated. Test 7 (scripts/mutation-guards-config.test.ts:399-411) asserts no path appears in bothmutateandnot-mutated. A file destined formutatetherefore cannot take amutation-guards.jsonentry — the two options in the issue body are not both available. The convention tweak is the only one that doesn't add a third mechanism:const MUTATION_GUARDS_PATTERN = /^mutation-guards-.+\.ts$/u
added to
listScriptsGateFiles. It ratchets in the right direction — any futuremutation-guards-*.tsmust be dispositioned — and doesn't collide withcheck-mutation-guards.ts, which already matchescheck-*.2. The relocation as specified will not reach 100% — blocking
Baseline first:
scripts/mutation-guards-config.test.tsis green at 26 tests. I hand-applied two mutants tosourceReachTransitiveand reran the whole file:if (!existsSync(join(repositoryRoot, current))) continue(line 271) →if (false): 26/26 pass. That branch is dead by construction. BothfollowQueue.pushsites already filter throughexistsSync(lines 262 and 274), so nothing in the queue can fail the check when it's shifted. It is an unkillable equivalent mutant.reachedModulesTransitivecarries the identical dead line, which went unnoticed only becausecheck-mutation-guards.tsisnot-mutated. Delete it during the move.current === undefined || visited.has(current)→&&: 26/26 pass. No fixture contains an import cycle, so dropping the visited-set dedup is unobservable. Needs a cycle fixture (a.ts↔b.ts).
Both survive against all 26 tests today. Under the proposed split they'd be judged by Tests 3–6 alone — a strictly smaller set.
That smaller set is the other half of the problem. Tests 3a/3b/4/5/6 are all single-entry, same-tree, literal-path fixtures.
findMutateEntriesWithoutSourceReachhas four guards none of them touch: theisLiteralPathglob skip, thetreeOf(testFile) === undefinedskip, thetreeOf(mutateEntry) !== testTreecross-tree skip, andnormalizePath's./stripping. Each is a predicted survivor once Tests 1 and 2 — the only tests that exercise the real mutate=9/testFiles=10 config with its genuine cross-tree combinations — stay behind in a file Stryker never loads.Move the entire
findMutateEntriesWithoutSourceReach (barrel exclusion)describe, Tests 1–6, not 3–6. Test 2 is the awkward one: its second half asserts thatfindMutateTestPairingViolationsis blind to the same modified config, and that function stays put per the issue. Split it — strict-check assertion to the new file, blindness assertion stays in the enumeration guard. Don't import across test files to keep them physically together; that registers one file's tests under the other and muddies which file Stryker credits for the kill.3. The
beforeAllsweep becomes a concurrency hazard the moment it runs under StrykerLines 628–634
rmSynceveryscripts/.mutation-guards-source-reach-*directory. Harmless today:mutation-guards-config.test.tsisn't intestFiles, so it never executes inside a Stryker sandbox. Put the relocated fixtures intestFilesand it does —concurrencyis unset instryker.config.json, so Stryker defaults to several test-runner processes against one shared sandbox, and the sweep will delete a sibling worker's live fixture directory mid-test. Scope the sweep to a per-process prefix, or drop it and rely on the per-testfinally.Watch, not block
BARREL_EXPORT_STATEMENT_PATTERNis the largest regex to enter the mutate set — three top-level alternations with nested optional groups. Regex mutation itself is proven here (scripts/check-wiki-private-presence.ts:56already carries a literal at 100%), but this repo has documented precedent for a syntactically-invalid regex mutant zeroing dry-run collection for an entire run. The wrapper already fails closed on that viaTestFileNotExecuted, so it'll be loud rather than a falseclean. Budget a run. Test 5 pins six forms, but no fixture exercises the trailing\s*;?or multi-space variants — expect quantifier mutants to survive there.No trigger-set edit needed.
scripts/mutation-guards-reach.tsand its test enter the trigger set throughbuildTriggerSet'sconfigEntriesunion. Don't add either toFIXED_TRIGGER_FILES.Acceptance criteria
- Convention extended with
mutation-guards-*.ts; nomutation-guards.jsonentry (Test 7 forbids it). - Dead
existsSyncdeleted from the relocatedsourceReachTransitive. - Import-cycle fixture added.
- Fixtures covering glob, undefined-tree, cross-tree, and
./-prefixed entries forfindMutateEntriesWithoutSourceReach. beforeAllsweep scoped per-process or removed.- A live
pnpm check:mutation-guardsverdict ofcleanpasted in the PR body — per-module kill counts, not a green enumeration test. The whole reason this issue exists is that a fixture list wasn't proof.
The new module also needs its own default
SourceReader;check-mutation-guards.tskeepsdefaultReadSourceprivate, anddefaultTestReadSourcecurrently lives in the test file being split.Sequencing confirmed as written: not a Unit 5 dependency, lands before Unit 6 registers the required context.
Run Summary
Field Value Event issues Repository fro-bot/.github Run ID 34010512279 Cache miss Session ses_f8b1c43fbffezjes1mAKDGiJJb - added a commit that references this issue
on Sep 8, 2026 This issue is stale because it has been open for 30 days with no activity. Remove this label to keep it open.
isPureReexportBarrel,sourceReachTransitive, andfindMutateEntriesWithoutSourceReach(added in #3834) live inscripts/mutation-guards-config.test.ts. Test files are excluded fromwalkNonTestTsFiles's enumeration, so they can never appear inmutate— the guard that enforces mutation coverage on every gate is itself outside that coverage. The barrel-form regex shipped wrong once in #3834 and was caught by a hand probe, not a test; Test 5 now pins all six forms, but that's a fixture list, not mutation coverage.Change: move the three functions to
scripts/mutation-guards-reach.ts, add it tostryker.config.jsonmutatewithscripts/mutation-guards-reach.test.tsintestFiles, and relocate Tests 3–6 beside it.findMutateTestPairingViolationsand itsreachedModulesTransitivewalk stay where they are.Constraint: the
check-*/wiki-*-gatesnaming convention inmutation-guards-config.test.tswon't auto-enumerate the new file, so it needs either an explicitmutation-guards.jsonentry or a convention tweak — decide which in the PR; don't add a third mechanism.Origin: Fro Bot carry-forward on #3834, rounds 1 and 2. Not a Unit 5 dependency; land before Unit 6 registers the required context.