Issue: #103 User-Visible: no
15 KiB
Код-ревью issue #103 · цикл r1/4
Toggle confirmation: показывать текущее и ожидаемое состояние.
- Issue: https://github.com/Matysh/houseplan-card/issues/103
- ТЗ:
docs/specs/103-toggle-confirmation-state.md, принято зелёнымdocs/reviews/SPEC-REVIEW-103-r1.md - Диапазон:
origin/dev..HEAD, 3 коммита (a449edcТЗ,6c7958cревью ТЗ,9327737/9327737...cfреализация) - Ревьюер: Claude, свежая сессия без контекста реализации
Скоуп
Диапазон изменил:
src/device-toggle.ts— новый pureformatToggleConfirmation()и типToggleConfirmationFormatter;resolveToggleIntent/резолвер не менялись;src/houseplan-card.ts—_tapConfirmрасширен до discriminated union{kind:'toggle', lines, initialIntent, deviceId, exec}/{kind:'run', text, exec}; новый_toggleConfirmationLines()и_toggleConfirmationStateText(); шаблон диалога рендеритlinesдля toggle и прежний<p>для run;src/styles.ts—.tapconfirm-body/.tapconfirm-line(перенос длинных строк, нет горизонтального скролла);src/i18n/en.json+src/i18n/ru.json— 15 новых ключейconfirm.*в обоих файлах;test/device-toggle.test.mjs— юнит-тесты форматтера;demo/smoke_toggle_confirmation.mjs— новый браузерный смок;docs/CHANGELOG.md/docs/CHANGELOG.ru.md,docs/USER-GUIDE(.ru).md,docs/TESTING.md,docs/specs/README.md— документация и changelog в том же коммите, что и поведение;- три копии бандла (
dist/,custom_components/.../frontend/,demo/srv/assets/) иdocs/images/screenshots.json(обновлениеsourceFingerprint/sourceSha256,imageSha256каждого сценария не изменился — визуальный контент прежний).
Соответствует docs/SCOPE.md J3 («tap-to-toggle for safe domains») и не
задевает lock invariant: secure-цели (lock/alarm_control_panel/guarded
cover) резолвер отфильтровывает раньше формирования intent, toggleOperation
для них null, confirmation не открывается — новая ветка кода это условие не
меняет.
Как проверялось
| Гейт | Команда | Результат |
|---|---|---|
| Typecheck | npx tsc --noEmit |
green |
| Unit | npm test |
880/880 green |
| Build + сверка бандлов | npm run build && cmp dist/houseplan-card.js custom_components/houseplan/frontend/houseplan-card.js && cmp dist/houseplan-card.js demo/srv/assets/houseplan-card.js |
green, три копии идентичны |
| Целевой browser smoke (AC1,3,6,7,8,9) | node demo/smoke_toggle_confirmation.mjs |
green, все 9 подпроверок true |
| Регрессия смежных confirmation-путей | node demo/smoke_cover_tap.mjs, node demo/smoke_virtual_light_toggle.mjs, node demo/smoke_tap_run.mjs, node demo/smoke_ha_controls.mjs, node demo/smoke_controls.mjs |
все green |
| Golden | npm run golden:verify (свежий бандл) |
100% passed, 0 failed/error — новых сценариев dialog нет, стили аддитивны и не задели существующие сцены |
| i18n-паритет | node -e "..." сравнение ключей en.json/ru.json |
равны, расхождений нет |
| Process gate | node scripts/process-gate.mjs |
«гейт пройден, предупреждений 0» (без --issues, офлайн-часть) |
| Backend | не прогонялся | custom_components/**/*.py в диффе нет — гейт не применим |
| Performance-профили | не прогонялись | не названы в AC, изменение — только текст/CSS диалога |
Дисциплина «тест умеет падать» — проверено для прогнанных тестов:
- временно нейтрализовал ветку
isGroup && nextEffect==='turn-on'вformatToggleConfirmation→npm testдалnot ok 159(тест группы), после отката снова 880/880; - временно подменил
closed → confirm.state_openв_toggleConfirmationLines→node demo/smoke_toggle_confirmation.mjsдалenglishCurrentExpected: falseиrussianCurrentExpected: false, после отката снова все 9 true и бандлы пересобраны/сверены заново.
AC — разбор
| AC | Доказательство | Вывод |
|---|---|---|
| 1. current/expected lines для исполняемой цели | unit toggle confirmation formats every next effect...; smoke englishCurrentExpected/russianCurrentExpected |
подтверждено |
2. expected строго по nextEffect, не по домену |
unit-тест перебирает все ToggleNextEffect на одном intent с фиксированным state; чтение formatToggleConfirmation — ветвление только по nextEffect/isGroup, не по domain/semantics |
подтверждено |
| 3. локализация power/cover/valve/virtual/group | unit (partial/all-off group, virtual) + smoke englishGroupAndVirtualCopy, RU/EN рендер |
подтверждено |
| 4. partial group не обещает изменение skipped | unit describes executable group targets and skipped targets separately (denominator = только targets, skipped — отдельная строка); проверено на способность теста падать |
подтверждено |
5. toggle → «решит HA» |
unit-тест nextEffect: 'toggle' → expected:by-ha |
подтверждено |
| 6. no-op не открывает confirmation | smoke noOperationDoesNotOpen (guarded cover, secure no-op) |
подтверждено |
| 7. Confirm выполняет заново разрешённый intent | smoke sameTargetUsesCurrentDirection (cover меняет state closed→open между открытием и подтверждением, выполняется close_cover, а не команда снимка) |
подтверждено |
| 8. смена target set отменяет actuation, старый toast | smoke changedTargetCancels (переброс bindingRef на другую cover-сущность → 0 service calls, toast.tap_target_changed) |
подтверждено |
| 9. desktop/mobile, keyboard, screen-reader order | smoke accessibleDomOrder (dialog.title === текст вопроса, data-line 0/1, 2 кнопки) + narrowDialogDoesNotScrollHorizontally на 390px; keyboard focus/Escape/scrim — проверено чтением, не исполнением: dismiss-on-scrim, @hp-close и структура footer/кнопок в диффе не изменены |
подтверждено (смок + чтение) |
| 10. run/другие confirmations не регрессируют | smoke smoke_tap_run.mjs green; чтение шаблона — ветка kind==='run' рендерит тот же <p>{text}</p> с тем же заголовком btn.run, что и раньше |
подтверждено |
Что проверено и корректно
- Инвариант lock/alarm/guarded-cover не тронут: секьюрные цели отфильтровываются
резолвером
#94до формированияnextEffect/targets, confirmation для них не строится ни при каком новом коде. formatToggleConfirmation— чистая функция, не знает про HA/DOM; вся i18n и HA-formatter-специфика инкапсулированы в_toggleConfirmationStateText/_toggleConfirmationLinesвhouseplan-card.ts, как и требовало ТЗ §5 («houseplan-card.tsне выводит next state по domain самостоятельно»).- Direction всегда берётся из
intent.nextEffect, не изcurrentState— единственный источник расчёта — резолвер #94; UI не дублирует его логику. - Denominator группы — количество фактических
targets(byEntityбез skipped), а не число сконфигурированных ссылок; skipped выводится отдельной строкой и не входит в «включено N из M». - Race-контракт #94 не нарушен:
exec()заново находитcurrentDevice→ пересчитывает intent → сравнивает черезsameToggleOperationTargetsс исходным снимком; при совпадении целей выполняется текущее направление/команда (не снимок на момент открытия). run-подтверждение осталось на прежней простой форме (text+<p>), discriminated union не заставил его притворяться toggle — ТЗ §7 требование выполнено буквально.- i18n: все 15 новых ключей присутствуют в обоих словарях; в целом по файлам
en.json/ru.jsonрасхождений ключей нет. - Три копии бандла синхронны (
cmpbyte-for-byte),docs/images/screenshots.jsonобновил только fingerprint/sourceSha256 —imageSha256каждого сценария не изменился, то есть визуальный результат существующих сцен не задет; отдельного golden-сценария для диалога не требовалось (ТЗ §11 — narrow smoke покрывает). - Оба changelog правлены в том же коммите, что и поведение (
932773773f0b), трейлерыIssue: #103/User-Visible: yes|noна месте на всех трёх коммитах.
Находки
Ни одной High/Medium-находки. Три ранее отмеченные Low из ревью ТЗ
(SPEC-REVIEW-103-r1.md) не имеют кодового следствия — направление в рантайме
берётся из nextEffect, а не из нормативной таблицы ТЗ, поэтому неточность
таблицы (closing в одной строке с open) не воспроизвелась в коде.
- Low (снято, без правки).
houseplan-card.ts, веткаif (actionDevice.marker?.tap_confirm) { const lines = this._toggleConfirmationLines(initial); if (!lines.length) return; ... }— защитныйif (!lines.length) returnнедостижим при нынешних инвариантах резолвера: везде, гдеtoggleOperation(intent)истинен (единственное условие, пропущенное раньше по коду), у intent уже гарантированно естьnextEffectиtargets.length >= 1(прослежено по всем веткамresolveToggleIntent/resolveGroupEntities/virtual-light intent). Формально это тихий no-op вместо открытия диалога, если бы условие когда-нибудь стало достижимым, но сегодня это мёртвый код, а не наблюдаемый дефект. Правка не требуется; снимаю с записью здесь, как того требует §8/§12 процесса.
Чего не проверял
- Backend (
python -m pytest tests_backend) — не прогонял: диапазон не затрагивает ни одного файлаcustom_components/**/*.py. - Performance-профили — не прогонял: AC их не называет, изменение не трогает чувствительные к перфу пути (только текст диалога и аддитивный CSS).
- Полный набор из 127 браузерных смоков — прогнал только целевой
(
smoke_toggle_confirmation) плюс пять смежных по confirmation/cover/virtual- light/run поверхности; остальные 121 не запускал — diff не задевает геометрию, wall-thickness, canvas, sun/light рендер и т.д., которые они покрывают. - Ручная проверка в реальном браузере вне demo-харнесса (реальный screen reader, реальная Tab-навигация) — не выполнялась; keyboard/Escape/scrim утверждение AC9 закрыто чтением кода (структура диалога не менялась), как и предусмотрено самим ТЗ («narrow browser smoke + code review»).
node scripts/check-docs.mjs --external— не перепрогонял; автор в хендоффе указал green, доверяю без повторного запуска (внешний, не входит в обязательный набор ревью).process-gate.mjs --issues(сетевая проверка статуса issue черезgh) — не прогонял с токеном; офлайн-часть (process-gate.mjsбез флага) прошла чисто.
Вердикт
Зелёный. Все 10 AC доказаны — юнитами/смоками, которые я прогнал и убедился,
что они умеют падать, либо явным «проверено чтением, не исполнением» там, где
так предусмотрено самим ТЗ (клавиатура/screen-reader в AC9, идентичность
run-ветки в AC10). High: 0, Medium: 0. Единственная Low-находка — недостижимая
защитная ветка — снята без правки, с записью выше.
Вердикт: зелёный · цикл r1/4 · High: 0 · Medium: 0 → нет · Документ: docs/reviews/CODE-REVIEW-103-r1.md