Files
houseplan-card/test/bundle-sync.test.mjs
T
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

124 lines
5.1 KiB
JavaScript

import assert from 'node:assert/strict';
import { spawnSync } from 'node:child_process';
import { createHash } from 'node:crypto';
import {
existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync,
} from 'node:fs';
import { dirname, join } from 'node:path';
import { tmpdir } from 'node:os';
import test from 'node:test';
const sha256 = (contents) => createHash('sha256').update(contents).digest('hex');
const writeCurrentBundle = (root) => {
const contents = new Map([
['houseplan-card.js', 'card facade'],
['houseplan-panel.js', 'panel shell'],
['houseplan-assets/card-HASH.js', 'shared card implementation'],
]);
for (const [name, value] of contents) {
const path = join(root, 'dist', name);
mkdirSync(dirname(path), { recursive: true });
writeFileSync(path, value);
}
const files = [...contents].map(([path, value]) => ({
path,
sha256: sha256(value),
rawBytes: Buffer.byteLength(value),
gzipBytes: path === 'houseplan-card.js' ? 5 : path === 'houseplan-panel.js' ? 3 : 7,
isEntry: path === 'houseplan-card.js' || path === 'houseplan-panel.js',
imports: path === 'houseplan-panel.js'
? ['houseplan-assets/card-HASH.js']
: path === 'houseplan-card.js' ? ['houseplan-assets/card-HASH.js'] : [],
dynamicImports: [],
}));
const manifest = {
schema: 1,
fingerprint: 'f'.repeat(64),
entry: 'houseplan-card.js',
panelEntry: 'houseplan-panel.js',
initialViewFiles: ['houseplan-assets/card-HASH.js', 'houseplan-card.js'],
initialViewGzipBytes: 12,
initialPanelFiles: [
'houseplan-assets/card-HASH.js', 'houseplan-panel.js',
],
initialPanelGzipBytes: 10,
initialPanelOnlyFiles: ['houseplan-panel.js'],
initialPanelOnlyGzipBytes: 3,
files,
};
writeFileSync(join(root, 'dist/houseplan-assets.json'), `${JSON.stringify(manifest)}\n`);
return manifest;
};
const writeLegacyTarget = (root, target) => {
const targetRoot = join(root, target);
mkdirSync(join(targetRoot, 'houseplan-assets'), { recursive: true });
writeFileSync(join(targetRoot, 'houseplan-card.js'), 'old card');
writeFileSync(join(targetRoot, 'houseplan-legacy.js'), 'old root entry');
writeFileSync(join(targetRoot, 'houseplan-assets/old-HASH.js'), 'old chunk');
writeFileSync(join(targetRoot, 'houseplan-assets.json'), JSON.stringify({
schema: 1,
fingerprint: 'legacy',
entry: 'houseplan-card.js',
files: [
{ path: 'houseplan-card.js' },
{ path: 'houseplan-legacy.js' },
{ path: 'houseplan-assets/old-HASH.js' },
],
}));
};
test('#486/#657 bundle sync materializes both entries, the HACS copy only with --release', () => {
const root = mkdtempSync(join(tmpdir(), 'houseplan-bundle-sync-'));
try {
mkdirSync(join(root, 'scripts'), { recursive: true });
for (const name of ['bundle-sync.mjs', 'bundle-tree.mjs']) {
writeFileSync(
join(root, 'scripts', name),
readFileSync(new URL(`../scripts/${name}`, import.meta.url)),
);
}
const manifest = writeCurrentBundle(root);
for (const target of [
'custom_components/houseplan/frontend', 'demo/srv/assets',
]) writeLegacyTarget(root, target);
// #657: без --release копия HACS не трогается — бандл меняет только кандидат.
const demoOnly = spawnSync(process.execPath, ['scripts/bundle-sync.mjs'], {
cwd: root, encoding: 'utf8',
});
assert.equal(demoOnly.status, 0, `${demoOnly.stdout}\n${demoOnly.stderr}`);
assert.equal(readFileSync(join(root, 'demo/srv/assets/houseplan-card.js'), 'utf8'), 'card facade');
assert.equal(readFileSync(join(root, 'custom_components/houseplan/frontend/houseplan-card.js'), 'utf8'), 'old card',
'обычная раскладка не должна менять закоммиченный бандл (#657)');
assert.equal(existsSync(join(root, 'custom_components/houseplan/frontend/houseplan-legacy.js')), true);
const run = spawnSync(process.execPath, ['scripts/bundle-sync.mjs', '--release'], {
cwd: root, encoding: 'utf8',
});
assert.equal(run.status, 0, `${run.stdout}\n${run.stderr}`);
for (const target of [
'custom_components/houseplan/frontend', 'demo/srv/assets',
]) {
assert.equal(readFileSync(join(root, target, 'houseplan-card.js'), 'utf8'), 'card facade');
assert.equal(readFileSync(join(root, target, 'houseplan-panel.js'), 'utf8'), 'panel shell');
assert.deepEqual(
JSON.parse(readFileSync(join(root, target, 'houseplan-assets.json'), 'utf8')),
manifest,
);
assert.equal(existsSync(join(root, target, 'houseplan-legacy.js')), false);
assert.equal(existsSync(join(root, target, 'houseplan-assets/old-HASH.js')), false);
}
writeFileSync(join(root, 'dist/houseplan-orphan.js'), 'unlisted entry');
const orphan = spawnSync(process.execPath, ['scripts/bundle-sync.mjs'], {
cwd: root, encoding: 'utf8',
});
assert.notEqual(orphan.status, 0);
assert.match(`${orphan.stdout}${orphan.stderr}`, /orphan bundle asset: houseplan-orphan\.js/);
} finally {
rmSync(root, { recursive: true, force: true });
}
});