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
This commit is contained in:
Codex
2026-08-31 04:35:56 +03:00
parent cc44b28f3b
commit 98028a3093
6 changed files with 179 additions and 6 deletions
+44 -4
View File
@@ -207,16 +207,56 @@ test('гейты диапазона судят от доказанного пр
assert.match(frontend, /git merge-base origin\/dev "\$HEAD_SHA"/);
});
test('HA-харнесс ставится по точным версиям, а не по воле резолвера (#392)', () => {
/** Ставит ли workflow python-зависимости — по собственному содержимому.
*
* #399: раньше проверка перебирала два имени и молча пропускала файл, где
* не нашлось ни имени пакета, ни пути к пинам. Обе зацепки исчезают разом
* при возврате к `pip install pytest …`, то есть гейт выключался ровно тем
* изменением, ради которого заведён. Признак теперь положительный: есть
* установка python-пакетов — файл обязан ставить их из файла пинов. */
const installsPythonDeps = (workflow) => /(?:^|\s)(?:python -m )?pip\s+install\s/.test(workflow);
test('HA-харнесс ставится по точным версиям, а не по воле резолвера (#392, #399)', () => {
// Плавающие версии означают, что «зелёный backend» значит разное в разные
// дни: по SHA коммита нельзя сказать, чем его проверяли. Ровно так харнесс
// полгода тихо проверял интеграцию против февральского Home Assistant.
for (const file of ['validate.yml', 'mutation-gate.yml']) {
//
// Перебирается ВЕСЬ каталог, а не список имён: новый workflow с
// неверсионированной установкой обязан краснеть сам, без правки теста.
const workflows = readdirSync(WORKFLOWS).filter((name) => name.endsWith('.yml'));
assert.ok(workflows.length >= 2, 'каталог workflows обязан читаться');
const installers = [];
for (const file of workflows) {
const workflow = read(file);
if (!/pytest-homeassistant-custom-component|tests_backend\/requirements\.txt/.test(workflow)) continue;
if (!installsPythonDeps(workflow)) continue;
installers.push(file);
assert.match(workflow, /pip install -r tests_backend\/requirements\.txt/,
`${file}: зависимости харнесса ставятся мимо файла пинов`);
`${file}: ставит python-зависимости мимо файла пинов`);
assert.equal(/pip install pytest /.test(workflow), false,
`${file}: остался установ без версий`);
}
// Факт выводится проверкой, а не задаётся ей: список нужен для сообщения об
// ошибке, а не для решения, кого проверять.
assert.ok(installers.length > 0,
'ни один workflow не ставит python-зависимости — либо каталог прочитан'
+ ' неверно, либо бэкенд-гейт исчез; и то и другое стоит увидеть');
});
test('#399 AC5: проверка ловит новый workflow, которого нет ни в каком списке', () => {
// Синтетический третий файл: при переборе по именам он бы не попал в
// проверку вовсе — именно так гейт и обходили бы, ничего не нарушая.
const rogue = [
'name: rogue',
'jobs:',
' backend:',
' steps:',
' - run: pip install pytest voluptuous homeassistant',
].join('\n');
assert.equal(installsPythonDeps(rogue), true,
'установка python-зависимостей обязана распознаваться по содержимому');
assert.equal(/pip install -r tests_backend\/requirements\.txt/.test(rogue), false,
'и такой файл обязан провалить проверку пинов');
// Обратный случай: файл без установки не должен требовать пинов.
const innocent = 'name: docs\njobs:\n build:\n steps:\n - run: npm ci\n';
assert.equal(installsPythonDeps(innocent), false);
});