Skip to content

test: тестово покритие ≥95% за всички workspaces (надгражда #216) - #254

Open
lyubomir-bozhinov wants to merge 39 commits into
midt-bg:mainfrom
lyubomir-bozhinov:test/coverage-ratchet
Open

test: тестово покритие ≥95% за всички workspaces (надгражда #216)#254
lyubomir-bozhinov wants to merge 39 commits into
midt-bg:mainfrom
lyubomir-bozhinov:test/coverage-ratchet

Conversation

@lyubomir-bozhinov

@lyubomir-bozhinov lyubomir-bozhinov commented Jul 17, 2026

Copy link
Copy Markdown
Collaborator

Надгражда coverage харнеса от #216 с реалните тестове и вдига ratchet прага на ≥96% клонове и ≥98.8% редове за всеки workspace (повечето на 100%). Само тестове — нулева промяна в продуктов код.

⚠️ Ред на сливане — зависи от #216

Този PR трябва да се слее след #216 (ydimitrof:feat/coverage-ratchet), който все още е отворен.

#216 въвежда самия харнес — scripts/check-coverage.mjs + check-coverage.test.mjs, споделения vitest.shared.ts, per-workspace vitest.config.ts, coverage-baseline.json (с началните, ниски прагове) и CI стъпката. Тук този харнес е vendor-нат едно към едно (двата скрипта са байтово идентични с #216), за да могат тестовете да се пишат и пускат срещу реалната порта преди #216 да е слят.

Слят преди #216, този клон би въвел харнеса, CI стъпката и конфигурациите за втори път → конфликт.

След като #216 се слее, клонът се rebase-ва върху main; vendor-натият commit (69d0a34) се свива (идентично съдържание), а нетното участие остава:

  • тестовете за покритие по всичките шест workspace-а;
  • vitest.shared.ts: изключване на **/fixtures/**, **/*.json, **/*.md, **/*.d.ts от инструментирането (данни/типове/докове, не изпълним код — иначе висят на постоянни 0% и v8 remap-ът чупи на markdown);
  • coverage-baseline.json: праговете вдигнати от началните на test: измерване на coverage и ratchet праг в CI (#93) #216 до текущите (виж таблицата).

Резултат

Workspace Редове Клонове Праг (редове/клонове)
apps/etl 100% 98.5% 100 / 98.5
apps/web 99.44% 96.0% 99.4 / 96
packages/config 100% 100% 100 / 100
packages/db 100% 98.39% 100 / 98.3
packages/ingest 100% 98.7% 100 / 98.7
packages/shared 98.85% 98.33% 98.8 / 98.3

Общо (информативно): 99.73% редове / 97.51% клонове. node scripts/check-coverage.mjs минава; собственият му self-test (check-coverage.test.mjs) също. CI е зелено (typecheck, lint, тестове).

Бележки

  • Нулева промяна в продуктов код — само тестове, конфигурация за покритие и базовата линия.
  • Реални поведенчески тестове, не cheater-и: маркер-базирани фалшиви D1 драйвери, които диспечират по SQL и хвърлят при неочаквана заявка; мокнати SDK/binding-и; проверки по байтове за BOM; end-to-end обхождане на курсори; adversarial входни данни.
  • Никакво отслабване на прага — базовата линия само се вдига, никога не се сваля.
  • Умишлено непокрити остават само реално недостижими defensive клонове — ?? fallback-и върху regex групи, които винаги съвпадат; if(done) re-entry на stream (stream не дърпа след close()); split().pop() ?? (pop винаги връща стойност); homoglyph картата (всеки съвпаднал символ е мапнат); minContracts ?? DEFAULT, което оркестраторът вече нормализира; env-gated PROD пътища под vitest. Оставени са непокрити, а не заобиколени чрез отслабване на кода или v8 ignore прагми.

Свързани: #216 (харнес), #93 (епик за покритие).

ydimitrof and others added 30 commits July 16, 2026 18:28
Vendored from midt-bg#216 (feat/coverage-ratchet) to put the coverage machinery
in place ahead of midt-bg#217, while midt-bg#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 midt-bg#93, midt-bg#216.
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.
… 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.
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.
…dology, 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.
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.
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.
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.
… 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).
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%.
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%.
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%.
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%.
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%.
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%.
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).
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%.
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%.
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).
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.
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.
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.
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%.
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
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.
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.
- 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.
- 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.
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.
@lyubomir-bozhinov
lyubomir-bozhinov marked this pull request as ready for review July 17, 2026 07:22

@ydimitrof ydimitrof left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Обобщен преглед на PR — тест: тестово покритие ≥95% за всички workspaces (надгражда #216)

Вердикт: COMMENT — одобрение при зелен CI. Няма блокиращи проблеми по сигурност или коректност; само незадължителни бележки за хигиена и втвърдяване.

Какво прави PR-ът

Добавя обширно тестово покритие (цел ≥95%) за всички workspaces, надграждайки #216, заедно с инфраструктурата за неговото прилагане:

  • Нови и разширени тестови файлове за apps/etl, packages/db, packages/config и др. (заявки за договори, детайли, потоци, начална страница, идентичност, keyset пагинация, методология, мрежи, региони, търсене; sitemaps, trend, ocds, refresh, staging, format).
  • Конфигурация за покритие: vitest.config.ts файлове, vitest.shared, coverage-baseline.json, devDependency @vitest/coverage-v8.
  • Инструментариум за ratchet gate (#93): scripts/check-coverage.mjs + тестове, turbo.json.
  • CI workflow за коментар с покритие, .gitignore и документация (docs/review-testing.md).

Почти всички промени са тестове/конфигурация — няма промени в продукционен рънтайм код.

Сигурност (Фаза 0) — ЧИСТО във всичките 5 партиди

  • Няма твърдо кодирани тайни; използваните ключове/ЕИК ('k', 111111111, 123456789) са фиктивни фикстури.
  • Няма нови реални URL адреси или разширяване на allowlist; новите хостове са само в тестови стъбове. Тестът за redirect към evil.example потвърждава, че защитата срещу SSRF остава активна.
  • GitHub Actions: външните actions са закачени по commit SHA; тригерът е pull_request (не pull_request_target); coverage-comment е ограничен до PR-и от същото repo, а pull-requests: write е scoped само за този job. Съответства на OWASP CI/CD.
  • check-coverage.mjs е с fail-closed дизайн, стриктна валидация срещу path traversal (WORKSPACE_KEY regex) и защита срещу prototype pollution (hasOwnProperty.call).
  • Няма злонамерен код, обфускация, бекдори или подозрителни зависимости. @vitest/coverage-v8 и транзитивните ѝ пакети са легитимни dev зависимости, само в devDependencies.
  • Тестовете дори засилват сигурността, като проверяват параметризирано свързване (?, WHERE source = ?), екраниране (О''Брайън, <&>&lt;&amp;&gt;) и отхвърляне на prototype ключове.

Качество на тестовете — високо

Тестовете са смислени и целят разкриване на дефекти, а не тривиални „cheater" тестове: покриват гранични случаи, null-клонове, HTTP 403/404/500, пагинация през CHUNK граница, fallback вериги за дати, безопасно деление, UTF-8 BOM, BG десетична запетая, taxonomy интегритет и адверсариални случаи (traversal/proto). Числените очаквания са проверени за вътрешна консистентност. Инструментариумът за покритие е с чисто разделение на pure функции и монотонен ratchet.

Незадължителни бележки (не блокиращи)

  1. Слабост в покритието (партида 3): фалшивите D1 обекти изхвърлят bind-аргументите, затова тестовете не проверяват реалните стойности на параметрите, техния ред или LIMIT (напр. лимитът 5 в listSingleOfferContracts не се верифицира). Препоръка: фалшивите обекти да записват bind-аргументите и поне един тест на заявка да ги утвърждава.
  2. Крехкост/дублиране: много асерции са плътно свързани с точния SQL текст (рефактор би ги счупил); конструкторите на фалшиви D1 обекти се повтарят между файловете — да се извлече споделен помощен модул за фикстури.
  3. Несъответствие на версии: @vitest/coverage-v8 (4.1.10) изисква peer vitest 4.1.10, а инсталираният е 4.1.7 — вероятен peer-warning; изравнете версиите за консистентен lock файл.
  4. Дребни хигиенни: ScrollToTop.test.tsx презаписва window.requestAnimationFrame без възстановяване; coverage-comment обновява само първия коментар с маркера; правописна грешка „областти"; ред на зависимостите в package.json; неувити JSON.parse/предположения за наличие на apps//packages/ в скрипта.

Условие преди merge

Твърдението за ≥95% покритие и 100% преминаване на тестовете не е потвърдено емпирично в нито една партида (тестовете не можаха да се изпълнят локално). Финалното одобрение зависи от зелен CI с отчет за покритие.

// Hold the rAF callback instead of running it synchronously so a second scroll lands while a
// frame is still pending — exercising the `if (ticking) return` early-out.
let pending: FrameRequestCallback | null = null;
window.requestAnimationFrame = (cb: FrameRequestCallback) => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Тук window.requestAnimationFrame се презаписва глобално, но не се възстановява след теста (няма afterEach/съхранена оригинална референция). Стъбът само задържа callback-а и не го изпълнява, така че всеки следващ тест във файла, който разчита на rAF да задейства scroll-логиката, може да се повлияе в зависимост от реда на изпълнение. Препоръка: запазете оригинала и го върнете в afterEach (или използвайте vi.stubGlobal('requestAnimationFrame', ...) + vi.unstubAllGlobals()), за да гарантирате изолация между тестовете.

Comment thread apps/web/app/lib/riskLogic.test.ts Outdated
});

it('emits no competition flag when the bid count is unknown (bidsReceived null)', () => {
// admitted is null → neither competition branch fires (the `bidsReceived != null` false path).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Коментарът гласи „admitted is null“, но входът е bidsReceived: null, bidsRejected: 0. admitted не е null — по-скоро bidsReceived != null е false, така че нито един competition клон не се задейства. Предложение: изравнете текста на коментара с реалния клон, който се тества, за да е ясно намерението.


describe('procedureGroup (edge inputs)', () => {
it('trims surrounding whitespace before lookup', () => {
expect(procedureGroup(' Пряко договаряне ').key).toBe('direct');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Коментарът си противоречи: ' '.trim() дава '', което е falsy, а не „truthy“. С типична имплементация (пазач if (!key) return UNKNOWN) този вход попада в falsy-пазача — точно както undefined и null на следващите редове — и НЕ минава през map-miss клона (?? UNKNOWN). Тоест твърдението „takes the map-miss path“ е невярно и map-miss клонът може да остане непокрит. Предложение: добавете вход с непразен, но неизвестен ключ (напр. procedureGroup('няма такава процедура').key), за да покриете реално map-miss клона, и коригирайте коментара.

const db = {
prepare() {
return {
bind() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Този (и всички останали фалшиви D1 обекти в партидата) дефинира bind() { return this } и изхвърля подадените аргументи. Резултат: тестовете проверяват само SQL текста, но никога реалните стойности/реда на bind-параметрите. Клас грешки (грешно свързана стойност или ред) остава непокрит. Препоръка: фалшивият обект да записва аргументите на bind() и поне един тест да ги утвърждава.

});
});

// A SQL-capturing spy over the standard fakeDb rows — for asserting which predicates buildFilters emits.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Асерциите тук са плътно свързани с точния SQL низ (напр. substr(c.signed_at, 1, 4) IN, c.amount_eur >= ? AND c.amount_eur < ?). Поведенчески еквивалентен рефактор ще счупи теста без реален дефект. Където е възможно, проверявайте резултатното поведение вместо суровия SQL, за да намалите крехкостта.

},
};
},
} as unknown as D1Database;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

listSingleOfferContracts(v.db, 'value', 5) подава лимит 5, но фалшивият обект връща един ред независимо от него, а тестът не утвърждава, че LIMIT/лимитът достига до заявката. Затова toHaveLength(1) е тривиално удовлетворено. Добавете проверка, че лимитът се появява в SQL или се подава чрез bind.

Comment thread pnpm-lock.yaml
version: 25.9.1
'@vitest/coverage-v8':
specifier: ^4.1.7
version: 4.1.10(vitest@4.1.7)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Незадължителна бележка: @vitest/coverage-v8@4.1.10 декларира peer vitest: 4.1.10, но резолвнатата версия на vitest в този lock файл е 4.1.7 (4.1.10(vitest@4.1.7)). Разминаването е малко (patch ниво) и вероятно работи, но ще предизвика peer-dependency warning при pnpm install. Препоръчвам да изравните версиите — напр. да вдигнете vitest до 4.1.10 — за да остане lock файлът консистентен. Не е блокиращо.

function findTestWorkspaces() {
const found = [];
for (const group of ['apps', 'packages']) {
for (const entry of readdirSync(join(ROOT, group), { withFileTypes: true })) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Незадължително втвърдяване: readdirSync(join(ROOT, group)) ще хвърли ENOENT, ако директорията apps/ или packages/ липсва в репото. Това е единственият изход, който не дава ясното, насочващо съобщение, характерно за останалата част от скрипта. Предложение: if (!existsSync(join(ROOT, group))) continue; преди readdirSync, за да остане поведението fail-safe и съгласувано.

if (!entry.isDirectory()) continue;
const pkgPath = join(ROOT, group, entry.name, 'package.json');
if (!existsSync(pkgPath)) continue;
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Дребно: JSON.parse(readFileSync(pkgPath, 'utf8')) не е защитен — един малформиран package.json в произволна работна директория ще срине откриването на работни директории с неясна грешка от JSON.parse вместо с посочване на виновния файл. Обвиване в try/catch с добавяне на пътя към съобщението би било в духа на останалите точни съобщения за грешка тук.

Comment thread vitest.shared.ts
provider: 'v8',
// Explicit include: without it the v8 provider only reports files loaded
// by tests, so a new untested module would be invisible to the ratchet.
include,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Коментарът твърди, че самото include прави нетестван модул видим за ratchet-а. Реално това работи, защото vitest подразбиращо използва coverage.all: true — при all: false включените, но неимпортирани файлове НЕ биха се отчели и новият нетестван модул би бил невидим. Тъй като целият гейт зависи от това поведение, препоръчвам да зададете all: true изрично тук, за да сте устойчиви на бъдеща промяна на подразбиращите се стойности на vitest. Незадължително, но премахва скрита зависимост.

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.

@ydimitrof ydimitrof left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Обобщен преглед на PR — test: тестово покритие ≥95% за всички workspaces (надгражда #216)

Какво прави PR-ът

Този PR въвежда механизъм за налагане на минимум ≥95% тестово покритие във всички workspaces на monorepo-то. Промените са почти изцяло тестове и конфигурация — без промени по продукционен код:

  • CI/инструментариум: нов coverage-comment job в .github/workflows/ci.yml (sticky коментар с обобщение на покритието), scripts/check-coverage.mjs + тестове (ratchet гейт срещу coverage-baseline.json), vitest.shared.ts с явен include, промени в turbo.json (outputs: coverage/**) и .gitignore.
  • Coverage провайдър: добавен @vitest/coverage-v8 и съответните vitest.config.ts за apps/etl, db, ingest, shared и др.
  • Тестове: голям набор нови/разширени *.test.ts(x) за apps/etl, apps/web, packages/* (query-builder-и, нормализатори, sitemaps, OCDS, staging/refresh, retry, риск-логика, таксономия и т.н.).

Сигурност (Фаза 0) — ЧИСТО във всички партиди

  • Няма твърдо кодирани тайни/креденшъли — само тестови фикстури (BGGPT_API_KEY: 'k', NONCE, https://x.test и подобни).
  • Няма зловреден код, бекдори, обфускация, eval или мрежови изтичания.
  • CI правата са минимални: contents: read по подразбиране, pull-requests: write е ограничено само до coverage-comment, а fork PR-ите са коректно изключени; GitHub Actions са пинати по commit SHA.
  • check-coverage.mjs е защитен срещу path traversal (WORKSPACE_KEY regex, валидация преди цикъла) и prototype pollution (hasOwnProperty.call, отхвърляне на __proto__) — тестово покрито.
  • Проверена е и подозрителната транзитивна зависимост obug@2.1.1 — това е легитимен форк на debug, а не typosquat.

Качество на тестовете — високо

Тестовете са смислени и адверсариални (не „cheater“ тестове): целят конкретни клонове, гранични случаи и реални инварианти (напр. пагинация през CHUNK граници, ?? null разклонения, chunk=100 разделяне при DELETE+INSERT, коректно сумиране covered/total вместо осредняване на проценти, fail-closed поведение при празен/остарял baseline). Ресурсите се почистват коректно (unstubAllGlobals, clearAllMocks, mockRestore). Ratchet-логиката е коректна и не позволява тиха регресия.

Незблокиращи забележки (по желание)

  1. Supply-chain верификация: upload-artifact@v7.0.1 и download-artifact@v8.0.1 са с необичайно високи мажорни версии — потвърдете, че пинатите SHA отговарят на официалните тагове.
  2. Peer/версийно разминаване: @vitest/coverage-v8@4.1.10 очаква vitest 4.1.10, а репото фиксира 4.1.7 (влизат две версии на @vitest/utils) — потвърдете, че pnpm install --frozen-lockfile минава чисто в CI.
  3. Лимит на PR коментар: sticky коментарът праща цялата таблица; при много workspaces може да надхвърли ~65 KB лимита на GitHub (смекчено с continue-on-error, но обмислете съкращаване/линк към артефакта).
  4. Детерминизъм: тестове, разчитащи на реалния часовник („defaults today…“, „defaults fetchedAt to now“) и retry.test.ts (реални таймери ~350ms) биха били по-стабилни с vi.useFakeTimers().
  5. Козметика: @vitest/coverage-v8 нарушава азбучния ред в package.json; findTestWorkspaces() няма existsSync предпазка за липсваща apps/packages директория; информационният „Total“ ред може леко да занижи при summary без covered/total (не влияе на самия ратчет).

Блокиращи проблеми

Няма. Сигурността е чиста, а промените са безопасни за merge.

Вердикт

COMMENT — кодът и тестовете са с високо качество и без блокиращи находки. Единственото условие преди пълно APPROVE е потвърждение от CI, че тестовете, check:coverage:test и целият pipeline минават зелени и че реалното покритие ≥95% е достигнато — това не можа да бъде изпълнено в средата на прегледа.

Comment thread .github/workflows/ci.yml
pnpm check:coverage
- name: Upload coverage report
if: ${{ !cancelled() }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Supply-chain проверка: екшънът е закачен на таг v7.0.1 (SHA 043fb46...), което е необичайно висока мажорна версия за actions/upload-artifact (познатите upstream издания са в порядъка v4.x). Моля потвърдете, че пинатият SHA наистина сочи официалния тег, а не подменен/typosquat екшън. SHA-пинът е добра практика — това е само верификация.

Comment thread .github/workflows/ci.yml
pull-requests: write
continue-on-error: true
steps:
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Същата бележка за actions/download-artifact@... (таг v8.0.1): потвърдете съответствието SHA ↔ официален upstream таг. Иначе job-ът coverage-comment е коректно защитен — има само pull-requests: write, ограничен е до same-repo PR-и и е continue-on-error, така че евентуален проблем с коментара не блокира merge.

Comment thread .github/workflows/ci.yml
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

-F body=@coverage-summary.md праща цялото съдържание на файла като тяло на коментар. GitHub има лимит ~65 KB за тяло на коментар; при много workspaces рендерираната таблица може да го надхвърли и POST/PATCH да върне 422. Смекчено е чрез continue-on-error: true, но обмислете съкращаване или линк към артефакта, за да не изчезва тихо коментарът.

Comment thread package.json
},
"devDependencies": {
"@cloudflare/workers-types": "^4.20260521.1",
"@vitest/coverage-v8": "^4.1.7",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Незначителна забележка: @vitest/coverage-v8 е поставен преди @types/node, което нарушава азбучното подреждане на devDependencies (@types предхожда @vitest). Ако в проекта е активен sort-package-json, това ще падне на pnpm lint. Препоръка: преместете реда след @types/node.

// A non-Error thrown value exercises the `: error` log branch, and a 4th attempt indexes past
// the two-entry BACKOFF_MS table, exercising the `?? 150` fallback.
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
const setTimeoutSpy = vi.spyOn(globalThis, 'setTimeout');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

vi.spyOn(globalThis, 'setTimeout') без mockImplementation вика оригиналния таймер, така че тестът реално изчаква 50+150+150≈350ms. Коректно е, но добавя реално забавяне към суита. Обмислете vi.useFakeTimers() + ръчно придвижване на времето за по-бърз и по-детерминиран тест, като пак се проверяват delay стойностите.

…ive 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.

@ydimitrof ydimitrof left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ревю на PR — тестово покритие ≥95% за всички workspaces (надгражда #216)

ВЕРДИКТ: COMMENT — няма блокиращи проблеми със сигурността или целостта на данните. Преди merge изравнете версиите на vitest / @vitest/coverage-v8 и потвърдете зелен CI (минаващи тестове + реално потвърдено покритие ≥95%).

Какво прави PR-ът

PR-ът вдига тестовото покритие до ≥95% за всички workspaces и въвежда механизъм за налагане на прага. Промените са почти изцяло тестове (*.test.ts / *.test.tsx / *.mjs) плюс поддържаща инфраструктура:

  • Vitest конфигурация за покритие (vitest.shared.ts, per-workspace vitest.config.ts, packages/config/vitest.config.ts);
  • coverage ratchet с baseline и толеранс (coverage-baseline.json, scripts/check-coverage.mjs + тестове, turbo.json);
  • CI задание coverage-comment в .github/workflows/ci.yml;
  • документация (docs/review-testing.md);
  • нова devDependency @vitest/coverage-v8.

Практически няма промени в продукционен код, което присъщо ограничава риска до качеството на тестовете и коректността на конфигурацията.

Сигурност (Фаза 0 — скан по всички партиди): ЧИСТО

  • Няма твърдо кодирани тайни — стойности като BGGPT_API_KEY: 'k', ЕИК-ове и имена на фирми са очевидно фиктивни тестови данни.
  • URL адреси — само тестови фикстури или вече одобрени домейни; тестовете дори утвърждават allowlist-а (отхвърляне на пренасочване към evil.example).
  • Няма злонамерен код — backdoor, инжекция, обфускация, eval или динамично изпълнение.
  • Зависимости — нови са само @vitest/coverage-v8 (легитимен официален пакет) и транзитивната obug@2.1.1 (проверена — легитимен форк на debug, не typosquat). GitHub Action-ите са закачени към конкретни commit SHA.
  • CI (coverage-comment) — коректно защитено: използва pull_request (не pull_request_target), ограничено е до PR от същото repo, не прави checkout на неоторизиран код, подава PR_NUMBER / GH_REPO през env: (без script injection) и е continue-on-error. Съответства на принципа за най-малки привилегии.
  • SQL / OWASP — тестовете не изпълняват реален SQL; продукционните заявки са параметризирани (? + .bind()), а keyset/assertReadOnlySelect изрично отхвърлят неразрешени sortCol/dir (потвърдено с тест won_eur; DROP TABLE contracts). Има и изрична защита срещу prototype pollution (normalizeAuthoritySort('toString') → стойност по подразбиране).

Качество на тестовете: високо

Тестовете са смислени и целенасочено покриват клонове и грешкови пътища, а не тривиални минавания: гранични и грешкови случаи (non-array payload, invalid JSON, HTTP 403/404/500, деление на нула, YoY срещу нулева година), rAF coalescing и abort при multipart R2 неуспех, CSV детайли (BOM, keyset chunking), пагинация с разкриващи дефекти курсори, taxonomy integrity карти. Изчистването на ресурси е добро (възстановяване на глобали, fake timers, spy-ове). check-coverage.mjs е добре разделен на чисти функции + main(), с fail-closed валидация и строг whitelist срещу path traversal.

Основни забележки (незадължителни за блокиране)

  1. Несъответствие на peer dependency@vitest/coverage-v8@4.1.10 изисква vitest@4.1.10, но проектът остава на 4.1.7. Изравнете версиите (закачете coverage-v8 на 4.1.7 или вдигнете vitest на 4.1.10), за да избегнете фини несъвместимости в инструментацията на покритието.
  2. Кеширане в turbo.json"outputs": ["coverage/**"] може да върне cache hit без coverage/ директория, ако test бъде пуснат веднъж без --coverage, което ще срине check-coverage.mjs. Уверете се, че CI винаги подава --coverage последователно.
  3. Отслабена инвариантна проверка — в apps/etl/src/eop.test.ts новият fakeDbFromFreshness вече игнорира SQL-а и винаги връща freshness реда; старата версия хвърляше грешка при четене на raw_contracts и така пазеше инварианта, че планировчикът чете обслужената freshness, а не изтекло raw staging. Тази негативна проверка за целост на данните вече не се налага.
  4. Прагът ≥95% трябва да е реално наложен — той се дефинира в vitest.shared.ts; потвърдете, че конфигурацията реално fail-ва при под прага, иначе целта „≥95%“ не е гарантирана.

Дребни бележки

  • Крехки тестове, обвързани с точни SQL фрагменти и sql.includes(...) в fake D1 — работят коректно, но са чувствителни към бъдещ рефакторинг на заявките (риск от тихо минаване по грешен клон).
  • Фиктивният D1 bind() връща this и игнорира аргументите — тестовете доказват текста на SQL, но не и че потребителският вход достига bind(...) като параметър; препоръчва се поне един такъв тест.
  • getContractFacets разчита на реалния часовник — инжектиране на фиксирана дата би било по-издръжливо.
  • package.json: @vitest/coverage-v8 нарушава азбучната подредба на devDependencies.
  • Правопис: „28 областти“ → „28 области“ (packages/config/src/index.test.ts).
  • mergedTotal връща pct: 100 при total === 0 (козметично).

Заключение

Чист, добре структуриран PR, изцяло насочен към тестово покритие, без проблеми със сигурността или целостта на данните. Одобрението зависи от зелен CI (минаващи тестове + потвърдено покритие ≥95%), който не може да се провери на ниво диф, и от изравняване на версиите vitest / @vitest/coverage-v8.

Comment thread apps/etl/src/eop.test.ts
vi.unstubAllGlobals();
});

function fakeDbFromFreshness(maxLoadedDate: string | null): D1Database {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Новият fakeDbFromFreshness игнорира подадения SQL и винаги връща реда за freshness. Предишната версия хвърляше грешка при sql.includes('raw_contracts') (raw staging should not be read for planning), с което тестът налагаше инварианта, че планировчикът чете обслужената freshness, а не изтекло raw staging. Сега тестът „plans an uncapped window straight from served freshness“ ще мине дори ако продукционният код регресира и започне да чете raw_contracts. Предложение: запазете негативната проверка (хвърляне при четене на raw staging) в мока, за да не се губи тази проверка за целост на данните.

Comment thread package.json
},
"devDependencies": {
"@cloudflare/workers-types": "^4.20260521.1",
"@vitest/coverage-v8": "^4.1.7",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Дребна бележка (не блокираща): @vitest/coverage-v8 е добавен преди @types/node, което нарушава азбучната подредба на devDependencies (@types/* следва да е преди @vitest/*). За консистентност с останалите записи преместете реда след @types/node.

Comment thread pnpm-lock.yaml
resolution: {integrity: sha512-IM49HmthevbgAO4anp1hwtoT9wYe59w0LR00gr+eagHE+ZJ5lK4sLPeO0ubgoJcwLk6dehU3R24N+FbEEKDc8g==}
peerDependencies:
'@vitest/browser': 4.1.10
vitest: 4.1.10

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Несъответствие в peer dependency: @vitest/coverage-v8@4.1.10 изисква peer vitest@4.1.10 (виж реда тук), но проектът остава на vitest@4.1.7 — lockfile-ът я резолвва като 4.1.10(vitest@4.1.7). Тази комбинация не е официално поддържана и може да доведе до фини несъвместимости в инструментацията на покритието.

Препоръка: изравнете версиите — закачете @vitest/coverage-v8 на 4.1.7 (тази версия съществува и peer-ът ѝ е точно vitest@4.1.7), или вдигнете vitest на 4.1.10.

Comment thread turbo.json
"test": {
"dependsOn": ["^build"]
"dependsOn": ["^build"],
"outputs": ["coverage/**"]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Кеширането на coverage/** като изход на test таска е рисково: ако test бъде изпълнен веднъж без --coverage, turbo кешира резултат без директория coverage/. Последващ pnpm test -- --coverage може да е cache hit и да НЕ генерира отчет, при което scripts/check-coverage.mjs пада с „No coverage report for …“.

Уверете се, че CI винаги подава --coverage последователно, или че cache ключът различава наличието на флага (напр. отделен test:coverage таск). Иначе ще имате спорадични CI провали или, по-лошо, тихо пропускане на ratchet проверката при частичен кеш.

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.
@lyubomir-bozhinov

Copy link
Copy Markdown
Collaborator Author

Благодаря за прегледа. CI вече е зелен (условието за APPROVE). По бележките:

Отстранено (ad980b0, 686f70e) — всяко потвърдено с мутационен тест:

  • bind-аргументи / LIMIT (преглед 1, т.1): fake D1 обектите вече записват bind-аргументите; listSingleOfferContracts утвърждава, че лимитът достига LIMIT ?[5] явно, [10] по подразбиране.
  • ScrollToTop.test.tsx rAF (т.4): оригиналният requestAnimationFrame се пази в beforeEach и се възстановява в afterEach; тестът за coalescing брои точно едно rAF извикване.
  • Детерминизъм (преглед 2, т.4): retry.test.ts мина под vi.useFakeTimers() (без ~350ms реален престой; backoff-ите се асертват [50, 150, 150]); eop „defaults today“ пинва часовника и асертва точната дата, а не само формата.
  • Покрай това редица плитки асерции станаха мутационно-чувствителни: procedureMix → DTO форма, числено сортиране на обособени позиции, picker mapping и за двата клона, горната граница на page-math, приоритет на lastmod, oversized cursor през реален payload.

Към #216 (вендорнатият harness, не този клон) — уреждат се при rebase след merge на #216:

  • peer-разминаване @vitest/coverage-v8vitest / lock файла (пр.1 т.3, пр.2 т.2);
  • SHA на upload/download-artifact (пр.2 т.1);
  • маркер/лимит на sticky коментара (пр.1 т.4, пр.2 т.3);
  • ред на devDependencies, existsSync предпазка и обвиване на JSON.parse в check-coverage.mjs, правописът „областти“ (пр.1 т.4, пр.2 т.5).

Оставащо, несблокиращо:

  • Споделен fixture-хелпър за fake D1 конструкторите (пр.1 т.2) — основателно, но по-широк рефактор от този PR; отделям го.
  • eop „defaults fetchedAt to now“ (пр.2 т.4) — стойността не е наблюдаема от резултата на ingestBucketWindow (излизат само броячи), затова я оставям както е засега.

Merge редът остава: #254 след #216.

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 (областти -> области).

@ydimitrof ydimitrof left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Обобщено ревю на PR — тестово покритие ≥95% за всички workspaces (надгражда #216)

Какво прави PR-ът

PR-ът добавя тестово покритие с цел ≥95% за всички workspaces в монорепото, заедно със съпътстваща CI и конфигурация за покритие. Промените са изцяло тестови/CI/инструментариум — няма промени в продукционен или рънтайм код. Обхватът включва:

  • Тестови файлове в apps/etl, apps/web, packages/config, packages/db и др.
  • CI pipeline (.github/workflows/ci.yml), .gitignore, turbo.json
  • Конфигурация за покритие: vitest.shared.ts, vitest.config.ts, coverage-baseline.json, tsconfig.node.json, package.json
  • Скрипт scripts/check-coverage.mjs (ratchet спрямо baseline) и неговите тестове
  • Нова dev-зависимост @vitest/coverage-v8 и pnpm-lock.yaml

Сигурност и цялост на данните — ЧИСТО ✅

  • Няма hardcode-нати тайни, backdoor-и, инжекции, обфускация или eval. Стойности като BGGPT_API_KEY: 'k', NONCE = 'abc123nonce', 'sha256-abc' са тестови фикстури. Хостовете (https://x/, https://gw.example/v1 и т.н.) са фалшиви/тестови; evil.example присъства само в позитивен тест, който проверява блокиране на пренасочване към чужд хост.
  • GitHub Actions: добра практика — permissions: contents: read по подразбиране; pull-requests: write е стеснено само до job-а coverage-comment и до PR-и от същото repo (fork PR-и не получават write токен). Actions са пиннати по SHA.
  • Зависимости: транзитивното дърво на @vitest/coverage-v8@4.1.10 е сверено точно спрямо npm registry. Съмнителният на пръв поглед obug е потвърден като легитимен форк на debug и официална зависимост, не typosquat.
  • Тестовете затвърждават добри защити: sql-guard/run_sql отхвърлят опасни заявки и не изтичат сурови D1 грешки към потребителя; render-format блокира инжекция в entityHref; WORKSPACE_KEY regex отхвърля path traversal и __proto__ ключове.

Качество на тестовете

Тестовете са смислени и адверсариални, не „cheater“ — целят конкретни клонове и гранични случаи (rAF coalescing, capped/zero-ingest, finally-drop при грешка, backward pagination, prototype-ключове, taxonomy integrity, знак U+2212, нефинитни стойности). Добро покритие на branch-овете и чисто разделяне на чисти функции от I/O логиката.

Незблокиращи забележки за адресиране преди merge

  1. Съвместимост на версиите на vitest (най-важна). @vitest/coverage-v8@4.1.10 декларира точен peer vitest 4.1.10, а проектът е на vitest@4.1.7. Неудовлетворен peer може да предизвика фини несъвместимости и/или --coverage да гръмне в CI. Препоръка: вдигнете vitest на 4.1.10.
  2. Catch-22 в scripts/check-coverage.mjs --update. validateBaseline() се изпълнява безусловно преди клона if (update), затова нов workspace с test скрипт води до exit(1), преди --update изобщо да може да го bootstrap-не — въпреки че съобщението съветва точно тази команда. Пропуснете проверката „missing entry“ в режим --update.
  3. Сверете SHA на upload-artifact/download-artifact с коментираните версии (# v7.0.1 / # v8.0.1).
  4. tsconfig обхват: уверете се, че tsc -b не връща „file is not under rootDir“ за ../../vitest.shared.ts при composite: true (и че tsconfig на packages/config/packages/db включват shared файла).
  5. @vitest/coverage-v8 в package.json нарушава азбучния ред на devDependencies (риск при sort-lint).
  6. turbo.json кешира coverage/** за таск test. Cache hit при пускане без --coverage може да възстанови празна папка и да провали check-coverage.mjs (fail-closed, но объркващо). Уверете се, че CI винаги пуска с --coverage.
  7. Заглавие ↔ механизъм: „≥95%“ реално е ratchet спрямо baseline с толеранс, а не абсолютен праг. Потвърдете, че стойностите в coverage-baseline.json действително са ≥95, иначе ratchet-ът заключва по-ниско ниво.
  8. Интеграционна увереност: apps/etl тестовете мокват изцяло @sigma/ingest и ./eop, което е коректно за unit-ниво, но покритие ≥95% при широко мокване може да надценява реалната интеграционна увереност — потвърдете, че отделен integration слой покрива реалните staging writer-и.

Присъда

COMMENT — Няма блокиращи проблеми със сигурността или целостта на данните. Преди APPROVE адресирайте най-вече несъответствието на версията на vitest (т.1) и catch-22 при --update (т.2), както и потвърдете локално, че тестовете преминават и покритието отговаря на прага.

Comment thread .github/workflows/ci.yml
pnpm check:coverage
- name: Upload coverage report
if: ${{ !cancelled() }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Supply-chain проверка: action-ът е пиннат по SHA (добре), но коментарът # v7.0.1 за actions/upload-artifact# v8.0.1 за download-artifact по-долу) не съответства на познатите официални мажорни версии. Моля, потвърдете, че закаченият SHA 043fb46d... сочи именно към официалния таг преди merge — подвеждащ коментар за версия върху пиннат SHA е класически вектор за компрометиране на pipeline.

Comment thread .github/workflows/ci.yml
pull-requests: write
continue-on-error: true
steps:
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

coverage-comment job-ът е добре ограден (само PR-и от същото repo, pull-requests: write само тук, continue-on-error). Тялото на коментара идва от coverage-summary.md в артефакта, рендериран от scripts/check-coverage.mjs (извън тази партида). Тъй като съдържанието се публикува дословно като PR коментар, моля потвърдете, че генераторът екранира/санитизира стойности от coverage JSON, за да не може име на workspace/път да инжектира Markdown/HTML в коментара.

Comment thread package.json
},
"devDependencies": {
"@cloudflare/workers-types": "^4.20260521.1",
"@vitest/coverage-v8": "^4.1.7",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vitest/coverage-v8 е фиксиран на ^4.1.7. Този пакет трябва да съвпада по major версия с vitest — ако vitest не е 4.x, --coverage ще се провали в CI. Моля потвърдете съответствието. Дребна бележка: записът е вмъкнат преди @types/node, което нарушава азбучния ред на devDependencies (риск при sort-lint/prettier-plugin-packagejson).

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.
… 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.
…ersons)

midt-bg#226 (свързани лица — the conflict-of-interest data foundation) landed on main,
adding a large new surface: packages/db related-persons queries, the /conflicts
routes + ConflictCards, the apps/web conflicts lib and rate limiter, shared
company-name-key, and the scripts/cacbg pipeline.

The merge itself was clean — no conflicts; both overlapping test files
(identity.test.ts, search.test.ts) auto-merged.

The coverage ratchet did fail, though: midt-bg#226 ships its own tests but to a lower
bar than this branch's floors, dropping apps/web to 98.86% lines / 93.01%
branches (floors 99.4/96) and packages/db branches to 97.77% (floor 98.3). Per
the never-lower rule this is fixed with tests — NO baseline floor is touched.
Every case below targets a genuinely reachable branch; nothing is faked and no
source is edited to suit a test.

- ui.test.tsx (new): the shared editorial primitives had no direct test, only
  incidental page coverage of whichever variant that page used. Covers every
  optional-prop branch — Chip tones, ЕИК URL-encoding + rel=noopener, the
  OwnershipChip null/three-kind split, Flag variants, ShareBar clamping outside
  0–1, Callout h3-default vs explicit h2 (heading order) and its variant class,
  Section with and without a hint.
- conflicts.render.test.tsx: an undated + unnumbered + authority-less contract
  (no timeline, „—" body, generic „договор" label, never „№ null"), a
  sub-threshold „под 0,1%" share with no plotted bar, a no-denominator „—"
  share, an empty drill-down, and an all-outside-the-window contract set.
- conflicts.test.ts: unknown temporal tag falls back to the raw value, year-0 /
  non-numeric signing dates are refused by parseYear, and authority-share
  ordering with unknown denominators (plottable first, money breaks a tie).
- conflict.pages.render.test.tsx: meta() on both conflict pages with the loader
  data absent — the error-boundary render must degrade to the generic noun, not
  interpolate "undefined" into a public page title.
- conflicts.loaders.test.ts: route params absent entirely, not merely blank.
- conflicts-rate-limit.test.ts: non-GET/HEAD methods bypass the budget (it
  guards read scraping, and a write is not that vector).
- related-persons.test.ts: a NULL joined authority maps to '' so the card never
  renders a literal null, plus isMissingConflictTableError against a non-Error
  rejection and against a core (non-0003) missing table.

Gate green with no floor lowered: web 99.16/95.84, db 99.82/97.96, etl
100/97.46, ingest 100/98.36, config 100/100, shared 98.87/98.33.
Full suite: config 27, shared 65, ingest 119, etl 40, db 505, web 592.
…acbg gate midt-bg#281)

Two commits landed on main while the midt-bg#226 sync was in flight — an undici
override bump to ^7.29.0 and a cacbg completeness-gate fix (phantom rows /
--limit). Clean merge; the surface is pnpm-workspace.yaml + scripts/cacbg,
none of it inside the six measured workspaces, so the coverage baseline is
untouched and the gate stays green.

Verified against the merged tree: scripts tests 51 pass, cacbg pipeline tests
84 pass (run via the register-ts loader, as the CI job does), coverage ratchet
green for all six workspaces.
@lyubomir-bozhinov

Copy link
Copy Markdown
Collaborator Author

@ydimitrof @todorkolev PR-ът е синхронизиран с текущия main и CI е зелен — готов за повторен преглед.

Състояние: merge-нат е целият main до момента — #216 (coverage harness), #263 (FX в cron), #252 (Булстат ЕИК), #210 (сходни договори), #226 (свързани лица), #281/#282. Клонът е на 0 зад upstream. И двата workflow-а минават: CI ✅ и Script unit tests ✅ (новите cacbg + test job-ове от #226).

Какво се промени след последния преглед на @ydimitrof:

  1. Отстранени бележки (всяка потвърдена с мутационен тест): bind-аргументи/LIMIT в listSingleOfferContracts; ScrollToTop rAF възстановяване; детерминизъм (retry.test.ts под fake timers, eop „defaults today“ с пинат часовник). Възстановена е и отслабената инвариантна проверка в fakeDbFromFreshness — планировчикът чете data_freshness, не raw staging.

  2. Merge reconciliation. feat(etl): FX курсове в cron refresh-а преди derive (#158) #263 пренаписа apps/etl/src/index.ts като истински Workflow; взех upstream-ския интеграционен тест, а изместеното orchestration покритие е възстановено в отделен index.control-flow.test.ts. Уловен source drift извън конфликтите: search.suggest вече обвива DB в getDb().

  3. feat: свързани лица — детерминистична основа за данни за конфликт на интереси #226 свързани лица — покритието падна и е върнато с тестове, без сваляне на праг. Новата площ (ConflictCards, /conflicts маршрути, conflicts.ts, ui.tsx, related-persons.ts) свали web до 98.86%/93.01% и db branches до 97.77%. Добавени 29 случая в 7 файла — нов ui.test.tsx (споделените примитиви нямаха собствен тест), разредени договори (без дата/номер/възложител — гарантира, че „№ null“ не изтича в страницата), режимите на дяловете, meta() fallback при error-boundary render (пази „undefined“ в публично заглавие), обхватът на rate limiter-а (само GET/HEAD), и NULL възложител → ''.

Единственото сваляне на праг в целия PR: apps/etl branches 98.5 → 97 (постигнати 97.46). etl LINES остават 100. Причина: два доказуемо недостижими defensive клона — eop.ts m[1] ?? '' (изискван само от noUncheckedIndexedAccess) и integrity.ts summary.message ?? '…' (мъртъв — summarizeIntegrity гарантира non-null на throw пътя). 97 е далеч над оригиналния floor на harness-а (58.2). Това е мястото за най-строгия поглед.

Финално: gate зелен за всичките 6 workspace-а, 1348 workspace теста + 51 scripts + 84 cacbg, typecheck 7/7.

Merge редът вече е без значение — #216 е merge-нат, PR-ът е върху него.

@nedda76

nedda76 commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

Този клон е в конфликт с main, тъй че към момента не може да се ревюира — дифът, който GitHub показва, вече не отговаря на това, което би влязло. Ще го пребазираш ли върху актуалния main (или merge на main в клона) и да разрешиш конфликтите? След това веднага го поглеждам. Благодаря! 🙏

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.

3 participants