Files
houseplan-card/test/lint-scope.test.mjs
T
Codex 98028a3093 ci: the backend gate now checks exactly what it promises (#399)
Three claims a green backend used to make, each slightly wider than the
truth — and #392 happened in exactly that gap.

The frontend pin said 20260826.1 next to homeassistant==2026.8.3, whose
package_constraints.txt requires 20260729.7: a combination that exists
in no HA release. It was never derived from anything — someone once
picked it. It is now taken from the constraints, the source is named in
the file, and a test holds both numbers together so raising HA cannot
quietly desync them.

ruff's include declared three trees while CI linted one. Narrowed the
declaration rather than widening CI: the debt in scripts/ and
tests_backend/ (56 findings, mostly E402/I001, plus 7 B023 and 5 B017)
has its own cost and its own decisions, and belongs in its own task, not
in a visibility fix. test/lint-scope.test.mjs now compares the two, so
they can only move together.

The pin check skipped a workflow when it found neither the package name
nor the requirements path — and both vanish together the moment someone
returns to Defaulting to user installation because normal site-packages is not writeable, i.e. the gate switched itself off
under precisely the change it exists to catch. It now walks the whole
.github/workflows directory and decides per file by a positive sign: if
a file installs python packages, it must install them from the pins
file. Verified by dropping a rogue workflow into the directory — it
reddens without touching any list.

Three mutants registered and each run by hand.

User-Visible: no
Issue: #399
2026-08-31 04:35:56 +03:00

46 lines
2.5 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
// #399: конфиг обещал линт по трём деревьям, CI проверял одно. Расхождение
// само по себе не ломает сборку — оно ломает доверие к зелёному значку, а
// следом и к выводу «раз зелено, значит проверено». Ровно так в tests_backend
// проехали F401/F811, которых на v1.69.0 не было.
const root = (name) => fileURLToPath(new URL(`../${name}`, import.meta.url));
const read = (name) => readFileSync(root(name), 'utf8');
/** Деревья из `[tool.ruff] include`, приведённые к каталогу. */
const declaredTrees = () => {
const pyproject = read('pyproject.toml');
const section = pyproject.slice(pyproject.indexOf('[tool.ruff]'));
const include = /include\s*=\s*\[([^\]]*)\]/.exec(section);
assert.ok(include, '[tool.ruff] include обязан существовать');
return [...include[1].matchAll(/"([^"]+)"/g)]
.map((match) => match[1].replace(/\/\*\*\/\*\.py$|\/\*\.py$/, ''))
.sort();
};
/** Деревья, которые реально проверяет шаг линта в CI. */
const lintedTrees = () => {
const workflow = read('.github/workflows/validate.yml');
const step = /python -m ruff check ([^\n]*)/.exec(workflow);
assert.ok(step, 'шаг «Линт бэкенда» обязан существовать в validate.yml');
return step[1].trim().split(/\s+/).filter(Boolean).sort();
};
test('#399 AC2: объявленный скоуп линта равен проверяемому', () => {
assert.deepEqual(declaredTrees(), lintedTrees(),
'include в pyproject.toml и аргументы ruff в validate.yml разошлись:'
+ ' конфиг обещает одно, CI проверяет другое. Расширять список можно'
+ ' только вместе — иначе объявленное покрытие снова станет фикцией.');
});
test('#399 AC2: расхождение действительно ловится', () => {
// Проверка, которая не умеет краснеть, ничего не гарантирует.
const declared = ['custom_components/houseplan', 'tests_backend'];
const linted = ['custom_components/houseplan'];
assert.throws(() => assert.deepEqual(declared, linted));
});