21 KiB
CODE-REVIEW-54-r2 — #54: контекстные связи Zigbee на плане (hover overlay)
- Issue: #54
- Этап:
code(PROCESS.md §2.7) - Диапазон общий:
origin/dev...HEAD,origin/dev=ec9824f2,HEAD=f0ed526a(веткаissue/54-zigbee-topology-hover, детачHEAD, merge-base сorigin/devне сдвинулся —ec9824f2тот же, что в r1, ребейза не было) - ТЗ:
docs/specs/054-zigbee-topology-overlay.md, ревью ТЗ зелёное на заходе r2 —SPEC-REVIEW-54-r2.md - Заход: r2 код-ревью
- Блокирующих циклов израсходовано: 1/4 (зелёных вердиктов ревью ТЗ бюджет не тратит, §2.10/#227; израсходован ровно жёлтый заход r1 код-ревью)
- Вердикт: зелёный
Скоуп ревью (по дельте, PROCESS.md §2.10)
Предыдущий заход (r1) отревьюил материал на SHA a2867df9 полным разбором и
вернул один Medium (M1). Дельта этого раунда — git diff a2867df9..HEAD,
3 коммита:
| Коммит | Класс | Содержимое |
|---|---|---|
aab5c3ca |
C (docs) | публикация docs/reviews/CODE-REVIEW-54-r1.md шагом конвейера |
4203a01b |
A/B (продукт+тест) | правка M1: src/zigbee-topology-runtime.ts (+12/-3), новый юнит и мутант в test/zigbee-topology.test.mjs (+28), scripts/mutation-gate.mjs (+11, одна новая запись) |
f0ed526a |
C/D (docs+generated) | пересчёт sourceFingerprint в docs/images/screenshots.json после правки src/**; dist/** и custom_components/houseplan/frontend/** регенерированы пересборкой |
Дельта локальна и не расширяет скоуп: новая подсистема не задета, контракт
поведения docs/specs/054-zigbee-topology-overlay.md не менялся, ребейза на
ушедший вперёд dev не было (merge-base с origin/dev — тот же ec9824f2,
что и на r1). Объём разбора этого раунда — единственная правка (M1) плюс всё,
до чего она дотягивается: контракт Z2M-runtime (AC13–AC16), i18n/perf-бюджет
бандла, регенерация бандла/скриншотов. Остальные 17 AC (AC1–AC12, AC17–AC20)
дельта не задевает — унаследованы из r1 без повторной проверки (раздел ниже).
Трейлеры всех трёх коммитов: Issue: #54 / User-Visible: no — верно, правка
внутренняя (фича ещё не выпущена, публичного поведения не меняет).
Закрытие раунда r1
| Находка r1 | Чем закрыта | Где это видно |
|---|---|---|
M1 — невалидный ответ Zigbee2MQTT на верном топике/transaction молча роняется (parseMessage → null), запрос висит полный 150 с таймаут и репортит timeout вместо invalid_payload (AC16) |
src/zigbee-topology-runtime.ts:201,219-222,227 — добавлен флаг responseActive, выставляемый в true только после публикации запроса (:227, сразу перед callService). Пока responseActive === false, «мусорное» сообщение на response-топике молча игнорируется (перед отправкой запроса это заведомо чужой/протухший пакет — ровно то, что и раньше корректно игнорировалось для retain===true). После responseActive === true тот же parseMessage → null немедленно вызывает responseReject?.(fail('invalid_payload')) (:220), и withTimeout (Promise.race) возвращает ошибку сразу, не дожидаясь дедлайна |
Новый юнит test/zigbee-topology.test.mjs:175-197 («Z2M runtime rejects a malformed response immediately instead of timing out») — воспроизвёл лично node --test --test-name-pattern="malformed response immediately" test/zigbee-topology.test.mjs: green, error: 'invalid_payload', cleanups: 2, elapsed < 250 ms при таймауте 500 ms. Мутант scripts/mutation-gate.mjs:5644-5654 (zigbee-topology-z2m-malformed-response-waits-for-timeout) снимает именно новую ветку (if (value === null) return; вместо ветки с responseReject) — лично прогнал node scripts/mutation-gate.mjs --id=zigbee-topology-z2m-malformed-response-waits-for-timeout: чистый прогон green, мутация красит тест, «поймано 1 из 1» |
| L1 (diagnostics.py не фильтрует settings) | не тронута — принята r1 без правки, отдельным улучшением вне #54 | не в скоупе этого раунда |
L2 (duplicate_link шире факта) |
не тронута — принята r1 как снятая находка | не в скоупе этого раунда |
| L3 (плотность диффа/стиль) | не тронута — принята r1 как снятая находка | не в скоупе этого раунда |
Регрессии от фикса нет: существующий тест Z2M runtime verifies retained bridge info, correlates transaction and cleans subscriptions (test/zigbee-topology.test.mjs:138-172)
теперь дополнительно шлёт stale-not-json на response-топик до
responseActive (в момент подписки, до отправки запроса) — прогнан, остаётся
green: подтверждает, что новая ветка не путает «протухший мусор до запроса»
(игнорировать) с «мусор после запроса» (немедленный invalid_payload).
Унаследовано из r1
Документ docs/reviews/CODE-REVIEW-54-r1.md (материал: коммит a2867df9a224,
дерево 90d12b3a6980a4537b454e8a922212ac9ce0fed3). Принято без повторной
проверки в этом раунде, так как дельта их не задевает:
- AC1–AC12, AC17–AC20 — доказаны в r1 таблицей AC (unit/smoke/mutation
witness, чтение кода) и не связаны с изменённым кодом (
refreshZ2mTopologyзатрагивает только AC13–AC16). - Defence-in-depth admin-гейта (AC3) — лично воспроизведённая r1 мутация
(
zigbee-topology-overlay-bridge.ts:17) не пересматривалась повторно. - AC5 «route не создаёт рёбер» — лично воспроизведённая r1 мутация
normalizeZhaTopologyне пересматривалась повторно, кодzigbee-topology.tsдельтой не тронут. - Приватность на уровне персистентной конфигурации (AC17), включая
backend-тест
tests_backend/test_support_package.py— кодsettings-слоя дельтой не тронут, backend Python дельтой не тронут (git diff a2867df9..HEAD -- '*.py'пуст). - Golden red на трёх кадрах общих настроек (
general-color-popover-desktop-en,settings-help-zoom-200-en-light,settings-help-zoom-200-ru-dark) — диагноз r1 (топологически ожидаемый сдвиг от нового блока Zigbee, не принят намеренно) не пересматривался: правка M1 не рендерит и не меняет разметку General Settings, только таймаут MQTT-обработчика. Остаётся предрелизным вопросом (PROCESS.md §8), не блокирует код-ревью. - Полный CI Validate на SHA
a2867df9(типы/юниты/мутанты/бандл-синхрон, 3 browser-smoke шарда, perf-smoke — success) — не перезапрашивался повторно; дельта покрыта локальными гейтами этого раунда (таблица ниже). На SHAf0ed526aзелёного Validate не найдено на момент ревью — гейты прогнаны лично. - AC18 (performance budget) — численный бенчмарк r1
(
npm run benchmark:zigbee-topology, 500 узлов/2979 связей, 5–13× запас) не перезапускался: правка M1 не в горячем пути normalize/map/hover, а в MQTT-обработчике ошибок, который выполняется только при явном ручном refresh Z2M. - AC19 (lazy boundary) — три chunk'а вне
initialViewFilesне перепроверялись поимённо; косвенно подтверждено этим раундом черезnpm run bundle:budget(см. таблицу гейтов) — initial View почти не изменился (295355 → 295365 B, +10 B), что согласуется с тем, чтоzigbee-topology-runtime.tsостаётся lazy-chunk'ом.
Как проверялось (гейты этого раунда)
npm ci не выполнялся — зависимости и Chromium уже установлены средой,
рабочая копия чистая на входе и осталась чистой после всех прогонов.
| Гейт | Команда | Результат |
|---|---|---|
| Типы | npx tsc --noEmit |
green, код 0 |
| Unit | npm test |
1906/1907 green, 1 skipped, 0 failed (включая новый тест zigbee-topology.test.mjs:175, subtest ok 1884) |
| Сборка | npm run build |
green |
| Синхронность бандлов | cmp dist/houseplan-card.js custom_components/houseplan/frontend/houseplan-card.js + npm run bundle:sync (нулевой diff после, git status --short пуст) |
идентичны байт-в-байт |
| Документация/скриншоты | node scripts/check-docs.mjs (обязателен: дельта трогает src/**) |
green — Documentation checks passed (7 files, 12 external links); sourceFingerprint уже обновлён коммитом f0ed526a, imageSha256 кадров не изменился (диф docs/images/screenshots.json — только хеши источника) |
Новый any |
node scripts/no-new-any.mjs --base a2867df9 --head HEAD |
green — 11 добавленных строк в 1 файле, новых any нет |
| Целевой мутант (новый, M1) | node scripts/mutation-gate.mjs --id=zigbee-topology-z2m-malformed-response-waits-for-timeout |
green: чистый прогон green, мутация красит тест — «поймано 1 из 1» |
| Регрессия остальных zigbee-мутантов | node scripts/mutation-gate.mjs --id=<каждый из 4 прежних id> (по одному) |
все 4 green, «поймано 1 из 1» каждый — правка M1 не сломала соседние guard'ы |
| Целевой smoke | node demo/smoke_zigbee_topology_hover.mjs |
green — 12/12 полей true |
| Выбор смоков | node scripts/smoke-select.mjs --base a2867df9 --head HEAD |
НЕОПРЕДЕЛЁННОСТЬ: 1 файл src/** изменён, 0 символов проекта на изменённых строках привязано к смокам — связь не доказана инструментом. Решение ревьюера: правка — таймер/promise-логика внутри refreshZ2mTopology, без DOM/UI поверхности; достаточным свидетелем считаю юнит + mutation witness выше и повторный прогон целевого smoke (регрессии на 12/12 нет) |
| Bundle budget | npm run bundle:budget |
green — initial View 295365 B gzip (потолок 296000±2000, запас 4635 B); +10 B к r1 — предупреждение о низком запасе (#367) не новое и не относится к #54, project-wide долг |
| Модельные инварианты | не прогонялся | дельта не трогает геометрию/layout/marker.space/open_spans/рёбра комнат — гейт неприменим, инвариант из r1 не пересматривался |
| Backend pytest | не прогонялся | дельта не трогает custom_components/**/*.py (git diff a2867df9..HEAD -- '*.py' пуст) |
| Golden / полный smoke-матрикс (221) | не прогонялся | предрелизный гейт (PROCESS.md §8); диагноз r1 по трём кадрам General Settings не изменился, правка M1 не рендерит UI |
CI Validate на точном SHA f0ed526a |
не найден зелёный прогон | подтверждено оператором задачи в постановке ревью — прогон не найден/не завершён/не success; дешёвые гейты выше прогнаны лично взамен |
Критерии приёмки — таблица (только AC, которых касается дельта)
| AC | Вердикт | Доказательство | Чем доказан | Чем краснеет |
|---|---|---|---|---|
| AC14 Z2M contract | доказан | zigbee-topology-runtime.ts:201,216-224,227,234 — transaction/timeout/retained/foreign-фильтрация не изменились по сути, добавлена только явная ветка «наш топик, наша transaction-фаза, parseMessage вернул null» |
unit zigbee-topology.test.mjs:138-197 (обе Z2M-runtime теста) |
мутанты zigbee-topology-z2m-foreign-response-accepted, zigbee-topology-z2m-subscriptions-leak, zigbee-topology-z2m-malformed-response-waits-for-timeout — все три лично прогнаны, все красят соответствующий тест |
| AC15 no implicit work | не тронут дельтой, унаследован из r1 | — | — | — |
| AC16 failures | доказан (M1 закрыта) | zigbee-topology-runtime.ts:219-222 — invalid_payload теперь достижим немедленно на верном топике/фазе, без ожидания 150 с |
unit zigbee-topology.test.mjs:175-197 (error === 'invalid_payload', elapsed < 250ms при timeout 500ms) |
лично воспроизвёл мутацию zigbee-topology-z2m-malformed-response-waits-for-timeout — тест красится, поймано 1 из 1 |
Остальные AC (AC1–AC13, AC17–AC20) — вне зоны действия дельты, наследуются из r1 без повторной проверки (раздел «Унаследовано из r1»).
Что проверено и корректно
- Правка M1 закрывает ровно то, что требовал r1:
invalid_payloadтеперь достижим сразу после начала собственного запроса, а не только теоретически существует как код ошибки. - Различение «протухший/чужой пакет до запроса» (игнорировать) и «наш топик,
наша фаза, но нераспарсиваемый payload» (немедленный
invalid_payload) подтверждено двумя тестами одновременно: новым (мусор после запроса → ошибка) и существующим изменённым (мусор до запроса → игнорируется,readyвсё равно достигается по корректному payload). - Cleanup обеих MQTT-подписок (
cleanups === 2) сохраняется и на ошибочном пути —finally-блок (:238-242) не менялся. - Регенерация
dist/**/custom_components/houseplan/frontend/**после правки синхронна во всех проверяемых копиях;docs/images/screenshots.jsonобновил толькоsourceFingerprint/sourceSha256,imageSha256не изменился — правка не затронула видимый рендер, что снимает вопрос «одно число — один источник» для скриншотов в этом раунде (фикс не производит никакой пользовательской величины). - Ни одна из четырёх ранее существовавших
zigbee-topology-*мутаций не перестала краситься — правка не задела соседние guard'ы контракта Z2M/ZHA.
Чего не проверял
- Golden-изображения — не пересматривал; диагноз r1 (три ожидаемых кадра General Settings, топологически объяснимых новым блоком Zigbee) не переоценивался, поскольку правка этого раунда не касается разметки/стилей. Остаётся предрелизным гейтом.
- Полный локальный прогон 221 браузерного смока — не делал;
smoke-selectна этой дельте вернул НЕОПРЕДЕЛЁННОСТЬ (0 символов проекта привязано к смокам на изменённой строке), решил не гонять полный матрикс: правка — чистая асинхронная логика внутри уже покрытого unit+mutation модуля, без новой DOM/UI поверхности; ограничился повторным прогоном целевогоsmoke_zigbee_topology_hover.mjs(green, без регрессии). - CI Validate на точном SHA
f0ed526a— не нашёл зелёного прогона на момент ревью (подтверждено в постановке задачи); не проверял историю запусков черезgh run listповторно после локальных гейтов. - Модельные инварианты и backend pytest — не прогонял: дельта не касается
геометрии/
layoutи не касаетсяcustom_components/**/*.py, оба гейта неприменимы по построению диффа. - Немецкий/французский текст, мультипровайдерный browser-сценарий, diagnostics.py — как и в r1, не пересматривались: дельта их не касается.
Вердикт
Единственная находка предыдущего раунда (M1) закрыта точечной, доказанной
правкой: новый юнит-тест воспроизводит ровно сценарий из находки и
показывает исчезновение дефекта (invalid_payload вместо 150-секундного
timeout), выделенный мутационный тест подтверждает, что защита реальна и
умеет падать без неё. Регрессий у соседних guard'ов и AC, которых касается
код Z2M-runtime, нет. Новых High/Medium находок в этом раунде не найдено.
Вердикт: зелёный · заход r2 · блокирующих циклов 1/4 · High: 0 · Medium: 0
Материал раунда
- Ветка:
issue/54-zigbee-topology-hover, коммитf0ed526a5924— ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет. - Дерево материала:
f65587d326738cadfbbecd0d501fad0118bf10a2git log --all --format='%H %T' | grep f65587d32673