mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
feat(ci): ratchet the Control UI startup JS budget against a committed baseline (#112880)
The startup JS gzip gate was a fixed ceiling that main drifted to within 1-3 KiB of, forcing repeated hand-bumps (315->317 KiB) and failing feature PRs on drift they did not cause (#112649 burned two CI rounds). check-control-ui-performance.mjs now also enforces a committed baseline (config/control-ui-startup-budget-baseline.json) with a 512 B tolerance: regressions beyond baseline+tolerance fail with an actionable message, intentional increases become a reviewed one-line diff via --update-baseline --reason, meaningfully lighter builds print a lower-the-baseline hint, and a malformed/missing baseline fails closed. The fixed ceiling stays as the cumulative-creep backstop. Closes #112743.
This commit is contained in:
committed by
GitHub
parent
a195d6fcee
commit
c11f175282
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"startupJsGzipBytes": 322526,
|
||||
"reason": "initial ratchet baseline",
|
||||
"updatedAt": "2026-07-23"
|
||||
}
|
||||
@@ -33,31 +33,47 @@ export type ControlUiPerformanceBudgets = {
|
||||
largestCssGzipBytes: number;
|
||||
};
|
||||
|
||||
export type ControlUiStartupBudgetBaseline = {
|
||||
startupJsGzipBytes: number;
|
||||
reason: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
export type ControlUiPerformanceBudgetViolation = {
|
||||
metric: string;
|
||||
actual: number;
|
||||
limit: number;
|
||||
unit: "count" | "bytes";
|
||||
baseline?: number;
|
||||
tolerance?: number;
|
||||
};
|
||||
|
||||
export const CONTROL_UI_PERFORMANCE_BUDGETS: Readonly<ControlUiPerformanceBudgets>;
|
||||
export const CONTROL_UI_STARTUP_JS_GZIP_TOLERANCE_BYTES: 512;
|
||||
export function extractControlUiStartupAssetPaths(html: string): string[];
|
||||
export function collectControlUiPerformanceMetrics(distDir: string): ControlUiPerformanceMetrics;
|
||||
export function evaluateControlUiPerformanceBudgets(
|
||||
metrics: ControlUiPerformanceMetrics,
|
||||
budgets?: Readonly<ControlUiPerformanceBudgets>,
|
||||
startupBudgetBaseline?: Readonly<ControlUiStartupBudgetBaseline>,
|
||||
startupJsTolerance?: number,
|
||||
): ControlUiPerformanceBudgetViolation[];
|
||||
export function formatControlUiPerformanceBytes(bytes: number): string;
|
||||
export function formatControlUiPerformanceReport(
|
||||
metrics: ControlUiPerformanceMetrics,
|
||||
budgets?: Readonly<ControlUiPerformanceBudgets>,
|
||||
startupBudgetBaseline?: Readonly<ControlUiStartupBudgetBaseline>,
|
||||
startupJsTolerance?: number,
|
||||
): string;
|
||||
export function runControlUiPerformanceCheck(
|
||||
distDir: string,
|
||||
budgets?: Readonly<ControlUiPerformanceBudgets>,
|
||||
baselinePath?: string,
|
||||
): {
|
||||
metrics: ControlUiPerformanceMetrics;
|
||||
budgets: Readonly<ControlUiPerformanceBudgets>;
|
||||
startupBudgetBaseline: ControlUiStartupBudgetBaseline;
|
||||
startupJsTolerance: number;
|
||||
violations: ControlUiPerformanceBudgetViolation[];
|
||||
report: string;
|
||||
};
|
||||
|
||||
@@ -6,6 +6,16 @@ import process from "node:process";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const KIB = 1024;
|
||||
const STARTUP_JS_BASELINE_RATCHET_BYTES = 4096;
|
||||
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
|
||||
const DEFAULT_STARTUP_BUDGET_BASELINE_PATH = path.resolve(
|
||||
SCRIPT_DIR,
|
||||
"../config/control-ui-startup-budget-baseline.json",
|
||||
);
|
||||
|
||||
// Each landed change can consume this much ratchet tolerance, so small increases
|
||||
// may accumulate. The fixed startup JS ceiling bounds that cumulative creep.
|
||||
export const CONTROL_UI_STARTUP_JS_GZIP_TOLERANCE_BYTES = 512;
|
||||
|
||||
// Small, explicit headroom over the optimized baseline. Budget changes should
|
||||
// accompany an intentional loading or chunking decision.
|
||||
@@ -125,6 +135,8 @@ export function collectControlUiPerformanceMetrics(distDir) {
|
||||
export function evaluateControlUiPerformanceBudgets(
|
||||
metrics,
|
||||
budgets = CONTROL_UI_PERFORMANCE_BUDGETS,
|
||||
startupBudgetBaseline = null,
|
||||
startupJsTolerance = CONTROL_UI_STARTUP_JS_GZIP_TOLERANCE_BYTES,
|
||||
) {
|
||||
const checks = [
|
||||
["startup JS requests", metrics.startup.js.requests, budgets.startupJsRequests, "count"],
|
||||
@@ -134,9 +146,23 @@ export function evaluateControlUiPerformanceBudgets(
|
||||
["largest JS gzip", metrics.largest.js.gzipBytes, budgets.largestJsGzipBytes, "bytes"],
|
||||
["largest CSS gzip", metrics.largest.css.gzipBytes, budgets.largestCssGzipBytes, "bytes"],
|
||||
];
|
||||
return checks.flatMap(([metric, actual, limit, unit]) =>
|
||||
const violations = checks.flatMap(([metric, actual, limit, unit]) =>
|
||||
actual > limit ? [{ metric, actual, limit, unit }] : [],
|
||||
);
|
||||
if (
|
||||
startupBudgetBaseline &&
|
||||
metrics.startup.js.gzipBytes > startupBudgetBaseline.startupJsGzipBytes + startupJsTolerance
|
||||
) {
|
||||
violations.push({
|
||||
metric: "startup JS gzip vs baseline",
|
||||
actual: metrics.startup.js.gzipBytes,
|
||||
limit: startupBudgetBaseline.startupJsGzipBytes + startupJsTolerance,
|
||||
unit: "bytes",
|
||||
baseline: startupBudgetBaseline.startupJsGzipBytes,
|
||||
tolerance: startupJsTolerance,
|
||||
});
|
||||
}
|
||||
return violations;
|
||||
}
|
||||
|
||||
export function formatControlUiPerformanceBytes(bytes) {
|
||||
@@ -152,6 +178,9 @@ function formatAssetSummary(summary) {
|
||||
}
|
||||
|
||||
function formatViolation(violation) {
|
||||
if (violation.baseline !== undefined && violation.tolerance !== undefined) {
|
||||
return `${violation.metric}: ${violation.actual} B exceeds baseline ${violation.baseline} B + tolerance ${violation.tolerance} B (limit ${violation.limit} B); intentionally raise the baseline with node scripts/check-control-ui-performance.mjs --update-baseline --reason "<reason>"`;
|
||||
}
|
||||
const actual =
|
||||
violation.unit === "bytes"
|
||||
? formatControlUiPerformanceBytes(violation.actual)
|
||||
@@ -170,17 +199,40 @@ function formatViolation(violation) {
|
||||
export function formatControlUiPerformanceReport(
|
||||
metrics,
|
||||
budgets = CONTROL_UI_PERFORMANCE_BUDGETS,
|
||||
startupBudgetBaseline = null,
|
||||
startupJsTolerance = CONTROL_UI_STARTUP_JS_GZIP_TOLERANCE_BYTES,
|
||||
) {
|
||||
const violations = evaluateControlUiPerformanceBudgets(metrics, budgets);
|
||||
const violations = evaluateControlUiPerformanceBudgets(
|
||||
metrics,
|
||||
budgets,
|
||||
startupBudgetBaseline,
|
||||
startupJsTolerance,
|
||||
);
|
||||
const lines = [
|
||||
"Control UI performance:",
|
||||
` startup JS: ${formatAssetSummary(metrics.startup.js)} (limits: ${formatRequestCount(budgets.startupJsRequests)}, ${formatControlUiPerformanceBytes(budgets.startupJsGzipBytes)} gzip)`,
|
||||
];
|
||||
if (startupBudgetBaseline) {
|
||||
lines.push(
|
||||
` startup JS gzip vs baseline: ${metrics.startup.js.gzipBytes} B (baseline ${startupBudgetBaseline.startupJsGzipBytes} B + tolerance ${startupJsTolerance} B, ceiling ${budgets.startupJsGzipBytes} B)`,
|
||||
);
|
||||
}
|
||||
lines.push(
|
||||
` startup CSS: ${formatAssetSummary(metrics.startup.css)} (limits: ${formatRequestCount(budgets.startupCssRequests)}, ${formatControlUiPerformanceBytes(budgets.startupCssGzipBytes)} gzip)`,
|
||||
` largest JS: ${metrics.largest.js.file}, ${formatControlUiPerformanceBytes(metrics.largest.js.gzipBytes)} gzip (limit: ${formatControlUiPerformanceBytes(budgets.largestJsGzipBytes)})`,
|
||||
` largest CSS: ${metrics.largest.css.file}, ${formatControlUiPerformanceBytes(metrics.largest.css.gzipBytes)} gzip (limit: ${formatControlUiPerformanceBytes(budgets.largestCssGzipBytes)})`,
|
||||
` all JS: ${formatAssetSummary(metrics.total.js)}`,
|
||||
` all CSS: ${formatAssetSummary(metrics.total.css)}`,
|
||||
];
|
||||
);
|
||||
if (
|
||||
startupBudgetBaseline &&
|
||||
metrics.startup.js.gzipBytes + STARTUP_JS_BASELINE_RATCHET_BYTES <
|
||||
startupBudgetBaseline.startupJsGzipBytes
|
||||
) {
|
||||
lines.push(
|
||||
` hint: startup JS gzip is more than ${STARTUP_JS_BASELINE_RATCHET_BYTES} B below the ${startupBudgetBaseline.startupJsGzipBytes} B baseline; lower it with node scripts/check-control-ui-performance.mjs --update-baseline --reason "<reason>"`,
|
||||
);
|
||||
}
|
||||
if (violations.length > 0) {
|
||||
lines.push(
|
||||
" violations:",
|
||||
@@ -190,24 +242,117 @@ export function formatControlUiPerformanceReport(
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
export function runControlUiPerformanceCheck(distDir, budgets = CONTROL_UI_PERFORMANCE_BUDGETS) {
|
||||
function baselineUpdateCommand() {
|
||||
return 'node scripts/check-control-ui-performance.mjs --update-baseline --reason "<reason>"';
|
||||
}
|
||||
|
||||
function isIsoDate(value) {
|
||||
if (!/^\d{4}-\d{2}-\d{2}$/u.test(value)) {
|
||||
return false;
|
||||
}
|
||||
const date = new Date(`${value}T00:00:00Z`);
|
||||
return !Number.isNaN(date.valueOf()) && date.toISOString().slice(0, 10) === value;
|
||||
}
|
||||
|
||||
function readControlUiStartupBudgetBaseline(baselinePath) {
|
||||
try {
|
||||
const parsed = JSON.parse(fs.readFileSync(baselinePath, "utf8"));
|
||||
if (
|
||||
!parsed ||
|
||||
typeof parsed !== "object" ||
|
||||
!Number.isSafeInteger(parsed.startupJsGzipBytes) ||
|
||||
parsed.startupJsGzipBytes < 0 ||
|
||||
typeof parsed.reason !== "string" ||
|
||||
parsed.reason.trim().length === 0 ||
|
||||
typeof parsed.updatedAt !== "string" ||
|
||||
!isIsoDate(parsed.updatedAt)
|
||||
) {
|
||||
throw new Error("expected startupJsGzipBytes, non-empty reason, and YYYY-MM-DD updatedAt");
|
||||
}
|
||||
return {
|
||||
startupJsGzipBytes: parsed.startupJsGzipBytes,
|
||||
reason: parsed.reason,
|
||||
updatedAt: parsed.updatedAt,
|
||||
};
|
||||
} catch (error) {
|
||||
const detail = error instanceof Error ? error.message : String(error);
|
||||
throw new Error(
|
||||
`Cannot read Control UI startup budget baseline ${baselinePath}: ${detail}. Regenerate it with ${baselineUpdateCommand()}.`,
|
||||
{ cause: error },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function writeControlUiStartupBudgetBaseline(baselinePath, startupJsGzipBytes, reason) {
|
||||
const baseline = {
|
||||
startupJsGzipBytes,
|
||||
reason,
|
||||
updatedAt: new Date().toISOString().slice(0, 10),
|
||||
};
|
||||
fs.writeFileSync(baselinePath, `${JSON.stringify(baseline, null, 2)}\n`);
|
||||
return baseline;
|
||||
}
|
||||
|
||||
export function runControlUiPerformanceCheck(
|
||||
distDir,
|
||||
budgets = CONTROL_UI_PERFORMANCE_BUDGETS,
|
||||
baselinePath = DEFAULT_STARTUP_BUDGET_BASELINE_PATH,
|
||||
) {
|
||||
const startupBudgetBaseline = readControlUiStartupBudgetBaseline(baselinePath);
|
||||
const metrics = collectControlUiPerformanceMetrics(distDir);
|
||||
const violations = evaluateControlUiPerformanceBudgets(metrics, budgets, startupBudgetBaseline);
|
||||
const report = formatControlUiPerformanceReport(metrics, budgets, startupBudgetBaseline);
|
||||
return {
|
||||
metrics,
|
||||
budgets,
|
||||
violations: evaluateControlUiPerformanceBudgets(metrics, budgets),
|
||||
report: formatControlUiPerformanceReport(metrics, budgets),
|
||||
startupBudgetBaseline,
|
||||
startupJsTolerance: CONTROL_UI_STARTUP_JS_GZIP_TOLERANCE_BYTES,
|
||||
violations,
|
||||
report,
|
||||
};
|
||||
}
|
||||
|
||||
function main(argv = process.argv.slice(2)) {
|
||||
const unknown = argv.filter((arg) => arg !== "--json");
|
||||
if (unknown.length > 0) {
|
||||
throw new Error(`Unknown option: ${unknown[0]}`);
|
||||
let json = false;
|
||||
let updateBaseline = false;
|
||||
let reason;
|
||||
for (let index = 0; index < argv.length; index += 1) {
|
||||
const arg = argv[index];
|
||||
if (arg === "--json") {
|
||||
json = true;
|
||||
} else if (arg === "--update-baseline") {
|
||||
updateBaseline = true;
|
||||
} else if (arg === "--reason") {
|
||||
reason = argv[index + 1];
|
||||
if (!reason || reason.trim().length === 0 || reason.startsWith("--")) {
|
||||
throw new Error("--reason requires a non-empty value");
|
||||
}
|
||||
index += 1;
|
||||
} else {
|
||||
throw new Error(`Unknown option: ${arg}`);
|
||||
}
|
||||
}
|
||||
const here = path.dirname(fileURLToPath(import.meta.url));
|
||||
const result = runControlUiPerformanceCheck(path.resolve(here, "../dist/control-ui"));
|
||||
if (argv.includes("--json")) {
|
||||
if (reason !== undefined && !updateBaseline) {
|
||||
throw new Error("--reason requires --update-baseline");
|
||||
}
|
||||
if (json && updateBaseline) {
|
||||
throw new Error("--json cannot be combined with --update-baseline");
|
||||
}
|
||||
const distDir = path.resolve(SCRIPT_DIR, "../dist/control-ui");
|
||||
if (updateBaseline) {
|
||||
const metrics = collectControlUiPerformanceMetrics(distDir);
|
||||
const baseline = writeControlUiStartupBudgetBaseline(
|
||||
DEFAULT_STARTUP_BUDGET_BASELINE_PATH,
|
||||
metrics.startup.js.gzipBytes,
|
||||
reason ?? "manual baseline update",
|
||||
);
|
||||
process.stdout.write(
|
||||
`Updated config/control-ui-startup-budget-baseline.json to ${baseline.startupJsGzipBytes} B (${baseline.reason}).\n`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
const result = runControlUiPerformanceCheck(distDir);
|
||||
if (json) {
|
||||
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
||||
} else {
|
||||
process.stdout.write(`${result.report}\n`);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
@@ -7,6 +8,7 @@ import {
|
||||
evaluateControlUiPerformanceBudgets,
|
||||
extractControlUiStartupAssetPaths,
|
||||
formatControlUiPerformanceReport,
|
||||
runControlUiPerformanceCheck,
|
||||
} from "../../scripts/check-control-ui-performance.mjs";
|
||||
|
||||
const tempDirs: string[] = [];
|
||||
@@ -28,6 +30,54 @@ function createDistFixture() {
|
||||
return { distDir, writeAsset };
|
||||
}
|
||||
|
||||
function createMetrics(startupJsGzipBytes: number) {
|
||||
return {
|
||||
schemaVersion: 1 as const,
|
||||
startup: {
|
||||
js: { requests: 1, rawBytes: 2_000, gzipBytes: startupJsGzipBytes, brotliBytes: 900 },
|
||||
css: { requests: 1, rawBytes: 50, gzipBytes: 15, brotliBytes: 12 },
|
||||
assets: [],
|
||||
},
|
||||
total: {
|
||||
js: { requests: 1, rawBytes: 2_000, gzipBytes: startupJsGzipBytes, brotliBytes: 900 },
|
||||
css: { requests: 1, rawBytes: 50, gzipBytes: 15, brotliBytes: 12 },
|
||||
},
|
||||
largest: {
|
||||
js: {
|
||||
file: "assets/index-a.js",
|
||||
type: "js" as const,
|
||||
rawBytes: 2_000,
|
||||
gzipBytes: startupJsGzipBytes,
|
||||
brotliBytes: 900,
|
||||
},
|
||||
css: {
|
||||
file: "assets/index-c.css",
|
||||
type: "css" as const,
|
||||
rawBytes: 50,
|
||||
gzipBytes: 15,
|
||||
brotliBytes: 12,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const looseBudgets = {
|
||||
startupJsRequests: 10,
|
||||
startupCssRequests: 10,
|
||||
startupJsGzipBytes: 100_000,
|
||||
startupCssGzipBytes: 100_000,
|
||||
largestJsGzipBytes: 100_000,
|
||||
largestCssGzipBytes: 100_000,
|
||||
};
|
||||
|
||||
function startupBaseline(startupJsGzipBytes: number) {
|
||||
return {
|
||||
startupJsGzipBytes,
|
||||
reason: "test baseline",
|
||||
updatedAt: "2026-07-22",
|
||||
};
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
for (const tempDir of tempDirs.splice(0)) {
|
||||
fs.rmSync(tempDir, { force: true, recursive: true });
|
||||
@@ -145,6 +195,125 @@ describe("Control UI performance budgets", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("allows startup JS growth within the ratchet tolerance", () => {
|
||||
const violations = evaluateControlUiPerformanceBudgets(
|
||||
createMetrics(10_512),
|
||||
looseBudgets,
|
||||
startupBaseline(10_000),
|
||||
);
|
||||
|
||||
expect(violations).toEqual([]);
|
||||
});
|
||||
|
||||
it("fails startup JS growth over the ratchet tolerance with update guidance", () => {
|
||||
const metrics = createMetrics(10_513);
|
||||
const baseline = startupBaseline(10_000);
|
||||
|
||||
expect(
|
||||
evaluateControlUiPerformanceBudgets(metrics, looseBudgets, baseline).map(
|
||||
(entry) => entry.metric,
|
||||
),
|
||||
).toContain("startup JS gzip vs baseline");
|
||||
expect(formatControlUiPerformanceReport(metrics, looseBudgets, baseline)).toContain(
|
||||
'10513 B exceeds baseline 10000 B + tolerance 512 B (limit 10512 B); intentionally raise the baseline with node scripts/check-control-ui-performance.mjs --update-baseline --reason "<reason>"',
|
||||
);
|
||||
});
|
||||
|
||||
it("enforces the fixed startup JS ceiling even when the baseline is higher", () => {
|
||||
const budgets = { ...looseBudgets, startupJsGzipBytes: 10_000 };
|
||||
|
||||
expect(
|
||||
evaluateControlUiPerformanceBudgets(
|
||||
createMetrics(10_001),
|
||||
budgets,
|
||||
startupBaseline(1_000_000),
|
||||
).map((entry) => entry.metric),
|
||||
).toEqual(["startup JS gzip"]);
|
||||
});
|
||||
|
||||
it("suggests lowering a baseline after a meaningful size reduction", () => {
|
||||
expect(
|
||||
formatControlUiPerformanceReport(
|
||||
createMetrics(10_000),
|
||||
looseBudgets,
|
||||
startupBaseline(14_097),
|
||||
),
|
||||
).toContain("hint: startup JS gzip is more than 4096 B below the 14097 B baseline");
|
||||
});
|
||||
|
||||
it("fails closed when the startup baseline is malformed", () => {
|
||||
const { distDir, writeAsset } = createDistFixture();
|
||||
fs.writeFileSync(
|
||||
path.join(distDir, "index.html"),
|
||||
'<script type="module" src="./assets/index-a.js"></script>\n' +
|
||||
'<link rel="stylesheet" href="./assets/index-c.css">\n',
|
||||
);
|
||||
writeAsset("index-a.js", { rawBytes: 100, gzipBytes: 40, brotliBytes: 30 });
|
||||
writeAsset("index-c.css", { rawBytes: 50, gzipBytes: 15, brotliBytes: 12 });
|
||||
const baselinePath = path.join(distDir, "baseline.json");
|
||||
fs.writeFileSync(baselinePath, '{"startupJsGzipBytes":"not-a-number"}\n');
|
||||
|
||||
expect(() => runControlUiPerformanceCheck(distDir, looseBudgets, baselinePath)).toThrow(
|
||||
/Cannot read Control UI startup budget baseline .*--update-baseline/u,
|
||||
);
|
||||
});
|
||||
|
||||
it("updates the baseline from exact current dist metrics without rebuilding", () => {
|
||||
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-control-ui-budget-cli-"));
|
||||
tempDirs.push(rootDir);
|
||||
const scriptsDir = path.join(rootDir, "scripts");
|
||||
const configDir = path.join(rootDir, "config");
|
||||
const distDir = path.join(rootDir, "dist/control-ui");
|
||||
const assetsDir = path.join(distDir, "assets");
|
||||
fs.mkdirSync(scriptsDir, { recursive: true });
|
||||
fs.mkdirSync(configDir, { recursive: true });
|
||||
fs.mkdirSync(assetsDir, { recursive: true });
|
||||
const scriptPath = path.join(scriptsDir, "check-control-ui-performance.mjs");
|
||||
fs.copyFileSync(path.resolve("scripts/check-control-ui-performance.mjs"), scriptPath);
|
||||
fs.writeFileSync(
|
||||
path.join(distDir, "index.html"),
|
||||
'<script type="module" src="./assets/index-a.js"></script>\n' +
|
||||
'<link rel="stylesheet" href="./assets/index-c.css">\n',
|
||||
);
|
||||
for (const [file, sizes] of [
|
||||
["index-a.js", { rawBytes: 100, gzipBytes: 65, brotliBytes: 50 }],
|
||||
["index-c.css", { rawBytes: 50, gzipBytes: 15, brotliBytes: 12 }],
|
||||
] as const) {
|
||||
const assetPath = path.join(assetsDir, file);
|
||||
fs.writeFileSync(assetPath, Buffer.alloc(sizes.rawBytes));
|
||||
fs.writeFileSync(`${assetPath}.gz`, Buffer.alloc(sizes.gzipBytes));
|
||||
fs.writeFileSync(`${assetPath}.br`, Buffer.alloc(sizes.brotliBytes));
|
||||
}
|
||||
|
||||
const result = spawnSync(process.execPath, [fs.realpathSync(scriptPath), "--update-baseline"], {
|
||||
cwd: rootDir,
|
||||
encoding: "utf8",
|
||||
});
|
||||
|
||||
expect(result.status, result.stderr).toBe(0);
|
||||
expect(
|
||||
JSON.parse(
|
||||
fs.readFileSync(path.join(configDir, "control-ui-startup-budget-baseline.json"), "utf8"),
|
||||
),
|
||||
).toEqual({
|
||||
startupJsGzipBytes: 65,
|
||||
reason: "manual baseline update",
|
||||
updatedAt: expect.stringMatching(/^\d{4}-\d{2}-\d{2}$/u),
|
||||
});
|
||||
|
||||
const customReasonResult = spawnSync(
|
||||
process.execPath,
|
||||
[fs.realpathSync(scriptPath), "--update-baseline", "--reason", "fixture update"],
|
||||
{ cwd: rootDir, encoding: "utf8" },
|
||||
);
|
||||
expect(customReasonResult.status, customReasonResult.stderr).toBe(0);
|
||||
expect(
|
||||
JSON.parse(
|
||||
fs.readFileSync(path.join(configDir, "control-ui-startup-budget-baseline.json"), "utf8"),
|
||||
),
|
||||
).toMatchObject({ startupJsGzipBytes: 65, reason: "fixture update" });
|
||||
});
|
||||
|
||||
it("fails when a compressed sidecar is missing", () => {
|
||||
const { distDir } = createDistFixture();
|
||||
fs.writeFileSync(
|
||||
|
||||
Reference in New Issue
Block a user