test(bundle): полосу потолка по поставляемому манифесту судить только у свежего бандла (#676)

С #657 закоммиченный бандл меняет только кандидат, поэтому между
кандидатами dist/houseplan-assets.json законно отстаёт от исходников — и от
потолков, которые задача поднимает в том же коммите, что и свой граф.
Четыре теста (#438 ×2, #593, #627 AC1) сравнивали отстающий манифест с
новым потолком: после #663 бандл беты.4 лежал ниже поднятой полосы
редактора, после #676 — бандл беты.5 (235 435 против полосы 237 000…
239 000), и на чистом чекауте до кандидата они краснели, а в мутантном
worktree CI (git worktree без сборки) роняли «чистый прогон» шарда 2/6
(run 36324644336).

Теперь поставляемые графы сравниваются с потолками только когда отпечаток
манифеста равен отпечатку исходников — у кандидата и после сборки в
Validate/gate:small; отстающий бандл даёт диагностику #657 вместо ложного
красного. Синтетические проверки обеих сторон полосы от свежести не
зависят, поэтому мутанты потолков ловятся по-прежнему.

Issue: #676
User-Visible: no
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
This commit is contained in:
Claude
2026-09-27 14:41:19 +00:00
committed by claude[bot]
parent 69e0345122
commit ab89187f4f
+43 -22
View File
@@ -28,6 +28,27 @@ import {
import {
cssTemplateMinifier, minifyCssText, minifyStaticCssTemplates,
} from '../scripts/css-template-minifier.mjs';
import { sourceFingerprint } from '../scripts/source-fingerprint.mjs';
// Поставляемый манифест — закоммиченный `dist/houseplan-assets.json`. С #657
// бандл меняет только кандидат беты/релиза, поэтому между кандидатами манифест
// законно отстаёт от исходников — и от потолков, которые задачи поднимают
// вслед за своим графом в том же коммите. «Поставляемый граф лежит в полосе
// потолка» (#438, #593, #627) поэтому судится только по СВЕЖЕМУ бандлу: у
// кандидата (его бандл и есть закоммиченный) и после `npm run build` в Validate
// и gate:small. Отстающий манифест против нового потолка — не измерение:
// после слияния #663 бандл беты.4 лежал ниже поднятой полосы редактора, после
// #676 — бандл беты.5, и на чистом чекауте эти тесты краснели до кандидата, а
// в мутантном worktree CI (`git worktree` без сборки) роняли чистый прогон.
// Свежесть — тот же отпечаток, которым `bundle-policy` судит кандидата (#657).
const SHIPPED_MANIFEST_URL = new URL('../dist/houseplan-assets.json', import.meta.url);
const shippedManifest = () => JSON.parse(readFileSync(SHIPPED_MANIFEST_URL, 'utf8'));
const shippedDist = fileURLToPath(new URL('../dist/', import.meta.url));
const shippedBundleIsFresh = (manifest) => (
manifest.fingerprint === sourceFingerprint(fileURLToPath(new URL('..', import.meta.url)))
);
const STALE_SHIPPED_BUNDLE = '#657: закоммиченный бандл отстаёт от исходников до кандидата — '
+ 'полосу потолка судит `npm run bundle:budget` по свежей сборке, здесь сравнивать не с чем';
/** #627: the nine namespace × language chunks, each a dynamic import of a lazy chunk. */
const namespaceLocaleChunkPath = (entry) => `houseplan-assets/${entry.namespace}-${entry.language}-HASH.js`;
@@ -847,13 +868,13 @@ test('#429 проверка владения не судит размер гра
// --- #438: потолок графа с полосой ------------------------------------------
test('#438 поставляемый граф лежит внутри полосы потолка', () => {
test('#438 поставляемый граф лежит внутри полосы потолка', (t) => {
// Гейт живёт и здесь, а не только в `npm run bundle:budget`: манифест
// закоммичен, значит проверка стоит ровно там, где её увидит любой прогон
// тестов. Рост, который не заметили в бете.2, краснел бы на этом тесте.
const manifest = JSON.parse(
readFileSync(new URL('../dist/houseplan-assets.json', import.meta.url), 'utf8'),
);
// С #657 — только пока бандл свежий, см. `shippedBundleIsFresh`.
const manifest = shippedManifest();
if (!shippedBundleIsFresh(manifest)) { t.diagnostic(STALE_SHIPPED_BUNDLE); return; }
const violation = initialViewCeilingViolation(manifest.initialViewGzipBytes);
assert.equal(violation, null, violation?.text);
});
@@ -884,7 +905,7 @@ test('#438 падение ниже полосы требует опустить
assert.equal(initialViewCeilingViolation(undefined).kind, 'missing');
});
test('#438 полоса шире наблюдаемого шума метрики', () => {
test('#438 полоса шире наблюдаемого шума метрики', (t) => {
// gzip не монотонен по исходнику: на beta.2 initial-чанк стал меньше на 344
// сырых байта и на 40 байт больше в сжатом виде. Полоса обязана быть заметно
// шире таких колебаний, иначе гейт краснеет на коммитах, сокращающих код, —
@@ -894,9 +915,9 @@ test('#438 полоса шире наблюдаемого шума метрик
// И потолок обязан оставаться под общим бюджетом: иначе он ничего не значит.
assert.ok(INITIAL_VIEW_GZIP_CEILING < INITIAL_VIEW_GZIP_BUDGET);
// Факт лежит не у края полосы: до отказа есть место в обе стороны.
const shipped = JSON.parse(
readFileSync(new URL('../dist/houseplan-assets.json', import.meta.url), 'utf8'),
).initialViewGzipBytes;
const manifest = shippedManifest();
if (!shippedBundleIsFresh(manifest)) { t.diagnostic(STALE_SHIPPED_BUNDLE); return; }
const shipped = manifest.initialViewGzipBytes;
assert.ok(INITIAL_VIEW_GZIP_CEILING - shipped > 500, 'сверху меньше 500 Б — это шум');
assert.ok(shipped - (INITIAL_VIEW_GZIP_CEILING - INITIAL_VIEW_CEILING_BAND) > 500,
'снизу меньше 500 Б — гейт потребует опустить потолок из-за шума');
@@ -1030,16 +1051,17 @@ test('#438 CLI действительно применяет потолок, а
// #593: до этой задачи размеры ленивых графов только печатались в отчёт. Тогда
// «бюджет ленивого графа защищён» было заявлением без гейта — сравнения не
// существовало ни одного, и рост уезжал молча.
test('#593 потолки ленивых графов — гейт, а не строка отчёта', () => {
const manifest = JSON.parse(
readFileSync(new URL('../dist/houseplan-assets.json', import.meta.url), 'utf8'),
);
for (const [bytes, ceiling, label] of [
test('#593 потолки ленивых графов — гейт, а не строка отчёта', (t) => {
const manifest = shippedManifest();
// Поставляемые графы против потолков — только по свежему бандлу (#657).
const fresh = shippedBundleIsFresh(manifest);
if (!fresh) t.diagnostic(STALE_SHIPPED_BUNDLE);
for (const [bytes, ceiling, label] of fresh ? [
[manifest.lazyFurnitureArtGzipBytes, LAZY_FURNITURE_ART_GZIP_CEILING, 'lazy furniture art graph'],
[manifest.lazyEditorGzipBytes, LAZY_EDITOR_GZIP_CEILING, 'lazy editor graph'],
// #627 AC1: the first-run graph is the third gated lazy graph.
[manifest.lazyOnboardingGzipBytes, LAZY_ONBOARDING_GZIP_CEILING, 'lazy onboarding graph'],
]) {
] : []) {
const violation = lazyGraphCeilingViolation(bytes, { ceiling, label });
assert.equal(violation, null, violation?.text);
// Факт лежит не у края полосы — с тем же запасом, что у стартового графа.
@@ -1047,7 +1069,9 @@ test('#593 потолки ленивых графов — гейт, а не ст
assert.ok(bytes - (ceiling - LAZY_GRAPH_CEILING_BAND) > 500,
`${label}: снизу меньше 500 Б — гейт потребует опустить потолок из-за шума`);
}
// Гейт обязан быть исполняемым и на синтетике, обе стороны.
// Гейт обязан быть исполняемым и на синтетике, обе стороны — и это не
// зависит от свежести бандла: мутант «потолок никогда не срабатывает»
// ловится здесь.
const grew = lazyGraphCeilingViolation(20_000, { ceiling: 17_900, label: 'lazy furniture art graph' });
assert.equal(grew.kind, 'grew');
assert.equal(grew.over, 2_100);
@@ -1064,12 +1088,7 @@ test('#593 потолки ленивых графов — гейт, а не ст
/lazy editor graph/);
});
const shippedManifest = () => JSON.parse(
readFileSync(new URL('../dist/houseplan-assets.json', import.meta.url), 'utf8'),
);
const shippedDist = fileURLToPath(new URL('../dist/', import.meta.url));
test('#627 AC1 граф онбординга гейтится тем же потолком с полосой, что editor и furniture art', () => {
test('#627 AC1 граф онбординга гейтится тем же потолком с полосой, что editor и furniture art', (t) => {
const manifest = shippedManifest();
const ceilings = (onboarding) => [
manifest.lazyFurnitureArtGzipBytes, manifest.lazyEditorGzipBytes, onboarding,
@@ -1085,7 +1104,9 @@ test('#627 AC1 граф онбординга гейтится тем же пот
() => assertBundleBudget(manifest, 1_000_000, undefined, ...ceilings(bytes + LAZY_GRAPH_CEILING_BAND + 1)),
/lazy onboarding graph \d+ B gzip ниже потолка .*Опустите потолок/,
);
// Потолок по умолчанию — поставляемый, и поставляемый граф в его полосе.
// Потолок по умолчанию — поставляемый, и поставляемый граф в его полосе —
// пока бандл свежий (#657); синтетика выше от свежести не зависит.
if (!shippedBundleIsFresh(manifest)) { t.diagnostic(STALE_SHIPPED_BUNDLE); return; }
assert.doesNotThrow(() => assertBundleBudget(manifest));
});