Skip to content

mutation (diff) gate is unpassable for DB-heavy files: whole-file scoping + #100's no-coverage shift #103

Description

@KenTaniguchi-R

Symptom

PR #102 — a 2-line behavior fix in src/lib/simplefin/sync.ts plus 2 new tests — failed mutation (diff) at 28.07% against thresholds.break = 60.

The red was not caused by the diff. Stryker run locally on a clean checkout of main's sync.ts returns exactly the same numbers:

killed survived no-coverage errors score
main baseline 32 15 67 131 28.07%
PR #102 32 15 67 131 28.07%

The PR's own mutants were killed. The score is entirely pre-existing debt in lines the PR never touched.

Mechanism — two things compounding

1. Whole-file scoping. scripts/mutate-diff.sh builds --mutate from file paths:

MUTATE=$(echo "$FILES" | paste -sd, -)
exec pnpm exec stryker run --mutate "$MUTATE"

So any edit to sync.ts pulls all ~700 lines into scope. This is the mechanism already documented in #34.

2. #100 changed what no-coverage means. #100 decoupled mutation testing from Postgres via vitest.stryker.config.ts, which was the right call for runtime (30min → <2min). But it means the DB half of sync.tssyncConnection and everything under it — can no longer be exercised at all during mutation, so those mutants report no-coverage instead of being killed by the DB-backed tests that do cover them in pnpm test.

vitest.stryker.config.ts states this plainly and treats it as a feature:

Mutants covered only by those report as NoCoverage rather than Survived, which is honest signal about unit-test strength instead of a container tax.

That reasoning holds. What was missed is that thresholds.break = 60 was never re-tuned for the new, structurally lower scores.

Impact

Any PR touching a DB-heavy file under src/lib now fails this gate regardless of the change's quality. 67 of the 145 non-error mutants in sync.ts are unreachable by construction, so the file cannot clear 60% no matter how well the diff is tested. #102 was the first PR to hit this; every subsequent one touching sync.ts, plaid/sync.ts, or the query modules will too.

main has no branch protection, so this is currently advisory — but it trains everyone to merge through a red check, which defeats the gate.

Options

  1. Scope to changed lines. Use Stryker's --since / diff integration so the gate reflects the diff's own quality rather than whole-file legacy debt. This is the "reconsider the gate shape" half of Raise mutation coverage on sync.ts / reports.ts (and other large touched files) to ≥60% #34 and fixes the root cause.
  2. Exclude no-coverage from the score, or re-tune thresholds.break, acknowledging that no-coverage is now the expected state for DB-backed code rather than a signal of weak tests.
  3. Split the config so DB-backed files are mutated against a real Postgres in a separate, slower job, and unit-only files stay in the fast path.

(1) is the most honest and helps every future PR; (2) is the cheapest.

Context

Found while merging #102 (fixes #91). That PR was merged with the gate red, with the baseline comparison recorded on the PR.

Related: #34 (raise coverage on sync.ts / reports.ts — the other half of this problem), #100 (introduced the no-coverage shift).

Activity

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

    bugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions