mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
Решения владельца: 1б — индекс ревью не пересобирается в ветке задачи, только коммитами, идущими в dev; 2б — бандл меняет только кандидат беты/релиза, стенд dev берёт его из артефакта Validate. - scripts/bundle-policy.mjs: коммит, трогающий dist/** или custom_components/houseplan/frontend/**, обязан нести Release: (хук commit-msg и история в CI через validate-commit-provenance; коммиты с датой автора до 2026-09-27 не судятся); --verify судит целостность свежей сборки всегда, побайтовую сверку с закоммиченной копией — только на коммите, меняющем бандл, или кандидате; --clean. - release-prerelease: публикация отказывает, если отпечаток исходников в закоммиченном манифесте не равен отпечатку дерева (хотфикс поверх кандидата без пересборки). - bundle-sync: по умолчанию только demo/srv/assets; --release (npm run bundle:release) — ещё и custom_components. - rebase-on-dev: конфликт в бандле берёт копию dev, без пересборки и amend. - validate.yml: job dev_build публикует card-bundle головы dev в сиротскую ветку dev-build (scripts/dev-build.mjs); стенд накладывает её demo/stand/update-dev-bundle.sh. - _process.yml: индекс ревью больше не пересобирается при приведении к dev и при публикации документа в ветку задачи. - golden-wsl-artifact/golden-container: сборка перед съёмкой не считается правкой источника, после — bundle:clean. - test/bundle-tree-committed: судит закоммиченный снимок, не диск. - 11 мутантов в реестре; PROCESS/AGENTS/DEVELOPMENT/AUTHOR/REVIEWER. Issue: #657 User-Visible: no
74 lines
4.4 KiB
JavaScript
74 lines
4.4 KiB
JavaScript
import test from 'node:test';
|
||
import assert from 'node:assert/strict';
|
||
import { readFileSync } from 'node:fs';
|
||
import { parseArgs, parallelSteps, postBuildSteps, summarize } from '../scripts/gate-small.mjs';
|
||
|
||
// #479 AC6: одна команда вместо списка §8 — состав обязательной части закреплён,
|
||
// информационный шаг (smoke-select) не считается падением, сверка бандла идёт
|
||
// после сборки.
|
||
|
||
test('gate:small гоняет обязательную часть PROCESS §8 и сверяет бандл (#479)', () => {
|
||
const names = parallelSteps('origin/dev').map((s) => `${s.cmd} ${s.args.join(' ')}`);
|
||
assert.ok(!names.some((n) => n.endsWith('npm test') || n.endsWith('npm.cmd test')),
|
||
'юниты не должны гоняться одновременно со сборкой, которая пересоздаёт dist');
|
||
assert.ok(names.some((n) => n.includes('run build')));
|
||
assert.ok(names.some((n) => n.includes('scripts/no-new-any.mjs --base origin/dev --head HEAD')));
|
||
assert.ok(names.some((n) => n.includes('scripts/no-new-private-writes.mjs --base origin/dev --head HEAD')),
|
||
'#629: гейт приватных записей смоков обязан идти в локальном наборе');
|
||
assert.ok(names.some((n) => n.includes('scripts/smoke-select.mjs --base origin/dev --head HEAD')));
|
||
const serial = postBuildSteps().map((s) => s.args.join(' '));
|
||
assert.equal(serial[0], 'test', 'юниты первыми читают уже готовый свежий dist');
|
||
assert.ok(serial.some((s) => s.includes('bundle-policy.mjs --verify HEAD')));
|
||
assert.ok(serial.some((s) => s.includes('bundle:budget')));
|
||
assert.equal(parseArgs(['--base=abc']).base, 'abc');
|
||
assert.equal(parseArgs([]).base, 'origin/dev');
|
||
assert.equal(JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).scripts['gate:small'],
|
||
'node scripts/gate-small.mjs');
|
||
});
|
||
|
||
test('сводка: информационный шаг не падение, упавший шаг с подсказкой (#479)', () => {
|
||
const { lines, failed } = summarize([
|
||
{ name: 'a', code: 0, ms: 1000 },
|
||
{ name: 'select', code: 1, ms: 10, informational: true },
|
||
{ name: 'bundle', code: 1, ms: 10, hint: 'npm run bundle:sync' },
|
||
]);
|
||
assert.equal(failed, 1);
|
||
assert.match(lines[0], /^ok /);
|
||
assert.match(lines[1], /^info/);
|
||
assert.match(lines[2], /^FAIL.*→ npm run bundle:sync/);
|
||
});
|
||
|
||
// #496: browser-consumers отделены от подготовки артефактов и идут после неё.
|
||
import { runLimited, smokesToRun } from '../scripts/gate-small.mjs';
|
||
|
||
test('smokesToRun берёт прямые и зарегистрированные смоки, не «широкие»; без диффа — пусто (#496)', () => {
|
||
assert.deepEqual(smokesToRun({ noExecutableDiff: true, direct: [{ smoke: 'smoke_x.mjs' }] }), []);
|
||
assert.deepEqual(smokesToRun({
|
||
direct: [{ smoke: 'smoke_b.mjs', strong: true }, { smoke: 'smoke_a.mjs', strong: false }],
|
||
registered: [{ smoke: 'smoke_b.mjs' }, { smoke: 'smoke_c.mjs' }],
|
||
broad: [{ smoke: 'smoke_everything.mjs' }],
|
||
}), ['smoke_a.mjs', 'smoke_b.mjs', 'smoke_c.mjs']);
|
||
assert.deepEqual(smokesToRun(null), []);
|
||
assert.deepEqual(parseArgs(['--smokes', '--jobs=3']), { base: 'origin/dev', smokes: true, jobs: 3 });
|
||
assert.deepEqual(parseArgs(['--jobs=abc']).jobs, 2);
|
||
});
|
||
|
||
test('runLimited держит не больше N одновременных consumers и сохраняет порядок (#496)', async () => {
|
||
let active = 0; let peak = 0;
|
||
const out = await runLimited([1, 2, 3, 4, 5], 2, async (n) => {
|
||
active++; peak = Math.max(peak, active);
|
||
await new Promise((r) => setTimeout(r, 5));
|
||
active--; return n * 10;
|
||
});
|
||
assert.deepEqual(out, [10, 20, 30, 40, 50]);
|
||
assert.equal(peak, 2);
|
||
});
|
||
|
||
test('gate-small: смоки идут только после bundle-sync и только с --smokes (#496)', () => {
|
||
const source = readFileSync(new URL('../scripts/gate-small.mjs', import.meta.url), 'utf8');
|
||
const smokesAt = source.indexOf("args: [`demo/${smoke}`]");
|
||
const syncAt = source.indexOf("args: ['scripts/bundle-sync.mjs']");
|
||
assert.ok(syncAt > 0 && smokesAt > syncAt, 'bundle-sync предшествует запуску смоков');
|
||
assert.match(source, /if \(smokes && buildOk\)/);
|
||
});
|