15 KiB
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):Это независимое подтверждение AC5 из самого CI-лога, а не только со слов автора.ok radar-profile-source-entity-id-omitted: тест покраснел, как обязан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+ optionalpresence_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_idrange/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/zoneentity_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()несколькими строками выше и уже чинится этим диффом). Файл не тронут этим коммитом и был идентичен уже на аудиторском SHA66a64854— то есть это существующее, не внесённое этой задачей поведение. У неё есть асимметрия: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 на точном SHA81daebd7зелёный, включая полный 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— ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет. - Дерево материала:
f8ce151d796b63484e9b8b59865488102273291agit log --all --format='%H %T' | grep f8ce151d796b - Тело issue:
8b063fe89abbf48179a894847dff1b5c223f394fa4f6e98ec284302624b71437 - Вердикт конвейера:
green· High 0