mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-30 19:58:50 +00:00
ci: guard asset inputs in Validate manifest
Issue: #671 User-Visible: no
This commit is contained in:
+4
-4
@@ -358,12 +358,12 @@ manifest читают и `classify-changes.mjs` (job `changes`: job запуск
|
||||
не зависит, а
|
||||
`custom_components/houseplan/manifest.json` (версия) держит правило «кандидат
|
||||
релиза прогоняет всё» и для него;
|
||||
- **неизвестный исполняемый вход** — файл под `scripts/`, `demo/`, `test/`,
|
||||
`tests_backend/`, `.github/`, `custom_components/`, `src/`, которого нет в
|
||||
manifest ни одной проверки, — расширяет прогон до полного набора и называется
|
||||
- `assets/furniture/**` — вход `frontend` (#671); `assets/fonts/**` — явное исключение ручного `generate-pdf-font.mjs`;
|
||||
- **неизвестный охраняемый вход** — исполняемый файл под `scripts/`, `demo/`, `test/`, `tests_backend/`, `.github/`, `custom_components/`, `src/` либо tracked-файл под `assets/**`, которого нет в manifest ни
|
||||
одной проверки, — расширяет прогон до полного набора и называется
|
||||
в summary. Лист покрытия (`node scripts/check-inputs.mjs --coverage`,
|
||||
`test/check-inputs.test.mjs`) требует, чтобы каждый такой файл был чьим-то
|
||||
входом либо стоял в `NOT_AN_INPUT` с причиной: новый скрипт без записи —
|
||||
входом либо стоял в `NOT_AN_INPUT` с причиной: новый скрипт или asset без записи —
|
||||
красный юнит, не вечное расширение прогонов;
|
||||
- **overlay принятых эталонов** (`demo/golden/baselines/**`, #573) — вход
|
||||
только `golden`, у которой он стоит явным корнем. Раскрытие каталога по
|
||||
|
||||
@@ -17,8 +17,9 @@
|
||||
// полного набора (§5.2): «не знаю» не равно «не влияет».
|
||||
//
|
||||
// Лист покрытия (§5.5, test/check-inputs.test.mjs): каждый отслеживаемый
|
||||
// исполняемый файл обязан входить в manifest хотя бы одной проверки либо в
|
||||
// NOT_AN_INPUT с причиной. Новый скрипт без записи — красный тест, не тихое
|
||||
// исполняемый файл и каждый файл под явно охраняемым корнем данных
|
||||
// обязан входить в manifest хотя бы одной проверки либо в NOT_AN_INPUT с
|
||||
// причиной. Новый скрипт или asset без записи — красный тест, не тихое
|
||||
// расширение прогонов навсегда.
|
||||
|
||||
import { execFileSync } from 'node:child_process';
|
||||
@@ -30,6 +31,14 @@ import { isMainModule } from './spawn-portable.mjs';
|
||||
export const EXECUTABLE_ROOTS = ['scripts', 'demo', 'test', 'tests_backend', '.github', 'custom_components', 'src'];
|
||||
export const EXECUTABLE_EXT = /\.(mjs|cjs|js|ts|py|json|ya?ml|html|toml|txt|sh)$/;
|
||||
|
||||
/**
|
||||
* Корни данных, где каждый tracked-файл обязан иметь владельца или явное
|
||||
* исключение. В отличие от EXECUTABLE_ROOTS здесь важны и бинарные файлы:
|
||||
* тесты могут читать SVG/TTF по динамически собранному пути, который сканер не
|
||||
* выведет из одного строкового литерала (#671).
|
||||
*/
|
||||
export const GUARDED_DATA_ROOTS = ['assets'];
|
||||
|
||||
/** Копии бандла и результаты сборки: класс D, входом не являются. */
|
||||
export const BUILD_OUTPUT = [
|
||||
'dist/**', 'custom_components/houseplan/frontend/**', 'demo/srv/assets/**',
|
||||
@@ -77,6 +86,7 @@ export const NOT_AN_INPUT = [
|
||||
['scripts/support-relay/deploy/**', 'деплой relay на стенд'],
|
||||
['scripts/wsl-setup.sh', 'установка локального Linux/WSL-контура с пинами CI (#496), ручной запуск'],
|
||||
['scripts/windows-toolchain.ps1', 'изолированная установка и запуск Windows toolchain с пинами CI (#557), ручной запуск'],
|
||||
['assets/fonts/**', 'исходный TTF читает только ручной generate-pdf-font.mjs; Validate использует закоммиченный результат'],
|
||||
['.github/ISSUE_TEMPLATE/**', 'шаблоны issue GitHub, не исполняются'],
|
||||
['.githooks/**', 'локальные хуки'],
|
||||
];
|
||||
@@ -323,7 +333,7 @@ export const CHECKS = {
|
||||
entries: ['test/*.test.mjs', 'scripts/no-new-any.mjs', 'scripts/no-new-private-writes.mjs', 'scripts/render-layout-read.mjs', 'scripts/bundle-budget.mjs',
|
||||
'scripts/fix-test-build.mjs', 'scripts/unused-locals-gate.mjs'],
|
||||
// demo/helpers/** — область no-new-private-writes (#629): гейт читает их текст.
|
||||
roots: [...BUILD_INPUTS, 'test/**', 'tsconfig*.json', 'scripts/monolith-baseline.json', 'demo/smoke_*.mjs', 'demo/benchmark_*.mjs', 'demo/guard/*.mjs', 'demo/helpers/**', ...WORKFLOW],
|
||||
roots: [...BUILD_INPUTS, 'assets/furniture/**', 'test/**', 'tsconfig*.json', 'scripts/monolith-baseline.json', 'demo/smoke_*.mjs', 'demo/benchmark_*.mjs', 'demo/guard/*.mjs', 'demo/helpers/**', ...WORKFLOW],
|
||||
},
|
||||
changed_mutants: {
|
||||
entries: ['scripts/mutation-*.mjs', 'scripts/*-guard.mjs', 'test/*.test.mjs', 'demo/smoke_*.mjs', 'tests_backend/**/*.py'],
|
||||
@@ -401,6 +411,10 @@ export function manifest(root = process.cwd(), options = {}) {
|
||||
export const isExecutableInput = (file) => EXECUTABLE_ROOTS.some((r) => file === r || file.startsWith(`${r}/`))
|
||||
&& EXECUTABLE_EXT.test(file) && !isBuildOutput(file);
|
||||
|
||||
/** Вход, который не имеет права тихо выпасть из manifest. */
|
||||
export const isGuardedInput = (file) => isExecutableInput(file)
|
||||
|| GUARDED_DATA_ROOTS.some((r) => file === r || file.startsWith(`${r}/`));
|
||||
|
||||
export const isDeclaredNotAnInput = (file) => NOT_AN_INPUT.some(([glob]) => globToRegExp(glob).test(file));
|
||||
|
||||
/**
|
||||
@@ -414,19 +428,19 @@ export function checksAffectedBy(files, root = process.cwd(), options = {}) {
|
||||
for (const file of files) {
|
||||
let known = false;
|
||||
for (const name of CHECK_NAMES) if (man[name].has(file)) { affected.add(name); known = true; }
|
||||
if (!known && isExecutableInput(file) && !isDeclaredNotAnInput(file)) unknown.push(file);
|
||||
if (!known && isGuardedInput(file) && !isDeclaredNotAnInput(file)) unknown.push(file);
|
||||
}
|
||||
if (unknown.length) for (const name of CHECK_NAMES) affected.add(name);
|
||||
return { affected, unknown };
|
||||
}
|
||||
|
||||
/** Лист покрытия (§5.5): исполняемые файлы, которые никто не считает своими. */
|
||||
/** Лист покрытия (§5.5): исполняемые и охраняемые data-файлы без владельца. */
|
||||
export function coverage(root = process.cwd(), options = {}) {
|
||||
const tracked = options.tracked || trackedFiles(root);
|
||||
const man = options.manifest || manifest(root, { ...options, tracked });
|
||||
const covered = new Set();
|
||||
for (const name of CHECK_NAMES) for (const f of man[name]) covered.add(f);
|
||||
const unknown = tracked.filter((f) => isExecutableInput(f) && !covered.has(f) && !isDeclaredNotAnInput(f));
|
||||
const unknown = tracked.filter((f) => isGuardedInput(f) && !covered.has(f) && !isDeclaredNotAnInput(f));
|
||||
// запись NOT_AN_INPUT лишняя, если ВСЕ её файлы и так чьи-то входы
|
||||
const declaredButCovered = NOT_AN_INPUT.map(([glob]) => glob).filter((glob) => {
|
||||
const hits = tracked.filter((f) => globToRegExp(glob).test(f));
|
||||
|
||||
@@ -5196,6 +5196,28 @@ const MUTANT_DEFINITIONS = [
|
||||
replace: ' // mutant: dynamic cross-runtime inputs are absent',
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'furniture-assets-dropped-from-frontend-inputs',
|
||||
guard: 'node --test --test-name-pattern="#671: весь пакет мебели" test/check-inputs.test.mjs',
|
||||
because: 'тесты frontend читают assets/furniture по составным путям; без явного корня '
|
||||
+ 'правка пакета не выбирает frontend и не запускает сверку generated-модулей (#671)',
|
||||
patches: [{
|
||||
file: 'scripts/check-inputs.mjs',
|
||||
find: " roots: [...BUILD_INPUTS, 'assets/furniture/**', 'test/**',",
|
||||
replace: " roots: [...BUILD_INPUTS, 'test/**', // mutant: furniture package is not a frontend input",
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'assets-data-root-not-guarded',
|
||||
guard: 'node --test --test-name-pattern="#671: новый assets" test/check-inputs.test.mjs',
|
||||
because: 'лист покрытия, который смотрит только на исполняемые расширения, снова '
|
||||
+ 'пропустит любой новый бинарный или текстовый пакет данных под assets/** (#671)',
|
||||
patches: [{
|
||||
file: 'scripts/check-inputs.mjs',
|
||||
find: "export const GUARDED_DATA_ROOTS = ['assets'];",
|
||||
replace: 'export const GUARDED_DATA_ROOTS = []; // mutant: asset data is not audited',
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'guard-inputs-ignore-wrapper-defaults',
|
||||
guard: 'node --test --test-name-pattern="#492 §8.2" test/mutation-gate.test.mjs',
|
||||
|
||||
@@ -4,7 +4,8 @@ import { execFileSync } from 'node:child_process';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import {
|
||||
BASELINE_OVERLAY, CHECKS, CHECK_NAMES, NOT_AN_INPUT, REUSE_JOBS, checksAffectedBy, closure, coverage,
|
||||
globToRegExp, inputsOf, isBaselineOverlay, isDeclaredNotAnInput, isExecutableInput, manifest, referencesOf,
|
||||
GUARDED_DATA_ROOTS, globToRegExp, inputsOf, isBaselineOverlay, isDeclaredNotAnInput, isExecutableInput,
|
||||
isGuardedInput, manifest, referencesOf,
|
||||
stripComments,
|
||||
} from '../scripts/check-inputs.mjs';
|
||||
|
||||
@@ -258,6 +259,32 @@ test('#542: каждый динамический cross-runtime input выбир
|
||||
assert.ok(!checksAffectedBy([unrelated], ROOT, { manifest: MANIFEST }).affected.has('backend'));
|
||||
});
|
||||
|
||||
test('#671: весь пакет мебели — точный вход frontend, а шрифты исключены явно', () => {
|
||||
const furniture = p('assets', 'furniture', 'houseplan-0.4.1', 'svg', 'menu', 'air_conditioner.svg');
|
||||
const font = p('assets', 'fonts', 'Roboto-Regular.ttf');
|
||||
assert.deepEqual(GUARDED_DATA_ROOTS, ['assets']);
|
||||
assert.ok(isGuardedInput(furniture));
|
||||
assert.ok(MANIFEST.frontend.has(furniture), `${furniture}: frontend потерял пакет`);
|
||||
const { affected, unknown } = checksAffectedBy([furniture], ROOT, { manifest: MANIFEST });
|
||||
assert.deepEqual([...affected], ['frontend']);
|
||||
assert.deepEqual(unknown, []);
|
||||
assert.ok(isDeclaredNotAnInput(font), `${font}: ручной генератор должен быть явным исключением`);
|
||||
assert.equal(MANIFEST.frontend.has(font), false);
|
||||
});
|
||||
|
||||
test('#671: новый assets/** без владельца не молчит, а расширяет прогон', () => {
|
||||
const orphan = p('assets', 'future-pack', 'new.bin');
|
||||
const { affected, unknown } = checksAffectedBy([orphan], ROOT, { manifest: MANIFEST });
|
||||
assert.deepEqual(unknown, [orphan]);
|
||||
assert.deepEqual([...affected].sort(), [...CHECK_NAMES].sort());
|
||||
|
||||
const synthetic = coverage('/virtual', {
|
||||
tracked: [orphan],
|
||||
manifest: Object.fromEntries(CHECK_NAMES.map((name) => [name, new Set()])),
|
||||
});
|
||||
assert.deepEqual(synthetic.unknown, [orphan]);
|
||||
});
|
||||
|
||||
test('§8.1 обратная проба (AC6): UI — не вход backend, backend — не вход браузерных job без причины', () => {
|
||||
for (const file of ['src/houseplan-card.ts', 'src/houseplan-editor-runtime.ts', 'src/iso-overlays.ts', 'package.json']) {
|
||||
assert.ok(!MANIFEST.backend.has(file), `backend зависит от ${file}`);
|
||||
|
||||
Reference in New Issue
Block a user