Files
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

110 lines
3.7 KiB
TypeScript

import { execFileSync } from 'node:child_process';
import os from 'node:os';
import path from 'node:path';
import type { GitRevision, HostIdentity, Target, Toolchain } from './types.ts';
export function readToolchain(): Toolchain {
return {
node: process.version,
pnpm: commandVersion('pnpm', ['--version']),
xcode: commandText('xcodebuild', ['-version']),
simctl: commandText('xcrun', ['simctl', '--version']),
os: `${os.platform()} ${os.release()}`,
arch: os.arch(),
};
}
export function readHostIdentity(): HostIdentity {
return parseHardwareProfile(commandText('system_profiler', ['SPHardwareDataType']));
}
export function parseHardwareProfile(output: string): HostIdentity {
const fields = new Map<string, string>();
for (const line of output.split('\n')) {
const match = /^\s*([^:]+):\s*(.+?)\s*$/u.exec(line);
if (match) fields.set(match[1]!, match[2]!);
}
const model = requiredHardwareField(fields, 'Model Name');
const modelIdentifier = requiredHardwareField(fields, 'Model Identifier');
const cpu = fields.get('Chip') ?? fields.get('Processor Name');
if (!cpu) throw new Error('system_profiler did not report a Mac CPU.');
const coreText = requiredHardwareField(fields, 'Total Number of Cores');
const coreMatch = /^(\d+)\b/u.exec(coreText);
const cpuCores = Number(coreMatch?.[1]);
if (!Number.isSafeInteger(cpuCores) || cpuCores < 1) {
throw new Error(`system_profiler reported an invalid core count: ${coreText}`);
}
return { model, modelIdentifier, cpu, cpuCores };
}
export function readGitRevision(repoRoot: string): GitRevision {
return {
commit: commandText('git', ['rev-parse', 'HEAD'], repoRoot),
branch: commandText('git', ['branch', '--show-current'], repoRoot) || 'detached',
dirty: commandText('git', ['status', '--porcelain'], repoRoot).length > 0,
};
}
export function readTarget(udid: string, appId: string, appPath?: string): Target {
const inventory = parseSimctlDevices(
commandText('xcrun', ['simctl', 'list', 'devices', 'available', '--json']),
);
const device = inventory.find((candidate) => candidate.udid === udid);
if (!device) throw new Error(`Simulator ${udid} is not available in simctl inventory.`);
return {
platform: 'ios',
kind: 'simulator',
udid,
name: device.name,
runtime: device.runtime,
appId,
...(appPath ? { appPath } : {}),
};
}
function commandText(command: string, args: string[], cwd?: string): string {
try {
return execFileSync(command, args, {
cwd,
encoding: 'utf8',
env: { ...process.env, AGENT_DEVICE_NO_UPDATE_NOTIFIER: '1' },
maxBuffer: 1024 * 1024,
stdio: ['ignore', 'pipe', 'ignore'],
}).trim();
} catch {
return 'unavailable';
}
}
function commandVersion(command: string, args: string[]): string {
return commandText(command, args) || 'unavailable';
}
function requiredHardwareField(fields: Map<string, string>, name: string): string {
const value = fields.get(name);
if (!value) throw new Error(`system_profiler did not report ${name}.`);
return value;
}
function parseSimctlDevices(
output: string,
): Array<{ udid: string; name: string; runtime: string }> {
if (output === 'unavailable') return [];
try {
const parsed = JSON.parse(output) as {
devices?: Record<string, Array<{ udid?: string; name?: string; state?: string }>>;
};
return Object.entries(parsed.devices ?? {}).flatMap(([runtime, devices]) =>
devices.flatMap((device) =>
device.udid && device.name ? [{ udid: device.udid, name: device.name, runtime }] : [],
),
);
} catch {
return [];
}
}
export function resolveRepoRoot(): string {
return path.resolve(import.meta.dirname, '..', '..');
}