Issue: #402 User-Visible: no
19 KiB
CODE-REVIEW-402-r1
- Issue: https://github.com/Matysh/houseplan-card/issues/402
- ТЗ:
docs/specs/402-confirm-outside-main-branch.md(ревизия 2, зелёное SPEC-REVIEW-402-r2) - Диапазон:
origin/dev..HEAD, SHA на момент вывода вердикта:ccd870ddf77c9a03ec4618df3e9ded5f159d174b - Заход: код-ревью r1 · блокирующих циклов израсходовано 0/4 (первый заход этапа code)
- Класс изменения: A (продукт) + B (гейт-мутант) + C (документация/changelog) + D (бандл/скриншоты)
Скоуп
Диагноз и контракт полностью изложены в ТЗ и обоих раундах ревью ТЗ (оба уже
пройдены, r1 — красный, r2 — зелёный на 11959e4c). Реализация:
src/houseplan-card.ts:render()разбит на_renderBody()(старая цепочка раннихreturn, без изменений по существу) + новыйrender()-обёртку, которая достраивает<hp-confirm>рядом с телом, если тело неnoChangeи неnothing;_confirmDangerотказывает немедленно (Promise.resolve(false)), если!this._config || !this.hass.scripts/mutation-gate.mjs: новый мутантdanger-confirm-back-into-the-branch, возвращающийhp-confirmвнутрь ветки, guard — новый смок.demo/smoke_danger_confirm_branches.mjs(новый, 140 строк, 9 проверок, touch-эмуляцияhasTouch: true).test/optional-space-model-contract.test.mjs: перенесена цель чтения на_renderBody, добавлены статические проверки обёрткиrender().docs/CHANGELOG.md/docs/CHANGELOG.ru.md: пункт про #402, в том же коммите, что и код (00b6f412).ccd870dd: только классы D/C — пересборка бандла и обязательная пересъёмка отпечатка документации (см. «Как проверялось»).
Как проверялось
Зелёного Validate на этом SHA не найдено — все гейты ниже прогнаны лично.
| Гейт | Команда | Результат |
|---|---|---|
| Типы | npx tsc --noEmit |
чисто, 0 ошибок |
| Юниты | npm test |
# tests 1690, # pass 1689, # fail 0, # skipped 1 (совпадает с заявленным в хендоффе) |
| Сборка + сверка копий | npm run build затем cmp dist/houseplan-card.js custom_components/houseplan/frontend/houseplan-card.js |
собрано за 15.6с, cmp без вывода — байт-в-байт совпадение; git status после сборки чист (комитнутый dist уже в актуальном состоянии) |
| Документация | node scripts/check-docs.mjs (обязателен — diff трогает src/**) |
«Documentation checks passed (7 files, 10 external links)» |
| Бюджет (AC7) | npm run bundle:budget |
initial View: 287386 B gzip (budget 300000 B, headroom 12614 B) — совпадает с числом из хендоффа день-в-день; предупреждение про запас бюджета — известный факт #367, не связано с #402 |
| Выбор смоков | node scripts/smoke-select.mjs --base origin/dev --head HEAD |
9 прямых совпадений (_confirmDanger/_dangerConfirm), 18 слабых (общее имя _config) |
| Прямые смоки (9/9) | node demo/smoke_danger_confirm_branches.mjs и ещё 8, см. ниже |
все зелёные |
| Мутационная дисциплина | ручной откат правки + пересборка + прогон нового смока | смок подвисает (промис никогда не резолвится) — мутант красит смок, как заявлено в реестре |
| process-gate | node scripts/process-gate.mjs --range origin/dev..HEAD |
«гейт пройден, предупреждений 0» |
| Инвентарь | npm run inventory |
1690 unit / 211 browser smokes — числа не копировались вручную |
Прямые смоки и их результат:
demo/smoke_danger_confirm_branches.mjs 9/9 → OK (новый)
demo/smoke_danger_confirmation.mjs OK (AC6: без правок утверждений)
demo/smoke_orphan_space_references.mjs OK
demo/smoke_binding_picker.mjs OK
demo/smoke_free_walls.mjs OK
demo/smoke_hidden_flag.mjs OK
demo/smoke_lock_invariant.mjs OK — инвариант блокировок (SCOPE.md) не задет
demo/smoke_optional_space_model.mjs OK
demo/smoke_registryless_opening.mjs OK
smoke_danger_confirmation.mjs печатает необработанный TypeError
(_bindingHasHaPage, чтение .split из undefined внутри рендера диалога
маркера) до итогового OK. Проверил, не регрессия ли это: поднял отдельный
git worktree на чистом origin/dev (символическая ссылка на тот же
node_modules), собрал бандл, прогнал тот же смок — то же самое исключение,
слово в слово, воспроизводится и без диффа #402. Совпадает с тем, что описал
автор в хендоффе (неполные фикстуры диалога маркера, которые не относятся к
этой задаче). Не регрессия — не блокирует.
Слабые совпадения (18, общее имя _config) не прогонял: diff #402 не трогает
ни один из путей, которые эти смоки проверяют (общие настройки, цветовые
пикеры, геометрия стен, раскладка вкладок) — общее имя случайное, не
зарегистрированная связь.
Не прогонял и почему:
npm run golden:verify— diff не меняет видимый статический результат: разметка<hp-confirm>не изменилась ни на строку, изменилось только место инстанцирования в дереве рендера; ни один голден-сценарий не открывает этот диалог по умолчанию (ТЗ прямо фиксирует «в статике диалог не открыт»).python -m pytest tests_backend -q— ни один файлcustom_components/**/*.pyне тронут.node scripts/model-invariants.mjs— diff не касается геометрии,layout,marker.space,open_spans.- Performance-профили — не названы в AC, путь не чувствителен к перфу (структурная правка порядка рендера, не алгоритм).
Находки
Ни одной High, ни одной Medium. Ниже — Low-наблюдения, все закрыты чтением кода (не блокируют, сняты с записью, как разрешает §8/§2.7).
L1 — AC2 не покрыт смоком буквально (ветки fixed_floor pending/invalid, !space)
ТЗ обещает доказательство «тот же смок, три ветки», но
smoke_danger_confirm_branches.mjs реально входит только в ветку
!model.length (онбординг) через enterBranch([]); веток fixed_floor
pending/invalid и !space смок не касается.
Закрыто чтением. render() (src/houseplan-card.ts:11193-11202) — общая
обёртка над результатом _renderBody(): она не знает, какая именно ранняя
ветка вернула шаблон, и достраивает _renderDangerConfirm() к любому телу,
кроме noChange/nothing. Ветки fixed.kind === 'pending'/'invalid'
(:11230-11254) и !space (:11286) не трогают _dangerConfirm ни прямо, ни
косвенно — единственное место, где это поле меняется, это колбэк контроллера
(:2151, вызывается только из HpConfirmController). Значит поведение этих
трёх веток идентично уже доказанной ветке онбординга по построению, а не по
совпадению. Риск регрессии низкий: правка одна и общая для всех веток.
L2 — AC3 не проверен буквальным сценарием issue (клик по корзине → _deleteServerPlan)
Смок вызывает card._confirmDanger(...) напрямую, а не через клик по
<button class="btn ghost danger"> (houseplan-onboarding-runtime.ts:273) и
не проверяет, что план реально удаляется через hass.callWS.
Закрыто чтением. _deleteServerPlan (houseplan-onboarding-runtime.ts:216-243)
— тонкая обёртка: await this.host._confirmDanger({...}), дальше
ревалидация (не в скоупе, введена #32) и callWS. Единственный кусок,
который меняет #402, — это то, что _confirmDanger теперь действительно
разрешает промис в ветке онбординга; это доказано AC1. Обвязка вокруг него не
менялась.
L3 — AC8: тап по scrim не проверен явно
Смок проверяет тап по кнопке «Отмена» в touch-эмуляции, но не тап по
затемнению (scrim) — а в тексте AC8 «тап по scrim не проваливается в план»
назван отдельно.
Закрыто чтением. Разметка и обработчик hp-confirm, включая scrim, не
менялись ни на строку (диф не касается файла компонента диалога, только
места его инстанцирования в houseplan-card.ts); scrimIsSafe — уже
существующая, непереписанная проверка в smoke_danger_confirmation.mjs (тот
же прогон выше, зелёная) — покрывает то же поведение того же компонента на
мыши. Отдельного тач-пути в разметке hp-confirm нет.
L4 — _confirmDanger не проверяет языковой гейт warm явно
Ранний отказ в _confirmDanger (:2153-2161) смотрит только на
!this._config || !this.hass; состояние languageRenderGate(...) === 'warm'
не проверяется вовсе, хотя ТЗ называет его вторым «неготовым» состоянием
наравне с первым.
Закрыто чтением, но с оговоркой. Проследил до конца: _dangerConfirm —
реактивное поле (:2594, state: true), а languageRenderGate
(src/i18n/language-runtime.ts:99-130) при входе в pending ставит
host.inert = true и планирует runtime.ensure(code).then(() => host.requestUpdate()). Значит (а) на warm карточка inert, и ни один из 7
call site'ов _confirmDanger (все — синхронные обработчики кликов) физически
не может сработать по клику; (б) даже если запрос всё же пришёл программно,
он не виснет навсегда — как только ensure() разрешится (успехом или
фоллбэком на английский, оба варианта — settled-состояния), requestUpdate()
запустит обычный рендер, _renderBody() перестанет возвращать noChange, и
render() дорисует _renderDangerConfirm() с уже накопленным состоянием.
Это не «наглухо висящий промис» — класс дефекта #402 — а отложенный до конца
локального переключения языка рендер, что безопасно. Вне скоупа фикс не
требуется. Стоит держать в голове на будущее: если когда-нибудь появится
вызов _confirmDanger не из клика (например, из WS-события синхронизации
между клиентами, J6), этот путь перестанет быть защищён кликовым inert
неявно — но такого вызова сегодня нет ни в одном из 7 мест.
Что проверено и корректно
- AC1 (онбординг спрашивает, «Отмена» →
false) — смокonboardingBranchShowsConfirm+onboardingBranchResolves. Ветка «согласие →true» доказана составным путём (openConfirmSurvivesBranchChangeрешаетtrue) плюс чтением: решение резолвится тем же обработчиком_onDangerConfirmDecisionнезависимо от того, в какой ветке был открыт диалог. - AC2 — см. L1, закрыто чтением.
- AC3 — см. L2, закрыто чтением.
- AC4 (открытый диалог переживает смену ветки) — смок
openConfirmSurvivesBranchChange: диалог остаётся, промис резолвитсяtrueпосле перехода в ветку онбординга. - AC5 (
!_config || !hassотказывает немедленно) — смокnotReadyCardRefusesInsteadOfHanging. Часть проwarm— см. L4. - AC6 (существующее поведение основной ветки не изменилось) —
demo/smoke_danger_confirmation.mjsне тронут диффом (git diffпуст) и зелёный при личном прогоне. - AC7 (бюджет не растёт) —
npm run bundle:budgetлично: 287 386 Б, совпадает с цифрой из хендоффа. - AC8 (touch: диалог появляется, тап по «Отмена» →
false) — смокtouchTapOnCancelResolvesFalseподhasTouch: true, isMobile: true. Про scrim — см. L3. - AC9 (соседи
_tapConfirm/_vacCalConfirmне переехали) — по диффу: единственное удаление в старой финальной ветке — блок_dangerConfirm(:11872-11878в старой нумерации), сама ветка и соседние блоки не задеты; плюс смокneighbourConfirmsUntouched. - Мутационная дисциплина: откатил правку руками
(
return html\${body}${this._renderDangerConfirm()}`;→return html`${body}`;), пересобрал, прогнал новый смок — подвис (промис из ветки онбординга не резолвится), т.е. красит именно так, как заявляет мутантdanger-confirm-back-into-the-branchвscripts/mutation-gate.mjs. Автор честно указал, что штатный прогонmutation-gate.mjs` не укладывается в лимит времени песочницы и сделал то же руками — переприверил независимо, совпадает. - Трейлеры и changelog:
00b6f412несётIssue: #402,User-Visible: yesи правки в оба changelog в этом же коммите (свереноgit show). Остальные коммиты —User-Visible: no, тоже с трейлером.node scripts/process-gate.mjs --range origin/dev..HEAD— «гейт пройден, предупреждений 0». - Класс D/C коммит
ccd870dd: только бандл-деревья и обязательная пересъёмка отпечатка документации (любая правкаsrc/**делает его устаревшим, §8) — без продуктового кода,Baseline-Reviewedне требуется (это неdemo/golden/baselines/**). - Инвариант блокировок (SCOPE.md):
smoke_lock_invariant.mjsзелёный — правка не открыла новый путь актуации.
Чего не проверял
npm run golden:verifyи полный наборdemo/smoke_*.mjs(211 файлов) — не запускал; обоснование пропуска — в разделе «Как проверялось». Это предрелизная обязанность, не гейт этого ревью.- Полный HA-харнесс /
pytest tests_backend— Python не тронут. node scripts/model-invariants.mjs— геометрия не тронута.- Ручного тестирования в браузере (кроме smoke-инфраструктуры) не проводил — фазы ручного тестирования в процессе нет.
Вердикт
Зелёный. High: 0, Medium: 0. Четыре Low-наблюдения (L1–L4) — все закрыты
чтением кода тем же ревью, без правок; ни одно не открывает issue и не
возвращает автору по §202 (все либо в скоупе и не требуют правки, либо
структурно недостижимы). Задача чинит именно заявленный класс дефекта:
подтверждение больше не привязано к конкретной ветке render(), отказ в
неготовом состоянии — явный, а не подвешенный, и это подтверждено
исполняемым тестом, который умеет падать (проверено лично откатом правки).