diff --git a/docs/specs/399-backend-gate-honesty.md b/docs/specs/399-backend-gate-honesty.md new file mode 100755 index 00000000..123fc620 --- /dev/null +++ b/docs/specs/399-backend-gate-honesty.md @@ -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).