Files
Claudeandclaude[bot] 88c4e4e9ae infra(process): bundle and review index change only on the way to dev (#657)
Решения владельца: 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
2026-09-26 07:28:23 +00:00

74 lines
4.4 KiB
JavaScript
Raw Permalink 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 { 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\)/);
});