Files
2026-10-01 12:12:15 +00:00

15 KiB
Raw Permalink Blame History

CODE-REVIEW-757-r1

Issue: #757 · Этап: code · Трек: show · Заход: r1 · блокирующих циклов 0/2 Материал: 3398b69d635e775979c31f6f9ad128913919a313 (одна коммита поверх origin/dev 7ce3f662)

Скоуп

Баг из #731: _renderBody на каждой отрисовке звал _ensureEditorRuntime() / _ensureOnboardingRuntime(), если поверхность (диалог масштаба киоска, мастер импорта этажей, диалог, воскрешённый тёплым ре-маунтом) просила рантайм, а его не было. После не-терминальной сетевой ошибки загрузчик возвращался в idle, и следующая же отрисовка — не действие пользователя — снова читалась загрузчиком как намерение и запускала новый цикл загрузки с тостом каждые ≈3,5 с, пока поверхность открыта. Это противоречит контракту #353 («повтор только по следующему явному намерению, фоновых повторов нет»).

Правка (класс A, src/editor-runtime-loader.ts + 2 строки src/houseplan-card.ts): EditorRuntimeLoader.ensure(intent) различает 'explicit' (по умолчанию, действие пользователя или открывающий метод) и 'reconcile' (сверочный вызов из отрисовки). Сверка запускает первый цикл, который нужен поверхности, но после не-терминальной ошибки возвращает false без загрузки, пока явный вызов не начнёт новый цикл. Терминальная ошибка (несовпадение отпечатка), ready и полёт (_inFlight) не меняются. Обслуживает J6 (карточка остаётся верна состоянию рантайма без ложной активности) и общий принцип «View/редактор не делают ничего без действия пользователя» (UX-MODES).

Критерии §5 (трек show, маршрут fix): сложность низкая (один класс, булев флаг состояния + параметр), одна поверхность (EditorRuntimeLoader и два места его сверочного вызова в одном методе), миграции конфига нет, нового UX-контракта нет — исправляет уже описанный контракт #353, перфа и touch не затронуты, ожидаемое поведение зафиксировано документально. Задача проходит критерии трека show.

Как проверялось

Прочитан весь дифф построчно: src/editor-runtime-loader.ts, src/houseplan-card.ts (2 изменённые строки), test/editor-runtime-loader.test.mjs, demo/smoke_lazy_editor_chunk.mjs, scripts/mutation-registry.mjs, docs/testing-notes/mutation-browser-guards.md, оба CHANGELOG.

Прослежены все вызовы _ensureEditorRuntime() / _ensureOnboardingRuntime() (строки 745, 9440, 9660, 9666, 10120, 10131, 10139 src/houseplan-card.ts) — подтверждено, что все они остаются явными (клик по вкладке, открывающие методы диалогов, _reviveMoonStatus на тёплом ре-маунте — событие «ровно один раз», не отрисовка) и ни один не маскирует сверку под намерение. Только 2 места в _renderBody (онбординг и редактор) стали ensure('reconcile') — ровно то, что заявлено в ТЗ.

Разобрана логика ensure(): ready→true, failed→false, полёт→join, reconcile && _awaitingIntent→false без загрузки, иначе обнуление флага и старт цикла. _awaitingIntent выставляется в !sawMismatch после каждого цикла — т.е. терминальная ошибка (failed) не трогает флаг сверки, нетерминальная взводит его. Это in корректно реализует AC1/AC2.

Гейты, которые прогнал сам:

Гейт Результат
npm run build (tsc --noEmit + rollup) зелёный, 22.7 с — подтверждает typecheck и сборку на этом SHA независимо от Validate
npm run bundle:sync → node demo/smoke_lazy_editor_chunk.mjs (назван в AC1–AC3) зелёный, все 22 проверки true, включая новый раздел (а)/(б)/(в)
Мутация вручную: применил патч render-reconcile-restarts-editor-runtime-cycle из реестра (ensure('reconcile') → ensure() на строке редактора), пересобрал, перегнал смок смок покраснел ровно там, где и обещано: kioskDialogFailureIsOneCycle (4 вместо 2), kioskDialogFailureIsOneNotice (3 вместо 1), warmReviveFailureIsOneCycle (3 вместо 1), warmReviveFailureIsOneNotice (2 вместо 1) — остальные 18 проверок, включая линию мастера импорта (не задета этим мутантом), остались зелёными
Патч отменён, npm run bundle:clean рабочая копия восстановлена до закоммиченного бандла, git status чист

Это прямое исполнительное доказательство: тест умеет падать, и падает именно от той дыры, которую описывает AC1 (цикл без намерения).

Гейты, подтверждённые Validate на этом SHA (не перегонял повторно, см. ссылку в промпте): npx tsc --noEmit, npm test (включая новые 14 тестов test/editor-runtime-loader.test.mjs и test/core-file-budget.test.mjs, не нарушенный потолок монолита), npm run build со сверкой бандла.

smoke-select.mjs --base origin/dev --head HEAD (вывод приложен, решение по каждой строке):

  • Прямое совпадение (16 файлов). demo/smoke_lazy_editor_chunk.mjs — прогнан (см. выше, трижды: baseline, с мутантом, после очистки). Остальные 15 (smoke_danger_confirmation, smoke_houseplan_panel, smoke_plan_upload_limit, smoke_post_write_adoption, smoke_version_recovery, smoke_backdrop_guard, smoke_grid_scale_invariance, smoke_help_affordance, smoke_lazy_admin_locale, smoke_warm_dialogs, smoke_discard_copy, smoke_nav_persist, smoke_preloader_lifecycle, smoke_warm_owners, smoke_zigbee_topology_hover) — не прогонял: совпадение по символам _editorRuntime/_ensureEditorRuntime/ _onboardingRuntime, но все они упражняют только явный путь (ensure() по умолчанию или _ensureEditorRuntime() напрямую), а он в этом диффе не изменился — проверено чтением (см. выше, список строк вызова). Отдельно прочитан smoke_version_recovery.mjs: он не гоняет цикл загрузчика через сеть, а дёргает options.failed(...) напрямую, минуя ensure(), — изменение его не касается. smoke_warm_dialogs.mjs не инжектирует сетевых ошибок рантайма вовсе.
  • Слабая связь (31 файл, общий символ _editorRuntime) — не прогонял: ни один не относится к состояниям загрузчика после сбоя, участвующего в этом диффе; это обычные редакторские смоки, где рантайм грузится успешно с первой попытки до появления диалога.
  • golden:verify — не запускал: нет метки ci:golden, дифф не меняет рендер плана/сцены (только состояние загрузчика и JS-условие вызова).
  • python -m pytest tests_backend — не запускал: правок custom_components/**/*.py нет.
  • npm run invariants — не запускал: геометрия и ссылки на неё не затронуты.
  • node scripts/mutation-gate.mjs --check — не перегонял отдельно (входит в gate:small, который автор прогнал: 273 с, зелёный); факт регистрации мутанта и его смертность проверены вручную выше.
  • performance-профили — не названы в AC, не запускал.

Находки

Нет. Логика состояний (ready/failed/_inFlight/reconcile+_awaitingIntent) разобрана построчно и подтверждена исполнением с ручной мутацией. Все вызовы ensure() вне _renderBody остаются явными. CHANGELOG (RU и EN) в том же коммите, текст совпадает с формулировкой из ТЗ. Трейлеры Issue: и User-Visible: yes на месте. Единственное видимое пользователю число в диффе — счётчики запросов/тостов из таблицы issue/комментария автора — они не попадают в код или документацию пользователя (только в CHANGELOG, где нет числа), так что вопрос «одно ли число — один источник» (§8) здесь не применим.

Единственная формальная не-находка (бухгалтерия, Low, трек show не открывает цикл): зарегистрирован один мутант (render-reconcile-restarts-editor-runtime-cycle), покрывающий только строку вызова редактора; строка вызова онбординга (_onboardingRuntimeLoader.ensure('reconcile')) отдельного мутанта в реестре не имеет, хотя и покрыта тем же смоком через проверки importWizardFailureIsOneCycle/importWizardFailureIsOneNotice (это я подтвердил чтением теста, не мутацией). Снимаю без правки: AC3 называет единственный мутант, трек show не требует полноты реестра, и фактическое покрытие смоком у онбординг-строки есть — просто без отдельной записи в MUTANT_DEFINITIONS.

Что проверено и корректно

  • Контракт #353 (терминальная/нетерминальная ошибка, _inFlight, ready) не нарушен — ветки ready/failed/_inFlight в ensure() не тронуты этим диффом.
  • Явные вызовы (_ensureEditorRuntime, _ensureOnboardingRuntime, все открывающие методы, _requestMode, warm-revive-как-открытие) ведут себя как раньше — прочитаны все их места вызова.
  • _renderBody: оба сверочных вызова (онбординг и редактор) переведены на ensure('reconcile'), правка в src/houseplan-card.ts строго 2 строки, монолит не вырос (12 895 строк в wc -l, бюджет не задет — core-file-budget.test.mjs в npm test, покрыт Validate).
  • Исполнительно подтверждено мутацией: без правки смок красный ровно на тех трёх сценариях, что и описаны в AC1 (киоск, воскрешённый диалог; мастер импорта не затронут этим конкретным мутантом, так как патч меняет только строку редактора — ожидаемо).
  • CHANGELOG RU/EN добавлены в том же коммите, текст соответствует ТЗ дословно.
  • Юнит-тесты (test/editor-runtime-loader.test.mjs, 14/14, из них 2 новых для #757) логически перепроверены: без правки (intent не различается) ассерты на cycles после серии ensure('reconcile') не сошлись бы — тест умеет падать.

Чего не проверял

  • Полную матрицу смоков (286 файлов) — не предрелизный гейт.
  • 15 прямых и 31 слабое совпадение smoke-select вне smoke_lazy_editor_chunk — обоснование по каждой группе выше, в разделе «Как проверялось».
  • golden:verify, pytest tests_backend, invariants, performance — не применимы к этому диффу (см. выше).
  • Ручное HA-окружение/реальный Zigbee-разрыв сети — вне диапазона code-review, достаточно смока с перехватом сети на уровне Chromium (тот же путь, которым ловится прод-дефект в 404 на хешированный чанк).

Вердикт

Зелёный. AC1–AC3 доказаны: AC1/AC2 — исполнением (smoke_lazy_editor_chunk.mjs, включая ручную мутацию, доказывающую тест умеет падать), AC3 — чтением плюс исполнением юнит-тестов и npm run build. Критерии трека show (§5) пройдены, маршрут — fix.


Материал раунда

  • Ветка: issue/757-runtime-loader-no-loop, коммит 3398b69d635e — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
  • Дерево материала: 8ee4c0d4d88af6ec6508633a3c5841b261554825
    git log --all --format='%H %T' | grep 8ee4c0d4d88a
    
  • Тело issue: 7da4b97ada3ff166c59d48c1ed58f958ee8dcda491e0c1e424b6e1b6937e3de1
  • Вердикт конвейера: green · High 0 · маршрут fix