Files
houseplan-card/test/dev-build.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

127 lines
7.1 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.
// #657 (2б): стенд dev берёт бандл из артефакта Validate через ветку dev-build,
// а не из дерева dev, где бандл меняет только кандидат.
import assert from 'node:assert/strict';
import { execFileSync, spawnSync } from 'node:child_process';
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import test from 'node:test';
import { fileURLToPath } from 'node:url';
import { createHash } from 'node:crypto';
import {
DEV_BUILD_MARKER, DEV_BUILD_TARGET, parseArgs, publishDevBuild,
} from '../scripts/dev-build.mjs';
const STAND = fileURLToPath(new URL('../demo/stand/update-dev-bundle.sh', import.meta.url));
const ENV = { GIT_AUTHOR_NAME: 't', GIT_AUTHOR_EMAIL: 't@t', GIT_COMMITTER_NAME: 't', GIT_COMMITTER_EMAIL: 't@t' };
const git = (cwd, ...args) => execFileSync('git', args, {
cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], env: { ...process.env, ...ENV },
}).trim();
const sha256 = (text) => createHash('sha256').update(text).digest('hex');
function writeDist(root, card) {
const dir = join(root, 'dist');
mkdirSync(join(dir, 'houseplan-assets'), { recursive: true });
const entries = [['houseplan-card.js', card], ['houseplan-panel.js', 'panel'], ['houseplan-assets/c-HASH.js', `chunk ${card}`]];
for (const [name, value] of entries) writeFileSync(join(dir, name), value);
const files = entries.map(([path, value]) => ({
path, sha256: sha256(value), rawBytes: value.length, gzipBytes: 1,
isEntry: !path.includes('/'), imports: [], dynamicImports: [],
}));
writeFileSync(join(dir, 'houseplan-assets.json'), `${JSON.stringify({
schema: 1, fingerprint: 'a'.repeat(64), entry: 'houseplan-card.js', panelEntry: 'houseplan-panel.js',
initialViewFiles: ['houseplan-card.js'], initialViewGzipBytes: 1,
initialPanelFiles: ['houseplan-panel.js'], initialPanelGzipBytes: 1,
initialPanelOnlyFiles: ['houseplan-panel.js'], initialPanelOnlyGzipBytes: 1, files,
})}\n`);
}
function fixture() {
const root = mkdtempSync(join(tmpdir(), 'hp-dev-build-test-'));
const origin = join(root, 'origin.git');
const work = join(root, 'work');
git(root, 'init', '--bare', '-q', '-b', 'dev', origin);
git(root, 'clone', '-q', origin, work);
git(work, 'checkout', '-q', '-b', 'dev');
mkdirSync(join(work, DEV_BUILD_TARGET), { recursive: true });
writeFileSync(join(work, DEV_BUILD_TARGET, 'houseplan-card.js'), 'card of the last beta');
writeFileSync(join(work, DEV_BUILD_TARGET, 'houseplan-assets.json'), '{}\n');
writeFileSync(join(work, 'src.ts'), 'v1');
git(work, 'add', '-A');
git(work, 'commit', '-q', '-m', 'base');
git(work, 'push', '-q', '-u', 'origin', 'dev');
return { root, origin, work, head: git(work, 'rev-parse', 'HEAD') };
}
test('#657 parseArgs: значения по умолчанию и обе формы флагов', () => {
assert.deepEqual(parseArgs(['--sha', 'x']), {
sha: 'x', dist: 'dist', remote: 'origin', branch: 'dev-build', expectRef: 'refs/heads/dev', dryRun: false,
});
assert.equal(parseArgs(['--sha=y', '--dry-run']).dryRun, true);
assert.equal(parseArgs(['--sha=y']).sha, 'y');
});
test('#657 dev-build: одна ветка без истории с бандлом головы dev и SHA источника', () => {
const { root, origin, work, head } = fixture();
try {
writeDist(work, 'fresh card');
const first = publishDevBuild({ cwd: work, sha: head, log: () => {} });
assert.equal(first.published, true);
const branchTip = git(origin, 'rev-parse', 'refs/heads/dev-build');
assert.equal(git(origin, 'rev-list', '--count', branchTip), '1', 'коммит без родителей');
assert.equal(git(origin, 'show', `${branchTip}:${DEV_BUILD_TARGET}/houseplan-card.js`), 'fresh card');
const marker = JSON.parse(git(origin, 'show', `${branchTip}:${DEV_BUILD_MARKER}`));
assert.equal(marker.source, head);
assert.equal(marker.files, 3);
assert.equal(git(work, 'status', '--porcelain', '--', DEV_BUILD_TARGET), '',
'рабочее дерево и индекс источника не тронуты');
assert.equal(git(origin, 'rev-parse', 'refs/heads/dev'), head, 'dev не тронут');
// Второй прогон заменяет ветку целиком — истории не копится.
writeDist(work, 'fresher card');
publishDevBuild({ cwd: work, sha: head, log: () => {} });
const second = git(origin, 'rev-parse', 'refs/heads/dev-build');
assert.notEqual(second, branchTip);
assert.equal(git(origin, 'rev-list', '--count', second), '1');
} finally { rmSync(root, { recursive: true, force: true }); }
});
test('#657 dev-build: dev ушёл вперёд — прогон не пушит устаревшую сборку', () => {
const { root, origin, work, head } = fixture();
try {
writeFileSync(join(work, 'src.ts'), 'v2');
git(work, 'commit', '-qam', 'next');
git(work, 'push', '-q', 'origin', 'dev');
writeDist(work, 'card of the older head');
const lines = [];
const result = publishDevBuild({ cwd: work, sha: head, log: (l) => lines.push(l) });
assert.deepEqual({ published: result.published, reason: result.reason }, { published: false, reason: 'stale' });
assert.equal(git(origin, 'for-each-ref', 'refs/heads/dev-build'), '', 'ветка не создана');
assert.match(lines.join('\n'), /публикует следующий прогон/);
assert.throws(() => publishDevBuild({ cwd: work, sha: 'HEAD', log: () => {} }), /full commit SHA/);
} finally { rmSync(root, { recursive: true, force: true }); }
});
test('#657 стенд: update-dev-bundle.sh ставит бандл dev-build поверх рабочей копии и умеет вернуть её до pull', () => {
const { root, origin, work, head } = fixture();
try {
writeDist(work, 'fresh card');
publishDevBuild({ cwd: work, sha: head, log: () => {} });
const stand = join(root, 'stand');
git(root, 'clone', '-q', '-b', 'dev', origin, stand);
writeFileSync(join(stand, DEV_BUILD_TARGET, 'stale-orphan.js'), 'left over');
const run = spawnSync('bash', [STAND, stand], { encoding: 'utf8', env: { ...process.env, ...ENV } });
assert.equal(run.status, 0, run.stderr);
assert.equal(readFileSync(join(stand, DEV_BUILD_TARGET, 'houseplan-card.js'), 'utf8'), 'fresh card');
assert.equal(existsSync(join(stand, DEV_BUILD_TARGET, 'houseplan-assets/c-HASH.js')), true);
assert.equal(existsSync(join(stand, DEV_BUILD_TARGET, 'stale-orphan.js')), false, 'каталог заменён целиком');
assert.match(run.stdout, new RegExp(head.slice(0, 8)));
const reset = spawnSync('bash', [STAND, '--reset', stand], { encoding: 'utf8', env: { ...process.env, ...ENV } });
assert.equal(reset.status, 0, reset.stderr);
assert.equal(git(stand, 'status', '--porcelain'), '', 'до pull рабочая копия чистая');
assert.equal(readFileSync(join(stand, DEV_BUILD_TARGET, 'houseplan-card.js'), 'utf8'), 'card of the last beta');
} finally { rmSync(root, { recursive: true, force: true }); }
});