14 KiB
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 не было.
Контракт: объявленный скоуп и проверяемый совпадают. Допустимы два исхода, и оба честны — выбрать при реализации, замерив цену:
- сузить
includeдо реально проверяемого дерева, а расширение оставить отдельной задачей с разбором долга; - расширить CI до полного
include, разобрав накопленные находки (ruff check custom_components/houseplan scripts tests_backend— порядка полусотни, преимущественно I001/E402/F401 в тестах).
Решение фиксируется в комментарии рядом с include, чтобы следующий читатель
не гадал, почему так.
(3) Low «в» — проверка пинов сама себя отключает
test/validate-workflow.test.mjs:216:
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-скоупу иmypystrict остаются зелёными.
План автотестов
Unit (test/validate-workflow.test.mjs, test/backend-pins.test.mjs):
- Пин фронтенда совпадает с ожидаемой версией закреплённого HA (AC1).
- Списки деревьев в
includeи в шаге линта совпадают (AC2). - Синтетический workflow без обеих зацепок → проверка пинов краснеет (AC4).
- Каталог
.github/workflows/*.ymlперебирается целиком: синтетический третий workflow сpip install pytestбез версий краснеет без правки каких-либо списков в тесте (AC5). Списка обходимых файлов в тесте нет и быть не должно — файл, не ставящий python-зависимостей, распознаётся по собственному содержимому. - Реальный каталог проверяется тем же кодом: девять 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).