test(infra): coverage-gate + acceptInvitation atomicity + turn-end unit (#324) #335
Reference in New Issue
Block a user
Delete Branch "fix/324-coverage-gate"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Хвост #244 (test/infra). Три пункта, все зелёные.
closes #324.1. Coverage-gate (основной)
На develop не было ни coverage-тулинга, ни порога. Добавил
@vitest/coverage-v8@4.1.6(пиннинг под используемый vitest) в три vitest-пакета —git-sync,editor-ext(+ его недостающий прямойvitest),apps/client— и включил v8-coverage с пер-пакетными порогами (root-vitest-конфига нет). v8, не istanbul: истанбул ломался на ESM-импорте@docmost/editor-ext; v8 берёт нативное runtime-покрытие и не переразбирает ESM.enabled: trueвшивает гейт в обычныйtest-скрипт →pnpm -r test(CI-entrypoint) его исполняет без ручного--coverage.Пороги на ~4–5 пунктов НИЖЕ измеренного — гейт проходит сегодня и падает на регрессе (проверено:
lines=95на editor-ext → exit 1):all: false(задокументировано в конфигах): покрытие считается по файлам, которых касаются тесты — ловит ослабление/удаление тестов у покрытых файлов, но не новый непокрытый файл. Осознанный компромисс: сall: trueпороги пришлось бы утопить (бочки/типы), гейт стал бы бессмысленным.2.
acceptInvitationатомарность (int-spec)apps/server/test/integration/workspace-accept-invitation-atomicity.int-spec.ts(+ сидеры вdb.ts). РеальныйWorkspaceInvitationServiceс реальными репозиториями против тестовой Kysely, заглушены только пост-коммитные коллабораторы. Ассертит инвариантusers_email_workspace_id_unique: (a) два КОНКУРЕНТНЫХ accept → ровно один успех, одинBadRequestException('Invitation already accepted'), membership==1, приглашение потреблено; (b) повторный accept → всё ещё одно членство; (c) выживший — в дефолтной группе воркспейса (вся транзакция, без полусостояний). Прогнан на реальном Postgres+Redis: 3/3.3.
decideTurnEndюнитСимвола
decideTurnEndв репо нет; логика завершения хода — вonFinishchat-thread.tsx. Добавил блок в существующийchat-thread.test.tsx: clean → flush queued (continue); abort/disconnect/error → queue сохранён (end) + нужный notice; parent нотифицируется на всех исходах. 8 passed.How verified
vitest runс гейтом: git-sync 712, editor-ext 247, client 888 — все exit 0.tsc --noEmit— чисто для client и server.pnpm install --frozen-lockfile— локфайл консистентен и аддитивен (проверено--lockfile-only --offline→ байт-идентично).Checklist
pnpm testсчитает покрытие; порог проваливает прогон при регрессе (DoD пункта 1)Заметки для ревью (non-blocking, из внутреннего ревью)
all: false— гейт не ловит регресс через НОВЫЙ непокрытый файл (задокументировано; кандидат на отдельный lint «новый .ts требует теста»).vitest— локфайл консистентен, риск низкий (md/tiptap-тесты нечувствительны к минору jsdom).🤖 Generated with Claude Code
Готово к ревью (review/needs). Все 3 пункта #324 зелёные: coverage-gate (v8, чтобы обойти istanbul-ESM-грабли; пер-пакетные пороги на ~4-5пт ниже измеренного, вшито в pnpm -r test, fails-on-regression подтверждён exit 1), acceptInvitation atomicity int-spec (прогнан на реальном Postgres, 3/3, гонка двух accept → одно членство через unique-constraint), decideTurnEnd (символа нет — покрыл onFinish-логику chat-thread, 8 passed). Локфайл аддитивен, --frozen-lockfile консистентен. Два non-blocking момента вынес в тело PR: all:false (задокументировано в конфигах) и транзитивный дрейф editor-ext (jsdom27, риск низкий). tsc чисто.
Ревью — #335 (test/infra: coverage-gate + acceptInvitation atomicity + turn-end unit, хвост #244/#324), round 1, head
baa41d66, base developf5d19f97Вердикт: PASS — чистый test/infra-PR, продакшн-код не тронут. Все три части сделаны добротно и не вакуозно, объективка зелёная (прогнал полностью сам, включая int-spec против реального Postgres). Готово к мержу.
Объективку запустил сам (head
baa41d66, main-клон + docker Postgres+pgvector):WorkspaceInvitationService+ реальныеUserRepo/GroupRepo/GroupUserRepoпротив реального Postgres; застаблены только пост-коммитные периферии — mail/session/billing/audit/env — на tx-пути ничего не заглушено). Логиduplicate key ... users_email_workspace_id_uniqueв выводе — это ОЖИДАЕМЫЙ путь проигравшего accept'а (сервис ловит →BadRequestException('Invitation already accepted')).Что проверено по существу (веер 7 аспектов — security/stability/test-coverage/conventions/simplification/regressions/coherence, все LGTM):
SELECTуходят до входа в любую tx (пул max:5), оба видят живое приглашение → проигравший ГАРАНТИРОВАННО входит в tx и ловит unique-conflict (никогда не «not found»), что и ассертит тест по точному message. «Оба успеха» невозможны из-заusers_email_workspace_id_unique, дедлока нет (одностороннее ожидание). Ассерты падают, если сломать атомарность/констрейнт/трансляцию ошибки.maxWorkers:1+ per-file изоляция реестра модулей;afterAll→destroyTestDb()закрывает пул; изоляция по свежемуrandomUUID()workspace + email на каждый сид, все запросы фильтруют по workspaceId → в общейdocmost_testкросс-тестовой утечки нет. Property-тесты (numRuns 100/300) дают побайтово одинаковое покрытие прогон-к-прогону (ветки насыщены) — «рандом + жёсткий branch-порог = флейк» эмпирически опровергнут (git-sync gate прогнан дважды — идентичные числа).test(enabled:true), CI гоняетpnpm -r test→ порог исполняется без флага. Все три vitest-пакета покрыты (server=jest, mcp=node --test — другой механизм, вне scope vitest-гейта).all:false— осознанный задокументированный компромисс (сall:trueбочки/типы/ESM-барели утопили бы пороги в бессмысленность); последствие (новый непокрытый файл проходит) реально, но это защитимый инженерный выбор для первого гейта, автор его назвал — ниже DROP-порога, не блокер.db.ts+56 — чисто ДОБАВЛЕНИЕ двух сидеров (сигнатуры существующих не тронуты → 15 других int-spec'ов получают тот же API);chat-thread.test.tsx— только новый describe +cleanupв импорте, существующий блокsend now (#198)не тронут, hoisted-state не течёт назад;@vitest/coverage-v8@4.1.6точно матчитvitest@4.1.6, резолюция per-importer. Секретов нет (test-DB URL — задокументированный local-dev дефолт, env-overridable, вне диффа).DROP: нет (веер не дал ни одной находки выше порога). Вне scope: ветка
invitation.groupIdsв acceptInvitation (сиды используютgroupIds:null) не покрыта — но это отдельная фича-ветка вне атомарностной цели #324, не гейт (all:false), намеренно не выношу как находку.