test: измерване на coverage и ratchet праг в CI (#93) - #216
Conversation
|
Прегледах #216 стриктно (харнесът за #93). Сигурност — издържана: trigger е Ratchet — коректен и fail-closed: липсващ отчет = твърда грешка, липсваща метрика = грешка, спад > tolerance = fail; Две точни бележки — за да не се чете покритието погрешно (не блокер за merge): 1) Derive слоят на ETL е ИЗВЪН обхвата на покритието — по конструкция. 2) Сред измерените workspace-и, branch подовете на accuracy слоевете са ниски. Иначе е точно каквото #93 иска и е сигурно направено. Approve; двете бележки са за roadmap-а на покритието. |
…ар fix Адресира ревю находките по PR midt-bg#216: - validateBaseline: празен/невалиден workspaces обект, изтрит или забравен ключ за workspace с test script, stale ключ и traversal/__proto__ ключове вече fail-ват gate-а вместо тихо да го изключват - --update печата per-workspace делти и предупреждава шумно при спад, за да не може реална регресия тихо да влезе в нов baseline - coverage exclude покрива и .test.tsx/.spec.* варианти - sticky comment: gh api | head пренаписан на два стъпки (pipefail SIGPIPE) - по-точно съобщение при липсващ отчет след crash-нал vitest процес
|
Прегледах новия връх
Approve остава. Двете ми roadmap бележки (ETL derive извън обхвата на coverage; ниски branch подове на db/config) са проследени в #217 (priority: high) — не блокират този PR. |
lyubomir-bozhinov
left a comment
There was a problem hiding this comment.
Ratchet-ът е реален: check-coverage.mjs прави process.exit(1) при спад, self-test-ът върви пръв, а validateBaseline е fail-closed (хваща и занулен workspaces).
Една дупка за проверка: findTestWorkspaces (:239) открива workspace само по наличие на scripts.test. apps/web има test (vitest run --config vitest.config.ts), но интеграционният lane от #177 върви под отделен config/test:integration — ако той не е включен в vitest.config.ts, ratchet-ът няма да мери точно покритието, което #177 добавя. Увери се, че umbrella test-ът покрива и двата runner-а, иначе новото integration coverage остава невидимо за gate-а.
…ар fix Адресира ревю находките по PR midt-bg#216: - validateBaseline: празен/невалиден workspaces обект, изтрит или забравен ключ за workspace с test script, stale ключ и traversal/__proto__ ключове вече fail-ват gate-а вместо тихо да го изключват - --update печата per-workspace делти и предупреждава шумно при спад, за да не може реална регресия тихо да влезе в нов baseline - coverage exclude покрива и .test.tsx/.spec.* варианти - sticky comment: gh api | head пренаписан на два стъпки (pipefail SIGPIPE) - по-точно съобщение при липсващ отчет след crash-нал vitest процес
d16f32c to
4a982eb
Compare
- @vitest/coverage-v8 през общ preset (vitest.shared.ts); vitest.config.ts за всичките 6 workspace-а с тестове, с експлицитен include за да не са невидими непокритите модули - scripts/check-coverage.mjs: per-workspace ratchet (lines/branches срещу комитнат coverage-baseline.json, 0.5pp толеранс), markdown таблица с делти + информативен общ сбор; --update вдига baseline-а локално - CI: pnpm test -- --coverage + ratchet gate (self-test първо), artifact с отчета, step summary, sticky PR коментар само за same-repo PR-и (fork token-ът е read-only); turbo test task пази coverage/** outputs - документирано в docs/review-testing.md
…ар fix Адресира ревю находките по PR midt-bg#216: - validateBaseline: празен/невалиден workspaces обект, изтрит или забравен ключ за workspace с test script, stale ключ и traversal/__proto__ ключове вече fail-ват gate-а вместо тихо да го изключват - --update печата per-workspace делти и предупреждава шумно при спад, за да не може реална регресия тихо да влезе в нов baseline - coverage exclude покрива и .test.tsx/.spec.* варианти - sticky comment: gh api | head пренаписан на два стъпки (pipefail SIGPIPE) - по-точно съобщение при липсващ отчет след crash-нал vitest процес
4a982eb to
924b276
Compare
lyubomir-bozhinov
left a comment
There was a problem hiding this comment.
Прегледах стриктно механизма на връх 924b276. Силен ratchet — реален gate, не просто измерване.
Проверих:
scripts/check-coverage.mjsfail-ва CI приpct < base − tolerance(0.5pp) — истински gate,process.exit(1). CI го вика blocking (pnpm check:coverage, без|| true/continue-on-error), и self-тества checker-а преди gate (check:coverage:test) — добра дисциплина.- Anti-gaming-ът е точно правилен: explicit
includeglob вvitest.shared.ts— коментарът го казва изрично: без него v8 репортва само файловете, заредени от тестове, тъй че нов нетестван модул би бил невидим за ratchet-а. С explicit include нетестван source дърпа coverage-а надолу вместо да се скрие — най-честият начин ratchet-и да се заобикалят е затворен. Exclude листата е само test/dist/build/node_modules — нищо source не се крие. - Baseline-ите в
coverage-baseline.jsonса реални (apps/web 89.3/81.3, shared 94.8/78.1, db 82/65.5), не 0%. Умишлено намаление се изразява със свалянето на baseline-а в същия PR (reviewable).
Една бележка (strict):
apps/etlbaseline е 18.7% lines / 19.4% branches — а ETL-ът е точно там, където живеят accuracy бъговете на СИГМА (#154/#158 FX, #194/#195 канонични имена/ЕИК). Ratchet-ът замразява най-рисковия код на най-слабия под. Механизмът е коректен (записва текущото състояние, не е дефект в PR-а), но предлагам follow-up (вържете с #217) да вдигне etl floor-а — ratchet на 18.7% там е слаба защита. Branch-покритията (config 58.3, db 65.5) също са скромни — edge-бъговете се крият в клоните.
Одобрявам ratchet механизма; etl baseline-ът е за follow-up (#217), не блокер за този PR.
Resolve the add/add conflict in apps/etl/vitest.config.ts by keeping BOTH sides: main's sql-text-module plugin, the cloudflare:workers/workflows aliases and the 120s timeout (without them the Workflow suite cannot load), plus this branch's sharedCoverage(['src/**']) reporter config. Refresh coverage-baseline.json from the current corpus. The committed baseline predates the tests main gained since the branch was cut, so it no longer described reality - apps/etl alone reads 74.0% lines against a 18.7% floor. A ratchet 55pp below the real level cannot catch a regression, which is the whole point of the gate. Measured on this merge: etl 74.0/58.2, web 89.7/81.8, config 92.8/72.2, db 94.2/79.0, ingest 85.8/80.0, shared 95.4/80.0.
todorkolev
left a comment
There was a problem hiding this comment.
Одобрявам. Ratchet върху реалното текущо покритие, с честни числа вместо кръгли амбиции - включително ниските за ETL - е правилният подход: пази от регресия, без да принуждава хората да пишат тестове за показност. Само-тестващият се гейт (check:coverage:test преди check:coverage) също е хубав детайл.
Две неща поправих в клона:
Конфликтът add/add в apps/etl/vitest.config.ts - запазих и двете страни. Без плъгина за .sql модули и alias-ите към cloudflare:workers/workflows от main, наборът на Workflow-а изобщо не се зарежда; към тях добавих sharedCoverage от този клон и 120-секундния timeout.
Обнових coverage-baseline.json. Записаната базова линия беше отпреди тестовете, които main получи междувременно, тоест вече не описваше реалността - apps/etl мери 74,0% редове срещу праг 18,7%. Праг с 55 пункта под истинското ниво не може да хване регресия, което е целият смисъл на гейта. Измерено върху този merge: etl 74,0/58,2, web 89,7/81,8, config 92,8/72,2, db 94,2/79,0, ingest 85,8/80,0, shared 95,4/80,0.
За протокола: #254 предлага същия механизъм с прагове 96-100%; предпочитам този.
midt-bg#216 (coverage harness) merged to main, so the vendored harness reconciles to the canonical one. Also folds in the feature work that landed since the branch base — midt-bg#263 (worker-native FX load, rewritten refresh Workflow), midt-bg#252 (Bulstat EIK control code), midt-bg#210 (similar-contracts cohort) — and re-validates the whole suite against the moved source. Conflict resolutions: - apps/etl/vitest.config.ts: take upstream (needs the SQL text-module plugin + cloudflare:workers alias for midt-bg#263's real-Workflow index.ts). - vitest.shared.ts: keep our fixtures/json/md/d.ts excludes; add **/src/test/** (SQLite/workers stubs are test scaffolding, not product code). - coverage-baseline.json: keep our floors (baseline reconciliation to the merged tree's actuals follows in a separate commit). - apps/etl/src/index.test.ts: take upstream's FX integration test; restore the orchestration coverage it does not cover in a new mock-based control-flow test. - packages/db/src/queries/home.test.ts: union both added fakeDb params (singleOffer + capture). Post-merge fixes for source drift: - search.suggest.test.tsx: stub getDb (the route now wraps env in getDb()). - index.control-flow.test.ts (new): capped-window, zero-ingest, FX-uncovered, integrity-gate logger + failure (Error and non-Error), scheduled — restoring the run()/scheduled() branch coverage displaced by taking upstream's test. Full suite green: config 27, shared 56, ingest 116, etl 40, db 458, web 469.
* test: coverage measurement + ratchet gate in CI Vendored from #216 (feat/coverage-ratchet) to put the coverage machinery in place ahead of #217, while #216 is pending merge upstream. Squashes ydimitrof's two harness commits into one; original authorship preserved via the commit author. @vitest/coverage-v8 through a shared vitest preset, per-workspace coverage configs with explicit include, committed coverage-baseline.json, the scripts/check-coverage.mjs ratchet gate (+ node:test self-test), and the CI wiring. Ref #93, #216. * test(config): cover regionByName + taxonomy integrity to 100% regionByName had zero tests (both lines uncovered). Adds its full branch matrix (valid, whitespace-trim, null/undefined/empty/unknown, verbatim round-trip of all 28 regions) plus edge inputs for categoryForDivision and procedureGroup, and taxonomy-integrity invariants (unique CPV codes, single procedure-type ownership, classified = competitive∪non-competitive, 28 unique NUTS3 regions). config: 92.85/72.22 -> 100/100 lines/branches. * test(shared): cover format.ts edge branches (eik/unp, date fallbacks, periodRange) Adds eik/unp passthrough, one-sided and empty periodRange, date/monthYear/ longDate no-match + datetime-prefix + out-of-range-month fallbacks, count sign/absence, pct/signedPct dp + non-finite, entityName non-collapsing paths, cleanName unbalanced-quote drop, ЕТ/ET latin detection. shared branches 78.1 -> 98.3; residual is signedPct's provably-unreachable defensive return. Caught: count(-0.4) emits '−0' (missing money()'s rounded-zero sign guard); latent only (count takes non-negative integers), logged not patched. * test(ingest): cover staging, refresh, and base/ocds edge branches New staging.test (0%->100%): scoped DELETE, chunked INSERT at CHUNK=100 boundaries (100/101/250 rows), null-fill of absent columns, table+column routing per target. New refresh.test: SQL splitter (escaped '' in literals, -- comments inside/outside literals, trailing statement), @refresh-batch grouping, transient-table drop order, D1 orchestration. base/ocds additions: toBool, Date.parse date fallback, annexes mapping, baseSqlLiteral numeric/ text/null branches, secured_inverse/variants coercions, party/lot/amendment and catch-up-window branches. ingest lines 83.8->100, branches 78.9->95.1. * test(db): close branch gaps in identity, keyset, home, regions, methodology, flows identity slug fallbacks + undecodable name slug; keyset decodeCursor malformed/oversized/bad-type-guard paths; home zero single-offer aggregate; regions year + EU/national funding predicates; methodology absent-count coalesce with positive total; flows sankey sort tiebreak on shared authority. * test(db): cover sitemap streaming end to end streamAuthority/Company/Contract sitemaps via a paginating fake D1: XML escaping + C0 stripping, lastmod fallbacks (row date -> as_of -> none), natural-person filtering, empty-chunk skip loop, CHUNK-boundary pagination, contract page rowid windowing, and contractSitemapPages math. sitemaps branch 34.9 -> 95.3; residual is the defensive post-close pull guard. * test(db): cover getCompany, getAuthority, and getContract derivations getCompany + getAuthority were entirely untested. Adds DTO assembly, share math (won/spent denominators, zero-guards), avg-bids rounding, consortium membership (list->participants, prose->note), hasEik, sector top6+tail rollup, and getContract subcontractor (EUR/BGN/null/blank), framework call-off detection, eurFromNative currency paths (EUR/BGN peg/FX/no-rate), deltaPct suspect+zero-base guards, lot dedup/totals, and not-found. details branch 43.6 -> 88.5, lines 53 -> 100. * test(db): cover company-centred network, defaults, and hop-2 reduction Adds the company-centre direction, null-param default (top authority) + its empty fallback, includeCenterOptions=false, loadCenter sample-name fallback (authority + company), hop-2 top-1-per-neighbour dedup, centre self-skip, and edgeless-node weighting. network branch 50 -> 88, lines 86.7 -> 100. * test(db): cover search empty-query + trend zero-year YoY and coverage guards search: empty/punctuation query -> empty shape, searchMoreHref unknown-kind fallback. trend: YoY guarded against a zero prior year, coverage pct when nothing is dated (no divide-by-zero). * test(db): cover authorities query branches to 96% Add coverage for the base-aggregation source (year/EU/single- vs multi-sector primary_sector), the entity WHERE type/text filters, sort normalization, facet label fallback and sort, page overflow, and the CSV stream body across the CHUNK boundary. Branch 55%->96%, lines 100%. * test(db): cover companies query branches to 95% Add coverage for sort normalization, the base-aggregation source (year/EU/ single- vs multi-sector), the kind/text entity WHERE, facet kind mapping and sector sort, page overflow, missing total row, and the CSV body across the CHUNK boundary. Branch 61%->95%, lines 100%. * test(db): cover competition query branches to 95% Add coverage for the authority-detail wrappers (getAuthoritySingleOffer, getAuthorityProcedureCompetition), getCompetitionSummary (both the qualifying and null-topConcentration paths), the MAX_TOP cap, EU/national funding scope, and a degenerate corpus exercising the zero-guard fallbacks. Branch 70%->95%. * test(db): cover contracts query branches to 96% Add coverage for buildFilters (every year/sector/procedure/value-bucket/EU/ bids/authority/bidder/text predicate), summary override, page overflow, contractsSummary null row, listSingleOfferContracts modes, facet procedure folding / sector sort / year ordering, and the streamed CSV body across the CHUNK boundary. Branch 66%->96%, lines 100%. * test(db): cover details query fallback branches to 98% Add degenerate-input coverage for getCompany (absent metadata/bids/suspect rows, null primary sector and procedure value), getAuthority (spent-nothing authority with an unknown CPV division, zeroed tail share, null bids/suspect), and a getContract lot defaulting to the BGN peg. Branch 88%->98%, lines 100%. * test(db): cover network query branches to 98% Add coverage for an unresolvable company centre (null name → empty network), the company-kind fallback when neither rollup nor sample carries a kind, the empty-default path with includeCenterOptions off, and deduping a hop-1 neighbour that appears twice. Branch 88%->98%, lines 100%. * test(db): cover flows, search, and trend branches to 95%+ flows: EU/national/all funding scope + long-label truncation (branch 89->100). search: nullish raw-query coalescing (branch ->97). trend: EU/national funding, includeSectors=false, and an empty series with an absent coverage row (branch 89->100). * test(etl): cover the EOP ingest worker and bucket pipeline to 98% index.ts: full RefreshWorkflow.run coverage (staging lifecycle, capped and zero-ingest branches, derive-slice loop, finally-drop on success and error) and the scheduled cron entrypoint, via mocked platform/ingest/eop seams. eop.ts: bucket-key parse/classify, catch-up planning (uncapped/capped/default), bucket listing status + redirect guard, OCDS/base staging, and the window walk. Workspace 19%->98% branch, lines 100%. * test(web): cover filters URL-state helpers to 97% Add coverage for singleSelectFilters (unknown sector/year flags, funding/top defaults), buildSectorGroup (category grouping, summed vs absent counts, uncategorised skip), sortHref, and the withParams/pageNav null-override, array, empty-result, and page-default branches. filters.ts branch 47%->97%. * test(web): cover assistant agent, report binder, and tool registry agent.ts: SDK-wiring coverage (model/base-URL resolution, tool-set assembly, stream Response + onError) via mocked ai/@ai-sdk. report-schema.ts: flows block, facts sub-line, unknown-handle, empty title, 0-row and null chart edges (branch 78->94). tools.ts: run_sql AST-reject/error/meta-less paths, semantic hits, eop_fetch, source_link (branch 57->95). Also exclude test fixtures and type-only declarations from coverage (permanent 0% data files, not code). * test(web): cover assistant format, results, eop-fetch, rag, emit-shape render-format null-date; tool-results missing-cell + truncation flag; eop-fetch null-date/non-array/invalid-JSON/thrown-fetch; rag embed mismatch + metadata mapping + empty-vector guards; validateEmitShape callout/flows/timeseries and the object/question/items/columns negatives. * test(web): cover CSV export ranges, freshness, and multipart edges Add coverage for the v0 freshness fallback, non-string/null q classification, empty and empty-chunk multipart bodies, the abort-on-part-failure path, and suffix (bytes=-N) / open-ended (bytes=A-) R2 range shapes (the fake now emits R2-native range objects). csv-export lines 87->95, branch 76->90. * build: exclude markdown files from coverage instrumentation The v8 provider instruments every file matched by a workspace's include globs. Markdown docs colocated in src (e.g. the assistant README) carry no coverable statements, report a permanent 0%, and — being non-JS — make the reporter's remap step throw a parse error. Exclude **/*.md alongside the existing JSON and type-only exclusions so the ratchet total reflects executable code only. * test(web): raise branch coverage to the 95 floor Cover the remaining thin spots in the web workspace with real behavioural tests, no code changes: - cache.publicCache: default and explicit stale-while-revalidate windows - eopSource: missing/malformed dates, DD.MM.YYYY key shape, OCDS cutoff boundary - search.suggest: trimGroup cap + loader query/trim/headers, empty-q default - app.ts hardening: nonce re-read path, OPTIONS short-circuit, no-Content-Type - retry: non-Error rejection logging + default backoff past the table - security: nonce-less headers omit the CSP outside production - riskLogic: unknown bid count (null) and missing bidsRejected fallback - ScrollToTop: rAF coalescing of a scroll burst Web branch total 81.3% -> 95.47%, lines 89.3% -> 99.25%. * style(config): wrap the curated-sector assertion per prettier * chore: ratchet coverage floors to >=95 for every workspace With the new tests in place, every workspace clears 95% on both lines and branches. Regenerate the ratchet baseline from current coverage so the gate now enforces the 95 floor going forward: etl 18.7/19.4 -> 100/98.5 web 89.3/81.3 -> 99.2/95.4 config 88.2/58.3 -> 100/100 db 82/65.5 -> 100/97.3 ingest 83.8/78.9 -> 100/95 shared 94.8/78.1 -> 98.8/98.3 * test(web): assert non-null categories in buildSectorGroup tests buildSectorGroup always returns categories, but the group type marks it optional. vitest transpiles without type-checking so this passed locally; tsc --noEmit under noUncheckedIndexedAccess (CI typecheck) rejected the possibly-undefined access. Assert non-null at the three call sites. * test(ingest): cover sparse-release OCDS nullish branches Add branch-completion tests for releaseToContracts/Amendments/Lots on releases with absent optional fields: missing tag/contracts keys, id-less and identifier-less parties, a scheme-less CPV classification, an empty-string value amount, a blank date, and an ocid/tender-id-less lot. ocds.ts branch 92.17% -> 98.26%, ingest workspace 95.06% -> 98.7%. Residual uncovered branches are unreachable defensive code: the validDateOnly regex reject (day is always pre-normalized to YYYY-MM-DD) and the `rel.contracts ?? []` / `c.id ?? null` right-sides the length/id guards above them make impossible. * test(db): close reachable query branch gaps to 98.4% - keyset: unsafe-direction guard, before-cursor with an ascending sort, and the empty before-page (both cursors null) - flows: two-authority sankey so the authority-column sort comparator runs - regions: empty dataset → the total==0 coverage-pct guard (no divide-by-zero) - companies: base aggregation from a non-sector filter (no CPV predicate) - contracts: the „Неизвестна" year bucket sinking below real years regardless of input order - authorities/companies/contracts: backward pagination — page forward for a cursor, back for a before-cursor, then feed it back so keyset's reverse path runs db branch 97.35% -> 98.39%. Residual gaps are unreachable defensive code: CSV/sitemap `if(done)` re-entry (a stream never pulls after close), the minContracts `?? DEFAULT` the orchestrator already normalises, split().pop() `?? ` fallbacks (pop is always defined), the homoglyph map (every regex-matched char is mapped), and cross-namespace network self-edges. * test(web): cover read-only SQL guard, tool, and eop-fetch branches - assertReadOnlySelect: empty/comment-only query, a forbidden keyword hidden in a single CTE-prefixed statement (cheap keyword layer, not just the AST guard), and the sqlite_master/sqlite_schema catalog-table rejection - run_sql: a driver returning no results array (the results ?? [] fallback) - eop-fetch: a non-Error thrown value → the generic fetch-error label web branch 95.47% -> 96.0%. Residual gaps are deep AST-shape defenses (sql-ast-guard), schema-validation guards (report-schema), and unreachable code: regex capture-group ?? fallbacks (csp, always matched), PROD-gated redirect/OPTIONS paths under vitest, and the module-init Date.now tag. * chore: ratchet coverage floors up after deeper branch tests Coverage rose across web/db/ingest with the new branch tests; raise the ratchet floors to match (never down): web 99.2/95.4 -> 99.4/96 db 100/97.3 -> 100/98.3 ingest 100/95 -> 100/98.7 etl (100/98.5), config (100/100) and shared (98.8/98.3) unchanged. Monorepo total 99.73% lines / 97.51% branches. * style(db): wrap flows two-authority test per prettier * test: make coverage-only tests mutation-sensitive An adversarial mutation audit found six added tests that lit up a branch for the ratchet without asserting its behaviour (each survived deleting the very line it claimed to cover). Strengthened so the assertion fails under the targeted mutation: - db backward pagination (authorities/companies/contracts): pageSize 1 made slice+reverse a no-op; now pageSize 2 over 3 rows and asserts the page comes back in reversed fetch order (fails if rows.reverse() is dropped) - db flows ribbon order: input was pre-sorted so the comparator was unguarded; now feeds unsorted pairs and asserts ranked toName order - web app.harden nonce swap: unobservable under dev PROD=false; now stubs PROD=true and asserts the CSP nonce is replaced by a sha256 hash - web retry backoff: asserts setTimeout was called with [50,150,150], pinning the BACKOFF_MS[i] ?? 150 fallback value Coverage unchanged (db 98.39%, web 96%); same branches, real assertions. * test(coverage): replace coverage-only assertions with mutation-sensitive ones Adversarial audit pass over the coverage-ratchet suite: each finding was confirmed by breaking the exact production line and watching the test stay green, then fixed and re-verified so the mutation now fails. - etl stagedRows: fixture now sets baseAmendments/ocdsAmendments non-zero so both amendment terms of the sum are guarded (were unexercised). - db details: procedureMix assertions go from .length>0 to the folded DTO shape (contracts/valueEur/sharePct); numeric lot sort now fed out of order with multi-digit labels; consortium lot contractor name asserted through the row kind. - db sitemaps: page math now guards the upper rowid bound (hi), and lastmod precedence proves signed_at wins over published_at when both are present. - db network: centre-picker test asserts the full mapped shape for both the authority and (previously unexercised) company branch. - db trend: sector options asserted through the includeSectors=true path. - db keyset: oversized-cursor test uses a genuinely decodable payload so the length guard is exercised, not the JSON.parse catch; plus a near-limit pass. - db contracts: listSingleOfferContracts asserts the limit reaches LIMIT ?. - web retry: fake timers; ScrollToTop: rAF coalescing counted. * test(etl): pin the clock in the catch-up default-today test The 'defaults today' test only asserted the plan window was date-shaped (/^\d{4}-\d{2}-\d{2}$/), which a mutation to any hard-coded ISO string would survive. Pin the system clock and assert plan.to equals that exact date, so the new Date() default is actually verified. Addresses the reviewer's determinism note on real-clock reliance. * test(etl): restore the data-integrity invariant weakened in the fake DB A prior commit on this branch reshaped fakeDbFromFreshness so prepare() ignores the SQL and always returns the freshness row, dropping the guard that threw when raw staging was read for planning. Restore it as the stronger positive invariant: the catch-up planner must read data_freshness, never raw_*, so a regression that reads raw staging for the max-loaded date fails loudly. Also fix a BG typo in the config test name (областти -> области). * test(ingest): cover FX/OCDS edge branches; reconcile etl branch floor to merged-tree actual Restore ingest to its line floor and hold branches after the merge: - fx.ts: a malformed (unpadded) staged contract_date surfaces through findFxCoverageGaps' MIN/MAX, so loadFxRates must reject the range before fetching — asserted (skips the currency, warns, never fetches). - ocds.ts: a release with no bids block nulls bids_received via the optional chain instead of throwing. Reconcile apps/etl branches 98.5 -> 97 (achieved 97.46). etl LINES hold at 100. The two uncovered branches are provably unreachable: eop.ts parseBucketKeys `m[1] ?? ''` (a matched regex group is never undefined — the `??` is required only by noUncheckedIndexedAccess) and integrity.ts `summary.message ?? '...'` (dead — summarizeIntegrity guarantees a non-null message on the throw path). 97 is still far above the harness's original etl branch floor (58.2); covering the branches would require a fake or a source edit, both disallowed. check:coverage passes for all six workspaces. * test(db,web): address review findings on the coverage ratchet (#254) Four findings from ydimitrof's review, verified against the source before acting. competition.test.ts — the „caps at MAX_TOP" test asserted top:50 → 50 and named a clamp the source does not perform. `getCompetition` reads `p.top === MAX_TOP ? MAX_TOP : DEFAULT_TOP`: an exact 50 selects the large size and everything else falls back to 20, so nothing is ever reduced to 50. The old assertion did kill a mutation (collapsing the toggle to DEFAULT_TOP fails it), but it left the fallback — the half that stops a caller naming its own leaderboard size and its own LIMIT — untested, under a name that overstated it. Renamed and extended to cover 999, 51, 35, 0, -1, NaN and an omitted top. Now kills three mutations, including the `Math.min` clamp the old test would have passed. vitest.shared.ts — dropped `**/*.json` and `**/*.md` from the coverage excludes. The provider only reports files it can instrument as modules, so neither ever reached a report; removing both leaves all six workspaces' numbers byte-identical. Recorded that verification in the comment rather than the globs. vitest.shared.ts — documented the contract behind `**/src/test/**`: the glob is wide, so the exclusion holds only while src/test/ stays test HELPERS. Product code placed there would leave the coverage denominator silently, which is the single way this list can hide an untested module rather than an uncoverable file. search.suggest.test.tsx — filled the dangling „upstream #…" placeholder with the real reference (#225, the read-only D1 chokepoint for #199). trend.test.ts — the local `customDb` returned the period series for every query, including the sector_totals read. Dispatches on the SQL now, like the other fakes in the file, so a later assertion on `sectors` cannot be fed rows of the wrong shape. Coverage unchanged: etl 100/97.64, web 99.47/96.21, config 100/100, db 100/98.47, ingest 100/98.69, shared 98.87/98.33. No floor moved. * test(web): tighten the coverage denominator contract and the date-cell assertions (#254) Second round of ydimitrof's review. vitest.shared.ts — replaced the `**/src/test/**` directory glob with the three files it actually covers: the cloudflare:workers/workflows stubs apps/etl aliases the real modules to, and the ingest SQLite D1 shim. A directory glob was the one entry on this exclude list that could hide an untested product MODULE rather than an uncoverable file, since anything later dropped into src/test/ would leave the coverage denominator by virtue of its location. With an explicit list, a new file there is measured until someone deliberately adds it — a reviewable act. Stricter than a suffix convention, which would still let a *.helper.ts product file through. Coverage is unchanged in all six workspaces, confirming the list covers exactly what the glob did. render-format.test.ts — the date-null case asserted `formatCell(null, 'date')` equals `date(null)`, which only proves delegation and passes for any value the shared formatter returns. Pinned to the literal em-dash, and added the non-null path (a formatted date and an unparseable value that must be echoed, never rendered as a fake date). The pair now kills two mutations the old assertion could not see — including a `date` branch hard-coded to return the em-dash. eop.test.ts — dropped the describe-local afterEach that duplicated the file-level vi.unstubAllGlobals introduced when this file was union-merged with upstream. contracts.test.ts — the two `describe('getContractFacets')` blocks now carry distinct names for what each covers. Coverage unchanged: etl 100/97.64, web 99.47/96.21, config 100/100, db 100/98.47, ingest 100/98.69, shared 98.87/98.33. * test(config): name both paths to the unknown procedure bucket (#254 review) The comment said a whitespace-only value 'trims to ""' and grouped it with the nullish inputs, which reads as if it hits the `if (!procedureType)` guard. It does not: ' ' is truthy, passes the guard, and reaches the map lookup, where .get('') misses and the ?? fallback supplies PROCEDURE_UNKNOWN. Spelling out both paths so the map-miss branch is visibly exercised here as well as by the unrecognised-type case above. * style: run prettier on the three migrated test files Line-length reflow only — no assertion or fixture changes. The three files whose fakeD1 call sites pushed a line past printWidth after the #331 migration. * test: give the fully-covered workspaces a line-floor margin Per @todorkolev on #254: the five workspaces sitting at lines 100 drop to a 99 floor. Measured coverage is unchanged at 100% in all five — this only widens the margin before the ratchet fires. Why it mattered: `tolerance` is a percentage, so it is near-zero slack in a small workspace. At 100/100 a single uncovered line red-builds packages/config (28 lines), packages/test-support (99) and apps/etl (169) — including on PRs that never touch tests. packages/db needed 6 and packages/ingest 3. Branch floors are untouched. * test(web): pin the two ConflictDetail assertions to what they claim Both findings are @ydimitrof's on #254, and both were real. - the source-URL test selected `.cc-source, .conflict-detail`. There is no `.cc-source` in the component — the stat cells carry no per-field class — so it always widened to the whole card, where `toContain('—')` can be satisfied by any other dash. Now pinned to the „Източник" cell via its `<dt>`, with a positive control asserting the same cell holds the declaration link when the URL is present, so a broken finder cannot make the negative case pass. - the in/out-window test asserted only that both rows render and both numbers appear, which holds with the split broken in either direction. Now asserts the per-row `contract-item-conflict` modifier and that the outside row sits behind the „Извън периода" disclosure. Mutation-verified: forcing every row to carry the modifier, forcing none to, and deleting the source-cell fallback each fail the suite. --------- Co-authored-by: Yoan Dimitrov <ydimitrof@users.noreply.github.com> Co-authored-by: Todor Kolev <tkolev@obecto.com>
Какво и защо
В monorepo-то нямаше измерване на test coverage — нито
@vitest/coverage-*, нито праг, а CI пускашеpnpm testбез--coverage. Този PR добавя харнеса от #93: измерване per workspace, ratchet праг който fail-ва CI при спад, и coverage делта видима в PR-а. Само харнесът — новите тестове бяха отделен обхват (#74, вече merged).@vitest/coverage-v8(root devDep) през общ presetvitest.shared.ts;vitest.config.tsза всичките 6 workspace-а с тестове, с експлицитенinclude— иначе v8 provider-ът брои само файловете, заредени от тестове, и нов непокрит модул би бил невидим за ratchet-а.coverage-baseline.json(seed-нат от реалните числа) +scripts/check-coverage.mjs— lines% и branches% на всеки workspace не може да падне >0.5pp под baseline-а. При покачване >1pp скриптът подканя--update(локално; никога в CI). Умишлен спад = сваляне на числото в baseline-а в същия PR, видимо на ревю. Общият сбор е информативен, смятан от сумирани covered/total бройки, не осреднени проценти.pnpm test -- --coverage(тестовете се пускат веднъж), ratchet gate със self-test първо (по модела на docs проверката), таблицата с делтите в step summary + artifactcoverage-report. Sticky PR коментар само за same-repo PR-и — fork PR-ите получават read-only token независимо отpermissions:, затова коментарният job е отделен, гейтнат поhead.repo.full_nameиcontinue-on-error;checkjob-ът остава с read-only token.testtask-ът декларираoutputs: ["coverage/**"], така че cache replay възстановява отчетите (проверено: FULL TURBO + наличенcoverage-summary.json).docs/review-testing.md.Бележка:
apps/web/tsconfig.node.jsonдобавя../../vitest.shared.tsвinclude(composite проектът изисква изброени файлове).packages/api-contractе извън baseline-а, докато няма тестове. Sticky-коментарният job може да се провери реално чак на този PR — всичко останало е проверено локално.Свързан issue
Closes #93.
Вид промяна
test/ci/build/chore— поддръжкаКак е тествано
pnpm check:coverage:test— 12/12 (node:test, адверсариални случаи: спад отвъд толеранса, спад в толеранса, независим branches ratchet, липсващ отчет/метрика, merge на бройки vs осредняване).pnpm test -- --coverage— 6/6 workspace-а зелени, всеки произвеждаcoverage/coverage-summary.json.packages/shared+2pp → exit 1 с actionable съобщение; +0.3pp → exit 0 (толеранс).outputs).pnpm typecheck,pnpm lint,pnpm check:docs— зелени.Чеклист
Co-Authored-By:trailermidt-bg/sigma:mainpnpm typecheckминаваpnpm test(поне за засегнатите пакети) минаваpnpm lintе чисто.env*или.dev.varsdocs/е обновена, ако промяната го налага