Files
houseplan-card/docs/reviews/CODE-REVIEW-399-r1.md
2026-08-31 01:44:20 +00:00

20 KiB
Raw Permalink Blame History

CODE-REVIEW-399-r1

  • Issue: https://github.com/Matysh/houseplan-card/issues/399
  • Этап: code (PROCESS.md §2.7)
  • Заход: r1 · блокирующих циклов израсходовано 0/4 → 1/4 после этого вердикта
  • HEAD: 98028a3093713122d6dbc1b08fc830b730e8a2a3 (детач на origin/issue/399-backend-gate-honesty)
  • Материал: git log --oneline origin/dev..HEAD, git diff origin/dev...HEAD
  • ТЗ на входе: docs/specs/399-backend-gate-honesty.md, ревизия 3, принята зелёным вердиктом SPEC-REVIEW-399-r3 (заход прошёл spec r1→r3 из-за двух Medium по формулировке AC5, оба закрыты, см. комментарии issue)

Скоуп

Три независимые правки инфраструктуры бэкенд-гейта, без продуктового кода и без пользовательских изменений (User-Visible: no на всех коммитах, подтверждено):

  1. home-assistant-frontend в tests_backend/requirements.txt выводится из констрейнтов закреплённого homeassistant, а не назначается вручную (AC1).
  2. [tool.ruff] include в pyproject.toml сужен до реально линтимого дерева, решение записано комментарием рядом (AC2, AC3).
  3. test/validate-workflow.test.mjs перебирает каталог .github/workflows/*.yml целиком и решает по позитивному признаку («файл ставит python-пакеты»), а не по паре имён/по отсутствию подстроки (AC4, AC5).

Diff: pyproject.toml, tests_backend/requirements.txt, test/validate-workflow.test.mjs, новые test/backend-pins.test.mjs, test/lint-scope.test.mjs, три новых мутанта в scripts/mutation-gate.mjs, плюс документы ревью/ТЗ. Продуктовый код (src/**, custom_components/**) не тронут — это сузило объём гейтов ниже (см. «Что не проверялось»).

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

Это первый код-ревью раунд, зелёного Validate на 98028a30 нет — гейты прогнаны вручную.

  • npx tsc --noEmit — чисто, exit 0.
  • npm test — 1667/1667 pass, 1 skipped, совпадает с заявленным автором числом.
  • npm run build — собрался (tsc --noEmit && rollup -c, 15.6s). Сверка трёх копий бандла не делалась: diff не трогает src/**, бандл не мог измениться по содержанию — сверка была бы проверкой того, что заведомо не менялось.
  • node scripts/check-docs.mjs — не прогонялся: diff не трогает src/** (условие запуска по инструкции), а других тронутых поверхностей у отпечатка скриншотов нет.
  • Геометрические инварианты (npm run invariants), golden, browser-смоки — не прогонялись: diff не касается геометрии, рендера, слоёв, стилей и не меняет видимое поведение вовсе (продукт не видит эту задачу).
  • python -m pytest tests_backend, ruff check — не прогонялись: в песочнице нет homeassistant, pytest-homeassistant-custom-component, home-assistant-frontend, ruff (проверено pip show, python3 -m ruff --version → «No module named ruff»). Это гейты CI-окружения с предустановленным HA-харнессом, здесь их нет физически.
  • AC1 (сердце находки M3) проверено сетевым источником: в spec-ревью r1 это было явно помечено как непроверенное («нет сетевого доступа в этой среде»). Здесь сеть есть через gh api: gh api repos/home-assistant/core/contents/homeassistant/package_constraints.txt?ref=2026.8.3 → декодированный файл, строка 42: home-assistant-frontend==20260729.7. Совпадает с новым пином в tests_backend/requirements.txt буквально. AC1 подтверждён независимо, не с чужих слов.
  • Три зарегистрированных мутанта прогнаны штатным раннером (node scripts/mutation-gate.mjs --id=<name>), не только руками автора: workflow-scan-hardcodes-the-list, lint-scope-drifts, frontend-pin-drifts-from-ha — все три «поймано 1 из 1». Рабочее дерево после прогона чистое (git status --short пуст).
  • Реальный каталог .github/workflows/*.yml: ls | wc -l → 9, grep -n "pip install" находит установку ровно в validate.yml и mutation-gate.yml, оба через pip install -r tests_backend/requirements.txt. Факт из AC5 («девять workflow, два ставят зависимости») подтверждён построчным чтением, не с чужих слов.
  • Дополнительно (не входит в стандартный набор, но потребовалось для проверки самого центрального контракта задачи AC5): вручную воспроизведена регрессия, которую AC5 обязан ловить, — см. находку ниже.

Находки

[High] AC5 не доказан автотестом: возврат к прежнему хардкод-списку из

двух имён + новый непинованный workflow проходят все тесты зелёными

Файл: test/validate-workflow.test.mjs:210-262 (тест #399 AC5: проверка ловит новый workflow, которого нет ни в каком списке, он же новый installsPythonDeps)

Контракт п.(3) ТЗ и AC5 требуют буквально: «Появление нового workflow, ставящего зависимости мимо файла пинов, краснеет само, без правки списка». План автотестов, п.4: «синтетический третий workflow с pip install pytest без версий краснеет без правки каких-либо списков в тесте». Мутант workflow-scan-hardcodes-the-list зарегистрирован ровно под это.

Тест, который называется «AC5», на деле не перебирает каталог вообще — он вызывает голую функцию installsPythonDeps на двух строковых литералах, объявленных в самом тесте (rogue, innocent), и сравнивает предикат сам с собой. Реальный код сканирования (readdirSync(WORKFLOWS).filter(...)) этим тестом не исполняется ни разу. Единственное место, где сканирование каталога действительно упражняется, — основной тест «HA-харнесс ставится по точным версиям…», а он работает на реальном, неизменном дереве .github/workflows/, где сегодня ровно 9 файлов и ровно 2 инсталлятора: он не может отличить «сканирование каталога» от «хардкод списка из этих же двух имён», потому что результат обхода в обоих случаях одинаков.

Воспроизведение (дерево возвращено в исходное состояние после каждого шага, git status --short пуст):

# 1) вернуть сканирование к прежнему хардкод-списку из ДВУХ реальных имён —
#    буквально та конструкция, которую #399 отменяет
sed -i "s|const workflows = readdirSync(WORKFLOWS).filter((name) => name.endsWith('.yml'));|const workflows = ['validate.yml', 'mutation-gate.yml'];|" \
  test/validate-workflow.test.mjs

# 2) добавить ровно тот "третий workflow", который по тексту AC5 обязан
#    красить проверку
cat > .github/workflows/zz-rogue.yml <<'EOF'
name: rogue
jobs:
  backend:
    steps:
      - run: pip install pytest voluptuous homeassistant
EOF

node --test test/validate-workflow.test.mjs
# ...
# tests 10
# pass 10
# fail 0

Результат: 10/10 зелёных. Ровно тот сценарий, ради которого написан AC5 («хотя в прежнем списке из двух имён его бы не было» — дословно из самого AC5), проходит незамеченным.

Для сравнения: зарегистрированный в scripts/mutation-gate.mjs мутант workflow-scan-hardcodes-the-list действительно ловится (node scripts/mutation-gate.mjs --id=workflow-scan-hardcodes-the-list → «поймано 1 из 1») — но не потому, что тест проверяет поведение сканирования. Его патч подставляет список из одного имени (['validate.yml']), и единственная причина падения — побочная проверка `assert.ok(workflows.length

= 2, 'каталог workflows обязан читаться'), которая тривиально ловит любой список короче двух элементов, независимо от того, сканируется каталог или нет. Стоит мутанту (или будущей регрессии) сохранить оба существующих имени — а это самый вероятный вид регрессии, потому что он неотличим от корректного кода на сегодняшнем дереве, — и защита исчезает вместе с zz-rogue.yml`, добавленным где угодно.

Сценарий отказа: инженер через полгода «оптимизирует» цикл (readdirSync → фиксированный массив, потому что «мы же знаем, какие два файла ставят зависимости») либо частично отменяет коммит 98028a30 при конфликте ребейза, сохранив список из двух настоящих имён. Одновременно (или отдельным PR) третий workflow ставит pip install fastapi без версий. CI зелёный. Это в точности форма #392, которую всю задачу и заводили закрывать — только с постоянной инвариантностью «зелёный, но проверено не то», а не с постепенным устареванием пина.

Как закрыть: тест, помеченный как AC5, обязан упражнять реальный путь сканирования, а не предикат-функцию в изоляции — например, подменить WORKFLOWS на временную директорию с тремя синтетическими файлами (двумя «старыми» именами + рогом) и убедиться, что именно вызываемая в проде функция сканирования их все находит и ровно рог красит проверку. Как минимум, добавить утверждение вида «список найденных инсталляторов равен ['mutation-gate.yml', 'validate.yml', 'zz-rogue...']» на реальном временном дереве, а не полагаться на побочный length >= 2 от искусственно короткого мутанта.

Что проверено и признано корректным

  • AC1 — подтверждён независимо от заявления автора: gh api к тегу 2026.8.3 home-assistant/core даёт home-assistant-frontend==20260729.7 в package_constraints.txt, это ровно значение в tests_backend/requirements.txt. Источник назван в комментарии рядом с пином (test/backend-pins.test.mjs держит обе строки, тест на источник версии тоже проверен построчно). Мутант frontend-pin-drifts-from-ha ловится штатным раннером.
  • AC2/AC3 — include в pyproject.toml сужен до ["custom_components/houseplan/**/*.py"], что дословно равно аргументу шага «Линт бэкенда» в validate.yml:788 (ruff check custom_components/houseplan). test/lint-scope.test.mjs сравнивает оба списка по построенному парсингу, не строкой целиком — прочитан построчно, логика корректна для текущего формата обоих файлов. Комментарий у include называет исход (1), даёт объём долга (56 находок, разбивка по правилам) и условие расширения. Мутант lint-scope-drifts ловится.
  • AC4 — детектор installsPythonDeps заменил механизм «пропустить при отсутствии одной из двух зацепок» на позитивный признак «есть pip install» — это сильнее буквы AC4 (нет самого понятия «пропуск», есть явное решение по каждому файлу), контракт п.(3) ТЗ прямо предполагает именно такую конструкцию («доказывается явно… не выводится из отсутствия подстроки»).
  • AC6 — npm test 1667/0 (см. «Как проверялось»); ruff/mypy не прогонялись здесь физически (нет пакетов), но diff не расширяет и не меняет [tool.ruff.lint]/[tool.mypy], а сужение include может только уменьшить число проверяемых файлов ruff — деградации со стороны этого диффа структурно не может возникнуть, кроме случая, разобранного в находке High (сканирование workflow), который к npm test/ruff/mypy не относится.
  • Трейлеры: Issue: #399, User-Visible: no на всех коммитах диапазона. Соответствует ТЗ (пользовательских изменений нет), changelog не тронут — верно.
  • Реальный каталог workflow (9 файлов, 2 инсталлятора, оба через файл пинов) — сверен ls/grep, совпадает с фактом, заявленным в AC5.
  • Откат: три независимых правки, продуктовый код не затронут — подтверждено diff'ом (src/**, custom_components/** не встречаются).

Дополнительное наблюдение (Low, не блокирует, с записью)

  • Коммит 98028a30 содержит в теле явно склеенный текст: «...the gate switched itself off under precisely the change it exists to catch» через предложение вида «and both vanish together the moment someone returns to Defaulting to user installation because normal site-packages is not writeable, i.e. …» — похоже на вставленный по ошибке кусок вывода терминала (pip'а) вместо задуманного «pip install pytest voluptuous … без версий». Смысл сообщения не искажён настолько, чтобы вводить в заблуждение о содержании правки, код не затронут. Снимается без действия: автор правит только если решит переписать историю по своей инициативе — требовать этого не буду.
  • installsPythonDeps (/(?:^|\s)(?:python -m )?pip\s+install\s/) не распознаёт pip3 install, python3 -m pip install или установку через выделенный Action (uses: ...install...) как «ставит python-зависимости». Сегодня ни один из 9 workflow так не делает (проверено grep), риск не реализован. Не блокирует и не в скоупе, чтобы её не мог обходить именно такой воркфлоу текстовый признак — вне текущего AC5 (тот описывает текстовый pip install, не произвольные способы установки). Оставляю как наблюдение, не как находку.

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

  • python -m pytest tests_backend, ruff check custom_components/houseplan, mypy strict — нет установленных бэкенд-зависимостей в этой среде (pip show → not found на все три пакета, ruff модуль отсутствует). Ожидаю зелёный CI-прогон на 98028a30 как факт, а не как «verified» без команды.
  • Полный HA-харнесс с новым пином фронтенда (реальная установка home-assistant-frontend==20260729.7 и импорт от него) — то же ограничение среды; автор прямо оговорил то же самое в комментарии к реализации.
  • npm run golden:verify, geometry invariants, browser-смоки — не по необходимости: diff не меняет геометрию, рендер, стили, слои и не затрагивает видимое поведение продукта вовсе (весь diff — конфигурация CI/линта/тестов, ноль строк в src/**).
  • node scripts/check-docs.mjs — условие запуска (диф трогает src/**) не выполнено, не прогонялся.
  • Содержимое остальных 55 ruff-находок, упомянутых в комментарии к include («56 находок… 22 E402, 12 I001, 7 B023, 5 B017») — цифры не пересчитывались построчно (нет ruff в среде); они не в скоупе задачи (это долг для отдельной задачи по формулировке ТЗ), их точность не влияет на приёмку AC1–AC6.