Files
houseplan-card/docs/reviews/CODE-REVIEW-356-r1.md
T
2026-08-28 14:30:04 +00:00

12 KiB
Raw Blame History

CODE-REVIEW-356-r1

Issue: #356 — «layout/set без expected_rev тоже перезаписывает молча — тот же класс C4, что и config/set (#340)» Трек: trivial (S1→S2→S5→S6→S7, ТЗ в теле issue, без spec-review-документа) Заход: r1 · блокирующих циклов израсходовано 0/2 (лимит trivial-трека — 2) SHA материала: 24c9a169ace86e8de502143b51874690828e0994 (единственный коммит в диапазоне origin/dev..HEAD, git rev-parse HEAD совпадает)

Скоуп

Диапазон git diff origin/dev...HEAD:

custom_components/houseplan/websocket_api.py | 22 ++++++--
docs/ARCHITECTURE.md                         |  2 +-
docs/CHANGELOG.md                            |  5 ++
docs/CHANGELOG.ru.md                         |  5 ++
docs/TESTING.md                              | 10 ++--
tests_backend/test_ha_websocket.py           | 80 ++++++++++++++++++++++++++++

Чисто бэкенд + документация; src/** не тронут вовсе (фронтенд не вызывает layout/set — использует point-wise layout/update, см. «Проверено» ниже). Ветка соответствует issue (issue/356-layout-revision), коммит несёт трейлеры Issue: #356 и User-Visible: yes, оба changelog правлены в том же коммите.

AC из тела issue (trivial-трек, ТЗ = issue):

  • AC1 — layout/set без expected_rev при current rev > 0 возвращает conflict до canonical no-op/записи; store/rev/служебные поля не меняются, houseplan_layout_updated не отправляется. Доказательство: backend HA websocket test.
  • AC2 — bootstrap rev=0 без expected_rev остаётся допустимым и создаёт rev=1; явный совпадающий expected_rev работает; явный stale rev получает conflict. Доказательство: backend HA websocket test + существующая CAS-матрица.
  • AC3 — schema оставляет expected_rev optional только ради bootstrap и стабильного доменного conflict; документация не обещает больше compatibility blind overwrite; EN/RU changelog сообщает о защите от тихой потери позиций. Доказательство: ревью кода и docs checks.

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

Дешёвые гейты. Подтверждённый в задании прогон Validate на этом же SHA (https://github.com/Matysh/houseplan-card/actions/runs/33179882646) я перепроверил напрямую (gh run view 33179882646 --json jobs): conclusion: success, headSha совпадает с HEAD. Разбивка по job:

Job Результат
changes (классификация) success
reuse success
предполётные (docs/provenance/process-gate) success
hacs success
hassfest success
frontend (typecheck/test/build/bundle-sync) skipped
backend (pytest в Home Assistant) success
golden, smoke, performance_smoke skipped

Важное уточнение к тексту задания: npx tsc --noEmit / npm test / npm run build в этом прогоне не «подтверждены зелёным», а корректно пропущены job'ом changes — путевой фильтр frontend требует изменений в src|demo|test|dist|custom_components/houseplan/frontend|package(-lock).json| rollup.config|tsconfig, и диапазон коммита ни одного из них не касается (проверено: git diff origin/dev...HEAD --stat не содержит src/**, и grep -n "layout/set" src/ показывает единственное упоминание — в комментарии, подтверждающем, что фронтенд ходит через layout/update, а не layout/set). Поэтому пропуск этих гейтов не тихий — они здесь не применимы к диффу; гонять их вручную ради нулевого покрытия диффа не стал.

backend job — единственный релевантный этому диффу тяжёлый гейт — реально прогнан и зелёный на точном SHA; это и есть исполнение нового теста test_issue_356_layout_set_without_revision_is_bootstrap_only в CI на Linux с настоящим HA-харнессом. Локальный прогон pytest в этой ревью-среде недоступен (python3 -c "import homeassistant" → ModuleNotFoundError, .venv-backend отсутствует) — ожидаемое ограничение окружения ревьюера, см. PROCESS.md «Backend»/AGENTS.md «Environments».

Не прогонялось и почему:

  • npm run golden:verify, браузерные смоки, node scripts/check-docs.mjs — диф не трогает src/**/визуал, не применимо;
  • npm run invariants -- --config … — диф не трогает геометрию комнат, рёбра, marker.space, open_spans; это чисто транспортный CAS-гейт над уже существующим layout-документом, модель не меняется;
  • «одно число — один источник» — в диффе нет новой пользовательски видимой величины (это код отказа записи, а не отображаемое значение), не применимо.

Прочитано вручную (доказательство «чтением, не исполнением» там, где автотеста нет — AC3 и общая согласованность):

  • полный текст ws_layout_set (websocket_api.py:559-616) построчно, сверка порядка проверок с симметричным ws_config_set (audit C4/#340, websocket_api.py:1286-1334) — паттерн идентичен: warning без содержимого → send_error("conflict") → return, до вычисления _live_layout/no-op/записи;
  • все остальные точки записи layout (grep async_save_layout_state) — websocket_api.py:130,208,226,679,742,778,1096,1787,1803,1871,1882 и __init__.py:160,169,223 — вызывают async_save_layout_state напрямую, не через ws_layout_set; правка их не касается, что соответствует заявленному «не-цели: layout/update, layout/delete, import/optimize не меняются»;
  • существующие вызовы houseplan/layout/set без expected_rev в tests_backend/test_ha_websocket.py (строки 136, 214, 249, 320 и др.) — во всех случаях это первый layout-write в своём тесте (rev=0 на входе), то есть законный bootstrap-путь, который правка не ломает; это же неявно подтверждено зелёным backend job (эти тесты входят в тот же файл и тот же прогон);
  • docs/ARCHITECTURE.md, docs/TESTING.md, docs/CHANGELOG(.ru).md — текст соответствует фактическому поведению после правки, не обещает больше «blind overwrite для совместимости».

Находки

Не найдено. High: 0, Medium: 0.

Разобрано отдельно и не сочтено находкой: тест «An equal body is still a write attempt and must not bypass the CAS guard» (test_ha_websocket.py:602-611) — хорошее покрытие ровно того случая, где conflict-проверка обязана стоять до сравнения с canonical no-op, а не полагаться на то, что no-op сам по себе безопасен без ревизии. Порядок в коде это подтверждает (см. выше).

Что проверено и корректно

  • AC1 — доказано автотестом (test_issue_356_layout_set_without_revision_is_bootstrap_only, ветка «stale_client without expected_rev»), тест умеет падать: без правки старый код проверяет только "expected_rev" in msg (ложно) и не возвращает conflict, дошёл бы до записи нового rev/события — тогда assert not rejected["success"] и assert layout_events == [] были бы красными. Подтверждено также прогоном на CI (backend job, success, тот же SHA).
  • AC2 — доказано тем же тестом (bootstrap-ветка rev=0→1, явный совпадающий expected_rev=1 в конце) плюс не сломанной существующей CAS-матрицей (explicit stale rev в других тестах файла, не тронутых диффом).
  • AC3 — проверено чтением: docstring, ARCHITECTURE.md (табличная строка layout/set), TESTING.md (пункт про backend hardening) и оба changelog синхронно описывают новое поведение; формулировка симметрична описанию #340 для config/set.
  • Утечки данных в лог нет: тест явно проверяет "stale-secret" not in caplog.text.
  • Область изменения не задевает фронтенд, layout/update, layout/delete, import/optimize, что соответствует заявленным «не-целям» задачи и риск-оценке аналитика (P3, риск 3/10, ограничен старыми/сторонними клиентами).
  • Трейлеры и changelog-дисциплина в порядке (Issue: #356, User-Visible: yes, правки в обоих changelog в том же коммите).

Чего не проверял

  • npx tsc --noEmit / npm test / npm run build — не прогонял вручную; диф не содержит ни одного файла из frontend-фильтра, гейт CI сам корректно их пропустил (see «Как проверялось»), перегонять вслепую смысла не было бы: проверяли бы код, который не менялся.
  • Полный python -m pytest tests_backend -q локально — недоступен в этой среде (нет homeassistant/.venv-backend); полагаюсь на зелёный backend job CI на точном SHA (перепроверено gh run view напрямую, не со слов автора).
  • Golden, браузерные смоки, performance — не применимо, диф не трогает визуал/фронтенд.
  • Инварианты модели геометрии — не применимо, диф не трогает геометрию.

Вердикт

Зелёный. AC1–AC3 выполнены и доказаны (автотест, который умеет падать, плюс независимо перепроверенный зелёный CI на точном SHA); находок нет; скоуп не расширен; трейлеры и changelog в порядке.