# ТЗ #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).