mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
scripts/check-inputs.mjs declares every Validate check with its roots and entry points and computes the rest: imports and path literals of the entries, transitively for code, as leaves for data. classify-changes and gate-reuse both read it, so "which job runs" and "what its key hashes" cannot disagree any more. An executable file no check knows widens the run to the full set and is named in the summary; the coverage list makes such a file a red unit test rather than a permanent widening. The workflow file is a toolchain input of every job; backend no longer hashes src/**. Issue: #492 User-Visible: no
165 lines
9.4 KiB
JavaScript
165 lines
9.4 KiB
JavaScript
import test from 'node:test';
|
||
import assert from 'node:assert/strict';
|
||
import { execFileSync } from 'node:child_process';
|
||
import {
|
||
CHECK_OF_OUTPUT, CLASSIFIERS, OUTPUTS, PERF_PROFILES, classifyAll, classifyChanges, formatOutputs,
|
||
} from '../scripts/classify-changes.mjs';
|
||
import { manifest } from '../scripts/check-inputs.mjs';
|
||
|
||
// Классификация идёт из единого manifest входов (#492 §5.2) на РЕАЛЬНОМ
|
||
// дереве репозитория: тест доказывает решения job `changes` для настоящих
|
||
// файлов, а не для выдуманных путей. Manifest считается один раз.
|
||
//
|
||
// Пути, которые НЕ должны быть входами, собираются из кусков: строковый
|
||
// литерал в этом файле сам сделал бы их входом frontend (тесты читают то, что
|
||
// называют), и проверка стала бы самосбывающейся.
|
||
const MANIFEST = manifest(process.cwd());
|
||
const classify = (files) => classifyChanges(files, { manifest: MANIFEST });
|
||
const p = (...parts) => parts.join('/');
|
||
|
||
test('дифф по изометрии включает perf_iso и только его из перф-выходов (#473 AC8)', () => {
|
||
const out = classify(['src/iso-overlays.ts']);
|
||
assert.equal(out.perf_iso, 'true');
|
||
assert.equal(out.perf_interaction, 'false');
|
||
assert.equal(out.frontend, 'true');
|
||
assert.deepEqual(out.unknown, []);
|
||
});
|
||
|
||
test('дифф по живому пути и оркестраторам кадра включает perf_interaction (#473 AC8)', () => {
|
||
for (const file of ['src/live-hover.ts', 'src/render-invalidation.ts',
|
||
'src/houseplan-render-lifecycle.ts', 'src/houseplan-card.ts']) {
|
||
const out = classify([file]);
|
||
assert.equal(out.perf_interaction, 'true', file);
|
||
assert.equal(out.perf_iso, 'false', file);
|
||
}
|
||
});
|
||
|
||
test('дифф по документации не включает ни одного перф-профиля и ни одной job (#473 AC8)', () => {
|
||
const out = classify([p('docs', 'SUN.md')]);
|
||
assert.equal(out.perf_iso, 'false');
|
||
assert.equal(out.perf_interaction, 'false');
|
||
assert.equal(out.frontend, 'false');
|
||
assert.equal(out.backend, 'false');
|
||
});
|
||
|
||
test('тесты и демо перф-профили не включают: кадр они не замедляют', () => {
|
||
const out = classify(['test/iso-scene-render.test.mjs', 'demo/benchmark_large_house.mjs']);
|
||
assert.equal(out.perf_iso, 'false');
|
||
assert.equal(out.perf_interaction, 'false');
|
||
});
|
||
|
||
test('правка реестра мутантов даёт mutants=true; юниты реестра — тоже вход frontend (#475 r1, #492)', () => {
|
||
const out = classify(['scripts/mutation-gate.mjs']);
|
||
assert.equal(out.mutants, 'true');
|
||
assert.equal(out.frontend, 'true', 'test/mutation-gate.test.mjs читает реестр');
|
||
assert.equal(out.backend, 'false');
|
||
assert.equal(classify(['src/color.ts']).mutants, 'true', 'патчи мутантов лежат в src/**');
|
||
});
|
||
|
||
test('#492 §1.2: relay, converter, schema и pyproject запускают backend', () => {
|
||
for (const file of ['scripts/support-relay/relay.py', 'scripts/support-relay/hp_relay/app.py',
|
||
'scripts/sh3d-convert/convert.mjs', 'scripts/sh3d-convert/golden/two-levels.space-1.json',
|
||
'scripts/config-schema.json', 'scripts/dump-config-schema.py', 'pyproject.toml',
|
||
'custom_components/houseplan/frontend_registration.py']) {
|
||
const out = classify([file]);
|
||
assert.equal(out.backend, 'true', file);
|
||
assert.deepEqual(out.unknown, [], file);
|
||
}
|
||
});
|
||
|
||
test('#492 AC6: правка UI не классифицируется как backend', () => {
|
||
const out = classify(['src/houseplan-card.ts', 'src/houseplan-editor-runtime.ts']);
|
||
assert.equal(out.backend, 'false');
|
||
assert.equal(out.frontend, 'true');
|
||
});
|
||
|
||
test('интеграция: манифест, hacs.json, переводы (как в inline-shell до выноса)', () => {
|
||
assert.equal(classify(['custom_components/houseplan/manifest.json']).integration, 'true');
|
||
assert.equal(classify(['hacs.json']).integration, 'true');
|
||
assert.equal(classify(['custom_components/houseplan/translations/ru.json']).integration, 'true');
|
||
assert.equal(classify(['src/color.ts']).integration, 'false');
|
||
assert.equal(classify(['']).frontend, 'false');
|
||
});
|
||
|
||
test('копия бандла — не вход ни одной проверки (класс D)', () => {
|
||
const out = classify(['custom_components/houseplan/frontend/houseplan-card.js', 'dist/houseplan-card.js']);
|
||
for (const name of Object.keys(CHECK_OF_OUTPUT)) assert.equal(out[name], 'false', name);
|
||
assert.deepEqual(out.unknown, []);
|
||
});
|
||
|
||
test('#492 §5.2: неизвестный исполняемый вход расширяет прогон до полного набора и называется', () => {
|
||
const out = classify(['scripts/brand-new-gate.mjs']);
|
||
for (const name of OUTPUTS) assert.equal(out[name], 'true', name);
|
||
assert.deepEqual(out.unknown, ['scripts/brand-new-gate.mjs']);
|
||
// Объявленный не-вход и документация не расширяют.
|
||
const quiet = classify([p('demo', 'stand', 'README.md'), p('demo', 'shot_sun.mjs'), p('docs', 'new-page.md')]);
|
||
assert.deepEqual(quiet.unknown, []);
|
||
for (const name of OUTPUTS) assert.equal(quiet[name], 'false', name);
|
||
});
|
||
|
||
test('fallback --all выставляет каждый известный выход, включая перф-профили', () => {
|
||
const all = classifyAll();
|
||
assert.deepEqual(Object.keys(all).filter((k) => k !== 'unknown'), OUTPUTS);
|
||
assert.ok(OUTPUTS.every((name) => all[name] === 'true'));
|
||
assert.deepEqual(Object.keys(CLASSIFIERS), OUTPUTS);
|
||
assert.deepEqual(Object.keys(PERF_PROFILES), ['perf_iso', 'perf_interaction']);
|
||
});
|
||
|
||
test('CLI пишет формат $GITHUB_OUTPUT: stdin — список файлов, --all — всё true', () => {
|
||
const script = new URL('../scripts/classify-changes.mjs', import.meta.url).pathname;
|
||
const doc = p('docs', 'SUN.md');
|
||
const fromStdin = execFileSync('node', [script], { input: `src/iso-overlays.ts\n${doc}\n`, encoding: 'utf8' });
|
||
assert.equal(fromStdin, formatOutputs(classify(['src/iso-overlays.ts', doc])));
|
||
assert.match(fromStdin, /^perf_iso=true$/m);
|
||
assert.match(fromStdin, /^perf_interaction=false$/m);
|
||
assert.match(fromStdin, /^unknown_inputs=$/m);
|
||
const all = execFileSync('node', [script, '--all'], { input: '', encoding: 'utf8' });
|
||
assert.equal(all, OUTPUTS.map((name) => `${name}=true`).join('\n') + '\nunknown_inputs=\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');
|
||
});
|