mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-01 04:09:17 +00:00
197 lines
14 KiB
Markdown
Executable File
197 lines
14 KiB
Markdown
Executable File
# ТЗ #399 — Бэкенд-гейт проверяет ровно то, что обещает
|
||
|
||
- Issue: https://github.com/Matysh/houseplan-card/issues/399
|
||
- Приоритет: P2, infra; полный трек — класс B (конфигурация гейта, workflow,
|
||
тесты), три несвязанные поверхности в одном issue по решению аудита
|
||
- Ревизия: 3 (2026-08-31) — по SPEC-REVIEW-399-r2 (Medium: план тестов повторял
|
||
снятую формулировку AC5)
|
||
|
||
## Сценарий
|
||
|
||
Разработчик смотрит на зелёный `backend` и делает вывод: «интеграция проверена
|
||
против закреплённого Home Assistant, линт прошёл по объявленному скоупу,
|
||
версии зафиксированы». Каждое из трёх утверждений сегодня чуть шире правды, и
|
||
именно из-за таких зазоров #392 уже случился: харнесс полгода тихо проверял
|
||
интеграцию против февральского HA, и ни один гейт об этом не сказал.
|
||
|
||
## Что человек увидит до и после
|
||
|
||
Пользователь продукта — ничего. Разработчик перестаёт получать три ложных
|
||
обещания: пин фронтенда соответствует закреплённому HA, объявленный скоуп
|
||
линта совпадает с проверяемым, а проверка пинов не может выключить сама себя.
|
||
|
||
## Проблема и контракты по пунктам
|
||
|
||
### (1) M3 — пин фронтенда не соответствует закреплённому HA
|
||
|
||
`tests_backend/requirements.txt:29` фиксирует
|
||
`home-assistant-frontend==20260826.1`, тогда как рядом (`:32`) закреплён
|
||
`homeassistant==2026.8.3`, а его `package_constraints.txt` в теге `2026.8.3`
|
||
репозитория `home-assistant/core` требует `home-assistant-frontend==20260729.7`
|
||
(проверено загрузкой файла). `pytest-homeassistant-custom-component` фронтенд
|
||
в зависимостях не объявляет вовсе, то есть версия выбрана вручную и ничем не
|
||
выведена.
|
||
|
||
Функционального отказа сегодня нет: пакет — статика, тесты его не исполняют.
|
||
Но цель #392 формулировалась дословно как «по SHA видно, чем проверяли», а
|
||
проверяется набор, которого не существует ни в одном релизе HA.
|
||
|
||
**Контракт**: версия фронтенда выводится из констрейнтов закреплённого HA, а
|
||
не назначается. В комментарии рядом сказано, откуда она берётся, чтобы
|
||
следующий подъём HA не превратился в угадывание.
|
||
|
||
### (2) Low «а» — ruff линтит уже, чем объявляет конфиг
|
||
|
||
`pyproject.toml:5` включает в `[tool.ruff] include` три дерева:
|
||
`custom_components/houseplan/**/*.py`, `scripts/*.py`, `tests_backend/**/*.py`.
|
||
Шаг «Линт бэкенда» (`.github/workflows/validate.yml:787-788`) исполняет
|
||
`python -m ruff check custom_components/houseplan` — одно дерево из трёх.
|
||
|
||
Сужение было осознанным решением #42 (щадящий трек, «без массового rewrite»),
|
||
и пересматривать само решение эта задача не обязана. Проблема в том, что
|
||
конфиг об этом молчит: читающий `pyproject.toml` видит три дерева и делает
|
||
неверный вывод. Наблюдаемое следствие — в `tests_backend/test_validation.py`
|
||
проехали мёртвые `import sys`/`import types` (F401) и переопределения (F811),
|
||
которых на v1.69.0 не было.
|
||
|
||
**Контракт**: объявленный скоуп и проверяемый совпадают. Допустимы два исхода,
|
||
и оба честны — выбрать при реализации, замерив цену:
|
||
|
||
1. сузить `include` до реально проверяемого дерева, а расширение оставить
|
||
отдельной задачей с разбором долга;
|
||
2. расширить CI до полного `include`, разобрав накопленные находки
|
||
(`ruff check custom_components/houseplan scripts tests_backend` — порядка
|
||
полусотни, преимущественно I001/E402/F401 в тестах).
|
||
|
||
Решение фиксируется в комментарии рядом с `include`, чтобы следующий читатель
|
||
не гадал, почему так.
|
||
|
||
### (3) Low «в» — проверка пинов сама себя отключает
|
||
|
||
`test/validate-workflow.test.mjs:216`:
|
||
|
||
```js
|
||
if (!/pytest-homeassistant-custom-component|tests_backend\/requirements\.txt/.test(workflow)) continue;
|
||
```
|
||
|
||
Если workflow не содержит ни имени пакета, ни пути к файлу пинов, проверка
|
||
пропускает файл молча. То есть возврат к `pip install pytest voluptuous …` без
|
||
версий — ровно то, от чего защищались в #392 — гейт не заметит: исчезнут обе
|
||
зацепки разом.
|
||
|
||
Вторая половина той же дыры: проверка перебирает **жёстко заданный массив**
|
||
`['validate.yml', 'mutation-gate.yml']`, а не каталог. Новый workflow с
|
||
неверсионированной установкой гейт не увидит просто потому, что его нет в
|
||
списке — и это ровно тот способ, которым #392 уже случился: корректность
|
||
держалась на том, что никто не добавит третий файл.
|
||
|
||
**Контракт**: проверка перебирает **все** `.github/workflows/*.yml`. Для
|
||
каждого файла возможны ровно два исхода: либо он ставит зависимости бэкенда —
|
||
и тогда обязан делать это из `tests_backend/requirements.txt`, либо он их не
|
||
ставит — и тогда это утверждение проверяется явно (в файле нет установки
|
||
python-зависимостей), а не выводится из отсутствия подстроки. Появление нового
|
||
workflow, ставящего зависимости мимо файла пинов, краснеет само, без правки
|
||
списка.
|
||
|
||
## Скоуп / не-скоуп
|
||
|
||
**В скоупе**: `tests_backend/requirements.txt`, `pyproject.toml` (`[tool.ruff]
|
||
include`) и/или шаг линта в `.github/workflows/validate.yml`,
|
||
`test/validate-workflow.test.mjs`, при выборе исхода (2) — разбор ruff-долга в
|
||
`scripts/*.py` и `tests_backend/**/*.py`.
|
||
|
||
**Не в скоупе**: сам факт сужения линта из #42 как решение (пересматривается
|
||
только его видимость в конфиге); версии `homeassistant` и
|
||
`pytest-homeassistant-custom-component` (закреплены #392, менять только вместе
|
||
с полным прогоном); гейт типизации (#42) и гвард `sys.modules` (#398).
|
||
|
||
## UX
|
||
|
||
Не применимо.
|
||
|
||
## Модель данных и миграция
|
||
|
||
Не применимо.
|
||
|
||
## i18n
|
||
|
||
Новых строк нет.
|
||
|
||
## Критерии приёмки
|
||
|
||
- **AC1**. `home-assistant-frontend` в `tests_backend/requirements.txt` равен
|
||
версии из констрейнтов закреплённого `homeassistant`. Доказательство: тест,
|
||
сверяющий пин с зафиксированной в репозитории копией ожидаемой версии (без
|
||
обращения в сеть на прогоне), плюс комментарий в файле с источником.
|
||
- **AC2**. Скоуп линта в workflow и `include` в `pyproject.toml` совпадают.
|
||
Доказательство: контрактный тест, читающий оба файла и сравнивающий списки
|
||
деревьев; расхождение краснеет.
|
||
- **AC3**. Выбранный исход (1) или (2) записан комментарием рядом с `include`
|
||
с причиной. Доказательство: тест на присутствие обоснования не требуется —
|
||
проверяется ревьюером; но при исходе (2) `ruff check` по полному `include`
|
||
зелёный в CI.
|
||
- **AC4**. `test/validate-workflow.test.mjs` отказывает, когда в workflow нет
|
||
ни имени пакета, ни пути к файлу пинов. Доказательство: тест на синтетическом
|
||
workflow без обеих зацепок → красный.
|
||
- **AC5**. Проверка перебирает каталог `.github/workflows/*.yml`, а не
|
||
фиксированный список имён. Для файла, который зависимостей бэкенда не
|
||
ставит, это доказывается явно — в нём нет установки python-пакетов, — а не
|
||
выводится из отсутствия подстроки. Доказательство: тест на синтетическом
|
||
каталоге, где третий workflow ставит `pip install pytest` без версий →
|
||
красный, хотя в прежнем списке из двух имён его бы не было. Сегодня в
|
||
репозитории девять workflow, установку python-зависимостей делают два
|
||
(`validate.yml`, `mutation-gate.yml`) — это факт, который проверка обязана
|
||
вывести сама, а не принять на веру.
|
||
- **AC6**. Существующие гейты не ослаблены: `npm test`, `ruff` по текущему
|
||
CI-скоупу и `mypy` strict остаются зелёными.
|
||
|
||
## План автотестов
|
||
|
||
**Unit** (`test/validate-workflow.test.mjs`, `test/backend-pins.test.mjs`):
|
||
|
||
1. Пин фронтенда совпадает с ожидаемой версией закреплённого HA (AC1).
|
||
2. Списки деревьев в `include` и в шаге линта совпадают (AC2).
|
||
3. Синтетический workflow без обеих зацепок → проверка пинов краснеет (AC4).
|
||
4. Каталог `.github/workflows/*.yml` перебирается целиком: синтетический
|
||
третий workflow с `pip install pytest` без версий краснеет **без** правки
|
||
каких-либо списков в тесте (AC5). Списка обходимых файлов в тесте нет и
|
||
быть не должно — файл, не ставящий python-зависимостей, распознаётся по
|
||
собственному содержимому.
|
||
5. Реальный каталог проверяется тем же кодом: девять workflow, установка
|
||
python-зависимостей найдена ровно в двух — это вывод проверки, а не
|
||
зафиксированное в ней ожидание (AC5).
|
||
|
||
**Мутанты** (`scripts/mutation-gate.mjs`):
|
||
|
||
- `backend-pins-check-opts-out`: убрать из workflow путь к файлу пинов →
|
||
`validate-workflow.test.mjs` красный.
|
||
- `lint-scope-drifts`: добавить дерево в `include`, не тронув workflow →
|
||
контракт скоупа красный.
|
||
- `workflow-scan-hardcodes-the-list`: заменить перебор каталога на
|
||
фиксированный список имён → пункт 4 плана красный (иначе от снятой
|
||
конструкции ничто не удерживает).
|
||
|
||
## Риски
|
||
|
||
- **Исход (2) вскроет долг, который придётся чинить в этой же задаче.**
|
||
Смягчение: оценить объём до выбора; при неприемлемом объёме брать исход (1),
|
||
а расширение выносить отдельным issue со своим бюджетом.
|
||
- **Сверка версии фронтенда без сети.** Ожидаемая версия должна лежать в
|
||
репозитории, иначе тест станет зависеть от доступности GitHub. Смягчение:
|
||
хранить ожидание рядом с пином (комментарий + константа в тесте), обновлять
|
||
вместе с подъёмом HA — это и есть «видно по SHA».
|
||
- **Ложное чувство завершённости.** Задача чинит видимость, а не покрытие:
|
||
после неё линт по-прежнему может проверять одно дерево. Смягчение: явная
|
||
формулировка в комментарии и, при исходе (1), заведённый follow-up.
|
||
|
||
## Откат
|
||
|
||
Три независимые правки, каждая — одна строка плюс тест. Продуктовый код не
|
||
затронут.
|
||
|
||
## Release-артефакты
|
||
|
||
Пользовательских изменений нет: `User-Visible: no`, changelog не трогается.
|
||
`docs/ARCHITECTURE.md` — одна строка в разделе про гейты бэкенда, если выбран
|
||
исход (2).
|