Skip to content

ci: extract the source-reach check so the mutation guard can mutate its own guard logic #3835

Description

@marcusrbrown

isPureReexportBarrel, sourceReachTransitive, and findMutateEntriesWithoutSourceReach (added in #3834) live in scripts/mutation-guards-config.test.ts. Test files are excluded from walkNonTestTsFiles's enumeration, so they can never appear in mutate — 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 to stryker.config.json mutate with scripts/mutation-guards-reach.test.ts in testFiles, and relocate Tests 3–6 beside it. findMutateTestPairingViolations and its reachedModulesTransitive walk stay where they are.

Constraint: the check-* / wiki-*-gates naming convention in mutation-guards-config.test.ts won't auto-enumerate the new file, so it needs either an explicit mutation-guards.json entry 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.

Activity

  1. fro-bot commented on Sep 6, 2026

    @fro-bot
    Owner

    Premise holds. walkNonTestTsFiles filters .test.ts at scripts/mutation-guards-config.test.ts:68, so the three reach functions can never enter mutate while 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.json carries exactly one array: not-mutated. Test 7 (scripts/mutation-guards-config.test.ts:399-411) asserts no path appears in both mutate and not-mutated. A file destined for mutate therefore cannot take a mutation-guards.json entry — 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 future mutation-guards-*.ts must be dispositioned — and doesn't collide with check-mutation-guards.ts, which already matches check-*.

    2. The relocation as specified will not reach 100% — blocking

    Baseline first: scripts/mutation-guards-config.test.ts is green at 26 tests. I hand-applied two mutants to sourceReachTransitive and reran the whole file:

    • if (!existsSync(join(repositoryRoot, current))) continue (line 271) → if (false): 26/26 pass. That branch is dead by construction. Both followQueue.push sites already filter through existsSync (lines 262 and 274), so nothing in the queue can fail the check when it's shifted. It is an unkillable equivalent mutant. reachedModulesTransitive carries the identical dead line, which went unnoticed only because check-mutation-guards.ts is not-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. findMutateEntriesWithoutSourceReach has four guards none of them touch: the isLiteralPath glob skip, the treeOf(testFile) === undefined skip, the treeOf(mutateEntry) !== testTree cross-tree skip, and normalizePath'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 that findMutateTestPairingViolations is 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 beforeAll sweep becomes a concurrency hazard the moment it runs under Stryker

    Lines 628–634 rmSync every scripts/.mutation-guards-source-reach-* directory. Harmless today: mutation-guards-config.test.ts isn't in testFiles, so it never executes inside a Stryker sandbox. Put the relocated fixtures in testFiles and it does — concurrency is unset in stryker.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-test finally.

    Watch, not block

    BARREL_EXPORT_STATEMENT_PATTERN is 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:56 already 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 via TestFileNotExecuted, so it'll be loud rather than a false clean. 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.ts and its test enter the trigger set through buildTriggerSet's configEntries union. Don't add either to FIXED_TRIGGER_FILES.

    Acceptance criteria

    1. Convention extended with mutation-guards-*.ts; no mutation-guards.json entry (Test 7 forbids it).
    2. Dead existsSync deleted from the relocated sourceReachTransitive.
    3. Import-cycle fixture added.
    4. Fixtures covering glob, undefined-tree, cross-tree, and ./-prefixed entries for findMutateEntriesWithoutSourceReach.
    5. beforeAll sweep scoped per-process or removed.
    6. A live pnpm check:mutation-guards verdict of clean pasted 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.ts keeps defaultReadSource private, and defaultTestReadSource currently 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
  2. fro-bot commented on Oct 6, 2026

    @fro-bot
    Contributor

    This issue is stale because it has been open for 30 days with no activity. Remove this label to keep it open.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions