mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
194 lines
12 KiB
JavaScript
194 lines
12 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, mutantsRequested,
|
||
} from '../scripts/classify-changes.mjs';
|
||
import { manifest } from '../scripts/check-inputs.mjs';
|
||
import { fileURLToPath } from 'node:url';
|
||
|
||
// Классификация идёт из единого 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('#548: parity выбирается по обоим зеркалам, fixture и toolchain, но не по чужому UI', () => {
|
||
for (const file of ['src/junction-limits.ts', 'src/space-geometry.ts',
|
||
'custom_components/houseplan/junction_limits.py',
|
||
'custom_components/houseplan/wall_segment_model.py',
|
||
'tests_backend/junction_parity.py', 'test/fixtures/junction-limits-parity.json',
|
||
'tsconfig.junction-parity.json', 'package-lock.json', '.nvmrc', '.python-version']) {
|
||
const out = classify([file]);
|
||
assert.equal(out.geometry_parity, 'true', file);
|
||
assert.deepEqual(out.unknown, [], file);
|
||
}
|
||
assert.equal(classify(['src/houseplan-card.ts']).geometry_parity, 'false');
|
||
assert.equal(classify(['custom_components/houseplan/websocket_api.py']).geometry_parity, 'false');
|
||
});
|
||
|
||
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', () => {
|
||
// fileURLToPath, не URL.pathname (#496): на Windows pathname даёт «/C:/…», Node ищет C:\C:\… .
|
||
const script = fileURLToPath(new URL('../scripts/classify-changes.mjs', import.meta.url));
|
||
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\nmutants_requested=false');
|
||
assert.equal(run({ EVENT_NAME: 'push', HEAD_MESSAGE: 'x\n\nRelease: v1.2.3' }), 'heavy=true\nmutants_requested=true');
|
||
assert.equal(run({ EVENT_NAME: 'workflow_dispatch', FULL_INPUT: 'true', HEAD_MESSAGE: '' }), 'heavy=true\nmutants_requested=true');
|
||
// #510: мутанты по кнопке без полного набора — вызов конвейера ревью и слияния
|
||
assert.equal(run({ EVENT_NAME: 'workflow_dispatch', FULL_INPUT: 'false', MUTANTS_INPUT: 'true', HEAD_MESSAGE: '' }), 'heavy=false\nmutants_requested=true');
|
||
});
|
||
|
||
test('#510 AC1: мутанты по диффу запрашиваются кандидатом, PR, ночью и по кнопке — не обычным пушем', () => {
|
||
const t = (env) => mutantsRequested(env);
|
||
assert.equal(t({ eventName: 'push', headMessage: 'fix: x\n\nIssue: #1\nUser-Visible: no' }), false, 'обычный push');
|
||
assert.equal(t({ eventName: 'push', headMessage: 'x\n\nRelease: v1.2.3' }), true, 'кандидат беты');
|
||
assert.equal(t({ eventName: 'pull_request' }), true);
|
||
assert.equal(t({ eventName: 'schedule' }), true);
|
||
assert.equal(t({ eventName: 'workflow_dispatch', fullInput: 'true' }), true);
|
||
assert.equal(t({ eventName: 'workflow_dispatch', fullInput: 'false', mutantsInput: 'true' }), true);
|
||
assert.equal(t({ eventName: 'workflow_dispatch', fullInput: 'false', mutantsInput: 'false' }), false, 'кнопка без запроса');
|
||
});
|