Files
callstack__agent-device/scripts/size-report-package.mjs
Michał Pierzchała b15121ffc8 test(ios): establish snapshot convergence baselines and permanent evidence (#2204)
* test(ios): add snapshot convergence evidence harness

* fix(ios): satisfy benchmark CI guards

* fix(ios): constrain benchmark proxy routes

* fix(ios-benchmark): enforce cell admission evidence

* fix(ios-benchmark): protect benchmark state ownership

* fix(ios-benchmark): use proxy port flag

* fix(ios-benchmark): let proxy choose an ephemeral port

* test(ios-benchmark): keep CLI process seam local

* fix(ios-benchmark): parse proxy startup envelope

* fix(ios-benchmark): bind proxy lease to simulator

* fix(ios-benchmark): keep fresh proxy CLI sessions isolated

* fix(ios-benchmark): preserve async timeout evidence

* docs(ios-benchmark): retain exact-head evidence

* test(ios): reveal offscreen alert fixture controls

* test(ios): reset alert between relaunch samples

* test(ios): admit native alert snapshots

* docs(ios): publish snapshot convergence corpus

* chore(ios): format benchmark evidence

* fix(ios-benchmark): admit proxy fixture anchors

* docs(ios): republish exact-head benchmark corpus

* fix(size): make publish asset evidence hermetic

* style(size): format package evidence test

* test(size): update publish preparation contracts

* fix: retire stale utils layering zone

* test: pin shared publish asset owner

* test: verify preserved size reporter closure

* fix: move mutation ownership to snapshot module

* test(ios): add snapshot convergence evidence harness

* fix(ios): satisfy benchmark CI guards

* fix(ios): constrain benchmark proxy routes

* fix(ios-benchmark): enforce cell admission evidence

* fix(ios-benchmark): protect benchmark state ownership

* fix(ios-benchmark): use proxy port flag

* fix(ios-benchmark): let proxy choose an ephemeral port

* test(ios-benchmark): keep CLI process seam local

* fix(ios-benchmark): parse proxy startup envelope

* fix(ios-benchmark): bind proxy lease to simulator

* fix(ios-benchmark): keep fresh proxy CLI sessions isolated

* fix(ios-benchmark): preserve async timeout evidence

* docs(ios-benchmark): retain exact-head evidence

* test(ios): reveal offscreen alert fixture controls

* test(ios): reset alert between relaunch samples

* test(ios): admit native alert snapshots

* docs(ios): publish snapshot convergence corpus

* chore(ios): format benchmark evidence

* fix(ios-benchmark): admit proxy fixture anchors

* docs(ios): republish exact-head benchmark corpus

* fix(size): make publish asset evidence hermetic

* test(size): update publish preparation contracts

* fix: keep git-state gates out of mutation sandboxes
2026-09-01 21:39:05 +02:00

222 lines
7.2 KiB
JavaScript

import { execFileSync } from 'node:child_process';
import fs from 'node:fs';
import path from 'node:path';
import {
formatBytes,
formatDiff,
formatMaybeBytes,
formatSignedBytes,
} from './size-report-format.mjs';
const PACKAGE_COMPONENTS = [
{
id: 'js',
label: 'JS / dist source',
matches: (entryPath) => entryPath === 'dist/src' || entryPath.startsWith('dist/src/'),
},
{
id: 'apple-runner',
label: 'Apple runner source/project',
matches: (entryPath) =>
entryPath === 'dist/apple/runner' || entryPath.startsWith('dist/apple/runner/'),
},
{
id: 'apple-snapshot-presentation',
label: 'Apple snapshot presentation source',
matches: (entryPath) =>
entryPath === 'dist/apple/snapshot-presentation' ||
entryPath.startsWith('dist/apple/snapshot-presentation/'),
},
{
id: 'macos-helper',
label: 'macOS helper source',
matches: (entryPath) =>
entryPath === 'apple/macos-helper' || entryPath.startsWith('apple/macos-helper/'),
},
{
id: 'android-helpers',
label: 'Android helper artifacts',
matches: (entryPath) =>
/^android\/(?:snapshot-helper|ime-helper)\/dist(?:\/|$)/.test(entryPath),
},
{ id: 'other', label: 'Other package files' },
];
export function collectNpmPack(root) {
const cachePath = path.join(root, '.tmp', 'npm-cache');
fs.mkdirSync(cachePath, { recursive: true });
const stdout = execFileSync(
'npm',
['pack', '--ignore-scripts', '--json', '--pack-destination', cachePath, '--cache', cachePath],
{ cwd: root, encoding: 'utf8' },
);
const pack = parseNpmPackOutput(stdout);
const entries = normalizeNpmPackEntries(pack);
assertPublishPackageContents(entries);
return {
filename: pack.filename,
tarballPath: path.join(cachePath, pack.filename),
tarballBytes: pack.size,
unpackedBytes: pack.unpackedSize,
files: entries.length,
entries,
components: summarizeEntries(pack.unpackedSize, entries),
};
}
export function assertPublishPackageContents(entries) {
const paths = entries.map((entry) => entry.path);
const requiredAssets = [
{ directory: 'android/snapshot-helper/dist/', suffix: '.apk' },
{ directory: 'android/snapshot-helper/dist/', suffix: '.manifest.json' },
{ directory: 'android/ime-helper/dist/', suffix: '.apk' },
{ directory: 'android/ime-helper/dist/', suffix: '.manifest.json' },
];
const missingAssets = requiredAssets.filter(
(asset) =>
!paths.some(
(entryPath) => entryPath.startsWith(asset.directory) && entryPath.endsWith(asset.suffix),
),
);
if (missingAssets.length > 0) {
throw new Error(
`npm pack is missing publish assets: ${missingAssets
.map((asset) => `${asset.directory}*${asset.suffix}`)
.join(', ')}`,
);
}
const forbiddenPaths = paths.filter(
(entryPath) => entryPath === 'scripts' || entryPath.startsWith('scripts/'),
);
if (forbiddenPaths.length > 0) {
throw new Error(`npm pack includes benchmark or build scripts: ${forbiddenPaths.join(', ')}`);
}
}
function parseNpmPackOutput(stdout) {
const parsed = JSON.parse(stdout);
return Array.isArray(parsed) ? parsed[0] : parsed;
}
function normalizeNpmPackEntries(pack) {
if (!Array.isArray(pack.files)) {
throw new Error('npm pack did not return per-file path/size data in files[]');
}
return pack.files.map((entry) => {
if (
!entry ||
typeof entry.path !== 'string' ||
!Number.isSafeInteger(entry.size) ||
entry.size < 0
) {
throw new Error(`npm pack returned an invalid file entry: ${JSON.stringify(entry)}`);
}
return { path: entry.path, size: entry.size };
});
}
export function classifyNpmPackEntry(entry) {
const matches = PACKAGE_COMPONENTS.filter((component) => component.matches?.(entry.path));
if (matches.length > 1) {
throw new Error(
`Package entry ${JSON.stringify(entry.path)} matched ${matches.length} size components`,
);
}
return { ...entry, component: matches[0]?.id ?? 'other' };
}
export function summarizeNpmPackComponents(pack) {
return summarizeEntries(pack.unpackedSize, normalizeNpmPackEntries(pack));
}
function summarizeEntries(unpackedSize, entries) {
if (!Number.isSafeInteger(unpackedSize) || unpackedSize < 0) {
throw new Error(`npm pack returned an invalid unpackedSize: ${unpackedSize}`);
}
const totals = new Map(
PACKAGE_COMPONENTS.map((component) => [component.id, { files: 0, unpackedBytes: 0 }]),
);
for (const entry of entries.map(classifyNpmPackEntry)) {
const total = totals.get(entry.component);
total.files += 1;
total.unpackedBytes += entry.size;
}
const components = PACKAGE_COMPONENTS.map((component) => ({
id: component.id,
label: component.label,
...totals.get(component.id),
}));
const componentBytes = components.reduce(
(total, component) => total + component.unpackedBytes,
0,
);
if (componentBytes !== unpackedSize) {
throw new Error(
`Package component byte sum ${componentBytes} does not match npm pack unpackedSize ${unpackedSize}`,
);
}
return components;
}
export function formatPackageComponents(currentPack, basePack) {
const currentById = new Map(
(currentPack.components ?? []).map((component) => [component.id, component]),
);
const baseById = new Map(
(basePack?.components ?? []).map((component) => [component.id, component]),
);
const rows = PACKAGE_COMPONENTS.map((component) => {
const current = currentById.get(component.id)?.unpackedBytes ?? 0;
const base = baseById.get(component.id)?.unpackedBytes;
return `| ${component.label} | ${formatMaybeBytes(base)} | ${formatBytes(current)} | ${formatDiff(base, current)} |`;
});
return `### npm unpacked components
| Component | Base | Current | Diff |
|---|---:|---:|---:|
${rows.join('\n')}
`;
}
export function formatPackedFiles(currentEntries, baseEntries) {
if (!baseEntries) return formatTopPackedFiles(currentEntries);
const currentByPath = new Map(currentEntries.map((entry) => [entry.path, entry.size]));
const baseByPath = new Map(baseEntries.map((entry) => [entry.path, entry.size]));
const paths = new Set([...currentByPath.keys(), ...baseByPath.keys()]);
const rows = [...paths]
.map((filePath) => {
const current = currentByPath.get(filePath) ?? 0;
const base = baseByPath.get(filePath) ?? 0;
return { path: filePath, base, current, diff: current - base };
})
.filter((entry) => entry.diff !== 0)
.sort((left, right) => Math.abs(right.diff) - Math.abs(left.diff))
.slice(0, 10)
.map(
(entry) =>
`| \`${entry.path}\` | ${formatBytes(entry.base)} | ${formatBytes(entry.current)} | ${formatSignedBytes(entry.diff)} |`,
);
if (rows.length === 0) {
return '### Top changed packed files\n\nNo changed packed files.\n';
}
return `### Top changed packed files
| Packed file | Base | Current | Diff |
|---|---:|---:|---:|
${rows.join('\n')}
`;
}
function formatTopPackedFiles(entries) {
const rows = [...entries]
.sort((left, right) => right.size - left.size)
.slice(0, 10)
.map((entry) => `| \`${entry.path}\` | ${formatBytes(entry.size)} |`);
return `### Top packed files
| Packed file | Unpacked |
|---|---:|
${rows.join('\n')}
`;
}