fix: verify demo bundle freshness for smokes too

Golden runs, benchmarks and documentation captures each called
assertFreshDemoBundle; the smoke launcher never did, so all ~128 smokes could
silently test a stale demo/srv/assets bundle. On #234 that cost a round of
analysis: three assertions went red and a fourth went green, because the old
code was wrong in two places that agreed with each other, and a mixed result
reads as a logic defect rather than a stale artefact.

launch() now runs the check once for every smoke, against the repository root
rather than the serving root — demo/srv has no src/** to fingerprint.
HP_ALLOW_STALE_BUNDLE=1 skips it for debugging and warns out loud, because a
guard that says nothing when it steps aside is the silent success this project
keeps removing. A mutation entry proves the call cannot quietly disappear.

Issue: #236
User-Visible: no
This commit is contained in:
Matysh
2026-08-22 01:47:47 +03:00
parent 5fd9716906
commit e4b4f33c6d
+59 -2
View File
@@ -1,9 +1,13 @@
import assert from 'node:assert/strict';
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { resolve } from 'node:path';
import test from 'node:test';
import { assertFreshDemoBundle } from '../demo/bundle-freshness.mjs';
import {
ALLOW_STALE_BUNDLE,
assertFreshDemoBundle,
assertFreshDemoBundleUnlessAllowed,
} from '../demo/bundle-freshness.mjs';
import { sourceFingerprint } from '../scripts/source-fingerprint.mjs';
const fixtureRoot = () => {
@@ -55,3 +59,56 @@ test('bundle freshness uses the target tree fingerprint contract', async () => {
rmSync(root, { recursive: true, force: true });
}
});
test('the launcher gate refuses a stale bundle by default (#236)', async () => {
// Смок против несвежего бандла не падает честно: на #234 три проверки
// покраснели, а четвёртая ПРОШЛА, потому что старый код одинаково врал в двух
// местах, которые сверялись друг с другом. Такой результат читается как
// дефект логики и отправляет искать причину не туда.
const root = fixtureRoot();
try {
const stale = { evaluate: async () => 'fingerprint-of-an-older-tree' };
await assert.rejects(
() => assertFreshDemoBundleUnlessAllowed(stale, root, {}),
/stale/,
);
const fresh = { evaluate: async () => sourceFingerprint(root) };
assert.equal(
await assertFreshDemoBundleUnlessAllowed(fresh, root, {}),
sourceFingerprint(root),
);
} finally {
rmSync(root, { recursive: true, force: true });
}
});
test('the escape hatch skips the gate but never silently (#236)', async () => {
const root = fixtureRoot();
const warnings = [];
const realWarn = console.warn;
console.warn = (...args) => warnings.push(args.join(' '));
try {
const stale = { evaluate: async () => 'stale' };
const result = await assertFreshDemoBundleUnlessAllowed(
stale, root, { [ALLOW_STALE_BUNDLE]: '1' },
);
assert.equal(result, null, 'пропуск возвращает null, а не выдуманный фингерпринт');
assert.equal(warnings.length, 1, 'пропуск обязан быть слышен');
assert.match(warnings[0], new RegExp(ALLOW_STALE_BUNDLE));
assert.match(warnings[0], /#236/);
} finally {
console.warn = realWarn;
rmSync(root, { recursive: true, force: true });
}
});
test('the smoke launcher enforces the gate on the repository root (#236)', async () => {
// Контракт места вызова: гейт живёт в общем лаунчере, поэтому защита есть у
// всех ~128 смоков без правки каждого. И считает фингерпринт по корню
// репозитория, а не по каталогу раздачи demo/srv, где нет src/**.
const source = readFileSync(
new URL('../demo/serve.mjs', import.meta.url), 'utf8',
);
assert.match(source, /assertFreshDemoBundleUnlessAllowed\(page, REPO_ROOT\)/);
assert.match(source, /const REPO_ROOT = dirname\(dirname\(/);
});