Files
houseplan-card/test/classify-changes.test.mjs
T
Codex 0145f9bafe ci: правка реестра мутантов сама запускает гейт по диффу (#475 r1)
Классификатор отдаёт выход mutants по scripts/mutation-gate.mjs, job
changed_mutants получает третий дизъюнкт из ТЗ §2; fallback-ветки
выставляют mutants=true. Тест AC7 отбирает бэкенд-мутанты по файлу патча.

Issue: #475
User-Visible: no
2026-09-06 19:33:08 +03:00

119 lines
6.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import test from 'node:test';
import assert from 'node:assert/strict';
import { execFileSync } from 'node:child_process';
import { classifyChanges, classifyAll, formatOutputs, OUTPUTS, CLASSIFIERS } from '../scripts/classify-changes.mjs';
// #473 AC8: диффозависимость перф-смока доказана на самой функции
// классификации, которую исполняет job `changes`.
test('дифф по изометрии включает perf_iso и только его из перф-выходов (#473 AC8)', () => {
const out = classifyChanges(['src/iso-x.ts']);
assert.equal(out.perf_iso, 'true');
assert.equal(out.perf_interaction, 'false');
assert.equal(out.frontend, 'true');
});
test('дифф по живому пути и оркестраторам кадра включает perf_interaction (#473 AC8)', () => {
for (const file of ['src/live-x.ts', 'src/render-invalidation.ts',
'src/houseplan-render-lifecycle.ts', 'src/houseplan-card.ts']) {
const out = classifyChanges([file]);
assert.equal(out.perf_interaction, 'true', file);
assert.equal(out.perf_iso, 'false', file);
}
});
test('дифф по документации не включает ни одного перф-профиля (#473 AC8)', () => {
const out = classifyChanges(['docs/x.md']);
assert.equal(out.perf_iso, 'false');
assert.equal(out.perf_interaction, 'false');
assert.equal(out.frontend, 'false');
});
test('тесты и демо перф-профили не включают: кадр они не замедляют', () => {
const out = classifyChanges(['test/iso-scene-render.test.mjs', 'demo/benchmark_large_house.mjs', 'src/iso-x.test.ts.md']);
assert.equal(out.perf_iso, 'false');
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', mutants: 'false' });
assert.deepEqual(classifyChanges(['custom_components/houseplan/frontend/houseplan-card.js']),
{ 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', mutants: 'false' });
assert.equal(classifyChanges(['scripts/support-relay/x.py']).backend, 'true');
assert.equal(classifyChanges(['']).frontend, 'false');
});
test('fallback --all выставляет каждый известный выход, включая перф-профили', () => {
const all = classifyAll();
assert.deepEqual(Object.keys(all), OUTPUTS);
assert.ok(OUTPUTS.every((name) => all[name] === 'true'));
assert.deepEqual(Object.keys(CLASSIFIERS), OUTPUTS);
});
test('CLI пишет формат $GITHUB_OUTPUT: stdin — список файлов, --all — всё true', () => {
const script = new URL('../scripts/classify-changes.mjs', import.meta.url).pathname;
const fromStdin = execFileSync('node', [script], { input: 'src/iso-x.ts\ndocs/x.md\n', encoding: 'utf8' });
assert.equal(fromStdin, formatOutputs(classifyChanges(['src/iso-x.ts', 'docs/x.md'])));
assert.match(fromStdin, /^perf_iso=true$/m);
assert.match(fromStdin, /^perf_interaction=false$/m);
const all = execFileSync('node', [script, '--all'], { input: '', encoding: 'utf8' });
assert.equal(all, OUTPUTS.map((name) => `${name}=true`).join('\n') + '\n');
});
// #479: тяжёлые job идут на кандидате беты, по кнопке, на PR и по расписанию —
// и НЕ идут на обычном пуше. Обе стороны доказаны на самой функции, которую
// исполняет шаг `heavy` job `changes`.
import { heavyGatesRequested, hasReleaseTrailer } from '../scripts/classify-changes.mjs';
test('обычный push в dev не запрашивает тяжёлые job (#479)', () => {
assert.equal(heavyGatesRequested({
eventName: 'push',
headMessage: 'fix: speed up wall-chain commits\n\nIssue: #461\nUser-Visible: yes\n',
}), false);
assert.equal(heavyGatesRequested({ eventName: 'push', headMessage: '' }), false);
assert.equal(heavyGatesRequested({}), false);
});
test('кандидат беты и релиза — трейлер Release: — запрашивает тяжёлые job (#479)', () => {
assert.equal(heavyGatesRequested({
eventName: 'push',
headMessage: 'build: prepare v1.73.0-beta.1 candidate\n\nIssue: #160\nUser-Visible: yes\nRelease: v1.73.0-beta.1\n',
}), true);
assert.equal(hasReleaseTrailer('Release v1.72.0\n\nRelease: v1.72.0'), true);
// Слово в теле — не трейлер: строка должна начинаться с `Release:`.
assert.equal(hasReleaseTrailer('docs: mention the Release: process in AGENTS'), false);
assert.equal(hasReleaseTrailer('Release: soon'), false);
});
test('workflow_dispatch запрашивает тяжёлые job только с full=true (#479)', () => {
assert.equal(heavyGatesRequested({ eventName: 'workflow_dispatch', fullInput: 'true' }), true);
assert.equal(heavyGatesRequested({ eventName: 'workflow_dispatch', fullInput: true }), true);
assert.equal(heavyGatesRequested({ eventName: 'workflow_dispatch', fullInput: 'false' }), false);
assert.equal(heavyGatesRequested({ eventName: 'workflow_dispatch', fullInput: '' }), false);
});
test('pull_request и schedule всегда запрашивают тяжёлые job (#479)', () => {
assert.equal(heavyGatesRequested({ eventName: 'pull_request', headMessage: 'x' }), true);
assert.equal(heavyGatesRequested({ eventName: 'schedule' }), true);
});
test('CLI --heavy читает событие и сообщение из окружения (#479)', () => {
const run = (env) => execFileSync(process.execPath, ['scripts/classify-changes.mjs', '--heavy'], {
encoding: 'utf8', env: { ...process.env, ...env },
}).trim();
assert.equal(run({ EVENT_NAME: 'push', HEAD_MESSAGE: 'fix: x\n\nIssue: #1\nUser-Visible: no' }), 'heavy=false');
assert.equal(run({ EVENT_NAME: 'push', HEAD_MESSAGE: 'x\n\nRelease: v1.2.3' }), 'heavy=true');
assert.equal(run({ EVENT_NAME: 'workflow_dispatch', FULL_INPUT: 'true', HEAD_MESSAGE: '' }), 'heavy=true');
});