feat(15-6): CI coverage gating — jest --coverage with 40% threshold floor#116
Merged
Conversation
… 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.
5 tasks
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes Epic 15.6 deliverable at
shippable-roadmap.mdline 299 — wiresjest --coverageinto CI with a 40% threshold floor onsrc/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.js—collectCoverageFromscope (src/lib/ + src/hooks/;__tests__/+.d.tsexcluded) +coverageThreshold40% floor on statements/branches/functions/lines +coverageReporters: ["text-summary", "lcov"].package.json— newtest:coveragescript wired tojest --coverage..github/workflows/ci.yml— new stepTests with coverage thresholdAFTER the existingTestsstep (Q1 keep both: fast green-light + slow gate). Nocontinue-on-error: true(Story 12-10 R1-H2 silent-disable defense).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)
≈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)
Tests(--no-coverage) +Tests with coverage thresholdsteps — fast feedback + slow gate separation.15-6-followup-codecov-integration(needs account setup).Test plan
npm run test:coverage— passes gate by ≈11 points on each metricnpm run type-check— 0 errorsnpm run lint— 0 warningsnpm run format:check— cleannpm run check:tokens— cleannpm test src/lib/__tests__/ci-coverage-gate-source-drift.test.ts— 6/6 passNotes for reviewer
Testsstep first, slow coverage step second) means a regular test-failure PR fails fast in the first step without paying the instrumentation cost.coverageThresholdblock injest.config.js; no CI workflow change needed. The drift detector regex pins:\s*40literally; ratchet PRs will also need to update the drift case.15-6-followup-codecov-integration(operator account setup)15-6-followup-coverage-ratchet-cadence(when to bump 40 → 50 → 60)