mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 11:18:48 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1312eb14e7 |
@@ -0,0 +1,177 @@
|
||||
# Код-ревью issue #107 — переключение виртуального источника света «Всегда» (r2)
|
||||
|
||||
- **Issue:** https://github.com/Matysh/houseplan-card/issues/107
|
||||
- **ТЗ:** [`docs/specs/107-virtual-light-toggle.md`](../specs/107-virtual-light-toggle.md), ревью
|
||||
[`SPEC-REVIEW-107-r1.md`](SPEC-REVIEW-107-r1.md) — зелёное, High 0 / Medium 0.
|
||||
- **Предыдущий цикл:** [`CODE-REVIEW-107-r1.md`](CODE-REVIEW-107-r1.md) — красный,
|
||||
H1 (презентация маркера не отражает ручное состояние при сохранённых исходящих
|
||||
`controls`) + M1 (сбор `pytest tests_backend` падает без HA) → M1 заведён
|
||||
отдельным issue [#135](https://github.com/Matysh/houseplan-card/issues/135).
|
||||
- **Диапазон:** `origin/dev...HEAD`. Новый коммит цикла — `de0171dd028f87622a42e0b3d698473ef5e70cb9`
|
||||
("fix: keep manual virtual light face canonical"), поверх `1079cdfab25617df924b8c3592631aa40e078d87`.
|
||||
Ветка `issue/107-virtual-light-toggle`.
|
||||
- **Ревьюер:** Claude (код-ревью ≠ ревью ТЗ ≠ r1, свежая сессия, без контекста
|
||||
реализации и без контекста r1-обсуждения).
|
||||
- **Цикл:** r2/4.
|
||||
|
||||
## 1. Скоуп изменения (дельта к r1)
|
||||
|
||||
r1 уже проверил backend (`virtual_lights.py`, `store.py`, `websocket_api.py`,
|
||||
миграция в `__init__.py`), фронтовую персистентность/live-sync
|
||||
(`virtual-light-state.ts`, `config-store.ts`, `houseplan-card.ts`), typed-intent
|
||||
toggle (`device-toggle.ts`, `devices.ts`), i18n и документацию — эта часть не
|
||||
менялась в r2 и повторно не пересматривается по существу.
|
||||
|
||||
Коммит `de0171d` правит ровно то, что требовал вердикт r1:
|
||||
|
||||
- `src/device-presentation.ts` — `resolvePresentationSources()` теперь для
|
||||
точной manual-тройки (`isManualVirtualLightMarker`) отдаёт лицо маркера
|
||||
собственному canonical light source, а не агрегату `controls`; сохранённые
|
||||
исходящие `controls` остаются в графе (не исчезают из конфигурации/light-графа
|
||||
устройства-цели), но не перехватывают иконку/CSS-класс `on`/`off` самого
|
||||
маркера;
|
||||
- `src/space-card.ts` — две конструкции `virtualFingerprint` (`_captureRenderDeviceSnapshot`,
|
||||
`_frameFingerprint`) переведены на `snap?.virtualLights` вместо `snap ? snap.virtualLights…`,
|
||||
для единообразия с остальными местами файла, которые уже использовали
|
||||
optional chaining (строки 309/392/736 в исходном 1079cdf). Функционально
|
||||
эквивалентно в текущем коде (`virtualLights` всегда строится и в
|
||||
`cachedSnapshot()`, и в `fetchFresh()`), но защищает от падения, если где-то в
|
||||
графе появится `_snap`/`snap` без гарантированного поля `virtualLights`
|
||||
(например, унаследованный модульный `cache` из другой версии бандла на той же
|
||||
странице) — не увидел, что это было наблюдаемым падением сейчас, но и не
|
||||
увидел вреда от правки;
|
||||
- `test/device-presentation.test.mjs` — новый unit `issue 107 manual virtual
|
||||
source owns its face despite saved outgoing controls`, воспроизводящий ровно
|
||||
комбинацию AC12/H1 (marker с `controls: ['light.ceiling']`) для `off` и `on`;
|
||||
- `docs/CHANGELOG.md`/`docs/CHANGELOG.ru.md` — уточнение формулировки («including
|
||||
markers that retain saved outgoing controls» / «в том числе при сохранённых
|
||||
исходящих связях управления») в том же коммите, `User-Visible: yes`.
|
||||
|
||||
M1 в этот коммит не входит (сознательно, по хендоффу) — это корректно: Medium
|
||||
не должен чиниться заодно с фиксом High, он уже заведён issue'ом.
|
||||
|
||||
## 2. Как проверялось
|
||||
|
||||
| Гейт | Команда | Результат |
|
||||
|---|---|---|
|
||||
| Typecheck | `npx tsc --noEmit` | зелёный, без вывода |
|
||||
| Unit (frontend) | `npm test` | **782/782 pass** (было 781/781 в r1; +1 — новый регресс-тест `device-presentation.test.mjs`) |
|
||||
| Build + сверка бандлов | `npm run build && sha256sum dist/houseplan-card.js custom_components/houseplan/frontend/houseplan-card.js demo/srv/assets/houseplan-card.js` | один и тот же SHA-256 `d75945b8…8442e17` для всех трёх копий, совпадает с указанным автором в хендоффе; `git status --short` пусто после сборки — воспроизводимо |
|
||||
| Целевой browser smoke (тот самый, что провалил H1 в r1) | `node demo/smoke_virtual_light_toggle.mjs` (после свежей сборки и синхронизации бандла) | **7/7 true**: `initialEverywhereOn`, `clickEverywhereOff`, `secondClickEverywhereOn`, `touchSingleToggle`, `reloadFirstStateOff`, `oneServerTogglePerGesture`, `noHaServiceCalls` — включая ровно две проверки, которые падали в r1 (`clickEverywhereOff`, `reloadFirstStateOff`). Файл смока не менялся между r1 и r2 (`git log` на него показывает только исходный коммит `1079cdf`) — фикс проверен тем же, изначально проваленным, воспроизводимым тестом, не переписанным «чтобы позеленело` |
|
||||
| Backend pytest, чистое подмножество (повтор r1-методики, без HA) | `python3 -m venv /tmp/venv-review-r2 && pip install pytest voluptuous && PYTHONPATH=. pytest tests_backend -q` | **всё ещё падает сбором** на `tests_backend/test_virtual_lights.py` — идентично r1 (`ModuleNotFoundError: No module named 'homeassistant'`, `Interrupted: 1 error during collection`). Ожидаемо: M1 сознательно не входит в этот коммит, чинится отдельно в #135 |
|
||||
|
||||
Не прогонялись (осознанно, диапазон r1→r2 не касается backend/live-sync/i18n
|
||||
кода, только `device-presentation.ts`/`space-card.ts` и один unit-файл):
|
||||
|
||||
- полный набор `demo/smoke_*.mjs` (130 сценариев) — диапазон изменения узкий
|
||||
(один resolver презентации плюс два defensive `?.`), не задевает все
|
||||
поверхности; целевой smoke уже прогнан и является тем же смоком, который
|
||||
ловил дефект;
|
||||
- `npm run golden:verify` — изменение видимого результата ограничено ровно
|
||||
той же новой комбинацией (manual virtual toggle + сохранённые `controls`),
|
||||
для которой в существующих golden-baseline нет сценария (feature появилась в
|
||||
этом же issue); в существующие baseline-сценарии эта тройка не входит, и
|
||||
визуальные regressions на них не ожидаются — не подтверждено прогоном golden
|
||||
(это решение сужения объёма, а не находка);
|
||||
- performance-профили — в AC не назван численный бюджет, ТЗ §15.4 явно относит
|
||||
это к pre-beta gate (без изменений к r1);
|
||||
- полный HA backend harness (`pytest-homeassistant-custom-component`) —
|
||||
недоступен в этом окружении (нет `.venv-backend`, чистый Linux-раннер).
|
||||
Backend-код (`virtual_lights.py`, `store.py`, `websocket_api.py`,
|
||||
`__init__.py`) в этом цикле не менялся относительно r1, поэтому AC2/AC3/AC4/
|
||||
AC8/AC9 повторно по существу не пересматривались — их разбор чтением из r1
|
||||
остаётся в силе.
|
||||
|
||||
## 3. Находки
|
||||
|
||||
Блокирующих находок нет.
|
||||
|
||||
Не найдено новых High/Medium в дельте r1→r2. Единственное отмеченное в §1 —
|
||||
изменение в `space-card.ts` (`?.virtualLights`) — не тождественно
|
||||
воспроизводимому дефекту: не нашёл сценария, где `this._snap`/`snap` ненулевой,
|
||||
но `virtualLights` при этом отсутствует (оба источника, `cachedSnapshot()` и
|
||||
`fetchFresh()`, безусловно строят это поле через `virtualLightSnapshot(...)`/
|
||||
`adoptVirtualLightServerSnapshot(...)`). Правка защитная и не меняет наблюдаемое
|
||||
поведение ни в одном пройденном тесте — не поднимаю до Low, так как не нашёл ни
|
||||
воспроизведения, ни вреда.
|
||||
|
||||
## 4. Что проверено и корректно
|
||||
|
||||
- **H1 закрыт, доказано исполнением, не только чтением.** Целевой
|
||||
`demo/smoke_virtual_light_toggle.mjs` — тот же файл, что и в r1, без
|
||||
изменений — теперь проходит 7/7, включая обе ранее красные проверки.
|
||||
Дополнительно инструментирован новый unit
|
||||
(`test/device-presentation.test.mjs`), который специально воспроизводит
|
||||
именно тройку H1 (`binding:'virtual', is_light:true, tap_action:'toggle',
|
||||
controls:['light.ceiling']`) и проверяет оба направления (`off`/`on`) для
|
||||
`sourceKind`, `visualSources`, `visual.status` и CSS-класса `on`. Тест умеет
|
||||
падать: без ветки `manualVirtualFace` в `resolvePresentationSources()`
|
||||
`sourceKind` откатывается на `'controls'`, `visualSources` включает
|
||||
`light.ceiling` (state `on`) первым источником, и assert
|
||||
`off.visual.status === 'neutral'`/`!classes.includes('on')` не проходит —
|
||||
ровно это давал прогон смока в r1 до фикса.
|
||||
- **Причинный разбор совпадает с диагнозом r1.** `resolvePresentationSources()`
|
||||
(`src/device-presentation.ts:252-260,285-287`): для eligible-тройки
|
||||
(`isManualVirtualLightMarker(d.marker)`) `lights` строится только из
|
||||
`ownedLights` (без `via==='controls'` источников), и `sourceKind` принудительно
|
||||
остаётся `'light'` независимо от того, есть ли у маркера сохранённые исходящие
|
||||
`controls`. Сами `controls`-источники при этом не исчезают из общего
|
||||
`resolvedLightSources()`/Glow-графа устройства-цели (`light.ceiling`
|
||||
продолжает управляться и показываться как обычно у себя) — правка узкая,
|
||||
ограничена лицом самого manual-маркера, что соответствует ТЗ §6.3
|
||||
(«Controls остаются lossless … но не управляют state этого источника»).
|
||||
- **Не расширяет eligibility.** `isManualVirtualLightMarker()` — тот же
|
||||
переиспользуемый предикат из `virtual-light-state.ts`, что уже покрыт
|
||||
AC1-тестами в r1 (`devices.test.mjs`/`device-toggle.test.mjs`), новой логики
|
||||
распознавания тройки не введено — риск регресса для неэлигибл-маркеров
|
||||
(обычные `controls`-контроллеры, cover, switch, passive sensor) минимален и
|
||||
подтверждён тем, что все существовавшие кейсы `device-presentation.test.mjs`
|
||||
(44 теста в файле) остаются зелёными без изменений.
|
||||
- **AC5/§6.2 (canonical consumer contract).** `docs/LIGHT.md:137-147` уже
|
||||
декларирует «device presentation» как обязательного потребителя canonical
|
||||
ручного состояния — до r2 это было расхождением документации с кодом
|
||||
(H1), теперь код соответствует уже написанному тексту; правка документации
|
||||
не потребовалась.
|
||||
- **CHANGELOG.** Обе версии (`docs/CHANGELOG.md`, `docs/CHANGELOG.ru.md`)
|
||||
уточнены в том же коммите `de0171d` — фраза, которую r1 отметил как
|
||||
«станет неполной без H1», исправлена точным добавлением про сохранённые
|
||||
исходящие связи. Трейлеры коммита: `Issue: #107`, `User-Visible: yes` — оба
|
||||
changelog в этом же коммите, соответствует правилу.
|
||||
- **M1 корректно вынесен.** Отдельный issue
|
||||
[#135](https://github.com/Matysh/houseplan-card/issues/135) существует,
|
||||
ссылается на #107 и на находку CODE-REVIEW-107-r1.md, помечен `S1-new`,
|
||||
`tech-debt`, `tests` — не оставлен как TODO в тексте ревью (§12 process).
|
||||
- **Инвариант сборки.** `npm run build` воспроизводим, три копии бандла
|
||||
побайтно совпадают друг с другом и с указанным в хендоффе SHA-256.
|
||||
|
||||
## 5. Чего не проверял
|
||||
|
||||
- Полный HA backend harness — недоступен в этой среде; поскольку backend-код
|
||||
не менялся между r1 и r2, повторно не разбирал по существу AC2/AC3/AC4/AC8/AC9
|
||||
сверх того, что зафиксировано в CODE-REVIEW-107-r1.md §4.
|
||||
- Полный набор из 130 `demo/smoke_*.mjs` и `npm run golden:verify` — не
|
||||
запускал; обоснование сужения в §2 (диапазон изменения узкий, целевой smoke
|
||||
уже покрывает ровно сценарий дефекта, новая визуальная комбинация не входит
|
||||
ни в один существующий golden baseline).
|
||||
- Performance-профили — не в AC, отложено на pre-beta gate тем же ТЗ, что и в
|
||||
r1.
|
||||
- Не проверял защитную правку `space-card.ts` (`?.virtualLights`) на предмет
|
||||
реального воспроизводимого сценария падения — не нашёл такого сценария при
|
||||
чтении обоих источников `_snap`/`snap` (`cachedSnapshot()`, `fetchFresh()` в
|
||||
`config-store.ts`), поэтому не поднимаю её в находки, но и не подтверждаю
|
||||
специальным тестом, что именно она предотвращает.
|
||||
- Продуктовое соответствие `docs/SCOPE.md`/выбор J1/J3 — предмет ревью ТЗ
|
||||
(зелёное, r1), в код-ревью повторно не пересматривается.
|
||||
|
||||
## 6. Вердикт
|
||||
|
||||
H1 из r1 исправлен и подтверждён исполнением: тот же ранее красный
|
||||
`demo/smoke_virtual_light_toggle.mjs` теперь зелёный 7/7 без изменений в самом
|
||||
файле смока, плюс новый целевой unit-регресс. M1 корректно вынесен отдельным
|
||||
issue (#135), в этот коммит не подмешан. Новых High/Medium в дельте r1→r2 не
|
||||
найдено. Все обязательные гейты (`typecheck`, `npm test`, `npm run build` +
|
||||
сверка бандлов, целевой smoke) зелёные; сознательно суженные (`golden:verify`,
|
||||
полный набор smoke, performance, полный HA harness) обоснованы объёмом дельты
|
||||
и перечислены выше.
|
||||
|
||||
`High: 0 · Medium: 0` — задача уходит в очередь на пре-релиз.
|
||||
Reference in New Issue
Block a user