diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index c7582df6..cd1cfba8 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -786,6 +786,22 @@ jobs: pip list --format=columns | grep -Ei 'homeassistant|voluptuous|^pytest ' - name: Линт бэкенда (ruff, узкий набор) run: python -m ruff check custom_components/houseplan + # #42: типизация была измеримой только локально — CI её не исполнял, и + # регрессия в любом из allowlist-модулей проходила молча. Список модулей + # берётся ИЗ pyproject.toml, а не дублируется здесь: разошедшийся дубль + # означал бы зелёный шаг, проверяющий не то. Пустой список — отказ. + - name: Типы бэкенда (mypy strict по allowlist) + run: | + modules=$(python -c " + import tomllib + cfg = tomllib.load(open('pyproject.toml', 'rb')) + for o in cfg['tool']['mypy'].get('overrides', []): + if o.get('strict'): + print(' '.join('-p ' + m for m in o['module'])) + ") + test -n "$modules" || { echo '::error::strict-allowlist в pyproject.toml пуст — проверять нечего'; exit 1; } + echo "mypy strict: $modules" + python -m mypy $modules # #42: защита от тихого скипа HA-harness — импорт и порог collect - name: HA-harness присутствует run: | diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 935317b8..91e98826 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -1552,10 +1552,15 @@ their passports; `scripts/config-audit.mjs` treats both as `current`. - `tests_backend/requirements.txt` is the single source of backend CI dependencies (validate.yml and mutation-gate.yml install from it; the file itself was introduced by #392, which also moved the harness to python 3.14 - and the current Home Assistant — #42 adds ruff to it for the lint step). + and the current Home Assistant — #42 adds ruff and mypy to it for the lint + and typing steps). - `pyproject.toml` configures ruff (E/F/B/I, E501 excluded by decision) and mypy strict for a grow-only allowlist of pure modules; the completeness guard lives in `tests_backend/test_backend_quality.py`. +- Both linters RUN in the backend CI job: the typing step derives its module + list from the `pyproject.toml` allowlist rather than repeating it, refuses an + empty list, and is itself guarded by a test plus the `typing-gate-stops- + running` mutant — a configured-but-unexecuted gate measures nothing. - The backend CI job measures branch coverage (pure + HA harness combined), fails below `scripts/backend-coverage-baseline.txt` and refuses to run when the HA harness would silently skip. diff --git a/scripts/mutation-gate.mjs b/scripts/mutation-gate.mjs index 09840644..89da9373 100644 --- a/scripts/mutation-gate.mjs +++ b/scripts/mutation-gate.mjs @@ -757,6 +757,17 @@ const MUTANT_DEFINITIONS = [ replace: ' "invalid_toggle_entity", "invalid_value_badge",', }], }, + { + id: 'typing-gate-stops-running', + guard: 'python3 -m pytest tests_backend/test_backend_quality.py -q -p no:cacheprovider', + because: 'a strict-typing allowlist that CI never executes is a measurement ' + + 'that measures nothing — the step must be load-bearing (#42 r6 AC4)', + patches: [{ + file: '.github/workflows/validate.yml', + find: ' python -m mypy $modules', + replace: ' echo "skip: $modules"', + }], + }, { id: 'error-scanner-loses-a-class-source', guard: 'python3 -m pytest tests_backend/test_backend_quality.py -q -p no:cacheprovider', diff --git a/tests_backend/requirements.txt b/tests_backend/requirements.txt index 8be1fc20..76458cc3 100644 --- a/tests_backend/requirements.txt +++ b/tests_backend/requirements.txt @@ -32,3 +32,8 @@ home-assistant-frontend==20260826.1 homeassistant==2026.8.3 # #42: линт бэкенда (шаг «Линт бэкенда» в validate.yml ставится отсюда же). ruff==0.16.5 +# #42: строгая типизация allowlist-модулей (шаг «Типы бэкенда»). Без пина +# гейт типизации был бы не воспроизводим по SHA — ровно то, от чего этот +# файл и заведён: новая версия mypy добавляет проверки и краснеет на коде, +# который не менялся. +mypy==2.3.1 diff --git a/tests_backend/test_backend_quality.py b/tests_backend/test_backend_quality.py index f8e3dbcb..295d633a 100644 --- a/tests_backend/test_backend_quality.py +++ b/tests_backend/test_backend_quality.py @@ -120,6 +120,33 @@ def test_issue_42_mypy_strict_allowlist_only_grows(): "the list only ever grows (#42)") +def test_issue_42_mypy_strict_is_actually_executed_by_ci(): + """The typing gate must RUN, not merely be configured (#42 r6 Medium). + + The allowlist test above compares text; without a workflow step that + invokes mypy, a regression in any of the six modules reaches dev + unnoticed — measurable quality that nothing measures. Three facts are + pinned: mypy is pinned in the dependency file the backend job installs + from, a step actually invokes it, and that step derives the module list + from pyproject.toml instead of duplicating it (a drifted duplicate is a + green step checking the wrong thing). + """ + requirements = (REPO / "tests_backend" / "requirements.txt").read_text(encoding="utf-8") + assert re.search(r"^mypy==\d+\.\d+", requirements, re.M), ( + "mypy is not pinned in tests_backend/requirements.txt — the typing gate " + "would not be reproducible from the SHA (#42)") + + workflow = (REPO / ".github" / "workflows" / "validate.yml").read_text(encoding="utf-8") + steps = [block for block in workflow.split(" - name: ") if "mypy" in block] + assert steps, "no validate.yml step runs mypy — AC4 has no execution in CI (#42)" + step = steps[0] + assert re.search(r"python -m mypy\s", step), ( + "the mypy step must invoke the checker itself, not only mention it") + assert "tool" in step and "mypy" in step and "pyproject.toml" in step, ( + "the step must read the strict allowlist from pyproject.toml, not repeat it") + assert "exit 1" in step, "an empty allowlist must fail the step, not pass it silently" + + def test_issue_42_every_noqa_carries_a_reason(): for path in sorted(BACKEND.glob("*.py")): for index, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1):