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

14 KiB
Executable File
Raw Blame History

ТЗ #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:

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).