17 KiB
CODE-REVIEW-607-r2
Скоуп
Issue #607 — в реальном
Home Assistant (ветка ha-dialog внутри hp-dialog, не нативный <dialog>
демо-стенда) крестик HA в шапке диалога настроек, за которым следует выбор
«Продолжить» в подтверждении «Отменить изменения?», оставлял диалог невидимым
с живым состоянием формы до перезагрузки страницы.
Материал раунда — ровно fc70a3fcf458f8703acdd78aeeda686fc86e73b7, рабочая
копия на этом SHA не менялась (git status чист до и после моих проверок,
включая два временных прогона мутаций в отдельных worktree — они сами себя
подчищают).
Это второй заход. Предыдущий вердикт — жёлтый на 9bbf6fb2487bfef24bfab6da256165d0965c278f
(docs/reviews/CODE-REVIEW-607-r1.md, комментарий issue от 2026-09-22T23:21:29Z),
единственная находка — Medium в скоупе, без High. Бюджет: этот жёлтый уже
потратил 1 блокирующий цикл из 4 (см. заголовок задачи), r2 — второй заход
общего счёта.
Дельта r1→r2 (git diff 9bbf6fb2..fc70a3fc, git log --oneline 9bbf6fb2..fc70a3fc):
ровно два файла, только вставки, без единой правки продуктового кода:
docs/reviews/CODE-REVIEW-607-r1.md | 200 +++++++++++++++++++++++++++++++++++++
scripts/mutation-registry.mjs | 32 ++++++
2 files changed, 232 insertions(+)
docs/reviews/CODE-REVIEW-607-r1.md — публикация документа предыдущего
раунда (коммит 4382817e, не авторская правка). scripts/mutation-registry.mjs
— две новые записи мутантов (коммит fc70a3fc, test(dialog): register rejected-close mutants (#607)), которыми автор закрывает единственную находку
r1. origin/dev за это время не сдвинулся (61d51dd9, тот же, что был базой
r1) — ребейза, смены контракта или новой подсистемы нет. Дельта локальна:
разбор сужен до неё и до всего, что она задевает (AC4), остальные AC
наследуются из r1 по §2.9.
Что изменилось в этом раунде и как проверялось
Единственная находка r1 (Medium, в скоупе): три новых защитных поведения AC4
(rejectedHaCloseReopensSameShell, repeatedRejectIsIdempotent,
disconnectedRejectDoesNotReopen в demo/smoke_dialog_modal_recovery.mjs)
проверялись только браузерным смоком без записи в scripts/mutation-registry.mjs
— «дорогой гейт без мутанта» по §2.7.
Автор добавил две записи:
dialog-ha-rejected-close-reopen-disabled— патчит.open=${live(!this._closing)}→.open=${true}во втором (безdescribedBy) рендере<ha-dialog>;dialog-ha-disconnected-reject-reopen-enabled— убираетif (this.isConnected)передrequestUpdate()вrejectClose().
Я не принял заявление автора на веру и прогнал обе записи лично, а не только
--check:
node scripts/mutation-gate.mjs --check --id=dialog-ha-rejected-close-reopen-disabled
→ ok dialog-ha-rejected-close-reopen-disabled
node scripts/mutation-gate.mjs --check --id=dialog-ha-disconnected-reject-reopen-enabled
→ ok dialog-ha-disconnected-reject-reopen-enabled
node scripts/mutation-gate.mjs --id=dialog-ha-rejected-close-reopen-disabled
→ ok чистый прогон: node demo/smoke_dialog_modal_recovery.mjs
→ ok dialog-ha-rejected-close-reopen-disabled: заявленный тест покраснел на мутанте
→ поймано 1 из 1
node scripts/mutation-gate.mjs --id=dialog-ha-disconnected-reject-reopen-enabled
→ ok чистый прогон: node demo/smoke_dialog_modal_recovery.mjs
→ ok dialog-ha-disconnected-reject-reopen-enabled: заявленный тест покраснел на мутанте
→ поймано 1 из 1
--check подтверждает уникальность анкера (hits === 1 в mutation-gate.mjs
— иначе тест был бы хрупким к любой соседней правке), полный прогон
подтверждает поведение: чистое дерево — смок зелёный, мутированное — тот же
смок красный, рабочая копия обеих команд вернулась к материалу без следа
(git status чист после каждой). Оба ID уникальны в реестре, дублей нет
(814 записей всего, проверено программно).
Соответствие находке r1: r1's собственная таблица «чем краснеет» уже
использовала ровно эти два патча вручную (Мутация 1 = reopen-disabled,
Мутация 2 = disconnected-reopen-enabled) и сама зафиксировала, что Мутация 1
одним прогоном красит сразу оба поведения — rejectedHaCloseReopensSameShell
и repeatedRejectIsIdempotent (оба читают lateHa.open, которое мутация не
даёт стать true). Значит формально «трёх находок → двух ID» — не пробел:
третье поведение (идемпотентность) физически не имеет отдельной ветки кода,
которую можно было бы сломать независимо от первой мутации — rejectClose()
не содержит собственного guard от повторного вызова, он безопасен по
построению (второй вызов — no-op над уже консистентным состоянием). Регистрация
двух записей полностью покрывает три названных в r1 поведения.
Отдельная адверсарная проверка (не входила в находку r1, проверена из
осторожности)
.open=${live(!this._closing)} встречается в src/hp-dialog.ts дважды — во
втором рендере (патчится новым мутантом) и в первом, под if (this.describedBy)
(строка 565, используется только hp-confirm.ts). Я проверил, не остаётся ли
эта вторая ветка без защиты от регрессии:
hp-confirm.tsникогда не вызываетrejectClose()на себе — наhp-closeон всегда диспатчитhp-confirm-decisionсaccepted: falseи не восстанавливает modal; сценарий «отклонённое закрытие confirm-диалога» структурно не существует сегодня, поэтому смок его не проверяет ни для одной ветки;- но регрессия в этой ветке всё равно не прошла бы незамеченной:
test/hp-dialog-contract.test.mjs:29-37(добавлен в r1, часть каждогоnpm test) — дешёвый, всегда исполняемый unit-тест, который черезsource.match(/<ha-dialog[\s\S]*?>/g)находит ровно оба рендера иassert.matchна.open=\$\{live\(!this\._closing\)\}для каждого из них. Регистрация мутанта в §2.7 обязательна для защит, живущих только за дорогим гейтом («там ревьюер не воспроизведёт отрицательный прогон второй раз») — здесь же гейт дешёвый и его отрицательный прогон воспроизводим тривиально чтением. Находки нет.
Закрытие раунда r1
| Находка r1 | Чем закрыта | Где это видно |
|---|---|---|
Medium (в скоупе): три guard-поведения AC4 в demo/smoke_dialog_modal_recovery.mjs проверяются только смоком, без записи в scripts/mutation-registry.mjs (§2.7) |
Добавлены записи dialog-ha-rejected-close-reopen-disabled и dialog-ha-disconnected-reject-reopen-enabled, патчи — ровно те, что r1 использовал вручную для Мутаций 1–2 |
scripts/mutation-registry.mjs (коммит fc70a3fc), лично перепрогнано мной: node scripts/mutation-gate.mjs --id=<оба> — «поймано 1 из 1» для каждого |
Унаследовано из r1
Ничего в этой дельте не задевает продуктовый код, поведение или доказательства
AC1–AC3 — они наследуются без повторной проверки из
docs/reviews/CODE-REVIEW-607-r1.md, SHA 9bbf6fb2487bfef24bfab6da256165d0965c278f:
- AC1/AC2 (реальное восстановление modal и черновика в HA, Discard/чистое
закрытие) — доказаны
demo/verify_ha_dialog_discard_recovery.mjsна пиновомhome-assistant-frontend==20260729.7для всех четырёх форм; r1 прогнал лично. - AC3 (нативный fallback без регрессии) — четыре
smoke_*_settings_formзелёные, прогнаны r1 лично. - Контракт поведения ТЗ, пункты 1–2, 7 — прочитан построчно в r1
(
src/hp-dialog.ts:449–470, 562–590), код с тех пор не менялся (дельта его не касается). - Не-скоуп ТЗ, i18n, трейлеры коммита
9bbf6fb2, changelog — проверены r1, диапазон не расширялся. - Дешёвые гейты (
typecheck,npm test,npm run buildсо сверкой копий бандла) — r1 сослался на зелёный Validate на9bbf6fb2(.../actions/runs/35795417282); в этом раунде действует отдельное подтверждение — зелёный Validate уже наfc70a3fc(.../actions/runs/35797453549, дано в постановке задачи) — не перегонялись повторно мной. node scripts/check-docs.mjs,no-new-any,bundle:budget, golden/скриншоты, инварианты модели,pytest tests_backend— не применимы или зелёные в r1; дельта r2 не трогаетsrc/**, геометрию, Python-бэкенд, рендер — переносится без повторного прогона.
Что проверено и корректно (в этом раунде)
- Обе новые записи реестра синтаксически валидны,
idуникальны в реестре из 814 мутантов, анкер каждого патча встречается вsrc/hp-dialog.tsровно один раз (--checkзелёный для обеих). - Обе мутации лично прогнаны целиком (
mutation-gate.mjsбез--check): чистое дерево —node demo/smoke_dialog_modal_recovery.mjsзелёный; мутированное — тот же смок красный; рабочая копия репозитория осталась чистой после обоих прогонов (мутации выполняются во временных worktree). - Патчи реестра дословно совпадают с двумя мутациями, которые r1 уже зафиксировал в своей таблице «чем краснеет» — не новая, самодеятельная проверка, а формализация уже воспроизведённого результата.
node scripts/smoke-select.mjs --base origin/dev --head HEADдаёт то же единственное прямое совпадение (smoke_dialog_modal_recovery.mjs←rejectClose), что и в r1 — дельта не расширила смок-поверхность.- Трейлеры обоих коммитов дельты корректны:
4382817e(docs: review document for #607) иfc70a3fc(test(dialog): register rejected-close mutants (#607)) оба несутIssue: #607иUser-Visible: no— правильно, видимое поведение не меняется, правка — только тестовая инфраструктура и публикация документа. Changelog не требуется. - Отдельно проверил (не по находке r1, а из осторожности к дублирующемуся
коду), что вторая, непатченная ветка
live()(используетсяhp-confirm.ts, ветка сdescribedBy) не осталась без защиты от регрессии — она покрыта дешёвымtest/hp-dialog-contract.test.mjs, добавленным ещё в r1; отдельного дорогого гейта для неё не требуется по букве §2.7.
Чего не проверял
- Полный
npm test/npx tsc --noEmit/npm run buildсо сверкой трёх копий бандла — не перегонял: зелёный Validate на точном SHAfc70a3fc(.../actions/runs/35797453549) уже это покрывает, дельта раунда к тому же не трогаетsrc/**. demo/verify_ha_dialog_discard_recovery.mjs(пиновый HA), четыреsmoke_*_settings_form,node --test test/hp-dialog-contract.test.mjs— не перепрогонял отдельно в этом раунде: продуктовый код не менялся с r1, где все они уже прогнаны лично прежним ревьюером; повторный прогон дельта не требует.npm run golden:verify,python -m pytest tests_backend, инварианты модели — не применимо: дельта не меняет рендер, Python-бэкенд, геометрию или ссылки на неё.- Полный ночной прогон реестра мутаций (814 записей) — не применим к ревью; ночной прогон — отдельное расписание (#513).
- Ручное тестирование в живом браузере Home Assistant — вне гейтов цикла ревью; офлайн-пиновая фикстура — предусмотренная ТЗ замена, прогнана в r1.
Вердикт
Единственная находка r1 (Medium, в скоупе) закрыта и лично перепроверена
поведенчески (не только чтением diff'а или словами автора). Новых находок —
ни High, ни Medium — в дельте r1→r2 нет; отдельная адверсарная проверка
дублирующейся live()-ветки тоже не дала находки. AC1–AC4 полностью доказаны
(AC1–AC3 унаследованы из r1 без изменений в их доказательной базе, AC4 теперь
дополнительно защищён мутационным гейтом). Зелёный, без потраченного цикла.
Материал раунда: SHA fc70a3fcf458f8703acdd78aeeda686fc86e73b7, дельта
9bbf6fb2..fc70a3fc (origin/dev не сдвигался, база 61d51dd9). Рабочая
копия временно мутировалась дважды во внешних worktree для адверсарной
проверки реестра и была не тронута в основном дереве (git status чист).
Материал раунда
- Ветка:
issue/607-ha-dialog-close, коммитfc70a3fcf458— ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет. - Дерево материала:
ebb43bfa79f4ba762532ef83c4e4ea2bd63c70a5git log --all --format='%H %T' | grep ebb43bfa79f4 - Тело issue:
c78f51ffc0d5268ea3a82d980f9061b9f2756d0a850072fec514342889af8c9e - Вердикт конвейера:
green· High 0