From 0145f9bafe68cfe0e1abe37496ea3c279688359f Mon Sep 17 00:00:00 2001 From: Codex Date: Sun, 6 Sep 2026 14:38:30 +0300 Subject: [PATCH] =?UTF-8?q?ci:=20=D0=BF=D1=80=D0=B0=D0=B2=D0=BA=D0=B0=20?= =?UTF-8?q?=D1=80=D0=B5=D0=B5=D1=81=D1=82=D1=80=D0=B0=20=D0=BC=D1=83=D1=82?= =?UTF-8?q?=D0=B0=D0=BD=D1=82=D0=BE=D0=B2=20=D1=81=D0=B0=D0=BC=D0=B0=20?= =?UTF-8?q?=D0=B7=D0=B0=D0=BF=D1=83=D1=81=D0=BA=D0=B0=D0=B5=D1=82=20=D0=B3?= =?UTF-8?q?=D0=B5=D0=B9=D1=82=20=D0=BF=D0=BE=20=D0=B4=D0=B8=D1=84=D1=84?= =?UTF-8?q?=D1=83=20(#475=20r1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Классификатор отдаёт выход mutants по scripts/mutation-gate.mjs, job changed_mutants получает третий дизъюнкт из ТЗ §2; fallback-ветки выставляют mutants=true. Тест AC7 отбирает бэкенд-мутанты по файлу патча. Issue: #475 User-Visible: no --- .github/workflows/validate.yml | 6 +++--- scripts/classify-changes.mjs | 3 +++ test/classify-changes.test.mjs | 14 +++++++++++--- test/mutation-gate.test.mjs | 8 ++++++-- test/validate-workflow.test.mjs | 7 ++++++- 5 files changed, 29 insertions(+), 9 deletions(-) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 7a50a42c..b96f7045 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -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: diff --git a/scripts/classify-changes.mjs b/scripts/classify-changes.mjs index 53d3c24b..c547d58c 100755 --- a/scripts/classify-changes.mjs +++ b/scripts/classify-changes.mjs @@ -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); diff --git a/test/classify-changes.test.mjs b/test/classify-changes.test.mjs index ffa6d472..64130cdc 100644 --- a/test/classify-changes.test.mjs +++ b/test/classify-changes.test.mjs @@ -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'); }); diff --git a/test/mutation-gate.test.mjs b/test/mutation-gate.test.mjs index 4da1643b..76f5c3ca 100644 --- a/test/mutation-gate.test.mjs +++ b/test/mutation-gate.test.mjs @@ -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} не отобран по патчу`); diff --git a/test/validate-workflow.test.mjs b/test/validate-workflow.test.mjs index c88d1d6f..28b4e2f3 100644 --- a/test/validate-workflow.test.mjs +++ b/test/validate-workflow.test.mjs @@ -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"/);