infra: local contour — pre-push gate:small by default, sandbox bootstrap, test:chunk

- .githooks/pre-push + scripts/pre-push-gate.mjs --hook: gate:small runs by
  default for issue/* branches with an executable diff; C/D-only diffs and
  non-issue refs are skipped, HP_PREPUSH_GATE=0 turns it off, =1 forces it.
  A non-HEAD push with an executable diff is rejected (the gate checks the
  working tree). The gate runs with every GIT_* variable removed: git gives
  hooks GIT_DIR (and GIT_CONFIG_PARAMETERS with the pusher's -c), and unit
  tests that `git init` temporary repositories otherwise write into the real
  one — seen on the first live run. The manual #343 mode is unchanged.
- scripts/sandbox-bootstrap.sh: idempotent worktree / deps / chromium (npm
  @sparticuz/chromium@152.0.0 + Playwright shims) / bundle / check steps,
  no owner paths or credentials.
- scripts/test-chunk.mjs, npm run test:chunk -- N/M: round-robin over
  test/*.test.mjs sorted by code point.
- Tests, seven mutants, docs/DEVELOPMENT.md «Локальный контур за 5 минут»,
  docs/TESTING.md. package.json changed → bundle rebuilt (fingerprint input).

Issue: #633
User-Visible: no
This commit is contained in:
Claude
2026-09-24 05:00:45 +03:00
parent 4de5c0c0b5
commit af62bd4c64
56 changed files with 1452 additions and 519 deletions
+51
View File
@@ -39,6 +39,57 @@ the degradation is accepted.
- GitHub pushes: classic PAT (repo+workflow scopes), created via the user's Chrome;
stored in `~/.git-credentials` for the session.
## Local contour in 5 minutes (локальный контур за 5 минут, #633)
Three commands take a fresh Linux sandbox (agent session, WSL, a clean VM) from
nothing to a green smoke, a full unit run in parts and a pre-push gate. Each
step fits the ≈3-minute limit of one sandbox command; everything is idempotent,
so after a timeout or a sandbox restart the same command is simply repeated.
```bash
# 1. Worktree + dependencies + Chromium + bundle + one Playwright page.
# HP_BRANCH picks the branch (taken from origin if it exists there);
# without it the worktree is a detached origin/dev.
HP_BRANCH=issue/NNN-slug HP_WORKTREE=/tmp/w-NNN bash scripts/sandbox-bootstrap.sh
# or step by step: worktree | deps | chromium | bundle | check
cd /tmp/w-NNN && node demo/smoke_edge_cases.mjs # AC2 of #633: green
# 2. The full unit suite in parts that each fit one command.
npm run test:chunk -- 1/6 # builds test-build/, then the first sixth
npm run test:chunk -- 2/6 --no-build
npm run test:chunk -- 3/6 --list # only print the files of the part
# 3. Push: the pre-push hook runs npm run gate:small for issue/* branches.
git push origin issue/NNN-slug
HP_PREPUSH_GATE=0 git push origin issue/NNN-slug # explicit opt-out
```
What each command guarantees:
- **`scripts/sandbox-bootstrap.sh`** — the worktree comes from the clone the
script lives in (`HP_CLONE` overrides), `npm ci --ignore-scripts` runs only
when `package-lock.json` changed (`HP_SHARED_NODE_MODULES` links a ready
`node_modules` instead), Chromium comes from the npm package
`@sparticuz/chromium@152.0.0` (the Playwright CDN is closed in the sandbox,
the npm registry is not) and gets a shim at every path Playwright expects —
a real browser already there is left alone. `bundle` is `npm run
bundle:sync`; `check` opens one page in Playwright. The script carries no
owner paths and no credentials: pushing is configured separately. Golden
frames are still captured only in Linux CI (#455).
- **`npm run test:chunk -- N/M`** — `test/*.test.mjs` sorted by code point,
file *i* goes to part *i* mod M + 1 (round-robin). Chosen over size-balanced
parts so that a file stays in the same part while tests are edited; the M
parts together cover every file exactly once (`test/test-chunk.test.mjs`).
The test build runs in every part unless `--no-build`, so a part is
self-contained after a sandbox restart.
- **pre-push** — see [TESTING.md «Локальный набор перед пушем»](TESTING.md#локальный-набор-перед-пушем-343):
on by default for `issue/*` branches with an executable diff, skipped when the
branch diff against `origin/dev` is class C/D only (review documents,
changelogs, bundle), off with `HP_PREPUSH_GATE=0`, forced for any branch with
`HP_PREPUSH_GATE=1`. `gate:small` takes minutes; when a push must fit a
3-minute command, run `npm run gate:small` separately and push with
`HP_PREPUSH_GATE=0`.
## Local Windows workstation
The CI contract is **Node.js 22 + Python 3.14**. Do not use Codex's bundled
+23 -8
View File
@@ -1203,16 +1203,31 @@ node scripts/pre-push-gate.mjs --max-smokes=3 --max-mutants=1
прогоняется; **связь не доказана** — отдельная громкая строка, потому что это не
«проверять нечего»: молчание стоило #234 бета-блокирующего регресса.
### Как включить в хук
### В хуке — по умолчанию для веток задач (#633)
Набор намеренно не включён в `.githooks/pre-push` по умолчанию: 20–45 секунд на
каждый пуш, включая пуши одной строки документации, — цена, которую стоит платить
осознанно. Включается переменной окружения:
Прежде набор включался только переменной `HP_PREPUSH_GATE=1`, и ошибки, которые
ловит `gate:small`, находил Validate с конвейером через полчаса после пуша. Теперь
`.githooks/pre-push` сам вызывает `node scripts/pre-push-gate.mjs --hook`, и тот
для каждой пушимой ветки решает:
```bash
export HP_PREPUSH_GATE=1 # в профиль оболочки
git push # хук прогонит набор перед процессным гейтом
```
| Случай | Что делает хук |
|---|---|
| ветка `issue/*`, в диффе от merge-base с `origin/dev` есть хоть один файл не класса C/D | `npm run gate:small` (`scripts/gate-small.mjs --base=<merge-base>`); красный — push отклонён |
| дифф ветки — только класс C/D (документы ревью, changelog, бандл) или пуст | набор не гонится, причина печатается |
| ветка не `issue/*`, тег, удаление | набор не гонится |
| исполняемый дифф, но пушится не `HEAD` | push отклонён: набор проверяет рабочее дерево, а оно не совпадает с пушимым коммитом |
| `HP_PREPUSH_GATE=0` | выключен явно |
| `HP_PREPUSH_GATE=1` | гонится для любой ветки и любого диффа |
Процессный гейт идёт первым — он отвечает за секунды; набор — после, и
прогоняются оба, чтобы все провалы были видны одним кругом. `gate:small` идёт
минуты: в песочнице агента, где команда живёт ≈ 3 минуты, набор гоняют отдельной
командой, а пушат с `HP_PREPUSH_GATE=0` — и пишут об этом в хендоффе.
Ручной режим выше (`node scripts/pre-push-gate.mjs` без `--hook`) остался как был:
tsc, юниты, смоки и мутанты по диффу. Решение хука покрыто
`test/pre-push-gate.test.mjs`, включая настоящий `.githooks/pre-push` на
временном репозитории.
Обойти, как и процессный гейт, можно через `git push --no-verify` — и тогда то же
самое найдёт Validate, уже после того как код окажется в `dev`.