mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-04 13:48:57 +00:00
247 lines
20 KiB
Markdown
247 lines
20 KiB
Markdown
# 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` пуст):
|
||
|
||
```bash
|
||
# 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.
|