refactor: collapse public Platform ios/macos into apple (#979) (#1002)

* refactor: collapse public Platform ios/macos into apple (#979)

Phase 3 d.3: collapse the internal `Platform` union from `ios`/`macos` to a
single `apple` platform, with `appleOs` as the sole OS discriminant. Approach
(b) NON-BREAKING: the daemon still ACCEPTS the legacy `ios`/`macos` selectors on
every read path and still EMITS the leaf `ios`/`macos` strings on every output,
so machine consumers see no change.

Kernel (src/kernel/device.ts):
- PLATFORMS = ['apple','android','linux','web']; add PUBLIC_PLATFORMS (leaf) and
  PublicPlatform; PLATFORM_SELECTORS keeps legacy `ios`/`macos` as input aliases.
- New predicates: isMacOs (appleOs- or legacy-leaf-based), isIosFamily (the
  post-collapse equivalent of `platform === 'ios'`), publicPlatformString (output
  projection), deviceFieldsFromPublicPlatform (inverse), isPublicPlatform.
- isMobilePlatform and matchesPlatformSelector are now device-aware (appleOs).

Discovery now stamps `platform: 'apple'` (+ appleOs); ~125 internal
`device.platform === 'ios'|'macos'` branch sites migrated to the predicates,
behavior-preserving. Apple plugin owns `['apple']`; platformDescriptors collapse
to one `apple` row.

Output projection (approach b) emits the leaf via publicPlatformString at:
devices / session_list (session-inventory), boot / shutdown / appstate /
prepare-ios-runner (session-state, session), the selector/backend platform
(selector-runtime/screenshot-runtime/snapshot-runtime/interaction-runtime),
proxy device key, request-lock backfill, runtime-set binding, click-button
validation, and both `.ad` context-line writers.

Contracts/client keep leaf types (PublicPlatform); read paths (parsePlatform,
REPLAY_METADATA_PLATFORMS, matchesPlatformSelector) accept `apple` + legacy
leaves. Adds a parity test gate (platform-collapse-parity.test.ts).

Refs #979 (part of #972).

* fix: project platform to the public leaf at open/perf response sites (#979)

The Platform collapse left two daemon response builders emitting the raw
internal `device.platform` ('apple'), which the client normalizer rejects
(isPublicPlatform excludes 'apple') — dropping the resolved device from the
response:
- session-open-surface.ts: `open` result `platform`/device projection.
- session-perf.ts: the perf/frames/memory base response builders.
Both now go through `publicPlatformString(device)`, so output stays the leaf
`ios`/`macos` per approach (b). (The android/non-apple perf branches were
already leaf-safe.)

Also update macos-desktop provider test: the lifecycle mock observes the
INTERNAL DeviceInfo, which is now `platform:'apple'` (+ appleOs:'macos'), so the
recorded tag is `prepare:apple:desktop`.

Fixes the provider-integration assertions that blocked both the Integration
Tests and Coverage CI jobs (both run the provider-integration project).
Verified: provider-integration 82/82, coverage passes, tsc/oxlint/oxfmt/layering/
fallow green.

* fix: project platform to the public leaf at nested output sites (#979)

The Platform collapse (approach b) projects device.platform through
publicPlatformString at emit sites so machine consumers keep seeing the
leaf ios/macos and never the internal `apple`. Several nested output
fields were missed. Project them and narrow their emitted types to
PublicPlatform:

- Apple perf memory snapshot support (buildAppleMemorySnapshotSupport) —
  response.support.platform / artifact.support.platform, plus the
  sibling sampleAppleFramePerf error data.
- Apple xctrace perf capture/result platform surfaced in the perf
  cpu-profile started/stopped response data.
- snapshotDiagnostics.stats.platform (recordSnapshotTiming) surfaced in
  snapshot/test response data and the slow-snapshot warning string.
- doctor target-app evidence.platform + human summary, and doctor
  target-app-device evidence.booted[].platform.
- provider/cloud UNSUPPORTED_OPERATION error.data.platform for cloud
  Apple devices (reachable via deviceFieldsFromPublicPlatform).

Internal 'apple' emissions (selector-matching input, diagnostic
emitDiagnostic telemetry, session appLog state, replay .ad flags) are
left as-is. Adds focused tests pinning Apple perf memory support to the
leaf and a guard asserting no emitted platform field equals 'apple'.
This commit is contained in:
Michał Pierzchała
2026-07-01 19:29:46 +02:00
committed by GitHub
parent d1b56c0897
commit cd1551bf42
148 changed files with 1072 additions and 674 deletions
+3 -3
View File
@@ -1,10 +1,10 @@
import { test } from 'vitest';
import assert from 'node:assert/strict';
import { normalizeOpenDevice } from '../client/client-normalizers.ts';
import { PLATFORMS } from '../kernel/device.ts';
import { PUBLIC_PLATFORMS } from '../kernel/device.ts';
test('normalizeOpenDevice accepts exactly the canonical leaf platforms', () => {
for (const platform of PLATFORMS) {
for (const platform of PUBLIC_PLATFORMS) {
const result = normalizeOpenDevice({
platform,
id: 'device-1',
@@ -14,7 +14,7 @@ test('normalizeOpenDevice accepts exactly the canonical leaf platforms', () => {
assert.equal(result.platform, platform);
}
// Lock the membership so the derived check cannot silently widen/narrow.
assert.deepEqual([...PLATFORMS], ['ios', 'macos', 'android', 'linux', 'web']);
assert.deepEqual([...PUBLIC_PLATFORMS], ['ios', 'macos', 'android', 'linux', 'web']);
});
test('normalizeOpenDevice rejects the apple selector and unknown platforms', () => {
@@ -51,7 +51,7 @@ function makeProviderRuntimeWorld() {
expiresAt: 60_001,
};
const device: DeviceInfo = {
platform: 'ios',
platform: 'apple',
kind: 'simulator',
id: 'provider:ios:lease-a',
name: 'Provider iOS',
+10 -6
View File
@@ -9,27 +9,30 @@ export const ANDROID_EMULATOR: DeviceInfo = {
};
export const IOS_SIMULATOR: DeviceInfo = {
platform: 'ios',
platform: 'apple',
id: 'sim-1',
name: 'iPhone 17 Pro',
kind: 'simulator',
appleOs: 'ios',
booted: true,
};
export const IOS_DEVICE: DeviceInfo = {
platform: 'ios',
platform: 'apple',
id: 'ios-device-1',
name: 'iPhone',
kind: 'device',
appleOs: 'ios',
booted: true,
};
export const MACOS_DEVICE: DeviceInfo = {
platform: 'macos',
platform: 'apple',
id: 'host-macos-local',
name: 'Mac',
kind: 'device',
target: 'desktop',
appleOs: 'macos',
booted: true,
};
@@ -59,11 +62,12 @@ export const ANDROID_TV_DEVICE: DeviceInfo = {
};
export const TVOS_SIMULATOR: DeviceInfo = {
platform: 'ios',
platform: 'apple',
id: 'tv-sim-1',
name: 'Apple TV',
kind: 'simulator',
target: 'tv',
appleOs: 'tvos',
};
// iPadOS / visionOS carry the explicit `appleOs` discriminant discovery stores, so
@@ -72,7 +76,7 @@ export const TVOS_SIMULATOR: DeviceInfo = {
// the touch iOS engine (`platform: 'ios'`, mobile target) and are capability-identical
// to iOS today.
export const IPADOS_SIMULATOR: DeviceInfo = {
platform: 'ios',
platform: 'apple',
id: 'ipad-sim-1',
name: 'iPad Pro 11-inch',
kind: 'simulator',
@@ -81,7 +85,7 @@ export const IPADOS_SIMULATOR: DeviceInfo = {
};
export const VISIONOS_SIMULATOR: DeviceInfo = {
platform: 'ios',
platform: 'apple',
id: 'vision-sim-1',
name: 'Apple Vision Pro',
kind: 'simulator',
+5 -2
View File
@@ -2,7 +2,7 @@ import type { AlertAction, AlertInfo } from './alert-contract.ts';
import type { AppsFilter } from './contracts/app-inventory.ts';
import type { Point, SnapshotNode, SnapshotOptions, SnapshotState } from './kernel/snapshot.ts';
import type { NetworkIncludeMode } from './kernel/contracts.ts';
import type { DeviceTarget, Platform, PlatformSelector } from './kernel/device.ts';
import type { DeviceTarget, Platform, PlatformSelector, PublicPlatform } from './kernel/device.ts';
import type { BackMode } from './core/back-mode.ts';
import type { RepeatedInput } from './commands/command-input.ts';
import type { ClickButton } from './core/click-button.ts';
@@ -18,7 +18,10 @@ import type {
} from './snapshot-capture-annotations.ts';
import type { ScreenshotResultData } from './utils/screenshot-result.ts';
export type AgentDeviceBackendPlatform = Platform;
// The backend's public leaf platform (approach b): backends distinguish iOS from
// macOS (e.g. snapshot backend routing, the macOS surface guard), so this carries the
// leaf string, not the internal collapsed `apple`.
export type AgentDeviceBackendPlatform = PublicPlatform;
export const BACKEND_CAPABILITY_NAMES = [
'android.shell',
+11 -5
View File
@@ -2,7 +2,13 @@ import { resolveDaemonPaths } from '../../daemon/config.ts';
import { stopReactDevtoolsCompanion } from '../../client/client-react-devtools-companion.ts';
import { stopMetroTunnel } from '../../metro/metro.ts';
import { resolveRemoteConfigProfile } from '../../remote/remote-config.ts';
import { resolveDevice, type DeviceInfo } from '../../kernel/device.ts';
import {
deviceFieldsFromPublicPlatform,
isIosFamily,
publicPlatformString,
resolveDevice,
type DeviceInfo,
} from '../../kernel/device.ts';
import { shouldAgentCdpUseRemoteBridgeUrl } from './agent-cdp.ts';
import type { MetroBridgeScope } from '../../client/client-companion-tunnel-contract.ts';
import {
@@ -718,7 +724,7 @@ async function resolveProxyLeaseState(options: {
function applyResolvedDeviceSelector(flags: CliFlags, device: DeviceInfo): void {
flags.platform = device.platform;
flags.target = device.target ?? flags.target;
if (device.platform === 'ios') {
if (isIosFamily(device)) {
flags.udid = device.id;
return;
}
@@ -742,7 +748,7 @@ async function resolveSelectedDevice(
});
return await resolveDevice(
devices.map((device) => ({
platform: device.platform,
...deviceFieldsFromPublicPlatform(device.platform),
id: device.id,
name: device.name,
kind: device.kind,
@@ -760,11 +766,11 @@ async function resolveSelectedDevice(
}
function buildProxyDeviceKey(device: DeviceInfo): string {
return `${device.platform}:${device.target ?? 'mobile'}:${device.id}`;
return `${publicPlatformString(device)}:${device.target ?? 'mobile'}:${device.id}`;
}
function leaseBackendForDevice(device: DeviceInfo): LeaseBackend | undefined {
if (device.platform === 'ios') return 'ios-instance';
if (isIosFamily(device)) return 'ios-instance';
if (device.platform === 'android') return 'android-instance';
return undefined;
}
+2 -2
View File
@@ -4,7 +4,7 @@ import type { DaemonRequest, SessionRuntimeHints } from '../daemon/types.ts';
import { AppError, type NormalizedError } from '../kernel/errors.ts';
import type { SnapshotNode } from '../kernel/snapshot.ts';
import { buildAppIdentifiers, buildDeviceIdentifiers } from './client-shared.ts';
import { isPlatform } from '../kernel/device.ts';
import { isPublicPlatform } from '../kernel/device.ts';
import {
leaseScopeFromOptions,
leaseScopeToCommandFlags,
@@ -184,7 +184,7 @@ export function normalizeOpenDevice(
const platform = value.platform;
const id = readOptionalString(value, 'id');
const name = readOptionalString(value, 'device');
if (!isPlatform(platform) || !id || !name) {
if (!isPublicPlatform(platform) || !id || !name) {
return undefined;
}
const target = readDeviceTarget(value, 'target');
+2 -2
View File
@@ -14,7 +14,7 @@ import {
publicSnapshotCaptureAnnotations,
type SnapshotCaptureAnnotations,
} from '../snapshot-capture-annotations.ts';
import type { Platform } from '../kernel/device.ts';
import type { PublicPlatform } from '../kernel/device.ts';
import { successText, withSuccessText } from '../utils/success-text.ts';
export function buildAppIdentifiers(params: {
@@ -33,7 +33,7 @@ export function buildAppIdentifiers(params: {
}
export function buildDeviceIdentifiers(
platform: Platform,
platform: PublicPlatform,
id: string,
name: string,
): AgentDeviceIdentifiers {
+9 -4
View File
@@ -12,7 +12,12 @@ import type {
SessionIsolationMode,
SessionRuntimeHints,
} from '../kernel/contracts.ts';
import type { DeviceKind, DeviceTarget, Platform, PlatformSelector } from '../kernel/device.ts';
import type {
DeviceKind,
DeviceTarget,
PublicPlatform,
PlatformSelector,
} from '../kernel/device.ts';
import type { BackMode } from '../core/back-mode.ts';
import type { ClickButton } from '../core/click-button.ts';
import type { RecordingExportQuality } from '../core/recording-export-quality.ts';
@@ -151,7 +156,7 @@ export type AgentDeviceSelectionOptions = {
};
export type AgentDeviceDevice = {
platform: Platform;
platform: PublicPlatform;
target: DeviceTarget;
kind: DeviceKind;
id: string;
@@ -167,7 +172,7 @@ export type AgentDeviceDevice = {
};
export type AgentDeviceSessionDevice = {
platform: Platform;
platform: PublicPlatform;
target: DeviceTarget;
id: string;
name: string;
@@ -225,7 +230,7 @@ export type AppDeployOptions = AgentDeviceRequestOverrides &
export type AppDeployResult = {
app: string;
appPath: string;
platform: Platform;
platform: PublicPlatform;
appId?: string;
bundleId?: string;
package?: string;
+8 -4
View File
@@ -13,7 +13,11 @@ import type {
ProviderDeviceInstallResult,
ProviderDeviceRuntime,
} from '../provider-device-runtime.ts';
import type { DeviceInfo, Platform } from '../kernel/device.ts';
import {
deviceFieldsFromPublicPlatform,
publicPlatformString,
type DeviceInfo,
} from '../kernel/device.ts';
import { AppError } from '../kernel/errors.ts';
import { unavailableCloudArtifactsResult } from './artifact-results.ts';
import {
@@ -31,7 +35,7 @@ import {
} from './webdriver-client.ts';
import { createWebDriverInteractor } from './webdriver-interactor.ts';
export type CloudWebDriverPlatform = Extract<Platform, 'android' | 'ios'>;
export type CloudWebDriverPlatform = 'android' | 'ios';
export type CloudWebDriverUploadResult = ProviderDeviceInstallResult & {
appReference: string;
@@ -337,7 +341,7 @@ class CloudWebDriverRuntime implements ProviderDeviceRuntime {
private deviceForLease(lease: DeviceLease, prepared: CloudWebDriverPreparedSession): DeviceInfo {
return {
platform: prepared.platform,
...deviceFieldsFromPublicPlatform(prepared.platform),
id:
prepared.deviceId ??
this.options.deviceId?.(lease) ??
@@ -364,7 +368,7 @@ class CloudWebDriverRuntime implements ProviderDeviceRuntime {
throw new AppError(
'UNSUPPORTED_OPERATION',
unsupportedCapabilityMessage(session.capabilities, 'install'),
{ provider: this.provider, deviceId: device.id, platform: device.platform },
{ provider: this.provider, deviceId: device.id, platform: publicPlatformString(device) },
);
}
const uploadApp = session.prepared.uploadApp ?? this.options.uploadApp;
@@ -45,7 +45,7 @@ test('common command input accepts web platform selector', () => {
const platformSchema = snapshotMetadata.inputSchema.properties?.platform;
const input = snapshotMetadata.readInput({ platform: 'web' }) as { platform?: unknown };
assert.deepEqual(platformSchema?.enum, ['ios', 'macos', 'android', 'linux', 'web', 'apple']);
assert.deepEqual(platformSchema?.enum, ['apple', 'android', 'linux', 'web', 'ios', 'macos']);
assert.equal(input.platform, 'web');
});
+9 -10
View File
@@ -1,4 +1,3 @@
import type { Platform } from '../../kernel/device.ts';
import type { ElementSelectorKey } from '../../core/interactor-types.ts';
import type { Rect, SnapshotNode, SnapshotState } from '../../kernel/snapshot.ts';
import { parseSelectorChain } from '../../daemon/selectors.ts';
@@ -82,7 +81,7 @@ export function resolveMaestroNodeFromSnapshot(
snapshot: SnapshotState,
selector: string,
options: MaestroTapOnOptions,
platform: Platform,
platform: 'ios' | 'android',
frame: TouchReferenceFrame | undefined,
resolutionOptions: MaestroMatchResolutionOptions = {},
): { ok: true; node: SnapshotNode; rect: Rect } | { ok: false; message: string } {
@@ -132,7 +131,7 @@ export function resolveMaestroNodeFromSnapshot(
export function resolveMaestroFuzzyTextNodeFromSnapshot(
snapshot: SnapshotState,
query: string,
platform: Platform,
platform: 'ios' | 'android',
frame: TouchReferenceFrame | undefined,
resolutionOptions: MaestroMatchResolutionOptions = {},
): { ok: true; node: SnapshotNode; rect: Rect } | { ok: false; message: string } {
@@ -161,7 +160,7 @@ export function resolveMaestroFuzzyTextNodeFromSnapshot(
export function resolveVisibleMaestroNodeFromSnapshot(
snapshot: SnapshotState,
selector: string,
platform: Platform,
platform: 'ios' | 'android',
frame: TouchReferenceFrame | undefined,
): { ok: true; node: SnapshotNode; rect: Rect; matches: number } | { ok: false; message: string } {
const matches = findMaestroSelectorMatches(snapshot, selector, platform, {
@@ -201,7 +200,7 @@ export function resolveVisibleMaestroNodeFromSnapshot(
function filterVisibleMaestroMatches(params: {
nodes: SnapshotState['nodes'];
matches: SnapshotNode[];
platform: Platform;
platform: 'ios' | 'android';
}): { matches: SnapshotNode[]; blockedByReactNativeOverlay: boolean } {
const visibleMatches = params.matches.filter(
(node) =>
@@ -226,7 +225,7 @@ function filterVisibleMaestroMatches(params: {
function filterReactNativeOverlayBlockedMatches(
nodes: SnapshotState['nodes'],
matches: SnapshotNode[],
platform: Platform,
platform: 'ios' | 'android',
): ReactNativeOverlayFilterResult {
const overlay = detectReactNativeOverlay(nodes);
if (!overlay.detected) {
@@ -259,7 +258,7 @@ function filterReactNativeOverlayBlockedMatches(
};
}
export function readMaestroSelectorPlatform(flags: DaemonRequest['flags']): Platform {
export function readMaestroSelectorPlatform(flags: DaemonRequest['flags']): 'ios' | 'android' {
return flags?.platform === 'android' ? 'android' : 'ios';
}
@@ -280,7 +279,7 @@ export function extractMaestroVisibleTextQuery(selectorExpression: string): stri
function findMaestroSelectorMatches(
snapshot: SnapshotState,
selectorExpression: string,
platform: Platform,
platform: 'ios' | 'android',
options: MaestroSelectorMatchOptions = {},
): SnapshotNode[] {
const chain = parseSelectorChain(selectorExpression);
@@ -315,7 +314,7 @@ function findMaestroFuzzyTextMatches(snapshot: SnapshotState, query: string): Sn
function matchesMaestroSelector(
node: SnapshotNode,
selector: Selector,
platform: Platform,
platform: 'ios' | 'android',
options: MaestroSelectorMatchOptions,
): boolean {
if (matchesSelector(node, selector, platform)) return true;
@@ -325,7 +324,7 @@ function matchesMaestroSelector(
function matchesMaestroTerm(
node: SnapshotNode,
term: SelectorTerm,
platform: Platform,
platform: 'ios' | 'android',
options: MaestroSelectorMatchOptions,
): boolean {
if (typeof term.value !== 'string' || !isMaestroRegexTextKey(term.key)) {
+3 -3
View File
@@ -1,4 +1,4 @@
import type { DeviceKind, DeviceTarget, Platform } from '../kernel/device.ts';
import type { DeviceKind, DeviceTarget, PublicPlatform } from '../kernel/device.ts';
import type { TargetShutdownResult } from '../target-shutdown-contract.ts';
/**
@@ -8,7 +8,7 @@ import type { TargetShutdownResult } from '../target-shutdown-contract.ts';
* spreads nothing, so this shape is intentionally closed.
*/
export type BootCommandResult = {
platform: Platform;
platform: PublicPlatform;
target: DeviceTarget;
/** Human-readable device name (`device.name`). */
device: string;
@@ -26,7 +26,7 @@ export type BootCommandResult = {
* field is the raw {@link TargetShutdownResult} from `shutdownDeviceTarget`.
*/
export type ShutdownCommandResult = {
platform: Platform;
platform: PublicPlatform;
target: DeviceTarget;
/** Human-readable device name (`device.name`). */
device: string;
+7 -6
View File
@@ -5,14 +5,14 @@ import { matchesPlatformSelector, type DeviceInfo } from '../../kernel/device.ts
import { WEB_DESKTOP_DEVICE } from '../../__tests__/test-utils/index.ts';
const iosSimulator: DeviceInfo = {
platform: 'ios',
platform: 'apple',
id: 'sim-1',
name: 'iPhone',
kind: 'simulator',
};
const iosDevice: DeviceInfo = {
platform: 'ios',
platform: 'apple',
id: 'dev-1',
name: 'iPhone',
kind: 'device',
@@ -33,7 +33,8 @@ const androidEmulator: DeviceInfo = {
};
const macOsDevice: DeviceInfo = {
platform: 'macos',
platform: 'apple',
appleOs: 'macos',
id: 'mac-1',
name: 'Mac',
kind: 'device',
@@ -51,7 +52,7 @@ const linuxDevice: DeviceInfo = {
const webDevice = WEB_DESKTOP_DEVICE;
const tvOsSimulator: DeviceInfo = {
platform: 'ios',
platform: 'apple',
id: 'tv-sim-1',
name: 'Apple TV',
kind: 'simulator',
@@ -458,8 +459,8 @@ test('audio probe support is limited to browser and host-rendered audio targets'
});
test('apple selector does not match web platform', () => {
assert.equal(matchesPlatformSelector(webDevice.platform, 'apple'), false);
assert.equal(matchesPlatformSelector(webDevice.platform, 'web'), true);
assert.equal(matchesPlatformSelector(webDevice, 'apple'), false);
assert.equal(matchesPlatformSelector(webDevice, 'web'), true);
});
test('unknown commands default to supported', () => {
@@ -1,6 +1,8 @@
import assert from 'node:assert/strict';
import { test } from 'vitest';
import {
isIosFamily,
isMacOs,
DEVICE_TARGETS,
PLATFORMS,
type DeviceInfo,
@@ -101,27 +103,27 @@ const SAMPLE_DEVICES: DeviceInfo[] = [
// hand so this oracle stays INDEPENDENT of the descriptor it pins (mirrors the
// `selectCapabilityByHandSwitch` copy in platform-descriptor/__tests__/parity.test.ts).
// ---------------------------------------------------------------------------
const isNotMacOs = (device: DeviceInfo): boolean => device.platform !== 'macos';
const isNotMacOs = (device: DeviceInfo): boolean => !isMacOs(device);
const isMacOsOrAppleSimulator = (device: DeviceInfo): boolean =>
device.platform === 'macos' || device.kind === 'simulator';
isMacOs(device) || device.kind === 'simulator';
const isIosMobileSimulator = (device: DeviceInfo): boolean =>
device.platform === 'ios' && device.kind === 'simulator' && device.target !== 'tv';
isIosFamily(device) && device.kind === 'simulator' && device.target !== 'tv';
const supportsSynthesisGesture = (device: DeviceInfo): boolean =>
device.platform === 'android' || isIosMobileSimulator(device);
const supportsAndroidOrIosNonTv = (device: DeviceInfo): boolean =>
device.platform === 'android' || (device.platform === 'ios' && device.target !== 'tv');
device.platform === 'android' || (isIosFamily(device) && device.target !== 'tv');
const supportsHostAudioProbe = (device: DeviceInfo): boolean =>
device.platform === 'web' ||
(process.platform === 'darwin' &&
(device.platform === 'macos' ||
(device.platform === 'ios' && device.kind === 'simulator') ||
(isMacOs(device) ||
(isIosFamily(device) && device.kind === 'simulator') ||
(device.platform === 'android' && device.kind === 'emulator')));
const synthesisGestureUnsupportedHint = (device: DeviceInfo): string | undefined => {
if (device.platform === 'macos')
if (isMacOs(device))
return 'macOS automation has no multi-touch input — this gesture is supported on Android and the iOS simulator only.';
if (device.platform === 'ios' && device.target === 'tv')
if (isIosFamily(device) && device.target === 'tv')
return 'tvOS has no touch input — this gesture is supported on Android and the iOS simulator only.';
if (device.platform === 'ios' && device.kind === 'device')
if (isIosFamily(device) && device.kind === 'device')
return 'Two-finger gesture synthesis is iOS-simulator only — not available on physical iOS devices.';
return undefined;
};
@@ -140,13 +142,13 @@ const SUPPORTS_REF: Record<string, (device: DeviceInfo) => boolean> = {
clipboard: (device) =>
device.platform === 'android' ||
device.platform === 'linux' ||
device.platform === 'macos' ||
isMacOs(device) ||
device.kind === 'simulator',
keyboard: supportsAndroidOrIosNonTv,
rotate: supportsAndroidOrIosNonTv,
alert: (device) => device.platform === 'android' || isMacOsOrAppleSimulator(device),
settings: (device) =>
device.platform === 'android' || device.platform === 'macos' || device.kind === 'simulator',
device.platform === 'android' || isMacOs(device) || device.kind === 'simulator',
audio: supportsHostAudioProbe,
pinch: supportsSynthesisGesture,
'rotate-gesture': supportsSynthesisGesture,
@@ -235,7 +237,7 @@ test('(b.2) the Apple plugin carries exactly the relocated supports/hint closure
// plugin (the family that owns every discriminating device). Pin the RELOCATED maps'
// key sets against the independent verbatim reference so no closure was silently
// dropped or added while moving off the command facet.
const appleCapability = getPlugin('ios').capability;
const appleCapability = getPlugin('apple').capability;
assert.deepEqual(
Object.keys(appleCapability.supportsByDefault ?? {}).sort(),
Object.keys(SUPPORTS_REF).sort(),
@@ -247,14 +249,14 @@ test('(b.2) the Apple plugin carries exactly the relocated supports/hint closure
'unsupportedHintByDefault key set equals the verbatim reference',
);
// ios and macos are the SAME Apple plugin instance, so both leaves read one map.
assert.equal(getPlugin('ios').capability, getPlugin('macos').capability);
assert.equal(getPlugin('apple').capability, getPlugin('apple').capability);
});
test('(b.2) the relocated Apple closures are byte-for-byte the verbatim originals', () => {
// Closure-equivalence: for every command x sample-device, the closure now living on
// the Apple plugin returns an identical boolean / identical hint STRING to the
// independent verbatim copy of the original command-facet closure.
const appleCapability = getPlugin('ios').capability;
const appleCapability = getPlugin('apple').capability;
for (const [command, reference] of Object.entries(SUPPORTS_REF)) {
const relocated = appleCapability.supportsByDefault?.[command];
assert.ok(relocated, `${command} supports closure present on the Apple plugin`);
+1 -1
View File
@@ -47,7 +47,7 @@ beforeEach(() => {
test('dispatch open rejects URL as first argument when second URL is provided', async () => {
const device: DeviceInfo = {
platform: 'ios',
platform: 'apple',
id: 'sim-1',
name: 'iPhone 15',
kind: 'simulator',
+7 -5
View File
@@ -24,7 +24,7 @@ import type { DeviceInfo } from '../../kernel/device.ts';
import { AppError } from '../../kernel/errors.ts';
const physical: DeviceInfo = {
platform: 'ios',
platform: 'apple',
id: 'phys-1',
name: 'My iPhone',
kind: 'device',
@@ -33,7 +33,7 @@ const physical: DeviceInfo = {
};
const simulator: DeviceInfo = {
platform: 'ios',
platform: 'apple',
id: 'sim-1',
name: 'iPhone 16',
kind: 'simulator',
@@ -42,7 +42,7 @@ const simulator: DeviceInfo = {
};
const bootedSimulator: DeviceInfo = {
platform: 'ios',
platform: 'apple',
id: 'sim-2',
name: 'iPhone 15',
kind: 'simulator',
@@ -175,7 +175,8 @@ test('resolveTargetDevice resolves web through generic inventory without Apple f
test('resolveTargetDevice fast-paths explicit macOS without Apple mobile discovery', async () => {
const result = await resolveTargetDevice({ platform: 'macos' });
assert.equal(result.platform, 'macos');
assert.equal(result.platform, 'apple');
assert.equal(result.appleOs, 'macos');
assert.equal(result.id, 'host-macos-local');
assert.equal(mockListAppleDevices.mock.calls.length, 0);
});
@@ -187,7 +188,8 @@ test('resolveTargetDevice fast-paths Apple desktop target without simulator-set
iosSimulatorDeviceSet: '/tmp/simulators',
});
assert.equal(result.platform, 'macos');
assert.equal(result.platform, 'apple');
assert.equal(result.appleOs, 'macos');
assert.equal(result.target, 'desktop');
assert.equal(mockListAppleDevices.mock.calls.length, 0);
});
+1 -1
View File
@@ -9,7 +9,7 @@ import {
import { AppError } from '../../kernel/errors.ts';
import type { DeviceInfo } from '../../kernel/device.ts';
const iosDevice: DeviceInfo = { platform: 'ios', id: 'test', name: 'iPhone', kind: 'simulator' };
const iosDevice: DeviceInfo = { platform: 'apple', id: 'test', name: 'iPhone', kind: 'simulator' };
const androidDevice: DeviceInfo = {
platform: 'android',
id: 'emu',
+12 -10
View File
@@ -1,6 +1,8 @@
import type { DeviceInfo } from '../kernel/device.ts';
import { isIosFamily, isMacOs, publicPlatformString, type DeviceInfo } from '../kernel/device.ts';
import { AppError } from '../kernel/errors.ts';
type AppEventDevice = Pick<DeviceInfo, 'platform' | 'appleOs'>;
const APP_EVENT_NAME_PATTERN = /^[A-Za-z0-9_.:-]{1,64}$/;
const MAX_APP_EVENT_PAYLOAD_BYTES = 8 * 1024;
const MAX_APP_EVENT_URL_LENGTH = 4 * 1024;
@@ -32,11 +34,12 @@ export function parseTriggerAppEventArgs(positionals: string[]): {
}
export function resolveAppEventUrl(
platform: DeviceInfo['platform'],
device: AppEventDevice,
eventName: string,
payload?: AppEventPayload,
): string {
const template = readAppEventUrlTemplate(platform);
const platform = publicPlatformString(device);
const template = readAppEventUrlTemplate(device);
if (!template) {
throw new AppError(
'UNSUPPORTED_OPERATION',
@@ -89,13 +92,12 @@ function parseTriggerEventPayload(
}
}
function readAppEventUrlTemplate(platform: DeviceInfo['platform']): string | undefined {
const platformSpecific =
platform === 'ios'
? process.env.AGENT_DEVICE_IOS_APP_EVENT_URL_TEMPLATE
: platform === 'macos'
? process.env.AGENT_DEVICE_MACOS_APP_EVENT_URL_TEMPLATE
: process.env.AGENT_DEVICE_ANDROID_APP_EVENT_URL_TEMPLATE;
function readAppEventUrlTemplate(device: AppEventDevice): string | undefined {
const platformSpecific = isIosFamily(device)
? process.env.AGENT_DEVICE_IOS_APP_EVENT_URL_TEMPLATE
: isMacOs(device)
? process.env.AGENT_DEVICE_MACOS_APP_EVENT_URL_TEMPLATE
: process.env.AGENT_DEVICE_ANDROID_APP_EVENT_URL_TEMPLATE;
const candidate = platformSpecific ?? process.env.AGENT_DEVICE_APP_EVENT_URL_TEMPLATE;
const trimmed = candidate?.trim();
return trimmed ? trimmed : undefined;
+16 -10
View File
@@ -1,5 +1,11 @@
import { AppError } from '../kernel/errors.ts';
import { isTvOsDevice, type DeviceInfo } from '../kernel/device.ts';
import {
isIosFamily,
isMacOs,
isTvOsDevice,
publicPlatformString,
type DeviceInfo,
} from '../kernel/device.ts';
import { successText, withSuccessText } from '../utils/success-text.ts';
import { findMistargetedTypeRefToken } from '../utils/type-target-warning.ts';
import {
@@ -146,13 +152,13 @@ export async function handlePressCommand(
positionals: string[],
context: DispatchContext | undefined,
): Promise<Record<string, unknown>> {
if (context?.directElementSelector && device.platform === 'ios') {
if (context?.directElementSelector && isIosFamily(device)) {
return await handleDirectElementSelectorPress(interactor, context.directElementSelector);
}
const { x, y } = readPoint(positionals, 'press requires x y');
if (device.platform === 'macos' && context?.surface && context.surface !== 'app') {
if (isMacOs(device) && context?.surface && context.surface !== 'app') {
return await handleMacOsSurfacePress(x, y, context);
}
@@ -267,7 +273,7 @@ function assertAlternateClickSupported(
): void {
const validationError = getClickButtonValidationError({
commandLabel: 'click',
platform: device.platform,
platform: publicPlatformString(device),
button,
count: context?.count,
intervalMs: context?.intervalMs,
@@ -419,7 +425,7 @@ function buildPressSequenceSteps(
// Mirror the individual `tap` command: on touch-input iOS (not the tvOS leaf), tap steps
// use synthesized HID taps (synthesizedTapAt) rather than the drag-based XCUICoordinate
// tapAt, matching iosTapCommand.
const synthesized = kind === 'tap' && device.platform === 'ios' && !isTvOsDevice(device);
const synthesized = kind === 'tap' && isIosFamily(device) && !isTvOsDevice(device);
return Array.from({ length: series.count }, (_, index) => {
const [dx, dy] = computeDeterministicJitter(index, series.jitterPx);
const isLast = index === series.count - 1;
@@ -450,7 +456,7 @@ function buildSwipeSequenceSteps(params: {
effectiveDurationMs: number;
}): RunnerSequenceStep[] {
const { device, x1, y1, x2, y2, count, pauseMs, pattern, effectiveDurationMs } = params;
const synthesized = device.platform === 'ios' && !isTvOsDevice(device);
const synthesized = isIosFamily(device) && !isTvOsDevice(device);
return Array.from({ length: count }, (_, index) => {
const reverse = pattern === 'ping-pong' && index % 2 === 1;
const isLast = index === count - 1;
@@ -869,7 +875,7 @@ export async function handlePinchCommand(
if (device.target === 'tv') {
throw new AppError('UNSUPPORTED_OPERATION', 'gesture pinch is not supported on tvOS');
}
if (device.platform === 'macos' && context?.surface && context.surface !== 'app') {
if (isMacOs(device) && context?.surface && context.surface !== 'app') {
throw new AppError(
'UNSUPPORTED_OPERATION',
'gesture pinch is only supported in macOS app sessions. Re-open the target app without --surface desktop|menubar|frontmost-app first.',
@@ -894,7 +900,7 @@ export async function handleRotateGestureCommand(
if (device.target === 'tv') {
throw new AppError('UNSUPPORTED_OPERATION', 'gesture rotate is not supported on tvOS');
}
if (device.platform === 'macos') {
if (isMacOs(device)) {
throw new AppError(
'UNSUPPORTED_OPERATION',
'gesture rotate is not supported on macOS; XCTest rotation gestures are available only for iOS app sessions.',
@@ -922,7 +928,7 @@ export async function handleTransformGestureCommand(
if (device.target === 'tv') {
throw new AppError('UNSUPPORTED_OPERATION', 'gesture transform is not supported on tvOS');
}
const supportedIosSimulator = device.platform === 'ios' && device.kind === 'simulator';
const supportedIosSimulator = isIosFamily(device) && device.kind === 'simulator';
if (device.platform !== 'android' && !supportedIosSimulator) {
throw new AppError(
'UNSUPPORTED_OPERATION',
@@ -1050,7 +1056,7 @@ export async function handleReadCommand(
const text = await readLinuxTextAtPoint(x, y, context?.surface);
return { action: 'read', text };
}
if (device.platform === 'macos' && context?.surface && context.surface !== 'app') {
if (isMacOs(device) && context?.surface && context.surface !== 'app') {
const { runMacOsReadTextAction } = await import('../platforms/apple/os/macos/helper.ts');
const result = await runMacOsReadTextAction(x, y, {
bundleId: context.appBundleId,
+5 -5
View File
@@ -1,7 +1,7 @@
import { promises as fs } from 'node:fs';
import pathModule from 'node:path';
import { AppError } from '../kernel/errors.ts';
import type { DeviceInfo } from '../kernel/device.ts';
import { isIosFamily, type DeviceInfo } from '../kernel/device.ts';
import { getInteractor } from './interactors.ts';
import type { Interactor, RunnerContext } from './interactor-types.ts';
import { isDeepLinkTarget } from './open-target.ts';
@@ -260,7 +260,7 @@ async function handleOpenCommand(
await interactor.openDevice();
return { app: null, ...successText('Opened device') };
}
if (launchConsole && (device.platform !== 'ios' || device.kind !== 'simulator')) {
if (launchConsole && (!isIosFamily(device) || device.kind !== 'simulator')) {
throw new AppError('UNSUPPORTED_OPERATION', LAUNCH_CONSOLE_IOS_SIMULATOR_ONLY_MESSAGE);
}
if (device.platform === 'linux' && launchArgs && launchArgs.length > 0) {
@@ -315,7 +315,7 @@ async function handleTriggerAppEventCommand(
context: DispatchContext | undefined,
): Promise<Record<string, unknown>> {
const { eventName, payload } = parseTriggerAppEventArgs(positionals);
const eventUrl = resolveAppEventUrl(device.platform, eventName, payload);
const eventUrl = resolveAppEventUrl(device, eventName, payload);
await interactor.open(eventUrl, { appBundleId: context?.appBundleId });
return {
event: eventName,
@@ -408,7 +408,7 @@ async function handleKeyboardCommand(
if (device.platform === 'android') {
return await handleAndroidKeyboardCommand(device, action);
}
if (device.platform === 'ios') {
if (isIosFamily(device)) {
return await handleIosKeyboardCommand(device, action, context, runnerCtx);
}
throw new AppError('UNSUPPORTED_OPERATION', 'keyboard is supported only on Android and iOS');
@@ -590,7 +590,7 @@ async function handlePushCommand(
throw new AppError('INVALID_ARGS', 'push requires <bundle|package> <payload.json|inline-json>');
}
const payload = await readNotificationPayload(payloadArg);
if (device.platform === 'ios') {
if (isIosFamily(device)) {
const { pushIosNotification } = await import('../platforms/apple/core/apps.ts');
await pushIosNotification(device, target, payload);
return {
+2 -2
View File
@@ -1,4 +1,4 @@
import type { DeviceInfo } from '../kernel/device.ts';
import { publicPlatformString, type DeviceInfo } from '../kernel/device.ts';
import { AppError } from '../kernel/errors.ts';
import { getProviderDeviceInteractor, isActiveProviderDevice } from '../provider-device-runtime.ts';
import type { Interactor, RunnerContext } from './interactor-types.ts';
@@ -19,7 +19,7 @@ export async function getInteractor(
throw new AppError(
'UNSUPPORTED_OPERATION',
'Provider device runtime does not have an active interactor for this device.',
{ deviceId: device.id, platform: device.platform },
{ deviceId: device.id, platform: publicPlatformString(device) },
);
}
@@ -17,8 +17,7 @@ function selectCapabilityByHandSwitch(
platform: Platform,
): CommandCapability[CapabilityBucket] {
switch (platform) {
case 'ios':
case 'macos':
case 'apple':
return capability.apple;
case 'android':
return capability.android;
+4 -5
View File
@@ -8,17 +8,16 @@ import type { PlatformDescriptor } from './types.ts';
* Each row is copied VERBATIM from the facts the hand-authored control flow
* implies today:
* - `capabilityBucket` — the bucket `selectCapabilityForPlatform` returned for
* the platform (`ios`/`macos`→`apple`, `android`→`android`,
* the platform (`apple`→`apple`, `android`→`android`,
* `linux`→`linux`, `web`→`web`).
* - `isApple` — whether `isApplePlatform` is true for the leaf platform
* (`ios`/`macos` only).
* - `isApple` — whether `isApplePlatform` is true for the platform
* (`apple` only).
*
* `as const satisfies` pins each literal while checking the shape, and the row
* order matches the `PLATFORMS` tuple so the parity test can prove totality.
*/
export const platformDescriptors = [
{ platform: 'ios', capabilityBucket: 'apple', isApple: true },
{ platform: 'macos', capabilityBucket: 'apple', isApple: true },
{ platform: 'apple', capabilityBucket: 'apple', isApple: true },
{ platform: 'android', capabilityBucket: 'android', isApple: false },
{ platform: 'linux', capabilityBucket: 'linux', isApple: false },
{ platform: 'web', capabilityBucket: 'web', isApple: false },
+8 -2
View File
@@ -1,4 +1,10 @@
import type { DeviceInfo, DeviceTarget, PlatformSelector } from '../kernel/device.ts';
import {
isIosFamily,
isMacOs,
type DeviceInfo,
type DeviceTarget,
type PlatformSelector,
} from '../kernel/device.ts';
export const LOCAL_DEVICE_INVENTORY_PLATFORM_SELECTORS = ['android', 'apple', 'linux'] as const;
@@ -98,7 +104,7 @@ function emptyDeviceInventoryGroupCounts(): DeviceInventoryGroupCounts {
}
function deviceInventoryGroupForDevice(device: DeviceInfo): DeviceInventoryGroup {
if (device.platform === 'ios' || device.platform === 'macos') return 'apple';
if (isIosFamily(device) || isMacOs(device)) return 'apple';
return device.platform;
}
@@ -2,6 +2,8 @@ import assert from 'node:assert/strict';
import { test } from 'vitest';
import { isAudioProbeSupportedDevice } from '../../../kernel/audio-probe-support.ts';
import {
isIosFamily,
isMacOs,
DEVICE_TARGETS,
PLATFORMS,
type AppleOS,
@@ -41,21 +43,21 @@ registerBuiltinPlatformPlugins();
// table read), kept BYTE-FOR-BYTE by hand so this oracle stays INDEPENDENT of the
// table it pins (mirrors the copy in capability-plugin-routing-parity.test.ts).
// ---------------------------------------------------------------------------
const isNotMacOs = (device: DeviceInfo): boolean => device.platform !== 'macos';
const isNotMacOs = (device: DeviceInfo): boolean => !isMacOs(device);
const isMacOsOrAppleSimulator = (device: DeviceInfo): boolean =>
device.platform === 'macos' || device.kind === 'simulator';
isMacOs(device) || device.kind === 'simulator';
const isIosMobileSimulator = (device: DeviceInfo): boolean =>
device.platform === 'ios' && device.kind === 'simulator' && device.target !== 'tv';
isIosFamily(device) && device.kind === 'simulator' && device.target !== 'tv';
const supportsSynthesisGesture = (device: DeviceInfo): boolean =>
device.platform === 'android' || isIosMobileSimulator(device);
const supportsAndroidOrIosNonTv = (device: DeviceInfo): boolean =>
device.platform === 'android' || (device.platform === 'ios' && device.target !== 'tv');
device.platform === 'android' || (isIosFamily(device) && device.target !== 'tv');
const synthesisGestureUnsupportedHint = (device: DeviceInfo): string | undefined => {
if (device.platform === 'macos')
if (isMacOs(device))
return 'macOS automation has no multi-touch input — this gesture is supported on Android and the iOS simulator only.';
if (device.platform === 'ios' && device.target === 'tv')
if (isIosFamily(device) && device.target === 'tv')
return 'tvOS has no touch input — this gesture is supported on Android and the iOS simulator only.';
if (device.platform === 'ios' && device.kind === 'device')
if (isIosFamily(device) && device.kind === 'device')
return 'Two-finger gesture synthesis is iOS-simulator only — not available on physical iOS devices.';
return undefined;
};
@@ -71,13 +73,13 @@ const SUPPORTS_REF: Record<string, (device: DeviceInfo) => boolean> = {
clipboard: (device) =>
device.platform === 'android' ||
device.platform === 'linux' ||
device.platform === 'macos' ||
isMacOs(device) ||
device.kind === 'simulator',
keyboard: supportsAndroidOrIosNonTv,
rotate: supportsAndroidOrIosNonTv,
alert: (device) => device.platform === 'android' || isMacOsOrAppleSimulator(device),
settings: (device) =>
device.platform === 'android' || device.platform === 'macos' || device.kind === 'simulator',
device.platform === 'android' || isMacOs(device) || device.kind === 'simulator',
// `audio` is NOT part of the AppleOS-table relocation — it stays the standalone
// `isAudioProbeSupportedDevice` predicate. Included here only so the key-set
// assertion stays strict (catches a dropped command) and confirms the rebase
@@ -169,7 +171,7 @@ test('resolveDeviceAppleOs prefers the stored discriminant, else infers from tar
});
test('table-driven Apple supports() closures are byte-for-byte the verbatim originals', () => {
const appleSupports = getPlugin('ios').capability.supportsByDefault;
const appleSupports = getPlugin('apple').capability.supportsByDefault;
assert.ok(appleSupports, 'the Apple plugin carries supportsByDefault');
// Every command that had an original predicate must still carry one, keyed the same.
assert.deepEqual(Object.keys(appleSupports).sort(), Object.keys(SUPPORTS_REF).sort());
@@ -187,7 +189,7 @@ test('table-driven Apple supports() closures are byte-for-byte the verbatim orig
});
test('table-driven Apple unsupportedHint() closures are byte-for-byte the verbatim originals', () => {
const appleHints = getPlugin('ios').capability.unsupportedHintByDefault;
const appleHints = getPlugin('apple').capability.unsupportedHintByDefault;
assert.ok(appleHints, 'the Apple plugin carries unsupportedHintByDefault');
assert.deepEqual(Object.keys(appleHints).sort(), Object.keys(HINT_REF).sort());
for (const [command, reference] of Object.entries(HINT_REF)) {
@@ -18,11 +18,7 @@ registerBuiltinPlatformPlugins();
// registry's covered set is proven byte-for-byte equal to THIS reference list,
// so the assertion stays meaningful even if `parsePlatform` is later derived.
function parsePlatformByHand(value: unknown): Platform | undefined {
return value === 'ios' ||
value === 'macos' ||
value === 'android' ||
value === 'linux' ||
value === 'web'
return value === 'apple' || value === 'android' || value === 'linux' || value === 'web'
? value
: undefined;
}
@@ -73,9 +69,9 @@ test('every plugin capability bucket matches the platform-descriptor registry',
test('a family plugin resolves to the SAME instance for every leaf it owns', () => {
// Apple owns both ios + macos (folds in the eventual macOS unwind).
assert.equal(getPlugin('ios'), getPlugin('macos'));
assert.equal(getPlugin('ios').id, 'apple');
assert.equal(getPlugin('ios').familySelector, 'apple');
assert.equal(getPlugin('apple'), getPlugin('apple'));
assert.equal(getPlugin('apple').id, 'apple');
assert.equal(getPlugin('apple').familySelector, 'apple');
// Single-platform plugins are distinct objects.
assert.notEqual(getPlugin('android'), getPlugin('linux'));
});
@@ -124,7 +124,9 @@ export function resolveDeviceAppleOs(
// Real discovery sets `appleOs`; legacy/synthetic records without it fall back to
// the target-based inference the capability predicates used before this table.
if (device.appleOs) return device.appleOs;
if (device.platform === 'macos') return 'macos';
// Back-compat: a legacy persisted record may still carry the pre-collapse `macos`
// leaf platform string (the type no longer allows it, hence the cast).
if ((device.platform as string) === 'macos') return 'macos';
if (isTvOsDevice(device)) return 'tvos';
// iOS / iPadOS / visionOS are indistinguishable without discovery descriptors and
// are capability-identical, so an unlabeled mobile Apple record collapses to `ios`.
+2 -2
View File
@@ -96,8 +96,8 @@ export type PlatformPlugin = {
};
// The single registry instance: leaf platform -> owning plugin. A family plugin
// is registered once per leaf platform it owns, so `getPlugin('ios')` and
// `getPlugin('macos')` resolve to the SAME Apple plugin object.
// is registered once per leaf platform it owns, so `getPlugin('apple')` and
// `getPlugin('apple')` resolve to the SAME Apple plugin object.
const registry = new Map<Platform, PlatformPlugin>();
/**
@@ -1,6 +1,8 @@
import assert from 'node:assert/strict';
import { test } from 'vitest';
import {
isIosFamily,
isMacOs,
DEVICE_TARGETS,
PLATFORMS,
type DeviceInfo,
@@ -33,8 +35,8 @@ registerBuiltinPlatformPlugins();
// --- INDEPENDENT verbatim copy of the former `resolveLogBackend` hand branch ---
function resolveLogBackendByHand(device: DeviceInfo): LogBackend {
if (device.platform === 'macos') return 'macos';
if (device.platform === 'ios') {
if (isMacOs(device)) return 'macos';
if (isIosFamily(device)) {
return device.kind === 'device' ? 'ios-device' : 'ios-simulator';
}
return 'android';
@@ -89,8 +91,8 @@ test('resolveLogBackend routed through the plugin is byte-identical to the forme
test('only families with an app-log backend carry the appLog facet', () => {
// Apple owns ios + macos (SAME plugin instance); Android carries its own.
assert.equal(getPlugin('ios'), getPlugin('macos'));
assert.ok(getPlugin('ios').appLog, 'apple plugin exposes appLog');
assert.equal(getPlugin('apple'), getPlugin('apple'));
assert.ok(getPlugin('apple').appLog, 'apple plugin exposes appLog');
assert.ok(getPlugin('android').appLog, 'android plugin exposes appLog');
// linux/web historically fell through to the `'android'` default; they get NO
// facet, and the daemon lookup preserves that fallthrough (asserted below).
@@ -100,7 +102,7 @@ test('only families with an app-log backend carry the appLog facet', () => {
test('each populated appLog facet resolves the backend its family owns', () => {
for (const device of SAMPLE_DEVICES.filter(
(d) => d.platform === 'ios' || d.platform === 'macos' || d.platform === 'android',
(d) => isIosFamily(d) || isMacOs(d) || d.platform === 'android',
)) {
assert.equal(
getPlugin(device.platform).appLog?.resolveBackend(device),
@@ -1,6 +1,8 @@
import assert from 'node:assert/strict';
import { test } from 'vitest';
import {
isIosFamily,
isMacOs,
DEVICE_TARGETS,
PLATFORMS,
type DeviceInfo,
@@ -35,7 +37,7 @@ registerBuiltinPlatformPlugins();
// --- INDEPENDENT verbatim copy of the former `supportsPlatformPerfMetrics` branch ---
function supportsPlatformPerfMetricsByHand(device: DeviceInfo): boolean {
return device.platform === 'android' || device.platform === 'ios' || device.platform === 'macos';
return device.platform === 'android' || isIosFamily(device) || isMacOs(device);
}
// --- the exhaustive synthetic device matrix (every platform x kind x target) ---
@@ -87,8 +89,8 @@ test('perf.supportsMetrics facet is byte-identical to the former hand predicate'
test('only families with perf metrics carry the perf facet', () => {
// Apple owns ios + macos (SAME plugin instance); Android carries its own.
assert.equal(getPlugin('ios'), getPlugin('macos'));
assert.ok(getPlugin('ios').perf, 'apple plugin exposes perf');
assert.equal(getPlugin('apple'), getPlugin('apple'));
assert.ok(getPlugin('apple').perf, 'apple plugin exposes perf');
assert.ok(getPlugin('android').perf, 'android plugin exposes perf');
// linux/web historically returned `false`; they get NO facet, and the daemon
// lookup preserves that fallthrough (asserted below).
@@ -8,7 +8,7 @@ const IOS_SESSION: SessionState = {
createdAt: Date.now(),
actions: [],
device: {
platform: 'ios',
platform: 'apple',
target: 'mobile',
id: 'SIM-001',
name: 'iPhone 16',
@@ -19,7 +19,7 @@ import { withRequestPlatformProviderScope } from '../request-platform-providers.
import type { DaemonRequest } from '../types.ts';
const OTHER_IOS_SIMULATOR: DeviceInfo = {
platform: 'ios',
platform: 'apple',
id: 'sim-2',
name: 'iPhone 17',
kind: 'simulator',
@@ -20,7 +20,7 @@ function makeIosSimulatorSession(showTouches: boolean): SessionState {
createdAt: Date.now(),
actions: [],
device: {
platform: 'ios',
platform: 'apple',
target: 'mobile',
id: 'sim-1',
name: 'iPhone 17 Pro',
@@ -39,7 +39,7 @@ function makeIosSession(name: string): SessionState {
createdAt: 1_700_000_000_000,
actions: [],
device: {
platform: 'ios',
platform: 'apple',
target: 'mobile',
id: 'SIM-001',
name: 'iPhone 16',
@@ -29,7 +29,7 @@ function makeIosSession(name: string): SessionState {
createdAt: Date.now(),
actions: [],
device: {
platform: 'ios',
platform: 'apple',
target: 'mobile',
id: 'SIM-001',
name: 'iPhone 16',
@@ -20,7 +20,7 @@ const mockEnsureDeviceReady = vi.mocked(ensureDeviceReady);
function makeIosDevice(id: string): DeviceInfo {
return {
platform: 'ios',
platform: 'apple',
id,
name: `iPhone ${id}`,
kind: 'simulator',
@@ -35,7 +35,7 @@ test('router blocks non-record commands when recording was invalidated', async (
actions: [],
appBundleId: 'com.apple.Preferences',
device: {
platform: 'ios',
platform: 'apple',
target: 'mobile',
id: 'sim-1',
name: 'iPhone 17 Pro',
@@ -88,7 +88,7 @@ test('router allows iOS simulator gestures during overlay recording after runner
actions: [],
appBundleId: 'com.apple.Preferences',
device: {
platform: 'ios',
platform: 'apple',
target: 'mobile',
id: 'sim-1',
name: 'iPhone 17 Pro',
@@ -48,7 +48,7 @@ function makeIosSession(name: string): SessionState {
createdAt: 1_700_000_000_000,
actions: [],
device: {
platform: 'ios',
platform: 'apple',
target: 'mobile',
id: 'SIM-001',
name: 'iPhone 16',
@@ -41,7 +41,8 @@ function makeMacOsMenubarSession(name: string): SessionState {
return {
name,
device: {
platform: 'macos',
platform: 'apple',
appleOs: 'macos',
id: 'host-macos-local',
name: 'Mac',
kind: 'device',
@@ -31,7 +31,7 @@ function makeIosSession(name: string): SessionState {
createdAt: 1_700_000_000_000,
actions: [],
device: {
platform: 'ios',
platform: 'apple',
target: 'mobile',
id: 'SIM-001',
name: 'iPhone 16',
+1 -1
View File
@@ -146,7 +146,7 @@ async function withMockedXcrun(
process.env.AGENT_DEVICE_TEST_ARGS_FILE = argsLogPath;
const device: DeviceInfo = {
platform: 'ios',
platform: 'apple',
id: 'sim-1',
name: 'iPhone 17 Pro',
kind: 'simulator',
@@ -63,7 +63,7 @@ test('selector mismatch explains session recovery commands', () => {
test('accepts --platform apple alias for ios sessions', () => {
const session = makeSession({
device: {
platform: 'ios',
platform: 'apple',
id: 'tv-sim-1',
name: 'Apple TV',
kind: 'simulator',
@@ -132,7 +132,7 @@ test('rejects mismatched device selector', () => {
test('accepts matching ios simulator set selector for iOS simulator sessions', () => {
const session = makeSession({
device: {
platform: 'ios',
platform: 'apple',
id: 'sim-1',
name: 'iPhone 17',
kind: 'simulator',
+1 -1
View File
@@ -18,7 +18,7 @@ function makeSession(name: string): SessionState {
return {
name,
device: {
platform: 'ios',
platform: 'apple',
id: 'sim-1',
name: 'iPhone',
kind: 'simulator',
+3 -3
View File
@@ -32,7 +32,7 @@ beforeEach(() => {
test('shutdownDeviceTarget treats already-stopped targets as success', async () => {
const device: DeviceInfo = {
platform: 'ios',
platform: 'apple',
id: 'sim-1',
name: 'iPhone',
kind: 'simulator',
@@ -51,7 +51,7 @@ test('shutdownDeviceTarget treats already-stopped targets as success', async ()
test('shutdownDeviceTarget treats iOS Shutdown final state as success', async () => {
const device: DeviceInfo = {
platform: 'ios',
platform: 'apple',
id: 'sim-1',
name: 'iPhone',
kind: 'simulator',
@@ -76,7 +76,7 @@ test('shutdownDeviceTarget treats iOS Shutdown final state as success', async ()
test('shutdownDeviceTarget preserves iOS shutdown failure when final state probe fails', async () => {
const device: DeviceInfo = {
platform: 'ios',
platform: 'apple',
id: 'sim-1',
name: 'iPhone',
kind: 'simulator',
+10 -10
View File
@@ -1,6 +1,6 @@
import fs from 'node:fs';
import path from 'node:path';
import type { DeviceInfo } from '../kernel/device.ts';
import { isIosFamily, isMacOs, type DeviceInfo } from '../kernel/device.ts';
import { AppError } from '../kernel/errors.ts';
import { tryGetPlugin } from '../core/platform-plugin/plugin.ts';
import { registerBuiltinPlatformPlugins } from '../core/interactors/register-builtins.ts';
@@ -248,7 +248,7 @@ export async function readSessionNetworkCapture(params: {
}
}
const canRecoverIosSimulatorLogShow =
device.platform === 'ios' && device.kind === 'simulator' && Boolean(appBundleId);
isIosFamily(device) && device.kind === 'simulator' && Boolean(appBundleId);
if (canRecoverIosSimulatorLogShow && dump.entries.length === 0) {
const recovered = await readRecentIosSimulatorNetworkCapture({
deviceId: device.id,
@@ -282,7 +282,7 @@ export async function readSessionNetworkCapture(params: {
'Capture uses the session app log file. For fresh traffic, run logs clear --restart before reproducing requests.',
);
} else if (appLogState !== 'active' && notes.length === 0) {
if (device.platform === 'ios' && device.kind === 'simulator') {
if (isIosFamily(device) && device.kind === 'simulator') {
notes.push(
'Session app log stream is inactive. The iOS simulator recovery path scanned recent simctl log history, but a fresh logs clear --restart window is still the most reliable repro loop.',
);
@@ -358,7 +358,7 @@ async function startLocalAppLog({
ensureLogPath(outPath);
const stream = fs.createWriteStream(outPath, { flags: 'a' });
const redactionPatterns = getAppLogRedactionPatterns();
if (device.platform === 'ios') {
if (isIosFamily(device)) {
if (device.kind === 'device') {
return await startIosDeviceAppLog(device.id, stream, redactionPatterns, pidPath);
}
@@ -375,7 +375,7 @@ async function startLocalAppLog({
assertAndroidPackageArgSafe(appBundleId);
return await startAndroidAppLog(device.id, appBundleId, stream, redactionPatterns, pidPath);
}
if (device.platform === 'macos') {
if (isMacOs(device)) {
return await startMacOsAppLog(appBundleId, stream, redactionPatterns, pidPath);
}
stream.end();
@@ -416,10 +416,10 @@ async function readRecentIosSimulatorNetworkCapture(params: {
}
function buildNoHttpEntriesNote(device: DeviceInfo): string {
if (device.platform === 'ios' && device.kind === 'simulator') {
if (isIosFamily(device) && device.kind === 'simulator') {
return 'No HTTP(s) entries were found in recent iOS simulator app logs. If the app only emits non-HTTP diagnostics, inspect logs path or add app-side URLSession/network logging for per-request timing and payload details.';
}
if (device.platform === 'ios') {
if (isIosFamily(device)) {
return 'No HTTP(s) entries were found in recent iOS device app logs. iOS network dump only sees what the app emits into Unified Logging for this process.';
}
return 'No HTTP(s) entries were found in recent session app logs.';
@@ -463,7 +463,7 @@ export async function runAppLogDoctor(
}
}
}
if (device.platform === 'ios' && device.kind === 'simulator') {
if (isIosFamily(device) && device.kind === 'simulator') {
try {
const simctl = await runXcrun(['simctl', 'help'], { allowFailure: true });
checks.simctlAvailable = simctl.exitCode === 0;
@@ -471,7 +471,7 @@ export async function runAppLogDoctor(
checks.simctlAvailable = false;
}
}
if (device.platform === 'ios' && device.kind === 'device') {
if (isIosFamily(device) && device.kind === 'device') {
try {
const devicectl = await runXcrun(['devicectl', '--version'], { allowFailure: true });
checks.devicectlAvailable = devicectl.exitCode === 0;
@@ -479,7 +479,7 @@ export async function runAppLogDoctor(
checks.devicectlAvailable = false;
}
}
if (device.platform === 'macos') {
if (isMacOs(device)) {
try {
const log = await runCmd('log', ['help'], { allowFailure: true });
checks.logAvailable = log.exitCode === 0;
+2 -2
View File
@@ -2,7 +2,7 @@ import { isDeepLinkTarget } from '../core/open-target.ts';
import type { SessionSurface } from '../core/session-surface.ts';
import type { AppleRunnerLifecycleOptions } from '../platforms/apple/core/runner/runner-provider.ts';
import { prewarmAppleRunnerCache } from '../platforms/apple/core/runner/runner-client.ts';
import type { DeviceInfo } from '../kernel/device.ts';
import { isIosFamily, type DeviceInfo } from '../kernel/device.ts';
import { contextFromFlags } from './context.ts';
import type { DaemonRequest } from './types.ts';
@@ -63,7 +63,7 @@ export function createAppleRunnerCachePrewarmOnColdBoot(params: {
enabled: boolean;
}): ((device: DeviceInfo) => void) | undefined {
const { req, logPath, device, traceLogPath, enabled } = params;
if (!enabled || device.platform !== 'ios' || device.kind !== 'simulator') {
if (!enabled || !isIosFamily(device) || device.kind !== 'simulator') {
return undefined;
}
return (bootingDevice) =>
+2 -2
View File
@@ -1,4 +1,4 @@
import type { DeviceInfo } from '../kernel/device.ts';
import { isIosFamily, type DeviceInfo } from '../kernel/device.ts';
import os from 'node:os';
import path from 'node:path';
import { promises as fs } from 'node:fs';
@@ -39,7 +39,7 @@ export async function ensureDeviceReady(
readyCache.delete(cacheKey);
}
if (device.platform === 'ios') {
if (isIosFamily(device)) {
if (device.kind === 'simulator') {
const { ensureBootedSimulator } = await import('../platforms/apple/core/simulator.ts');
await ensureBootedSimulator(device, {
+2 -2
View File
@@ -1,7 +1,7 @@
import type { DeviceInfo } from '../kernel/device.ts';
import { isIosFamily, type DeviceInfo } from '../kernel/device.ts';
export function isIosSimulator(device: DeviceInfo): boolean {
return device.platform === 'ios' && device.kind === 'simulator';
return isIosFamily(device) && device.kind === 'simulator';
}
export function isAndroidEmulator(device: DeviceInfo): boolean {
+2 -1
View File
@@ -1,3 +1,4 @@
import { isIosFamily } from '../kernel/device.ts';
import type { SessionState } from './types.ts';
import { tryParseSelectorChain } from './selectors.ts';
import { asAppError } from '../kernel/errors.ts';
@@ -11,7 +12,7 @@ export function readSimpleIosSelectorTarget(params: {
}): DirectIosSelectorTarget | null {
const { session, selectorExpression } = params;
if (!session) return null;
if (session.device.platform !== 'ios') return null;
if (!isIosFamily(session.device)) return null;
if (session.postGestureStabilization) return null;
const chain = tryParseSelectorChain(selectorExpression);
if (!chain) return null;
@@ -1932,7 +1932,8 @@ test('click --button secondary on @ref dispatches a secondary press on macOS and
const sessionName = 'default';
const session = makeSession(sessionName);
session.device = {
platform: 'macos',
platform: 'apple',
appleOs: 'macos',
id: 'macos-desktop',
name: 'My Mac',
kind: 'device',
@@ -1992,7 +1993,8 @@ test('click --button middle on macOS fails with an explicit unsupported-operatio
const sessionName = 'default';
const session = makeSession(sessionName);
session.device = {
platform: 'macos',
platform: 'apple',
appleOs: 'macos',
id: 'macos-desktop',
name: 'My Mac',
kind: 'device',
@@ -78,7 +78,7 @@ test('react-native dismiss-overlay taps collapsed warning close affordance inste
expect(response?.ok).toBe(true);
expect(mockDispatchCommand).toHaveBeenCalledWith(
expect.objectContaining({ platform: 'ios' }),
expect.objectContaining({ platform: 'apple' }),
'press',
['379', '820'],
undefined,
@@ -137,7 +137,7 @@ test('react-native dismiss-overlay prefers non-trailing collapsed warning close
expect(response?.ok).toBe(true);
expect(mockDispatchCommand).toHaveBeenCalledWith(
expect.objectContaining({ platform: 'ios' }),
expect.objectContaining({ platform: 'apple' }),
'press',
['27', '820'],
undefined,
@@ -209,7 +209,7 @@ test('react-native dismiss-overlay does not confuse app dismiss buttons with ove
expect(response?.ok).toBe(true);
expect(mockDispatchCommand).toHaveBeenCalledWith(
expect.objectContaining({ platform: 'ios' }),
expect.objectContaining({ platform: 'apple' }),
'press',
['369', '813'],
undefined,
@@ -324,7 +324,7 @@ test('react-native dismiss-overlay dismisses RedBox error overlays instead of mi
expect(response?.ok).toBe(true);
expect(mockDispatchCommand).toHaveBeenCalledWith(
expect.objectContaining({ platform: 'ios' }),
expect.objectContaining({ platform: 'apple' }),
'press',
['95', '752'],
undefined,
@@ -446,7 +446,7 @@ test('react-native dismiss-overlay uses Dismiss when RedBox Minimize is absent',
expect(response?.ok).toBe(true);
expect(mockDispatchCommand).toHaveBeenCalledWith(
expect.objectContaining({ platform: 'ios' }),
expect.objectContaining({ platform: 'apple' }),
'press',
['95', '752'],
undefined,
@@ -501,7 +501,7 @@ test('react-native dismiss-overlay accepts RedBox control labels with keyboard s
expect(response?.ok).toBe(true);
expect(mockDispatchCommand).toHaveBeenCalledWith(
expect.objectContaining({ platform: 'ios' }),
expect.objectContaining({ platform: 'apple' }),
'press',
['70', '722'],
undefined,
@@ -872,7 +872,7 @@ function makeSession(name: string, platform: 'ios' | 'android' = 'ios'): Session
createdAt: Date.now(),
actions: [],
device: {
platform,
platform: platform === 'ios' ? 'apple' : 'android',
id: 'sim-1',
name: platform === 'ios' ? 'iPhone' : 'Pixel',
kind: platform === 'ios' ? 'simulator' : 'emulator',
@@ -107,7 +107,7 @@ function makeSession(name: string, device: SessionState['device']): SessionState
function makeIosDeviceSession(name: string, appBundleId?: string): SessionState {
const session = makeSession(name, {
platform: 'ios',
platform: 'apple',
id: 'ios-device-1',
name: 'My iPhone',
kind: 'device',
@@ -121,7 +121,7 @@ function makeIosDeviceSession(name: string, appBundleId?: string): SessionState
function makeIosSimulatorSession(name: string): SessionState {
return makeSession(name, {
platform: 'ios',
platform: 'apple',
id: 'ios-sim-1',
name: 'iPhone 16',
kind: 'simulator',
@@ -876,7 +876,7 @@ test('record stop resizes iOS simulator recording when max-size is explicit', as
sessionStore.set(
sessionName,
makeSession(sessionName, {
platform: 'ios',
platform: 'apple',
id: 'sim-1',
name: 'Simulator',
kind: 'simulator',
@@ -917,7 +917,7 @@ test('record stop forwards the requested quality to the resize step', async () =
sessionStore.set(
sessionName,
makeSession(sessionName, {
platform: 'ios',
platform: 'apple',
id: 'sim-1',
name: 'Simulator',
kind: 'simulator',
@@ -958,7 +958,7 @@ test('record stop leaves a short visual tail after iOS simulator gestures', asyn
const sessionName = 'ios-sim-visual-tail';
const kill = vi.fn();
const session = makeSession(sessionName, {
platform: 'ios',
platform: 'apple',
id: 'sim-1',
name: 'Simulator',
kind: 'simulator',
@@ -996,7 +996,7 @@ test('record stop reports too-short iOS simulator recordings without leaving inv
const outPath = path.join(os.tmpdir(), `agent-device-too-short-${Date.now()}.mp4`);
fs.writeFileSync(outPath, 'not-a-video');
const session = makeSession(sessionName, {
platform: 'ios',
platform: 'apple',
id: 'sim-1',
name: 'Simulator',
kind: 'simulator',
@@ -1033,7 +1033,7 @@ test('record stop measures too-short iOS simulator recordings from stop request
const outPath = path.join(os.tmpdir(), `agent-device-too-short-delayed-${Date.now()}.mp4`);
fs.writeFileSync(outPath, 'not-a-video');
const session = makeSession(sessionName, {
platform: 'ios',
platform: 'apple',
id: 'sim-1',
name: 'Simulator',
kind: 'simulator',
@@ -1128,7 +1128,7 @@ test('record start stores iOS simulator recorder pid for scoped cleanup', async
sessionStore.set(
sessionName,
makeSession(sessionName, {
platform: 'ios',
platform: 'apple',
id: 'sim-1',
name: 'Simulator',
kind: 'simulator',
@@ -1162,7 +1162,7 @@ test('record stop prefers session-owned iOS recorder processes before path fallb
const sessionName = 'ios-sim-owned-recorder';
const kill = vi.fn();
const session = makeSession(sessionName, {
platform: 'ios',
platform: 'apple',
id: 'sim-1',
name: 'Simulator',
kind: 'simulator',
@@ -1231,7 +1231,7 @@ test('record stop falls back to path matching for stale iOS simulator recordVide
const sessionName = 'ios-sim-stale-recorder';
const kill = vi.fn();
const session = makeSession(sessionName, {
platform: 'ios',
platform: 'apple',
id: 'sim-1',
name: 'Simulator',
kind: 'simulator',
@@ -1280,7 +1280,7 @@ test('record stop keeps iOS simulator video when overlay export fails', async ()
sessionStore.set(
sessionName,
makeSession(sessionName, {
platform: 'ios',
platform: 'apple',
id: 'sim-1',
name: 'Simulator',
kind: 'simulator',
@@ -1326,7 +1326,7 @@ test('record stop skips touch overlay export when no gestures were recorded', as
sessionStore.set(
sessionName,
makeSession(sessionName, {
platform: 'ios',
platform: 'apple',
id: 'sim-1',
name: 'Simulator',
kind: 'simulator',
@@ -1362,7 +1362,7 @@ test('record stop keeps iOS simulator video when resize export fails', async ()
sessionStore.set(
sessionName,
makeSession(sessionName, {
platform: 'ios',
platform: 'apple',
id: 'sim-1',
name: 'Simulator',
kind: 'simulator',
@@ -1400,7 +1400,7 @@ test('record start does not fail when iOS simulator runner warm-up fails', async
const sessionStore = makeSessionStore();
const sessionName = 'ios-sim-warm-failure';
const session = makeSession(sessionName, {
platform: 'ios',
platform: 'apple',
id: 'sim-1',
name: 'Simulator',
kind: 'simulator',
@@ -1447,7 +1447,7 @@ test('record start anchors gesture clock from simulator warm-up and skips standa
const sessionStore = makeSessionStore();
const sessionName = 'ios-sim-warm-anchor';
const session = makeSession(sessionName, {
platform: 'ios',
platform: 'apple',
id: 'sim-1',
name: 'Simulator',
kind: 'simulator',
@@ -1492,7 +1492,7 @@ test('record start falls back to standalone uptime when warm response lacks curr
const sessionStore = makeSessionStore();
const sessionName = 'ios-sim-warm-missing';
const session = makeSession(sessionName, {
platform: 'ios',
platform: 'apple',
id: 'sim-1',
name: 'Simulator',
kind: 'simulator',
@@ -1534,7 +1534,7 @@ test('record start rejects non-finite or non-positive warm anchors', async () =>
const sessionStore = makeSessionStore();
const sessionName = `ios-sim-warm-bad-${badValue}`;
const session = makeSession(sessionName, {
platform: 'ios',
platform: 'apple',
id: 'sim-1',
name: 'Simulator',
kind: 'simulator',
@@ -1576,7 +1576,7 @@ test('record start degrades to wall-clock when warm anchor missing and uptime fa
const sessionStore = makeSessionStore();
const sessionName = 'ios-sim-anchor-degraded';
const session = makeSession(sessionName, {
platform: 'ios',
platform: 'apple',
id: 'sim-1',
name: 'Simulator',
kind: 'simulator',
@@ -1615,7 +1615,7 @@ test('record start skips iOS simulator runner warm-up when touch overlays are hi
const sessionStore = makeSessionStore();
const sessionName = 'ios-sim-hide-touches';
const session = makeSession(sessionName, {
platform: 'ios',
platform: 'apple',
id: 'sim-1',
name: 'Simulator',
kind: 'simulator',
@@ -2068,7 +2068,7 @@ test('record stop keeps iOS simulator video when touch overlay recording was inv
const sessionStore = makeSessionStore();
const sessionName = 'ios-invalidated-recording';
const session = makeSession(sessionName, {
platform: 'ios',
platform: 'apple',
id: 'sim-1',
name: 'iPhone 17 Pro',
kind: 'simulator',
@@ -98,7 +98,7 @@ test('close --shutdown calls shutdownSimulator for iOS simulator and includes re
sessionStore.set(
sessionName,
makeSession(sessionName, {
platform: 'ios',
platform: 'apple',
id: 'sim-udid-1',
name: 'iPhone 15',
kind: 'simulator',
@@ -230,7 +230,7 @@ test('close --shutdown is ignored for non-simulator iOS devices', async () => {
sessionStore.set(
sessionName,
makeSession(sessionName, {
platform: 'ios',
platform: 'apple',
id: 'physical-device-1',
name: 'My iPhone',
kind: 'device',
@@ -281,7 +281,7 @@ test('close stops active Apple xctrace perf capture before deleting session', as
};
sessionStore.set(sessionName, {
...makeSession(sessionName, {
platform: 'ios',
platform: 'apple',
id: 'sim-udid-4',
name: 'iPhone 15',
kind: 'simulator',
@@ -330,7 +330,7 @@ test('daemon session teardown stops active Apple xctrace perf capture', async ()
};
const session = {
...makeSession(sessionName, {
platform: 'ios',
platform: 'apple',
id: 'sim-udid-5',
name: 'iPhone 15',
kind: 'simulator',
@@ -402,7 +402,8 @@ test('close stops active host audio probe before deleting session', async () =>
const kill = vi.fn();
const session = {
...makeSession(sessionName, {
platform: 'macos',
platform: 'apple',
appleOs: 'macos',
id: 'macos',
name: 'Mac',
kind: 'device',
@@ -680,7 +681,7 @@ test('close --shutdown returns success and failure payload when shutdownSimulato
sessionStore.set(
sessionName,
makeSession(sessionName, {
platform: 'ios',
platform: 'apple',
id: 'sim-udid-3',
name: 'iPhone 15',
kind: 'simulator',
@@ -10,7 +10,7 @@ const iosSimulatorSession: SessionState = {
name: 'ios-sim',
createdAt: Date.now(),
device: {
platform: 'ios',
platform: 'apple',
id: 'sim-1',
name: 'iPhone 17 Pro',
kind: 'simulator',
@@ -537,7 +537,7 @@ test('perf memory snapshot reports physical iOS memgraph as unavailable', async
actions: [],
appBundleId: 'com.example.app',
device: {
platform: 'ios',
platform: 'apple',
id: 'ios-device-1',
name: 'iPhone',
kind: 'device',
@@ -162,7 +162,7 @@ test('open applies launch-only flags only to the direct app launch before runtim
launchUrl: 'myapp://dev-client',
});
mockResolveTargetDevice.mockResolvedValue({
platform: 'ios',
platform: 'apple',
id: 'sim-1',
name: 'iPhone 15',
kind: 'simulator',
@@ -8,7 +8,7 @@ test('resolveRequestedOpenSurface rejects surface flag on iOS', () => {
() =>
resolveRequestedOpenSurface({
device: {
platform: 'ios',
platform: 'apple',
id: 'sim-1',
name: 'iPhone 17',
kind: 'simulator',
@@ -88,7 +88,7 @@ test('reinstall requires active session or explicit device selector', async () =
test('reinstall validates required args before device operations', async () => {
const sessionStore = makeStore();
const session = makeSession('default', {
platform: 'ios',
platform: 'apple',
id: 'sim-1',
name: 'iPhone',
kind: 'simulator',
@@ -233,7 +233,7 @@ test('install accepts path without app id hint', async () => {
test('reinstall resolves uploaded artifacts by id and cleans temp files after completion', async () => {
const sessionStore = makeStore();
const session = makeSession('default', {
platform: 'ios',
platform: 'apple',
id: 'sim-1',
name: 'iPhone',
kind: 'simulator',
@@ -2276,7 +2276,7 @@ test('runReplayScriptFile writes per-action timing events to active trace', asyn
sessionStore.set('s', {
name: 's',
device: {
platform: 'ios',
platform: 'apple',
id: 'sim-1',
name: 'iPhone',
kind: 'simulator',
+89 -78
View File
@@ -1,3 +1,4 @@
import { isMacOs } from '../../../kernel/device.ts';
import { test, expect, vi, beforeEach } from 'vitest';
vi.mock('../../../core/dispatch.ts', async (importOriginal) => {
@@ -195,7 +196,7 @@ beforeEach(() => {
mockResolveIosApp.mockImplementation(async (device, app) => {
const normalizedApp = app.toLowerCase();
if (normalizedApp === 'settings') {
return device.platform === 'macos' ? 'com.apple.systempreferences' : 'com.apple.Preferences';
return isMacOs(device) ? 'com.apple.systempreferences' : 'com.apple.Preferences';
}
if (normalizedApp === 'menubarapp') {
return 'com.example.menubarapp';
@@ -263,7 +264,7 @@ test('devices filters Apple-family platform selectors', async () => {
]);
mockListAppleDevices.mockResolvedValue([
{
platform: 'ios' as const,
platform: 'apple' as const,
id: 'sim-1',
name: 'iPhone 17 Pro',
kind: 'simulator' as const,
@@ -271,7 +272,8 @@ test('devices filters Apple-family platform selectors', async () => {
booted: true,
},
{
platform: 'macos' as const,
platform: 'apple',
appleOs: 'macos' as const,
id: 'host-macos-local',
name: 'Host Mac',
kind: 'device' as const,
@@ -321,7 +323,7 @@ test('devices omits internal appleOs from the public inventory projection', asyn
mockListAndroidDevices.mockResolvedValue([]);
mockListAppleDevices.mockResolvedValue([
{
platform: 'ios' as const,
platform: 'apple' as const,
id: 'sim-1',
name: 'iPad Pro 11-inch (M4)',
kind: 'simulator' as const,
@@ -729,7 +731,7 @@ test('close clears applied runtime transport hints before deleting the session',
});
sessionStore.set(sessionName, {
...makeSession(sessionName, {
platform: 'ios',
platform: 'apple',
id: 'sim-1',
name: 'iPhone 17 Pro',
kind: 'simulator',
@@ -763,7 +765,7 @@ test('close clears retained materialized install paths bound to the session', as
const sessionName = 'materialized-close-active';
sessionStore.set(sessionName, {
...makeSession(sessionName, {
platform: 'ios',
platform: 'apple',
id: 'sim-1',
name: 'iPhone 17 Pro',
kind: 'simulator',
@@ -875,7 +877,7 @@ test('boot prefers explicit device selector over active session device', async (
}),
);
const selectedDevice: SessionState['device'] = {
platform: 'ios',
platform: 'apple',
id: 'sim-2',
name: 'iPhone 17 Pro',
kind: 'simulator',
@@ -1143,7 +1145,7 @@ test('boot keeps --target validation when emulator is fallback-launched', async
test('shutdown turns off selected iOS simulator', async () => {
const sessionStore = makeSessionStore();
const selectedDevice: SessionState['device'] = {
platform: 'ios',
platform: 'apple',
id: 'sim-2',
name: 'iPhone 17 Pro',
kind: 'simulator',
@@ -1186,7 +1188,7 @@ test('shutdown rejects active session device and points to close --shutdown', as
const sessionStore = makeSessionStore();
const sessionName = 'default';
const selectedDevice: SessionState['device'] = {
platform: 'ios',
platform: 'apple',
id: 'sim-2',
name: 'iPhone 17 Pro',
kind: 'simulator',
@@ -1270,7 +1272,7 @@ test('shutdown turns off selected Android emulator', async () => {
test('shutdown rejects unsupported physical devices', async () => {
const sessionStore = makeSessionStore();
mockResolveTargetDevice.mockResolvedValue({
platform: 'ios',
platform: 'apple',
id: 'device-1',
name: 'iPhone',
kind: 'device',
@@ -1305,7 +1307,7 @@ test('shutdown rejects unsupported physical devices', async () => {
test('shutdown returns an error response when selected target shutdown fails', async () => {
const sessionStore = makeSessionStore();
const selectedDevice: SessionState['device'] = {
platform: 'ios',
platform: 'apple',
id: 'sim-2',
name: 'iPhone 17 Pro',
kind: 'simulator',
@@ -1353,7 +1355,7 @@ test('appstate on iOS requires active session on selected device', async () => {
const sessionName = 'default';
sessionStore.set(sessionName, {
...makeSession(sessionName, {
platform: 'ios',
platform: 'apple',
id: 'sim-1',
name: 'iPhone 15',
kind: 'simulator',
@@ -1363,7 +1365,7 @@ test('appstate on iOS requires active session on selected device', async () => {
appName: 'Settings',
});
const selectedDevice: SessionState['device'] = {
platform: 'ios',
platform: 'apple',
id: 'sim-2',
name: 'iPhone 17 Pro',
kind: 'simulator',
@@ -1399,7 +1401,7 @@ test('appstate returns session appName when bundle id is unavailable', async ()
const sessionName = 'sim';
sessionStore.set(sessionName, {
...makeSession(sessionName, {
platform: 'ios',
platform: 'apple',
id: 'sim-1',
name: 'iPhone 17 Pro',
kind: 'simulator',
@@ -1409,7 +1411,7 @@ test('appstate returns session appName when bundle id is unavailable', async ()
});
const selectedDevice: SessionState['device'] = {
platform: 'ios',
platform: 'apple',
id: 'sim-1',
name: 'iPhone 17 Pro',
kind: 'simulator',
@@ -1450,7 +1452,7 @@ test('appstate fails when iOS session has no tracked app', async () => {
sessionStore.set(
sessionName,
makeSession(sessionName, {
platform: 'ios',
platform: 'apple',
id: 'sim-1',
name: 'iPhone 17 Pro',
kind: 'simulator',
@@ -1459,7 +1461,7 @@ test('appstate fails when iOS session has no tracked app', async () => {
);
const selectedDevice: SessionState['device'] = {
platform: 'ios',
platform: 'apple',
id: 'sim-1',
name: 'iPhone 17 Pro',
kind: 'simulator',
@@ -1492,7 +1494,7 @@ test('appstate fails when iOS session has no tracked app', async () => {
test('appstate without session on iOS selector returns SESSION_NOT_FOUND', async () => {
const sessionStore = makeSessionStore();
const selectedDevice: SessionState['device'] = {
platform: 'ios',
platform: 'apple',
id: 'sim-2',
name: 'iPhone 17 Pro',
kind: 'simulator',
@@ -1629,7 +1631,7 @@ test('clipboard rejects unsupported iOS physical devices', async () => {
sessionStore.set(
sessionName,
makeSession(sessionName, {
platform: 'ios',
platform: 'apple',
id: 'ios-device-1',
name: 'iPhone Device',
kind: 'device',
@@ -1790,7 +1792,8 @@ test('perf samples Apple cpu and memory metrics on macOS app sessions', async ()
const sessionName = 'perf-session-macos';
sessionStore.set(sessionName, {
...makeSession(sessionName, {
platform: 'macos',
platform: 'apple',
appleOs: 'macos',
id: 'host-mac',
name: 'Host Mac',
kind: 'device',
@@ -1850,7 +1853,7 @@ test('perf samples Apple cpu and memory metrics on iOS simulator app sessions',
const sessionName = 'perf-session-ios-sim';
sessionStore.set(sessionName, {
...makeSession(sessionName, {
platform: 'ios',
platform: 'apple',
id: 'sim-1',
name: 'iPhone 17 Pro',
kind: 'simulator',
@@ -1907,7 +1910,7 @@ test('perf samples Apple cpu and memory metrics on physical iOS devices', async
const sessionName = 'perf-session-ios-device';
sessionStore.set(sessionName, {
...makeSession(sessionName, {
platform: 'ios',
platform: 'apple',
id: 'ios-device-1',
name: 'iPhone Device',
kind: 'device',
@@ -2031,7 +2034,7 @@ test('perf reports physical iOS cpu and memory as unavailable without an app bun
const sessionName = 'perf-session-ios-device-no-bundle';
sessionStore.set(sessionName, {
...makeSession(sessionName, {
platform: 'ios',
platform: 'apple',
id: 'ios-device-2',
name: 'iPhone Device',
kind: 'device',
@@ -2070,7 +2073,7 @@ test('open URL on existing iOS session clears stale app bundle id', async () =>
const sessionName = 'ios-session';
sessionStore.set(sessionName, {
...makeSession(sessionName, {
platform: 'ios',
platform: 'apple',
id: 'sim-1',
name: 'iPhone 15',
kind: 'simulator',
@@ -2081,7 +2084,7 @@ test('open URL on existing iOS session clears stale app bundle id', async () =>
});
mockResolveTargetDevice.mockResolvedValue({
platform: 'ios',
platform: 'apple',
id: 'sim-1',
name: 'iPhone 15',
kind: 'simulator',
@@ -2120,7 +2123,8 @@ test('open URL on existing macOS session clears stale app bundle id', async () =
const sessionName = 'macos-session';
sessionStore.set(sessionName, {
...makeSession(sessionName, {
platform: 'macos',
platform: 'apple',
appleOs: 'macos',
id: 'host-mac',
name: 'Mac',
kind: 'device',
@@ -2164,7 +2168,7 @@ test('open URL on existing iOS device session preserves app bundle id context',
const sessionName = 'ios-device-session';
sessionStore.set(sessionName, {
...makeSession(sessionName, {
platform: 'ios',
platform: 'apple',
id: 'ios-device-1',
name: 'iPhone Device',
kind: 'device',
@@ -2207,7 +2211,7 @@ test('open custom URL on existing iOS simulator session preserves app bundle id
const sessionName = 'ios-simulator-session';
sessionStore.set(sessionName, {
...makeSession(sessionName, {
platform: 'ios',
platform: 'apple',
id: 'sim-1',
name: 'iPhone 17 Pro',
kind: 'simulator',
@@ -2217,7 +2221,7 @@ test('open custom URL on existing iOS simulator session preserves app bundle id
appName: 'Example App',
});
mockResolveTargetDevice.mockResolvedValue({
platform: 'ios',
platform: 'apple',
id: 'sim-1',
name: 'iPhone 17 Pro',
kind: 'simulator',
@@ -2260,7 +2264,7 @@ test('open custom URL on fresh iOS simulator session infers app bundle id from U
const sessionStore = makeSessionStore();
const sessionName = 'ios-simulator-url-session';
mockResolveTargetDevice.mockResolvedValue({
platform: 'ios',
platform: 'apple',
id: 'sim-1',
name: 'iPhone 17 Pro',
kind: 'simulator',
@@ -2305,7 +2309,7 @@ test('open iOS simulator app prewarms runner cache during cold boot', async () =
const sessionStore = makeSessionStore();
const sessionName = 'ios-simulator-cold-boot-cache-prewarm';
const device: SessionState['device'] = {
platform: 'ios',
platform: 'apple',
id: 'sim-1',
name: 'iPhone 17 Pro',
kind: 'simulator',
@@ -2349,7 +2353,7 @@ test('open iOS app session prewarms runner session when app bundle id is known',
const sessionName = 'ios-device-session';
sessionStore.set(sessionName, {
...makeSession(sessionName, {
platform: 'ios',
platform: 'apple',
id: 'ios-device-1',
name: 'iPhone Device',
kind: 'device',
@@ -2377,7 +2381,7 @@ test('open iOS app session prewarms runner session when app bundle id is known',
expect(response?.ok).toBe(true);
expect(mockPrewarmIosRunnerSession).toHaveBeenCalledTimes(1);
expect(mockPrewarmIosRunnerSession).toHaveBeenCalledWith(
expect.objectContaining({ platform: 'ios', id: 'ios-device-1' }),
expect.objectContaining({ platform: 'apple', id: 'ios-device-1' }),
expect.objectContaining({ logPath: expect.stringMatching(/daemon\.log$/) }),
);
});
@@ -2389,7 +2393,7 @@ test('open iOS Maestro app link waits for runner prewarm before launching app',
let finishPrewarm: (() => void) | undefined;
sessionStore.set(sessionName, {
...makeSession(sessionName, {
platform: 'ios',
platform: 'apple',
id: 'ios-device-1',
name: 'iPhone Device',
kind: 'device',
@@ -2450,7 +2454,7 @@ test('open iOS Maestro app link reports blocking runner prewarm failures before
const sessionName = 'ios-maestro-open-link-prewarm-failed';
sessionStore.set(sessionName, {
...makeSession(sessionName, {
platform: 'ios',
platform: 'apple',
id: 'ios-device-1',
name: 'iPhone Device',
kind: 'device',
@@ -2490,7 +2494,7 @@ test('open iOS Maestro app link reports blocking runner prewarm failures before
});
expect(mockDispatch).not.toHaveBeenCalled();
expect(mockPrewarmIosRunnerSession).toHaveBeenCalledWith(
expect.objectContaining({ platform: 'ios', id: 'ios-device-1' }),
expect.objectContaining({ platform: 'apple', id: 'ios-device-1' }),
expect.objectContaining({ propagateError: true }),
);
});
@@ -2501,7 +2505,7 @@ test('open iOS URL without app bundle id skips runner prewarm', async () => {
sessionStore.set(
sessionName,
makeSession(sessionName, {
platform: 'ios',
platform: 'apple',
id: 'ios-device-1',
name: 'iPhone Device',
kind: 'device',
@@ -2532,7 +2536,7 @@ test('prepare ios-runner starts the XCTest runner on an explicit iOS selector',
const sessionStore = makeSessionStore();
const sessionName = 'prepare-ios-runner';
mockResolveTargetDevice.mockResolvedValue({
platform: 'ios',
platform: 'apple',
id: 'sim-1',
name: 'iPhone 17 Pro',
kind: 'simulator',
@@ -2557,11 +2561,11 @@ test('prepare ios-runner starts the XCTest runner on an explicit iOS selector',
expect(response).toBeTruthy();
expect(response?.ok).toBe(true);
expect(mockEnsureDeviceReady).toHaveBeenCalledWith(
expect.objectContaining({ platform: 'ios', id: 'sim-1' }),
expect.objectContaining({ platform: 'apple', id: 'sim-1' }),
);
expect(mockPrepareIosRunner).toHaveBeenCalledTimes(1);
expect(mockPrepareIosRunner).toHaveBeenCalledWith(
expect.objectContaining({ platform: 'ios', id: 'sim-1' }),
expect.objectContaining({ platform: 'apple', id: 'sim-1' }),
expect.objectContaining({
cleanStaleBundles: true,
buildTimeoutMs: 240000,
@@ -2594,7 +2598,8 @@ test('prepare ios-runner starts the XCTest runner on an explicit macOS selector'
const sessionStore = makeSessionStore();
const sessionName = 'prepare-macos-runner';
mockResolveTargetDevice.mockResolvedValue({
platform: 'macos',
platform: 'apple',
appleOs: 'macos',
id: 'host-macos-local',
name: 'Host Mac',
kind: 'device',
@@ -2620,7 +2625,7 @@ test('prepare ios-runner starts the XCTest runner on an explicit macOS selector'
expect(response).toBeTruthy();
expect(response?.ok).toBe(true);
expect(mockPrepareIosRunner).toHaveBeenCalledWith(
expect.objectContaining({ platform: 'macos', id: 'host-macos-local' }),
expect.objectContaining({ platform: 'apple', id: 'host-macos-local' }),
expect.objectContaining({
buildTimeoutMs: 240000,
healthTimeoutMs: 240000,
@@ -2710,7 +2715,7 @@ test('open web URL on iOS device session without active app falls back to Safari
sessionStore.set(
sessionName,
makeSession(sessionName, {
platform: 'ios',
platform: 'apple',
id: 'ios-device-1',
name: 'iPhone Device',
kind: 'device',
@@ -2751,7 +2756,7 @@ test('open app and URL on existing iOS device session keeps app context', async
const sessionName = 'ios-device-session';
sessionStore.set(sessionName, {
...makeSession(sessionName, {
platform: 'ios',
platform: 'apple',
id: 'ios-device-1',
name: 'iPhone Device',
kind: 'device',
@@ -2797,7 +2802,8 @@ test('open app on existing macOS session resolves and stores bundle id', async (
const sessionName = 'macos-session';
sessionStore.set(sessionName, {
...makeSession(sessionName, {
platform: 'macos',
platform: 'apple',
appleOs: 'macos',
id: 'host-mac',
name: 'Mac',
kind: 'device',
@@ -2839,7 +2845,7 @@ test('open app on existing macOS session resolves and stores bundle id', async (
test('open rejects --surface on non-macOS devices', async () => {
const sessionStore = makeSessionStore();
mockResolveTargetDevice.mockResolvedValue({
platform: 'ios',
platform: 'apple',
id: 'sim-1',
name: 'iPhone 17 Pro',
kind: 'simulator',
@@ -2871,7 +2877,8 @@ test('open on existing macOS frontmost-app session preserves surface without --s
const sessionName = 'macos-frontmost-existing';
sessionStore.set(sessionName, {
...makeSession(sessionName, {
platform: 'macos',
platform: 'apple',
appleOs: 'macos',
id: 'host-macos-local',
name: 'Host Mac',
kind: 'device',
@@ -2931,7 +2938,7 @@ test('open on existing iOS session refreshes unavailable simulator by name', asy
const sessionName = 'ios-session';
sessionStore.set(sessionName, {
...makeSession(sessionName, {
platform: 'ios',
platform: 'apple',
id: 'stale-sim',
name: 'iPhone 17 Pro',
kind: 'simulator',
@@ -2942,7 +2949,7 @@ test('open on existing iOS session refreshes unavailable simulator by name', asy
});
const resolvedDevice: SessionState['device'] = {
platform: 'ios',
platform: 'apple',
id: 'fresh-sim',
name: 'iPhone 17 Pro',
kind: 'simulator',
@@ -3182,7 +3189,7 @@ test('open --relaunch on iOS stops runner before close/open', async () => {
const sessionName = 'ios-session';
sessionStore.set(sessionName, {
...makeSession(sessionName, {
platform: 'ios',
platform: 'apple',
id: 'ios-device-1',
name: 'My iPhone',
kind: 'device',
@@ -3193,7 +3200,7 @@ test('open --relaunch on iOS stops runner before close/open', async () => {
const calls: string[] = [];
mockResolveTargetDevice.mockResolvedValue({
platform: 'ios',
platform: 'apple',
id: 'ios-device-1',
name: 'My iPhone',
kind: 'device',
@@ -3231,7 +3238,7 @@ test('open --relaunch on iOS simulator stops runner before close/open', async ()
const sessionName = 'ios-simulator-session';
sessionStore.set(sessionName, {
...makeSession(sessionName, {
platform: 'ios',
platform: 'apple',
id: 'sim-1',
name: 'iPhone 17 Pro',
kind: 'simulator',
@@ -3242,7 +3249,7 @@ test('open --relaunch on iOS simulator stops runner before close/open', async ()
const calls: string[] = [];
mockResolveTargetDevice.mockResolvedValue({
platform: 'ios',
platform: 'apple',
id: 'sim-1',
name: 'iPhone 17 Pro',
kind: 'simulator',
@@ -3282,7 +3289,7 @@ test('open --relaunch includes timing and waits for iOS runner prewarm after ope
const events: string[] = [];
sessionStore.set(sessionName, {
...makeSession(sessionName, {
platform: 'ios',
platform: 'apple',
id: 'ios-device-1',
name: 'My iPhone',
kind: 'device',
@@ -3348,7 +3355,7 @@ test('open --relaunch on iOS without existing session closes then opens target a
const sessionStore = makeSessionStore();
const sessionName = 'ios-new-session';
mockResolveTargetDevice.mockResolvedValue({
platform: 'ios',
platform: 'apple',
id: 'ios-device-1',
name: 'My iPhone',
kind: 'device',
@@ -3388,7 +3395,7 @@ test('open --relaunch on iOS simulator reaches settle path for close and open',
const sessionName = 'ios-sim-session';
sessionStore.set(sessionName, {
...makeSession(sessionName, {
platform: 'ios',
platform: 'apple',
id: 'sim-1',
name: 'iPhone 16',
kind: 'simulator',
@@ -3398,7 +3405,7 @@ test('open --relaunch on iOS simulator reaches settle path for close and open',
});
mockResolveTargetDevice.mockResolvedValue({
platform: 'ios',
platform: 'apple',
id: 'sim-1',
name: 'iPhone 16',
kind: 'simulator',
@@ -3435,7 +3442,8 @@ test('close on macOS session stops runner and dismisses automation alert before
const sessionName = 'macos-session';
sessionStore.set(sessionName, {
...makeSession(sessionName, {
platform: 'macos',
platform: 'apple',
appleOs: 'macos',
id: 'host-macos-local',
name: 'Host Mac',
kind: 'device',
@@ -3485,7 +3493,7 @@ test('close <app> on iOS stops runner before app close dispatch and performs fin
const sessionName = 'ios-close-session';
sessionStore.set(sessionName, {
...makeSession(sessionName, {
platform: 'ios',
platform: 'apple',
id: 'ios-device-1',
name: 'My iPhone',
kind: 'device',
@@ -3527,7 +3535,7 @@ test('close <app> on iOS simulator retains runner while terminating app', async
const sessionName = 'ios-simulator-close-session';
sessionStore.set(sessionName, {
...makeSession(sessionName, {
platform: 'ios',
platform: 'apple',
id: 'sim-1',
name: 'iPhone 17 Pro',
kind: 'simulator',
@@ -3569,7 +3577,8 @@ test('close <app> on macOS stops runner before app close dispatch and dismisses
const sessionName = 'macos-close-session';
sessionStore.set(sessionName, {
...makeSession(sessionName, {
platform: 'macos',
platform: 'apple',
appleOs: 'macos',
id: 'host-macos-local',
name: 'Host Mac',
kind: 'device',
@@ -3805,7 +3814,7 @@ test('open on in-use device returns DEVICE_IN_USE before readiness checks', asyn
sessionStore.set(
'busy-session',
makeSession('busy-session', {
platform: 'ios',
platform: 'apple',
id: 'ios-device-1',
name: 'iPhone Device',
kind: 'device',
@@ -3814,7 +3823,7 @@ test('open on in-use device returns DEVICE_IN_USE before readiness checks', asyn
);
mockResolveTargetDevice.mockResolvedValue({
platform: 'ios',
platform: 'apple',
id: 'ios-device-1',
name: 'iPhone Device',
kind: 'device',
@@ -3849,7 +3858,7 @@ test('open on in-use device returns DEVICE_IN_USE before readiness checks', asyn
test('open on device owned by recording session returns recording recovery hint', async () => {
const sessionStore = makeSessionStore();
const recordingSession = makeSession('default', {
platform: 'ios',
platform: 'apple',
id: 'ios-device-1',
name: 'iPhone Device',
kind: 'device',
@@ -3868,7 +3877,7 @@ test('open on device owned by recording session returns recording recovery hint'
sessionStore.set('default', recordingSession);
mockResolveTargetDevice.mockResolvedValue({
platform: 'ios',
platform: 'apple',
id: 'ios-device-1',
name: 'iPhone Device',
kind: 'device',
@@ -4113,7 +4122,7 @@ test('logs rejects invalid action', async () => {
sessionStore.set(
'default',
makeSession('default', {
platform: 'ios',
platform: 'apple',
id: 'sim-1',
name: 'iPhone',
kind: 'simulator',
@@ -4146,7 +4155,7 @@ test('logs start requires app session (appBundleId)', async () => {
sessionStore.set(
'default',
makeSession('default', {
platform: 'ios',
platform: 'apple',
id: 'sim-1',
name: 'iPhone',
kind: 'simulator',
@@ -4179,7 +4188,7 @@ test('logs stop requires active app log stream', async () => {
sessionStore.set(
'default',
makeSession('default', {
platform: 'ios',
platform: 'apple',
id: 'sim-1',
name: 'iPhone',
kind: 'simulator',
@@ -4257,7 +4266,7 @@ test('logs --restart is only supported with logs clear', async () => {
const sessionName = 'default';
sessionStore.set(sessionName, {
...makeSession(sessionName, {
platform: 'ios',
platform: 'apple',
id: 'sim-1',
name: 'iPhone Simulator',
kind: 'simulator',
@@ -4292,7 +4301,7 @@ test('logs clear --restart requires app session bundle id', async () => {
sessionStore.set(
sessionName,
makeSession(sessionName, {
platform: 'ios',
platform: 'apple',
id: 'sim-1',
name: 'iPhone Simulator',
kind: 'simulator',
@@ -4688,7 +4697,7 @@ test('network dump recovers iOS simulator entries from simctl log show when the
);
sessionStore.set(sessionName, {
...makeSession(sessionName, {
platform: 'ios',
platform: 'apple',
id: 'sim-1',
name: 'iPhone 17 Pro',
kind: 'simulator',
@@ -4696,7 +4705,7 @@ test('network dump recovers iOS simulator entries from simctl log show when the
}),
appBundleId: 'com.agentdevice.tester',
appLog: {
platform: 'ios',
platform: 'apple',
backend: 'ios-simulator',
outPath: appLogPath,
startedAt: 1_712_040_000_000,
@@ -4765,7 +4774,7 @@ test('network dump explains when iOS simulator recovery found app logs but no HT
);
sessionStore.set(sessionName, {
...makeSession(sessionName, {
platform: 'ios',
platform: 'apple',
id: 'sim-1',
name: 'iPhone 17 Pro',
kind: 'simulator',
@@ -4773,7 +4782,7 @@ test('network dump explains when iOS simulator recovery found app logs but no HT
}),
appBundleId: 'com.agentdevice.tester',
appLog: {
platform: 'ios',
platform: 'apple',
backend: 'ios-simulator',
outPath: appLogPath,
startedAt: 1_712_040_000_000,
@@ -4833,7 +4842,8 @@ test('network dump supports macOS desktop sessions', async () => {
const sessionName = 'macos-network';
sessionStore.set(sessionName, {
...makeSession(sessionName, {
platform: 'macos',
platform: 'apple',
appleOs: 'macos',
id: 'host-macos-local',
name: 'Host Mac',
kind: 'device',
@@ -4877,7 +4887,7 @@ test('network dump validates include mode and limit', async () => {
sessionStore.set(
sessionName,
makeSession(sessionName, {
platform: 'ios',
platform: 'apple',
id: 'sim-1',
name: 'iPhone Simulator',
kind: 'simulator',
@@ -4931,7 +4941,7 @@ test('session_list includes device_udid and ios_simulator_device_set for iOS ses
sessionStore.set(
'ios-scoped',
makeSession('ios-scoped', {
platform: 'ios',
platform: 'apple',
id: 'DEF-456',
name: 'iPhone 16',
kind: 'simulator',
@@ -4952,7 +4962,8 @@ test('session_list includes device_udid and ios_simulator_device_set for iOS ses
sessionStore.set(
'macos-1',
makeSession('macos-1', {
platform: 'macos',
platform: 'apple',
appleOs: 'macos',
id: 'host-macos-local',
name: 'Host Mac',
kind: 'device',
@@ -68,7 +68,7 @@ function makeSession(name: string, device: SessionState['device']): SessionState
}
const iosSimulatorDevice: SessionState['device'] = {
platform: 'ios',
platform: 'apple',
id: 'sim-1',
name: 'My iPhone Simulator',
kind: 'simulator',
@@ -76,7 +76,8 @@ const iosSimulatorDevice: SessionState['device'] = {
};
const macOsDevice: SessionState['device'] = {
platform: 'macos',
platform: 'apple',
appleOs: 'macos',
id: 'host-macos-local',
name: 'Host Mac',
kind: 'device',
@@ -310,7 +311,7 @@ test('snapshot rejects @ref scope without existing session snapshot', async () =
sessionStore.set(
sessionName,
makeSession(sessionName, {
platform: 'ios',
platform: 'apple',
id: 'sim-1',
name: 'My iPhone Simulator',
kind: 'simulator',
@@ -1479,7 +1480,7 @@ test('settings rejects unsupported iOS physical devices', async () => {
sessionStore.set(
sessionName,
makeSession(sessionName, {
platform: 'ios',
platform: 'apple',
id: 'ios-device-1',
name: 'My iPhone',
kind: 'device',
+2 -1
View File
@@ -1,3 +1,4 @@
import { isIosFamily } from '../../kernel/device.ts';
import {
installProviderDeviceInstallablePath,
type ProviderDeviceInstallResult,
@@ -159,7 +160,7 @@ export async function handleInstallFromSourceCommand(params: {
if (unsupported) return unsupported;
const requestSignal = getRequestSignal(req.meta?.requestId);
if (device.platform === 'ios') {
if (isIosFamily(device)) {
const { prepareIosInstallArtifact } =
await import('../../platforms/apple/core/install-artifact.ts');
const prepared = await prepareIosInstallArtifact(resolvedSource.source, {
+2 -1
View File
@@ -1,3 +1,4 @@
import { isIosFamily } from '../../kernel/device.ts';
import { dispatchCommand, type CommandFlags } from '../../core/dispatch.ts';
import { emitDiagnostic } from '../../utils/diagnostics.ts';
import { extractNodeReadText } from '../../snapshot/snapshot-processing.ts';
@@ -31,7 +32,7 @@ export async function readTextForNode(params: {
// Restricted to iOS because other backends read differently — macOS helper and Linux reads
// are value-first (AXValue/title/description), unlike the label-first snapshot readable text,
// so skipping their backend read would change the returned text.
if (device.platform === 'ios' && fallbackText && !prefersValueForReadableText(node.type ?? '')) {
if (isIosFamily(device) && fallbackText && !prefersValueForReadableText(node.type ?? '')) {
return fallbackText;
}
+2 -1
View File
@@ -1,4 +1,5 @@
import { dispatchCommand } from '../../core/dispatch.ts';
import { publicPlatformString } from '../../kernel/device.ts';
import type {
AgentDeviceBackend,
BackendActionResult,
@@ -47,7 +48,7 @@ function createInteractionBackend(
const { req, session } = params;
const webProvider = resolveNativeWebInteractionProvider(session);
return {
platform: session.device.platform,
platform: publicPlatformString(session.device),
captureSnapshot: async (_context, options): Promise<BackendSnapshotResult> => ({
snapshot: await params.captureSnapshotForSession(
session,
@@ -1,3 +1,4 @@
import { isMacOs } from '../../kernel/device.ts';
import type { DaemonResponse, SessionState } from '../types.ts';
import { errorResponse } from './response.ts';
@@ -5,7 +6,7 @@ export function unsupportedMacOsDesktopSurfaceInteraction(
session: SessionState,
command: 'click' | 'press' | 'fill' | 'longpress',
): DaemonResponse | null {
if (session.device.platform !== 'macos') {
if (!isMacOs(session.device)) {
return null;
}
if (session.surface !== 'desktop' && session.surface !== 'menubar') {
+2 -1
View File
@@ -1,4 +1,5 @@
import type { GestureReferenceFrame } from '../../core/scroll-gesture.ts';
import { publicPlatformString } from '../../kernel/device.ts';
import {
buttonTag,
getClickButtonValidationError,
@@ -98,7 +99,7 @@ async function dispatchTargetedTouchViaRuntime(
if (command !== 'longpress' && clickButton !== 'primary') {
const validationError = getClickButtonValidationError({
commandLabel,
platform: session.device.platform,
platform: publicPlatformString(session.device),
button: clickButton,
count: req.flags?.count,
intervalMs: req.flags?.intervalMs,
+2 -1
View File
@@ -1,3 +1,4 @@
import { isIosFamily } from '../../kernel/device.ts';
import { SessionStore } from '../session-store.ts';
import type { DaemonRequest, DaemonResponse, SessionState } from '../types.ts';
import { emitDiagnostic } from '../../utils/diagnostics.ts';
@@ -27,7 +28,7 @@ function findOtherActiveIosRunnerRecording(
.find(
(session) =>
session.name !== currentSessionName &&
session.device.platform === 'ios' &&
isIosFamily(session.device) &&
session.device.kind === 'device' &&
session.device.id === deviceId &&
session.recording?.platform === 'ios-device-runner',
@@ -1,3 +1,4 @@
import { isIosFamily, isMacOs } from '../../kernel/device.ts';
import fs from 'node:fs';
import path from 'node:path';
import type { DaemonRequest, DaemonResponse, SessionState } from '../types.ts';
@@ -75,9 +76,9 @@ export function resolveRecordingBackendForDevice(
): RecordingStartBackend {
if (device.platform === 'web') return webRecordingBackend;
if (device.platform === 'android') return androidRecordingBackend;
if (device.platform === 'macos') return macOsRecordingBackend;
if (device.platform === 'ios' && device.kind === 'device') return iosDeviceRecordingBackend;
if (device.platform === 'ios') return iosSimulatorRecordingBackend;
if (isMacOs(device)) return macOsRecordingBackend;
if (isIosFamily(device) && device.kind === 'device') return iosDeviceRecordingBackend;
if (isIosFamily(device)) return iosSimulatorRecordingBackend;
return unsupportedRecordingBackend;
}
+4 -5
View File
@@ -1,7 +1,7 @@
import fs from 'node:fs';
import { installProviderDeviceApp } from '../../provider-device-runtime.ts';
import { cleanupUploadedArtifact, prepareUploadedArtifact } from '../artifact-tracking.ts';
import type { DeviceInfo } from '../../kernel/device.ts';
import { isIosFamily, type DeviceInfo } from '../../kernel/device.ts';
import type { DaemonRequest, DaemonResponse } from '../types.ts';
import { SessionStore } from '../session-store.ts';
import { recordSessionAction } from './handler-utils.ts';
@@ -154,10 +154,9 @@ export async function handleAppDeployCommand(params: {
const unsupported = requireCommandSupported(command, device);
if (unsupported) return unsupported;
const result =
device.platform === 'ios'
? buildIosDeployResult(app, appPath, await deployOps.ios(device, app, appPath))
: buildAndroidDeployResult(app, appPath, await deployOps.android(device, app, appPath));
const result = isIosFamily(device)
? buildIosDeployResult(app, appPath, await deployOps.ios(device, app, appPath))
: buildAndroidDeployResult(app, appPath, await deployOps.android(device, app, appPath));
const data = withSuccessText(result, buildDeployMessage(result));
recordSessionAction(sessionStore, session, req, command, data);
+2 -2
View File
@@ -1,4 +1,4 @@
import type { DeviceInfo } from '../../kernel/device.ts';
import { isIosFamily, type DeviceInfo } from '../../kernel/device.ts';
import { AppError } from '../../kernel/errors.ts';
import { ensureDeviceReady } from '../device-ready.ts';
import { resolveTargetDevice } from '../../core/dispatch.ts';
@@ -57,7 +57,7 @@ export async function resolveCommandDevice(params: {
}
export async function refreshSessionDeviceIfNeeded(device: DeviceInfo): Promise<DeviceInfo> {
if (device.platform !== 'ios' || device.kind !== 'simulator') {
if (!isIosFamily(device) || device.kind !== 'simulator') {
return device;
}
if (process.platform !== 'darwin') {
+10 -4
View File
@@ -1,4 +1,9 @@
import type { DeviceInfo } from '../../kernel/device.ts';
import {
isIosFamily,
isMacOs,
publicPlatformString,
type DeviceInfo,
} from '../../kernel/device.ts';
import { AppError, normalizeError } from '../../kernel/errors.ts';
import type { SessionState } from '../types.ts';
import { appendDoctorCheck } from './session-doctor-output.ts';
@@ -19,8 +24,9 @@ export async function appendAppChecks(
appendDoctorCheck(checks, {
id: 'target-app',
status: 'info',
summary: `Target app installation checks are not supported for ${device.platform}.`,
evidence: { requested: targetApp, platform: device.platform },
// approach (b): emit the PUBLIC leaf platform (ios/macos), never the internal `apple`.
summary: `Target app installation checks are not supported for ${publicPlatformString(device)}.`,
evidence: { requested: targetApp, platform: publicPlatformString(device) },
});
return;
}
@@ -56,7 +62,7 @@ async function resolveInstalledAppForDoctor(
);
return match?.id;
}
if (device.platform === 'ios' || device.platform === 'macos') {
if (isIosFamily(device) || isMacOs(device)) {
const { listIosApps } = await import('../../platforms/apple/core/apps.ts');
const apps = await listIosApps(device, 'all');
const match = resolveUniqueInstalledAppMatch(
+3 -1
View File
@@ -10,6 +10,7 @@ import {
} from '../../core/platform-inventory.ts';
import {
matchesDeviceSelector,
publicPlatformString,
type DeviceInfo,
type DeviceTarget,
type Platform,
@@ -94,7 +95,8 @@ export function resolveDoctorDeviceForAppCheck(
evidence: {
targetApp,
booted: booted.map((device) => ({
platform: device.platform,
// approach (b): emit the PUBLIC leaf platform (ios/macos), never the internal `apple`.
platform: publicPlatformString(device),
id: device.id,
name: device.name,
})),
+16 -9
View File
@@ -3,6 +3,9 @@ import { assertResolvedAppsFilter } from '../../contracts/app-inventory.ts';
import { asAppError } from '../../kernel/errors.ts';
import {
isApplePlatform,
isMacOs,
matchesPlatformSelector,
publicPlatformString,
resolveAppleSimulatorSetPathForSelector,
type DeviceInfo,
type PlatformSelector,
@@ -40,17 +43,19 @@ export async function handleSessionInventoryCommands(params: {
name: session.name,
sessionStateDir,
runnerLogPath: resolveSessionRunnerLogPath(sessionStateDir),
platform: session.device.platform,
// approach (b): emit the PUBLIC leaf platform (ios/macos), not `apple`.
platform: publicPlatformString(session.device),
target: session.device.target ?? 'mobile',
surface: session.surface ?? 'app',
device: session.device.name,
id: session.device.id,
device_id: session.device.id,
createdAt: session.createdAt,
...(session.device.platform === 'ios' && {
device_udid: session.device.id,
ios_simulator_device_set: session.device.simulatorSetPath ?? null,
}),
...(isApplePlatform(session.device.platform) &&
!isMacOs(session.device) && {
device_udid: session.device.id,
ios_simulator_device_set: session.device.simulatorSetPath ?? null,
}),
};
}),
},
@@ -90,8 +95,12 @@ export async function handleSessionInventoryCommands(params: {
// Keep appleOs internal-only for now: it is discovery groundwork and the
// public `devices` shape is not yet meant to expose it. Surfacing it (so
// agents can tell iPad from iPhone) should be a deliberate later change.
// approach (b): project `platform` back to the PUBLIC leaf (ios/macos).
const publicDevices = filtered.map(
({ simulatorSetPath: _simulatorSetPath, appleOs: _appleOs, ...device }) => device,
({ simulatorSetPath: _simulatorSetPath, appleOs, ...device }) => ({
...device,
platform: publicPlatformString({ platform: device.platform, appleOs }),
}),
);
return { ok: true, data: { devices: publicDevices } };
} catch (err) {
@@ -145,7 +154,5 @@ function matchesRequestedPlatform(
device: DeviceInfo,
requestedPlatform: PlatformSelector | undefined,
): boolean {
if (!requestedPlatform) return true;
if (requestedPlatform === 'apple') return isApplePlatform(device.platform);
return device.platform === requestedPlatform;
return matchesPlatformSelector(device, requestedPlatform);
}
+10 -5
View File
@@ -1,6 +1,11 @@
import { parseSessionSurface, type SessionSurface } from '../../core/session-surface.ts';
import { resolveFrontmostMacOsApp } from '../../platforms/apple/os/macos/helper.ts';
import type { DeviceInfo } from '../../kernel/device.ts';
import {
isIosFamily,
isMacOs,
publicPlatformString,
type DeviceInfo,
} from '../../kernel/device.ts';
import type { SessionRuntimeHints, SessionState } from '../types.ts';
import { AppError } from '../../kernel/errors.ts';
import { successText } from '../../utils/success-text.ts';
@@ -49,7 +54,7 @@ export function buildOpenResult(params: {
result.runtime = runtime;
}
if (device) {
result.platform = device.platform;
result.platform = publicPlatformString(device);
result.target = device.target ?? 'mobile';
result.device = device.name;
result.id = device.id;
@@ -58,7 +63,7 @@ export function buildOpenResult(params: {
result.serial = device.id;
}
}
if (device?.platform === 'ios') {
if (device && isIosFamily(device)) {
result.device_udid = device.id;
result.ios_simulator_device_set = device.simulatorSetPath ?? null;
}
@@ -133,7 +138,7 @@ function resolveOpenSurface(
}
return surface;
}
if (device.platform !== 'macos') {
if (!isMacOs(device)) {
if (surfaceFlag) {
throw new AppError('INVALID_ARGS', 'surface is only supported on macOS and Linux');
}
@@ -153,7 +158,7 @@ export function resolveRequestedOpenSurface(params: {
existingSurface?: SessionSurface;
}): SessionSurface {
const { device, surfaceFlag, openTarget, existingSurface } = params;
if ((device.platform === 'macos' || device.platform === 'linux') && !surfaceFlag) {
if ((isMacOs(device) || device.platform === 'linux') && !surfaceFlag) {
return existingSurface ?? 'app';
}
return resolveOpenSurface(device, surfaceFlag, openTarget);
+2 -2
View File
@@ -3,7 +3,7 @@ import {
isWebUrl,
resolveIosDeviceDeepLinkBundleId,
} from '../../core/open-target.ts';
import { isApplePlatform, type DeviceInfo } from '../../kernel/device.ts';
import { isMacOs, isApplePlatform, type DeviceInfo } from '../../kernel/device.ts';
async function resolveIosBundleIdForOpen(
device: DeviceInfo,
@@ -12,7 +12,7 @@ async function resolveIosBundleIdForOpen(
): Promise<string | undefined> {
if (!isApplePlatform(device.platform) || !openTarget) return undefined;
if (isDeepLinkTarget(openTarget)) {
if (device.platform === 'macos') return undefined;
if (isMacOs(device)) return undefined;
if (device.kind === 'device') {
return resolveIosDeviceDeepLinkBundleId(currentAppBundleId, openTarget);
}
+2 -2
View File
@@ -12,7 +12,7 @@ import {
createAppleRunnerCacheColdBootPrewarmForOpen,
} from '../apple-runner-options.ts';
import { applyRuntimeHintsToApp } from '../runtime-hints.ts';
import type { DeviceInfo } from '../../kernel/device.ts';
import { isIosFamily, type DeviceInfo } from '../../kernel/device.ts';
import type { DaemonRequest, DaemonResponse, SessionRuntimeHints, SessionState } from '../types.ts';
import {
resolveSessionRequestLogPath,
@@ -187,7 +187,7 @@ async function completeOpenCommand(params: {
});
timing.runtimeHintsDurationMs = Math.max(0, Date.now() - runtimeHintsStartedAtMs);
const shouldPrewarmIosRunner =
device.platform === 'ios' && surface === 'app' && openPositionals.length > 0;
isIosFamily(device) && surface === 'app' && openPositionals.length > 0;
const runnerPrewarmOptions = buildAppleRunnerSessionOptions({
req,
logPath,
+4 -4
View File
@@ -1,7 +1,7 @@
import path from 'node:path';
import type { SessionAction, SessionState } from '../types.ts';
import { AppError, normalizeError } from '../../kernel/errors.ts';
import { isApplePlatform } from '../../kernel/device.ts';
import { isApplePlatform, publicPlatformString } from '../../kernel/device.ts';
import { tryGetPlugin } from '../../core/platform-plugin/plugin.ts';
import { registerBuiltinPlatformPlugins } from '../../core/interactors/register-builtins.ts';
import type { AndroidAdbExecutor } from '../../platforms/android/adb-executor.ts';
@@ -207,7 +207,7 @@ function buildBasePerfResponse(session: SessionState): PerfResponseData {
};
return {
session: session.name,
platform: session.device.platform,
platform: publicPlatformString(session.device),
device: session.device.name,
deviceId: session.device.id,
metrics: {
@@ -244,7 +244,7 @@ function buildDefaultUnavailableFrameMetric(): Record<string, unknown> {
function buildBasePerfFramesResponse(session: SessionState): PerfFramesResponseData {
return {
session: session.name,
platform: session.device.platform,
platform: publicPlatformString(session.device),
device: session.device.name,
deviceId: session.device.id,
metrics: {
@@ -259,7 +259,7 @@ function buildBasePerfFramesResponse(session: SessionState): PerfFramesResponseD
function buildBasePerfMemoryResponse(session: SessionState): PerfMemoryResponseData {
return {
session: session.name,
platform: session.device.platform,
platform: publicPlatformString(session.device),
device: session.device.name,
deviceId: session.device.id,
sampling: {
+13 -5
View File
@@ -1,4 +1,5 @@
import type { DaemonRequest, DaemonResponse } from '../types.ts';
import { publicPlatformString } from '../../kernel/device.ts';
import { SessionStore } from '../session-store.ts';
import { clearRuntimeHintsFromApp, hasRuntimeTransportHints } from '../runtime-hints.ts';
import { errorResponse } from './response.ts';
@@ -92,6 +93,12 @@ function showRuntimeCommand(
};
}
function sessionLeafPlatform(
session: ReturnType<SessionStore['get']>,
): ReturnType<typeof publicPlatformString> | undefined {
return session ? publicPlatformString(session.device) : undefined;
}
function setRuntimeCommand(params: {
req: DaemonRequest;
sessionName: string;
@@ -100,19 +107,20 @@ function setRuntimeCommand(params: {
current: ReturnType<SessionStore['getRuntimeHints']>;
}): DaemonResponse {
const { req, sessionName, sessionStore, session, current } = params;
const platform = toRuntimePlatform(
req.flags?.platform ?? current?.platform ?? session?.device.platform,
);
// approach (b): resolve the session's PUBLIC leaf platform (ios/macos), never the
// internal `apple`, so the legacy `--platform ios` selector still matches.
const sessionLeaf = sessionLeafPlatform(session);
const platform = toRuntimePlatform(req.flags?.platform ?? current?.platform ?? sessionLeaf);
if (!platform) {
return errorResponse(
'INVALID_ARGS',
'runtime set only supports iOS and Android sessions. Pass --platform ios|android or open an iOS/Android session first.',
);
}
if (session && session.device.platform !== platform) {
if (sessionLeaf !== undefined && sessionLeaf !== platform) {
return errorResponse(
'INVALID_ARGS',
`runtime set targets ${platform}, but session "${sessionName}" is already bound to ${session.device.platform}.`,
`runtime set targets ${platform}, but session "${sessionName}" is already bound to ${sessionLeaf}.`,
);
}
const nextRuntime = mergeRuntimeHints(current, buildRuntimeHints(req.flags, platform));
+3 -3
View File
@@ -1,5 +1,5 @@
import { AppError, asAppError } from '../../kernel/errors.ts';
import type { DeviceInfo } from '../../kernel/device.ts';
import { publicPlatformString, type DeviceInfo } from '../../kernel/device.ts';
import type { CommandFlags } from '../../core/dispatch.ts';
import type { DaemonRequest, SessionRuntimeHints, SessionState } from '../types.ts';
import { SessionStore } from '../session-store.ts';
@@ -163,7 +163,7 @@ function resolveSessionRuntimeHints(
): SessionRuntimeHints | undefined {
const runtime = sessionStore.getRuntimeHints(sessionName);
if (!runtime) return undefined;
const boundPlatform = device?.platform;
const boundPlatform = device ? publicPlatformString(device) : undefined;
const deviceRuntimePlatform = toRuntimePlatform(boundPlatform);
if (runtime.platform && device && !deviceRuntimePlatform) {
throw new AppError(
@@ -198,7 +198,7 @@ function resolveOpenRuntimeHints(params: {
const explicitRuntime = normalizeExplicitRuntimeHints({
runtime: req.runtime,
sessionName,
platform: toRuntimePlatform(device.platform),
platform: toRuntimePlatform(publicPlatformString(device)),
});
if (req.runtime === undefined) {
return {
+18 -12
View File
@@ -1,5 +1,11 @@
import { asAppError } from '../../kernel/errors.ts';
import { isApplePlatform, type DeviceInfo } from '../../kernel/device.ts';
import {
isApplePlatform,
isIosFamily,
isMacOs,
publicPlatformString,
type DeviceInfo,
} from '../../kernel/device.ts';
import type { DaemonRequest, DaemonResponse } from '../types.ts';
import { SessionStore } from '../session-store.ts';
import { ensureDeviceReady } from '../device-ready.ts';
@@ -65,7 +71,7 @@ async function handleAppStateCommand(params: {
const appName = session.appName ?? session.appBundleId;
if (!session.appName && !session.appBundleId) {
if (
session.device.platform === 'macos' &&
isMacOs(session.device) &&
session.surface &&
session.surface !== 'app' &&
session.surface !== 'frontmost-app'
@@ -73,7 +79,7 @@ async function handleAppStateCommand(params: {
return {
ok: true,
data: {
platform: session.device.platform,
platform: publicPlatformString(session.device),
appName: session.surface,
appBundleId: session.appBundleId,
source: 'session',
@@ -82,7 +88,7 @@ async function handleAppStateCommand(params: {
};
}
const sessionPlatform = session.device.platform === 'macos' ? 'macOS' : 'iOS';
const sessionPlatform = isMacOs(session.device) ? 'macOS' : 'iOS';
return errorResponse(
'COMMAND_FAILED',
`No foreground app is tracked for this ${sessionPlatform} session. Open an app in the session, then retry appstate.`,
@@ -92,12 +98,12 @@ async function handleAppStateCommand(params: {
return {
ok: true,
data: {
platform: session.device.platform,
platform: publicPlatformString(session.device),
appName: appName ?? 'unknown',
appBundleId: session.appBundleId,
source: 'session',
surface: session.surface ?? 'app',
...(session.device.platform === 'ios'
...(isIosFamily(session.device)
? {
device_udid: session.device.id,
ios_simulator_device_set: session.device.simulatorSetPath ?? null,
@@ -112,10 +118,10 @@ async function handleAppStateCommand(params: {
flags,
ensureReady: true,
});
if (device.platform === 'ios') {
if (isIosFamily(device)) {
return errorResponse('SESSION_NOT_FOUND', IOS_APPSTATE_SESSION_REQUIRED_MESSAGE);
}
if (device.platform === 'macos') {
if (isMacOs(device)) {
return errorResponse('SESSION_NOT_FOUND', MACOS_APPSTATE_SESSION_REQUIRED_MESSAGE);
}
if (device.platform === 'web') {
@@ -265,7 +271,7 @@ export async function handleSessionStateCommands(params: {
return {
ok: true,
data: {
platform: device.platform,
platform: publicPlatformString(device),
target: device.target ?? 'mobile',
device: device.name,
id: device.id,
@@ -302,7 +308,7 @@ export async function handleSessionStateCommands(params: {
{
hint: `Run agent-device close --shutdown --session ${sessionName}`,
session: sessionName,
platform: device.platform,
platform: publicPlatformString(device),
target: device.target ?? 'mobile',
device: device.name,
id: device.id,
@@ -317,7 +323,7 @@ export async function handleSessionStateCommands(params: {
shutdown.error?.code ?? 'COMMAND_FAILED',
shutdownFailureMessage(shutdown),
{
platform: device.platform,
platform: publicPlatformString(device),
target: device.target ?? 'mobile',
device: device.name,
id: device.id,
@@ -330,7 +336,7 @@ export async function handleSessionStateCommands(params: {
return {
ok: true,
data: {
platform: device.platform,
platform: publicPlatformString(device),
target: device.target ?? 'mobile',
device: device.name,
id: device.id,
+2 -2
View File
@@ -180,12 +180,12 @@ function resolveExplicitShardDevices(
function isImplicitShardDevice(device: DeviceInfo, flags: CommandFlags | undefined): boolean {
if (!isShardDeviceCandidate(device, flags)) return false;
if (!isMobilePlatform(device.platform)) return false;
if (!isMobilePlatform(device)) return false;
return device.booted !== false;
}
function isShardDeviceCandidate(device: DeviceInfo, flags: CommandFlags | undefined): boolean {
if (!matchesPlatformSelector(device.platform, flags?.platform)) return false;
if (!matchesPlatformSelector(device, flags?.platform)) return false;
if (flags?.target && (device.target ?? 'mobile') !== flags.target) return false;
return true;
}
+3 -3
View File
@@ -7,7 +7,7 @@ import {
type PrepareIosRunnerResult,
} from '../../platforms/apple/core/runner/runner-client.ts';
import type { DeviceInfo } from '../../kernel/device.ts';
import { isApplePlatform } from '../../kernel/device.ts';
import { isApplePlatform, publicPlatformString } from '../../kernel/device.ts';
import type { DaemonInvokeFn, DaemonRequest, DaemonResponse, SessionState } from '../types.ts';
import { SessionStore } from '../session-store.ts';
import { contextFromFlags } from '../context.ts';
@@ -120,7 +120,7 @@ function prepareIosRunnerResponseData(
): Record<string, unknown> {
return {
action,
platform: device.platform,
platform: publicPlatformString(device),
deviceId: device.id,
deviceName: device.name,
kind: device.kind,
@@ -221,7 +221,7 @@ async function handleClipboardCommand(params: {
},
);
recordSessionAction(sessionStore, session, req, req.command, result ?? {});
return { ok: true, data: { platform: device.platform, ...(result ?? {}) } };
return { ok: true, data: { platform: publicPlatformString(device), ...(result ?? {}) } };
}
// fallow-ignore-next-line complexity
+2 -1
View File
@@ -1,3 +1,4 @@
import { isMacOs } from '../../kernel/device.ts';
import {
ALERT_ACTION_RETRY_MS,
ALERT_POLL_INTERVAL_MS as POLL_INTERVAL_MS,
@@ -56,7 +57,7 @@ export async function handleAlertCommand(
}),
);
}
if (device.platform === 'macos') {
if (isMacOs(device)) {
const runAlert: NativeAlertRunner = async (alertAction) =>
await runMacOsAlertAction(alertAction, macOsAlertTarget);
return await handleNativeAlertCommand(params, action, runAlert);
+5 -4
View File
@@ -1,5 +1,5 @@
import { dispatchCommand, type CommandFlags } from '../../core/dispatch.ts';
import { isMobilePlatform } from '../../kernel/device.ts';
import { isMacOs, isMobilePlatform, publicPlatformString } from '../../kernel/device.ts';
import { sleep } from '../../utils/timeouts.ts';
import { runMacOsSnapshotAction } from '../../platforms/apple/os/macos/helper.ts';
import { snapshotLinux } from '../../platforms/linux/snapshot.ts';
@@ -106,7 +106,7 @@ async function capturePostActionAwareSnapshot(
pendingInteractionOutcome,
);
}
if (isMobilePlatform(params.device.platform) && params.session?.postGestureStabilization) {
if (isMobilePlatform(params.device) && params.session?.postGestureStabilization) {
return await capturePostGestureAwareSnapshot({ ...params, session: params.session });
}
const freshness = getActiveAndroidSnapshotFreshness(params.session);
@@ -209,7 +209,7 @@ export async function captureSnapshotData(params: CaptureSnapshotParams): Promis
},
);
}
if (device.platform === 'macos' && session?.surface && session.surface !== 'app') {
if (isMacOs(device) && session?.surface && session.surface !== 'app') {
const helperSnapshot = await runMacOsSnapshotAction(session.surface, {
bundleId: session.surface === 'menubar' ? session.appBundleId : undefined,
});
@@ -312,7 +312,8 @@ async function captureSnapshotAttempt(params: CaptureSnapshotParams): Promise<Sn
recordSnapshotTiming(params.session, {
durationMs: Date.now() - startedAt,
backend: data.backend,
platform: params.device.platform,
// approach (b): emit the PUBLIC leaf platform (ios/macos), never the internal `apple`.
platform: publicPlatformString(params.device),
});
return {
data,
+2 -1
View File
@@ -1,3 +1,4 @@
import { isIosFamily } from '../../kernel/device.ts';
import { resolveTargetDevice } from '../../core/dispatch.ts';
import {
resolveRunnerAppBundleId,
@@ -25,7 +26,7 @@ export async function withSessionlessRunnerCleanup<T>(
device: SessionState['device'],
task: () => Promise<T>,
): Promise<T> {
const shouldCleanupSessionlessIosRunner = !session && device.platform === 'ios';
const shouldCleanupSessionlessIosRunner = !session && isIosFamily(device);
try {
return await task();
} finally {
+2 -1
View File
@@ -1,3 +1,4 @@
import { isMacOs } from '../../kernel/device.ts';
import {
getUnsupportedMacOsSettingMessage,
isMacOsSettingSupported,
@@ -79,7 +80,7 @@ export async function handleSettingsCommand(
} = parsed;
const unsupported = requireCommandSupported('settings', device);
if (unsupported) return unsupported;
if (device.platform === 'macos' && !isMacOsSettingSupported(setting)) {
if (isMacOs(device) && !isMacOsSettingSupported(setting)) {
return errorResponse('INVALID_ARGS', getUnsupportedMacOsSettingMessage(setting));
}
+1 -1
View File
@@ -188,7 +188,7 @@ export function areInteractionSurfaceSignaturesStable(
}
function supportsInteractionOutcomePolicy(session: SessionState): boolean {
return isMobilePlatform(session.device.platform);
return isMobilePlatform(session.device);
}
function retryCommandForTap(command: string): string | undefined {
+4 -4
View File
@@ -19,7 +19,7 @@ export function markPostGestureStabilization(
positionals: string[] = [],
flags?: CommandFlags,
): void {
if (!supportsPostGestureStabilization(session.device.platform)) return;
if (!supportsPostGestureStabilization(session.device)) return;
if (!isPostGestureStabilizingAction(action, positionals, flags)) return;
session.postGestureStabilization = {
action,
@@ -40,7 +40,7 @@ export async function capturePostGestureStabilizedResult<T>(params: {
}): Promise<T> {
const { session, capture } = params;
const pending = session?.postGestureStabilization;
if (!session || !supportsPostGestureStabilization(session.device.platform) || !pending) {
if (!session || !supportsPostGestureStabilization(session.device) || !pending) {
return params.initial ?? (await capture());
}
@@ -97,6 +97,6 @@ function isPostGestureStabilizingAction(
return action === 'gesture' && positionals[0] === 'swipe';
}
function supportsPostGestureStabilization(platform: SessionState['device']['platform']): boolean {
return isMobilePlatform(platform);
function supportsPostGestureStabilization(device: SessionState['device']): boolean {
return isMobilePlatform(device);
}
+2 -1
View File
@@ -1,3 +1,4 @@
import { isIosFamily } from '../kernel/device.ts';
import type { RecordingGestureEvent, SessionState } from './types.ts';
import type { SnapshotState } from '../kernel/snapshot.ts';
import {
@@ -58,7 +59,7 @@ export function recordTouchVisualizationEvent(
fallbackFinishedAtMs: finishedAtMs,
});
const tMs =
session.device.platform === 'ios' &&
isIosFamily(session.device) &&
readNumber(merged.gestureStartUptimeMs) === undefined &&
shouldAnchorTapVisualizationNearCompletion(command, merged)
? resolveTapVisualizationOffsetMs({ ...timingSource, gestureDurationMs })
+4 -2
View File
@@ -7,7 +7,7 @@ import {
type SessionSelectorConflict,
type SessionSelectorConflictKey,
} from './session-selector.ts';
import { isApplePlatform, type PlatformSelector } from '../kernel/device.ts';
import { isApplePlatform, publicPlatformString, type PlatformSelector } from '../kernel/device.ts';
import { buildSessionRecoveryHint, describeSessionDevice } from './session-recovery-hints.ts';
import { shellQuoteIfNeeded } from '../utils/shell-quote.ts';
import { hasLockableDeviceSelector, hasSelectorValue } from './device-selector-intent.ts';
@@ -124,7 +124,9 @@ function applyStripLockPolicy(
): void {
if (existingSession) {
stripSessionConflicts(flags, conflicts);
flags.platform = existingSession.device.platform;
// approach (b): backfill the request selector with the PUBLIC leaf platform, not
// the internal `apple` — the leaf selector still matches the session's Apple device.
flags.platform = publicPlatformString(existingSession.device);
return;
}
stripSessionConflicts(flags, conflicts);
+2 -1
View File
@@ -1,3 +1,4 @@
import { isIosFamily } from '../kernel/device.ts';
import { getRunnerSessionSnapshot } from '../platforms/apple/core/runner/runner-client.ts';
import type { SessionState } from './types.ts';
@@ -27,7 +28,7 @@ export function refreshRecordingHealth(session: SessionState): void {
function recordingRequiresRunnerHealth(session: SessionState): boolean {
const recording = session.recording;
if (!recording || session.device.platform !== 'ios') return false;
if (!recording || !isIosFamily(session.device)) return false;
if (recording.platform === 'ios') return false;
return recording.showTouches !== false;
}
+3 -3
View File
@@ -1,4 +1,4 @@
import type { DeviceInfo } from '../kernel/device.ts';
import { isIosFamily, type DeviceInfo } from '../kernel/device.ts';
import { AppError, asAppError } from '../kernel/errors.ts';
import type { SessionRuntimeHints } from './types.ts';
import {
@@ -54,7 +54,7 @@ export async function applyRuntimeHintsToApp(params: {
return;
}
if (device.platform === 'ios' && device.kind === 'simulator') {
if (isIosFamily(device) && device.kind === 'simulator') {
await applyIosSimulatorRuntimeHints(device, appId, transport);
}
}
@@ -71,7 +71,7 @@ export async function clearRuntimeHintsFromApp(params: {
return;
}
if (device.platform === 'ios' && device.kind === 'simulator') {
if (isIosFamily(device) && device.kind === 'simulator') {
await clearIosSimulatorRuntimeHints(device, appId);
}
}
+3 -2
View File
@@ -1,3 +1,4 @@
import { isIosFamily, publicPlatformString } from '../kernel/device.ts';
import { promises as fs } from 'node:fs';
import os from 'node:os';
import path from 'node:path';
@@ -51,7 +52,7 @@ function createDispatchScreenshotBackend(params: {
}): AgentDeviceBackend {
const { session, outputPlacement, dispatchContext } = params;
return {
platform: session.device.platform,
platform: publicPlatformString(session.device),
captureScreenshot: async (_context, outPath, options) => {
const context = {
...dispatchContext,
@@ -59,7 +60,7 @@ function createDispatchScreenshotBackend(params: {
surface: options?.surface,
skipIosSimulatorBootCheck:
dispatchContext.skipIosSimulatorBootCheck ??
(session.device.platform === 'ios' && session.device.kind === 'simulator'),
(isIosFamily(session.device) && session.device.kind === 'simulator'),
};
if (outputPlacement === 'out') {
return readScreenshotResultData(
+3 -3
View File
@@ -5,7 +5,7 @@ import type {
} from '../backend.ts';
import { resolveTargetDevice, type CommandFlags } from '../core/dispatch.ts';
import { createAgentDevice } from '../runtime.ts';
import { isApplePlatform } from '../kernel/device.ts';
import { isMacOs, isApplePlatform, publicPlatformString } from '../kernel/device.ts';
import { noActiveSessionError, requireCommandSupported } from './handlers/response.ts';
import type { SnapshotNode } from '../kernel/snapshot.ts';
import { findNodeByLabel } from '../snapshot/snapshot-processing.ts';
@@ -101,7 +101,7 @@ function createSelectorBackend(params: SelectorRuntimeDeviceParams): AgentDevice
logPath,
});
return {
platform: device.platform,
platform: publicPlatformString(device),
captureSnapshot: async (_context, options): Promise<BackendSnapshotResult> => {
const flags = {
...req.flags,
@@ -166,7 +166,7 @@ async function findTextInMacosNonAppSurface(
params: SelectorRuntimeDeviceParams,
text: string,
): Promise<boolean | null> {
if (params.device.platform !== 'macos') return null;
if (!isMacOs(params.device)) return null;
if (!params.session?.surface || params.session.surface === 'app') return null;
return await findTextInWaitSnapshot(params, text);
}
+7 -3
View File
@@ -1,4 +1,4 @@
import type { Platform } from '../kernel/device.ts';
import type { Platform, PublicPlatform } from '../kernel/device.ts';
import type { SnapshotNode } from '../kernel/snapshot.ts';
import { isNodeEditable, isNodeVisible } from '../utils/selector-node.ts';
import { extractNodeText, normalizeType } from '../snapshot/snapshot-processing.ts';
@@ -10,12 +10,16 @@ export { isNodeEditable, isNodeVisible } from '../utils/selector-node.ts';
export function matchesSelector(
node: SnapshotNode,
selector: Selector,
platform: Platform,
platform: Platform | PublicPlatform,
): boolean {
return selector.terms.every((term) => matchesTerm(node, term, platform));
}
function matchesTerm(node: SnapshotNode, term: SelectorTerm, platform: Platform): boolean {
function matchesTerm(
node: SnapshotNode,
term: SelectorTerm,
platform: Platform | PublicPlatform,
): boolean {
switch (term.key) {
case 'id':
return textEquals(node.identifier, String(term.value));
+5 -5
View File
@@ -1,4 +1,4 @@
import type { Platform } from '../kernel/device.ts';
import type { Platform, PublicPlatform } from '../kernel/device.ts';
import type { SnapshotNode, SnapshotState } from '../kernel/snapshot.ts';
import { matchesSelector } from './selectors-match.ts';
import type { Selector, SelectorChain } from './selectors-parse.ts';
@@ -20,7 +20,7 @@ export function resolveSelectorChain(
nodes: SnapshotState['nodes'],
chain: SelectorChain,
options: {
platform: Platform;
platform: Platform | PublicPlatform;
requireRect?: boolean;
requireUnique?: boolean;
disambiguateAmbiguous?: boolean;
@@ -58,7 +58,7 @@ export function findSelectorChainMatch(
nodes: SnapshotState['nodes'],
chain: SelectorChain,
options: {
platform: Platform;
platform: Platform | PublicPlatform;
requireRect?: boolean;
},
): {
@@ -96,7 +96,7 @@ export function formatSelectorFailure(
function analyzeSelectorMatches(
nodes: SnapshotState['nodes'],
selector: Selector,
platform: Platform,
platform: Platform | PublicPlatform,
requireRect: boolean,
): { count: number; firstNode: SnapshotNode | null; disambiguated: SnapshotNode | null } {
let count = 0;
@@ -130,7 +130,7 @@ function analyzeSelectorMatches(
function countSelectorMatchesOnly(
nodes: SnapshotState['nodes'],
selector: Selector,
platform: Platform,
platform: Platform | PublicPlatform,
requireRect: boolean,
): number {
let count = 0;
+3 -1
View File
@@ -1,5 +1,6 @@
import fs from 'node:fs';
import path from 'node:path';
import { publicPlatformString } from '../kernel/device.ts';
import { inferFillText } from './action-utils.ts';
import { emitDiagnostic } from '../utils/diagnostics.ts';
import { formatPortableActionLine } from '../replay/script-formatting.ts';
@@ -157,7 +158,8 @@ function formatScript(session: SessionState, actions: SessionAction[]): string {
const kind = session.device.kind ? ` kind=${session.device.kind}` : '';
const theme = 'unknown';
lines.push(
`context platform=${session.device.platform} device=${formatScriptStringLiteral(session.device.name)}${kind} theme=${theme}`,
// approach (b): emit the PUBLIC leaf platform (ios/macos), never the internal `apple`.
`context platform=${publicPlatformString(session.device)} device=${formatScriptStringLiteral(session.device.name)}${kind} theme=${theme}`,
);
for (const action of actions) {
if (action.flags?.noRecord) continue;
+4 -4
View File
@@ -1,7 +1,7 @@
import { AppError } from '../kernel/errors.ts';
import type { CommandFlags } from '../core/dispatch.ts';
import type { SessionState } from './types.ts';
import { matchesPlatformSelector } from '../kernel/device.ts';
import { isIosFamily, matchesPlatformSelector } from '../kernel/device.ts';
import { parseSerialAllowlist } from '../utils/device-isolation.ts';
import { buildSessionRecoveryHint, describeSessionDevice } from './session-recovery-hints.ts';
@@ -44,14 +44,14 @@ export function listSessionSelectorConflicts(
const device = session.device;
const normalizedPlatform = flags.platform;
if (normalizedPlatform && !matchesPlatformSelector(device.platform, normalizedPlatform)) {
if (normalizedPlatform && !matchesPlatformSelector(device, normalizedPlatform)) {
mismatches.push({ key: 'platform', value: flags.platform! });
}
if (flags.target && flags.target !== (device.target ?? 'mobile')) {
mismatches.push({ key: 'target', value: flags.target });
}
if (flags.udid && (device.platform !== 'ios' || flags.udid !== device.id)) {
if (flags.udid && (!isIosFamily(device) || flags.udid !== device.id)) {
mismatches.push({ key: 'udid', value: flags.udid });
}
@@ -67,7 +67,7 @@ export function listSessionSelectorConflicts(
const requestedSetPath = flags.iosSimulatorDeviceSet.trim();
const sessionSetPath = device.simulatorSetPath?.trim();
if (
device.platform !== 'ios' ||
!isIosFamily(device) ||
device.kind !== 'simulator' ||
requestedSetPath !== sessionSetPath
) {
+2 -2
View File
@@ -1,5 +1,5 @@
import { emitDiagnostic } from '../utils/diagnostics.ts';
import { isApplePlatform } from '../kernel/device.ts';
import { isMacOs, isApplePlatform } from '../kernel/device.ts';
import { runMacOsAlertAction } from '../platforms/apple/os/macos/helper.ts';
import { stopAppLog } from './app-log.ts';
import { stopIosRunnerSession } from '../platforms/apple/core/runner/runner-client.ts';
@@ -14,7 +14,7 @@ export { stopSessionAudioProbe } from './audio-probe.ts';
export async function stopAppleRunnerForClose(session: SessionState): Promise<void> {
await stopIosRunnerSession(session.device.id);
if (session.device.platform !== 'macos') {
if (!isMacOs(session.device)) {
return;
}
+3 -2
View File
@@ -1,3 +1,4 @@
import { isIosFamily, publicPlatformString } from '../kernel/device.ts';
import type { AgentDeviceBackend, BackendSnapshotResult } from '../backend.ts';
import type { CommandSessionRecord } from '../runtime.ts';
import { createAgentDevice } from '../runtime.ts';
@@ -165,7 +166,7 @@ function requireIosAppSessionForSnapshot(
session: SessionState | undefined,
device: SessionState['device'],
): DaemonResponse | null {
if (device.platform !== 'ios' || session?.appBundleId) {
if (!isIosFamily(device) || session?.appBundleId) {
return null;
}
return errorResponse(
@@ -275,7 +276,7 @@ function createDaemonSnapshotBackend(params: {
}): AgentDeviceBackend {
const { req, logPath, session, device, snapshotScope } = params;
return {
platform: device.platform,
platform: publicPlatformString(device),
captureSnapshot: async (_context, options): Promise<BackendSnapshotResult> => {
const capture = await captureSnapshot({
device,

Some files were not shown because too many files have changed in this diff Show More