From cf1c90fb5bfa443bbdd2f0bfeacf5dec2ba4c198 Mon Sep 17 00:00:00 2001 From: Matysh Date: Mon, 5 Oct 2026 08:33:38 +0300 Subject: [PATCH] chore(process): align ship review guidance and guard inventory counts (#767) Clarify nightly review and beta coverage without changing gates or schedules. Cover the real night/beta publication summaries and reject stale category counts, totals and membership in the browser-guard documentation. Issue: #767 User-Visible: no --- .github/workflows/_process.yml | 4 +- .github/workflows/_ship-review.yml | 6 +- PROCESS.md | 21 ++-- docs/process/AUTHOR.md | 7 +- docs/process/REVIEWER.md | 10 +- docs/testing-notes/mutation-browser-guards.md | 5 + scripts/reviews-index.mjs | 5 +- scripts/ship-review.mjs | 8 +- scripts/wait-verdict.mjs | 2 +- test/mutation-gate.test.mjs | 108 +++++++++++++++++- test/process-digests.test.mjs | 3 +- test/ship-review.test.mjs | 18 ++- test/wait-verdict.test.mjs | 2 +- 13 files changed, 170 insertions(+), 29 deletions(-) diff --git a/.github/workflows/_process.yml b/.github/workflows/_process.yml index fb402e5a..e3a04614 100644 --- a/.github/workflows/_process.yml +++ b/.github/workflows/_process.yml @@ -1029,8 +1029,8 @@ jobs: contents: read issues: write needs: [guard, prepare] - # #696: `ship` в рамках сливается без модели — пакетное ревью диапазона - # перед бетой (PROCESS.md §11.7, ship-review.yml). + # #696/#727: `ship` в рамках сливается без модели — пакетное ревью ночью; + # перед бетой проверка покрытия и непрочитанная дельта (PROCESS.md §11.7). if: needs.prepare.outputs.proceed == 'true' && needs.prepare.outputs.reuse != 'true' && needs.prepare.outputs.ship != 'true' runs-on: ubuntu-24.04 concurrency: diff --git a/.github/workflows/_ship-review.yml b/.github/workflows/_ship-review.yml index 7369663a..caf636b2 100644 --- a/.github/workflows/_ship-review.yml +++ b/.github/workflows/_ship-review.yml @@ -375,7 +375,11 @@ jobs: commit -q -F "$msg" git diff --name-only "origin/dev...HEAD" | node scripts/review-doc-guard.mjs if git push -q "https://x-access-token:$TOKEN@github.com/${{ github.repository }}" HEAD:dev 2> "$push_err"; then - echo "### Пакетное ревью ship $TAG" >> "$GITHUB_STEP_SUMMARY" + if [ "$MODE" = "nightly" ]; then + echo "### Ночное пакетное ревью ship" >> "$GITHUB_STEP_SUMMARY" + else + echo "### Предрелизное пакетное ревью ship $TAG" >> "$GITHUB_STEP_SUMMARY" + fi echo "Задачи $ISSUES · High $high · Medium $medium · Low $low — \`$DOC\` в dev." >> "$GITHUB_STEP_SUMMARY" echo "::notice::$DOC опубликован: High $high · Medium $medium · Low $low" exit 0 diff --git a/PROCESS.md b/PROCESS.md index 3929d890..39d736bf 100644 --- a/PROCESS.md +++ b/PROCESS.md @@ -608,14 +608,15 @@ patch-id кандидата слияния: вердикт к работе за | ТЗ | строка «что меняется и чем проверить» в теле issue под `## ТЗ` | «Оценка» (§7.2) и до трёх AC в теле issue под `## ТЗ` | полное ТЗ по §7.1 | | Ревью ТЗ | нет | нет | да, лимит 4 цикла | | Локальный гейт | `npm run gate:small` | `npm run gate:small` плюс смоуки `smoke-select` и AC | §8 | -| Код-ревью | до слияния нет; пакетное ревью диапазона перед бетой | модель: корректность и AC | документ ревью, как в §2.7 | +| Код-ревью | до слияния нет; пакетное ревью ночью, перед бетой — проверка покрытия и непрочитанная дельта (§11.7) | модель: корректность и AC | документ ревью, как в §2.7 | | Лимит циклов код-ревью | — | 2 | 4 | **Рамки `track:ship`** механические: дифф `src/**` не больше 30 строк, без новых файлов в `src/**`, без ключей i18n, без полей конфига и без Python. Выход за рамки переводит задачу в `track:show`. Рамки и слияние `ship` без ревью модели проверяет и исполняет конвейер (§10.4, #696); код `ship` читает пакетное ревью -диапазона перед бетой (§11.7). +ночью заранее; перед бетой проверяется покрытие и читается непрочитанная дельта +(§11.7). **Проверка риска по изменённым участкам** (#707). Рамки считают строки и файлы, но не видят, что именно тронуто: обработчик `pointerdown` в двенадцать строк @@ -675,7 +676,8 @@ patch-id кандидата слияния: вердикт к работе за `ux-contract`, `perf-touch`, `undocumented`. Шаг решения по вердикту ставит `track:ask`, снимает `track:show` и переводит задачу в `S3-spec` комментарием: критерий словами, документы код-ревью, бюджет. Код остаётся в ветке, полное ТЗ -пишется в теле issue, код класса A не пушится до `S5`; бюджет код-ревью не +пишется в теле issue; до `S5` запрещён push всей ветки, включая тесты и документы +(§11.8), а не только кода класса A; бюджет код-ревью не обнуляется (§4). Подтверждённый владельцем `show` конвейер не повышает: задача получает `blocked` и вопрос владельцу с вариантом по умолчанию «повысить до `ask`». `reclassify` без критерия из списка, на `ask` или на ревью ТЗ — @@ -1411,7 +1413,7 @@ limit`) и своей логики трека не держит; ответ comp |---|---|---|---| | Validate на материале | лёгкий; годится завершённый push-прогон на этом SHA | лёгкий, как у `ship` | лёгкий, как у `ship` (#709) | | Ребейз до ревью | нет, если `git merge-tree` с `dev` чистый | нет, если чистый | да (#257) | -| Ревью модели | нет — пакетное ревью перед бетой (§11.7) | корректность и AC; окружение — по нужде | полное | +| Ревью модели | нет — пакетное ревью ночью; перед бетой проверка покрытия и непрочитанная дельта (§11.7) | корректность и AC; окружение — по нужде | полное | | Проверка кандидата слияния | ждёт push-прогон кандидата; dispatch — только если его нет | как у `ship` | как у `ship` (#709) | Мутантов по диффу нет ни на одном треке (#709, §2.7). Рамки `ship` @@ -1715,7 +1717,8 @@ Golden, браузерные смоки, performance и полный HA-харн Это исключение из правила «каждое изменение проходит код-ревью» (§5, §7.1). Второе такое место — трек `ship` (§5): его ревью не отменяется, а переносится -на пакетное ревью диапазона перед бетой. Здесь же исключение относится только к +на пакетное ревью ночью и проверку покрытия с чтением непрочитанной дельты перед +бетой (§11.7). Здесь же исключение относится только к окну между `S8-merged` и выпуском. ### 11.5 Независимое ревью линии перед стабильным релизом @@ -1784,7 +1787,9 @@ action ревью пускает ровно этого бота (`allowed_bots`, выпуск после такого отказа, исправляют причину и получают новый совместимый полный зелёный прогон на том же финальном SHA либо на новом SHA кандидата. -### 11.7 Пакетное ревью `ship` перед бетой + + +### 11.7 Пакетное ревью `ship`: ночью и перед бетой Решение владельца 2026-09-28, issue #696. @@ -1793,7 +1798,9 @@ action ревью пускает ровно этого бота (`allowed_bots`, до того, как она уйдёт пользователям беты. Одна сессия на все ship-задачи диапазона дешевле ревью на каждую, а рамки ship держат объём малым. -**Шаг.** Перед публикацией беты — `ship-review.yml` +**Шаг.** Ночью код читается заранее (режим ниже). Перед публикацией беты +проверяется покрытие; непрочитанные или изменённые после ревью задачи читает +`ship-review.yml` (`gh workflow run ship-review.yml --ref dev -f tag=vX.Y.Z-beta.N` или кнопка «Run workflow» в Actions). Это тонкий вызывающий файл, он лежит и в `main`: без этого GitHub запуск не даёт. Тело `_ship-review.yml` читается из `dev` при diff --git a/docs/process/AUTHOR.md b/docs/process/AUTHOR.md index c48a1316..cc04d0c6 100644 --- a/docs/process/AUTHOR.md +++ b/docs/process/AUTHOR.md @@ -64,8 +64,9 @@ ([§5](../../PROCESS.md#5-треки-ship-show-ask--метка-владельца)). - Вердикт код-ревью `show` с `route: reclassify` конвейер исполняет сам: `track:ask`, `S3-spec` и комментарий с критерием §5 (#726). Дальше — полное ТЗ - по §7.1 в теле issue под `## ТЗ` и ревью ТЗ; код остаётся в ветке, код класса A - не пушить до `S5`. Бюджет код-ревью не обнуляется, лимит — 4. Подтверждённый + по §7.1 в теле issue под `## ТЗ` и ревью ТЗ; код остаётся в ветке, до `S5` + запрещён push всей ветки, включая тесты и документы. Бюджет код-ревью не + обнуляется, лимит — 4. Подтверждённый владельцем `show` получает `blocked` и вопрос владельцу ([§5](../../PROCESS.md#5-треки-ship-show-ask--метка-владельца)). - `track:show`: `S2-analysis` → `S5-ready`, до трёх AC автор пишет в теле @@ -248,7 +249,7 @@ переводит в `track:show`. Код `ship` читает пакетное ревью: ночью (`SHIP-REVIEW-<база>-dev-.md`) и перед бетой — то, что ночь не прочла (`SHIP-REVIEW-<тег>.md`) ([§10.4](../../PROCESS.md#104-событийный-конвейер-метка-как-триггер), - [§11.7](../../PROCESS.md#117-пакетное-ревью-ship-перед-бетой)). + [§11.7](../../PROCESS.md#117-пакетное-ревью-ship-ночью-и-перед-бетой)). - Автор обязан дождаться вердикта, а не заканчивать сессию: `node scripts/wait-verdict.mjs --issue NN`, смотреть на метку, а не на комментарий; при `blocked` не ждать. После прогона ревью метка меняется diff --git a/docs/process/REVIEWER.md b/docs/process/REVIEWER.md index be82e770..f611232f 100644 --- a/docs/process/REVIEWER.md +++ b/docs/process/REVIEWER.md @@ -182,21 +182,21 @@ - Задачи `track:ship` слиты без ревью модели; их код одной сессией читает `ship-review.yml` — ночью (`SHIP-REVIEW-<база>-dev-.md`) и перед бетой то, что ночь не прочла (`SHIP-REVIEW-<тег>.md`): по строке ТЗ каждой задачи и - её коммитам ([§11.7](../../PROCESS.md#117-пакетное-ревью-ship-перед-бетой)). + её коммитам ([§11.7](../../PROCESS.md#117-пакетное-ревью-ship-ночью-и-перед-бетой)). - Вопросы к задаче: делает ли код заявленное и только его, не ломает ли соседнее, не вышла ли правка из ship по смыслу - ([§11.7](../../PROCESS.md#117-пакетное-ревью-ship-перед-бетой), + ([§11.7](../../PROCESS.md#117-пакетное-ревью-ship-ночью-и-перед-бетой), [§5](../../PROCESS.md#5-треки-ship-show-ask--метка-владельца)). - Строка «Риск по участкам» под задачей — рискованные участки ship, подтверждённого владельцем: конвейер их не повышал, пакетное ревью читает их - первыми ([§11.7](../../PROCESS.md#117-пакетное-ревью-ship-перед-бетой)). + первыми ([§11.7](../../PROCESS.md#117-пакетное-ревью-ship-ночью-и-перед-бетой)). - Документ `docs/reviews/SHIP-REVIEW-<тег>.md` публикует детерминированный шаг; High не пускает бету, Medium и Low решает владелец - ([§11.7](../../PROCESS.md#117-пакетное-ревью-ship-перед-бетой)). + ([§11.7](../../PROCESS.md#117-пакетное-ревью-ship-ночью-и-перед-бетой)). - Ночной документ `docs/reviews/SHIP-REVIEW-<база>-dev-.md` читает только непокрытые задачи — без документа или изменившиеся после ревью; бета читает только то, что ночь не прочла: гейт беты принимает ночной документ при том же - патч-наборе ([§11.7](../../PROCESS.md#117-пакетное-ревью-ship-перед-бетой)). + патч-наборе ([§11.7](../../PROCESS.md#117-пакетное-ревью-ship-ночью-и-перед-бетой)). ## Независимое ревью линии diff --git a/docs/testing-notes/mutation-browser-guards.md b/docs/testing-notes/mutation-browser-guards.md index 79454956..c5e750c7 100644 --- a/docs/testing-notes/mutation-browser-guards.md +++ b/docs/testing-notes/mutation-browser-guards.md @@ -6,6 +6,11 @@ The guideline is `200` (#699: a guideline, not a wall); `mutation-gate --check`, must be added deliberately under one reason below, and its mutant `because` must explain the concrete browser-only invariant. +`test/mutation-gate.test.mjs` checks each category count against its listed IDs, +the total against the browser guards in `MUTANTS`, and both guideline numbers +against `BROWSER_GUARD_LIMIT` (#767). Category names must agree between the table +and inventory; each current browser guard ID must occur exactly once. + Converted witnesses are not listed here: their registry guard names an explicit `node --test` suite. The original #659 conversions also use `test/mutation-browser-offload.test.mjs`; new behavioral witnesses execute the consumer directly where possible. Catching the mutant diff --git a/scripts/reviews-index.mjs b/scripts/reviews-index.mjs index 3bb46fec..81665392 100644 --- a/scripts/reviews-index.mjs +++ b/scripts/reviews-index.mjs @@ -28,8 +28,9 @@ import { isMainModule } from './spawn-portable.mjs'; export const INDEX_FILE = 'INDEX.md'; const DOC_NAME = /^(CODE|SPEC)-REVIEW-(?:issue-)?(\d+)(?:-r(\d+))?(?:-([a-z0-9-]+))?\.md$/i; const RELEASE_DOC_NAME = /^RELEASE-REVIEW-(v(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*))\.md$/i; -// #696: пакетное ревью ship-задач перед бетой (PROCESS.md §11.7) — по тегу беты. -// #727: ночной документ `SHIP-REVIEW-<база>-dev-.md` — тег здесь база диапазона. +// #696/#727: пакетное ревью ship — ночью заранее; перед бетой проверка покрытия +// и ревью непрочитанной дельты (PROCESS.md §11.7). Документ дельты — по тегу беты; +// ночной `SHIP-REVIEW-<база>-dev-.md` — по базе диапазона и SHA головы dev. const SHIP_DOC_NAME = /^SHIP-REVIEW-(v(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-beta\.(?:0|[1-9]\d*))?)(-dev-[0-9a-f]{12})?\.md$/i; const COLOUR = { 'зелёный': 'зелёный', 'зеленый': 'зелёный', green: 'зелёный', diff --git a/scripts/ship-review.mjs b/scripts/ship-review.mjs index 0c8d22d1..9770babd 100644 --- a/scripts/ship-review.mjs +++ b/scripts/ship-review.mjs @@ -1,6 +1,7 @@ #!/usr/bin/env node /** - * Пакетное ревью задач `track:ship` перед бетой (#696, PROCESS.md §11.7). + * Пакетное ревью `track:ship`: ночью и непрочитанная дельта перед бетой + * (#696/#727, PROCESS.md §11.7). * * node scripts/ship-review.mjs doc --tag=v1.79.0-beta.1 * node scripts/ship-review.mjs doc --tag=nightly --candidate= @@ -11,8 +12,9 @@ * * `ship` сливается без ревью модели (§5): правка в механических рамках и * зелёный лёгкий Validate. Прочитать её код обязан кто-то до того, как она - * уйдёт пользователям, — это пакетное ревью всех ship-задач диапазона - * «прошлый тег..кандидат беты» одной сессией модели. + * уйдёт пользователям: ночью — непокрытые ship-задачи диапазона + * «прошлый тег..голова dev», перед бетой — проверка покрытия и ревью + * непрочитанных либо изменённых задач «прошлый тег..кандидат беты». * * Какие задачи — ship, доказывает конвейер, а не метка: слияние без модели * оставляет в issue машинный маркер ``. diff --git a/scripts/wait-verdict.mjs b/scripts/wait-verdict.mjs index 73f1cb37..30c54d75 100644 --- a/scripts/wait-verdict.mjs +++ b/scripts/wait-verdict.mjs @@ -39,7 +39,7 @@ export const PIPELINE_EVENTS = [ { re: /^Лимит циклов ревью исчерпан/m, kind: 'exhausted', text: 'конвейер: лимит циклов исчерпан — решение владельца' }, // #726: маршрут вердикта show. После исчерпания: тот же комментарий может нести // и маршрут, но решение тогда за владельцем — первым находится `exhausted`. - { re: /^\*\*Ревью show: решать есть что — трек повышен до `track:ask`\.\*\*/m, kind: 'reclassify', text: 'конвейер: трек повышен до ask: полное ТЗ в теле issue, код класса A не пушить до S5' }, + { re: /^\*\*Ревью show: решать есть что — трек повышен до `track:ask`\.\*\*/m, kind: 'reclassify', text: 'конвейер: трек повышен до ask: полное ТЗ в теле issue, всю ветку (включая тесты и документы) не пушить до S5' }, { re: /^\*\*Ревью show: решать есть что — вопрос владельцу\.\*\*/m, kind: 'owner-question', text: 'конвейер: вопрос владельцу о треке show — ждёт владельца' }, { re: /^Конвейер ревью не запущен:/m, kind: 'refused', text: 'конвейер отказал (blocked/review-4) — читать комментарий' }, ]; diff --git a/test/mutation-gate.test.mjs b/test/mutation-gate.test.mjs index 184fc2b8..371df305 100644 --- a/test/mutation-gate.test.mjs +++ b/test/mutation-gate.test.mjs @@ -206,17 +206,121 @@ test('#332: каждый гвард реестра классифицирует assert.ok(rest >= 150, `небраузерных гвардов подозрительно мало: ${rest}`); }); -test('#659/#699: browser guard inventory is reviewed and exact; the limit is a guideline', () => { +// The registry decides which IDs need a browser; the document decides their +// reviewed categories. Parse both representations before comparing their counts +// so a Set cannot hide duplicate IDs or an overwritten category row (#767). +function assertBrowserGuardInventory(markdown, mutants, guideline) { + const lines = markdown.split(/\r?\n/); + const tableStart = lines.findIndex((line) => /^\| Category \| Count \|/.test(line)); + assert.ok(tableStart >= 0, 'browser inventory category table is missing'); + const counts = new Map(); + let total; + let documentedGuideline; + for (const line of lines.slice(tableStart + 2)) { + if (!line.startsWith('|')) break; + const [, categoryCell, countCell] = line.split('|'); + const category = categoryCell.replaceAll('**', '').trim(); + const count = countCell.replaceAll('**', '').trim(); + if (category === 'Total') { + assert.equal(total, undefined, 'duplicate browser inventory total'); + assert.match(count, /^\d+\s*\/\s*\d+$/, 'browser inventory total / guideline must be numeric'); + [total, documentedGuideline] = count.split('/').map(Number); + } else { + assert.ok(!counts.has(category), `duplicate category table row: ${category}`); + assert.match(count, /^\d+$/, `category count must be numeric: ${category}`); + counts.set(category, Number(count)); + } + } + + const inventoryStart = lines.indexOf('## Reviewed per-mutant inventory'); + assert.ok(inventoryStart >= 0, 'reviewed per-mutant inventory is missing'); + const categories = new Map(); + const documented = new Set(); + let currentCategory; + for (const line of lines.slice(inventoryStart + 1)) { + if (line.startsWith('## ')) break; + const heading = /^### (.+)$/.exec(line); + if (heading) { + currentCategory = heading[1].trim(); + assert.ok(!categories.has(currentCategory), `duplicate inventory category: ${currentCategory}`); + categories.set(currentCategory, []); + } + const item = /^- `([^`]+)`\s*$/.exec(line); + if (!item) continue; + assert.ok(currentCategory, `browser guard has no category: ${item[1]}`); + assert.ok(!documented.has(item[1]), `duplicate browser guard ID: ${item[1]}`); + documented.add(item[1]); + categories.get(currentCategory).push(item[1]); + } + assert.ok(categories.size > 0, 'browser inventory has no categories'); + assert.deepEqual([...counts.keys()].sort(), [...categories.keys()].sort(), + 'category table and reviewed inventory must name the same categories'); + const policy = browserGuardPolicy(mutants, documented); + assert.deepEqual(policy.missingReasons, [], 'browser registry IDs missing from inventory'); + assert.deepEqual(policy.staleReasons, [], 'inventory IDs absent from browser registry'); + for (const [category, ids] of categories) { + assert.equal(counts.get(category), ids.length, `category count differs from listed IDs: ${category}`); + } + assert.equal(total, policy.count, 'browser inventory total differs from registry'); + assert.equal(total, [...counts.values()].reduce((sum, count) => sum + count, 0), + 'browser inventory total differs from category sum'); + assert.equal(documentedGuideline, guideline, 'browser inventory guideline differs from policy'); + const proseGuideline = /The guideline is `(\d+)`/.exec(markdown); + assert.ok(proseGuideline, 'browser inventory prose guideline is missing'); + assert.equal(Number(proseGuideline[1]), guideline, 'browser inventory prose guideline differs from policy'); +} + +test('#659/#699/#767: browser guard inventory IDs, category counts and total match the registry', () => { const markdown = readFileSync(join(repoRoot, 'docs/testing-notes/mutation-browser-guards.md'), 'utf8'); + assertBrowserGuardInventory(markdown, MUTANTS, BROWSER_GUARD_LIMIT); const policy = browserGuardPolicy(MUTANTS, documentedBrowserGuards(markdown)); assert.ok(policy.count > 0); assert.deepEqual(policy.missingReasons, []); assert.deepEqual(policy.staleReasons, []); - assert.equal(BROWSER_GUARD_LIMIT, 200, 'ориентир остаётся числом в выводе --check'); const over = browserGuardPolicy([...MUTANTS, ...Array.from({ length: BROWSER_GUARD_LIMIT + 1 }, (_, i) => ({ id: `x-${i}`, guard: 'node demo/smoke_x.mjs' }))], new Set()); assert.equal(over.overLimit, true, 'сверх ориентира policy это видит — --check предупреждает'); }); +test('#767: browser inventory count contract rejects numeric and membership drift', () => { + const mutants = ['alpha', 'beta', 'gamma'].map((id) => ({ id, guard: 'node demo/smoke_x.mjs' })); + mutants.push({ id: 'unit-only', guard: 'node --test test/x.test.mjs' }); + const markdown = [ + 'The guideline is `2`.', + '| Category | Count | Why a browser is still required |', + '| --- | ---: | --- |', + '| Paint | 2 | Pixels |', + '| Lifecycle | 1 | Events |', + '| **Total** | **3 / 2** | Guideline only |', + '', + '## Reviewed per-mutant inventory', + '### Paint', + '- `alpha`', + '- `beta`', + '### Lifecycle', + '- `gamma`', + ].join('\n'); + assert.doesNotThrow(() => assertBrowserGuardInventory(markdown, mutants, 2)); + assert.doesNotThrow(() => assertBrowserGuardInventory(markdown.replaceAll('\n', '\r\n'), mutants, 2)); + const cases = [ + ['wrong category count', markdown.replace('| Paint | 2 |', '| Paint | 1 |'), /category count differs/], + ['wrong total', markdown.replace('**3 / 2**', '**4 / 2**'), /total differs from registry/], + ['wrong table guideline', markdown.replace('**3 / 2**', '**3 / 4**'), /guideline differs from policy/], + ['wrong prose guideline', markdown.replace('guideline is `2`', 'guideline is `4`'), /prose guideline differs/], + ['missing total', markdown.replace(/^\| \*\*Total\*\*.*\n/m, ''), /total differs from registry/], + ['duplicate total', markdown.replace('| **Total** |', '| **Total** | **3 / 2** | Duplicate |\n| **Total** |'), /duplicate browser inventory total/], + ['missing table category', markdown.replace(/^\| Paint.*\n/m, ''), /must name the same categories/], + ['missing inventory category', markdown.replace('### Lifecycle\n', ''), /must name the same categories/], + ['duplicate table category', markdown.replace('| Paint | 2 | Pixels |', '| Paint | 2 | Pixels |\n| Paint | 2 | Pixels |'), /duplicate category table row/], + ['duplicate inventory category', `${markdown}\n### Paint`, /duplicate inventory category/], + ['missing ID', markdown.replace('- `alpha`\n', ''), /registry IDs missing from inventory/], + ['duplicate ID', `${markdown}\n- \`alpha\``, /duplicate browser guard ID/], + ['non-browser ID', `${markdown}\n- \`unit-only\``, /inventory IDs absent from browser registry/], + ]; + for (const [name, changed, message] of cases) { + assert.throws(() => assertBrowserGuardInventory(changed, mutants, 2), message, name); + } +}); + test('#659: browser-only mutations reuse one clean bundle unless their patch is bundled', () => { const corpus = new Set(['src/card.ts', 'scripts/source-fingerprint.mjs']); const smoke = (file) => ({ guard: 'node demo/smoke_x.mjs', patches: [{ file }] }); diff --git a/test/process-digests.test.mjs b/test/process-digests.test.mjs index 4ab30ee4..c4b2703d 100644 --- a/test/process-digests.test.mjs +++ b/test/process-digests.test.mjs @@ -52,6 +52,7 @@ const KEY_RULES = { ['5-треки-ship-show-ask--метка-владельца', 'строкой `Трек: — решение владельца` в начале строки'], ['5-треки-ship-show-ask--метка-владельца', 'Метка без такой строки — предложение'], ['5-треки-ship-show-ask--метка-владельца', 'Агент эту строку не пишет никогда'], + ['5-треки-ship-show-ask--метка-владельца', 'до `S5` запрещён push всей ветки, включая тесты и документы'], ['71-цепочка', 'Владельцу задаются только продуктовые вопросы'], ['71-цепочка', 'issue остаётся в `S3-spec` и получает `blocked`'], ['26-в-разработке--реализация', 'каждый коммит с файлами классов A, B или D несёт трейлеры `Issue: #` и `User-Visible: yes|no`'], @@ -109,7 +110,7 @@ const KEY_RULES = { ['71-цепочка', 'Технический спор автора и ревьюера решается вердиктом, а не владельцем'], ['12-запрещено', 'Medium-находки, оставленные как TODO в документе ревью'], // #727: ночное пакетное ревью ship переиспользуется гейтом беты по патч-набору. - ['117-пакетное-ревью-ship-перед-бетой', 'гейт беты принимает ночной документ при том же патч-наборе'], + ['117-пакетное-ревью-ship-ночью-и-перед-бетой', 'гейт беты принимает ночной документ при том же патч-наборе'], // #729: готовый черновик не довод за непринятое ТЗ. ['24-тз-на-ревью', 'ревьюер ТЗ не читает и доводом не считает'], ], diff --git a/test/ship-review.test.mjs b/test/ship-review.test.mjs index 40235f18..bc8805fa 100644 --- a/test/ship-review.test.mjs +++ b/test/ship-review.test.mjs @@ -1,5 +1,5 @@ // #696, PROCESS.md §11.7: задачи track:ship сливаются без ревью модели; их код -// читает пакетное ревью диапазона перед бетой, и гейт беты требует документ. +// читает пакетное ревью ночью и по дельте перед бетой; гейт беты требует покрытие. import assert from 'node:assert/strict'; import test from 'node:test'; import { spawnSync } from 'node:child_process'; @@ -619,6 +619,8 @@ test('#727 AC2/AC5 _ship-review.yml на настоящем bash: ночь чи const bogus = { high: 0, medium: 1, low: 0, summary: 'ok', patches: '701:' + 'd'.repeat(40) }; const published = box.publish(first, bogus, { TAG: 'nightly' }); assert.equal(published.status, 0, published.stderr + published.stdout); + assert.match(published.summary, /^### Ночное пакетное ревью ship$/m); + assert.doesNotMatch(published.summary, /Предрелизное|Пакетное ревью ship nightly/); const text = box.git(box.work, 'show', `origin/dev:${nightDoc}`); const block = parseAnchorBlock(text); assert.equal(block.mode, 'nightly'); @@ -694,6 +696,20 @@ test('#727 AC2/AC5 _ship-review.yml на настоящем bash: ночь чи assert.match(foreign.stderr, /не предок origin\/dev/); }); +test('#767: публикация предрелизного ship-ревью называет режим и тег в сводке', (t) => { + if (!hasTools()) { t.skip('bash/jq/sha256sum недоступны'); return; } + const box = shipSandbox(t); + box.issue(701); + box.commit({ 'a.mjs': 'export const a = 1;\n' }, 'fix: a (#701)\n\nIssue: #701\nUser-Visible: no'); + box.push(); + const prepared = box.prepare({ TAG: 'v1.1.0-beta.1' }); + assert.equal(prepared.status, 0, prepared.stderr); + const published = box.publish(prepared, { high: 0, medium: 0, low: 0, summary: 'ok' }, { TAG: 'v1.1.0-beta.1' }); + assert.equal(published.status, 0, published.stderr + published.stdout); + assert.match(published.summary, /^### Предрелизное пакетное ревью ship v1\.1\.0-beta\.1$/m); + assert.doesNotMatch(published.summary, /Ночное|nightly/); +}); + test('#727 AC8 _ship-review.yml на настоящем bash: строка о High — только ночью и только при High > 0, без повтора', (t) => { if (!hasTools()) { t.skip('bash/jq/sha256sum недоступны'); return; } const box = shipSandbox(t); diff --git a/test/wait-verdict.test.mjs b/test/wait-verdict.test.mjs index 9e985edc..ed2c7570 100644 --- a/test/wait-verdict.test.mjs +++ b/test/wait-verdict.test.mjs @@ -135,7 +135,7 @@ test('#726 AC7: reclassify доставляется автору видом recl const comment = { id: 'r', createdAt: '2026-10-01T10:05:00Z', body }; const state = stateOf(snap(['S3-spec', 'track:ask'], [comment])); assert.equal(state.lastEvent.kind, 'reclassify'); - assert.match(state.lastEvent.text, /трек повышен до ask: полное ТЗ в теле issue, код класса A не пушить до S5/); + assert.equal(state.lastEvent.text, 'конвейер: трек повышен до ask: полное ТЗ в теле issue, всю ветку (включая тесты и документы) не пушить до S5'); // Метка сменилась тем же прогоном — вердикт (0) и текст маршрута в строках. const moved = decide(stateOf(snap(['S7-code-review', 'track:show'])), state); assert.equal(moved.code, 0);