Skip to content

Commit e42c0e8

Browse files
committed
test(config): unify coverage floors into a single coverage-thresholds.json
The CI gate hard-coded floors in a bash array while the vitest configs carried their own numbers — two ledgers that could drift apart (local green, CI red). Move the per-workspace line-floors into a root coverage-thresholds.json read by both: ci.yml via jq, and the workspace vitest/jest configs via import. packages/api-client (73) and routine-domain (74) gained local enforcement for the first time — previously vitest exited 0 at any coverage and only the CI bash array caught regressions. (audit P2 — double-bookkeeping coverage thresholds)
1 parent 14c5681 commit e42c0e8

7 files changed

Lines changed: 71 additions & 22 deletions

File tree

.github/workflows/ci.yml

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -411,32 +411,22 @@ jobs:
411411

412412
- name: Check coverage threshold (≥75%)
413413
# Fails CI if any workspace drops below its floor.
414-
# Default floor: 75%. Per-workspace overrides align the gate with
415-
# the floors documented in each package's vitest.config / jest.config:
416-
# apps/web: 39 (vitest.config.js lines: 39 — legacy idb/shared-lib-ui debt)
417-
# apps/mobile: 30 (jest.config.js lines: 30 — React Native coverage gaps)
418-
# packages/api-client: 73 (approaching 75%; see api-client/vitest.config.ts)
419-
# packages/routine-domain: 74 (approaching 75%; see routine-domain/vitest.config.ts)
420-
# Raise these floors as coverage improves in each package.
414+
# Floors live in coverage-thresholds.json (single source of truth) —
415+
# the same file is read by the workspace vitest/jest configs, so the
416+
# local `pnpm test:coverage` run and this gate can't drift apart.
417+
# Raise floors in that JSON as coverage improves.
421418
run: |
422419
set -euo pipefail
423-
THRESHOLD=75
420+
THRESHOLD=$(jq -r '.default' coverage-thresholds.json)
424421
FAILED=0
425422
426-
declare -A WORKSPACE_THRESHOLDS=(
427-
["apps/web"]=39
428-
["apps/mobile"]=30
429-
["packages/api-client"]=73
430-
["packages/routine-domain"]=74
431-
)
432-
433423
for summary in apps/*/coverage/coverage-summary.json packages/*/coverage/coverage-summary.json; do
434424
if [ ! -f "$summary" ]; then
435425
continue
436426
fi
437427
438428
WORKSPACE=$(echo "$summary" | sed 's|/coverage/coverage-summary.json||')
439-
WS_THRESHOLD=${WORKSPACE_THRESHOLDS[$WORKSPACE]:-$THRESHOLD}
429+
WS_THRESHOLD=$(jq -r --arg ws "$WORKSPACE" '.workspaces[$ws] // .default' coverage-thresholds.json)
440430
COVERAGE=$(jq -r '.total.lines.pct' "$summary")
441431
442432
if [ "$COVERAGE" = "null" ] || [ -z "$COVERAGE" ]; then

apps/mobile/jest.config.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@
66
* and a jsdom-free RN runtime. We run only tests under `src/**` to keep
77
* Expo Router's app-directory out of Jest's test discovery.
88
*/
9+
// Line-floor — з кореневого coverage-thresholds.json (single source of truth,
10+
// той самий файл читає CI-гейт у ci.yml). Решта метрик — локальні.
11+
const sharedThresholds = require("../../coverage-thresholds.json").workspaces;
12+
913
module.exports = {
1014
preset: "jest-expo",
1115
testMatch: [
@@ -110,7 +114,7 @@ module.exports = {
110114
coverageReporters: ["text-summary", "lcov", "json", "json-summary"],
111115
coverageThreshold: {
112116
global: {
113-
lines: 30,
117+
lines: sharedThresholds["apps/mobile"],
114118
branches: 25,
115119
functions: 30,
116120
statements: 30,

apps/web/vitest.config.js

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,19 @@
1+
import { readFileSync } from "node:fs";
12
import { defineConfig } from "vitest/config";
23
import react from "@vitejs/plugin-react";
34
import { resolve } from "path";
45
import { baseCoverageConfig } from "@sergeant/config/vitest.base";
56

7+
// Line-floor — з кореневого coverage-thresholds.json (single source of truth,
8+
// той самий файл читає CI-гейт у ci.yml). branches/functions/statements
9+
// лишаються локальними — CI гейтить тільки lines.
10+
const sharedThresholds = JSON.parse(
11+
readFileSync(
12+
new URL("../../coverage-thresholds.json", import.meta.url),
13+
"utf8",
14+
),
15+
).workspaces;
16+
617
export default defineConfig({
718
plugins: [react()],
819
test: {
@@ -83,8 +94,9 @@ export default defineConfig({
8394
// Floors set ~2pp below current actuals — same pattern as
8495
// `apps/server/vitest.config.ts`. Raise per sprint as the idb /
8596
// shared-lib-ui tests land (see docs/testing/2026-05-05-tests-
86-
// pr-plan.md → PR-T03 / PR-T04).
87-
lines: 39,
97+
// pr-plan.md → PR-T03 / PR-T04). `lines` приходить з кореневого
98+
// coverage-thresholds.json — піднімай floor там.
99+
lines: sharedThresholds["apps/web"],
88100
branches: 32,
89101
functions: 29,
90102
statements: 38,

coverage-thresholds.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"$comment": "Single source of truth для per-workspace coverage line-floors. Читається CI-гейтом (.github/workflows/ci.yml :: 'Check coverage threshold') і конфігами workspace-ів: vitest (apps/web, packages/api-client, packages/routine-domain) та jest (apps/mobile). Піднімай floors тут у міру росту покриття.",
3+
"default": 75,
4+
"workspaces": {
5+
"apps/web": 39,
6+
"apps/mobile": 30,
7+
"packages/api-client": 73,
8+
"packages/routine-domain": 74
9+
}
10+
}

docs/90-work/audits/2026-06-11-fable5-independent-audit.md

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -86,15 +86,20 @@
8686
- ✅ Monobank `historyFetch.ts`/`privat.ts` без тестів → додано 24 unit-тести (`privat.test.ts` — guard/path-allowlist/CRLF/upstream-mapping; `historyFetch.test.ts` — schema/buildMemoryContent/fetchAccountStatement; pure-helpers експортовані).
8787
- ✅ Postgres-skew: Testcontainers pg16 vs CI pg17 → усі 16 тест-контейнерів + прозові коментарі підняті до pg17 (збіг з CI service + docker-compose).
8888

89+
**P2 — закриті у другій хвилі 2026-06-12:**
90+
91+
- ✅ Hard Rule #1 без механізму → глобальний `pg.types.setTypeParser(int8)` у `apps/server/src/lib/pgInt8.ts` (install у db.ts; safe-integer guard — fail loud замість мовчазної втрати точності; 6 unit-тестів). Серіалізатори лишаються другим рубежем.
92+
- ✅ Coverage-пороги двобухгалтерні → single source `coverage-thresholds.json` у корені: ci.yml-гейт читає його через jq, vitest-конфіги (web, api-client, routine-domain) і jest (mobile) імпортують `lines`-floor звідти. api-client (73) і routine-domain (74) уперше отримали локальний enforcement (раніше — тільки CI-bash). Web-floor 39→50 — окремий burn-down, не закритий.
93+
- ✅ Orphan `syncV2Types.ts` → видалено (#3522, chip `task_abed59b4`).
94+
8995
**P2 — далі:**
9096

9197
- Freshness-механіка міряє churn, не review (`bump-last-validated` штампує будь-який staged .md; 53% корпусу проштамповано одним link-rewrite комітом). Потрібен дизайн: ручний validate-маркер vs churn-bump.
9298
- SLO/alert-стек: фантомні Alertmanager-згадки прибрані + wiring-статус задокументований (ws-15, #3519). Лишається: UptimeRobot (founder) + рішення про Grafana Cloud rules sync чи видалення 24 design-правил.
9399
- Merge-серіалізація: ≥3 колізії номерів міграцій на main; GitHub merge queue або timestamp-префікси. (= ws-14, founder-gated)
94-
- Coverage-пороги двобухгалтерні (vitest-конфіги vs bash-масив у ci.yml) + web-floor 39%/32% проти цілі 50/40.
100+
- Web coverage-floor 39%/32% проти цілі 50/40 — burn-down тестами, не конфігом.
95101
- pnpm audit critical-gate без exception-path (audit-exceptions ledger не читається гейтом; escape — лише PR-label `audit-exception` для high, не для critical).
96-
- Hard Rule #1 — конвенція без механізму: нема глобального pg `setTypeParser` для int8.
97-
- Orphan-схема: `syncV2Types.ts` (0 імпортерів; шит-маркер цитує неіснуючий ADR-0062-decomposition — номер зайнятий OpenAPI-ADR → chip `task_abed59b4`), m047/m070-072 billing-орфани (two-phase DROP post-launch), db-schema pg-runner з розбіжним ledger-default.
102+
- Orphan-схема: m047/m070-072 billing-орфани (two-phase DROP post-launch), db-schema pg-runner з розбіжним ledger-default.
98103
- i18n: en.ts 215 рядків vs uk.ts 847; allowlist 243 файли без ratchet.
99104
- ESLint baselines без дедлайнів: react-hooks ~152 off-порушень, ~96 non-null assertions (burn-down «2026-Q3» без enforcement дати).
100105

packages/api-client/vitest.config.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,17 @@
1+
import { readFileSync } from "node:fs";
12
import { defineConfig } from "vitest/config";
23
import { baseCoverageConfig } from "@sergeant/config/vitest.base";
34

5+
// Coverage line-floor — з кореневого coverage-thresholds.json (single source
6+
// of truth, той самий файл читає CI-гейт у ci.yml). Лишається тільки lines:
7+
// решту метрик CI не гейтить.
8+
const thresholds = JSON.parse(
9+
readFileSync(
10+
new URL("../../coverage-thresholds.json", import.meta.url),
11+
"utf8",
12+
),
13+
) as { workspaces: Record<string, number> };
14+
415
export default defineConfig({
516
test: {
617
environment: "node",
@@ -9,6 +20,9 @@ export default defineConfig({
920
coverage: {
1021
...baseCoverageConfig,
1122
include: ["src/**/*.ts"],
23+
thresholds: {
24+
lines: thresholds.workspaces["packages/api-client"],
25+
},
1226
},
1327
},
1428
});

packages/routine-domain/vitest.config.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,17 @@
1+
import { readFileSync } from "node:fs";
12
import { defineConfig } from "vitest/config";
23
import { baseCoverageConfig } from "@sergeant/config/vitest.base";
34

5+
// Coverage line-floor — з кореневого coverage-thresholds.json (single source
6+
// of truth, той самий файл читає CI-гейт у ci.yml). Лишається тільки lines:
7+
// решту метрик CI не гейтить.
8+
const thresholds = JSON.parse(
9+
readFileSync(
10+
new URL("../../coverage-thresholds.json", import.meta.url),
11+
"utf8",
12+
),
13+
) as { workspaces: Record<string, number> };
14+
415
export default defineConfig({
516
test: {
617
environment: "node",
@@ -9,6 +20,9 @@ export default defineConfig({
920
coverage: {
1021
...baseCoverageConfig,
1122
include: ["src/**/*.ts"],
23+
thresholds: {
24+
lines: thresholds.workspaces["packages/routine-domain"],
25+
},
1226
},
1327
},
1428
});

0 commit comments

Comments
 (0)