mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-02 21:01:21 +00:00
117 lines
14 KiB
Markdown
117 lines
14 KiB
Markdown
# CODE-REVIEW-340-r1
|
||
|
||
- Issue: https://github.com/Matysh/houseplan-card/issues/340
|
||
- Этап: код-ревью (PROCESS.md §2.7)
|
||
- Заход: r1 · блокирующих циклов израсходовано 0 из 4
|
||
- SHA материала ревью: `3e437ca3ec760cad21ca3e15048487bca6ea16b7` (`git rev-parse HEAD` перед подведением итогов)
|
||
- Диапазон: `git diff origin/dev...HEAD` (base `356b187e`, 2 продуктовых/гейт-коммита: `c9981e68`, `3e437ca3`; `9239f9d3` — коммит публикации SPEC-REVIEW-340-r1, не код)
|
||
- Ветка приведена к `dev` конвейером до ревью (1 коммит dev лёг поверх, `5f4d944f → 3e437ca3`). Разбор ниже — полный, не по дельте: это первый код-ревью цикл этого issue, и после ребейза материал — другой код (§7.2).
|
||
|
||
## Скоуп задачи
|
||
|
||
Аудит C4: `ws_config_set` принимал запись без `expected_rev` поверх непустого
|
||
store, ограничиваясь warning-ом — тихая потеря чужой работы при двух клиентах.
|
||
J6 (`docs/SCOPE.md`) прямо включает multi-client live sync и optimistic
|
||
locking, спецификация уже прошла зелёное ревью (`docs/reviews/SPEC-REVIEW-340-r1.md`,
|
||
`eb8a5941`). ТЗ фиксирует полный трек (публичный WS-контракт, риск >3) и 7 AC.
|
||
Код-ревью проверяет: доказан ли каждый AC, не сломан ли позитивный путь и
|
||
задокументирован ли новый контракт там, где раньше был указан warning-only.
|
||
|
||
## Как проверялось
|
||
|
||
| Гейт | Команда | Результат |
|
||
|---|---|---|
|
||
| Typecheck | `npx tsc --noEmit` | OK, без ошибок |
|
||
| Frontend unit | `npm test` | 1462 pass, 1 skip, 0 fail |
|
||
| Build + bundle sync | `npm run build` | OK; `dist/**` в диффе отсутствует — src/**\*.ts вообще не тронут этим issue, сверка трёх копий неприменима |
|
||
| Docs fingerprint | `node scripts/check-docs.mjs` | `Documentation checks passed (7 files, 10 external links)` — запущен по правилу "трогает src/\*\*"? Нет (src не тронут), прогнан всё равно, т.к. `USER-GUIDE.md`/`.ru.md` в диффе — дёшево и подтверждает целостность якорей |
|
||
| Backend HA harness | `python3 -m pytest tests_backend -q` (пакеты `pytest-homeassistant-custom-component` и др. установлены вручную в среде ревью — их не было) | `437 passed, 1 skipped, 1 error` |
|
||
| Backend targeted | `python3 -m pytest tests_backend/test_ha_websocket.py -k "config" -v` | все 13 отобранных PASSED (включая `test_config_rev_conflict`, `test_issue_340_config_set_without_revision_is_bootstrap_only`) |
|
||
| Frontend inventory guard | `node --test test/coordinate-write-barrier-guard.test.mjs` | 2/2 pass |
|
||
| Smoke-select | `node scripts/smoke-select.mjs --base origin/dev --head HEAD` | «Исполняемого frontend-диффа нет (src/\*\*/\*.ts не тронут). Browser-smoke этим диффом не выбираются — выбирать нечего.» → 0 браузерных смоков запущено, обоснованно |
|
||
| single-source-numbers | `node --test test/single-source-numbers.test.mjs` | 3/3 pass (диффом не затронуто, прогнан для очистки совести — дёшево) |
|
||
| Тест умеет падать | вручную: применил новый `tests_backend/test_ha_websocket.py` к `origin/dev` (код без фикса) | `test_issue_340_config_set_without_revision_is_bootstrap_only` → `FAILED — assert not True` (второй клиент без rev получал success). На `HEAD` тот же тест зелёный → тест реально доказывает контракт, не тавтология |
|
||
|
||
**1 ошибка (`error`) в полном backend-прогоне — не регрессия.** Teardown-ассерт
|
||
HA test harness про «протёкший» поток `_run_safe_shutdown_loop`
|
||
(`AssertionError: assert (False or False)`, `threading._DummyThread`).
|
||
Воспроизвёл тот же error на чистом `origin/dev` (`test_config_set_purges_tombstoned_and_absent_trails_durably`,
|
||
без единой строки из этого issue) — окружение/harness-флейк, не связан с
|
||
диффом. Сам тест перед teardown — `PASSED`.
|
||
|
||
### Не проверялось, и почему
|
||
|
||
- **`npm run golden:verify`** — не нужен: `src/**` не тронут ни строкой, визуал
|
||
не меняется (спецификация §15 и сам diff это подтверждают).
|
||
- **Инварианты модели (`npm run invariants`)** — не нужны: диффом не задета
|
||
геометрия, `layout`, `marker.space`, `open_spans` или толщина стен; изменение
|
||
— исключительно ревизионный guard в `config/set` и его документация.
|
||
- **Полный набор `demo/smoke_*.mjs`** — не нужен и не выбран инструментом
|
||
(см. таблицу выше): фронтенд не менялся вообще.
|
||
- **Performance-профили** — не названы в AC, путь не производительно
|
||
чувствительный (одна дешёвая integer-проверка на уже открытом `write_lock`).
|
||
|
||
## AC — построчно
|
||
|
||
| AC | Что требуется | Проверено | Как |
|
||
|---|---|---|---|
|
||
| AC1 | Первая запись без rev на пустом store — ok; любая следующая без rev — `conflict` | ✅ | код `websocket_api.py:1293-1318` (guard внутри `write_lock`, до валидации); backend-тест `test_issue_340_config_set_without_revision_is_bootstrap_only` — bootstrap даёт `rev=1`, повторная запись без rev даёт `conflict` |
|
||
| AC2 | Missing-rev conflict не меняет config/rev/backup/event, включая semantic no-op | ✅ | код: `return` на строке 1312 происходит до `msg["config"] == data.get("config")` (no-op-детекция, 1374), до `async_save_config_state`, до `_discard_optimizer_snapshot`, до `hass.bus.async_fire`; тест явно шлёт байт-в-байт совпадающий `first_config` без rev вторым клиентом и проверяет `stored_before` равенство, `OPTIMIZE_BACKUP` неизменность, `config_events == []` |
|
||
| AC3 | Два конкурентных клиента: после первого коммита второй blind write отклонён | ✅ | тест `test_issue_340_config_set_without_revision_is_bootstrap_only`: `first_client` коммитит rev=1, `stale_client` (не видевший этот коммит) получает `conflict`; сериализация гарантирована тем же `write_lock`, которым защищены все остальные writer'ы (не новый механизм) |
|
||
| AC4 | Explicit верный rev — success/no-op; explicit устаревший rev — `conflict` | ✅ | не задето изменением (строки 1313-1318 без правок) + существующий `test_config_rev_conflict` зелёный + новый тест дополнительно проверяет retry с `expected_rev=1` → `rev=2` |
|
||
| AC5 | Ровно один `config/set` во frontend, всегда с `expected_rev` | ✅ | `scripts/coordinate-write-barrier-guard.mjs:39` требует `expected_rev: this\._cfgRev` в том же окне, что `canonicalCandidate`; production `src/houseplan-card.ts:6887-6890` содержит ровно это; `checkCoordinateWriteBarriers()` вручную прогнан → `[]`; новый unit-тест мутирует fixture (убирает `expected_rev`) и подтверждает, что guard это ловит — прогнан, зелёный |
|
||
| AC6 | Публичная документация не обещает warning-only, описывает bootstrap-only compatibility | ✅ | `docs/ARCHITECTURE.md` (таблица + новый абзац), `docs/CONFIG-COMPATIBILITY.md` (новый раздел «Revision-less config writers (#340)»), `docs/TESTING.md` (пункт B2-B5 переписан) — все три больше не говорят про warning-only; `node scripts/check-docs.mjs` зелёный |
|
||
| AC7 | UI/i18n/schema/model/export version/View/kiosk/touch и позитивный write-path не меняются | ✅ | `git diff --stat` не содержит ни одного файла `src/**`, `custom_components/**/translations/**`, `manifest.json`; позитивный путь (`expected_rev` совпадает) — тот же код, что и раньше (строки 1313 и далее не менялись); `npx tsc --noEmit`/`npm run build` зелёные |
|
||
|
||
Все 7 AC доказаны автотестом; там, где тест — дисциплина «тест умеет падать»
|
||
подтверждена явным прогоном на pre-fix коде (см. таблицу гейтов). AC6/AC7 —
|
||
частично «проверено чтением, не исполнением» в части текста документации и
|
||
отсутствия diff в защищённых путях, что и требуется для этого типа критерия.
|
||
|
||
## Находки
|
||
|
||
Нет ни одной находки уровня High или Medium. Одно наблюдение уровня **Low**,
|
||
не требующее правки:
|
||
|
||
- `docs/ARCHITECTURE.md` в строке таблицы `houseplan/config/set` убрал `?` у
|
||
`expected_rev` (было `expected_rev?`), в то время как `houseplan/layout/set`
|
||
на строке выше сохраняет `expected_rev?`. На первый взгляд несогласованность,
|
||
но это осознанная и корректная асимметрия: `layout/set` действительно
|
||
остаётся warning-only (тот самый дефект, вынесенный ревьюером ТЗ в отдельный
|
||
#356 как вне скоупа #340), а `config/set` через 3 строки ниже таблицы получает
|
||
отдельный абзац, ровно объясняющий, что поле формально `vol.Optional`, но
|
||
семантически обязательно везде, кроме bootstrap. Снято без правки — документ
|
||
точен, объяснение рядом.
|
||
|
||
## Что проверено и корректно
|
||
|
||
- Guard стоит внутри `rt.write_lock`, сразу после `async_load()`/`current_rev`,
|
||
до `CONFIG_SCHEMA`/`validate_*` и до сравнения no-op — соответствует §6.2
|
||
ТЗ построчно.
|
||
- Warning-лог после правки не содержит конфига/имён маркеров: тест явно
|
||
проверяет `"stale-secret" not in caplog.text`.
|
||
- Bootstrap-исключение безопасно и при гонке двух bootstrap-клиентов: оба
|
||
идут через тот же `write_lock`, второй уже видит `rev=1` и попадает в ветку
|
||
`conflict` — не новый примитив, тот же, что уже страхует остальные writer'ы
|
||
этого файла.
|
||
- Ошибка пользователю (`toast.conflict`) не меняется и не начинает показывать
|
||
число ревизии карточке: `src/houseplan-card.ts:7068-7070` берёт только
|
||
`e.code === 'conflict'` и показывает статичный переведённый текст, `e.message`
|
||
(с текстом ревизии для прямого WS-клиента) не парсится и не отображается —
|
||
риска «одно число, два источника» это изменение не создаёт.
|
||
- Оба changelog (`docs/CHANGELOG.md`, `docs/CHANGELOG.ru.md`) отредактированы в
|
||
том же коммите `3e437ca3`, что несёт `User-Visible: yes` — трейлер-требование
|
||
соблюдено. `docs: specify config revision enforcement` (`c9981e68`, ТЗ+specs/README)
|
||
отдельно несёт `User-Visible: no` — верно, это не продуктовое изменение.
|
||
- Никаких файлов класса D (`dist/**`, `custom_components/houseplan/frontend/**`)
|
||
в диффе нет — ожидаемо, поскольку `src/**` не менялся вовсе.
|
||
- Medium-находка спец-ревью вне скоупа (`layout/set`, тот же класс C4) уже
|
||
заведена спек-ревьюером как #356 — не задача этого код-ревью её дублировать
|
||
или чинить.
|
||
|
||
## Вывод
|
||
|
||
Зелёный. Все 7 AC доказаны и воспроизведены самостоятельно (включая
|
||
подтверждение, что новый backend-тест падает на pre-fix коде). Дешёвые и
|
||
уместные по диффу гейты — все зелёные. Находок, требующих правки, нет.
|