ci: правка реестра мутантов сама запускает гейт по диффу (#475 r1)

Классификатор отдаёт выход mutants по scripts/mutation-gate.mjs, job
changed_mutants получает третий дизъюнкт из ТЗ §2; fallback-ветки
выставляют mutants=true. Тест AC7 отбирает бэкенд-мутанты по файлу патча.

Issue: #475
User-Visible: no
This commit is contained in:
Codex
2026-09-06 19:33:08 +03:00
parent d2d145427b
commit 0145f9bafe
5 changed files with 29 additions and 9 deletions
+3 -3
View File
@@ -209,9 +209,9 @@ jobs:
frontend: ${{ steps.classify.outputs.frontend }}
backend: ${{ steps.classify.outputs.backend }}
integration: ${{ steps.classify.outputs.integration }}
# Диффозависимые профили перф-смока (#473 §5).
perf_iso: ${{ steps.classify.outputs.perf_iso }}
perf_iso: ${{ steps.classify.outputs.perf_iso }} # #473 §5
perf_interaction: ${{ steps.classify.outputs.perf_interaction }}
mutants: ${{ steps.classify.outputs.mutants }}
# Тяжёлые job только на кандидате/по кнопке/на PR (#479), см. шаг heavy.
heavy: ${{ steps.heavy.outputs.heavy }}
base: ${{ steps.base.outputs.base }}
@@ -459,7 +459,7 @@ jobs:
changed_mutants:
name: "Мутанты по диффу: затронутые свидетели краснеют"
needs: changes
if: needs.changes.outputs.frontend == 'true' || needs.changes.outputs.backend == 'true'
if: needs.changes.outputs.frontend == 'true' || needs.changes.outputs.backend == 'true' || needs.changes.outputs.mutants == 'true'
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
+3
View File
@@ -25,6 +25,9 @@ export const CLASSIFIERS = {
// Только `src/**`: тесты и демо кадр не замедляют.
perf_iso: /^src\/iso-[^/]+\.ts$/,
perf_interaction: /^src\/(live-[^/]+|render-[^/]+|houseplan-render-lifecycle|houseplan-card)\.ts$/,
// Реестр мутантов сам по себе — вход гейта по диффу (#475 ревью r1): новый
// свидетель без правки в src/test иначе не проверялся бы до понедельника.
mutants: /^scripts\/mutation-gate\.mjs$/,
};
export const OUTPUTS = Object.keys(CLASSIFIERS);
+11 -3
View File
@@ -35,13 +35,21 @@ test('тесты и демо перф-профили не включают: ка
assert.equal(out.perf_interaction, 'false');
});
test('правка реестра мутантов даёт mutants=true и только его (#475 r1)', () => {
const out = classifyChanges(['scripts/mutation-gate.mjs']);
assert.equal(out.mutants, 'true');
assert.equal(out.frontend, 'false');
assert.equal(classifyChanges(['scripts/mutation-gate-report.mjs']).mutants, 'false');
assert.equal(classifyChanges(['src/x.ts']).mutants, 'false');
});
test('прежние три выхода классифицируются как в inline-shell до выноса', () => {
assert.deepEqual(classifyChanges(['custom_components/houseplan/frontend_registration.py']),
{ frontend: 'false', backend: 'true', integration: 'true', perf_iso: 'false', perf_interaction: 'false' });
{ frontend: 'false', backend: 'true', integration: 'true', perf_iso: 'false', perf_interaction: 'false', mutants: 'false' });
assert.deepEqual(classifyChanges(['custom_components/houseplan/frontend/houseplan-card.js']),
{ frontend: 'true', backend: 'false', integration: 'false', perf_iso: 'false', perf_interaction: 'false' });
{ frontend: 'true', backend: 'false', integration: 'false', perf_iso: 'false', perf_interaction: 'false', mutants: 'false' });
assert.deepEqual(classifyChanges(['hacs.json', 'tsconfig.json']),
{ frontend: 'true', backend: 'false', integration: 'true', perf_iso: 'false', perf_interaction: 'false' });
{ frontend: 'true', backend: 'false', integration: 'true', perf_iso: 'false', perf_interaction: 'false', mutants: 'false' });
assert.equal(classifyChanges(['scripts/support-relay/x.py']).backend, 'true');
assert.equal(classifyChanges(['']).frontend, 'false');
});
+6 -2
View File
@@ -319,8 +319,12 @@ test('#475 AC6: воспроизведение #467 — дифф по src/wall-t
test('#475 AC7: воспроизведение находки ревью — бэкенд-мутанты отбираются по .py гарду и патчу', () => {
const byGuard = selectChangedMutants(MUTANTS, ['tests_backend/test_ha_frontend_registration.py']).map((m) => m.id);
const byPatch = selectChangedMutants(MUTANTS, ['custom_components/houseplan/frontend_registration.py']).map((m) => m.id);
const registration = MUTANTS.filter((m) => m.id.startsWith('frontend-registration-')).map((m) => m.id);
assert.ok(registration.length >= 3, 'в реестре есть бэкенд-мутанты регистрации');
// Отбор по патчу, а не по префиксу id (ревью r1: `frontend-reload-notice-*`
// патчит тот же файл и обязан попасть в набор).
const registration = MUTANTS
.filter((m) => m.patches.some((patch) => patch.file === 'custom_components/houseplan/frontend_registration.py'))
.map((m) => m.id);
assert.ok(registration.length >= 4, 'в реестре есть бэкенд-мутанты регистрации');
for (const id of registration) {
assert.ok(byGuard.includes(id), `${id} не отобран по гарду`);
assert.ok(byPatch.includes(id), `${id} не отобран по патчу`);
+6 -1
View File
@@ -368,7 +368,12 @@ test('мутанты по диффу гоняются на каждом пуше
const job = workflow.slice(start, workflow.indexOf('\n frontend:\n', start));
// Триггер — и фронтенд, и бэкенд: бэкенд-мутанты патчат .py и охраняются
// pytest, а дифф только по ним даёт backend=true без frontend=true (ревью r1).
assert.match(job, /if: needs\.changes\.outputs\.frontend == 'true' \|\| needs\.changes\.outputs\.backend == 'true'/);
assert.match(job, /if: needs\.changes\.outputs\.frontend == 'true' \|\| needs\.changes\.outputs\.backend == 'true' \|\| needs\.changes\.outputs\.mutants == 'true'/);
// Третий дизъюнкт (ТЗ §2, ревью r1): правка одного реестра мутантов — тоже
// вход гейта, классификатор обязан выдавать `mutants` по этому файлу.
assert.match(workflow, /mutants: \$\{\{ steps\.classify\.outputs\.mutants \}\}/);
// Сам шаблон и fallback `--all` проверяет test/classify-changes.test.mjs
// (после #473 классификация живёт в scripts/classify-changes.mjs).
// База диапазона — та же, что у остальных гейтов ветки (#387/#388).
assert.match(job, /PROVEN_BASE: \$\{\{ needs\.changes\.outputs\.range_base \}\}/);
assert.match(job, /git merge-base origin\/dev "\$HEAD_SHA"/);