20 KiB
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 на всех коммитах,
подтверждено):
home-assistant-frontendвtests_backend/requirements.txtвыводится из констрейнтов закреплённогоhomeassistant, а не назначается вручную (AC1).[tool.ruff] includeвpyproject.tomlсужен до реально линтимого дерева, решение записано комментарием рядом (AC2, AC3).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.3home-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 test1667/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,mypystrict — нет установленных бэкенд-зависимостей в этой среде (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.