mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-03 05:08:53 +00:00
165 lines
12 KiB
Markdown
165 lines
12 KiB
Markdown
# 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 в порядке.
|