Skip to content

Commit cd1551b

Browse files
authored
refactor: collapse public Platform ios/macos into apple (#979) (#1002)
* refactor: collapse public Platform ios/macos into apple (#979) Phase 3 d.3: collapse the internal `Platform` union from `ios`/`macos` to a single `apple` platform, with `appleOs` as the sole OS discriminant. Approach (b) NON-BREAKING: the daemon still ACCEPTS the legacy `ios`/`macos` selectors on every read path and still EMITS the leaf `ios`/`macos` strings on every output, so machine consumers see no change. Kernel (src/kernel/device.ts): - PLATFORMS = ['apple','android','linux','web']; add PUBLIC_PLATFORMS (leaf) and PublicPlatform; PLATFORM_SELECTORS keeps legacy `ios`/`macos` as input aliases. - New predicates: isMacOs (appleOs- or legacy-leaf-based), isIosFamily (the post-collapse equivalent of `platform === 'ios'`), publicPlatformString (output projection), deviceFieldsFromPublicPlatform (inverse), isPublicPlatform. - isMobilePlatform and matchesPlatformSelector are now device-aware (appleOs). Discovery now stamps `platform: 'apple'` (+ appleOs); ~125 internal `device.platform === 'ios'|'macos'` branch sites migrated to the predicates, behavior-preserving. Apple plugin owns `['apple']`; platformDescriptors collapse to one `apple` row. Output projection (approach b) emits the leaf via publicPlatformString at: devices / session_list (session-inventory), boot / shutdown / appstate / prepare-ios-runner (session-state, session), the selector/backend platform (selector-runtime/screenshot-runtime/snapshot-runtime/interaction-runtime), proxy device key, request-lock backfill, runtime-set binding, click-button validation, and both `.ad` context-line writers. Contracts/client keep leaf types (PublicPlatform); read paths (parsePlatform, REPLAY_METADATA_PLATFORMS, matchesPlatformSelector) accept `apple` + legacy leaves. Adds a parity test gate (platform-collapse-parity.test.ts). Refs #979 (part of #972). * fix: project platform to the public leaf at open/perf response sites (#979) The Platform collapse left two daemon response builders emitting the raw internal `device.platform` ('apple'), which the client normalizer rejects (isPublicPlatform excludes 'apple') — dropping the resolved device from the response: - session-open-surface.ts: `open` result `platform`/device projection. - session-perf.ts: the perf/frames/memory base response builders. Both now go through `publicPlatformString(device)`, so output stays the leaf `ios`/`macos` per approach (b). (The android/non-apple perf branches were already leaf-safe.) Also update macos-desktop provider test: the lifecycle mock observes the INTERNAL DeviceInfo, which is now `platform:'apple'` (+ appleOs:'macos'), so the recorded tag is `prepare:apple:desktop`. Fixes the provider-integration assertions that blocked both the Integration Tests and Coverage CI jobs (both run the provider-integration project). Verified: provider-integration 82/82, coverage passes, tsc/oxlint/oxfmt/layering/ fallow green. * fix: project platform to the public leaf at nested output sites (#979) The Platform collapse (approach b) projects device.platform through publicPlatformString at emit sites so machine consumers keep seeing the leaf ios/macos and never the internal `apple`. Several nested output fields were missed. Project them and narrow their emitted types to PublicPlatform: - Apple perf memory snapshot support (buildAppleMemorySnapshotSupport) — response.support.platform / artifact.support.platform, plus the sibling sampleAppleFramePerf error data. - Apple xctrace perf capture/result platform surfaced in the perf cpu-profile started/stopped response data. - snapshotDiagnostics.stats.platform (recordSnapshotTiming) surfaced in snapshot/test response data and the slow-snapshot warning string. - doctor target-app evidence.platform + human summary, and doctor target-app-device evidence.booted[].platform. - provider/cloud UNSUPPORTED_OPERATION error.data.platform for cloud Apple devices (reachable via deviceFieldsFromPublicPlatform). Internal 'apple' emissions (selector-matching input, diagnostic emitDiagnostic telemetry, session appLog state, replay .ad flags) are left as-is. Adds focused tests pinning Apple perf memory support to the leaf and a guard asserting no emitted platform field equals 'apple'.
1 parent d1b56c0 commit cd1551b

148 files changed

Lines changed: 1072 additions & 674 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎src/__tests__/client-normalizers.test.ts‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import { test } from 'vitest';
22
import assert from 'node:assert/strict';
33
import { normalizeOpenDevice } from '../client/client-normalizers.ts';
4-
import { PLATFORMS } from '../kernel/device.ts';
4+
import { PUBLIC_PLATFORMS } from '../kernel/device.ts';
55

66
test('normalizeOpenDevice accepts exactly the canonical leaf platforms', () => {
7-
for (const platform of PLATFORMS) {
7+
for (const platform of PUBLIC_PLATFORMS) {
88
const result = normalizeOpenDevice({
99
platform,
1010
id: 'device-1',
@@ -14,7 +14,7 @@ test('normalizeOpenDevice accepts exactly the canonical leaf platforms', () => {
1414
assert.equal(result.platform, platform);
1515
}
1616
// Lock the membership so the derived check cannot silently widen/narrow.
17-
assert.deepEqual([...PLATFORMS], ['ios', 'macos', 'android', 'linux', 'web']);
17+
assert.deepEqual([...PUBLIC_PLATFORMS], ['ios', 'macos', 'android', 'linux', 'web']);
1818
});
1919

2020
test('normalizeOpenDevice rejects the apple selector and unknown platforms', () => {

‎src/__tests__/provider-device-runtime.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ function makeProviderRuntimeWorld() {
5151
expiresAt: 60_001,
5252
};
5353
const device: DeviceInfo = {
54-
platform: 'ios',
54+
platform: 'apple',
5555
kind: 'simulator',
5656
id: 'provider:ios:lease-a',
5757
name: 'Provider iOS',

‎src/__tests__/test-utils/device-fixtures.ts‎

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,27 +9,30 @@ export const ANDROID_EMULATOR: DeviceInfo = {
99
};
1010

1111
export const IOS_SIMULATOR: DeviceInfo = {
12-
platform: 'ios',
12+
platform: 'apple',
1313
id: 'sim-1',
1414
name: 'iPhone 17 Pro',
1515
kind: 'simulator',
16+
appleOs: 'ios',
1617
booted: true,
1718
};
1819

1920
export const IOS_DEVICE: DeviceInfo = {
20-
platform: 'ios',
21+
platform: 'apple',
2122
id: 'ios-device-1',
2223
name: 'iPhone',
2324
kind: 'device',
25+
appleOs: 'ios',
2426
booted: true,
2527
};
2628

2729
export const MACOS_DEVICE: DeviceInfo = {
28-
platform: 'macos',
30+
platform: 'apple',
2931
id: 'host-macos-local',
3032
name: 'Mac',
3133
kind: 'device',
3234
target: 'desktop',
35+
appleOs: 'macos',
3336
booted: true,
3437
};
3538

@@ -59,11 +62,12 @@ export const ANDROID_TV_DEVICE: DeviceInfo = {
5962
};
6063

6164
export const TVOS_SIMULATOR: DeviceInfo = {
62-
platform: 'ios',
65+
platform: 'apple',
6366
id: 'tv-sim-1',
6467
name: 'Apple TV',
6568
kind: 'simulator',
6669
target: 'tv',
70+
appleOs: 'tvos',
6771
};
6872

6973
// iPadOS / visionOS carry the explicit `appleOs` discriminant discovery stores, so
@@ -72,7 +76,7 @@ export const TVOS_SIMULATOR: DeviceInfo = {
7276
// the touch iOS engine (`platform: 'ios'`, mobile target) and are capability-identical
7377
// to iOS today.
7478
export const IPADOS_SIMULATOR: DeviceInfo = {
75-
platform: 'ios',
79+
platform: 'apple',
7680
id: 'ipad-sim-1',
7781
name: 'iPad Pro 11-inch',
7882
kind: 'simulator',
@@ -81,7 +85,7 @@ export const IPADOS_SIMULATOR: DeviceInfo = {
8185
};
8286

8387
export const VISIONOS_SIMULATOR: DeviceInfo = {
84-
platform: 'ios',
88+
platform: 'apple',
8589
id: 'vision-sim-1',
8690
name: 'Apple Vision Pro',
8791
kind: 'simulator',

‎src/backend.ts‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { AlertAction, AlertInfo } from './alert-contract.ts';
22
import type { AppsFilter } from './contracts/app-inventory.ts';
33
import type { Point, SnapshotNode, SnapshotOptions, SnapshotState } from './kernel/snapshot.ts';
44
import type { NetworkIncludeMode } from './kernel/contracts.ts';
5-
import type { DeviceTarget, Platform, PlatformSelector } from './kernel/device.ts';
5+
import type { DeviceTarget, Platform, PlatformSelector, PublicPlatform } from './kernel/device.ts';
66
import type { BackMode } from './core/back-mode.ts';
77
import type { RepeatedInput } from './commands/command-input.ts';
88
import type { ClickButton } from './core/click-button.ts';
@@ -18,7 +18,10 @@ import type {
1818
} from './snapshot-capture-annotations.ts';
1919
import type { ScreenshotResultData } from './utils/screenshot-result.ts';
2020

21-
export type AgentDeviceBackendPlatform = Platform;
21+
// The backend's public leaf platform (approach b): backends distinguish iOS from
22+
// macOS (e.g. snapshot backend routing, the macOS surface guard), so this carries the
23+
// leaf string, not the internal collapsed `apple`.
24+
export type AgentDeviceBackendPlatform = PublicPlatform;
2225

2326
export const BACKEND_CAPABILITY_NAMES = [
2427
'android.shell',

‎src/cli/commands/connection-runtime.ts‎

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,13 @@ import { resolveDaemonPaths } from '../../daemon/config.ts';
22
import { stopReactDevtoolsCompanion } from '../../client/client-react-devtools-companion.ts';
33
import { stopMetroTunnel } from '../../metro/metro.ts';
44
import { resolveRemoteConfigProfile } from '../../remote/remote-config.ts';
5-
import { resolveDevice, type DeviceInfo } from '../../kernel/device.ts';
5+
import {
6+
deviceFieldsFromPublicPlatform,
7+
isIosFamily,
8+
publicPlatformString,
9+
resolveDevice,
10+
type DeviceInfo,
11+
} from '../../kernel/device.ts';
612
import { shouldAgentCdpUseRemoteBridgeUrl } from './agent-cdp.ts';
713
import type { MetroBridgeScope } from '../../client/client-companion-tunnel-contract.ts';
814
import {
@@ -718,7 +724,7 @@ async function resolveProxyLeaseState(options: {
718724
function applyResolvedDeviceSelector(flags: CliFlags, device: DeviceInfo): void {
719725
flags.platform = device.platform;
720726
flags.target = device.target ?? flags.target;
721-
if (device.platform === 'ios') {
727+
if (isIosFamily(device)) {
722728
flags.udid = device.id;
723729
return;
724730
}
@@ -742,7 +748,7 @@ async function resolveSelectedDevice(
742748
});
743749
return await resolveDevice(
744750
devices.map((device) => ({
745-
platform: device.platform,
751+
...deviceFieldsFromPublicPlatform(device.platform),
746752
id: device.id,
747753
name: device.name,
748754
kind: device.kind,
@@ -760,11 +766,11 @@ async function resolveSelectedDevice(
760766
}
761767

762768
function buildProxyDeviceKey(device: DeviceInfo): string {
763-
return `${device.platform}:${device.target ?? 'mobile'}:${device.id}`;
769+
return `${publicPlatformString(device)}:${device.target ?? 'mobile'}:${device.id}`;
764770
}
765771

766772
function leaseBackendForDevice(device: DeviceInfo): LeaseBackend | undefined {
767-
if (device.platform === 'ios') return 'ios-instance';
773+
if (isIosFamily(device)) return 'ios-instance';
768774
if (device.platform === 'android') return 'android-instance';
769775
return undefined;
770776
}

‎src/client/client-normalizers.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import type { DaemonRequest, SessionRuntimeHints } from '../daemon/types.ts';
44
import { AppError, type NormalizedError } from '../kernel/errors.ts';
55
import type { SnapshotNode } from '../kernel/snapshot.ts';
66
import { buildAppIdentifiers, buildDeviceIdentifiers } from './client-shared.ts';
7-
import { isPlatform } from '../kernel/device.ts';
7+
import { isPublicPlatform } from '../kernel/device.ts';
88
import {
99
leaseScopeFromOptions,
1010
leaseScopeToCommandFlags,
@@ -184,7 +184,7 @@ export function normalizeOpenDevice(
184184
const platform = value.platform;
185185
const id = readOptionalString(value, 'id');
186186
const name = readOptionalString(value, 'device');
187-
if (!isPlatform(platform) || !id || !name) {
187+
if (!isPublicPlatform(platform) || !id || !name) {
188188
return undefined;
189189
}
190190
const target = readDeviceTarget(value, 'target');

‎src/client/client-shared.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {
1414
publicSnapshotCaptureAnnotations,
1515
type SnapshotCaptureAnnotations,
1616
} from '../snapshot-capture-annotations.ts';
17-
import type { Platform } from '../kernel/device.ts';
17+
import type { PublicPlatform } from '../kernel/device.ts';
1818
import { successText, withSuccessText } from '../utils/success-text.ts';
1919

2020
export function buildAppIdentifiers(params: {
@@ -33,7 +33,7 @@ export function buildAppIdentifiers(params: {
3333
}
3434

3535
export function buildDeviceIdentifiers(
36-
platform: Platform,
36+
platform: PublicPlatform,
3737
id: string,
3838
name: string,
3939
): AgentDeviceIdentifiers {

‎src/client/client-types.ts‎

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,12 @@ import type {
1212
SessionIsolationMode,
1313
SessionRuntimeHints,
1414
} from '../kernel/contracts.ts';
15-
import type { DeviceKind, DeviceTarget, Platform, PlatformSelector } from '../kernel/device.ts';
15+
import type {
16+
DeviceKind,
17+
DeviceTarget,
18+
PublicPlatform,
19+
PlatformSelector,
20+
} from '../kernel/device.ts';
1621
import type { BackMode } from '../core/back-mode.ts';
1722
import type { ClickButton } from '../core/click-button.ts';
1823
import type { RecordingExportQuality } from '../core/recording-export-quality.ts';
@@ -151,7 +156,7 @@ export type AgentDeviceSelectionOptions = {
151156
};
152157

153158
export type AgentDeviceDevice = {
154-
platform: Platform;
159+
platform: PublicPlatform;
155160
target: DeviceTarget;
156161
kind: DeviceKind;
157162
id: string;
@@ -167,7 +172,7 @@ export type AgentDeviceDevice = {
167172
};
168173

169174
export type AgentDeviceSessionDevice = {
170-
platform: Platform;
175+
platform: PublicPlatform;
171176
target: DeviceTarget;
172177
id: string;
173178
name: string;
@@ -225,7 +230,7 @@ export type AppDeployOptions = AgentDeviceRequestOverrides &
225230
export type AppDeployResult = {
226231
app: string;
227232
appPath: string;
228-
platform: Platform;
233+
platform: PublicPlatform;
229234
appId?: string;
230235
bundleId?: string;
231236
package?: string;

‎src/cloud-webdriver/runtime.ts‎

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,11 @@ import type {
1313
ProviderDeviceInstallResult,
1414
ProviderDeviceRuntime,
1515
} from '../provider-device-runtime.ts';
16-
import type { DeviceInfo, Platform } from '../kernel/device.ts';
16+
import {
17+
deviceFieldsFromPublicPlatform,
18+
publicPlatformString,
19+
type DeviceInfo,
20+
} from '../kernel/device.ts';
1721
import { AppError } from '../kernel/errors.ts';
1822
import { unavailableCloudArtifactsResult } from './artifact-results.ts';
1923
import {
@@ -31,7 +35,7 @@ import {
3135
} from './webdriver-client.ts';
3236
import { createWebDriverInteractor } from './webdriver-interactor.ts';
3337

34-
export type CloudWebDriverPlatform = Extract<Platform, 'android' | 'ios'>;
38+
export type CloudWebDriverPlatform = 'android' | 'ios';
3539

3640
export type CloudWebDriverUploadResult = ProviderDeviceInstallResult & {
3741
appReference: string;
@@ -337,7 +341,7 @@ class CloudWebDriverRuntime implements ProviderDeviceRuntime {
337341

338342
private deviceForLease(lease: DeviceLease, prepared: CloudWebDriverPreparedSession): DeviceInfo {
339343
return {
340-
platform: prepared.platform,
344+
...deviceFieldsFromPublicPlatform(prepared.platform),
341345
id:
342346
prepared.deviceId ??
343347
this.options.deviceId?.(lease) ??
@@ -364,7 +368,7 @@ class CloudWebDriverRuntime implements ProviderDeviceRuntime {
364368
throw new AppError(
365369
'UNSUPPORTED_OPERATION',
366370
unsupportedCapabilityMessage(session.capabilities, 'install'),
367-
{ provider: this.provider, deviceId: device.id, platform: device.platform },
371+
{ provider: this.provider, deviceId: device.id, platform: publicPlatformString(device) },
368372
);
369373
}
370374
const uploadApp = session.prepared.uploadApp ?? this.options.uploadApp;

‎src/commands/__tests__/command-surface-metadata.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ test('common command input accepts web platform selector', () => {
4545

4646
const platformSchema = snapshotMetadata.inputSchema.properties?.platform;
4747
const input = snapshotMetadata.readInput({ platform: 'web' }) as { platform?: unknown };
48-
assert.deepEqual(platformSchema?.enum, ['ios', 'macos', 'android', 'linux', 'web', 'apple']);
48+
assert.deepEqual(platformSchema?.enum, ['apple', 'android', 'linux', 'web', 'ios', 'macos']);
4949
assert.equal(input.platform, 'web');
5050
});
5151

0 commit comments

Comments
 (0)