mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-28 19:01:34 +00:00
93 lines
4.2 KiB
JavaScript
93 lines
4.2 KiB
JavaScript
import { createHash } from 'node:crypto';
|
|
import { existsSync, readFileSync } from 'node:fs';
|
|
import { relative, resolve } from 'node:path';
|
|
import { pathToFileURL } from 'node:url';
|
|
import { sourceFingerprint } from '../scripts/source-fingerprint.mjs';
|
|
import { assertBundleManifest, verifyBundleTree } from '../scripts/bundle-tree.mjs';
|
|
|
|
const fingerprintForTree = async (root) => {
|
|
const modulePath = resolve(root, 'scripts/source-fingerprint.mjs');
|
|
if (!existsSync(modulePath)) return sourceFingerprint(root);
|
|
const module = await import(pathToFileURL(modulePath).href);
|
|
if (typeof module.sourceFingerprint !== 'function') {
|
|
throw new Error(`${modulePath} does not export sourceFingerprint`);
|
|
}
|
|
return module.sourceFingerprint(root);
|
|
};
|
|
|
|
const verifyManifestTree = (root, expected) => {
|
|
const assetRoot = resolve(root, 'demo/srv/assets');
|
|
const manifestPath = resolve(assetRoot, 'houseplan-assets.json');
|
|
// Comparative benchmarks can target releases from before the multi-asset
|
|
// contract. Their own embedded fingerprint remains the legacy authority.
|
|
if (!existsSync(manifestPath)) return;
|
|
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
if (manifest?.schema !== 1 || manifest.fingerprint !== expected
|
|
|| !Array.isArray(manifest.files)) {
|
|
throw new Error('demo bundle manifest is stale or malformed; run npm run bundle:sync');
|
|
}
|
|
// Comparative performance runs intentionally build older trees. Require the
|
|
// two-entry contract exactly when that target contains the panel source; a
|
|
// pre-#486 baseline remains measurable under its own fingerprint contract.
|
|
if (existsSync(resolve(root, 'src/houseplan-panel.ts'))) {
|
|
assertBundleManifest(manifest, manifestPath);
|
|
verifyBundleTree(assetRoot);
|
|
return;
|
|
}
|
|
for (const file of manifest.files) {
|
|
const path = resolve(assetRoot, String(file?.path || ''));
|
|
const rel = relative(assetRoot, path);
|
|
if (!rel || rel.startsWith('..') || rel.includes(':') || !existsSync(path)) {
|
|
throw new Error(`demo bundle manifest asset is missing or unsafe: ${file?.path}`);
|
|
}
|
|
const hash = createHash('sha256').update(readFileSync(path)).digest('hex');
|
|
if (hash !== file.sha256) throw new Error(`demo bundle asset hash mismatch: ${file.path}`);
|
|
}
|
|
};
|
|
|
|
/** Refuse measurements/screenshots made by a committed bundle from old source. */
|
|
export async function assertFreshDemoBundle(page, root = process.cwd()) {
|
|
// A comparative performance run may load an older tree whose fingerprint
|
|
// contract is intentionally different from the candidate's. Validate that
|
|
// tree with the implementation that built it, not with today's algorithm.
|
|
const expected = await fingerprintForTree(root);
|
|
verifyManifestTree(root, expected);
|
|
const loaded = await page.evaluate(() => globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__ ?? null);
|
|
if (loaded !== expected) {
|
|
throw new Error(
|
|
'demo/srv/assets bundle tree is stale. Run npm run bundle:sync first. '
|
|
+ `Expected ${expected}, loaded ${loaded || 'no fingerprint'}.`,
|
|
);
|
|
}
|
|
return expected;
|
|
}
|
|
|
|
/** Env switch that lets a debugging session run against a stale bundle. */
|
|
export const ALLOW_STALE_BUNDLE = 'HP_ALLOW_STALE_BUNDLE';
|
|
|
|
/**
|
|
* The freshness gate for every browser check, escape hatch included (#236).
|
|
*
|
|
* The smoke launcher had no freshness check at all, while golden runs and
|
|
* benchmarks did. A smoke against a stale `demo/srv/assets/houseplan-card.js`
|
|
* does not fail cleanly: on #234 three assertions went red and a fourth went
|
|
* GREEN, because the old code was wrong in two places that agreed with each
|
|
* other. A partly-red partly-green result looks like a logic defect and sends
|
|
* the reader hunting in the wrong file.
|
|
*
|
|
* Skipping is allowed for debugging, but never silently: a skipped guard that
|
|
* says nothing is the same silent success this project keeps digging out.
|
|
*/
|
|
export async function assertFreshDemoBundleUnlessAllowed(
|
|
page, root = process.cwd(), env = process.env,
|
|
) {
|
|
if (env[ALLOW_STALE_BUNDLE]) {
|
|
console.warn(
|
|
`[houseplan] ${ALLOW_STALE_BUNDLE} is set — bundle freshness NOT verified. `
|
|
+ 'A red result may mean a stale bundle rather than a defect (#236).',
|
|
);
|
|
return null;
|
|
}
|
|
return assertFreshDemoBundle(page, root);
|
|
}
|