Files
houseplan-card/docs/reviews/CODE-REVIEW-545-r1.md
T
2026-09-12 20:36:47 +00:00

15 KiB
Raw Blame History

CODE-REVIEW — issue #545, заход r1

Материал: 81daebd788c532f9d0e321010ba471282ae076d1 (рабочая копия на нём же, git fetch/checkout не выполнялись). Диапазон origin/dev..HEAD — один коммит 81daebd7 fix: complete radar source inventory (#545).

Скоуп диффа

custom_components/houseplan/radar_validation.py |  31 ++-
docs/ARCHITECTURE.md                            |   7 +
docs/CHANGELOG.md                               |   5 +
docs/CHANGELOG.ru.md                            |   5 +
docs/TESTING.md                                 |  12 +
scripts/mutation-gate.mjs                       |  17 ++
tests_backend/test_ha_radar.py                  |  64 ++++-
tests_backend/test_ha_radar_websocket.py        | 170 ++++++++++-
tests_backend/test_radar_validation.py          |  62 ++++
9 files changed, 358 insertions(+), 15 deletions(-)

radar.py и radar_websocket.py не менялись — оба уже читали общий inventory через radar_source_entity_ids()/coordinator.source_ids(), поэтому исправление одного extractor'а автоматически чинит подписки coordinator, setup-подписку и ACL. Соответствует «Затронутые файлы» ТЗ («radar.py/radar_websocket.py — только если потребуется», и не потребовалось).

Как проверялось

Прочитан весь диффнутый код построчно (radar_validation.py, mutation-gate.mjs, три backend test-модуля, доки), плюс непосредственно задействованные, но не изменённые call sites в radar.py (source_ids(), _resubscribe_sources) и radar_websocket.py (_can_read, ws_radar_subscribe, ws_radar_setup_inspect, ws_radar_setup_subscribe), чтобы убедиться, что фикс действительно долетает до подписок и ACL, а не только до валидации.

Дешёвые гейты уже подтверждены Validate на этом SHA (ссылка в хендоффе, run 34716865575) — не перегонялись:

  • Фронтенд: типы, юниты, мутанты, синхрон бандла → success (покрывает tsc --noEmit, npm test, npm run build, no-new-any).
  • Бэкенд: pytest в Home Assistant → success (полный tests_backend, а не urlезанный набор без HA — так что backend действительно доказан, а не тихо пропущен).
  • Hassfest, HACS → success.
  • Мутанты по диффу (все 6 шардов) → success. Проверил лог шарда 6/6 напрямую (gh run view --job 103616172006 --log):
    ok   radar-profile-source-entity-id-omitted: тест покраснел, как обязан
    
    Это независимое подтверждение AC5 из самого CI-лога, а не только со слов автора.
  • Golden, Смоки, Перф-смок → skipped (тяжёлые гейты, диффом не задеты src/**/рендер — корректно не входят в обычный push).

Не прогонял сам: npx tsc --noEmit, npm test, npm run build, pytest tests_backend, mutation-gate — все уже зелёные на этой SHA (см. выше), перегонка добавила бы только время. check-docs.mjs и golden не нужны: диффом не тронут ни один файл src/**, экран/рендер не меняется. npm run invariants не нужен: геометрия (layout, marker.space, open_spans, толщина стен) диффом не тронута — меняется только radar source inventory, HA-подписки и ACL. Браузерные smoke не нужны по той же причине (нет фронтенд-файлов в диффе; smoke-select.mjs не запускал — diff не содержит ни одного файла, который мог бы задеть выборку по символам, а тема задачи (backend radar ACL) не пересекается с рендер-путём).

Проверка по AC

  • AC1 (exact profile inventory). _PROFILE_SOURCE_ROLES в radar_validation.py:23-32 — явная таблица profile → (group, roles), заменяющая эвристику key.endswith('_entity'). Значения совпадают построчно с контрактом ТЗ п.3: range_v1 → ("entity_id", "presence_entity"), zones_v1 → ("entity_id",), слоты — x/y/distance/ angle_entity + optional presence_entity, presence_v1 — без вложенных ролей. Параметризованный тест test_stage1_source_inventory_is_exact_for_each_profile (test_radar_validation.py) доказывает точный set для всех шести profiles, включая контрольный future_entity-мусор внутри common и вложенной группы — он остаётся вне set. Отдельный тест на неизвестный profile: "future_v2" возвращает set() целиком (соответствует контракту п.1: неизвестный профиль не даёт даже общих ролей). Доказано автотестом, тест умеет падать (см. AC5 mutation witness — красный на урезанном контракте).

  • AC2 (setup live-update range/zone). test_setup_subscribes_to_range_and_zone_primary_sources в test_ha_radar_websocket.py гоняет реальный ws_radar_setup_subscribe (мокаются только _coordinator и async_track_state_*/may_write, как и в остальных тестах файла — то есть production-shaped путь). Для range_v1/zones_v1 с единственным entity_id-источником (без occupancy/count/presence) проверяет, что оба HA-стрима (state_report, state_change) подписаны именно на этот entity, и что вызов callback приводит к новому send_result через ограниченный 1/MAX_FRAME_HZ коалесер — без переоткрытия. Проверены и saved, и draft путь в одном тесте. AC выполнен.

  • AC3 (saved coordinator + lifecycle). test_range_and_zone_primary_source_subscriptions_rebind_and_teardown в test_ha_radar.py — реальный RadarCoordinator, монkeypatch только на async_track_state_report_event/async_track_state_change_event. Проверяет: coordinator.source_ids("radar") содержит entity_id range/zone; при rebind (смена документа + async_refresh) старые подписки полностью отписаны (cleaned), новые — ровно на новый entity; teardown() снимает оставшиеся. AC выполнен.

  • AC4 (ACL fail-closed). test_range_and_zone_primary_sources_are_permission_checked_fail_closed использует записывающий _Permissions.checked (пер-entity, а не булев allow/deny — именно то, что в «Рисках» ТЗ названо необходимым, чтобы тест не проходил по общему allow/deny). Deny выставлен точно на range/zone entity_id; проверены ws_radar_setup_inspect (saved и draft) и ws_radar_subscribe (live) — во всех трёх ветках получен source_restricted/health: restricted с пустыми targets/ranges/zones (секретные secret-range/secret-zone из фикстуры frames_for_space не утекают). AC выполнен.

  • AC5 (mutation witness). Новый мутант radar-profile-source-entity-id-omitted в mutation-gate.mjs вырезает entity_id из обеих ролей (range_v1, zones_v1) и указывает guard'ом именно новый параметризованный тест. Проверено чтением: удаление роли ломает assertion точного set в тесте → тест обязан покраснеть. Проверено исполнением — CI-лог шарда 6/6 (Мутанты по диффу, run 34716865575) прямо печатает ok radar-profile-source-entity-id-omitted: тест покраснел, как обязан. Двойное (чтением + исполнением) подтверждение.

  • AC6 (bounded runtime). radar.py не менялся: coordinator по-прежнему регистрирует ровно два агрегированных listener'а (_resubscribe_sources, строки не тронуты), MAX_FRAME_HZ не менялся. Setup-подписка в radar_websocket.py — тоже два listener'а на marker (не менялась). Никаких polling/timer на source не добавлено — сам fix расширяет только set уже читаемых entity, не механизм подписки. Проверено чтением (файлы, реализующие лимиты, не в диффе).

  • AC7 (совместимость и артефакты). Схема marker.radar не менялась (только внутренний Python-словарь ролей). docs/ARCHITECTURE.md описывает единый inventory-контракт как основу для listeners/ACL; docs/TESTING.md называет три guard-модуля и мутанта; оба changelog (CHANGELOG.md, CHANGELOG.ru.md) обновлены в том же коммите, без раскрытия security-чувствительных деталей (совпадает с «Release Metadata» ТЗ). Frontend/i18n не тронуты. Трейлеры коммита: Issue: #545, User-Visible: yes — оба changelog присутствуют, условие выполнено.

Что проверено чтением, но не отдельным тестом

  • _immediate_source_ids в radar.py:197-213 — отдельная, не происходящая из нового _PROFILE_SOURCE_ROLES, ручная классификация «мгновенных» источников (используется только для решения immediate-vs-coalesced публикации, не для полноты подписки/ACL — та берётся из radar_source_entity_ids() несколькими строками выше и уже чинится этим диффом). Файл не тронут этим коммитом и был идентичен уже на аудиторском SHA 66a64854 — то есть это существующее, не внесённое этой задачей поведение. У неё есть асимметрия: ranges[].presence_entity считается «мгновенным», а сам ranges[].entity_id — нет (zones[].entity_id — считается). ТЗ прямо разрешает оставить отдельную политику для immediate-классификации («Принято предположительно», п.2) и явно требует сохранить существующую семантику немедленных/коалесцированных обновлений (контракт п.9) — эта асимметрия под неё подпадает, а не под AC2/AC3 (те про полноту подписки/ACL, доказанную тестами выше). Не блокирует; фиксирую как наблюдение, не находку — правка вне скоупа этой задачи и не меняет ни completeness инвентаря, ни ACL.

Находки

Нет ни High, ни Medium, ни Low. Диф точный, доказательства по каждому AC — автотестом с проверкой «умеет падать» (AC1, AC5 — двойное подтверждение чтением и исполнением CI), либо чтением production-shaped теста, который реально гоняет проверяемый код (AC2-AC4, AC7), либо чтением незатронутого кода с обоснованием, почему он не должен был измениться (AC6, наблюдение по _immediate_source_ids).

Не проверял и почему

  • npx tsc --noEmit, npm test, npm run build, pytest tests_backend, mutation-gate.mjs --check/--changed — не перегонял: Validate на точном SHA 81daebd7 зелёный, включая полный backend HA harness и все 6 шардов diff-мутантов (проверено по логу, не только по статусу).
  • golden:verify, demo/smoke_*.mjs, performance-профили — не запускал: диффом не тронут ни один файл src/** и ни один рендер-путь; Validate корректно пропустил (skipped) golden/smoke/perf как тяжёлые гейты для обычного пуша без Release:-трейлера.
  • npm run invariants — не запускал: диффом не тронуты геометрия, layout, marker.space, open_spans или толщина стен.
  • Ручного тестирования в UI не проводил (задача backend-only, UI не меняется по контракту ТЗ и подтверждено отсутствием файлов src/** в диффе).

Вердикт

Зелёный. Все AC1–AC7 доказаны либо исполняемым тестом с подтверждённой способностью падать, либо чтением незатронутого кода с явным обоснованием. Единственное наблюдение (_immediate_source_ids асимметрия) — не находка: предсуществующий код вне скоупа, явно разрешённый ТЗ оставить как есть.


Материал раунда

  • Ветка: issue/545-radar-source-inventory, коммит 81daebd788c5 — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
  • Дерево материала: f8ce151d796b63484e9b8b59865488102273291a
    git log --all --format='%H %T' | grep f8ce151d796b
    
  • Тело issue: 8b063fe89abbf48179a894847dff1b5c223f394fa4f6e98ec284302624b71437
  • Вердикт конвейера: green · High 0