Files
houseplan-card/docs/reviews/CODE-REVIEW-514-r1.md
T
2026-09-09 21:18:51 +00:00

136 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# CODE-REVIEW-514-r1
- **Issue:** https://github.com/Matysh/houseplan-card/issues/514 — «E2E на реальном HA как гейт стабильного релиза»
- **ТЗ:** `docs/specs/514-e2e-stable-release-gate.md`, ревью ТЗ зелёное (`docs/reviews/SPEC-REVIEW-514-r1.md`, r1, High 0/Medium 0)
- **Материал ревью:** `395b0e38f4b3e34071e2bcf236440bf476b2c75a` (рабочая копия на нём, HEAD detached from `52124529`)
- **Заход:** r1 · блокирующих циклов израсходовано 0 из 4
- **Вердикт: зелёный**
## Скоуп ревью
Диапазон `origin/dev..HEAD` — 5 коммитов, ровно issue #514 (`fd4f1077` ТЗ, `e226ac56` документ ревью ТЗ, `9b9e306b`/`5e9fe662`/`395b0e38` реализация и две правки по живым прогонам). Файлы только класса B (`scripts/**`, `test/**`, `.github/workflows/release.yml`) и C (`docs/**`); класс A не задет — `src/**`, `custom_components/**` не тронуты. `User-Visible: no` на всех пяти коммитах, что верно: изменение не видно ни пользователю HACS, ни пользователю карточки — только владельцу в логе гейта.
Проверялись все 6 AC ТЗ, три заявленных мутанта, соответствие `houseplan-e2e/e2e.yml` контракту, который `scripts/e2e-gate.mjs` полагает существующим, и трейлеры/процесс-гейт.
## Как проверялось
Материал ревью — код на точном SHA `395b0e38`, без git fetch/pull/checkout на другой коммит.
1. Прочитан `scripts/e2e-gate.mjs` целиком (149 строк): `previousStable`, `isOurRun`, `classifyRun`, `e2eGate`, `realOps`, CLI-обвязка.
2. Прочитаны `test/e2e-gate.test.mjs` (12 тестов) и `test/release-workflow.test.mjs` (2 теста) — сопоставлены с AC1–AC3, AC5.
3. Прочитан диф `.github/workflows/release.yml` — положение нового шага, условие, токен.
4. Прочитаны три новых определения в `scripts/mutation-gate.mjs` (`release-ships-on-red-e2e`, `release-upgrades-stable-onto-itself`, `release-trusts-foreign-e2e-run`).
5. Прочитан диф `PROCESS.md`, `AGENTS.md`, `docs/DEVELOPMENT.md`, `docs/TESTING.md`, `docs/specs/README.md` — сверены с фактическим положением вставки (номера строк из ТЗ и хендоффа совпали).
6. **`houseplan-e2e/.github/workflows/e2e.yml` получен напрямую** (`gh api repos/Matysh/houseplan-e2e/contents/...`), не со слов хендоффа: подтверждены job `plan` (собирает матрицу, `journeys-dev` только при `schedule`), job `e2e` с именем `"${{ matrix.suite }} · HP ${{ matrix.ref }} · HA ${{ matrix.ha }}"`, `concurrency.cancel-in-progress: false`.
7. **Оба живых прогона, названных в хендоффе, проверены напрямую через `gh api`**, а не приняты на слово:
- `gh api repos/Matysh/houseplan-e2e/actions/runs/34393136097/jobs` → `Матрица прогона`, `journeys · HP v1.73.0 · HA stable`, `first-run · HP v1.73.0 · HA stable`, `upgrade · HP v1.72.0 · HA stable` (conclusion всех `success`) — подтверждает AC4 (3 сьюта, без `journeys-dev`) и обоснованность правки в `isOurRun` (job `upgrade` несёт тег **предыдущего** stable, не тег под тестом).
- `gh api repos/Matysh/houseplan-e2e/actions/runs/34392391382/jobs` → `upgrade · HP stable · HA stable` = `failure` — подтверждает заявленную причину появления `previousStable` (обновление v1.73.0 на себя же).
- `gh release list --repo Matysh/houseplan-card --json tagName,createdAt,isPrerelease` — подтверждает допущение `previousStable`, что `gh release list` отдаёт новые релизы первыми (реализация полагается на этот порядок без явной сортировки).
8. Прогнаны тесты и мутанты (список — в разделе «Гейты»).
9. `node scripts/process-gate.mjs --range origin/dev..HEAD` — «гейт пройден, предупреждений 0».
## Гейты — что прогнано и почему
| Гейт | Результат | Основание |
|---|---|---|
| `npx tsc --noEmit`, `npm test`, `npm run build` + сверка бандла | не перегонялись | Validate зелёный на этом точном SHA (`395b0e38`, https://github.com/Matysh/houseplan-card/actions/runs/34394268282) — сошлись на этом прогоне |
| `node --test test/e2e-gate.test.mjs` | **12/12 green**, прогнан лично | новый файл, доказывает AC1/AC2 |
| `node --test test/release-workflow.test.mjs` | **2/2 green**, прогнан лично | новый файл, доказывает AC1/AC3 |
| `node --test test/release-contract.test.mjs test/performance-workflow.test.mjs test/validate-workflow.test.mjs` | **34/34 green**, прогнан лично | регресс-проверка: новый шаг в `release.yml` не сломал соседние тесты по этому же файлу |
| `node scripts/mutation-gate.mjs --id=release-ships-on-red-e2e` | **поймано 1 из 1** | защитный AC1/AC5 — гейт не должен читать `failure` как `green` |
| `node scripts/mutation-gate.mjs --id=release-upgrades-stable-onto-itself` | **поймано 1 из 1** | защитный AC5 — `upgrade_from` не должен всегда быть `stable` |
| `node scripts/mutation-gate.mjs --id=release-trusts-foreign-e2e-run` | **поймано 1 из 1** | защитный AC2/AC5 — гейт не должен принимать чужой dispatch |
| `node scripts/smoke-select.mjs --base origin/dev --head HEAD` | «исполняемого frontend-диффа нет … выбирать нечего» | `src/**/*.ts` не тронут — смоки проверяют собранную карточку, её нет в этом диффе |
| `npm run golden:verify` | не прогонялся | diff не меняет рендер/визуал (нет `src/**`) |
| `node scripts/check-docs.mjs` | не прогонялся | diff не трогает `src/**` — условие «прогонять» не выполнено |
| `node scripts/model-invariants.mjs` | не прогонялся | геометрия/`layout`/толщина/`marker.space` не затронуты |
| `python -m pytest tests_backend -q` | не прогонялся | `custom_components/**/*.py` не тронут |
| `node scripts/process-gate.mjs --range origin/dev..HEAD` | **«гейт пройден, предупреждений 0»**, прогнан лично | трейлеры/имя ветки/докладка `small` |
Полный список смоков (`ls demo/smoke_*.mjs | wc -l`) не запрашивался — инструмент выбора уже прямо ответил «выбирать нечего», уточнять число бессмысленно.
## Защитные AC — таблица «чем краснеет» (§2.7)
| AC | Чем доказан | Чем краснеет |
|---|---|---|
| AC1 (красный/отсутствующий/отменённый/ошибка dispatch → гейт красный) | `test/e2e-gate.test.mjs` (7 тестов), `test/release-workflow.test.mjs` | мутант `release-ships-on-red-e2e` (снята проверка `conclusion === 'success'`) — поймано 1/1, прогнано лично |
| AC2 (опознание своего прогона, игнор чужого) | `test/e2e-gate.test.mjs` (4 теста), живой прогон 34393136097 (v1.72.0-гейт не принял бы run v1.73.0) | мутант `release-trusts-foreign-e2e-run` (`isOurRun` всегда `true`) — поймано 1/1, прогнано лично |
| AC3 (пре-релизы шаг не выполняют) | `test/release-workflow.test.mjs` (regex на `if:`) | чтением: единственная точка контроля — `if: ${{ !github.event.release.prerelease }}` в самом yml, тест сверяет буквальную строку; отдельного мутанта на YAML-условие в репозитории для соседних гейтов тоже нет (см. `Require full performance`) |
| AC4 (`journeys-dev` не бежит на dispatch) | коммит в `houseplan-e2e` + живой прогон 34393136097 (3 job, без `journeys-dev`) | чтением + живым прогоном: `job.plan` собирает матрицу и добавляет `journeys-dev` только `if (schedule)` — код прочитан напрямую через `gh api`, не со слов хендоффа; мутанта нет, но у `houseplan-e2e` нет собственного процесса код-ревью с мутантами (внешний репозиторий) |
| AC5 (все три мутанта пойманы) | штатный `mutation-gate.mjs --id=…` | прогнано лично, все три «поймано 1 из 1» |
| AC6 (документация, `User-Visible: no`) | чтением дифа PROCESS.md/AGENTS.md/DEVELOPMENT.md/TESTING.md/specs/README.md + трейлеры коммитов | не защитный AC (расположение/текст) — таблица не требуется по исключению §2.7 |
## Находки
Не найдено ни одной High- или Medium-находки.
**Low (снимаю без правки — не искажает ни один AC, не создаёт риска пропуска красного гейта):**
- **L1.** Если job `plan` в `e2e.yml` сама завершится с `failure` до того, как заведётся хотя бы одна job вида `· HP … ·` (например, транзиентная ошибка в тривиальном `node -e`-скрипте планирования матрицы), `classifyRun` вернёт `'unknown'`, а поскольку прогон уже `completed`, код `e2eGate` (строка `if (kind === 'foreign' || candidate.status === 'completed') foreign.add(...)`) навсегда добавит его в `foreign` и продолжит ждать. Результат — `missing` через `appearMs` вместо диагностичного «наш прогон упал на планировании матрицы». Инвариант «не публиковать ассеты на красном» не нарушается: `missing` — это тоже небезопасный (не-green) результат, шаг `release.yml` падает, `build` не выполняется. Единственная цена — менее понятное сообщение владельцу в этом маловероятном сценарии (сам скрипт планирования — детерминированный `node -e` без внешних вызовов). Не блокирует, вне рамок AC (AC1 требует «красный/отсутствующий/… → гейт красный с причиной», а не конкретный текст причины для этого под-случая).
- **L2 (унаследовано из ревью ТЗ, переносится без повторной проверки).** Таймаут `totalMs = 45 мин` взят из `merge-candidate.mjs`, а не выведен из `timeout-minutes: 40` самих job `e2e.yml` — даёт 5 минут запаса, чего достаточно (уже отмечено ревью ТЗ как L2, снято без правки).
## Проверено чтением и/или исполнением — по каждому файлу
- `scripts/e2e-gate.mjs` — прочитан целиком, логика прослежена вручную по всем ветвям (green/red/missing/cancelled/error, чужой/свой прогон, ещё не решённый прогон) и подтверждена тестами + мутантами.
- `test/e2e-gate.test.mjs`, `test/release-workflow.test.mjs` — прогнаны лично, зелёные; содержание тестов сверено построчно с тем, что они утверждают доказать (не только «зелёный», но и что именно проверяет каждый `assert`).
- `.github/workflows/release.yml` — прочитан целиком в контексте (job `gate`/`build`, `needs: gate`), положение и условие нового шага верны; убедился, что `build` не может выполниться при падении шага (через `needs: gate`).
- `scripts/mutation-gate.mjs` — три новых определения прочитаны, мутации осмысленны (реально снимают заявленную защиту, а не косметическую строку), прогнаны лично — все поймано.
- `houseplan-e2e/.github/workflows/e2e.yml` — получен напрямую с GitHub, не из пересказа хендоффа; job `plan`/`e2e` прочитаны целиком.
- Живые прогоны `34393136097` и `34392391382` — сверены напрямую через `gh api`, не приняты на слово хендоффа.
- `PROCESS.md`, `AGENTS.md`, `docs/DEVELOPMENT.md`, `docs/TESTING.md`, `docs/specs/README.md` — прочитаны в диффе, соответствуют факту (проверено предыдущими пунктами: положение шага, имя job, токен, поведение `journeys-dev`).
- Трейлеры всех 5 коммитов (`Issue: #514`, `User-Visible: no`) — проверены `git show -s --format=full`, корректны.
- `node scripts/process-gate.mjs --range origin/dev..HEAD` — «гейт пройден, предупреждений 0».
## Чего не проверял
- `tsc`/`npm test` (полный)/`npm run build` — не перегонял, полагаюсь на зелёный Validate этого точного SHA (ссылка выше), как разрешает правило «дешёвые гейты уже подтверждены».
- Реальную работоспособность `HP_PROCESS_TOKEN`/`E2E_DISPATCH_TOKEN` на `workflow_dispatch` в `houseplan-e2e` при событии `release: published` — недоступно ревьюеру (нет прав на секреты, событие `release` нельзя сэмулировать без публикации настоящего релиза). ТЗ и хендофф это честно называют «первая живая проверка — следующий stable»; путь фейл-safe (403 → красный шаг с текстом про секрет, что покрыто тестом `#514 AC1: a dispatch refused by the token…`).
- Полный набор мутантов `mutation-gate.mjs` (весь файл, 8000+ строк) — прогонял только три новых по `--id`, не весь `--only`/полный корпус: задача добавляет ровно три мутанта, остальные к диффу не относятся и это предрелизный, а не ревью-гейт.
- `golden:verify`, `check-docs.mjs`, `model-invariants.mjs`, `pytest tests_backend` — не прогонялись: diff не затрагивает `src/**`, геометрию или `custom_components/**/*.py` (см. таблицу гейтов и обоснование там же).
- Полный список `demo/smoke_*.mjs` через `wc -l` не запрашивал — `smoke-select.mjs` прямо ответил «выбирать нечего».
## Что проверено и корректно
- Все 6 AC ТЗ пронумерованы и выполнены, каждый доказан тестом/мутантом/чтением с указанием, чем именно.
- Три заявленных мутанта реально снимают заявленную защиту и пойманы штатным раннером (1 из 1 каждый), прогнано лично, не принято на слово хендоффа.
- Контракт имени job в `houseplan-e2e/e2e.yml` (`"${suite} · HP ${ref} · HA ${ha}"`), на который опирается `isOurRun`, подтверждён прямым чтением workflow чужого репозитория, а не пересказом.
- Два живых прогона, на которые ссылается хендофф, подтверждены напрямую через `gh api` — job-состав и `conclusion` совпадают с заявленным.
- `previousStable` полагается на порядок `gh release list` (новые первыми) — порядок подтверждён прямым вызовом на реальном репозитории, а не одним лишь тестом с придуманным порядком входа.
- Трейлеры, имя ветки, `User-Visible: no` + отсутствие изменений в changelog (верно — no user-visible изменение) — корректны, `process-gate.mjs` подтверждает.
- Диапазон изменений строго в рамках скоупа ТЗ (§2): `scripts/e2e-gate.mjs`, `release.yml`, `mutation-gate.mjs`, два новых тестовых файла, документация. Скоуп не расширен, попутных правок «раз уж я здесь» не обнаружено.
- Одно число/один источник — неприменимо: диф не добавляет и не меняет ни одной пользователем видимой величины.
## Продуктовое рассуждение
Задача закрывает не строку `docs/SCOPE.md` напрямую (это инфраструктура процесса релиза, не поверхность продукта), а укрепляет процесс, которым продукт поставляется в HACS: раньше `houseplan-e2e` существовал и был зелёным, но stable-релиз уходил пользователям, не дождавшись его. Теперь дождётся. Это ровно то, что заявлено в issue и ТЗ, без расширения и без ухудшения смежного поведения (беты не тронуты, что подтверждено условием `if` и тестом AC3).
## Вопросы владельцу
Нет. Открытых продуктовых вопросов не осталось — единственный технический вопрос из тела issue (нужен ли `E2E_DISPATCH_TOKEN`) снят автором фолбэком на уровне кода, реализация фейл-safe при отсутствии прав.
---
## Материал раунда
- SHA материала: `395b0e38f4b3e34071e2bcf236440bf476b2c75a`
- Диапазон: `origin/dev..HEAD` (5 коммитов)
- ТЗ: `docs/specs/514-e2e-stable-release-gate.md`, ревью ТЗ: `docs/reviews/SPEC-REVIEW-514-r1.md` (зелёное, r1)
- Заход: r1 · это первый заход код-ревью, унаследовать нечего
---
<!-- material-anchors: сгенерировано конвейером (#414) -->
## Материал раунда
- Ветка: `issue/514-e2e-stable-release-gate`, коммит `5212452994ce` — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
- Дерево материала: `47cb0575e5e31df27eb1d4c56e109a440a23011f`
```
git log --all --format='%H %T' | grep 47cb0575e5e3
```
- ТЗ `docs/specs/514-e2e-stable-release-gate.md`, блоб `30795778fee11c8df610440dd86b745112e120c4`
```
git log --all --find-object=30795778fee11c8df610440dd86b745112e120c4 -- docs/specs/514-e2e-stable-release-gate.md
```
- Вердикт конвейера: `green` · High 0