13 KiB
CODE-REVIEW-319-r1
Issue: #319 — «beta.3: бэкенд навсегда отклоняет все структурные записи после v8→v9 миграции с осиротевшим open_span»
Трек: small (ТЗ в теле issue, ревью ТЗ — комментарий, зелёное, r1)
Ветка: issue/319-model-upgrade-not-outdated
SHA материала ревью: 3ab6fa9f8a1ef1c35730c3fb03870f69c3e50be6 (единственный коммит поверх origin/dev)
Заход: r1 · блокирующих циклов израсходовано 0 из 2 (лёгкий трек — лимит 2)
Скоуп
Диапазон git diff origin/dev...HEAD, 6 файлов:
custom_components/houseplan/validation.py— единственная правка контракта: вvalidate_wall_model_transitionусловие охраны «unchanged wall catalogue» сужено сold_model >= 8 and new_model >= 8доold_model >= 8 and 8 <= new_model <= old_model.docs/CHANGELOG.md/docs/CHANGELOG.ru.md— запись об исправлении, оба файла в том же коммите (User-Visible: yes).test/fixtures/319-orphan-span-migration.json— параstored(v8, с осиротевшимopen_span, продукт писателя v1.68.0-beta.2) /sent(v9, продукт текущей миграции).test/wall-segment-model.test.mjs— тест, пинящий фикстуру к текущей JS-миграции побайтово (защита фикстуры от дрейфа).tests_backend/test_wall_segment_model.py— регрессионный тестtest_first_write_of_a_newer_model_is_not_outdated_even_without_catalog_changeна AC1–AC3.
Продуктовый код класса A — только Python (validation.py); src/** не
тронут. Соответствует J6 docs/SCOPE.md («Keep the plan true as the home
evolves» — структурные записи и апгрейд модели стен не должны намертво
блокировать редактирование).
Как проверялось
Материал — origin/dev...HEAD, ручного тестирования нет, поэтому все AC
проверены исполнением, не только чтением.
| Гейт | Статус | Результат |
|---|---|---|
npx tsc --noEmit |
прогнан | чисто, без вывода |
npm test |
прогнан | 1346 pass, 0 fail, 1 skipped (skip — issue 281 private exact fixture, не связан с #319, зависит от отсутствующей приватной фикстуры) |
npm run build + сверка dist/ vs custom_components/houseplan/frontend/ |
прогнан | сборка чистая, cmp — файлы идентичны |
node scripts/smoke-select.mjs --base origin/dev --head HEAD |
прогнан | «Исполняемого frontend-диффа нет (src/**/*.ts не тронут). Browser-smoke этим диффом не выбираются». Смоки не запускались — выбирать нечего, а не пропуск |
node scripts/check-docs.mjs |
не прогнан | не требуется: диффом не тронут src/** |
npm run golden:verify |
не прогнан | не требуется: видимый рендер/геометрия не меняются, правка — только серверная валидация допустимости записи |
node scripts/model-invariants.mjs |
не прогнан | не требуется: диффом не тронуты рёбра/layout/marker.space/open_spans-производство — только guard, решающий принять/отклонить уже готовую запись |
python -m pytest tests_backend -q |
прогнан (полный набор, включая test_ha_*) |
окружение ревью по умолчанию без homeassistant/.venv-backend; установил pytest-homeassistant-custom-component+home-assistant-frontend через pip, чтобы получить реальный, а не «молчаливо пропущенный» результат. Итог: 405 passed, 1 skipped, 1 error. Ошибка — test_ha_upload.py::test_upload_ok, воспроизводится дважды подряд и не по файлу диффа: teardown-ассерт pytest-homeassistant-custom-component о фоновом потоке (_run_safe_shutdown_loop не _DummyThread/waitpid-), сам тест passed, падает только фикстура очистки. Не относится к validation.py/wall_segment_model.py, файл test_ha_upload.py диффом не тронут — квирк среды ad hoc-инсталляции, не регрессия задачи |
| Performance-профили | не требуется | не названы в AC, диффом не тронуты чувствительные к перфу пути |
Тест умеет падать (проверено, не заявлено). Патч validation.py
временно откачен (git apply -R), новый pytest-тест
test_first_write_of_a_newer_model_is_not_outdated_even_without_catalog_change
перезапущен изолированно:
FAILED …/test_wall_segment_model.py::test_first_write_of_a_newer_model_is_not_outdated_even_without_catalog_change
custom_components.houseplan.validation.WallModelClientOutdatedError: stored model=8; unchanged wall catalogue
— ровно та ошибка, что описана в симптоме issue. Патч восстановлен
(git apply), дерево чистое (git status --porcelain пусто), тест снова
зелёный (17 passed).
AC — доказательство
- AC1 (чистая миграционная запись v8→v9 проходит). Доказано исполнением:
validate_wall_model_transition(sent, stored)в тесте не бросает, тест красный до фикса (см. выше), зелёный после. - AC2 (то же с независимым черновиком
room_drafts). Доказано тем же тестом (with_draft), тем же красный/зелёный циклом. - AC3 (эхо той же версии 9→9 с изменёнными контурами и неизменным
каталогом по-прежнему отклоняется). Доказано исполнением: тот же тест,
блок
echoed, ловитWallModelClientOutdatedErrorс сообщениемunchanged wall catalogue; плюс существующийtest_stale_client_echoing_v8_catalog_gets_the_named_errorзелёный (первая ветвьifне тронута правкой — граница добавлена только условием<= old_model, которое не меняет поведение приnew_model == old_model). - AC4 (ветка «легаси-клиент»,
old_model >= 8 and new_model < 8, не изменена). Проверено чтением и исполнением: правка не касается первогоif-блока (строки 181–192 не тронуты диффом), существующие тестыtest_stale_client_round_trip_is_hydrated_but_structural_change_is_rejectedи смежные — зелёные в полном прогоне.
Продуктовое рассуждение (не только AC)
Ослабление гарда — не дыра в целостности данных. Проверено чтением
вызывающего кода, не только заявлением автора: в websocket_api.py
(строки 1324–1325 и 1644–1662) validate_wall_model_transition вызывается
перед CONFIG_SCHEMA(...) в обоих обработчиках сохранения, и
CONFIG_SCHEMA всегда прогоняет _config_wall_segment_invariants
(validation.py:1772) независимо от того, что решил guard в этой задаче.
Этот независимый инвариант жёстко сверяет room.poly/wall_ids с
wall_segments по ключу ребра, требует id у сегментов черновика и не
допускает open_spans/open_to при model_version >= 9. Значит, даже
теоретически «сломанная» запись с поднятой версией и неизменным каталогом
не проскочит мимо структурной проверки — она либо валидна, либо отклонена
CONFIG_SCHEMA отдельно от гарда staleness.
Предпосылка «устаревший клиент не может поднять model_version» опирается
на то, что model_version в записи — не пользовательский ввод, а константа
сборки клиента (WALL_SEGMENT_MODEL_VERSION/PLAN_MODEL_VERSION, сейчас
9 и в src/wall-segment-model.ts, и в
custom_components/houseplan/wall_segment_model.py, и в
custom_components/houseplan/const.py) — проверено чтением исходников,
согласуется с тем, что уже сверял ревью ТЗ.
Найдено и корректно
- Правка минимальна и буквально реализует контракт ТЗ: «гард применяется
только при
new_model <= old_model» — ровно8 <= new_model <= old_modelв коде. - Комментарий у изменённой строки объясняет постпродукт-причину (#319) не тривиальным образом — обоснован по правилам написания комментариев.
- Фикстура
test/fixtures/319-orphan-span-migration.json— не рукописная:storedподтверждённо продукт реального писателя v1.68.0-beta.2,sent— реального текущего мигратора; JS-тестthe #319 pair fixture matches the current initial migration byte for byteзащищает её от рассинхронизации, пересчитываяsentизstoredчерез боевойcommitWallSegmentModel. - Трейлеры коммита:
Issue: #319,User-Visible: yes; оба changelog правлены в этом же коммите — соответствует правилу. - «Одно число — один источник»: правка не вводит новую отображаемую пользователю величину (это guard принятия/отклонения записи, а не вычисляемое значение на экране) — раздел неприменим по существу.
- Ветка легаси-клиента (
new_model < 8) не затронута — предотвращает регрессию по «правка ломает ранее принятый AC» (#102-класс риска).
Чего не проверял
check-docs.mjs,golden:verify,model-invariants.mjs, performance-профили и browser-смоки — не запускал: диффом не тронутsrc/**, видимая геометрия/рендер и чувствительные к перфу пути; инструмент выбора смоков подтвердил «выбирать нечего».- HA-харнесс (
test_ha_*.py) прогнан не на пиновком CI-образе, а на ad hocpip installв среде ревью — как и предупреждаетAGENTS.md, «канон — Linux CI»; здесь это использовано только для получения неслепого, исполняемого результата вместо «not verified», а не как замена CI-гейта. Один teardown-квирк (test_ha_upload.py, не файл диффа) зафиксирован и не отнесён на счёт задачи. - CI-прогон ветки (
https://github.com/Matysh/houseplan-card/actions/runs/32984859492), на который ссылается автор, отдельно не открывал — верификация в этом документе получена независимым локальным исполнением на том же SHA.
Вердикт
High: 0, Medium: 0. Все AC1–AC4 доказаны исполнением (не только заявлением), тест-регрессия умеет падать (проверено откатом патча), ослабление гарда не открывает дыру в целостности — подтверждено чтением независимого инварианта схемы и его места вызова. Зелёный.