mirror of
https://github.com/callstack/agent-device.git
synced 2026-09-14 20:06:34 +08:00
b15121ffc8
* 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
170 lines
4.6 KiB
TypeScript
170 lines
4.6 KiB
TypeScript
import { execFile, spawnSync } from 'node:child_process';
|
|
import { performance } from 'node:perf_hooks';
|
|
import { asRecord, readString } from './result-values.ts';
|
|
|
|
const MAX_BUFFER = 64 * 1024 * 1024;
|
|
|
|
export type CliResult = {
|
|
exitCode: number;
|
|
startedAt: string;
|
|
finishedAt: string;
|
|
wallClockMs: number;
|
|
stdout: string;
|
|
stderr: string;
|
|
payload: unknown;
|
|
ok: boolean;
|
|
spawnErrorCode?: string;
|
|
};
|
|
|
|
export type CliContext = {
|
|
repoRoot: string;
|
|
stateDir: string;
|
|
session: string;
|
|
udid: string;
|
|
derivedPath: string;
|
|
extraFlags?: string[];
|
|
};
|
|
|
|
export function runCli(context: CliContext, args: string[], timeoutMs = 300_000): CliResult {
|
|
const argv = buildArgv(context, args);
|
|
const startedAt = new Date().toISOString();
|
|
const started = performance.now();
|
|
const result = spawnSync(process.execPath, argv, {
|
|
cwd: context.repoRoot,
|
|
env: { ...process.env, AGENT_DEVICE_NO_UPDATE_NOTIFIER: '1' },
|
|
encoding: 'utf8',
|
|
maxBuffer: MAX_BUFFER,
|
|
timeout: timeoutMs,
|
|
});
|
|
return buildResult({
|
|
startedAt,
|
|
started,
|
|
exitCode: typeof result.status === 'number' ? result.status : -1,
|
|
stdout: result.stdout,
|
|
stderr: result.stderr,
|
|
error: result.error,
|
|
});
|
|
}
|
|
|
|
export async function runCliAsync(
|
|
context: CliContext,
|
|
args: string[],
|
|
timeoutMs = 300_000,
|
|
): Promise<CliResult> {
|
|
const argv = buildArgv(context, args);
|
|
const startedAt = new Date().toISOString();
|
|
const started = performance.now();
|
|
return await new Promise((resolve) => {
|
|
execFile(
|
|
process.execPath,
|
|
argv,
|
|
{
|
|
cwd: context.repoRoot,
|
|
env: { ...process.env, AGENT_DEVICE_NO_UPDATE_NOTIFIER: '1' },
|
|
encoding: 'utf8',
|
|
maxBuffer: MAX_BUFFER,
|
|
timeout: timeoutMs,
|
|
},
|
|
(error, stdout, stderr) => {
|
|
resolve(
|
|
buildResult({
|
|
startedAt,
|
|
started,
|
|
exitCode: typeof error?.code === 'number' ? error.code : error ? -1 : 0,
|
|
stdout,
|
|
stderr,
|
|
error,
|
|
}),
|
|
);
|
|
},
|
|
);
|
|
});
|
|
}
|
|
|
|
function buildArgv(context: CliContext, args: string[]): string[] {
|
|
return ['bin/agent-device.mjs', ...args, ...baseFlags(context), '--json'];
|
|
}
|
|
|
|
function buildResult(options: {
|
|
startedAt: string;
|
|
started: number;
|
|
exitCode: number;
|
|
stdout: string | Buffer | null;
|
|
stderr: string | Buffer | null;
|
|
error?: Error | null;
|
|
}): CliResult {
|
|
const stdout = readOutput(options.stdout);
|
|
const stderr = readOutput(options.stderr);
|
|
const spawnErrorCode = readAsyncErrorCode(options.error);
|
|
const spawnError = options.error ? `${options.error.name}: ${options.error.message}` : '';
|
|
const payload = parseJson(stdout);
|
|
return {
|
|
exitCode: options.exitCode,
|
|
startedAt: options.startedAt,
|
|
finishedAt: new Date().toISOString(),
|
|
wallClockMs: performance.now() - options.started,
|
|
stdout,
|
|
stderr: [stderr, spawnError].filter(Boolean).join('\n'),
|
|
payload,
|
|
ok: options.exitCode === 0 && !isExplicitFailure(payload) && !isBatchStepFailure(payload),
|
|
...(spawnErrorCode ? { spawnErrorCode } : {}),
|
|
};
|
|
}
|
|
|
|
function readAsyncErrorCode(error: Error | null | undefined): string | undefined {
|
|
const processError = error as
|
|
| (NodeJS.ErrnoException & { killed?: boolean; signal?: NodeJS.Signals | null })
|
|
| null
|
|
| undefined;
|
|
if (processError?.killed && processError.signal === 'SIGTERM') return 'ETIMEDOUT';
|
|
return readString(processError?.code);
|
|
}
|
|
|
|
function baseFlags(context: CliContext): string[] {
|
|
return [
|
|
'--state-dir',
|
|
context.stateDir,
|
|
'--session',
|
|
context.session,
|
|
'--platform',
|
|
'ios',
|
|
'--udid',
|
|
context.udid,
|
|
'--ios-xctest-derived-data-path',
|
|
context.derivedPath,
|
|
...(context.extraFlags ?? []),
|
|
];
|
|
}
|
|
|
|
function readOutput(value: string | Buffer | null): string {
|
|
return typeof value === 'string' ? value : (value?.toString('utf8') ?? '');
|
|
}
|
|
|
|
function parseJson(value: string): unknown {
|
|
const trimmed = value.trim();
|
|
if (!trimmed) return undefined;
|
|
try {
|
|
return JSON.parse(trimmed);
|
|
} catch {
|
|
const start = trimmed.indexOf('{');
|
|
const end = trimmed.lastIndexOf('}');
|
|
if (start < 0 || end <= start) return undefined;
|
|
try {
|
|
return JSON.parse(trimmed.slice(start, end + 1));
|
|
} catch {
|
|
return undefined;
|
|
}
|
|
}
|
|
}
|
|
|
|
function isExplicitFailure(value: unknown): boolean {
|
|
const record = asRecord(value);
|
|
return record?.ok === false || Boolean(asRecord(record?.data)?.initialSnapshotError);
|
|
}
|
|
|
|
function isBatchStepFailure(value: unknown): boolean {
|
|
const results = asRecord(asRecord(value)?.data)?.results;
|
|
const first = Array.isArray(results) ? asRecord(results[0]) : undefined;
|
|
return first?.ok === false;
|
|
}
|