Skip to content

feat(15-6): CI coverage gating — jest --coverage with 40% threshold floor#116

Merged
Simplemart17 merged 3 commits into
mainfrom
feature/15-6-ci-coverage-gating
May 17, 2026
Merged

feat(15-6): CI coverage gating — jest --coverage with 40% threshold floor#116
Simplemart17 merged 3 commits into
mainfrom
feature/15-6-ci-coverage-gating

Conversation

@Simplemart17

Copy link
Copy Markdown
Owner

Summary

Closes Epic 15.6 deliverable at shippable-roadmap.md line 299 — wires jest --coverage into CI with a 40% threshold floor on src/lib/ + src/hooks/. A PR that removes tests OR adds substantial untested code now fails CI rather than silently merging.

Epic 15 implementation work is COMPLETE — 6 of 6 stories done.

Changes

  • jest.config.jscollectCoverageFrom scope (src/lib/ + src/hooks/; __tests__/ + .d.ts excluded) + coverageThreshold 40% floor on statements/branches/functions/lines + coverageReporters: ["text-summary", "lcov"].
  • package.json — new test:coverage script wired to jest --coverage.
  • .github/workflows/ci.yml — new step Tests with coverage threshold AFTER the existing Tests step (Q1 keep both: fast green-light + slow gate). No continue-on-error: true (Story 12-10 R1-H2 silent-disable defense).
  • NEW src/lib/__tests__/ci-coverage-gate-source-drift.test.ts (6 cases) pinning the threshold + scope + script + CI step + ordering + NEGATIVE silent-disable guard.
  • coverage/ added to .gitignore + .prettierignore (auto-generated artifacts).

Measured baseline (2026-05-17)

Metric Coverage Headroom over 40% floor
Statements 53.42% +13.42
Branches 55.80% +15.80
Functions 51.49% +11.49 (lowest)
Lines 54.12% +14.12

≈11 points of headroom against the lowest metric. Future PRs can ratchet to 50% once Functions coverage grows (filed as 15-6-followup-coverage-ratchet-cadence).

Operator decisions (per spec recommendations)

  • Q1: Keep both Tests (--no-coverage) + Tests with coverage threshold steps — fast feedback + slow gate separation.
  • Q2: Use spec-literal 40% floor (vs measured-floor-minus-3%) — already leaves ≈11 points headroom against the lowest metric; no need to over-tighten.
  • Q3: Defer Codecov / Coveralls integration to operator-action follow-up 15-6-followup-codecov-integration (needs account setup).

Test plan

  • npm run test:coverage — passes gate by ≈11 points on each metric
  • npm run type-check — 0 errors
  • npm run lint — 0 warnings
  • npm run format:check — clean
  • npm run check:tokens — clean
  • npm test src/lib/__tests__/ci-coverage-gate-source-drift.test.ts — 6/6 pass
  • Full Jest suite: 116 suites / 2165 tests pass (+6 net vs pre-15-6 baseline)
  • CI run on this PR — verify the new step runs + passes

Notes for reviewer

  • The new step adds ~30% to CI test-step duration due to coverage instrumentation overhead. The 2-step pattern (fast Tests step first, slow coverage step second) means a regular test-failure PR fails fast in the first step without paying the instrumentation cost.
  • Forward-compat: when ratcheting to 50%, edit only the coverageThreshold block in jest.config.js; no CI workflow change needed. The drift detector regex pins :\s*40 literally; ratchet PRs will also need to update the drift case.
  • Follow-ups filed:
    • 15-6-followup-codecov-integration (operator account setup)
    • 15-6-followup-coverage-ratchet-cadence (when to bump 40 → 50 → 60)

… floor on src/lib/ + src/hooks/

- jest.config.js: add `collectCoverageFrom` scope (src/lib/ + src/hooks/;
  __tests__/ + .d.ts excluded) + `coverageThreshold` 40% floor on all 4
  metrics (statements/branches/functions/lines) + `coverageReporters:
  ["text-summary", "lcov"]`.
- package.json: new `test:coverage` script wired to `jest --coverage`.
- .github/workflows/ci.yml: new step `Tests with coverage threshold` AFTER
  the existing `Tests` step (Q1 keep both — fast feedback + slow gate).
  No `continue-on-error: true` (Story 12-10 R1-H2 silent-disable defense).
- Drift detector at src/lib/__tests__/ci-coverage-gate-source-drift.test.ts
  (6 cases) pins coverageThreshold + collectCoverageFrom + script + CI
  step + ordering + NEGATIVE continue-on-error.
- coverage/ added to .gitignore + .prettierignore (generated artifacts).

Measured baseline (2026-05-17): Statements 53.42% / Branches 55.80% /
Functions 51.49% / Lines 54.12% — ≈11 points of headroom against the
lowest metric. Ratchet via future PRs (15-6-followup-coverage-ratchet-
cadence). Codecov integration deferred to operator action
(15-6-followup-codecov-integration).

Closes Epic 15.6 deliverable. Epic 15 implementation work is COMPLETE
(6 of 6 stories done).

+6 net Jest cases (2159 → 2165; matches spec target +5-7 squarely in range).
All 5 design-system gates green.
Simplemart17 added a commit that referenced this pull request May 17, 2026
3-agent adversarial review (Blind Hunter + Edge Case Hunter +
Acceptance Auditor) surfaced 7 actionable findings:

HIGH:
- BH-2/EH-1: name test files explicitly in run command (vacuous-pass
  defense — `deno test <empty-dir>` would have silently exited 0 OR
  exited non-zero with a confusing "No test modules found" — either way
  a future rename/move slips past the gate)
- BH-3/EH-2: parse-upstream-error_test.ts docstring claimed
  `--allow-all` while CI uses `--allow-net=127.0.0.1` — fixed
  docstring to match CI invocation
- EH-4: Case 5 ordering regex `/- name:\\s*Tests\\b/` matched BOTH
  `Tests` and PR #116's `Tests with coverage threshold` because
  \\b word boundary triggers between "Tests" and space. Anchored to
  line-end via `/^\\s{6}- name:\\s*Tests\\s*$/m`.

MED:
- BH-5: Case 4 silent-disable defense only covered the Deno test step;
  extended to also pin the Setup Deno step (tightly-coupled — silent
  disable on EITHER fails the gate). Pin both via explicit loop.
- BH-6: added `timeout-minutes: 3` so a hung worker can't burn the
  GitHub Actions 6-hour default.
- BH-7: rewrote misleading v1/v2 comment — `denoland/setup-deno@v2`
  is the action installer version; `deno-version: v1.x` pins the
  runtime version. Two independent axes.
- EH-10: added `--no-check` to skip redundant TS typecheck (npm
  type-check step already covers the source-of-truth files; saves
  ~5s/run + decouples from Deno's TS engine drift).

Story file housekeeping (per AA): Status review → done; populated
File List section.

Deferred (filed as follow-ups):
- 15-3-followup-action-sha-pinning (BH-1/EH-3 — warrants global PR
  for all 3 actions: checkout, setup-node, setup-deno)
- 15-3-followup-deno-cache (BH-4/EH-6 — perf + outage resilience)
- 15-3-followup-real-body-read-timeout-test (EH-8 — needs TCP server)

PR #116 `ci.yml` merge conflict acknowledged: operator resolves at
merge time (recommend: merge this PR before #116; Case 5 regex
defends against the order being flipped).

All 4 quality gates green: type-check 0 / lint 0 / prettier clean /
jest 2164 tests pass (no net case change — patches tightened existing
cases in place).
3-agent adversarial review surfaced 8 actionable findings. The
load-bearing one (BH-2/EH-4 per-directory thresholds) REVEALED a
real coverage gap that the global average had been masking.

HIGH:
- BH-2 / EH-4 (load-bearing): added per-directory coverageThreshold
  scopes ('./src/lib/' + './src/hooks/') so the spec deliverable's
  CONJUNCTION ("≥ 40% on src/lib AND src/hooks") is enforced, not
  the average. Per-directory floors immediately revealed src/hooks/
  is at 23.02% statements / 27.12% branches / 25.92% functions /
  23.36% lines — well below 40%. The global metric (53.42%) was
  averaging this against src/lib/ which is much higher. Per spec
  AC-E ("if below 40% on ANY metric, lower threshold to
  floor-minus-3% of actual"), calibrated src/hooks/ floors to
  20/24/22/20. Global stays at 40 (belt-and-suspenders); src/lib/
  stays at 40 (above measured). Filed
  15-6-followup-lift-hooks-coverage-to-40 to raise the per-dir
  floor as future stories add hook tests.
- BH-1 ordering regex anchor: pre-R1 'indexOf("- name: Tests\\n")'
  was fragile to trailing-whitespace/CRLF drift. Anchored via
  '/^\\s{6}- name:\\s*Tests\\s*$/m' (PR #115 EH-4 lesson reused).
- BH-3 tail-slice safer fallback: pre-R1 if coverage step ever
  moved to be the last step, the silent-disable negative guard
  would sweep entire file + false-positive on Expo Doctor's
  legitimate 'continue-on-error: true'. Bounded slice to 1500
  chars.
- EH-1 block-comment-wrap bypass defense: drift detector
  deliberately doesn't comment-strip jest.config.js (glob '/**'
  sequences eat the block-comment regex). New Case 0 verifies
  the 200 chars preceding 'coverageThreshold:' don't contain an
  unmatched opening block-comment marker.

MED:
- BH-5 gitignore anchor: 'coverage/' (no leading /) matches at
  any depth. Changed to '/coverage/' so a future sub-dir like
  'docs/insurance-coverage/' isn't silently dropped.
- BH-8 / EH-2 upload-artifact: AC #5 marked upload as
  'recommended' but wasn't implemented. Added
  'actions/upload-artifact@v4' with 'if: always()' so gate-
  failure PRs still upload coverage for triage. 14-day retention.
- EH-10 sibling-test exclude: extended collectCoverageFrom with
  '!**/*.test.{ts,tsx}' + '!**/*.spec.{ts,tsx}' so a future
  contributor adopting sibling-test convention can't artificially
  inflate coverage by running test files end-to-end.

LOW:
- EH-9 fractional tolerance: drift detector regex now tolerates
  '40' OR '40.0' (Prettier reformat resilience). Per-directory
  scopes use general '\\d+(?:\\.\\d+)?' since calibrated
  floors differ from 40.

Documented (no code change): BH-4/EH-7 CI cost trade-off (added
CI step comment), BH-6 scope gap + follow-up filed, BH-7/EH-5
forceExit + coverage interaction, EH-8 float-vs-rounded gotcha.

Deferred (filed as follow-ups):
- 15-6-followup-extend-coverage-scope-to-components-and-app (BH-6)
- 15-6-followup-lift-hooks-coverage-to-40 (R1 BH-2/EH-4)

+2 net drift cases (6 → 8). Coverage gate passes with calibrated
floors. All 4 quality gates green: type-check 0 / lint 0 /
prettier clean / jest 2167 tests pass.
@Simplemart17 Simplemart17 merged commit c5ccebd into main May 17, 2026
1 check failed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant