mirror of
https://github.com/callstack/agent-device.git
synced 2026-09-14 20:06:34 +08:00
* 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:
committed by
GitHub
parent
d1b56c0897
commit
cd1551bf42
@@ -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',
|
||||
|
||||
@@ -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
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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`);
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -18,7 +18,7 @@ function makeSession(name: string): SessionState {
|
||||
return {
|
||||
name,
|
||||
device: {
|
||||
platform: 'ios',
|
||||
platform: 'apple',
|
||||
id: 'sim-1',
|
||||
name: 'iPhone',
|
||||
kind: 'simulator',
|
||||
|
||||
@@ -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
@@ -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,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) =>
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
})),
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user