Files
2026-09-22 23:42:32 +00:00

17 KiB
Raw Permalink Blame History

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 на точном SHA fc70a3fc (.../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 — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
  • Дерево материала: ebb43bfa79f4ba762532ef83c4e4ea2bd63c70a5
    git log --all --format='%H %T' | grep ebb43bfa79f4
    
  • Тело issue: c78f51ffc0d5268ea3a82d980f9061b9f2756d0a850072fec514342889af8c9e
  • Вердикт конвейера: green · High 0