Files
houseplan-card/test/release-assets.test.mjs
T
2026-09-13 10:35:43 +03:00

90 lines
5.0 KiB
JavaScript

// #540: паспорт установочных ассетов — публикуются ровно те байты, что прошли гейты.
import assert from 'node:assert/strict';
import test from 'node:test';
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { spawnSync } from 'node:child_process';
import { fileURLToPath } from 'node:url';
import {
INSTALLABLE_ASSETS, PASSPORTED_ASSETS, SUMS_FILE, compareSums, formatSums, parseSums,
sha256Hex, sumsOfDirectory,
} from '../scripts/release-assets.mjs';
const A = 'a'.repeat(64);
const B = 'b'.repeat(64);
const C = 'c'.repeat(64);
test('#540/#547: installables stay two while the passport also binds release membership', () => {
assert.deepEqual(INSTALLABLE_ASSETS, ['houseplan-card.js', 'houseplan.zip']);
assert.deepEqual(PASSPORTED_ASSETS, [
'houseplan-card.js', 'houseplan.zip', 'RELEASE-MEMBERSHIP.json',
]);
assert.equal(SUMS_FILE, 'SHA256SUMS');
const text = formatSums({ 'houseplan.zip': A, 'houseplan-card.js': B });
assert.equal(text, `${B} houseplan-card.js\n${A} houseplan.zip\n`);
assert.deepEqual(parseSums(text), { 'houseplan-card.js': B, 'houseplan.zip': A });
assert.deepEqual(parseSums(`${A} *houseplan.zip\r\n`), { 'houseplan.zip': A }, 'binary marker and CRLF tolerated');
assert.throws(() => formatSums({}), /нет ни одного/);
assert.throws(() => parseSums(''), /пустой/);
assert.throws(() => parseSums('deadbeef x'), /непонятная/);
assert.throws(() => parseSums(`${A} x\n${B} x\n`), /дважды/);
});
test('#540 AC3: a present asset with another hash is a mismatch — never a silent replacement; an absent one is merely missing', () => {
const expected = { 'houseplan-card.js': B, 'houseplan.zip': A };
assert.deepEqual(compareSums(expected, { 'houseplan-card.js': B, 'houseplan.zip': A }),
{ ok: true, missing: [], mismatched: [], extra: [] });
assert.deepEqual(compareSums(expected, { 'houseplan-card.js': B }),
{ ok: false, missing: ['houseplan.zip'], mismatched: [], extra: [] }, 'repair may add what is missing');
assert.deepEqual(compareSums(expected, { 'houseplan-card.js': B, 'houseplan.zip': C }),
{ ok: false, missing: [], mismatched: ['houseplan.zip'], extra: [] }, 'v1.75.0 class: public bytes differ from the verified ones');
assert.deepEqual(compareSums(expected, { 'houseplan-card.js': B, 'houseplan.zip': A, 'extra.bin': C }).extra, ['extra.bin']);
});
test('#540: the CLI writes the passport from real files and refuses an incomplete set; check exits 1 on a mismatch', () => {
const script = fileURLToPath(new URL('../scripts/release-assets.mjs', import.meta.url));
const dir = mkdtempSync(join(tmpdir(), 'hp-release-assets-'));
try {
writeFileSync(join(dir, 'houseplan-card.js'), 'card');
let r = spawnSync(process.execPath, [script, 'sums', dir], { encoding: 'utf8' });
assert.equal(r.status, 1, 'no passport for a half set');
assert.match(r.stderr, /houseplan\.zip отсутствует/);
writeFileSync(join(dir, 'houseplan.zip'), 'zip');
r = spawnSync(process.execPath, [script, 'sums', dir, '--include-membership'], { encoding: 'utf8' });
assert.equal(r.status, 1, 'candidate membership is part of a prerelease passport');
assert.match(r.stderr, /RELEASE-MEMBERSHIP\.json отсутствует/);
writeFileSync(join(dir, 'RELEASE-MEMBERSHIP.json'), '{"schema":1}\n');
r = spawnSync(process.execPath, [script, 'sums', dir, '--include-membership'], { encoding: 'utf8' });
assert.equal(r.status, 0, r.stderr);
const sums = readFileSync(join(dir, SUMS_FILE), 'utf8');
assert.deepEqual(parseSums(sums), {
'houseplan-card.js': sha256Hex(Buffer.from('card')),
'houseplan.zip': sha256Hex(Buffer.from('zip')),
'RELEASE-MEMBERSHIP.json': sha256Hex(Buffer.from('{"schema":1}\n')),
});
assert.deepEqual(sumsOfDirectory(dir, PASSPORTED_ASSETS), parseSums(sums));
r = spawnSync(process.execPath, [script, 'check', dir, join(dir, SUMS_FILE)], { encoding: 'utf8' });
assert.equal(r.status, 0, r.stderr);
writeFileSync(join(dir, 'houseplan.zip'), 'other bytes');
r = spawnSync(process.execPath, [script, 'check', dir, join(dir, SUMS_FILE)], { encoding: 'utf8' });
assert.equal(r.status, 1);
assert.match(r.stdout, /MISMATCH houseplan\.zip/);
assert.match(r.stderr, /хеш расходится: houseplan\.zip/);
rmSync(join(dir, 'houseplan.zip'));
r = spawnSync(process.execPath, [script, 'check', dir, join(dir, SUMS_FILE)], { encoding: 'utf8' });
assert.equal(r.status, 1, 'missing is a failure by default');
r = spawnSync(process.execPath, [script, 'check', dir, join(dir, SUMS_FILE), '--allow-missing'], { encoding: 'utf8' });
assert.equal(r.status, 0, 'repair mode tolerates a missing asset — it will be added');
assert.match(r.stdout, /missing {2}houseplan\.zip/);
} finally {
rmSync(dir, { recursive: true, force: true });
}
});