12 KiB
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работает; явный stalerevполучаетconflict. Доказательство: backend HA websocket test + существующая CAS-матрица. - AC3 — schema оставляет
expected_revoptional только ради 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-путь, который правка не ломает; это же неявно подтверждено зелёнымbackendjob (эти тесты входят в тот же файл и тот же прогон); 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 (backendjob, 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); полагаюсь на зелёныйbackendjob CI на точном SHA (перепровереноgh run viewнапрямую, не со слов автора). - Golden, браузерные смоки, performance — не применимо, диф не трогает визуал/фронтенд.
- Инварианты модели геометрии — не применимо, диф не трогает геометрию.
Вердикт
Зелёный. AC1–AC3 выполнены и доказаны (автотест, который умеет падать, плюс независимо перепроверенный зелёный CI на точном SHA); находок нет; скоуп не расширен; трейлеры и changelog в порядке.