docs: specify the backend gate honesty fixes (#399)

User-Visible: no
Issue: #399
This commit is contained in:
Codex
2026-08-31 04:06:40 +03:00
parent f7fb3369b7
commit a1d7e0da55
+171
View File
@@ -0,0 +1,171 @@
# ТЗ #399 — Бэкенд-гейт проверяет ровно то, что обещает
- Issue: https://github.com/Matysh/houseplan-card/issues/399
- Приоритет: P2, infra; полный трек — класс B (конфигурация гейта, workflow,
тесты), три несвязанные поверхности в одном issue по решению аудита
- Ревизия: 1 (2026-08-31)
## Сценарий
Разработчик смотрит на зелёный `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 — гейт не заметит: исчезнут обе
зацепки разом.
**Контракт**: отсутствие зацепок — отказ, а не пропуск. Если 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**. Проверка при этом не ломается на workflow, которые бэкенд-зависимости
не ставят вовсе (если такие есть в репозитории): для них условие формулируется
явно, а не через отсутствие подстроки. Доказательство: перечисление файлов,
которые проверка обходит, зафиксировано в самом тесте.
- **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. Явный список обходимых файлов зафиксирован; добавление нового файла в
workflows без обновления списка краснеет (AC5).
**Мутанты** (`scripts/mutation-gate.mjs`):
- `backend-pins-check-opts-out`: убрать из workflow путь к файлу пинов →
`validate-workflow.test.mjs` красный.
- `lint-scope-drifts`: добавить дерево в `include`, не тронув workflow →
контракт скоупа красный.
## Риски
- **Исход (2) вскроет долг, который придётся чинить в этой же задаче.**
Смягчение: оценить объём до выбора; при неприемлемом объёме брать исход (1),
а расширение выносить отдельным issue со своим бюджетом.
- **Сверка версии фронтенда без сети.** Ожидаемая версия должна лежать в
репозитории, иначе тест станет зависеть от доступности GitHub. Смягчение:
хранить ожидание рядом с пином (комментарий + константа в тесте), обновлять
вместе с подъёмом HA — это и есть «видно по SHA».
- **Ложное чувство завершённости.** Задача чинит видимость, а не покрытие:
после неё линт по-прежнему может проверять одно дерево. Смягчение: явная
формулировка в комментарии и, при исходе (1), заведённый follow-up.
## Откат
Три независимые правки, каждая — одна строка плюс тест. Продуктовый код не
затронут.
## Release-артефакты
Пользовательских изменений нет: `User-Visible: no`, changelog не трогается.
`docs/ARCHITECTURE.md` — одна строка в разделе про гейты бэкенда, если выбран
исход (2).