Files
houseplan-card/docs/specs/399-backend-gate-honesty.md
T
2026-08-31 04:26:14 +03:00

197 lines
14 KiB
Markdown
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# ТЗ #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).