18 KiB
CODE-REVIEW-333-r1
Issue: #333 · Заход: r1 · Блокирующих циклов израсходовано 0 из 2
Материал: git log --oneline origin/dev..HEAD = один коммит c29df29b
(fix: plan/optimize passes the junction gate; import stays free by design (#333)),
родитель 73406225 (= origin/dev на момент проверки).
1. Скоуп
ТЗ (тело issue, small-трек, ревизия 2, оба раунда spec-ревью зелёные —
docs/reviews/SPEC-REVIEW-333-r1.md, -r2.md) требует:
ws_plan_optimizeвызываетvalidate_junction_limits(candidate, stored, …)внутри существующей executor-функции_validate_optimize_cpu, ПОСЛЕ миграции кандидата — наследование по правилу (легаси-нарушение проходит, новое — отказjunction_limit_<rule>).- Успешный optimize обновляет
rt.junction_baselineсчётчиками кандидата. - Import/restore остаются вне гейта; докстринг
junction_limits.pyиdocs/specs/329-junction-limits.md§5 фиксируют периметр честно.
Продуктовая рамка (docs/SCOPE.md): изменение защищает контракт «карточка
отказывает записи, ломающей П1–П5» (Core user jobs: геометрия остаётся
валидной) от обхода через второй командный путь — попадает в скоуп.
User-Visible: no в трейлере коммита корректен: поведение честного клиента
не меняется (AC10 #329 уже доказывает, что оптимизация не добавляет
нарушений), меняется только реакция на крафтовый payload.
2. Как проверялось
Дешёвые гейты, на которые ссылался бриф ревью («Validate на c29df29b
завершился success»), не могут считаться подтверждёнными — см. находку
H1 ниже. Прогнал сам:
npx tsc --noEmit— чисто.npm test— 1425 passed, 0 failed, 1 skipped (без изменений от базовой линии).npm run build+cmp dist/houseplan-card.js custom_components/houseplan/frontend/houseplan-card.js— идентичны.node scripts/mutation-gate.mjs --check— реестр консистентен, новый мутантjunction-limit-optimize-unguardedзарегистрирован корректно (patch-строка совпадает с реальным кодом).- Ручная проверка «мутант умеет убивать»: применил patch мутанта
(
return validate_junction_limits(...)→return {}, None) кwebsocket_api.py, прогналpytest tests_backend/test_ha_websocket.py -k test_333_optimize_refuses_a_crafted_violation— тест красный (assert not True), рабочее дерево вернул к исходному (git statusчист). Тест умеет падать. python -m pytest tests_backend -q(полный бэкенд-набор, HA-харнес реально поднимается в этом окружении, вопреки заметке автора о песочнице) — 1 failed, 428 passed, 1 skipped, 1 error. Разбор ниже (находка H1).node scripts/smoke-select.mjs --base 73406225 --head c29df29b— «исполняемого frontend-диффа нет», браузерные смоки не выбираются (diff не трогаетsrc/**) — решение подтверждено инструментом.check-docs/ golden / invariants — не гонял, diff не трогаетsrc/**и не меняет геометрию/ссылки на неё (только добавляет вызов уже существующего валидатора на втором командном пути); обоснование ниже, §4.
Не гонял и не буду: python -m pytest tests_backend -q уже покрывает
паритет П1–П4 (test_junction_limits.py::test_parity_with_the_frontend_checks)
без дополнительных прогонов.
3. Находки
H1 (High, в скоупе). Optimize новым гейтом ломает существующий бэкенд-тест — подтверждённая регрессия
plan/optimize на конфиге, который НИКОГДА не сохранялся (пустое
config_data.get("config"), expected_config_rev: 0), теперь отказывает на
junction_limit_length, если в кандидате есть хоть один сегмент короче
20 см — потому что для «предыдущего» состояния old_counts пуст, и
существующее правило («первая запись не может прийти уже сломанной»,
junction_limits.py:415) считает любое нарушение новым.
Ровно это происходит с существующим (не тронутым этим диффом) тестом
test_plan_optimize_persists_exact_storage_roundtrip_target
(tests_backend/test_ha_websocket.py:599, фикстура
test/fixtures/optimize-storage-roundtrip.json, тест — про #248, байт-точный
роундтрип storage, а не про геометрию): пространство fine содержит комнату
6×6/12 «условных» см (специально крошечную — фикстура проверяет округление на
разных cell_cm), после миграции в ней есть стена длиной 6 см — П3, минимум
20 см.
Воспроизведение:
$ python -c "
from custom_components.houseplan.junction_limits import validate_junction_limits
import json
config = json.load(open('test/fixtures/optimize-storage-roundtrip.json'))['input']['config']
validate_junction_limits(config, None)
"
JunctionLimitError: space=fine; rule=length; subject=wall-2p5v3xgwtvsbainkhyk4; actual=6; limit=20
Доказано двумя прогонами полного pytest tests_backend/test_ha_websocket.py -k test_plan_optimize_persists_exact_storage_roundtrip_target:
- на родителе
73406225(до фикса #333): 1 passed (плюс тот же фоновый teardown-ERROR, см. ниже — он не от этого коммита); - на
c29df29b(после фикса): 1 failed (assert response["success"]→False).
Это не «недостающий тест», это диффом сломанный существующий зелёный тест —
полный набор pytest tests_backend -q на c29df29b красный
(1 failed, 428 passed). Мёртвого запуска этого набора на CI сейчас не
происходит (см. находку вне скоупа ниже), поэтому красный статус не был
замечен автоматически, но ревью обязано было его поймать — и поймало.
Фикс — решение автора; правдоподобные варианты: (а) поправить фикстуру
optimize-storage-roundtrip.json, вложив в тест уже сохранённое предыдущее
состояние с тем же нарушением (echo-паттерн AC2), раз тест не должен
проверять геометрию; (б) явно обсудить в ТЗ, должен ли plan/optimize как
первая-когда-либо запись (config_rev==0) подчиняться тому же правилу, что
и config/set — если да, чинится тест; если нет, гейту нужно исключение,
и это уже требует правки контракта, а не только кода.
Блокирует. Verdict: жёлтый.
Вне скоупа (заведено отдельным issue)
Тестируя допущение брифа «Validate на c29df29b завершился success ⇒ tsc/
npm test/npm build уже подтверждены», обнаружил, что job'ы Фронтенд: типы, юниты, мутанты, синхрон бандла и Бэкенд: pytest в Home Assistant в этом
прогоне — skipped, а не success; общий «success» рана этого не показывает.
Причина — в классификации изменённых файлов (.github/workflows/validate.yml,
job changes): BEFORE_SHA этого push ссылается на коммит, вышедший из
истории веткой при force-push (аменд предыдущего пуша той же ветки, run
33145390451, cancelled), фолбэк на merge-base origin/dev HEAD разрешился
так, что диапазон включил только два doc-коммита ревью и потерял сам код-фикс
— backend/frontend вышли false, тяжёлые job тихо пропущены.
Дефект соседний (инфраструктура CI, не продуктовый код этой задачи) и не
чинится в этой ветке. Заведён: #347 — ci: классификация файлов Validate теряет диапазон после force-push, тяжёлые job тихо skip'аются (labels:
bug, infra, P1, S1-new, ссылка на #333).
4. Что проверено и корректно
- Контракт п.1 (вызов гейта в optimize).
websocket_api.py:1609-1710:validate_junction_limits(msg["config"], config_data.get("config"))вызывается ПОСЛЕCONFIG_SCHEMA/миграции кандидата (commit_wall_segment_modelпри необходимости) и после остальных семантических валидаторов, внутри_validate_optimize_cpu— то же место в конвейере, что и вconfig/set(websocket_api.py:1343).JunctionLimitErrorв except-списке (было мёртвым) теперь действительно ловится и транслируется вconnection.send_errorтем же стабильным кодомjunction_limit_<rule>. - AC1 (крафтовый payload). Тест
test_333_optimize_refuses_a_crafted_violation_and_keeps_the_plan(tests_backend/test_ha_websocket.py:2388) прогнан — PASSED. Мутант подтверждён ручным патчем (см. §2). Хранимыеconfigиrevпобайтово не изменились после отказа — проверено ассертами теста. Layout-неизменность из формулировки AC1 тестом не проверяется явно, но гарантирована структурой кода:JunctionLimitErrorловится ДО первогоasync_save_layout_state/async_save_config_stateв функции — физически нечему меняться (см. Low-замечание ниже). - AC2 (наследование в optimize). Тест
test_333_optimize_inherits_stored_violations— PASSED: эхо-оптимизация пространства с унаследованным нарушением угла проходит. Согласуется с #329 AC10 (легаси-оптимизация не создаёт новых нарушений). - AC3 (обновление кэша). Тест
test_333_optimize_refreshes_the_junction_baseline_cache— PASSED: черезrecording-обёртку надvalidate_junction_limitsподтверждено, что следующийconfig/setвызывается сbaseline_countsиз кэша (rt.junction_baseline), не пересчитываяprevious.websocket_api.py:1783(rt.junction_baseline = (int(new_config_rev), optimize_counts or {})) — симметрично строке 1396 вconfig/set. - AC4 (докстринг/спека). Прочитал оба текста целиком:
junction_limits.py:1-15больше не обещает «hostile client cannot post» без оговорок — явно называет периметр (config/setиplan/optimize) и сознательно исключённый import/restore, с обоснованием (§3 #329: restore не блокируется никогда).docs/specs/329-junction-limits.md§5 (новый абзац, строки 127-134) — тот же периметр и тот же трейд-офф, ссылка на решение владельца 2026-08-28. Текст соответствует коду, догадок, выданных за решённое поведение, не нашёл. - Паритет П1–П4 и остальной бэкенд.
test_parity_with_the_frontend_checksи все прочие 424 тестаtests_backend(кроме H1) прошли без изменений. - Трейлеры коммита.
Issue: #333,User-Visible: no— оба на месте, соответствуют характеру изменения (нет пользовательского поведения, видимого честному клиенту). - Реестр мутаций.
--checkзелёный, новый мутант описан честно (becauseуказывает на реальный риск — крафтовый optimize-payload легализует нарушение для будущихconfig/set).
5. Чего не проверял и почему
- check-docs, golden:verify, browser-смоки — diff не трогает
src/**вообще (толькоcustom_components/**/*.py,docs/specs/*.md,scripts/mutation-gate.mjs,tests_backend/*.py);smoke-select.mjsподтвердил инструментом «исполняемого frontend-диффа нет». Рендер и геометрия не меняются — гейты не относятся к этому diff. npm run invariants— diff не меняет геометрию, рёбра комнат, записи толщины,layout,marker.spaceилиopen_spans; добавляет только вызов уже существующего Python-валидатора на втором командном пути. Инварианты модели тут не про что проверять.- performance-профили — не названы в AC; #330 §4.1 уже вынесла всю
дорогую цепочку (включая
validate_junction_limits) в executor, это не трогается. - Полный
node scripts/mutation-gate.mjs(все мутанты) — избыточен: диффу соответствует ровно один новый мутант, он проверен точечно (ручной патч + прогон целевого теста), это дешевле и доказательнее, чем гонять весь реестр. - HA-харнес — вопреки заметке автора («локальная песочница… не
поднимает»), в ЭТОЙ ревью-сессии
pytest-homeassistant-custom-componentустановился и харнес поднялся; воспользовался этим и прогнал реальные прогоны, а не читал код вслепую. Один постороннийERRORна teardown каждого HA-теста (assert isinstance(thread, threading._DummyThread) or thread.name.startswith("waitpid-")) — известная нестыковка версииpytest-homeassistant-custom-componentс Python 3.12 (первый поток теста назван иначе), воспроизводится одинаково и на родителе73406225, и наc29df29b, до и после патча мутанта — не связан с этим диффом, тестовые тела он не затрагивает (различаюFAILED/PASSEDв основном теле отERRORв teardown).
6. Вердикт
Контракт ТЗ реализован технически точно (AC1–AC4 каждый по отдельности доказан тестом, который умеет падать), но диффом ломается несвязанный существующий тест — реальная, воспроизведённая регрессия уровня High. Правки ТЗ не требуется (если автор выберет чинить фикстуру/тест), либо требуется ревизия контракта (если выберет явно исключить «первую запись» из-под гейта optimize) — в обоих случаях нужен повторный код-ревью цикл.
Вердикт: жёлтый · заход r1 · блокирующих циклов 0/2 · High: 1 · Medium: 1 → #347