test(platform): extract Apple perf and Android runtime fixtures (#2253)

* test(platform): extract shared fixtures from the Apple perf and Android runtime suites

Move the duplicated host-command routing, app-bundle writers, and ps
handlers behind packages/platform-apple/src/core/__tests__/perf.fixtures.ts,
and the Android runtime host, device, and ordinary-bind builders behind
packages/platform-android/src/runtime.fixtures.ts. Every test keeps its
title and its own assertions; test and assertion counts are unchanged.

* chore(gates): exclude .fixtures.ts modules from changed-line coverage

runtime.fixtures.ts (packages/platform-android/src) sits outside any
__tests__ dir, so vitest's coverage.include picked it up as production
source and the changed-line gate scored its 20 unreachable construction
lines directly, failing at 39.39% against the 70% threshold.

Add '**/*.fixtures.ts' to coverage.exclude (repo-wide convention:
40+ fixture modules, all test support, never production). The file
disappears from lcov and scripts/coverage-changed/model.ts's existing
excluded-path fallback reports it non-gating instead, with no
duplicate classifier needed there.

Planted red (targeted run against the real diff/model, not committed):
BEFORE (file present in lcov, all added lines uncovered): totalLines=110
coveredLines=0 pct=0 passed=false. AFTER (file absent from lcov post-fix):
totalLines=0 excludedTotal=99 excludedReason=excluded-path passed=true.
Confirmed against a real `vitest run --coverage` + `check:coverage-changed`
pass: runtime.fixtures.ts absent from coverage/lcov.info, gate PASS,
99 lines reported under excluded/excluded-path.
This commit is contained in:
Michał Pierzchała
2026-09-03 15:58:03 +02:00
committed by GitHub
parent 9941330dcc
commit 3726f027ce
5 changed files with 374 additions and 564 deletions
@@ -0,0 +1,110 @@
import type { AndroidClipboardShellSupport } from '@agent-device/contracts/android-clipboard-support';
import type { DeviceBinding } from '@agent-device/contracts/platform-runtime';
import type {
PlatformRuntimeHost,
PlatformRuntimeOperations,
PlatformRuntimeOwner,
} from '@agent-device/contracts/platform-runtime-operations';
import type { DeviceInfo } from '@agent-device/kernel/device';
export const ANDROID_EMULATOR: DeviceInfo = {
platform: 'android',
id: 'emulator-5554',
name: 'Android',
kind: 'emulator',
target: 'mobile',
booted: true,
};
export const UNKNOWN_KIND_DEVICE = {
...ANDROID_EMULATOR,
kind: 'unknown',
} as unknown as DeviceInfo;
const audioProbeHost: PlatformRuntimeHost['audioProbe'] = {
hostCapture: {
info: {
source: 'system-audio',
backend: 'fixture',
sourceCount: 0,
notes: () => [],
},
start: async () => {
throw new Error('Audio probe is outside this runtime fixture.');
},
inspectProcess: async () => 'missing',
terminateProcess: async () => 'already-missing',
},
web: { resolve: async () => undefined },
ownedProcesses: { replace: () => {}, clear: () => {} },
};
export const emptyAppInventory = {
apple: { listApps: async () => [] },
android: { listApps: async () => [] },
harmonyos: { listApps: async () => [] },
};
const emptyAppState = {
android: { run: async () => ({ stdout: '' }) },
harmonyos: { run: async () => ({ stdout: '' }) },
};
function localAndroidScreenRecording() {
return {
mode: 'local' as const,
start: async () => {
throw new Error('unused');
},
signal: async () => true,
isRunning: async () => false,
exists: async () => false,
pull: async () => ({ stdout: '', stderr: '', exitCode: 0 }),
remove: async () => true,
readManifest: async () => undefined,
writeManifest: async () => {},
removeManifest: async () => {},
};
}
/** The smallest host the Android runtime binds against; `overrides` replace whole facets. */
export function androidRuntimeHost(overrides: Record<string, unknown> = {}): PlatformRuntimeHost {
return {
androidTools: { probeClipboardShellSupport: async () => 'supported' as const },
processTransports: { resolve: async () => ({ mode: 'local' as const }) },
appInventory: emptyAppInventory,
localInteractors: { resolve: async () => ({}) },
audioProbe: audioProbeHost,
screenRecording: { android: { resolve: async () => localAndroidScreenRecording() } },
...overrides,
} as unknown as PlatformRuntimeHost;
}
/** A host with app state and device readiness, so navigation and clipboard cells can bind. */
export function androidNavigationHost(
probeClipboardShellSupport: () => Promise<AndroidClipboardShellSupport> = async () => 'supported',
): PlatformRuntimeHost {
return androidRuntimeHost({
androidTools: {
probeClipboardShellSupport,
runAdb: async () => ({ stdout: '', stderr: '', exitCode: 0 }),
},
appState: emptyAppState,
deviceReadiness: { android: { ensureReady: async (selected: DeviceInfo) => selected } },
});
}
export async function bindOrdinary(
runtime: PlatformRuntimeOwner,
device: DeviceInfo,
): Promise<DeviceBinding<PlatformRuntimeOperations>> {
return await runtime.bind({
device,
intent: { kind: 'ordinary' },
scope: {
signal: new AbortController().signal,
diagnostics: { emit: () => {} },
progress: { report: () => {} },
},
});
}
+78 -278
View File
@@ -7,62 +7,38 @@ import type {
} from '@agent-device/contracts/platform-runtime-operations';
import type { DeviceInfo } from '@agent-device/kernel/device';
import { createAndroidPlatformRuntime } from './runtime.ts';
import {
ANDROID_EMULATOR,
UNKNOWN_KIND_DEVICE,
androidNavigationHost,
androidRuntimeHost,
bindOrdinary,
emptyAppInventory,
} from './runtime.fixtures.ts';
const device: DeviceInfo = {
platform: 'android',
id: 'emulator-5554',
name: 'Android',
kind: 'emulator',
target: 'mobile',
booted: true,
};
const appStateUnavailable = {
available: false,
reason: 'unsupported-device-kind',
hint: 'Android appstate is supported only for Android emulators and devices.',
} as const;
const unknownKindDevice = { ...device, kind: 'unknown' } as unknown as DeviceInfo;
const audioProbeHost: PlatformRuntimeHost['audioProbe'] = {
hostCapture: {
info: {
source: 'system-audio',
backend: 'fixture',
sourceCount: 0,
notes: () => [],
},
start: async () => {
throw new Error('Audio probe is outside this runtime fixture.');
},
inspectProcess: async () => 'missing',
terminateProcess: async () => 'already-missing',
},
web: { resolve: async () => undefined },
ownedProcesses: { replace: () => {}, clear: () => {} },
};
test.each([
['emulator', device],
['device', { ...device, kind: 'device' as const }],
['unknown', unknownKindDevice],
['emulator', ANDROID_EMULATOR],
['device', { ...ANDROID_EMULATOR, kind: 'device' as const }],
['unknown', UNKNOWN_KIND_DEVICE],
])('classifies the Android %s runtime denominator', async (_name, runtimeDevice) => {
const listApps = vi.fn(async () => [{ id: 'com.example.app', name: 'Example' }]);
const appState = vi.fn(async () => ({
stdout: 'mCurrentFocus=Window{1 u0 com.example.app/.MainActivity}',
}));
const host = {
androidTools: { probeClipboardShellSupport: async () => 'supported' as const },
const host = androidRuntimeHost({
commands: {
which: async () => 'tool',
run: async () => ({ stdout: '1', stderr: '', exitCode: 0 }),
},
toolchains: { prepare: async () => {} },
clock: { now: () => 1, sleep: async () => {} },
processTransports: { resolve: async () => ({ mode: 'local' as const }) },
appInventory: {
apple: { listApps: async () => [] },
android: { listApps },
harmonyos: { listApps: async () => [] },
},
appInventory: { ...emptyAppInventory, android: { listApps } },
appState: {
android: { run: appState },
harmonyos: { run: async () => ({ stdout: '' }) },
@@ -76,36 +52,8 @@ test.each([
},
androidEmulator: { discover: async () => [], launch: () => 1, terminate: async () => {} },
},
localInteractors: { resolve: async () => ({}) },
audioProbe: audioProbeHost,
screenRecording: {
android: {
resolve: async () => ({
mode: 'local' as const,
start: async () => {
throw new Error('unused');
},
signal: async () => true,
isRunning: async () => false,
exists: async () => false,
pull: async () => ({ stdout: '', stderr: '', exitCode: 0 }),
remove: async () => true,
readManifest: async () => undefined,
writeManifest: async () => {},
removeManifest: async () => {},
}),
},
},
} as unknown as PlatformRuntimeHost;
const binding = await createAndroidPlatformRuntime(host).bind({
device: runtimeDevice,
intent: { kind: 'ordinary' },
scope: {
signal: new AbortController().signal,
diagnostics: { emit: () => {} },
progress: { report: () => {} },
},
});
const binding = await bindOrdinary(createAndroidPlatformRuntime(host), runtimeDevice);
const { facts } = binding;
expect(facts.device.providerMode).toBe('local');
expect(facts.operations.networkDump).toEqual({ available: true });
@@ -174,108 +122,31 @@ test.each([
});
test('rejects the non-discovered Android simulator cell for appstate', async () => {
const runtimeDevice = { ...device, kind: 'simulator' as const };
const host = {
androidTools: { probeClipboardShellSupport: async () => 'supported' as const },
processTransports: { resolve: async () => ({ mode: 'local' as const }) },
localInteractors: { resolve: async () => ({}) },
audioProbe: audioProbeHost,
const runtimeDevice = { ...ANDROID_EMULATOR, kind: 'simulator' as const };
const host = androidRuntimeHost({
appState: {
android: { run: async () => ({ stdout: '' }) },
harmonyos: { run: async () => ({ stdout: '' }) },
},
deviceReadiness: { android: { ensureReady: async (selected: DeviceInfo) => selected } },
screenRecording: {
android: {
resolve: async () => ({
mode: 'local' as const,
start: async () => {
throw new Error('unused');
},
signal: async () => true,
isRunning: async () => false,
exists: async () => false,
pull: async () => ({ stdout: '', stderr: '', exitCode: 0 }),
remove: async () => true,
readManifest: async () => undefined,
writeManifest: async () => {},
removeManifest: async () => {},
}),
},
},
} as unknown as PlatformRuntimeHost;
const binding = await createAndroidPlatformRuntime(host).bind({
device: runtimeDevice,
intent: { kind: 'ordinary' },
scope: {
signal: new AbortController().signal,
diagnostics: { emit: () => {} },
progress: { report: () => {} },
},
});
const binding = await bindOrdinary(createAndroidPlatformRuntime(host), runtimeDevice);
expect(binding.facts.operations.appState).toEqual(appStateUnavailable);
expect(binding.operations.appState).toBeUndefined();
});
function androidNavigationHostFixture(
probeClipboardShellSupport: () => Promise<AndroidClipboardShellSupport> = async () => 'supported',
) {
return {
androidTools: {
probeClipboardShellSupport,
runAdb: async () => ({ stdout: '', stderr: '', exitCode: 0 }),
},
processTransports: { resolve: async () => ({ mode: 'local' as const }) },
appInventory: {
apple: { listApps: async () => [] },
android: { listApps: async () => [] },
harmonyos: { listApps: async () => [] },
},
appState: {
android: { run: async () => ({ stdout: '' }) },
harmonyos: { run: async () => ({ stdout: '' }) },
},
deviceReadiness: { android: { ensureReady: async (selected: DeviceInfo) => selected } },
localInteractors: { resolve: async () => ({}) },
audioProbe: audioProbeHost,
screenRecording: {
android: {
resolve: async () => ({
mode: 'local' as const,
start: async () => {
throw new Error('unused');
},
signal: async () => true,
isRunning: async () => false,
exists: async () => false,
pull: async () => ({ stdout: '', stderr: '', exitCode: 0 }),
remove: async () => true,
readManifest: async () => undefined,
writeManifest: async () => {},
removeManifest: async () => {},
}),
},
},
} as unknown as PlatformRuntimeHost;
}
test.each([
['emulator', device],
['device', { ...device, kind: 'device' as const }],
['unknown', unknownKindDevice],
['emulator', ANDROID_EMULATOR],
['device', { ...ANDROID_EMULATOR, kind: 'device' as const }],
['unknown', UNKNOWN_KIND_DEVICE],
])(
'classifies Android %s back/home/orientation/keyboard facts through the shared touch gate',
async (_name, runtimeDevice) => {
const binding = await createAndroidPlatformRuntime(androidNavigationHostFixture()).bind({
device: runtimeDevice,
intent: { kind: 'ordinary' },
scope: {
signal: new AbortController().signal,
diagnostics: { emit: () => {} },
progress: { report: () => {} },
},
});
const binding = await bindOrdinary(
createAndroidPlatformRuntime(androidNavigationHost()),
runtimeDevice,
);
const { facts } = binding;
// back/home/orientation/keyboard status+dismiss+enter all ride the same adb-driven touch
@@ -303,16 +174,11 @@ test.each([
);
test('admits Android tv-remote only for a real TV target', async () => {
const tvDevice = { ...device, kind: 'emulator' as const, target: 'tv' as const };
const binding = await createAndroidPlatformRuntime(androidNavigationHostFixture()).bind({
device: tvDevice,
intent: { kind: 'ordinary' },
scope: {
signal: new AbortController().signal,
diagnostics: { emit: () => {} },
progress: { report: () => {} },
},
});
const tvDevice = { ...ANDROID_EMULATOR, kind: 'emulator' as const, target: 'tv' as const };
const binding = await bindOrdinary(
createAndroidPlatformRuntime(androidNavigationHost()),
tvDevice,
);
expect(binding.facts.operations.tvRemote).toEqual({ available: true });
expect(binding.operations.tvRemote).toBeTypeOf('function');
@@ -324,14 +190,10 @@ test('admits Android tv-remote only for a real TV target', async () => {
// bucket's name for a device with no declared kind, which `DeviceKind` cannot express.)
test('admits both clipboard halves and the app switcher on every real Android kind', async () => {
for (const kind of ['emulator', 'device'] as const) {
const binding = await createAndroidPlatformRuntime(androidNavigationHostFixture()).bind({
device: { ...device, id: `android-${kind}`, kind },
intent: { kind: 'ordinary' },
scope: {
signal: new AbortController().signal,
diagnostics: { emit: () => {} },
progress: { report: () => {} },
},
const binding = await bindOrdinary(createAndroidPlatformRuntime(androidNavigationHost()), {
...ANDROID_EMULATOR,
id: `android-${kind}`,
kind,
});
expect(binding.facts.operations.readClipboard).toEqual({ available: true });
expect(binding.facts.operations.writeClipboard).toEqual({ available: true });
@@ -355,16 +217,15 @@ test('admits both clipboard halves and the app switcher on every real Android ki
});
test('the synthetic Android simulator cell refuses back/home/orientation/keyboard like every other touch operation', async () => {
const simulatorDevice = { ...device, id: 'android-simulator', kind: 'simulator' as const };
const binding = await createAndroidPlatformRuntime(androidNavigationHostFixture()).bind({
device: simulatorDevice,
intent: { kind: 'ordinary' },
scope: {
signal: new AbortController().signal,
diagnostics: { emit: () => {} },
progress: { report: () => {} },
},
});
const simulatorDevice = {
...ANDROID_EMULATOR,
id: 'android-simulator',
kind: 'simulator' as const,
};
const binding = await bindOrdinary(
createAndroidPlatformRuntime(androidNavigationHost()),
simulatorDevice,
);
const { facts } = binding;
for (const operation of [
@@ -401,7 +262,7 @@ type LegacyLifecycleCell = Readonly<{
test.each([
[
'emulator',
device,
ANDROID_EMULATOR,
{
openTarget: true,
prepareAppleRunner: false,
@@ -412,7 +273,7 @@ test.each([
],
[
'device',
{ ...device, kind: 'device' as const },
{ ...ANDROID_EMULATOR, kind: 'device' as const },
{
openTarget: true,
prepareAppleRunner: false,
@@ -423,7 +284,7 @@ test.each([
],
[
'synthetic simulator',
{ ...device, id: 'android-simulator', kind: 'simulator' as const },
{ ...ANDROID_EMULATOR, id: 'android-simulator', kind: 'simulator' as const },
{
openTarget: false,
prepareAppleRunner: false,
@@ -435,44 +296,10 @@ test.each([
] satisfies ReadonlyArray<readonly [string, DeviceInfo, LegacyLifecycleCell]>)(
'classifies the Android %s lifecycle denominator against the legacy dispatch cell',
async (_name, runtimeDevice, legacy) => {
const host = {
androidTools: { probeClipboardShellSupport: async () => 'supported' as const },
processTransports: { resolve: async () => ({ mode: 'local' as const }) },
appInventory: {
apple: { listApps: async () => [] },
android: { listApps: async () => [] },
harmonyos: { listApps: async () => [] },
},
localInteractors: { resolve: async () => ({}) },
audioProbe: audioProbeHost,
screenRecording: {
android: {
resolve: async () => ({
mode: 'local' as const,
start: async () => {
throw new Error('unused');
},
signal: async () => true,
isRunning: async () => false,
exists: async () => false,
pull: async () => ({ stdout: '', stderr: '', exitCode: 0 }),
remove: async () => true,
readManifest: async () => undefined,
writeManifest: async () => {},
removeManifest: async () => {},
}),
},
},
} as unknown as PlatformRuntimeHost;
const binding = await createAndroidPlatformRuntime(host).bind({
device: runtimeDevice,
intent: { kind: 'ordinary' },
scope: {
signal: new AbortController().signal,
diagnostics: { emit: () => {} },
progress: { report: () => {} },
},
});
const binding = await bindOrdinary(
createAndroidPlatformRuntime(androidRuntimeHost()),
runtimeDevice,
);
const { facts } = binding;
expect(facts.device.providerMode).toBe('local');
expect(facts.operations.networkDump).toEqual({ available: true });
@@ -525,13 +352,13 @@ function expectLifecycleFacts(
// synthesis and to target-authored drag but never to a plain one-contact fling or pan.
test.each([
// name, device, plan, multiTouch, drag, scroll
['emulator', device, true, true, true, true],
['physical device', { ...device, kind: 'device' as const }, true, true, true, true],
['unknown kind', unknownKindDevice, true, true, true, true],
['TV target', { ...device, target: 'tv' as const }, true, false, false, true],
['emulator', ANDROID_EMULATOR, true, true, true, true],
['physical device', { ...ANDROID_EMULATOR, kind: 'device' as const }, true, true, true, true],
['unknown kind', UNKNOWN_KIND_DEVICE, true, true, true, true],
['TV target', { ...ANDROID_EMULATOR, target: 'tv' as const }, true, false, false, true],
[
'synthetic simulator row',
{ ...device, kind: 'simulator' as const },
{ ...ANDROID_EMULATOR, kind: 'simulator' as const },
false,
false,
false,
@@ -553,7 +380,7 @@ test.each([
test('carries the retired Android TV hints verbatim', async () => {
const facts = await createAndroidPlatformRuntime(gestureHost()).inspectFacts({
...device,
...ANDROID_EMULATOR,
target: 'tv',
});
expect(facts.operations.performMultiTouchGesturePlan).toEqual({
@@ -569,37 +396,20 @@ test('carries the retired Android TV hints verbatim', async () => {
test('binds only the Android gesture tiers the target admitted', async () => {
const bind = async (runtimeDevice: DeviceInfo) =>
await createAndroidPlatformRuntime(gestureHost()).bind({
device: runtimeDevice,
intent: { kind: 'ordinary' },
scope: {
signal: new AbortController().signal,
diagnostics: { emit: () => {} },
progress: { report: () => {} },
},
});
const phone = await bind(device);
await bindOrdinary(createAndroidPlatformRuntime(gestureHost()), runtimeDevice);
const phone = await bind(ANDROID_EMULATOR);
expect(phone.operations.performMultiTouchGesturePlan).toBeTypeOf('function');
expect(phone.operations.scrollDirection).toBeTypeOf('function');
const tv = await bind({ ...device, target: 'tv' });
const tv = await bind({ ...ANDROID_EMULATOR, target: 'tv' });
expect(tv.operations.performGesturePlan).toBeTypeOf('function');
expect(tv.operations.performMultiTouchGesturePlan).toBeUndefined();
expect(tv.operations.performTargetAuthoredDrag).toBeUndefined();
});
function gestureHost(): PlatformRuntimeHost {
return {
androidTools: { probeClipboardShellSupport: async () => 'supported' as const },
processTransports: { resolve: async () => ({ mode: 'local' as const }) },
appInventory: {
apple: { listApps: async () => [] },
android: { listApps: async () => [] },
harmonyos: { listApps: async () => [] },
},
localInteractors: { resolve: async () => ({}) },
audioProbe: audioProbeHost,
return androidRuntimeHost({
screenRecording: { android: { resolve: async () => ({ mode: 'local' as const }) } },
} as unknown as PlatformRuntimeHost;
});
}
// R55 defect, found on a Pixel 9 Pro XL / Android 36 emulator: the retired bucket admitted both
@@ -607,17 +417,10 @@ function gestureHost(): PlatformRuntimeHost {
// `clipboard read` then failed with `UNSUPPORTED_OPERATION` from the leaf. Admission now probes
// the same condition the leaf checks, so a build with no clipboard shell command refuses up front.
test('refuses both clipboard halves when the build reports no clipboard shell', async () => {
const binding = await createAndroidPlatformRuntime(
androidNavigationHostFixture(async () => 'unsupported'),
).bind({
device: { ...device, id: 'android-no-clipboard-shell', kind: 'device' },
intent: { kind: 'ordinary' },
scope: {
signal: new AbortController().signal,
diagnostics: { emit: () => {} },
progress: { report: () => {} },
},
});
const binding = await bindOrdinary(
createAndroidPlatformRuntime(androidNavigationHost(async () => 'unsupported')),
{ ...ANDROID_EMULATOR, id: 'android-no-clipboard-shell', kind: 'device' },
);
for (const key of ['readClipboard', 'writeClipboard'] as const) {
expect(binding.facts.operations[key]).toMatchObject({
@@ -635,8 +438,12 @@ test.each([['supported'], ['unsupported']] as const)(
'caches a definitive %s verdict instead of re-probing per inspection',
async (verdict) => {
const probe = vi.fn(async () => verdict);
const runtime = createAndroidPlatformRuntime(androidNavigationHostFixture(probe));
const target = { ...device, id: `android-probe-cache-${verdict}`, kind: 'device' as const };
const runtime = createAndroidPlatformRuntime(androidNavigationHost(probe));
const target = {
...ANDROID_EMULATOR,
id: `android-probe-cache-${verdict}`,
kind: 'device' as const,
};
await runtime.inspectFacts(target);
await runtime.inspectFacts(target);
@@ -650,17 +457,10 @@ test.each([['supported'], ['unsupported']] as const)(
// `capabilities` advertised — and must not be remembered, or one transport blip decides the
// question for the owner's whole life.
test('a failed probe refuses rather than fabricating availability', async () => {
const binding = await createAndroidPlatformRuntime(
androidNavigationHostFixture(async () => 'probe-failed'),
).bind({
device: { ...device, id: 'android-probe-failed', kind: 'device' },
intent: { kind: 'ordinary' },
scope: {
signal: new AbortController().signal,
diagnostics: { emit: () => {} },
progress: { report: () => {} },
},
});
const binding = await bindOrdinary(
createAndroidPlatformRuntime(androidNavigationHost(async () => 'probe-failed')),
{ ...ANDROID_EMULATOR, id: 'android-probe-failed', kind: 'device' },
);
for (const key of ['readClipboard', 'writeClipboard'] as const) {
expect(binding.facts.operations[key]).toMatchObject({ available: false });
@@ -676,8 +476,8 @@ test('a failed probe is not cached, so the next inspection asks again', async ()
.fn<() => Promise<AndroidClipboardShellSupport>>()
.mockResolvedValueOnce('probe-failed')
.mockResolvedValue('supported');
const runtime = createAndroidPlatformRuntime(androidNavigationHostFixture(probe));
const target = { ...device, id: 'android-probe-retry', kind: 'device' as const };
const runtime = createAndroidPlatformRuntime(androidNavigationHost(probe));
const target = { ...ANDROID_EMULATOR, id: 'android-probe-retry', kind: 'device' as const };
const first = await runtime.inspectFacts(target);
const second = await runtime.inspectFacts(target);
@@ -688,11 +488,11 @@ test('a failed probe is not cached, so the next inspection asks again', async ()
});
test('a host with no clipboard probe refuses rather than assuming support', async () => {
const host = androidNavigationHostFixture();
const host = androidNavigationHost();
const withoutProbe = { ...host, androidTools: {} } as unknown as PlatformRuntimeHost;
const facts = await createAndroidPlatformRuntime(withoutProbe).inspectFacts({
...device,
...ANDROID_EMULATOR,
id: 'android-no-probe',
kind: 'device',
});
@@ -0,0 +1,98 @@
import { promises as fs } from 'node:fs';
import path from 'node:path';
import { vi } from 'vitest';
import { runCmd } from '@agent-device/host-kit/command';
export type MockRunCmdResult = Awaited<ReturnType<typeof runCmd>>;
export type HostCommandHandler = (
cmd: string,
args: string[],
options: Parameters<typeof runCmd>[2],
) => Promise<MockRunCmdResult | null>;
/** Routes every `runCmd` call through `handlers` in order; the first non-null result wins. */
export function mockHostCommands(handlers: HostCommandHandler[]): void {
vi.mocked(runCmd).mockImplementation(async (cmd, args, options) => {
for (const handler of handlers) {
const result = await handler(cmd, args, options);
if (result) return result;
}
throw new Error(`unexpected command: ${cmd} ${args.join(' ')}`);
});
}
/** Writes `Example.app/Contents/Info.plist` under `tmpDir` and returns the bundle path. */
export async function writeMacosAppBundle(tmpDir: string, executable: string): Promise<string> {
const bundlePath = path.join(tmpDir, 'Example.app');
await fs.mkdir(path.join(bundlePath, 'Contents'), { recursive: true });
await fs.writeFile(
path.join(bundlePath, 'Contents', 'Info.plist'),
infoPlistXml(executable),
'utf8',
);
return bundlePath;
}
/** Writes a flat `Example.app/Info.plist` under `tmpDir` and returns the app path. */
export async function writeIosSimulatorApp(tmpDir: string, executable: string): Promise<string> {
const appPath = path.join(tmpDir, 'Example.app');
await fs.mkdir(appPath, { recursive: true });
await fs.writeFile(path.join(appPath, 'Info.plist'), infoPlistXml(executable), 'utf8');
return appPath;
}
/** `mdfind` locates the bundle; `plutil` fails so the executable is read from Info.plist. */
export function macosBundleLookup(bundlePath: string): HostCommandHandler {
return async (cmd) => {
if (cmd === 'mdfind') return { stdout: `${bundlePath}\n`, stderr: '', exitCode: 0 };
if (cmd === 'plutil') return plutilFallback();
return null;
};
}
/** `simctl get_app_container` locates the app; `plutil` fails like `macosBundleLookup`. */
export function iosSimulatorAppContainer(appPath: string): HostCommandHandler {
return async (cmd, args) => {
if (cmd === 'xcrun' && args.includes('get_app_container')) {
return { stdout: `${appPath}\n`, stderr: '', exitCode: 0 };
}
if (cmd === 'plutil') return plutilFallback();
return null;
};
}
export function hostPs(rows: string[]): HostCommandHandler {
return async (cmd) => (cmd === 'ps' ? psOutput(rows) : null);
}
export function simulatorPs(rows: string[]): HostCommandHandler {
return async (cmd, args) => (isSimulatorPs(cmd, args) ? psOutput(rows) : null);
}
export function simulatorPsUnavailable(): HostCommandHandler {
return async (cmd, args) =>
isSimulatorPs(cmd, args)
? { stdout: '', stderr: 'No such file or directory', exitCode: 2 }
: null;
}
export function isSimulatorPs(cmd: string, args: string[]): boolean {
return cmd === 'xcrun' && args.includes('spawn') && args.includes('ps');
}
function infoPlistXml(executable: string): string {
return [
'<?xml version="1.0" encoding="UTF-8"?>',
'<plist version="1.0"><dict>',
`<key>CFBundleExecutable</key><string>${executable}</string>`,
'</dict></plist>',
].join('');
}
function plutilFallback(): MockRunCmdResult {
return { stdout: '', stderr: 'mock fallback', exitCode: 1 };
}
function psOutput(rows: string[]): MockRunCmdResult {
return { stdout: rows.join('\n'), stderr: '', exitCode: 0 };
}
@@ -30,10 +30,21 @@ import { parseAppleFramePerfSample } from '../perf-frame.ts';
import { runCmd, runCmdBackground } from '@agent-device/host-kit/command';
import type { DeviceInfo } from '@agent-device/kernel/device';
import { AppError } from '@agent-device/kernel/errors';
import {
hostPs,
iosSimulatorAppContainer,
isSimulatorPs,
macosBundleLookup,
mockHostCommands,
simulatorPs,
simulatorPsUnavailable,
writeIosSimulatorApp,
writeMacosAppBundle,
type MockRunCmdResult,
} from './perf.fixtures.ts';
const mockRunCmd = vi.mocked(runCmd);
const mockRunCmdBackground = vi.mocked(runCmdBackground);
type MockRunCmdResult = Awaited<ReturnType<typeof runCmd>>;
type XcrunMockHandler = (args: string[]) => Promise<MockRunCmdResult | null>;
const IOS_SIMULATOR: DeviceInfo = {
@@ -160,39 +171,15 @@ test('parseAppleFramePerfSample summarizes app hitches and worst windows', () =>
test('sampleAppleMemoryPerf aggregates host ps memory for macOS app bundle', async () => {
const tmpDir = await mkdtempForTest('agent-device-macos-perf-');
const bundlePath = path.join(tmpDir, 'Example.app');
await fs.mkdir(path.join(bundlePath, 'Contents'), { recursive: true });
await fs.writeFile(
path.join(bundlePath, 'Contents', 'Info.plist'),
[
'<?xml version="1.0" encoding="UTF-8"?>',
'<plist version="1.0"><dict>',
'<key>CFBundleExecutable</key><string>ExampleExec</string>',
'</dict></plist>',
].join(''),
'utf8',
);
mockRunCmd.mockImplementation(async (cmd, args) => {
if (cmd === 'mdfind') {
return { stdout: `${bundlePath}\n`, stderr: '', exitCode: 0 };
}
if (cmd === 'plutil') {
return { stdout: '', stderr: 'mock fallback', exitCode: 1 };
}
if (cmd === 'ps') {
return {
stdout: [
`111 8.5 12000 ${path.join(bundlePath, 'Contents', 'MacOS', 'ExampleExec')}`,
`222 1.5 5000 ${path.join(bundlePath, 'Contents', 'MacOS', 'ExampleExec')} --helper`,
'333 9.0 9999 /Applications/Other.app/Contents/MacOS/Other',
].join('\n'),
stderr: '',
exitCode: 0,
};
}
throw new Error(`unexpected command: ${cmd} ${args.join(' ')}`);
});
const bundlePath = await writeMacosAppBundle(tmpDir, 'ExampleExec');
mockHostCommands([
macosBundleLookup(bundlePath),
hostPs([
`111 8.5 12000 ${path.join(bundlePath, 'Contents', 'MacOS', 'ExampleExec')}`,
`222 1.5 5000 ${path.join(bundlePath, 'Contents', 'MacOS', 'ExampleExec')} --helper`,
'333 9.0 9999 /Applications/Other.app/Contents/MacOS/Other',
]),
]);
try {
const memory = await sampleAppleMemoryPerf(MACOS_DEVICE, 'com.example.app');
@@ -205,38 +192,14 @@ test('sampleAppleMemoryPerf aggregates host ps memory for macOS app bundle', asy
test('sampleAppleMemoryPerf uses simctl spawn ps for iOS simulators', async () => {
const tmpDir = await mkdtempForTest('agent-device-ios-sim-perf-');
const appPath = path.join(tmpDir, 'Example.app');
await fs.mkdir(appPath, { recursive: true });
await fs.writeFile(
path.join(appPath, 'Info.plist'),
[
'<?xml version="1.0" encoding="UTF-8"?>',
'<plist version="1.0"><dict>',
'<key>CFBundleExecutable</key><string>Example Sim Exec</string>',
'</dict></plist>',
].join(''),
'utf8',
);
mockRunCmd.mockImplementation(async (cmd, args) => {
if (cmd === 'xcrun' && args.includes('get_app_container')) {
return { stdout: `${appPath}\n`, stderr: '', exitCode: 0 };
}
if (cmd === 'plutil') {
return { stdout: '', stderr: 'mock fallback', exitCode: 1 };
}
if (cmd === 'xcrun' && args.includes('spawn') && args.includes('ps')) {
return {
stdout: [
`111 12.0 8192 ${path.join(appPath, 'Example Sim Exec')}`,
'222 4.0 1024 SpringBoard',
].join('\n'),
stderr: '',
exitCode: 0,
};
}
throw new Error(`unexpected command: ${cmd} ${args.join(' ')}`);
});
const appPath = await writeIosSimulatorApp(tmpDir, 'Example Sim Exec');
mockHostCommands([
iosSimulatorAppContainer(appPath),
simulatorPs([
`111 12.0 8192 ${path.join(appPath, 'Example Sim Exec')}`,
'222 4.0 1024 SpringBoard',
]),
]);
try {
const memory = await sampleAppleMemoryPerf(IOS_SIMULATOR, 'com.example.sim');
@@ -249,38 +212,16 @@ test('sampleAppleMemoryPerf uses simctl spawn ps for iOS simulators', async () =
test('captureAppleMemorySnapshot records memgraph for iOS simulator processes', async () => {
const tmpDir = await mkdtempForTest('agent-device-ios-sim-memgraph-');
const appPath = path.join(tmpDir, 'Example.app');
const appPath = await writeIosSimulatorApp(tmpDir, 'ExampleSimExec');
const outPath = path.join(tmpDir, 'app.memgraph');
await fs.mkdir(appPath, { recursive: true });
await fs.writeFile(
path.join(appPath, 'Info.plist'),
[
'<?xml version="1.0" encoding="UTF-8"?>',
'<plist version="1.0"><dict>',
'<key>CFBundleExecutable</key><string>ExampleSimExec</string>',
'</dict></plist>',
].join(''),
'utf8',
);
mockRunCmd.mockImplementation(async (cmd, args, options) => {
if (cmd === 'xcrun' && args.includes('get_app_container')) {
return { stdout: `${appPath}\n`, stderr: '', exitCode: 0 };
}
if (cmd === 'plutil') {
return { stdout: '', stderr: 'mock fallback', exitCode: 1 };
}
if (cmd === 'xcrun' && args.includes('ps')) {
return {
stdout: [
`111 1.0 8192 ${path.join(appPath, 'ExampleSimExec')}`,
`222 1.0 16384 ${path.join(appPath, 'ExampleSimExec')} --helper`,
].join('\n'),
stderr: '',
exitCode: 0,
};
}
if (cmd === 'xcrun' && args.includes('leaks')) {
mockHostCommands([
iosSimulatorAppContainer(appPath),
simulatorPs([
`111 1.0 8192 ${path.join(appPath, 'ExampleSimExec')}`,
`222 1.0 16384 ${path.join(appPath, 'ExampleSimExec')} --helper`,
]),
async (cmd, args, options) => {
if (cmd !== 'xcrun' || !args.includes('leaks')) return null;
assert.equal(options?.timeoutMs, 120_000);
assert.deepEqual(args, [
'simctl',
@@ -291,10 +232,9 @@ test('captureAppleMemorySnapshot records memgraph for iOS simulator processes',
'222',
]);
await fs.writeFile(outPath, 'memgraph-bytes', 'utf8');
return { stdout: '', stderr: '', exitCode: 0 };
}
throw new Error(`unexpected command: ${cmd} ${args.join(' ')}`);
});
return emptyRunResult();
},
]);
try {
const snapshot = await captureAppleMemorySnapshot(IOS_SIMULATOR, 'com.example.sim', outPath);
@@ -311,41 +251,18 @@ test('captureAppleMemorySnapshot records memgraph for iOS simulator processes',
test('captureAppleMemorySnapshot records memgraph for macOS app processes', async () => {
const tmpDir = await mkdtempForTest('agent-device-macos-memgraph-');
const bundlePath = path.join(tmpDir, 'Example.app');
const bundlePath = await writeMacosAppBundle(tmpDir, 'ExampleExec');
const outPath = path.join(tmpDir, 'app.memgraph');
await fs.mkdir(path.join(bundlePath, 'Contents'), { recursive: true });
await fs.writeFile(
path.join(bundlePath, 'Contents', 'Info.plist'),
[
'<?xml version="1.0" encoding="UTF-8"?>',
'<plist version="1.0"><dict>',
'<key>CFBundleExecutable</key><string>ExampleExec</string>',
'</dict></plist>',
].join(''),
'utf8',
);
mockRunCmd.mockImplementation(async (cmd, args) => {
if (cmd === 'mdfind') {
return { stdout: `${bundlePath}\n`, stderr: '', exitCode: 0 };
}
if (cmd === 'plutil') {
return { stdout: '', stderr: 'mock fallback', exitCode: 1 };
}
if (cmd === 'ps') {
return {
stdout: `111 1.0 12000 ${path.join(bundlePath, 'Contents', 'MacOS', 'ExampleExec')}`,
stderr: '',
exitCode: 0,
};
}
if (cmd === 'leaks') {
mockHostCommands([
macosBundleLookup(bundlePath),
hostPs([`111 1.0 12000 ${path.join(bundlePath, 'Contents', 'MacOS', 'ExampleExec')}`]),
async (cmd, args) => {
if (cmd !== 'leaks') return null;
assert.deepEqual(args, [`--outputGraph=${outPath}`, '111']);
await fs.writeFile(outPath, 'mac-memgraph-bytes', 'utf8');
return { stdout: '', stderr: '', exitCode: 0 };
}
throw new Error(`unexpected command: ${cmd} ${args.join(' ')}`);
});
return emptyRunResult();
},
]);
try {
const snapshot = await captureAppleMemorySnapshot(MACOS_DEVICE, 'com.example.app', outPath);
@@ -361,40 +278,17 @@ test('captureAppleMemorySnapshot records memgraph for macOS app processes', asyn
test('captureAppleMemorySnapshot removes partial memgraph when leaks exits nonzero', async () => {
const tmpDir = await mkdtempForTest('agent-device-ios-memgraph-fail-');
const appPath = path.join(tmpDir, 'Example.app');
const appPath = await writeIosSimulatorApp(tmpDir, 'ExampleSimExec');
const outPath = path.join(tmpDir, 'app.memgraph');
await fs.mkdir(appPath, { recursive: true });
await fs.writeFile(
path.join(appPath, 'Info.plist'),
[
'<?xml version="1.0" encoding="UTF-8"?>',
'<plist version="1.0"><dict>',
'<key>CFBundleExecutable</key><string>ExampleSimExec</string>',
'</dict></plist>',
].join(''),
'utf8',
);
mockRunCmd.mockImplementation(async (cmd, args) => {
if (cmd === 'xcrun' && args.includes('get_app_container')) {
return { stdout: `${appPath}\n`, stderr: '', exitCode: 0 };
}
if (cmd === 'plutil') {
return { stdout: '', stderr: 'mock fallback', exitCode: 1 };
}
if (cmd === 'xcrun' && args.includes('ps')) {
return {
stdout: `111 1.0 8192 ${path.join(appPath, 'ExampleSimExec')}`,
stderr: '',
exitCode: 0,
};
}
if (cmd === 'xcrun' && args.includes('leaks')) {
mockHostCommands([
iosSimulatorAppContainer(appPath),
simulatorPs([`111 1.0 8192 ${path.join(appPath, 'ExampleSimExec')}`]),
async (cmd, args) => {
if (cmd !== 'xcrun' || !args.includes('leaks')) return null;
await fs.writeFile(outPath, 'partial-memgraph', 'utf8');
return { stdout: '', stderr: 'permission denied', exitCode: 1 };
}
throw new Error(`unexpected command: ${cmd} ${args.join(' ')}`);
});
},
]);
try {
await assert.rejects(
@@ -409,35 +303,13 @@ test('captureAppleMemorySnapshot removes partial memgraph when leaks exits nonze
test('captureAppleMemorySnapshot removes partial memgraph and hints when leaks times out', async () => {
const tmpDir = await mkdtempForTest('agent-device-ios-memgraph-timeout-');
const appPath = path.join(tmpDir, 'Example.app');
const appPath = await writeIosSimulatorApp(tmpDir, 'ExampleSimExec');
const outPath = path.join(tmpDir, 'app.memgraph');
await fs.mkdir(appPath, { recursive: true });
await fs.writeFile(
path.join(appPath, 'Info.plist'),
[
'<?xml version="1.0" encoding="UTF-8"?>',
'<plist version="1.0"><dict>',
'<key>CFBundleExecutable</key><string>ExampleSimExec</string>',
'</dict></plist>',
].join(''),
'utf8',
);
mockRunCmd.mockImplementation(async (cmd, args) => {
if (cmd === 'xcrun' && args.includes('get_app_container')) {
return { stdout: `${appPath}\n`, stderr: '', exitCode: 0 };
}
if (cmd === 'plutil') {
return { stdout: '', stderr: 'mock fallback', exitCode: 1 };
}
if (cmd === 'xcrun' && args.includes('ps')) {
return {
stdout: `111 1.0 8192 ${path.join(appPath, 'ExampleSimExec')}`,
stderr: '',
exitCode: 0,
};
}
if (cmd === 'xcrun' && args.includes('leaks')) {
mockHostCommands([
iosSimulatorAppContainer(appPath),
simulatorPs([`111 1.0 8192 ${path.join(appPath, 'ExampleSimExec')}`]),
async (cmd, args) => {
if (cmd !== 'xcrun' || !args.includes('leaks')) return null;
await fs.writeFile(outPath, 'partial-memgraph', 'utf8');
throw new AppError('COMMAND_FAILED', 'xcrun timed out after 120000ms', {
cmd,
@@ -447,9 +319,8 @@ test('captureAppleMemorySnapshot removes partial memgraph and hints when leaks t
exitCode: -1,
timeoutMs: 120_000,
});
}
throw new Error(`unexpected command: ${cmd} ${args.join(' ')}`);
});
},
]);
try {
await assert.rejects(
@@ -484,27 +355,11 @@ test('captureAppleMemorySnapshot reports physical iOS as unavailable', async ()
test('captureAppleMemorySnapshot reports iOS simulator without process tools as unavailable', async () => {
const tmpDir = await mkdtempForTest('agent-device-ios-sim-no-ps-');
const appPath = path.join(tmpDir, 'Example.app');
await fs.mkdir(appPath, { recursive: true });
await fs.writeFile(
path.join(appPath, 'Info.plist'),
[
'<?xml version="1.0" encoding="UTF-8"?>',
'<plist version="1.0"><dict>',
'<key>CFBundleExecutable</key><string>ExampleSimExec</string>',
'</dict></plist>',
].join(''),
'utf8',
);
mockRunCmd.mockImplementation(async (cmd, args) => {
if (cmd === 'xcrun' && args.includes('get_app_container')) {
return { stdout: `${appPath}\n`, stderr: '', exitCode: 0 };
}
if (cmd === 'plutil') {
return { stdout: '', stderr: 'mock fallback', exitCode: 1 };
}
if (cmd === 'xcrun' && args.includes('ps')) {
const appPath = await writeIosSimulatorApp(tmpDir, 'ExampleSimExec');
mockHostCommands([
iosSimulatorAppContainer(appPath),
async (cmd, args) => {
if (!isSimulatorPs(cmd, args)) return null;
throw new AppError(
'COMMAND_FAILED',
'The operation couldnt be completed. No such file or directory',
@@ -517,9 +372,8 @@ test('captureAppleMemorySnapshot reports iOS simulator without process tools as
processExitError: true,
},
);
}
throw new Error(`unexpected command: ${cmd} ${args.join(' ')}`);
});
},
]);
try {
const snapshot = await captureAppleMemorySnapshot(
@@ -538,41 +392,12 @@ test('captureAppleMemorySnapshot reports iOS simulator without process tools as
test('sampleAppleMemoryPerf falls back to host ps when simulator ps is unavailable', async () => {
const tmpDir = await mkdtempForTest('agent-device-ios-sim-perf-');
const appPath = path.join(tmpDir, 'Example.app');
await fs.mkdir(appPath, { recursive: true });
await fs.writeFile(
path.join(appPath, 'Info.plist'),
[
'<?xml version="1.0" encoding="UTF-8"?>',
'<plist version="1.0"><dict>',
'<key>CFBundleExecutable</key><string>Example Sim Exec</string>',
'</dict></plist>',
].join(''),
'utf8',
);
mockRunCmd.mockImplementation(async (cmd, args) => {
if (cmd === 'xcrun' && args.includes('get_app_container')) {
return { stdout: `${appPath}\n`, stderr: '', exitCode: 0 };
}
if (cmd === 'plutil') {
return { stdout: '', stderr: 'mock fallback', exitCode: 1 };
}
if (cmd === 'xcrun' && args.includes('spawn') && args.includes('ps')) {
return { stdout: '', stderr: 'No such file or directory', exitCode: 2 };
}
if (cmd === 'ps') {
return {
stdout: [
`111 12.0 8192 ${path.join(appPath, 'Example Sim Exec')}`,
'222 4.0 1024 SpringBoard',
].join('\n'),
stderr: '',
exitCode: 0,
};
}
throw new Error(`unexpected command: ${cmd} ${args.join(' ')}`);
});
const appPath = await writeIosSimulatorApp(tmpDir, 'Example Sim Exec');
mockHostCommands([
iosSimulatorAppContainer(appPath),
simulatorPsUnavailable(),
hostPs([`111 12.0 8192 ${path.join(appPath, 'Example Sim Exec')}`, '222 4.0 1024 SpringBoard']),
]);
try {
const memory = await sampleAppleMemoryPerf(IOS_SIMULATOR, 'com.example.sim');
@@ -659,39 +484,15 @@ test('sampleAppleFramePerf retries transient kperf lock failures', async () => {
test('startAppleXctracePerfCapture attaches to an active iOS simulator app process', async () => {
const tmpDir = await mkdtempForTest('agent-device-xctrace-sim-');
const appPath = path.join(tmpDir, 'Example.app');
const appPath = await writeIosSimulatorApp(tmpDir, 'Example Sim Exec');
const tracePath = path.join(tmpDir, 'app.trace');
await fs.mkdir(appPath, { recursive: true });
await fs.writeFile(
path.join(appPath, 'Info.plist'),
[
'<?xml version="1.0" encoding="UTF-8"?>',
'<plist version="1.0"><dict>',
'<key>CFBundleExecutable</key><string>Example Sim Exec</string>',
'</dict></plist>',
].join(''),
'utf8',
);
mockRunCmd.mockImplementation(async (cmd, args) => {
if (cmd === 'xcrun' && args.includes('get_app_container')) {
return { stdout: `${appPath}\n`, stderr: '', exitCode: 0 };
}
if (cmd === 'plutil') {
return { stdout: '', stderr: 'mock fallback', exitCode: 1 };
}
if (cmd === 'xcrun' && args.includes('spawn') && args.includes('ps')) {
return {
stdout: [
`111 12.0 8192 ${path.join(appPath, 'Example Sim Exec')}`,
'222 4.0 1024 SpringBoard',
].join('\n'),
stderr: '',
exitCode: 0,
};
}
throw new Error(`unexpected command: ${cmd} ${args.join(' ')}`);
});
mockHostCommands([
iosSimulatorAppContainer(appPath),
simulatorPs([
`111 12.0 8192 ${path.join(appPath, 'Example Sim Exec')}`,
'222 4.0 1024 SpringBoard',
]),
]);
try {
const capture = await startAppleXctracePerfCapture({
+1
View File
@@ -242,6 +242,7 @@ export default defineConfig({
exclude: [
'src/**/*.test.ts',
'src/**/__tests__/**',
'**/*.fixtures.ts',
'src/**/*-types.ts',
'src/**/types.ts',
'src/sdk/**',