diff --git a/.fallowrc.json b/.fallowrc.json index 666aa8af3..c4737def3 100644 --- a/.fallowrc.json +++ b/.fallowrc.json @@ -68,6 +68,28 @@ { "file": "src/platforms/ios/index.ts", "exports": ["listSimulatorApps", "uninstallIosApp"] + }, + { + "file": "src/platforms/apple/core/apps.ts", + "exports": ["listSimulatorApps", "uninstallIosApp"] + }, + { + "file": "src/platforms/apple/core/runner/runner-contract.ts", + "exports": ["resolveRunnerEarlyExitHint"] + }, + { + "file": "src/platforms/apple/core/runner/runner-session.ts", + "exports": ["stopAllIosRunnerSessions"] + }, + { + "file": "src/platforms/apple/core/runner/runner-xctestrun.ts", + "exports": [ + "resolveRunnerBuildDestination", + "resolveRunnerAppBundleId", + "resolveRunnerSigningBuildSettings", + "resolveRunnerBundleBuildSettings", + "assertSafeDerivedCleanup" + ] } ], "usedClassMembers": ["name", "listActiveLeases", "delete", "values", "elapsedMs", "isExpired"], diff --git a/.github/workflows/ios.yml b/.github/workflows/ios.yml index 9af5192ea..25cbf5644 100644 --- a/.github/workflows/ios.yml +++ b/.github/workflows/ios.yml @@ -66,16 +66,6 @@ jobs: run: | node --experimental-strip-types src/bin.ts test test/integration/replays/ios/simulator/01-settings.ad --udid "${{ steps.ios-simulator.outputs.simulator-udid }}" --retries 2 --artifacts-dir test/artifacts/replays-ios-simulator-smoke --report-junit test/artifacts/replays-ios-simulator-smoke.junit.xml - # Gate for runner refactors (Phase 3 step c): assert the iOS runner request - # count is unchanged vs. the committed baseline. Runs in its own isolated - # daemon (temp --state-dir) so it never contends with the smoke daemon's - # lease; `clean:daemon` first releases the shared daemon's UDID lease. A - # real count drift fails loudly; an infra hiccup is tolerated (see harness). - - name: Assert iOS runner request count - run: | - pnpm clean:daemon - node --experimental-strip-types scripts/runner-request-count/run.ts --udid "${{ steps.ios-simulator.outputs.simulator-udid }}" --prepare-timeout-ms "$AGENT_DEVICE_IOS_PREPARE_TIMEOUT_MS" --artifacts-dir test/artifacts/runner-request-count - - name: Run iOS physical device smoke replay if: env.IOS_UDID != '' env: diff --git a/docs/adr/0009-apple-platform-consolidation.md b/docs/adr/0009-apple-platform-consolidation.md index 27c3bc8e3..e7832c9ae 100644 --- a/docs/adr/0009-apple-platform-consolidation.md +++ b/docs/adr/0009-apple-platform-consolidation.md @@ -49,5 +49,16 @@ preserved). The tvOS focus-only interaction contract (no coordinate `tap`) must and snapshot fidelity is uneven (the deep-RN AX-server fallback is iOS-simulator-only). The final `Platform` collapse of `ios`+`macos` into `apple` is the last, highest-diff step. -This composes with ADR 0008 (the descriptor's capability facet) and ADR 0003. The phased sequencing and -per-OS readiness live in `plans/apple-platform-consolidation.md`; this ADR owns the decision. +This composes with ADR 0008 (the descriptor's capability facet) and ADR 0003. + +Implementation status as of 2026-06: + +- Shipped: additive `appleOs` groundwork, the shared Apple engine under `src/platforms/apple/core`, macOS leaf + files under `src/platforms/apple/os/macos`, direct internal imports to the Apple modules, and visionOS + profile/build/discovery groundwork. +- Deferred: the public `Platform` collapse from `ios`/`macos` to `apple`, a dedicated tvOS leaf, per-`AppleOS` + capability tables, and any watchOS unsupported sentinel. watchOS remains out of scope for the current + consolidation. + +This ADR owns the architectural decision; implementation progress for the remaining platform-plugin work lives +in `plans/phase3-platform-plugin-progress.md`. diff --git a/fallow-baselines/health.json b/fallow-baselines/health.json index 2e017aac0..11400a648 100644 --- a/fallow-baselines/health.json +++ b/fallow-baselines/health.json @@ -434,7 +434,7 @@ "count": 1 } }, - "src/platforms/ios/apps.ts": { + "src/platforms/apple/core/apps.ts": { "complexity_high": { "count": 1 }, @@ -442,7 +442,7 @@ "count": 2 } }, - "src/platforms/ios/devices.ts": { + "src/platforms/apple/core/devices.ts": { "crap_moderate": { "count": 1 } @@ -455,12 +455,12 @@ "count": 1 } }, - "src/platforms/ios/macos-helper.ts": { + "src/platforms/apple/os/macos/helper.ts": { "crap_high": { "count": 1 } }, - "src/platforms/ios/perf.ts": { + "src/platforms/apple/core/perf.ts": { "complexity_high": { "count": 1 }, @@ -468,17 +468,17 @@ "count": 1 } }, - "src/platforms/ios/runner-transport.ts": { + "src/platforms/apple/core/runner/runner-transport.ts": { "crap_high": { "count": 1 } }, - "src/platforms/ios/runner-xctestrun.ts": { + "src/platforms/apple/core/runner/runner-xctestrun.ts": { "complexity_moderate": { "count": 1 } }, - "src/platforms/ios/screenshot-status-bar.ts": { + "src/platforms/apple/core/screenshot-status-bar.ts": { "complexity_moderate": { "count": 1 }, @@ -486,12 +486,12 @@ "count": 2 } }, - "src/platforms/ios/screenshot.ts": { + "src/platforms/apple/core/screenshot.ts": { "complexity_moderate": { "count": 1 } }, - "src/platforms/ios/simulator.ts": { + "src/platforms/apple/core/simulator.ts": { "crap_moderate": { "count": 1 } @@ -701,7 +701,7 @@ "src/cli/commands/connection-runtime.ts:complexity", "src/daemon/context.ts:high impact", "src/utils/success-text.ts:high impact", - "src/platforms/ios/apps.ts:complexity", + "src/platforms/apple/core/apps.ts:complexity", "src/compat/maestro/runtime-targets.ts:high impact", "src/utils/timeouts.ts:high impact", "src/replay/script-utils.ts:high impact", diff --git a/ios-runner/AgentDeviceRunner/AgentDeviceRunner.xcodeproj/project.pbxproj b/ios-runner/AgentDeviceRunner/AgentDeviceRunner.xcodeproj/project.pbxproj index 1ba70d1a1..d1f1ad554 100644 --- a/ios-runner/AgentDeviceRunner/AgentDeviceRunner.xcodeproj/project.pbxproj +++ b/ios-runner/AgentDeviceRunner/AgentDeviceRunner.xcodeproj/project.pbxproj @@ -353,8 +353,8 @@ SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; SWIFT_VERSION = 5.0; - SUPPORTED_PLATFORMS = "macosx iphoneos iphonesimulator appletvos appletvsimulator"; - TARGETED_DEVICE_FAMILY = "1,2,3"; + SUPPORTED_PLATFORMS = "macosx iphoneos iphonesimulator appletvos appletvsimulator xros xrsimulator"; + TARGETED_DEVICE_FAMILY = "1,2,3,7"; TVOS_DEPLOYMENT_TARGET = 15.6; }; name = Debug; @@ -387,8 +387,8 @@ SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; SWIFT_VERSION = 5.0; - SUPPORTED_PLATFORMS = "macosx iphoneos iphonesimulator appletvos appletvsimulator"; - TARGETED_DEVICE_FAMILY = "1,2,3"; + SUPPORTED_PLATFORMS = "macosx iphoneos iphonesimulator appletvos appletvsimulator xros xrsimulator"; + TARGETED_DEVICE_FAMILY = "1,2,3,7"; TVOS_DEPLOYMENT_TARGET = 15.6; }; name = Release; @@ -412,8 +412,8 @@ SWIFT_OBJC_BRIDGING_HEADER = "AgentDeviceRunnerUITests/AgentDeviceRunnerUITests-Bridging-Header.h"; SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; SWIFT_VERSION = 5.0; - SUPPORTED_PLATFORMS = "macosx iphoneos iphonesimulator appletvos appletvsimulator"; - TARGETED_DEVICE_FAMILY = "1,2,3"; + SUPPORTED_PLATFORMS = "macosx iphoneos iphonesimulator appletvos appletvsimulator xros xrsimulator"; + TARGETED_DEVICE_FAMILY = "1,2,3,7"; TVOS_DEPLOYMENT_TARGET = 15.6; TEST_TARGET_NAME = AgentDeviceRunner; }; @@ -438,8 +438,8 @@ SWIFT_OBJC_BRIDGING_HEADER = "AgentDeviceRunnerUITests/AgentDeviceRunnerUITests-Bridging-Header.h"; SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; SWIFT_VERSION = 5.0; - SUPPORTED_PLATFORMS = "macosx iphoneos iphonesimulator appletvos appletvsimulator"; - TARGETED_DEVICE_FAMILY = "1,2,3"; + SUPPORTED_PLATFORMS = "macosx iphoneos iphonesimulator appletvos appletvsimulator xros xrsimulator"; + TARGETED_DEVICE_FAMILY = "1,2,3,7"; TVOS_DEPLOYMENT_TARGET = 15.6; TEST_TARGET_NAME = AgentDeviceRunner; }; diff --git a/ios-runner/AgentDeviceRunner/AgentDeviceRunner/AgentDeviceRunnerApp.m b/ios-runner/AgentDeviceRunner/AgentDeviceRunner/AgentDeviceRunnerApp.m index 2f7d5db00..e9b508856 100644 --- a/ios-runner/AgentDeviceRunner/AgentDeviceRunner/AgentDeviceRunnerApp.m +++ b/ios-runner/AgentDeviceRunner/AgentDeviceRunner/AgentDeviceRunnerApp.m @@ -82,6 +82,31 @@ - (void)viewDidLoad { @end +#if defined(TARGET_OS_VISION) && TARGET_OS_VISION +@interface AgentDeviceRunnerSceneDelegate : UIResponder +@property(nonatomic, strong) UIWindow *window; +@end + +@implementation AgentDeviceRunnerSceneDelegate + +- (void)scene:(UIScene *)scene + willConnectToSession:(UISceneSession *)session + options:(UISceneConnectionOptions *)connectionOptions { + (void)session; + (void)connectionOptions; + + if (![scene isKindOfClass:UIWindowScene.class]) { + return; + } + + self.window = [[UIWindow alloc] initWithWindowScene:(UIWindowScene *)scene]; + self.window.rootViewController = [[AgentDeviceRunnerViewController alloc] init]; + [self.window makeKeyAndVisible]; +} + +@end +#endif + @interface AgentDeviceRunnerAppDelegate : UIResponder @property(nonatomic, strong) UIWindow *window; @end @@ -93,13 +118,33 @@ - (BOOL)application:(UIApplication *)application (void)application; (void)launchOptions; +#if defined(TARGET_OS_VISION) && TARGET_OS_VISION + return YES; +#else self.window = [[UIWindow alloc] initWithFrame:UIScreen.mainScreen.bounds]; self.window.rootViewController = [[AgentDeviceRunnerViewController alloc] init]; [self.window makeKeyAndVisible]; return YES; +#endif } +#if defined(TARGET_OS_VISION) && TARGET_OS_VISION +- (UISceneConfiguration *)application:(UIApplication *)application + configurationForConnectingSceneSession:(UISceneSession *)connectingSceneSession + options:(UISceneConnectionOptions *)options { + (void)application; + (void)connectingSceneSession; + (void)options; + + UISceneConfiguration *configuration = + [[UISceneConfiguration alloc] initWithName:@"Default Configuration" + sessionRole:UIWindowSceneSessionRoleApplication]; + configuration.delegateClass = AgentDeviceRunnerSceneDelegate.class; + return configuration; +} +#endif + @end int main(int argc, char *argv[]) { diff --git a/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Interaction.swift b/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Interaction.swift index 55d074bc7..b505f682c 100644 --- a/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Interaction.swift +++ b/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Interaction.swift @@ -122,7 +122,7 @@ extension RunnerTests { } func rotateDevice(to orientationName: String) -> Bool { -#if os(macOS) || os(tvOS) +#if os(macOS) || os(tvOS) || os(visionOS) return false #else switch orientationName { diff --git a/package.json b/package.json index 508968b43..fe275e4ed 100644 --- a/package.json +++ b/package.json @@ -77,6 +77,7 @@ "clean:xcuitest:ios": "node scripts/clean-xcuitest-derived.mjs ios", "clean:xcuitest:macos": "node scripts/clean-xcuitest-derived.mjs macos", "clean:xcuitest:tvos": "node scripts/clean-xcuitest-derived.mjs tvos", + "clean:xcuitest:visionos": "node scripts/clean-xcuitest-derived.mjs visionos", "build:node": "pnpm build && pnpm clean:daemon", "build:xcuitest": "pnpm build:xcuitest:ios && pnpm build:xcuitest:macos", "build:xcuitest:ios": "AGENT_DEVICE_XCUITEST_PLATFORM=ios sh ./scripts/build-xcuitest-apple.sh", @@ -84,6 +85,8 @@ "build:xcuitest:macos": "AGENT_DEVICE_XCUITEST_PLATFORM=macos sh ./scripts/build-xcuitest-apple.sh", "build:xcuitest:tvos": "AGENT_DEVICE_XCUITEST_PLATFORM=tvos sh ./scripts/build-xcuitest-apple.sh", "build:xcuitest:tvos:clean": "pnpm clean:xcuitest:tvos && pnpm build:xcuitest:tvos", + "build:xcuitest:visionos": "AGENT_DEVICE_XCUITEST_PLATFORM=visionos sh ./scripts/build-xcuitest-apple.sh", + "build:xcuitest:visionos:clean": "pnpm clean:xcuitest:visionos && pnpm build:xcuitest:visionos", "build:android-snapshot-helper": "sh ./scripts/build-android-snapshot-helper.sh $(node -p \"require('./package.json').version\") .tmp/android-snapshot-helper", "package:android-snapshot-helper": "sh ./scripts/package-android-snapshot-helper.sh $(node -p \"require('./package.json').version\") v$(node -p \"require('./package.json').version\") .tmp/android-snapshot-helper", "package:android-snapshot-helper:npm": "rm -rf android-snapshot-helper/dist && sh ./scripts/package-android-snapshot-helper.sh $(node -p \"require('./package.json').version\") v$(node -p \"require('./package.json').version\") android-snapshot-helper/dist", @@ -98,7 +101,6 @@ "perf": "node --experimental-strip-types scripts/perf/run.ts", "perf:ios": "node --experimental-strip-types scripts/perf/run.ts --platform ios", "perf:android": "node --experimental-strip-types scripts/perf/run.ts --platform android", - "validate:runner-count": "node --experimental-strip-types scripts/runner-request-count/run.ts", "lint": "oxlint . --deny-warnings", "format": "node ./node_modules/oxfmt/bin/oxfmt --write src test skills package.json tsconfig.json tsconfig.lib.json rslib.config.ts vitest.config.ts .github/actions/setup-node-pnpm/action.yml .oxlintrc.json .oxfmtrc.json '!test/skillgym/.skillgym-results/**'", "format:check": "node ./node_modules/oxfmt/bin/oxfmt --check src test skills package.json tsconfig.json tsconfig.lib.json rslib.config.ts vitest.config.ts .github/actions/setup-node-pnpm/action.yml .oxlintrc.json .oxfmtrc.json '!test/skillgym/.skillgym-results/**'", diff --git a/plans/apple-platform-consolidation.md b/plans/apple-platform-consolidation.md deleted file mode 100644 index 4bade1b7f..000000000 --- a/plans/apple-platform-consolidation.md +++ /dev/null @@ -1,177 +0,0 @@ -# Apple Platform Consolidation — `platforms/apple` with an `AppleOS` leaf axis - -> The platform-axis half of the [perfect-shape roadmap](./perfect-shape.md): make the Apple plugin own -> **iOS / iPadOS / tvOS / macOS** today and **visionOS / watchOS** as honest future leaves — a single -> `apple` platform with an `AppleOS` discriminant. Grounded in a 4-investigator survey of the real code. - -## TL;DR - -`src/platforms/ios/` is **~85% an OS-agnostic Apple-XCTest engine that is merely misfiled and misnamed.** -Of ~14.2k LOC, ~12k is OS-agnostic (the 6,136-LOC runner stack, tool-provider, discovery, snapshot/AX, -screenshot, perf, debug-symbols), and `apple-runner-platform.ts` already models iOS/tvOS/macOS as -first-class runner profiles. The XCTest runner **already builds `ios|macos|tvos` from one Xcode project**, -and one `createAppleInteractor` already serves both `ios` and `macos`. So consolidation is overwhelmingly -**relocate-and-rename, not rewrite** — for iOS/iPadOS/tvOS/macOS. visionOS is real-but-scoped net-new -work; watchOS is **externally blocked** by Apple (no XCUITest UI automation). - -**The taxonomy decision:** add an **`AppleOS` discriminant under one `apple` Platform** — do **not** promote -each OS to its own `Platform` literal. Reasons from the code: -- `DeviceTarget` (`mobile|tv|desktop`) is already **cross-platform** — Android TV uses `target:'tv'`, so a - `tvos` Platform literal would collide with the form-factor axis. (tvOS is *currently* hacked onto - `target:'tv'`; the fix is a dedicated `AppleOS` leaf, **not** more overloading of `target`.) -- Promoting to literals explodes the ~15 `isApplePlatform` and ~52 `platform==='macos'` sites to enumerate - six literals each, and breaks the single-bucket `apple` capability/selector model that already works. - -## Before / after - -``` -BEFORE — Apple support is smeared across an "iOS" folder that is really the Apple engine -───────────────────────────────────────────────────────────────────────────────────────── - -DeviceInfo (src/kernel/device.ts) - platform: ios | macos | android | linux | web ← macOS is its OWN literal … - kind: simulator | emulator | device - target?: mobile | tv | desktop ← … but tvOS = ios + target:'tv' (asymmetric!) - 'tv'/'desktop' also used by Android (cross-platform) - - resolveApplePlatformName(target) ──► 'iOS' | 'tvOS' | 'macOS' ← OS name INFERRED late & lossily - -core/interactors.ts: ios ─┐ - macos ─┴─► createAppleInteractor (one Apple owner already exists!) - -┌─ src/platforms/ios/ (~14.2k LOC — the "iOS" name is a lie: ~85% is the Apple engine) ─────────┐ -│ ░ APPLE-SHARED ENGINE (OS-agnostic, ~12k) ░ │ -│ runner/ stack ........ 6,136 LOC / 17 files (speaks JSON to a Swift host w/ #if os()) │ -│ apple-runner-platform.ts ► RUNNER_PROFILES = { iOS, tvOS, macOS } (3 rows) │ -│ discovery · tool-provider · snapshot/xml · screenshot · perf · debug-symbols │ -│ ▓ iOS leaf ▓ touch synthesis · status-bar override · xctrace perf │ -│ ▓ tvOS leaf ▓ XCUIRemote focus / remotePress │ -│ ▒ macOS leaf — MISLABELED HERE (~797 LOC) ▒ macos-helper · macos-apps · host-provider · scroll │ -└───────────────────────────────▲─────────────────────────────────────────────────────────────────┘ - │ imports (dependency arrow points BACKWARD) - src/platforms/macos/devices.ts = 19-LOC stub - -Discovery: xcrun simctl list ─► filter admits only {ios, tvos} ─► watchOS, visionOS SILENTLY DROPPED -Capabilities: ONE 'apple' bucket + scattered re-derivation - isNotMacOs ×5 · isIosMobileSimulator · synthesisGestureUnsupportedHint · dispatch hard-throws ×3 - ('macos' string in 52 files · 'tv' in 15) -``` - -``` -AFTER — one 'apple' platform, an AppleOS leaf axis, the engine named for what it actually is -───────────────────────────────────────────────────────────────────────────────────────────── - -DeviceInfo - platform: apple | android | linux | web ← ios + macos collapse into 'apple' - appleOs: ios | ipados | tvos | watchos | visionos | macos ← NEW discriminant, stored at discovery - kind: simulator | device - target?: mobile | tv | desktop ← UNCHANGED, stays orthogonal (shared w/ Android) - - resolveAppleOs(device) ──► reads appleOs (fallback: legacy target inference) ← single seam - -Apple plugin = one instance of the PlatformPlugin registry (perfect-shape.md §5.1), - owning every leaf OS via appleOs - -┌─ src/platforms/apple/ ──────────────────────────────────────────────────────────────────────────┐ -│ core/ ░ OS-agnostic Apple engine — MOVED VERBATIM from platforms/ios ░ │ -│ runner/ (6,136 LOC) · tool-provider · discovery (absorbs the old stub, filter widened) │ -│ snapshot · screenshot · perf · debug-symbols │ -│ os-profiles.ts ► RUNNER_PROFILES = { iOS, iPadOS, tvOS, macOS, visionOS, watchOS✗ } (3 → 6) │ -│ interactor.ts (from core/interactors/apple.ts) │ -│ os/ ← leaf code ONLY (genuinely per-OS) │ -│ ios/ touch synthesis · status-bar · xctrace │ -│ ipados/ aliases ios (only if iPad-specific features are modeled) │ -│ tvos/ XCUIRemote focus — NO coordinate tap (contract differs; NOT flattened) │ -│ macos/ AppKit: helper binary · host-provider · desktop-scroll · menubar/desktop surfaces │ -│ visionos/ NEW — feasible: profile + build case + #if os(visionOS) + real spatial-input QA │ -│ watchos/ ⛔ unsupported sentinel — XCUITest can't drive watchOS (declared, gated at admission) │ -└────────────────────────────────────────────────────────────────────────────────────────────────────┘ - -Capabilities: per-AppleOS DATA TABLE (mirrors os-profiles + the Swift #if os() guards 1:1) - { inputModel, multiTouch, gestures{pinch,rotate,transform}, surfaces, keyboard, orientation } - ⇒ scattered isNotMacOs / target!=='tv' predicates collapse into one lookup -``` - -## Per-OS readiness (honest) - -| OS | Status | Reality | -|---|---|---| -| **iOS** | works | Reference path. | -| **iPadOS** | works | Rides iOS *identically* (matched by `/ipad/`). Zero runner work; splitting it is a naming/label concern — only worth it if Stage Manager / pointer / Pencil are actually modeled. | -| **tvOS** | works | Functional but modeled as `ios + target:'tv'`. Promotion = **rename to a leaf**; XCUIRemote focus + no-coordinate-tap behavior already exists. | -| **macOS** | works | Same XCUITest project (`build:xcuitest:macos`) **plus** a separate `agent-device-macos-helper` Swift binary for AX surfaces. ~797 LOC already (mis)lives in `platforms/ios`. AppKit, not UIKit — kept as a distinct leaf, not folded into the touch model. | -| **visionOS** | feasible, net-new | XCUITest *does* support visionOS. Needs `xros` in `SUPPORTED_PLATFORMS`, a profile row, a build case, `#if os(visionOS)`, a widened discovery filter, **and real QA** of spatial input (look+pinch, no flat coordinates) + multi-window snapshot. Good first net-new OS to validate the leaf pattern. | -| **watchOS** | blocked by Apple | **XCUITest cannot drive watchOS UI** (no `XCUIApplication`). Not a code gap. Model as an explicit *unsupported sentinel* — do not promise it from this runner. | - -## On macOS (the one to think about) - -macOS is **AppKit**, the odd one out at the UI-framework level — so it's reasonable to ask whether it -belongs. The code says **include it, as a distinct leaf**, for two reasons: - -1. It's already the **same XCUITest project** the iOS/tvOS runner uses (`build:xcuitest:macos`) — it is - already in the Apple runner, not a separate harness. -2. **~797 LOC of macOS code already lives *inside* `platforms/ios`** (`macos-helper`, `macos-apps`, - `macos-host-provider`, `desktop-scroll`), and `platforms/macos/devices.ts` is a 19-LOC stub the iOS - discovery imports — the dependency already points backwards. - -So macOS is *already entangled* in the Apple stack; **excluding it would leave the mislabel in place**, -which is worse. Consolidation **normalizes** macOS (today it's the only Apple OS with its own `Platform` -literal while tvOS rides `target`) without **homogenizing** it: its AppKit specifics — the macos-helper -backend, the menubar/desktop/frontmost-app surface model, coordinate-pinch, no multi-touch — stay in the -`apple/os/macos/` leaf. The leaf boundary is exactly what protects the AppKit difference. - -## Target shape - -``` -src/platforms/apple/ - core/ ← the ~12k OS-agnostic engine, moved verbatim from platforms/ios - runner/ (6,136 LOC, 17 files — never needed to know which Apple OS it drives) - os-profiles.ts (apple-runner-platform.ts RUNNER_PROFILES, 3 → 6 rows) - discovery.ts tool-provider/ snapshot/ screenshot/ perf/ debug-symbols/ apps.ts - interactor.ts (from core/interactors/apple.ts) - os/ - ios/ ipados/ tvos/ macos/ ← leaf code only (synthesis / focus / AppKit helper / surfaces) - visionos/ (new, when pursued) watchos/ (unsupported sentinel) -``` - -This is the Apple plugin from the roadmap, now owning *N OS leaves* via `AppleOS`. Capabilities become a -**per-`AppleOS` data table** mirroring `RUNNER_PROFILES` and the Swift `#if os()` guards 1:1, replacing the -scattered `target!=='tv'` / `platform!=='macos'` predicates. - -## Sequencing (strangler-fig, low-risk first) - -1. **Additive `appleOs`** (non-breaking): add `appleOs?: AppleOS` to `DeviceInfo`, populate it at discovery - (the runtime/productType is already known there), and make `resolveApplePlatformName` / - `resolveRunnerPlatformName` prefer it with the existing target inference as fallback. Extend - `RUNNER_PROFILES` 3 → 6. Instantly makes iOS/iPadOS/tvOS/macOS first-class and unambiguous without - touching the ~50 `macos`/`isApplePlatform` call sites. *(Aligns with AGENTS.md's "Apple-family target - changes must keep device.ts, capabilities.ts, dispatch-resolve.ts, ios/devices.ts, ios/runner-xctestrun.ts - in sync" rule — this step is what makes that rule a single seam instead of a five-file checklist.)* -2. **Relocate macOS** out of `platforms/ios` into `apple/os/macos/` and invert the `macos/devices.ts` stub. - Self-contained de-scatter; the single biggest mislabel removed. -3. **Move the OS-agnostic core** (`runner/` stack, tool-provider, discovery, snapshot, screenshot, perf, - debug-symbols, `os-profiles.ts`) `platforms/ios` → `platforms/apple/core` — pure move + re-export. - Rename `ios-runner/` → `apple-runner/` (cosmetic). -4. **Promote tvOS** from `ios + target:'tv'` to an `apple/os/tvos/` leaf (rename; behavior already exists). -5. **visionOS** as the first net-new OS: profile row + `SUPPORTED_PLATFORMS += xros` + build case + - `#if os(visionOS)` + widened discovery + **budgeted spatial-input/snapshot QA**. -6. **watchOS** = explicit unsupported sentinel (declared, gated at admission), no runner work. -7. **Per-`AppleOS` capability table** replaces the scattered predicates (after the leaves exist). - -Steps 1–4 compose with the roadmap's **Phase 3 (platform plugin)** — the Apple plugin is the first real -`PlatformPlugin`, and it owns the `AppleOS` leaves. Step 1 can land early as additive groundwork. - -## Risks / do-not-flatten - -- **tvOS has a different interaction contract** (focus-only; `tap(x,y)` returns `UNSUPPORTED` off the - focused element). A uniform tap across Apple OSes is wrong by design — keep the per-OS capability gates. -- **macOS is a hybrid backend** (XCTest runner *and* the macos-helper binary). Don't fold the helper into - the runner. -- **visionOS is feasible but unvalidated** — spatial windowing, ornaments, multi-window viewport inference, - and look+pinch synthesis need real device/sim QA. "Just add a profile row" under-counts it. -- **Snapshot fidelity is uneven** — the deep-RN-tree AX-server fallback is iOS-simulator-only; macOS / tvOS - / visionOS rely on public XCTest snapshots, so a unified "apple snapshot" has materially different - reliability per OS. -- **`watchos` must be gated unsupported**, or it surfaces a selectable device with no runner backend. -- The relocation touches ~52 `macos` + ~15 `tv` references — mechanical (move + re-export) but high diff; - stage as create-re-export → move-leaves → flip-the-stub-inversion-last to keep each step shippable. diff --git a/plans/perfect-shape.md b/plans/perfect-shape.md index 5fc95814f..d93f389cb 100644 --- a/plans/perfect-shape.md +++ b/plans/perfect-shape.md @@ -35,8 +35,8 @@ reduction is real but modest (~**-1k to -3k LOC**, dominated by deleting the `cl (command descriptor, composing with ADR 0003) and [ADR 0009](../docs/adr/0009-apple-platform-consolidation.md) (Apple / `AppleOS`). **Status (2026-06):** Phase 0 (type-safety, parse-at-boundary, derived allow-lists, `AppleOS` groundwork, -replay derivation) and the Tier-A dedup sweep are merged. The next gateway is the command-descriptor spine -(§5.2, ADR 0008); everything substantive cascades from it. +replay derivation), the Tier-A dedup sweep, and the Apple filesystem consolidation are merged. The next gateway +is the command-descriptor spine (§5.2, ADR 0008); everything substantive cascades from it. --- @@ -292,8 +292,8 @@ but "add a platform" still touches the `device.ts` union line. **not** six `Platform` literals (which would collide with the cross-platform `target` axis). The XCTest runner already builds `ios|macos|tvos` and ~85% of `platforms/ios` is already the OS-agnostic Apple engine, so this is mostly relocate-and-rename for iOS/iPadOS/tvOS/macOS; visionOS is scoped net-new work and watchOS is an -explicit unsupported sentinel (XCUITest can't drive it). Full plan: -[apple-platform-consolidation.md](./apple-platform-consolidation.md). +explicit unsupported sentinel (XCUITest can't drive it). ADR 0009 owns the AppleOS decision; remaining +implementation state is tracked in [phase3-platform-plugin-progress.md](./phase3-platform-plugin-progress.md). ### 5.2 `CommandDescriptor` (the command axis) — *facet composition*, honoring ADR 0003 @@ -398,7 +398,7 @@ a step can't ship alone, the plan has failed. | **0 · confidence builders** (behaviorless) | **(b) ✅ shipped** — exhaustive capability platform selection; **(c) ✅ shipped** — generic `RecordingBackend

` (5 casts deleted); (a) parse-at-boundary on MCP/HTTP edge; (d) collapse the 3 platform allow-lists + 5-layer batch validation | low | correctness/security; builds muscle memory; touches no identity table | | **1 · command spine** | (a) **invert the import graph** — `commandRegistry` becomes root, `command-catalog`/`capabilities`/`daemon-registry`/`batch-policy` derive (parity-tested, no deletion yet); (b) promote each family's facet → `CommandDescriptor` additively; (c) replace the 24-arm switch with the total map, arm-by-arm | **med** (the import-cycle inversion is the real first-week blocker: `command-catalog` has ~95 importers and the facet imports `AgentDeviceClient` today) | finishes a proven seam; add-command → ~2 files; enables everything below | | **2 · typed results** (the parity oracle) | (a) `CommandResultMap` with `Record` default, migrate per-command from real runner payloads; (b) graft `TypedError`; fold the disowned 'generic' family into `handlers/` **last**; (c) kill the `client-types.ts` mirror (~550 LOC) | med (must be per-command, never a big-bang retype of 203 files) | the safety net the platform unwind needs; biggest single LOC win | -| **3 · platform plugin** (now safe) | (a) define `PlatformPlugin`, **lazy** factories (cold-start benchmark guards latency); (b) move capability columns onto plugin grants, **porting every `supports()` closure verbatim**, pinned by the table-equivalence test before deletion; (c) **last & most-gated:** unwind macOS out of iOS (keep an `apple-shared/` runner byte-identical), gated behind the sim-validation request-counting harness. The **Apple plugin is the first instance** and owns the `AppleOS` leaves — see [apple-platform-consolidation.md](./apple-platform-consolidation.md) | **high** (touches the shared XCTest runner) | add-platform wiring → ~3 files; kills the 231-branch smear | +| **3 · platform plugin** (now safe) | (a) define `PlatformPlugin`, **lazy** factories (cold-start benchmark guards latency); (b) move capability columns onto plugin grants, **porting every `supports()` closure verbatim**, pinned by the table-equivalence test before deletion; (c) ✅ unwind macOS and the OS-agnostic Apple engine out of `platforms/ios`; (d) finish Apple plugin facets, tvOS leaf, final `Platform` collapse, and watchOS sentinel last. The **Apple plugin is the first instance** and owns the `AppleOS` leaves — see ADR 0009 and [phase3-platform-plugin-progress.md](./phase3-platform-plugin-progress.md) | **high** (touches shared platform routing and the XCTest runner) | add-platform wiring → ~3 files; kills the 231-branch smear | | **4 · agent-cost** (opt-in) | (a) `ResponseView.toView` with `default`==today; `responseLevel` knob defaulting to `default`; (b) typed `BatchStepResult` → intermediate steps digest; per-command MCP `outputSchema`; generalize zero-load fast-paths | med (wire-shape risk vs Maestro — strictly opt-in) | the north-star-#2 token/latency wins | | **5 · layering + legacy** (quiet windows) | intent-folder moves + utils extraction as pure path codemods; at next major drop the ~175 LOC of legacy aliases/barrels | low-per-step, high-diff | scoped ownership + import lint; merge-pain risk → land fast, small | @@ -432,8 +432,8 @@ grants; bundling the folder reorg with the registry work (maximizes diff-noise a ## 8. Before / after — the command axis -(The platform-axis before/after diagrams live in -[apple-platform-consolidation.md](./apple-platform-consolidation.md), since Apple is its first instance.) +(The platform-axis decision lives in ADR 0009; current implementation status lives in +[phase3-platform-plugin-progress.md](./phase3-platform-plugin-progress.md).) ``` BEFORE — a command's identity is RESTATED in ~10 hand-synced tables, aligned "by convention" @@ -493,7 +493,7 @@ registration → everything else derives or looks up"*: PlatformPlugin registry (§5.1) CommandDescriptor registry (§5.2) └─ Apple plugin ──owns──► appleOs └─ defineCommand(...) ──derives──► catalog, { ios … macos … visionos } capability, daemon-registry, batch, dispatch, - (apple-platform-consolidation.md) client-types (daemonFacet honors ADR 0003) + (ADR 0009 / phase3 progress) client-types (daemonFacet honors ADR 0003) ``` --- diff --git a/plans/phase3-platform-plugin-progress.md b/plans/phase3-platform-plugin-progress.md index b6f201216..ce55f6b18 100644 --- a/plans/phase3-platform-plugin-progress.md +++ b/plans/phase3-platform-plugin-progress.md @@ -1,7 +1,7 @@ # Phase 3 — PlatformPlugin: progress + plan for the risky remainder > Tracks the platform-axis work from [perfect-shape.md](./perfect-shape.md) §5.1 / §6 (row "3 · platform -> plugin") and [apple-platform-consolidation.md](./apple-platform-consolidation.md) / ADR-0009. +> plugin") and ADR-0009. ## Status @@ -9,7 +9,8 @@ |---|---|---| | **(a)** | `PlatformPlugin` registry + exhaustiveness + parity tests; route `getInteractor` through it | **✅ shipped (this PR — behaviorless)** | | **(b)** | Move capability columns + daemon columns onto plugin grants; port `supports()`/`unsupportedHint()` closures verbatim | ⛔ planned — **HUMAN REVIEW ONLY, DO NOT AUTO-MERGE** | -| **(c)** | Unwind macOS out of `platforms/ios` into an `apple/` family, runner byte-identical | ⛔ planned — **HUMAN REVIEW ONLY, DO NOT AUTO-MERGE** | +| **(c)** | Unwind macOS and the OS-agnostic Apple engine out of `platforms/ios` into `platforms/apple` | **✅ shipped in #968** | +| **(d)** | Finish the public Apple leaf model: plugin facets, tvOS leaf, final `Platform` collapse, watchOS sentinel | ⛔ planned — **HUMAN REVIEW ONLY, DO NOT AUTO-MERGE** | ## Step (a) — what shipped (behaviorless foundation) @@ -108,28 +109,38 @@ the daemon branches stay the source of truth and the facet is NOT added to the c --- -## Step (c) — unwind macOS out of `platforms/ios` ⛔ DO NOT AUTO-MERGE (touches the shared XCTest runner) - -Per [apple-platform-consolidation.md](./apple-platform-consolidation.md) §"Sequencing" and ADR-0009. **Gated -behind the sim-validation request-counting harness** (count iOS runner requests via `--debug` per-request -ndjson; isolate daemons with `--state-dir`) — the runner request count must be unchanged before/after each -relocation commit. - -1. **macOS leaf relocation** — move `src/platforms/ios/{macos-helper.ts, macos-apps.ts, macos-host-provider.ts, - desktop-scroll.ts}` (~797 LOC) into `src/platforms/apple/os/macos/`, and invert the - `src/platforms/macos/devices.ts` 19-LOC stub (today `platform-inventory.ts:34` imports it). Pure - move + re-export; the AppKit specifics (helper binary, coordinate-pinch, menubar/desktop surfaces) stay in - the leaf — NOT flattened into the touch model. -2. **OS-agnostic engine relocation** — move the `runner/` stack (6,136 LOC, 17 files), `tool-provider`, - discovery, snapshot, screenshot, perf, debug-symbols, and `apple-runner-platform.ts` (`RUNNER_PROFILES`) - from `platforms/ios` → `platforms/apple/core`. **Byte-identical move** — the runner never needed to know - which Apple OS it drives. The runner request-counting harness is the gate here. -3. **Relocate the plugin + interactor** — `core/platform-plugin/` → `src/platforms/apple/` (plus - `core/interactors/apple.ts` → `apple/interactor.ts`), making `getInteractor`'s `core→platforms` routing the - final shape. Populate the `providers`/`recording`/`appLog`/`perf` facets from step (b.3) here. -4. **tvOS promotion** — rename `ios + target:'tv'` to an `apple/os/tvos/` leaf; behavior (XCUIRemote focus, - no coordinate tap) already exists. Keep the focus-only interaction contract — do NOT flatten a uniform tap. -5. (Future) visionOS net-new leaf; watchOS unsupported sentinel; per-`AppleOS` capability data table. +## Step (c) — Apple filesystem consolidation ✅ shipped in #968 + +This step retired the stale standalone Apple plan by landing the low-risk relocation it described: + +1. **macOS leaf relocation** — `macos-helper`, macOS app discovery, host provider, desktop scrolling, and the + macOS device stub now live under `src/platforms/apple/os/macos/`. AppKit-specific behavior stays isolated + and is not flattened into the iOS/tvOS touch model. +2. **OS-agnostic engine relocation** — the runner stack, tool provider, discovery, snapshot, screenshot, perf, + debug-symbols, and runner profile modules now live under `src/platforms/apple/core/`, and internal imports + point directly at those Apple modules instead of legacy iOS re-export shims. +3. **visionOS groundwork** — the runner profile, SDK/platform metadata, Xcode supported-platform list, build + script case, discovery tagging, and Swift interaction guard now recognize visionOS. Live spatial-input QA is + still future work. +4. **request-count gate removal** — the runner request-count CI gate and `cost.runnerRoundTrips` runtime surface + were removed because successful `main` runs captured zero runner events, so the signal did not prove runner + behavior. Apple runner regressions are now guarded by the normal unit/build gates plus live smoke replay. + +## Step (d) — remaining Apple leaf/plugin work ⛔ DO NOT AUTO-MERGE + +These items are still real work and should not be inferred as done from the filesystem move: + +1. **Plugin + interactor placement/facets** — decide whether `core/platform-plugin/` and + `core/interactors/apple.ts` should move under `src/platforms/apple/` only when the platform-neutral + `providers` / `recording` / `appLog` / `perf` facets from step (b.3) are ready and parity-tested. +2. **tvOS promotion** — split `ios + target:'tv'` into an explicit tvOS leaf only with tests that preserve the + XCUIRemote focus-only contract and unsupported coordinate tap behavior. +3. **Final public platform collapse** — changing public `Platform` from `ios`/`macos` to `apple` is the + highest-diff compatibility step and should remain last. +4. **watchOS sentinel** — watchOS stays out of scope for now; when modeled, it must be an explicit unsupported + sentinel because XCUITest cannot drive watchOS UI. +5. **Per-`AppleOS` capability tables** — replace scattered Apple predicates only after table-equivalence tests + prove byte-for-byte behavior for iOS/iPadOS/tvOS/macOS/visionOS sample devices. **Do-not-flatten (perfect-shape §7):** the iOS XCTest two-finger synthesis (`RunnerSynthesizedGesture`) and adb/idb leaf code stay untouched; the plugin's job is to stop core/daemon BRANCHING on platform, not to diff --git a/scripts/build-xcuitest-apple.sh b/scripts/build-xcuitest-apple.sh index 0da8613a3..9cc8d5aad 100644 --- a/scripts/build-xcuitest-apple.sh +++ b/scripts/build-xcuitest-apple.sh @@ -7,7 +7,7 @@ SCHEME="AgentDeviceRunner" DEFAULT_IOS_RUNNER_APP_BUNDLE_ID="com.callstack.agentdevice.runner" if [ -z "$PLATFORM" ]; then - echo "AGENT_DEVICE_XCUITEST_PLATFORM is required (ios, macos, tvos)" >&2 + echo "AGENT_DEVICE_XCUITEST_PLATFORM is required (ios, macos, tvos, visionos)" >&2 exit 1 fi @@ -33,6 +33,9 @@ resolve_default_destination() { tvos) resolve_simulator_destination 'tvOS' 'Apple TV' || printf '%s\n' 'generic/platform=tvOS Simulator' ;; + visionos) + resolve_simulator_destination 'visionOS' 'Apple Vision' || printf '%s\n' 'generic/platform=visionOS Simulator' + ;; *) echo "Unsupported AGENT_DEVICE_XCUITEST_PLATFORM: $PLATFORM" >&2 exit 1 @@ -85,6 +88,9 @@ resolve_default_derived_path() { tvos) printf '%s\n' "$HOME/.agent-device/ios-runner/derived/tvos" ;; + visionos) + printf '%s\n' "$HOME/.agent-device/ios-runner/derived/visionos" + ;; *) echo "Unsupported AGENT_DEVICE_XCUITEST_PLATFORM: $PLATFORM" >&2 exit 1 @@ -102,7 +108,7 @@ resolve_clean_path() { ios) printf '%s\n' "$DERIVED_PATH/device" ;; - macos|tvos) + macos|tvos|visionos) printf '%s\n' "$DERIVED_PATH" ;; *) diff --git a/scripts/clean-daemon.ts b/scripts/clean-daemon.ts index 88030c9ad..24cb30a03 100644 --- a/scripts/clean-daemon.ts +++ b/scripts/clean-daemon.ts @@ -6,8 +6,8 @@ import { isAgentDeviceDaemonProcess, stopProcessForTakeover, } from '../src/utils/process-identity.ts'; -import { cleanupRunnerLeasesForOwner } from '../src/platforms/ios/runner-lease.ts'; -import { runnerLeaseCleanupAdapter } from '../src/platforms/ios/runner-disposal.ts'; +import { cleanupRunnerLeasesForOwner } from '../src/platforms/apple/core/runner/runner-lease.ts'; +import { runnerLeaseCleanupAdapter } from '../src/platforms/apple/core/runner/runner-disposal.ts'; const DAEMON_TERM_TIMEOUT_MS = 15_000; const DAEMON_KILL_TIMEOUT_MS = 2_000; diff --git a/scripts/clean-xcuitest-derived.mjs b/scripts/clean-xcuitest-derived.mjs index edbc17593..febacb92f 100644 --- a/scripts/clean-xcuitest-derived.mjs +++ b/scripts/clean-xcuitest-derived.mjs @@ -17,13 +17,19 @@ const ROOT_TRANSIENT_ENTRY_NAMES = new Set([ 'info.plist', ]); const platforms = process.argv.slice(2); -const requested = platforms.length > 0 ? platforms : ['ios', 'macos', 'tvos']; -const supported = new Set(['ios', 'macos', 'tvos']); +const requested = platforms.length > 0 ? platforms : ['ios', 'macos', 'tvos', 'visionos']; +const supported = new Set(['ios', 'macos', 'tvos', 'visionos']); +const DERIVED_PATHS = new Map([ + ['ios', DERIVED_ROOT], + ['macos', path.join(DERIVED_ROOT, 'macos')], + ['tvos', path.join(DERIVED_ROOT, 'tvos')], + ['visionos', path.join(DERIVED_ROOT, 'visionos')], +]); for (const platform of requested) { if (!supported.has(platform)) { console.error(`Unsupported XCTest cache platform: ${platform}`); - console.error('Supported platforms: ios, macos, tvos'); + console.error('Supported platforms: ios, macos, tvos, visionos'); process.exitCode = 1; continue; } @@ -48,14 +54,7 @@ function cleanDerivedPath(platform, targetPath) { } function resolveDerivedPath(platform) { - switch (platform) { - case 'ios': - return DERIVED_ROOT; - case 'macos': - return path.join(DERIVED_ROOT, 'macos'); - case 'tvos': - return path.join(DERIVED_ROOT, 'tvos'); - default: - throw new Error(`Unsupported platform: ${platform}`); - } + const targetPath = DERIVED_PATHS.get(platform); + if (targetPath) return targetPath; + throw new Error(`Unsupported platform: ${platform}`); } diff --git a/scripts/patch-xcuitest-runner-icon.ts b/scripts/patch-xcuitest-runner-icon.ts index 368eccab6..8fc61ed93 100644 --- a/scripts/patch-xcuitest-runner-icon.ts +++ b/scripts/patch-xcuitest-runner-icon.ts @@ -1,4 +1,4 @@ -import { applyXctestRunnerAppIconFromDerivedPath } from '../src/platforms/ios/runner-icon.ts'; +import { applyXctestRunnerAppIconFromDerivedPath } from '../src/platforms/apple/core/runner/runner-icon.ts'; const [derivedPath] = process.argv.slice(2); diff --git a/scripts/runner-request-count/expected-counts.json b/scripts/runner-request-count/expected-counts.json deleted file mode 100644 index d51411460..000000000 --- a/scripts/runner-request-count/expected-counts.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "$comment": "Expected iOS runner request-count baseline for the smoke-ios scenario. This gate proves a runner refactor (e.g. Phase 3 step c) does not add/drop runner round-trips. `established: false` means the gate is NOT armed yet: the harness records observed counts instead of asserting. To arm/regenerate, run on a host with a booted iOS simulator: `node --experimental-strip-types scripts/runner-request-count/run.ts --udid --update` (or read the values from the smoke-ios CI step log / uploaded artifact and edit this file), then commit it. Counts: runnerRoundTrips = ios_runner_command_send + ios_runner_readiness_preflight.", - "scenario": "test/integration/replays/ios/simulator/01-settings.ad", - "established": false, - "runnerRoundTrips": 0, - "byPhase": { - "ios_runner_command_send": 0, - "ios_runner_readiness_preflight": 0 - } -} diff --git a/scripts/runner-request-count/run.ts b/scripts/runner-request-count/run.ts deleted file mode 100644 index ad6dee341..000000000 --- a/scripts/runner-request-count/run.ts +++ /dev/null @@ -1,352 +0,0 @@ -#!/usr/bin/env node -/** - * iOS runner request-count gate. - * - * Drives a small, representative iOS replay scenario against a booted simulator - * with `--debug`, reads the per-request diagnostics ndjson the daemon appends to - * `/daemon.log`, counts the iOS-runner round-trip phases, and asserts - * the total is unchanged versus the committed baseline - * (`scripts/runner-request-count/expected-counts.json`). This proves a runner - * refactor (e.g. Phase 3 step c — relocating the shared Apple XCTest runner) - * adds or drops zero runner requests. - * - * Counting/assertion logic is the pure, unit-tested module - * `src/daemon/runner-request-count.ts`; this script is only orchestration + I/O. - * - * Usage: - * node --experimental-strip-types scripts/runner-request-count/run.ts \ - * --udid [--scenario ] [--artifacts-dir

] \ - * [--prepare-timeout-ms ] [--state-dir ] [--keep] [--strict] [--update] - * - * Modes: - * (default) assert observed counts == committed baseline (skips when unarmed). - * --update record observed counts into the committed baseline (arm/regenerate). - * - * Robustness: an infra hiccup (scenario fails to run, or zero round-trips - * captured) is reported as INCONCLUSIVE and does NOT fail the build unless - * `--strict` is passed; only a real count drift against an armed baseline fails. - * - * The CLI invocation defaults to running from source - * (`node --experimental-strip-types src/bin.ts`), matching the iOS workflow. - * Override with AGENT_DEVICE_RUNNER_COUNT_CLI (e.g. the built dist binary path). - */ -import fs from 'node:fs'; -import os from 'node:os'; -import path from 'node:path'; -import { fileURLToPath } from 'node:url'; -import { runCmdSync } from '../../src/utils/exec.ts'; -import { - buildRunnerRequestCountBaseline, - compareRunnerCounts, - countRunnerRequests, - parseRunnerRequestCountBaseline, - RUNNER_ROUND_TRIP_PHASES, - type RunnerRequestCountBaseline, - type RunnerRequestCounts, -} from '../../src/daemon/runner-request-count.ts'; - -const HERE = path.dirname(fileURLToPath(import.meta.url)); -const REPO_ROOT = path.resolve(HERE, '..', '..'); -const BASELINE_PATH = path.join(HERE, 'expected-counts.json'); -const DEFAULT_SCENARIO = 'test/integration/replays/ios/simulator/01-settings.ad'; -const DEFAULT_PREPARE_TIMEOUT_MS = 420_000; -const SCENARIO_TIMEOUT_MS = 600_000; -const MAX_BUFFER = 64 * 1024 * 1024; - -type HarnessConfig = { - mode: 'assert' | 'update'; - udid?: string; - scenario: string; - stateDir?: string; - artifactsDir?: string; - prepareTimeoutMs: number; - strict: boolean; - keep: boolean; -}; - -function log(msg: string): void { - process.stderr.write(`[runner-count] ${msg}\n`); -} - -function readValue(argv: string[], index: number, flag: string): string { - const value = argv[index]; - if (value === undefined) throw new Error(`Missing value for ${flag}`); - return value; -} - -function envPrepareTimeoutMs(): number { - const raw = process.env.AGENT_DEVICE_IOS_PREPARE_TIMEOUT_MS?.trim(); - const parsed = raw ? Number(raw) : NaN; - return Number.isInteger(parsed) && parsed > 0 ? parsed : DEFAULT_PREPARE_TIMEOUT_MS; -} - -// Uncovered CLI arg parser: fallow's CRAP score is inflated for scripts (no test -// coverage feeds the audit), so suppress the complexity finding here. -// fallow-ignore-next-line complexity -function parseArgs(argv: string[]): HarnessConfig { - const cfg: HarnessConfig = { - mode: 'assert', - scenario: DEFAULT_SCENARIO, - prepareTimeoutMs: envPrepareTimeoutMs(), - strict: false, - keep: false, - }; - for (let i = 0; i < argv.length; i++) { - const a = argv[i]!; - if (a === '--update' || a === '--save') cfg.mode = 'update'; - else if (a === '--strict') cfg.strict = true; - else if (a === '--keep') cfg.keep = true; - else if (a === '--help' || a === '-h') { - process.stdout.write(HELP); - process.exit(0); - } else i = applyValueFlag(cfg, a, argv, i); - } - return cfg; -} - -// Apply a flag that consumes the next argv token; returns the advanced index. -// fallow-ignore-next-line complexity -function applyValueFlag(cfg: HarnessConfig, flag: string, argv: string[], i: number): number { - const value = readValue(argv, i + 1, flag); - switch (flag) { - case '--udid': - cfg.udid = value; - break; - case '--scenario': - cfg.scenario = value; - break; - case '--state-dir': - cfg.stateDir = path.resolve(value); - break; - case '--artifacts-dir': - cfg.artifactsDir = path.resolve(value); - break; - case '--prepare-timeout-ms': - cfg.prepareTimeoutMs = Number(value); - break; - default: - throw new Error(`Unknown flag: ${flag}`); - } - return i + 1; -} - -const HELP = `iOS runner request-count gate - - --udid Simulator UDID to drive (required for a real run). - --scenario Replay scenario (default: ${DEFAULT_SCENARIO}). - --artifacts-dir Where to write the replay + observed-count artifacts. - --state-dir Reuse an existing daemon state dir instead of a temp one. - --prepare-timeout-ms Runner prepare timeout (default ${DEFAULT_PREPARE_TIMEOUT_MS}). - --update | --save Record observed counts into the committed baseline. - --strict Fail (not warn) on inconclusive/infra outcomes. - --keep Keep the temp state dir after running. - -h, --help Show this help. -`; - -function cliArgv(): string[] { - const override = process.env.AGENT_DEVICE_RUNNER_COUNT_CLI?.trim(); - if (override) return override.split(/\s+/); - return ['--experimental-strip-types', path.join(REPO_ROOT, 'src', 'bin.ts')]; -} - -function runCli(args: string[], timeoutMs: number): { exitCode: number; stderr: string } { - const full = [...cliArgv(), ...args]; - try { - const result = runCmdSync(process.execPath, full, { - cwd: REPO_ROOT, - maxBuffer: MAX_BUFFER, - allowFailure: true, - timeoutMs, - }); - return { exitCode: result.exitCode, stderr: result.stderr }; - } catch (error) { - return { exitCode: -1, stderr: error instanceof Error ? error.message : String(error) }; - } -} - -function loadBaseline(): RunnerRequestCountBaseline { - const raw = fs.readFileSync(BASELINE_PATH, 'utf8'); - return parseRunnerRequestCountBaseline(JSON.parse(raw) as unknown); -} - -function writeBaseline(scenario: string, counts: RunnerRequestCounts): void { - const baseline = buildRunnerRequestCountBaseline(scenario, counts); - const doc = { - $comment: - 'Expected iOS runner request-count baseline for the smoke-ios scenario. ' + - 'Regenerate with: node --experimental-strip-types scripts/runner-request-count/run.ts --udid --update. ' + - 'runnerRoundTrips = ios_runner_command_send + ios_runner_readiness_preflight.', - ...baseline, - }; - fs.writeFileSync(BASELINE_PATH, `${JSON.stringify(doc, null, 2)}\n`); - log(`baseline updated: ${BASELINE_PATH}`); -} - -function recordObserved(cfg: HarnessConfig, counts: RunnerRequestCounts): void { - const doc = buildRunnerRequestCountBaseline(cfg.scenario, counts); - process.stdout.write(`${JSON.stringify(doc, null, 2)}\n`); - if (!cfg.artifactsDir) return; - try { - fs.mkdirSync(cfg.artifactsDir, { recursive: true }); - fs.writeFileSync( - path.join(cfg.artifactsDir, 'expected-counts.observed.json'), - `${JSON.stringify(doc, null, 2)}\n`, - ); - } catch (error) { - log(`warning: could not write observed-count artifact: ${String(error)}`); - } -} - -function describeCounts(counts: RunnerRequestCounts): string { - const phases = RUNNER_ROUND_TRIP_PHASES.map((p) => `${p}=${counts.byPhase[p]}`).join(', '); - return `runnerRoundTrips=${counts.runnerRoundTrips} (${phases})`; -} - -function inconclusive(cfg: HarnessConfig, reason: string): number { - log(`INCONCLUSIVE: ${reason}`); - if (cfg.strict) { - log('exiting non-zero because --strict was set'); - return 1; - } - log('treating as an infra hiccup (not a count drift); not failing the build'); - return 0; -} - -// Warm the runner WITHOUT --debug so prepare diagnostics never pollute the count. -function prepareRunner(cfg: HarnessConfig, udid: string, stateDir: string): boolean { - log('preparing iOS runner (no --debug)…'); - const prepare = runCli( - [ - 'prepare', - 'ios-runner', - '--platform', - 'ios', - '--udid', - udid, - '--timeout', - String(cfg.prepareTimeoutMs), - '--json', - '--state-dir', - stateDir, - ], - cfg.prepareTimeoutMs + 120_000, - ); - return prepare.exitCode === 0; -} - -// Drive the scenario with --debug (single attempt for a deterministic count) and -// return the run's exit code + the round-trip counts read from the daemon log. -function runScenario( - cfg: HarnessConfig, - udid: string, - stateDir: string, -): { exitCode: number; observed: RunnerRequestCounts } { - // Truncate daemon.log so we count ONLY the scenario's --debug round-trips. - const logPath = path.join(stateDir, 'daemon.log'); - try { - fs.writeFileSync(logPath, ''); - } catch { - /* fresh state dir may not have a log yet; the daemon recreates it */ - } - - log('running scenario with --debug (single attempt)…'); - const args = [ - 'test', - cfg.scenario, - '--udid', - udid, - '--debug', - '--retries', - '0', - '--json', - '--state-dir', - stateDir, - ]; - if (cfg.artifactsDir) args.push('--artifacts-dir', path.join(cfg.artifactsDir, 'replay')); - const exitCode = runCli(args, SCENARIO_TIMEOUT_MS).exitCode; - - let logText = ''; - try { - logText = fs.readFileSync(logPath, 'utf8'); - } catch { - /* no log => zero counts, handled as inconclusive downstream */ - } - return { exitCode, observed: countRunnerRequests(logText) }; -} - -// Assert the observed counts against the committed baseline. Infra hiccups -// (failed run / zero captures) are inconclusive; only a real drift fails. -// fallow-ignore-next-line complexity -function assertObserved( - cfg: HarnessConfig, - exitCode: number, - observed: RunnerRequestCounts, -): number { - if (exitCode !== 0) { - return inconclusive(cfg, `scenario run failed (exit ${exitCode}); likely simulator/infra`); - } - if (observed.runnerRoundTrips === 0) { - return inconclusive( - cfg, - 'scenario passed but zero runner round-trips were captured (likely a capture/infra issue, not a real drift)', - ); - } - const comparison = compareRunnerCounts(loadBaseline(), observed); - if (comparison.status === 'unarmed') { - log('GATE NOT ARMED: committed baseline has established=false.'); - log('Arm it by committing the observed counts above (or re-run with --update).'); - return 0; - } - if (comparison.status === 'match') { - log(`MATCH: ${describeCounts(observed)} == committed baseline. No runner request drift.`); - return 0; - } - log('MISMATCH: iOS runner request count drifted from the committed baseline:'); - for (const diff of comparison.differences) { - log(` ${diff.key}: expected ${diff.expected}, got ${diff.actual}`); - } - log('If this drift is intentional, regenerate the baseline with --update and commit it.'); - return 1; -} - -function runGate(cfg: HarnessConfig, stateDir: string): number { - if (!cfg.udid) return inconclusive(cfg, 'no --udid provided; cannot drive a simulator scenario'); - if (!prepareRunner(cfg, cfg.udid, stateDir)) { - return inconclusive(cfg, 'prepare ios-runner failed'); - } - const { exitCode, observed } = runScenario(cfg, cfg.udid, stateDir); - log(`observed: ${describeCounts(observed)}`); - recordObserved(cfg, observed); - if (cfg.mode === 'update') { - writeBaseline(cfg.scenario, observed); - log('OK: baseline recorded (update mode)'); - return 0; - } - return assertObserved(cfg, exitCode, observed); -} - -function teardown(cfg: HarnessConfig, stateDir: string, createdStateDir: boolean): void { - runCli(['close', '--shutdown', '--state-dir', stateDir], 60_000); - if (!createdStateDir || cfg.keep) return; - try { - fs.rmSync(stateDir, { recursive: true, force: true }); - } catch { - /* best-effort */ - } -} - -function main(): number { - const cfg = parseArgs(process.argv.slice(2)); - const createdStateDir = !cfg.stateDir; - const stateDir = - cfg.stateDir ?? fs.mkdtempSync(path.join(os.tmpdir(), 'agent-device-runner-count-')); - log(`scenario: ${cfg.scenario}`); - log(`state-dir: ${stateDir}${createdStateDir ? ' (temp)' : ''}`); - try { - return runGate(cfg, stateDir); - } finally { - teardown(cfg, stateDir, createdStateDir); - } -} - -process.exit(main()); diff --git a/scripts/write-xcuitest-cache-metadata.mjs b/scripts/write-xcuitest-cache-metadata.mjs index 4c1d0d089..5aa7c6c07 100644 --- a/scripts/write-xcuitest-cache-metadata.mjs +++ b/scripts/write-xcuitest-cache-metadata.mjs @@ -9,7 +9,7 @@ const [platform, derivedPath, destination] = args; if (!platform || !derivedPath || !destination) { console.error( - 'Usage: write-xcuitest-cache-metadata.mjs ', + 'Usage: write-xcuitest-cache-metadata.mjs ', ); process.exit(1); } @@ -111,6 +111,7 @@ function resolvePlatformName() { if (platform === 'ios') return 'iOS'; if (platform === 'tvos') return 'tvOS'; if (platform === 'macos') return 'macOS'; + if (platform === 'visionos') return 'visionOS'; throw new Error(`Unsupported platform: ${platform}`); } @@ -146,6 +147,9 @@ function resolveRunnerSdkName() { if (platformName === 'tvOS') { return resolveDeviceKind() === 'simulator' ? 'appletvsimulator' : 'appletvos'; } + if (platformName === 'visionOS') { + return resolveDeviceKind() === 'simulator' ? 'xrsimulator' : 'xros'; + } return resolveDeviceKind() === 'simulator' ? 'iphonesimulator' : 'iphoneos'; } @@ -314,6 +318,14 @@ function scoreXctestrunCandidate(candidatePath) { } else if (platform === 'macos') { score += basename.includes('macos') || candidatePath.includes(`${path.sep}macos${path.sep}`) ? 100 : 0; + } else if (platform === 'visionos') { + score += destination.includes('Simulator') + ? basename.includes('xrsimulator') + ? 100 + : 0 + : basename.includes('xros') + ? 100 + : 0; } return score; } diff --git a/src/client/client.ts b/src/client/client.ts index a92171984..b866fbd19 100644 --- a/src/client/client.ts +++ b/src/client/client.ts @@ -1,7 +1,7 @@ import { sendToDaemon } from '../daemon/client/daemon-client.ts'; import { prepareMetroRuntime, reloadMetro } from '../metro/client-metro.ts'; import { resolveDaemonPaths } from '../daemon/config.ts'; -import { symbolicateCrashArtifact } from '../platforms/ios/debug-symbols.ts'; +import { symbolicateCrashArtifact } from '../platforms/apple/core/debug-symbols.ts'; import { INTERNAL_COMMANDS } from '../command-catalog.ts'; import { prepareDaemonCommandRequest, diff --git a/src/core/__tests__/dispatch-interactions.test.ts b/src/core/__tests__/dispatch-interactions.test.ts index 82e478e33..e48e6b734 100644 --- a/src/core/__tests__/dispatch-interactions.test.ts +++ b/src/core/__tests__/dispatch-interactions.test.ts @@ -5,8 +5,9 @@ const { mockRunIosRunnerCommand } = vi.hoisted(() => ({ mockRunIosRunnerCommand: vi.fn(), })); -vi.mock('../../platforms/ios/runner-client.ts', async (importOriginal) => { - const actual = await importOriginal(); +vi.mock('../../platforms/apple/core/runner/runner-client.ts', async (importOriginal) => { + const actual = + await importOriginal(); return { ...actual, runIosRunnerCommand: mockRunIosRunnerCommand }; }); @@ -19,12 +20,12 @@ import { handleTransformGestureCommand, } from '../dispatch-interactions.ts'; import type { Interactor } from '../interactor-types.ts'; -import type { RunnerCommand } from '../../platforms/ios/runner-contract.ts'; +import type { RunnerCommand } from '../../platforms/apple/core/runner/runner-contract.ts'; import { AppError } from '../../kernel/errors.ts'; import { ANDROID_EMULATOR, IOS_SIMULATOR } from '../../__tests__/test-utils/device-fixtures.ts'; -vi.mock('../../platforms/ios/macos-helper.ts', async (importOriginal) => { - const actual = await importOriginal(); +vi.mock('../../platforms/apple/os/macos/helper.ts', async (importOriginal) => { + const actual = await importOriginal(); return { ...actual, runMacOsPressAction: vi.fn(async () => ({})), diff --git a/src/core/__tests__/dispatch-keyboard.test.ts b/src/core/__tests__/dispatch-keyboard.test.ts index 8cd0a8c2e..6ede1afd3 100644 --- a/src/core/__tests__/dispatch-keyboard.test.ts +++ b/src/core/__tests__/dispatch-keyboard.test.ts @@ -2,13 +2,14 @@ import { beforeEach, test, vi } from 'vitest'; import assert from 'node:assert/strict'; import { promises as fs } from 'node:fs'; -vi.mock('../../platforms/ios/runner-client.ts', async (importOriginal) => { - const actual = await importOriginal(); +vi.mock('../../platforms/apple/core/runner/runner-client.ts', async (importOriginal) => { + const actual = + await importOriginal(); return { ...actual, runIosRunnerCommand: vi.fn() }; }); import { dispatchCommand } from '../dispatch.ts'; -import { runIosRunnerCommand } from '../../platforms/ios/runner-client.ts'; +import { runIosRunnerCommand } from '../../platforms/apple/core/runner/runner-client.ts'; import { ANDROID_EMULATOR, IOS_DEVICE } from '../../__tests__/test-utils/device-fixtures.ts'; import { withMockedAdb } from '../../__tests__/test-utils/mocked-binaries.ts'; diff --git a/src/core/__tests__/dispatch-open.test.ts b/src/core/__tests__/dispatch-open.test.ts index 0a61844ed..62ecb7881 100644 --- a/src/core/__tests__/dispatch-open.test.ts +++ b/src/core/__tests__/dispatch-open.test.ts @@ -3,13 +3,13 @@ import assert from 'node:assert/strict'; import { dispatchCommand } from '../dispatch.ts'; import { AppError } from '../../kernel/errors.ts'; import type { DeviceInfo } from '../../kernel/device.ts'; -import { openIosApp, setIosSetting } from '../../platforms/ios/apps.ts'; +import { openIosApp, setIosSetting } from '../../platforms/apple/core/apps.ts'; import { openAndroidApp } from '../../platforms/android/app-lifecycle.ts'; import { setAndroidSetting } from '../../platforms/android/settings.ts'; import { IOS_SIMULATOR } from '../../__tests__/test-utils/device-fixtures.ts'; -vi.mock('../../platforms/ios/apps.ts', async (importOriginal) => { - const actual = await importOriginal(); +vi.mock('../../platforms/apple/core/apps.ts', async (importOriginal) => { + const actual = await importOriginal(); return { ...actual, openIosApp: vi.fn(async () => {}), diff --git a/src/core/__tests__/dispatch-resolve.test.ts b/src/core/__tests__/dispatch-resolve.test.ts index 794b35ba1..f0c8dd414 100644 --- a/src/core/__tests__/dispatch-resolve.test.ts +++ b/src/core/__tests__/dispatch-resolve.test.ts @@ -6,8 +6,8 @@ const { mockFindBootableIosSimulator, mockListAppleDevices } = vi.hoisted(() => mockListAppleDevices: vi.fn(), })); -vi.mock('../../platforms/ios/devices.ts', async (importOriginal) => { - const actual = await importOriginal(); +vi.mock('../../platforms/apple/core/devices.ts', async (importOriginal) => { + const actual = await importOriginal(); return { ...actual, findBootableIosSimulator: mockFindBootableIosSimulator, diff --git a/src/core/__tests__/dispatch-rotate.test.ts b/src/core/__tests__/dispatch-rotate.test.ts index bd78b7469..84e5e152f 100644 --- a/src/core/__tests__/dispatch-rotate.test.ts +++ b/src/core/__tests__/dispatch-rotate.test.ts @@ -2,13 +2,14 @@ import { beforeEach, test, vi } from 'vitest'; import assert from 'node:assert/strict'; import { promises as fs } from 'node:fs'; -vi.mock('../../platforms/ios/runner-client.ts', async (importOriginal) => { - const actual = await importOriginal(); +vi.mock('../../platforms/apple/core/runner/runner-client.ts', async (importOriginal) => { + const actual = + await importOriginal(); return { ...actual, runIosRunnerCommand: vi.fn() }; }); import { dispatchCommand } from '../dispatch.ts'; -import { runIosRunnerCommand } from '../../platforms/ios/runner-client.ts'; +import { runIosRunnerCommand } from '../../platforms/apple/core/runner/runner-client.ts'; import { ANDROID_EMULATOR, IOS_DEVICE } from '../../__tests__/test-utils/device-fixtures.ts'; import { withMockedAdb } from '../../__tests__/test-utils/mocked-binaries.ts'; diff --git a/src/core/dispatch-interactions.ts b/src/core/dispatch-interactions.ts index e0794ac9a..c84eb7953 100644 --- a/src/core/dispatch-interactions.ts +++ b/src/core/dispatch-interactions.ts @@ -45,8 +45,8 @@ import { MAX_RUNNER_SEQUENCE_STEPS, buildRunnerSequenceCommand, parseRunnerSequenceResult, -} from '../platforms/ios/runner-sequence.ts'; -import type { RunnerSequenceStep } from '../platforms/ios/runner-contract.ts'; +} from '../platforms/apple/core/runner/runner-sequence.ts'; +import type { RunnerSequenceStep } from '../platforms/apple/core/runner/runner-contract.ts'; import type { DispatchContext } from './dispatch-context.ts'; import type { Interactor, RunnerCallOptions } from './interactor-types.ts'; @@ -226,7 +226,7 @@ async function handleMacOsSurfacePress( `${clickButton} click is not supported on macOS ${context.surface} sessions.`, ); } - const { runMacOsPressAction } = await import('../platforms/ios/macos-helper.ts'); + const { runMacOsPressAction } = await import('../platforms/apple/os/macos/helper.ts'); await runMacOsPressAction(x, y, { bundleId: context.appBundleId, surface: context.surface, @@ -245,7 +245,7 @@ async function handleAlternateClick( if (device.platform === 'linux') { return await runLinuxAlternateClick(x, y, button); } - const { runIosRunnerCommand } = await import('../platforms/ios/runner-client.ts'); + const { runIosRunnerCommand } = await import('../platforms/apple/core/runner/runner-client.ts'); await runIosRunnerCommand( device, { @@ -346,7 +346,7 @@ async function runIosSequenceChunks( steps: RunnerSequenceStep[], context: DispatchContext | undefined, ): Promise> { - const { runIosRunnerCommand } = await import('../platforms/ios/runner-client.ts'); + const { runIosRunnerCommand } = await import('../platforms/apple/core/runner/runner-client.ts'); const chunks = chunkRunnerSequenceStepsByBudget(steps, MAX_RUNNER_SEQUENCE_STEPS); let firstChunkRunnerResult: Record | undefined; @@ -1049,7 +1049,7 @@ export async function handleReadCommand( return { action: 'read', text }; } if (device.platform === 'macos' && context?.surface && context.surface !== 'app') { - const { runMacOsReadTextAction } = await import('../platforms/ios/macos-helper.ts'); + const { runMacOsReadTextAction } = await import('../platforms/apple/os/macos/helper.ts'); const result = await runMacOsReadTextAction(x, y, { bundleId: context.appBundleId, surface: context.surface, @@ -1057,7 +1057,7 @@ export async function handleReadCommand( return { action: 'read', text: result.text }; } // macOS app sessions run through the XCUITest runner; only desktop/menubar surfaces use the helper. - const { runIosRunnerCommand } = await import('../platforms/ios/runner-client.ts'); + const { runIosRunnerCommand } = await import('../platforms/apple/core/runner/runner-client.ts'); const result = await runIosRunnerCommand( device, { diff --git a/src/core/dispatch-resolve.ts b/src/core/dispatch-resolve.ts index fb4e467ee..2def47b95 100644 --- a/src/core/dispatch-resolve.ts +++ b/src/core/dispatch-resolve.ts @@ -63,7 +63,7 @@ async function resolveAppleDevice( const selected = await resolveAppleDeviceCandidate(devices, selector, context); if (shouldUseAppleSimulatorFallback(selector, selected)) { - const { findBootableIosSimulator } = await import('../platforms/ios/devices.ts'); + const { findBootableIosSimulator } = await import('../platforms/apple/core/devices.ts'); const simulator = await findBootableIosSimulator({ simulatorSetPath: context.simulatorSetPath, target: selector.target, diff --git a/src/core/dispatch.ts b/src/core/dispatch.ts index cca5a379d..d6db7b6eb 100644 --- a/src/core/dispatch.ts +++ b/src/core/dispatch.ts @@ -473,7 +473,7 @@ async function handleIosKeyboardCommand( ); } if (action === 'enter' || action === 'return') { - const { runIosRunnerCommand } = await import('../platforms/ios/runner-client.ts'); + const { runIosRunnerCommand } = await import('../platforms/apple/core/runner/runner-client.ts'); const result = await runIosRunnerCommand( device, { command: 'keyboardReturn', appBundleId: context?.appBundleId }, @@ -487,7 +487,7 @@ async function handleIosKeyboardCommand( ...successText('Keyboard enter pressed'), }; } - const { runIosRunnerCommand } = await import('../platforms/ios/runner-client.ts'); + const { runIosRunnerCommand } = await import('../platforms/apple/core/runner/runner-client.ts'); const result = await runIosRunnerCommand( device, { command: 'keyboardDismiss', appBundleId: context?.appBundleId }, @@ -590,7 +590,7 @@ async function handlePushCommand( } const payload = await readNotificationPayload(payloadArg); if (device.platform === 'ios') { - const { pushIosNotification } = await import('../platforms/ios/apps.ts'); + const { pushIosNotification } = await import('../platforms/apple/core/apps.ts'); await pushIosNotification(device, target, payload); return { platform: 'ios', diff --git a/src/core/interactors/apple.ts b/src/core/interactors/apple.ts index 3db89cef6..87ec13057 100644 --- a/src/core/interactors/apple.ts +++ b/src/core/interactors/apple.ts @@ -6,14 +6,14 @@ import { screenshotIos, setIosSetting, writeIosClipboardText, -} from '../../platforms/ios/apps.ts'; +} from '../../platforms/apple/core/apps.ts'; import { appleRemotePressCommand, iosRunnerOverrides, resolveAppleBackRunnerCommand, } from '../../platforms/ios/interactions.ts'; -import { runMacOsScreenshotAction } from '../../platforms/ios/macos-helper.ts'; -import { runIosRunnerCommand } from '../../platforms/ios/runner-client.ts'; +import { runMacOsScreenshotAction } from '../../platforms/apple/os/macos/helper.ts'; +import { runIosRunnerCommand } from '../../platforms/apple/core/runner/runner-client.ts'; import { withDiagnosticTimer } from '../../utils/diagnostics.ts'; import type { DeviceInfo } from '../../kernel/device.ts'; import { AppError } from '../../kernel/errors.ts'; diff --git a/src/core/platform-inventory.ts b/src/core/platform-inventory.ts index 35b368a47..969793ff1 100644 --- a/src/core/platform-inventory.ts +++ b/src/core/platform-inventory.ts @@ -33,7 +33,7 @@ export async function listLocalDeviceInventory( } if (shouldUseHostMacFastPath(request)) { - const { listMacosDevices } = await import('../platforms/macos/devices.ts'); + const { listMacosDevices } = await import('../platforms/apple/os/macos/devices.ts'); return await listMacosDevices(); } @@ -52,7 +52,7 @@ export async function listLocalDeviceInventory( } if (request.platform) { - const { listAppleDevices } = await import('../platforms/ios/devices.ts'); + const { listAppleDevices } = await import('../platforms/apple/core/devices.ts'); return await listAppleDevices({ simulatorSetPath: request.iosSimulatorSetPath, udid: request.udid, @@ -71,7 +71,7 @@ export async function listLocalDeviceInventory( ); } catch {} try { - const { listAppleDevices } = await import('../platforms/ios/devices.ts'); + const { listAppleDevices } = await import('../platforms/apple/core/devices.ts'); devices.push( ...(await listAppleDevices({ simulatorSetPath: request.iosSimulatorSetPath, diff --git a/src/core/platform-plugin/register-builtins.ts b/src/core/platform-plugin/register-builtins.ts index 23aee7e9a..f18e92c3f 100644 --- a/src/core/platform-plugin/register-builtins.ts +++ b/src/core/platform-plugin/register-builtins.ts @@ -26,10 +26,10 @@ const applePlugin = { // inventory if-chain, reusing the SAME predicate (no divergent copy). discoverDevices: async (request: DeviceInventoryRequest) => { if (shouldUseHostMacFastPath(request)) { - const { listMacosDevices } = await import('../../platforms/macos/devices.ts'); + const { listMacosDevices } = await import('../../platforms/apple/os/macos/devices.ts'); return await listMacosDevices(); } - const { listAppleDevices } = await import('../../platforms/ios/devices.ts'); + const { listAppleDevices } = await import('../../platforms/apple/core/devices.ts'); return await listAppleDevices({ simulatorSetPath: request.iosSimulatorSetPath, udid: request.udid, diff --git a/src/daemon-runtime.ts b/src/daemon-runtime.ts index faa1db94e..be1f7cfd0 100644 --- a/src/daemon-runtime.ts +++ b/src/daemon-runtime.ts @@ -33,7 +33,7 @@ import { } from './daemon/transport.ts'; import { prewarmPngWorker, terminatePngWorker } from './utils/png-worker-client.ts'; import { sleep } from './utils/timeouts.ts'; -import { setRunnerLeaseOwnerStateDir } from './platforms/ios/runner-lease.ts'; +import { setRunnerLeaseOwnerStateDir } from './platforms/apple/core/runner/runner-lease.ts'; const DAEMON_SESSION_TEARDOWN_TIMEOUT_MS = 5_000; const DAEMON_PNG_WORKER_TERMINATE_TIMEOUT_MS = 1_000; @@ -223,7 +223,8 @@ export async function startDaemonRuntime( } await closeDaemonServers(servers); await teardownDaemonSessions(); - const { stopAllIosRunnerSessions } = await import('./platforms/ios/runner-client.ts'); + const { stopAllIosRunnerSessions } = + await import('./platforms/apple/core/runner/runner-client.ts'); await stopAllIosRunnerSessions(); // Best effort: stop the PNG worker so an in-flight job cannot delay exit. await Promise.race([ diff --git a/src/daemon/__tests__/device-ready.test.ts b/src/daemon/__tests__/device-ready.test.ts index d4d9cdfb5..27780279f 100644 --- a/src/daemon/__tests__/device-ready.test.ts +++ b/src/daemon/__tests__/device-ready.test.ts @@ -8,7 +8,7 @@ vi.mock('../../utils/exec.ts', () => ({ runCmdSync: vi.fn(), whichCmd: vi.fn(async () => true), })); -vi.mock('../../platforms/ios/simulator.ts', () => ({ +vi.mock('../../platforms/apple/core/simulator.ts', () => ({ ensureBootedSimulator: vi.fn(async () => {}), })); vi.mock('../../platforms/android/devices.ts', () => ({ @@ -17,7 +17,7 @@ vi.mock('../../platforms/android/devices.ts', () => ({ import { runCmd } from '../../utils/exec.ts'; import { waitForAndroidBoot } from '../../platforms/android/devices.ts'; -import { ensureBootedSimulator } from '../../platforms/ios/simulator.ts'; +import { ensureBootedSimulator } from '../../platforms/apple/core/simulator.ts'; import { ANDROID_EMULATOR, IOS_DEVICE, IOS_SIMULATOR } from '../../__tests__/test-utils/index.ts'; import { clearDeviceReadyCacheForTests, diff --git a/src/daemon/__tests__/request-platform-providers.test.ts b/src/daemon/__tests__/request-platform-providers.test.ts index 44d33465c..47820aa0b 100644 --- a/src/daemon/__tests__/request-platform-providers.test.ts +++ b/src/daemon/__tests__/request-platform-providers.test.ts @@ -9,7 +9,10 @@ import { makeSession, } from '../../__tests__/test-utils/index.ts'; import { withTargetDeviceResolutionScope } from '../../core/dispatch-resolve.ts'; -import { createLocalAppleToolProvider, runXcrun } from '../../platforms/ios/tool-provider.ts'; +import { + createLocalAppleToolProvider, + runXcrun, +} from '../../platforms/apple/core/tool-provider.ts'; import { resolveWebProvider, type WebProvider } from '../../platforms/web/provider.ts'; import type { DeviceInfo } from '../../kernel/device.ts'; import { withRequestPlatformProviderScope } from '../request-platform-providers.ts'; diff --git a/src/daemon/__tests__/request-recording-health.test.ts b/src/daemon/__tests__/request-recording-health.test.ts index 87f026fb6..1deb8bbad 100644 --- a/src/daemon/__tests__/request-recording-health.test.ts +++ b/src/daemon/__tests__/request-recording-health.test.ts @@ -1,11 +1,11 @@ import { test, expect, vi, beforeEach } from 'vitest'; import type { SessionState } from '../types.ts'; -vi.mock('../../platforms/ios/runner-client.ts', () => ({ +vi.mock('../../platforms/apple/core/runner/runner-client.ts', () => ({ getRunnerSessionSnapshot: vi.fn(), })); -import { getRunnerSessionSnapshot } from '../../platforms/ios/runner-client.ts'; +import { getRunnerSessionSnapshot } from '../../platforms/apple/core/runner/runner-client.ts'; import { refreshRecordingHealth } from '../request-recording-health.ts'; const mockGetRunnerSessionSnapshot = vi.mocked(getRunnerSessionSnapshot); diff --git a/src/daemon/__tests__/request-router-cost.test.ts b/src/daemon/__tests__/request-router-cost.test.ts index b2fce0af0..d32bf15fe 100644 --- a/src/daemon/__tests__/request-router-cost.test.ts +++ b/src/daemon/__tests__/request-router-cost.test.ts @@ -7,8 +7,9 @@ vi.mock('../../core/dispatch.ts', async (importOriginal) => { return { ...actual, dispatchCommand: vi.fn(async () => ({})) }; }); -vi.mock('../../platforms/ios/runner-client.ts', async (importOriginal) => { - const actual = await importOriginal(); +vi.mock('../../platforms/apple/core/runner/runner-client.ts', async (importOriginal) => { + const actual = + await importOriginal(); return { ...actual, stopIosRunnerSession: vi.fn(async () => {}) }; }); @@ -20,7 +21,6 @@ import type { DaemonRequest, SessionState } from '../types.ts'; import { LeaseRegistry } from '../lease-registry.ts'; import { makeSessionStore } from '../../__tests__/test-utils/store-factory.ts'; import { daemonCommandRequestSchema } from '../../kernel/contracts.ts'; -import { emitDiagnostic } from '../../utils/diagnostics.ts'; const mockDispatch = vi.mocked(dispatchCommand); @@ -110,12 +110,9 @@ test('(b) flag-on additive-only: cost block is the ONLY delta vs flag-off', asyn expect(respFlagOn.ok).toBe(true); if (!respFlagOff.ok || !respFlagOn.ok) return; - // The cost block now carries BOTH wallClockMs and runnerRoundTrips, both - // numbers ≥ 0. A request that never touches the iOS runner reports 0 — honest. const cost = respFlagOn.data?.cost; expect(cost).toMatchObject({ wallClockMs: expect.any(Number), - runnerRoundTrips: 0, }); expect(cost?.wallClockMs).toBeGreaterThanOrEqual(0); // This payload has no node tree, so nodeCount is omitted entirely. @@ -126,30 +123,7 @@ test('(b) flag-on additive-only: cost block is the ONLY delta vs flag-off', asyn expect(respFlagOn.data).toEqual(respFlagOff.data); }); -test('(c) runnerRoundTrips counts real iOS-runner round-trip diagnostics in scope', async () => { - const { sessionStore, handler } = makeHandler(); - sessionStore.set('cost-session', makeIosSession('cost-session')); - - // The mocked dispatch runs inside the request's diagnostics scope, so emitting - // here is equivalent to the runner-session emitting these phases per round-trip. - mockDispatch.mockImplementation(async () => { - emitDiagnostic({ phase: 'ios_runner_readiness_preflight' }); // real round-trip - emitDiagnostic({ phase: 'ios_runner_command_send' }); // real round-trip - emitDiagnostic({ phase: 'ios_runner_command_send' }); // real round-trip - emitDiagnostic({ level: 'debug', phase: 'ios_runner_readiness_preflight_skipped' }); // NOT - emitDiagnostic({ phase: 'some_other_phase' }); // NOT - return { ...REPRESENTATIVE_PAYLOAD }; - }); - - const resp = await handler(baseRequest({ meta: { includeCost: true } })); - expect(resp.ok).toBe(true); - if (!resp.ok) return; - // 1 preflight + 2 command_send = 3; the _skipped marker and unrelated phases - // are excluded. - expect(resp.data?.cost?.runnerRoundTrips).toBe(3); -}); - -test('(c2) nodeCount reports the node-tree size whenever data carries a nodes array, additive-only', async () => { +test('(c) nodeCount reports the node-tree size whenever data carries a nodes array, additive-only', async () => { const { sessionStore, handler } = makeHandler(); sessionStore.set('cost-session', makeIosSession('cost-session')); diff --git a/src/daemon/__tests__/request-router-lock-policy.test.ts b/src/daemon/__tests__/request-router-lock-policy.test.ts index af2238472..bdbbf2e07 100644 --- a/src/daemon/__tests__/request-router-lock-policy.test.ts +++ b/src/daemon/__tests__/request-router-lock-policy.test.ts @@ -7,8 +7,9 @@ vi.mock('../../core/dispatch.ts', async (importOriginal) => { return { ...actual, dispatchCommand: vi.fn(async () => ({})) }; }); -vi.mock('../../platforms/ios/runner-client.ts', async (importOriginal) => { - const actual = await importOriginal(); +vi.mock('../../platforms/apple/core/runner/runner-client.ts', async (importOriginal) => { + const actual = + await importOriginal(); return { ...actual, stopIosRunnerSession: vi.fn(async () => {}) }; }); diff --git a/src/daemon/__tests__/request-router-recording-health.test.ts b/src/daemon/__tests__/request-router-recording-health.test.ts index 53caaf8fa..9ba13a2ed 100644 --- a/src/daemon/__tests__/request-router-recording-health.test.ts +++ b/src/daemon/__tests__/request-router-recording-health.test.ts @@ -7,12 +7,12 @@ vi.mock('../../core/dispatch.ts', async (importOriginal) => { return { ...actual, dispatchCommand: vi.fn(async () => ({})) }; }); -vi.mock('../../platforms/ios/runner-client.ts', () => ({ +vi.mock('../../platforms/apple/core/runner/runner-client.ts', () => ({ getRunnerSessionSnapshot: vi.fn(), })); import { dispatchCommand } from '../../core/dispatch.ts'; -import { getRunnerSessionSnapshot } from '../../platforms/ios/runner-client.ts'; +import { getRunnerSessionSnapshot } from '../../platforms/apple/core/runner/runner-client.ts'; import { createRequestHandler } from '../request-router.ts'; import type { SessionState } from '../types.ts'; import { LeaseRegistry } from '../lease-registry.ts'; diff --git a/src/daemon/__tests__/request-router-response-level.test.ts b/src/daemon/__tests__/request-router-response-level.test.ts index a5de26780..fd808d78e 100644 --- a/src/daemon/__tests__/request-router-response-level.test.ts +++ b/src/daemon/__tests__/request-router-response-level.test.ts @@ -7,8 +7,9 @@ vi.mock('../../core/dispatch.ts', async (importOriginal) => { return { ...actual, dispatchCommand: vi.fn(async () => ({})) }; }); -vi.mock('../../platforms/ios/runner-client.ts', async (importOriginal) => { - const actual = await importOriginal(); +vi.mock('../../platforms/apple/core/runner/runner-client.ts', async (importOriginal) => { + const actual = + await importOriginal(); return { ...actual, stopIosRunnerSession: vi.fn(async () => {}) }; }); @@ -125,7 +126,6 @@ test('(d) digest composes with --cost: viewed data plus an additive cost block', if (!resp.ok) return; expect(resp.data).toMatchObject({ homeDigest: true, hadItems: true }); expect(typeof resp.data?.cost?.wallClockMs).toBe('number'); - expect(resp.data?.cost?.runnerRoundTrips).toBe(0); }); test('(e) digest on a command with no registered view is byte-identical to default', async () => { diff --git a/src/daemon/__tests__/request-router-typed-error.test.ts b/src/daemon/__tests__/request-router-typed-error.test.ts index 0f8a04993..2529115f4 100644 --- a/src/daemon/__tests__/request-router-typed-error.test.ts +++ b/src/daemon/__tests__/request-router-typed-error.test.ts @@ -7,8 +7,9 @@ vi.mock('../../core/dispatch.ts', async (importOriginal) => { return { ...actual, dispatchCommand: vi.fn(async () => ({})) }; }); -vi.mock('../../platforms/ios/runner-client.ts', async (importOriginal) => { - const actual = await importOriginal(); +vi.mock('../../platforms/apple/core/runner/runner-client.ts', async (importOriginal) => { + const actual = + await importOriginal(); return { ...actual, stopIosRunnerSession: vi.fn(async () => {}) }; }); diff --git a/src/daemon/__tests__/runner-request-count.test.ts b/src/daemon/__tests__/runner-request-count.test.ts deleted file mode 100644 index 5ee913002..000000000 --- a/src/daemon/__tests__/runner-request-count.test.ts +++ /dev/null @@ -1,191 +0,0 @@ -import { test, expect } from 'vitest'; -import { - buildRunnerRequestCountBaseline, - compareRunnerCounts, - countRunnerRequests, - emptyRunnerRequestCounts, - parseDiagnosticNdjson, - parseRunnerRequestCountBaseline, - RUNNER_ROUND_TRIP_PHASES, - type RunnerRequestCountBaseline, -} from '../runner-request-count.ts'; - -// A representative daemon `--debug` daemon.log capture: plain (non-JSON) daemon -// log lines interleaved with diagnostic ndjson, plus a stderr-prefixed line, a -// blank line, and a malformed JSON line — every one of which the tolerant parser -// must skip without throwing. -function ndjsonLine(phase: string, extra: Record = {}): string { - return JSON.stringify({ - ts: '2026-06-30T00:00:00.000Z', - level: 'info', - phase, - session: 'gate', - requestId: 'req-1', - command: 'click', - durationMs: 12, - ...extra, - }); -} - -const SAMPLE_LOG = [ - '[daemon] started, pid 4242', - ndjsonLine('ios_runner_readiness_preflight'), - ndjsonLine('ios_runner_command_send', { command: 'open' }), - '', - ndjsonLine('ios_runner_command_send', { command: 'click' }), - // Not a round-trip — the daemon excludes these explicitly. - ndjsonLine('ios_runner_readiness_preflight_skipped'), - // Unrelated phase from another subsystem. - ndjsonLine('android_adb_shell'), - '{ this is not valid json', - `[agent-device][diag] ${ndjsonLine('ios_runner_command_send', { command: 'back' })}`, - '[daemon] request complete', -].join('\n'); - -test('parseDiagnosticNdjson skips non-JSON, blank, malformed, and phaseless lines', () => { - const events = parseDiagnosticNdjson(SAMPLE_LOG); - // 5 well-formed diagnostic events (4 runner phases + 1 unrelated + 1 skipped = 6), - // including the stderr-prefixed one; the plain log lines and bad JSON are dropped. - expect(events.map((e) => e.phase)).toEqual([ - 'ios_runner_readiness_preflight', - 'ios_runner_command_send', - 'ios_runner_command_send', - 'ios_runner_readiness_preflight_skipped', - 'android_adb_shell', - 'ios_runner_command_send', - ]); -}); - -test('parseDiagnosticNdjson strips the stderr diagnostic prefix', () => { - const events = parseDiagnosticNdjson( - `[agent-device][diag] ${ndjsonLine('ios_runner_command_send')}`, - ); - expect(events).toHaveLength(1); - expect(events[0]?.phase).toBe('ios_runner_command_send'); - expect(events[0]?.command).toBe('click'); -}); - -test('countRunnerRequests counts only the two round-trip phases (from text)', () => { - const counts = countRunnerRequests(SAMPLE_LOG); - expect(counts.runnerRoundTrips).toBe(4); - expect(counts.byPhase).toEqual({ - ios_runner_command_send: 3, - ios_runner_readiness_preflight: 1, - }); -}); - -test('countRunnerRequests matches the in-process counting semantics (3 round-trips)', () => { - // Mirrors request-router-cost.test.ts: 1 preflight + 2 command_send + a skipped - // marker + an unrelated phase => 3 runner round-trips. - const events = parseDiagnosticNdjson( - [ - ndjsonLine('ios_runner_readiness_preflight'), - ndjsonLine('ios_runner_command_send'), - ndjsonLine('ios_runner_command_send'), - ndjsonLine('ios_runner_readiness_preflight_skipped'), - ndjsonLine('snapshot_capture'), - ].join('\n'), - ); - expect(countRunnerRequests(events).runnerRoundTrips).toBe(3); -}); - -test('countRunnerRequests on empty input is zeroed', () => { - expect(countRunnerRequests('')).toEqual(emptyRunnerRequestCounts()); - expect(emptyRunnerRequestCounts().runnerRoundTrips).toBe(0); -}); - -test('RUNNER_ROUND_TRIP_PHASES is the documented pair', () => { - expect([...RUNNER_ROUND_TRIP_PHASES]).toEqual([ - 'ios_runner_command_send', - 'ios_runner_readiness_preflight', - ]); -}); - -// --- baseline parse + compare ------------------------------------------------ - -const ARMED_BASELINE: RunnerRequestCountBaseline = { - scenario: 'test/integration/replays/ios/simulator/01-settings.ad', - established: true, - runnerRoundTrips: 4, - byPhase: { ios_runner_command_send: 3, ios_runner_readiness_preflight: 1 }, -}; - -test('parseRunnerRequestCountBaseline validates and ignores unknown keys', () => { - const parsed = parseRunnerRequestCountBaseline({ - $comment: 'regenerate with --update', - scenario: ARMED_BASELINE.scenario, - established: true, - runnerRoundTrips: 4, - byPhase: { ios_runner_command_send: 3, ios_runner_readiness_preflight: 1 }, - }); - expect(parsed).toEqual(ARMED_BASELINE); -}); - -test('parseRunnerRequestCountBaseline treats missing/false established as unarmed', () => { - const parsed = parseRunnerRequestCountBaseline({ - scenario: ARMED_BASELINE.scenario, - runnerRoundTrips: 0, - byPhase: { ios_runner_command_send: 0, ios_runner_readiness_preflight: 0 }, - }); - expect(parsed.established).toBe(false); -}); - -test('parseRunnerRequestCountBaseline rejects malformed payloads', () => { - expect(() => parseRunnerRequestCountBaseline(null)).toThrow(/must be a JSON object/); - expect(() => parseRunnerRequestCountBaseline({ byPhase: {} })).toThrow(/scenario/); - expect(() => parseRunnerRequestCountBaseline({ scenario: 'x', runnerRoundTrips: 1 })).toThrow( - /byPhase/, - ); - expect(() => - parseRunnerRequestCountBaseline({ - scenario: 'x', - runnerRoundTrips: -1, - byPhase: { ios_runner_command_send: 0, ios_runner_readiness_preflight: 0 }, - }), - ).toThrow(/non-negative integer/); -}); - -test('compareRunnerCounts skips assertion when the baseline is unarmed', () => { - const unarmed = parseRunnerRequestCountBaseline({ - scenario: ARMED_BASELINE.scenario, - established: false, - runnerRoundTrips: 0, - byPhase: { ios_runner_command_send: 0, ios_runner_readiness_preflight: 0 }, - }); - expect(compareRunnerCounts(unarmed, countRunnerRequests(SAMPLE_LOG))).toEqual({ - status: 'unarmed', - }); -}); - -test('compareRunnerCounts matches identical counts', () => { - expect(compareRunnerCounts(ARMED_BASELINE, countRunnerRequests(SAMPLE_LOG))).toEqual({ - status: 'match', - }); -}); - -test('compareRunnerCounts reports per-key differences on drift', () => { - // Drop one command_send (a runner refactor that removed a request). - const drifted = countRunnerRequests( - [ - ndjsonLine('ios_runner_readiness_preflight'), - ndjsonLine('ios_runner_command_send'), - ndjsonLine('ios_runner_command_send'), - ].join('\n'), - ); - const result = compareRunnerCounts(ARMED_BASELINE, drifted); - expect(result).toEqual({ - status: 'mismatch', - differences: [ - { key: 'runnerRoundTrips', expected: 4, actual: 3 }, - { key: 'ios_runner_command_send', expected: 3, actual: 2 }, - ], - }); -}); - -test('buildRunnerRequestCountBaseline arms a baseline from observed counts', () => { - const baseline = buildRunnerRequestCountBaseline( - ARMED_BASELINE.scenario, - countRunnerRequests(SAMPLE_LOG), - ); - expect(baseline).toEqual(ARMED_BASELINE); -}); diff --git a/src/daemon/__tests__/target-shutdown.test.ts b/src/daemon/__tests__/target-shutdown.test.ts index 141a93278..291d192bc 100644 --- a/src/daemon/__tests__/target-shutdown.test.ts +++ b/src/daemon/__tests__/target-shutdown.test.ts @@ -1,8 +1,8 @@ import { beforeEach, expect, test, vi } from 'vitest'; import type { DeviceInfo } from '../../kernel/device.ts'; -vi.mock('../../platforms/ios/simulator.ts', async (importOriginal) => { - const actual = await importOriginal(); +vi.mock('../../platforms/apple/core/simulator.ts', async (importOriginal) => { + const actual = await importOriginal(); return { ...actual, getSimulatorState: vi.fn(), @@ -16,7 +16,7 @@ vi.mock('../../utils/exec.ts', async (importOriginal) => { }); import { shutdownDeviceTarget } from '../target-shutdown.ts'; -import { getSimulatorState, shutdownSimulator } from '../../platforms/ios/simulator.ts'; +import { getSimulatorState, shutdownSimulator } from '../../platforms/apple/core/simulator.ts'; import { runCmd } from '../../utils/exec.ts'; const mockGetSimulatorState = vi.mocked(getSimulatorState); diff --git a/src/daemon/app-log-ios.ts b/src/daemon/app-log-ios.ts index db9383265..9b14fa176 100644 --- a/src/daemon/app-log-ios.ts +++ b/src/daemon/app-log-ios.ts @@ -1,8 +1,8 @@ import fs from 'node:fs'; import path from 'node:path'; -import { buildSimctlArgs } from '../platforms/ios/simctl.ts'; +import { buildSimctlArgs } from '../platforms/apple/core/simctl.ts'; import { runCmd, runCmdBackground } from '../utils/exec.ts'; -import { runXcrun } from '../platforms/ios/tool-provider.ts'; +import { runXcrun } from '../platforms/apple/core/tool-provider.ts'; import { clearPidFile, writePidFile, type AppLogResult } from './app-log-process.ts'; import { attachChildToStream, createLineWriter, waitForChildExit } from './app-log-stream.ts'; diff --git a/src/daemon/app-log.ts b/src/daemon/app-log.ts index 8e2ece5f9..93054ae36 100644 --- a/src/daemon/app-log.ts +++ b/src/daemon/app-log.ts @@ -3,7 +3,7 @@ import path from 'node:path'; import type { DeviceInfo } from '../kernel/device.ts'; import { AppError } from '../kernel/errors.ts'; import { runCmd } from '../utils/exec.ts'; -import { runXcrun } from '../platforms/ios/tool-provider.ts'; +import { runXcrun } from '../platforms/apple/core/tool-provider.ts'; import { runAndroidAdb } from '../platforms/android/adb.ts'; import { createScopedProvider } from '../utils/scoped-provider.ts'; import { diff --git a/src/daemon/apple-runner-options.ts b/src/daemon/apple-runner-options.ts index fa82b15c5..a73f6109c 100644 --- a/src/daemon/apple-runner-options.ts +++ b/src/daemon/apple-runner-options.ts @@ -1,7 +1,7 @@ import { isDeepLinkTarget } from '../core/open-target.ts'; import type { SessionSurface } from '../core/session-surface.ts'; -import type { AppleRunnerLifecycleOptions } from '../platforms/ios/runner-provider.ts'; -import { prewarmIosRunnerCache } from '../platforms/ios/runner-client.ts'; +import type { AppleRunnerLifecycleOptions } from '../platforms/apple/core/runner/runner-provider.ts'; +import { prewarmIosRunnerCache } from '../platforms/apple/core/runner/runner-client.ts'; import type { DeviceInfo } from '../kernel/device.ts'; import { contextFromFlags } from './context.ts'; import type { DaemonRequest } from './types.ts'; diff --git a/src/daemon/artifact-materialization.ts b/src/daemon/artifact-materialization.ts index 1de7d97f8..3518863ca 100644 --- a/src/daemon/artifact-materialization.ts +++ b/src/daemon/artifact-materialization.ts @@ -6,7 +6,7 @@ import { resolveTarArchiveRootName, } from './artifact-archive.ts'; import { createArtifactTempDir, downloadArtifactToTempDir } from './artifact-download.ts'; -import { readInfoPlistString } from '../platforms/ios/plist.ts'; +import { readInfoPlistString } from '../platforms/apple/core/plist.ts'; import { AppError } from '../kernel/errors.ts'; export type MaterializeArtifactParams = { diff --git a/src/daemon/device-ready.ts b/src/daemon/device-ready.ts index a95192205..34997d332 100644 --- a/src/daemon/device-ready.ts +++ b/src/daemon/device-ready.ts @@ -3,8 +3,11 @@ import os from 'node:os'; import path from 'node:path'; import { promises as fs } from 'node:fs'; import { AppError } from '../kernel/errors.ts'; -import { resolveIosDevicectlHint, IOS_DEVICECTL_DEFAULT_HINT } from '../platforms/ios/devicectl.ts'; -import { runXcrun } from '../platforms/ios/tool-provider.ts'; +import { + resolveIosDevicectlHint, + IOS_DEVICECTL_DEFAULT_HINT, +} from '../platforms/apple/core/devicectl.ts'; +import { runXcrun } from '../platforms/apple/core/tool-provider.ts'; import { isActiveProviderDevice } from '../provider-device-runtime.ts'; const IOS_DEVICE_READY_TIMEOUT_MS = 15_000; @@ -38,7 +41,7 @@ export async function ensureDeviceReady( if (device.platform === 'ios') { if (device.kind === 'simulator') { - const { ensureBootedSimulator } = await import('../platforms/ios/simulator.ts'); + const { ensureBootedSimulator } = await import('../platforms/apple/core/simulator.ts'); await ensureBootedSimulator(device, { deviceHub: options.deviceHub, focusExisting: options.focusExisting, diff --git a/src/daemon/handlers/__tests__/interaction.test.ts b/src/daemon/handlers/__tests__/interaction.test.ts index e004adfeb..bb2fc3d40 100644 --- a/src/daemon/handlers/__tests__/interaction.test.ts +++ b/src/daemon/handlers/__tests__/interaction.test.ts @@ -57,8 +57,9 @@ vi.mock('../interaction-snapshot.ts', async (importOriginal) => { }; }); -vi.mock('../../../platforms/ios/runner-client.ts', async (importOriginal) => { - const actual = await importOriginal(); +vi.mock('../../../platforms/apple/core/runner/runner-client.ts', async (importOriginal) => { + const actual = + await importOriginal(); return { ...actual, runIosRunnerCommand: mockRunIosRunnerCommand, diff --git a/src/daemon/handlers/__tests__/record-trace-ios.test.ts b/src/daemon/handlers/__tests__/record-trace-ios.test.ts index 971288ed3..11fcc05fc 100644 --- a/src/daemon/handlers/__tests__/record-trace-ios.test.ts +++ b/src/daemon/handlers/__tests__/record-trace-ios.test.ts @@ -2,7 +2,7 @@ import assert from 'node:assert/strict'; import { test } from 'vitest'; import { IOS_DEVICE } from '../../../__tests__/test-utils/device-fixtures.ts'; import { makeSessionStore } from '../../../__tests__/test-utils/store-factory.ts'; -import type { RunnerCommand } from '../../../platforms/ios/runner-contract.ts'; +import type { RunnerCommand } from '../../../platforms/apple/core/runner/runner-contract.ts'; import type { RecordTraceDeps } from '../record-trace-types.ts'; import { startIosDeviceRecording } from '../record-trace-ios.ts'; diff --git a/src/daemon/handlers/__tests__/record-trace.test.ts b/src/daemon/handlers/__tests__/record-trace.test.ts index b8d1308ca..02dda4dee 100644 --- a/src/daemon/handlers/__tests__/record-trace.test.ts +++ b/src/daemon/handlers/__tests__/record-trace.test.ts @@ -15,8 +15,9 @@ vi.mock('../../../utils/exec.ts', async (importOriginal) => { }; }); -vi.mock('../../../platforms/ios/runner-client.ts', async (importOriginal) => { - const actual = await importOriginal(); +vi.mock('../../../platforms/apple/core/runner/runner-client.ts', async (importOriginal) => { + const actual = + await importOriginal(); return { ...actual, runIosRunnerCommand: vi.fn(async () => ({})), @@ -56,7 +57,7 @@ import { handleRecordTraceCommands } from '../record-trace.ts'; import { deriveRecordingTelemetryPath } from '../../recording-telemetry.ts'; import { SessionStore } from '../../session-store.ts'; import type { SessionState } from '../../types.ts'; -import { runIosRunnerCommand } from '../../../platforms/ios/runner-client.ts'; +import { runIosRunnerCommand } from '../../../platforms/apple/core/runner/runner-client.ts'; import { getRecordingOverlaySupportWarning, resizeRecording, diff --git a/src/daemon/handlers/__tests__/session-close-shutdown.test.ts b/src/daemon/handlers/__tests__/session-close-shutdown.test.ts index 98c206a77..a2d86fb64 100644 --- a/src/daemon/handlers/__tests__/session-close-shutdown.test.ts +++ b/src/daemon/handlers/__tests__/session-close-shutdown.test.ts @@ -6,20 +6,23 @@ import { SessionStore } from '../../session-store.ts'; import type { DaemonRequest, DaemonResponse, SessionState } from '../../types.ts'; import { AppError } from '../../../kernel/errors.ts'; -vi.mock('../../../platforms/ios/simulator.ts', async (importOriginal) => { - const actual = await importOriginal(); +vi.mock('../../../platforms/apple/core/simulator.ts', async (importOriginal) => { + const actual = + await importOriginal(); return { ...actual, shutdownSimulator: vi.fn() }; }); vi.mock('../../../utils/exec.ts', async (importOriginal) => { const actual = await importOriginal(); return { ...actual, runCmd: vi.fn() }; }); -vi.mock('../../../platforms/ios/runner-client.ts', async (importOriginal) => { - const actual = await importOriginal(); +vi.mock('../../../platforms/apple/core/runner/runner-client.ts', async (importOriginal) => { + const actual = + await importOriginal(); return { ...actual, stopIosRunnerSession: vi.fn(async () => {}) }; }); -vi.mock('../../../platforms/ios/perf-xctrace.ts', async (importOriginal) => { - const actual = await importOriginal(); +vi.mock('../../../platforms/apple/core/perf-xctrace.ts', async (importOriginal) => { + const actual = + await importOriginal(); return { ...actual, cleanupAppleXctracePerfCapture: vi.fn(async () => ({})) }; }); vi.mock('../../../platforms/android/perf.ts', async (importOriginal) => { @@ -31,8 +34,9 @@ vi.mock('../../../platforms/android/snapshot-helper.ts', async (importOriginal) await importOriginal(); return { ...actual, stopAndroidSnapshotHelperSessionForDevice: vi.fn(async () => {}) }; }); -vi.mock('../../../platforms/ios/macos-helper.ts', async (importOriginal) => { - const actual = await importOriginal(); +vi.mock('../../../platforms/apple/os/macos/helper.ts', async (importOriginal) => { + const actual = + await importOriginal(); return { ...actual, runMacOsAlertAction: vi.fn(async () => {}) }; }); vi.mock('../../runtime-hints.ts', async (importOriginal) => { @@ -50,10 +54,10 @@ vi.mock('../session-device-utils.ts', async (importOriginal) => { import { handleSessionCommands } from '../session.ts'; import { teardownSessionResources } from '../../session-teardown.ts'; -import { shutdownSimulator } from '../../../platforms/ios/simulator.ts'; +import { shutdownSimulator } from '../../../platforms/apple/core/simulator.ts'; import { runCmd } from '../../../utils/exec.ts'; import { dispatchCommand } from '../../../core/dispatch.ts'; -import { cleanupAppleXctracePerfCapture } from '../../../platforms/ios/perf-xctrace.ts'; +import { cleanupAppleXctracePerfCapture } from '../../../platforms/apple/core/perf-xctrace.ts'; import { cleanupAndroidNativePerfSession } from '../../../platforms/android/perf.ts'; import { stopAndroidSnapshotHelperSessionForDevice } from '../../../platforms/android/snapshot-helper.ts'; import { WEB_DESKTOP_DEVICE } from '../../../__tests__/test-utils/index.ts'; diff --git a/src/daemon/handlers/__tests__/session-observability.test.ts b/src/daemon/handlers/__tests__/session-observability.test.ts index fbcb9b45e..ef44f1fe7 100644 --- a/src/daemon/handlers/__tests__/session-observability.test.ts +++ b/src/daemon/handlers/__tests__/session-observability.test.ts @@ -7,7 +7,7 @@ import type { AndroidAdbExecutor } from '../../../platforms/android/adb-executor import { makeSessionStore } from '../../../__tests__/test-utils/store-factory.ts'; import { makeAndroidSession, makeIosSession } from '../../../__tests__/test-utils/index.ts'; import { AppError } from '../../../kernel/errors.ts'; -import type { AppleXctracePerfCapture } from '../../../platforms/ios/perf-xctrace.ts'; +import type { AppleXctracePerfCapture } from '../../../platforms/apple/core/perf-xctrace.ts'; import type { DaemonResponse } from '../../types.ts'; const applePerfMocks = vi.hoisted(() => ({ @@ -16,8 +16,9 @@ const applePerfMocks = vi.hoisted(() => ({ writeAppleXctracePerfReport: vi.fn(), })); -vi.mock('../../../platforms/ios/perf-xctrace.ts', async (importOriginal) => { - const actual = await importOriginal(); +vi.mock('../../../platforms/apple/core/perf-xctrace.ts', async (importOriginal) => { + const actual = + await importOriginal(); return { ...actual, startAppleXctracePerfCapture: applePerfMocks.startAppleXctracePerfCapture, diff --git a/src/daemon/handlers/__tests__/session-open-runtime.test.ts b/src/daemon/handlers/__tests__/session-open-runtime.test.ts index 09d18fbec..4effcd598 100644 --- a/src/daemon/handlers/__tests__/session-open-runtime.test.ts +++ b/src/daemon/handlers/__tests__/session-open-runtime.test.ts @@ -19,16 +19,17 @@ vi.mock('../../runtime-hints.ts', async (importOriginal) => { clearRuntimeHintsFromApp: vi.fn(async () => {}), }; }); -vi.mock('../../../platforms/ios/runner-client.ts', async (importOriginal) => { - const actual = await importOriginal(); +vi.mock('../../../platforms/apple/core/runner/runner-client.ts', async (importOriginal) => { + const actual = + await importOriginal(); return { ...actual, prewarmIosRunnerSession: vi.fn(), stopIosRunnerSession: vi.fn(async () => {}), }; }); -vi.mock('../../../platforms/ios/apps.ts', async (importOriginal) => { - const actual = await importOriginal(); +vi.mock('../../../platforms/apple/core/apps.ts', async (importOriginal) => { + const actual = await importOriginal(); return { ...actual, resolveIosApp: vi.fn(async () => 'com.example.demo'), diff --git a/src/daemon/handlers/__tests__/session.test.ts b/src/daemon/handlers/__tests__/session.test.ts index eb98babc2..6c74a93bb 100644 --- a/src/daemon/handlers/__tests__/session.test.ts +++ b/src/daemon/handlers/__tests__/session.test.ts @@ -13,8 +13,9 @@ vi.mock('../../runtime-hints.ts', async (importOriginal) => { clearRuntimeHintsFromApp: vi.fn(async () => {}), }; }); -vi.mock('../../../platforms/ios/runner-client.ts', async (importOriginal) => { - const actual = await importOriginal(); +vi.mock('../../../platforms/apple/core/runner/runner-client.ts', async (importOriginal) => { + const actual = + await importOriginal(); return { ...actual, prepareIosRunner: vi.fn(async () => ({ @@ -27,8 +28,9 @@ vi.mock('../../../platforms/ios/runner-client.ts', async (importOriginal) => { stopIosRunnerSession: vi.fn(async () => {}), }; }); -vi.mock('../../../platforms/ios/macos-helper.ts', async (importOriginal) => { - const actual = await importOriginal(); +vi.mock('../../../platforms/apple/os/macos/helper.ts', async (importOriginal) => { + const actual = + await importOriginal(); return { ...actual, runMacOsAlertAction: vi.fn(async () => {}) }; }); vi.mock('../session-device-utils.ts', async (importOriginal) => { @@ -39,8 +41,9 @@ vi.mock('../session-open-target.ts', async (importOriginal) => { const actual = await importOriginal(); return { ...actual, resolveAndroidPackageForOpen: vi.fn(async () => undefined) }; }); -vi.mock('../../../platforms/ios/simulator.ts', async (importOriginal) => { - const actual = await importOriginal(); +vi.mock('../../../platforms/apple/core/simulator.ts', async (importOriginal) => { + const actual = + await importOriginal(); return { ...actual, getSimulatorState: vi.fn(async () => null), shutdownSimulator: vi.fn() }; }); vi.mock('../../../utils/exec.ts', async (importOriginal) => { @@ -59,12 +62,12 @@ vi.mock('../../../platforms/android/devices.ts', async (importOriginal) => { ensureAndroidEmulatorBooted: vi.fn(), }; }); -vi.mock('../../../platforms/ios/devices.ts', async (importOriginal) => { - const actual = await importOriginal(); +vi.mock('../../../platforms/apple/core/devices.ts', async (importOriginal) => { + const actual = await importOriginal(); return { ...actual, listAppleDevices: vi.fn(async () => []) }; }); -vi.mock('../../../platforms/ios/apps.ts', async (importOriginal) => { - const actual = await importOriginal(); +vi.mock('../../../platforms/apple/core/apps.ts', async (importOriginal) => { + const actual = await importOriginal(); return { ...actual, listIosApps: vi.fn(async () => []), @@ -105,18 +108,21 @@ import { prewarmIosRunnerCache, prewarmIosRunnerSession, stopIosRunnerSession, -} from '../../../platforms/ios/runner-client.ts'; -import { runMacOsAlertAction } from '../../../platforms/ios/macos-helper.ts'; +} from '../../../platforms/apple/core/runner/runner-client.ts'; +import { runMacOsAlertAction } from '../../../platforms/apple/os/macos/helper.ts'; import { settleIosSimulator } from '../session-device-utils.ts'; import { resolveAndroidPackageForOpen } from '../session-open-target.ts'; import { runCmd } from '../../../utils/exec.ts'; -import { shutdownSimulator } from '../../../platforms/ios/simulator.ts'; +import { shutdownSimulator } from '../../../platforms/apple/core/simulator.ts'; import { listAndroidDevices, ensureAndroidEmulatorBooted, } from '../../../platforms/android/devices.ts'; -import { listAppleDevices } from '../../../platforms/ios/devices.ts'; -import { resolveIosApp, resolveIosSimulatorDeepLinkBundleId } from '../../../platforms/ios/apps.ts'; +import { listAppleDevices } from '../../../platforms/apple/core/devices.ts'; +import { + resolveIosApp, + resolveIosSimulatorDeepLinkBundleId, +} from '../../../platforms/apple/core/apps.ts'; import { startAppLog, stopAppLog } from '../../app-log.ts'; import { defaultInstallOps, defaultReinstallOps } from '../session-deploy.ts'; import { clearRequestCanceled, markRequestCanceled } from '../../request-cancel.ts'; diff --git a/src/daemon/handlers/__tests__/snapshot-handler.test.ts b/src/daemon/handlers/__tests__/snapshot-handler.test.ts index 85e7a2de3..3e9913c7e 100644 --- a/src/daemon/handlers/__tests__/snapshot-handler.test.ts +++ b/src/daemon/handlers/__tests__/snapshot-handler.test.ts @@ -23,8 +23,9 @@ vi.mock('../../../core/dispatch.ts', async (importOriginal) => { }; }); -vi.mock('../../../platforms/ios/runner-client.ts', async (importOriginal) => { - const actual = await importOriginal(); +vi.mock('../../../platforms/apple/core/runner/runner-client.ts', async (importOriginal) => { + const actual = + await importOriginal(); return { ...actual, runIosRunnerCommand: vi.fn(async () => ({})), @@ -32,8 +33,8 @@ vi.mock('../../../platforms/ios/runner-client.ts', async (importOriginal) => { }; }); -vi.mock('../../../platforms/ios/apps.ts', async (importOriginal) => { - const actual = await importOriginal(); +vi.mock('../../../platforms/apple/core/apps.ts', async (importOriginal) => { + const actual = await importOriginal(); return { ...actual, closeIosApp: vi.fn(async () => {}), @@ -41,8 +42,11 @@ vi.mock('../../../platforms/ios/apps.ts', async (importOriginal) => { }); import { dispatchCommand } from '../../../core/dispatch.ts'; -import { runIosRunnerCommand, stopIosRunnerSession } from '../../../platforms/ios/runner-client.ts'; -import { closeIosApp } from '../../../platforms/ios/apps.ts'; +import { + runIosRunnerCommand, + stopIosRunnerSession, +} from '../../../platforms/apple/core/runner/runner-client.ts'; +import { closeIosApp } from '../../../platforms/apple/core/apps.ts'; const mockDispatch = vi.mocked(dispatchCommand); const mockRunnerCommand = vi.mocked(runIosRunnerCommand); diff --git a/src/daemon/handlers/install-source.ts b/src/daemon/handlers/install-source.ts index ed3a74e7c..6128a19d2 100644 --- a/src/daemon/handlers/install-source.ts +++ b/src/daemon/handlers/install-source.ts @@ -160,7 +160,8 @@ export async function handleInstallFromSourceCommand(params: { const requestSignal = getRequestSignal(req.meta?.requestId); if (device.platform === 'ios') { - const { prepareIosInstallArtifact } = await import('../../platforms/ios/install-artifact.ts'); + const { prepareIosInstallArtifact } = + await import('../../platforms/apple/core/install-artifact.ts'); const prepared = await prepareIosInstallArtifact(resolvedSource.source, { signal: requestSignal, }); @@ -239,7 +240,7 @@ async function installPreparedIosArtifact( { appIdentifierHint: prepared.bundleId }, ); if (!providerResult) { - const { installIosInstallablePath } = await import('../../platforms/ios/apps.ts'); + const { installIosInstallablePath } = await import('../../platforms/apple/core/apps.ts'); await installIosInstallablePath(device, prepared.installablePath); } return buildIosInstallFromSourceResult(prepared, providerResult, retained); diff --git a/src/daemon/handlers/record-trace-ios.ts b/src/daemon/handlers/record-trace-ios.ts index af1e3811a..e959c1adf 100644 --- a/src/daemon/handlers/record-trace-ios.ts +++ b/src/daemon/handlers/record-trace-ios.ts @@ -1,7 +1,7 @@ import { SessionStore } from '../session-store.ts'; import type { DaemonRequest, DaemonResponse, SessionState } from '../types.ts'; import { emitDiagnostic } from '../../utils/diagnostics.ts'; -import { IOS_RUNNER_CONTAINER_BUNDLE_IDS } from '../../platforms/ios/runner-client.ts'; +import { IOS_RUNNER_CONTAINER_BUNDLE_IDS } from '../../platforms/apple/core/runner/runner-client.ts'; import { formatRecordTraceError } from '../record-trace-errors.ts'; import { buildAppleRunnerRequestOptions } from '../apple-runner-options.ts'; import type { RecordTraceDeps, RecordingBase } from './record-trace-types.ts'; diff --git a/src/daemon/handlers/record-trace-recording.ts b/src/daemon/handlers/record-trace-recording.ts index 1a344b09f..63b25a44f 100644 --- a/src/daemon/handlers/record-trace-recording.ts +++ b/src/daemon/handlers/record-trace-recording.ts @@ -8,8 +8,8 @@ import type { DaemonArtifact, DaemonRequest, DaemonResponse, SessionState } from import { runCmd } from '../../utils/exec.ts'; import { isPlayableVideo, waitForStableFile } from '../../utils/video.ts'; import { deriveRecordingTelemetryPath } from '../recording-telemetry.ts'; -import { runIosRunnerCommand } from '../../platforms/ios/runner-client.ts'; -import { runXcrun } from '../../platforms/ios/tool-provider.ts'; +import { runIosRunnerCommand } from '../../platforms/apple/core/runner/runner-client.ts'; +import { runXcrun } from '../../platforms/apple/core/tool-provider.ts'; import { overlayRecordingTouches, resizeRecording, diff --git a/src/daemon/handlers/record-trace-types.ts b/src/daemon/handlers/record-trace-types.ts index d710ee731..4b82a0380 100644 --- a/src/daemon/handlers/record-trace-types.ts +++ b/src/daemon/handlers/record-trace-types.ts @@ -1,7 +1,7 @@ import type { RecordingProvider } from '../recording-provider.ts'; import type { runCmd } from '../../utils/exec.ts'; import type { isPlayableVideo, waitForStableFile } from '../../utils/video.ts'; -import type { runIosRunnerCommand } from '../../platforms/ios/runner-client.ts'; +import type { runIosRunnerCommand } from '../../platforms/apple/core/runner/runner-client.ts'; import type { overlayRecordingTouches, resizeRecording, diff --git a/src/daemon/handlers/session-deploy.ts b/src/daemon/handlers/session-deploy.ts index 3bf1d7201..ec315d0d1 100644 --- a/src/daemon/handlers/session-deploy.ts +++ b/src/daemon/handlers/session-deploy.ts @@ -62,7 +62,7 @@ export const defaultReinstallOps: ReinstallOps = { return { bundleId: providerResult.bundleId ?? providerResult.launchTarget ?? app }; } - const { reinstallIosApp } = await import('../../platforms/ios/apps.ts'); + const { reinstallIosApp } = await import('../../platforms/apple/core/apps.ts'); return await reinstallIosApp(device, app, appPath); }, android: async (device, app, appPath) => { @@ -92,7 +92,7 @@ export const defaultInstallOps: InstallOps = { }; } - const { installIosApp } = await import('../../platforms/ios/apps.ts'); + const { installIosApp } = await import('../../platforms/apple/core/apps.ts'); const result = await installIosApp(device, appPath, { appIdentifierHint: app }); return { bundleId: result.bundleId, diff --git a/src/daemon/handlers/session-inventory.ts b/src/daemon/handlers/session-inventory.ts index ba0a3e5f6..f75864d29 100644 --- a/src/daemon/handlers/session-inventory.ts +++ b/src/daemon/handlers/session-inventory.ts @@ -14,7 +14,7 @@ import { import type { DaemonRequest, DaemonResponse } from '../types.ts'; import { resolveSessionRunnerLogPath, SessionStore } from '../session-store.ts'; import { listAndroidApps } from '../../platforms/android/app-lifecycle.ts'; -import { listIosApps } from '../../platforms/ios/apps.ts'; +import { listIosApps } from '../../platforms/apple/core/apps.ts'; import { requireSessionOrExplicitSelector, resolveCommandDevice } from './session-device-utils.ts'; import { errorResponse, requireCommandSupported } from './response.ts'; import { resolveImplicitSessionScope, sessionMatchesScope } from '../session-routing.ts'; diff --git a/src/daemon/handlers/session-open-surface.ts b/src/daemon/handlers/session-open-surface.ts index b17aa4cff..2fa7c4765 100644 --- a/src/daemon/handlers/session-open-surface.ts +++ b/src/daemon/handlers/session-open-surface.ts @@ -1,5 +1,5 @@ import { parseSessionSurface, type SessionSurface } from '../../core/session-surface.ts'; -import { resolveFrontmostMacOsApp } from '../../platforms/ios/macos-helper.ts'; +import { resolveFrontmostMacOsApp } from '../../platforms/apple/os/macos/helper.ts'; import type { DeviceInfo } from '../../kernel/device.ts'; import type { SessionRuntimeHints, SessionState } from '../types.ts'; import { AppError } from '../../kernel/errors.ts'; diff --git a/src/daemon/handlers/session-open-target.ts b/src/daemon/handlers/session-open-target.ts index 6973492e2..b8a95b7e6 100644 --- a/src/daemon/handlers/session-open-target.ts +++ b/src/daemon/handlers/session-open-target.ts @@ -31,7 +31,8 @@ async function tryResolveIosSimulatorDeepLinkBundleId( openTarget: string, ): Promise { try { - const { resolveIosSimulatorDeepLinkBundleId } = await import('../../platforms/ios/apps.ts'); + const { resolveIosSimulatorDeepLinkBundleId } = + await import('../../platforms/apple/core/apps.ts'); return await resolveIosSimulatorDeepLinkBundleId(device, openTarget); } catch { return undefined; @@ -43,7 +44,7 @@ async function tryResolveIosAppBundleId( openTarget: string, ): Promise { try { - const { resolveIosApp } = await import('../../platforms/ios/apps.ts'); + const { resolveIosApp } = await import('../../platforms/apple/core/apps.ts'); return await resolveIosApp(device, openTarget); } catch { return undefined; diff --git a/src/daemon/handlers/session-open.ts b/src/daemon/handlers/session-open.ts index 0474fe40e..a3c7fde99 100644 --- a/src/daemon/handlers/session-open.ts +++ b/src/daemon/handlers/session-open.ts @@ -6,7 +6,7 @@ import { createRequestCanceledError, isRequestCanceled } from '../request-cancel import { prewarmIosRunnerSession, stopIosRunnerSession, -} from '../../platforms/ios/runner-client.ts'; +} from '../../platforms/apple/core/runner/runner-client.ts'; import { buildAppleRunnerSessionOptions, createIosRunnerCacheColdBootPrewarmForOpen, diff --git a/src/daemon/handlers/session-perf-xctrace.ts b/src/daemon/handlers/session-perf-xctrace.ts index 478f24e75..36fce743a 100644 --- a/src/daemon/handlers/session-perf-xctrace.ts +++ b/src/daemon/handlers/session-perf-xctrace.ts @@ -8,7 +8,7 @@ import { writeAppleXctracePerfReport, type AppleXctracePerfMode, type AppleXctracePerfResult, -} from '../../platforms/ios/perf-xctrace.ts'; +} from '../../platforms/apple/core/perf-xctrace.ts'; import { PERF_AREA_ERROR_MESSAGE } from '../../contracts/perf.ts'; import { errorResponse, type DaemonFailureResponse } from './response.ts'; import { recordSessionAction } from './handler-utils.ts'; diff --git a/src/daemon/handlers/session-perf.ts b/src/daemon/handlers/session-perf.ts index a219484e5..eac208bfa 100644 --- a/src/daemon/handlers/session-perf.ts +++ b/src/daemon/handlers/session-perf.ts @@ -26,7 +26,7 @@ import { captureAppleMemorySnapshot, sampleAppleFramePerf, sampleApplePerfMetrics, -} from '../../platforms/ios/perf.ts'; +} from '../../platforms/apple/core/perf.ts'; import type { PerfKind } from '../../contracts/perf.ts'; import { SessionStore } from '../session-store.ts'; import { diff --git a/src/daemon/handlers/session.ts b/src/daemon/handlers/session.ts index 1bbcb15c5..714856670 100644 --- a/src/daemon/handlers/session.ts +++ b/src/daemon/handlers/session.ts @@ -5,7 +5,7 @@ import type { AndroidAdbExecutor } from '../../platforms/android/adb-executor.ts import { prepareIosRunner, type PrepareIosRunnerResult, -} from '../../platforms/ios/runner-client.ts'; +} from '../../platforms/apple/core/runner/runner-client.ts'; import type { DeviceInfo } from '../../kernel/device.ts'; import { isApplePlatform } from '../../kernel/device.ts'; import type { DaemonInvokeFn, DaemonRequest, DaemonResponse, SessionState } from '../types.ts'; diff --git a/src/daemon/handlers/snapshot-alert.ts b/src/daemon/handlers/snapshot-alert.ts index a2e5293e1..21df842ed 100644 --- a/src/daemon/handlers/snapshot-alert.ts +++ b/src/daemon/handlers/snapshot-alert.ts @@ -5,8 +5,8 @@ import { type AlertAction, } from '../../alert-contract.ts'; import { sleep } from '../../utils/timeouts.ts'; -import { runIosRunnerCommand } from '../../platforms/ios/runner-client.ts'; -import { runMacOsAlertAction } from '../../platforms/ios/macos-helper.ts'; +import { runIosRunnerCommand } from '../../platforms/apple/core/runner/runner-client.ts'; +import { runMacOsAlertAction } from '../../platforms/apple/os/macos/helper.ts'; import { handleAndroidAlert } from '../../platforms/android/alert.ts'; import { AppError } from '../../kernel/errors.ts'; import type { DaemonRequest, DaemonResponse, SessionState } from '../types.ts'; diff --git a/src/daemon/handlers/snapshot-capture.ts b/src/daemon/handlers/snapshot-capture.ts index b2e7f31a8..4595ee4be 100644 --- a/src/daemon/handlers/snapshot-capture.ts +++ b/src/daemon/handlers/snapshot-capture.ts @@ -1,7 +1,7 @@ import { dispatchCommand, type CommandFlags } from '../../core/dispatch.ts'; import { isMobilePlatform } from '../../kernel/device.ts'; import { sleep } from '../../utils/timeouts.ts'; -import { runMacOsSnapshotAction } from '../../platforms/ios/macos-helper.ts'; +import { runMacOsSnapshotAction } from '../../platforms/apple/os/macos/helper.ts'; import { snapshotLinux } from '../../platforms/linux/snapshot.ts'; import { attachRefs, diff --git a/src/daemon/handlers/snapshot-session.ts b/src/daemon/handlers/snapshot-session.ts index 3d199f468..6c486f94a 100644 --- a/src/daemon/handlers/snapshot-session.ts +++ b/src/daemon/handlers/snapshot-session.ts @@ -2,8 +2,8 @@ import { resolveTargetDevice } from '../../core/dispatch.ts'; import { resolveRunnerAppBundleId, stopIosRunnerSession, -} from '../../platforms/ios/runner-client.ts'; -import { closeIosApp } from '../../platforms/ios/apps.ts'; +} from '../../platforms/apple/core/runner/runner-client.ts'; +import { closeIosApp } from '../../platforms/apple/core/apps.ts'; import { emitDiagnostic } from '../../utils/diagnostics.ts'; import type { DaemonRequest, SessionState } from '../types.ts'; import { ensureDeviceReady } from '../device-ready.ts'; diff --git a/src/daemon/http-server.ts b/src/daemon/http-server.ts index bb91bb32b..f4c8184b3 100644 --- a/src/daemon/http-server.ts +++ b/src/daemon/http-server.ts @@ -794,7 +794,8 @@ async function abortInFlightIosRunnerSessionsWhileDisconnected( try { const deadline = Date.now() + CLIENT_DISCONNECT_ABORT_MAX_WINDOW_MS; while (isRequestCanceled(requestId) && Date.now() < deadline) { - const { abortAllIosRunnerSessions } = await import('../platforms/ios/runner-client.ts'); + const { abortAllIosRunnerSessions } = + await import('../platforms/apple/core/runner/runner-client.ts'); await abortAllIosRunnerSessions(); if (!isRequestCanceled(requestId)) break; await sleep(CLIENT_DISCONNECT_ABORT_POLL_INTERVAL_MS); diff --git a/src/daemon/recording-provider.ts b/src/daemon/recording-provider.ts index 56cad98a4..ec0fe2f62 100644 --- a/src/daemon/recording-provider.ts +++ b/src/daemon/recording-provider.ts @@ -1,4 +1,4 @@ -import { buildSimctlArgsForDevice } from '../platforms/ios/simctl.ts'; +import { buildSimctlArgsForDevice } from '../platforms/apple/core/simctl.ts'; import type { DeviceInfo } from '../kernel/device.ts'; import { runCmdBackground, type ExecBackgroundResult, type ExecResult } from '../utils/exec.ts'; import { createScopedProvider } from '../utils/scoped-provider.ts'; diff --git a/src/daemon/request-platform-providers.ts b/src/daemon/request-platform-providers.ts index 9eff68362..86f193fc2 100644 --- a/src/daemon/request-platform-providers.ts +++ b/src/daemon/request-platform-providers.ts @@ -3,11 +3,11 @@ import type { AndroidAdbExecutor, AndroidAdbProvider } from '../platforms/androi import type { AppleRunnerCommandExecutor, AppleRunnerProvider, -} from '../platforms/ios/runner-provider.ts'; +} from '../platforms/apple/core/runner/runner-provider.ts'; import type { AppleToolCommandExecutor, AppleToolProvider, -} from '../platforms/ios/tool-provider.ts'; +} from '../platforms/apple/core/tool-provider.ts'; import type { LinuxToolProvider } from '../platforms/linux/tool-provider.ts'; import type { WebProvider } from '../platforms/web/provider.ts'; import { isApplePlatform, type DeviceInfo } from '../kernel/device.ts'; @@ -152,7 +152,8 @@ const REQUEST_PLATFORM_PROVIDER_DESCRIPTORS = [ }, async appendWrapper(scopedProviders, wrappers) { if (!scopedProviders.appleRunner?.provider) return; - const { withAppleRunnerProvider } = await import('../platforms/ios/runner-provider.ts'); + const { withAppleRunnerProvider } = + await import('../platforms/apple/core/runner/runner-provider.ts'); appendRequestProviderWrapper(wrappers, scopedProviders.appleRunner, (provider, task) => withAppleRunnerProvider( provider, @@ -174,7 +175,7 @@ const REQUEST_PLATFORM_PROVIDER_DESCRIPTORS = [ }, async appendWrapper(scopedProviders, wrappers) { if (!scopedProviders.appleTool?.provider) return; - const { withAppleToolProvider } = await import('../platforms/ios/tool-provider.ts'); + const { withAppleToolProvider } = await import('../platforms/apple/core/tool-provider.ts'); appendRequestProviderWrapper(wrappers, scopedProviders.appleTool, withAppleToolProvider); }, }, diff --git a/src/daemon/request-recording-health.ts b/src/daemon/request-recording-health.ts index 2f1f4aee2..4bc1e0b18 100644 --- a/src/daemon/request-recording-health.ts +++ b/src/daemon/request-recording-health.ts @@ -1,4 +1,4 @@ -import { getRunnerSessionSnapshot } from '../platforms/ios/runner-client.ts'; +import { getRunnerSessionSnapshot } from '../platforms/apple/core/runner/runner-client.ts'; import type { SessionState } from './types.ts'; export function refreshRecordingHealth(session: SessionState): void { diff --git a/src/daemon/request-router.ts b/src/daemon/request-router.ts index 7fa16bec0..53e5210d4 100644 --- a/src/daemon/request-router.ts +++ b/src/daemon/request-router.ts @@ -22,7 +22,6 @@ import { withRequestPlatformProviderScope, } from './request-platform-providers.ts'; import { - countDiagnosticEventsByPhase, emitDiagnostic, flushDiagnosticsToSessionFile, getDiagnosticsMeta, @@ -40,10 +39,6 @@ import { import { canRunReplayScopedAction } from './daemon-command-registry.ts'; import { createAgentBrowserWebProvider } from '../platforms/web/agent-browser-provider.ts'; import type { LeaseLifecycleProvider } from './handlers/lease.ts'; -// Single source of truth for which diagnostic phases count as a real iOS-runner -// round-trip — shared with the external ndjson counter used by the -// runner-request-count CI gate (`scripts/runner-request-count/`). -import { RUNNER_ROUND_TRIP_PHASES } from './runner-request-count.ts'; // --------------------------------------------------------------------------- // Request handler API @@ -355,7 +350,6 @@ function buildResponseCost( ): ResponseCost { const cost: ResponseCost = { wallClockMs: Date.now() - startedAt, - runnerRoundTrips: countDiagnosticEventsByPhase(RUNNER_ROUND_TRIP_PHASES), }; // nodeCount reads the ORIGINAL node tree (the digest view may have already // collapsed `data.nodes`), so the count stays accurate. diff --git a/src/daemon/runner-request-count.ts b/src/daemon/runner-request-count.ts deleted file mode 100644 index c221d21f9..000000000 --- a/src/daemon/runner-request-count.ts +++ /dev/null @@ -1,220 +0,0 @@ -/** - * Hardware-free counter for "iOS runner requests" in the daemon `--debug` - * diagnostics ndjson stream. - * - * The daemon emits one diagnostic event per real iOS-runner round-trip - * (`emitDiagnostic` in `../utils/diagnostics.ts`). When a request is in debug - * mode those events are streamed as one JSON object per line into the daemon - * log (`/daemon.log`). This module parses that stream and counts the - * round-trip phases, so the runner request count can be asserted in CI without - * re-implementing the hand-counting an operator used to do by reading the - * ndjson by eye. - * - * `RUNNER_ROUND_TRIP_PHASES` is the single source of truth shared by the - * in-process cost graft (`request-router.ts` `buildResponseCost`) and this - * external ndjson counter, so the two can never drift on which phases count. - */ - -// Diagnostic phases emitted once per real iOS-runner round-trip. `..._command_send` -// is the command itself; `..._readiness_preflight` is the pre-command uptime probe -// (a real network round-trip). The `..._skipped` / `..._recovered` markers do NOT -// hit the runner and are intentionally excluded. -export const RUNNER_ROUND_TRIP_PHASES = [ - 'ios_runner_command_send', - 'ios_runner_readiness_preflight', -] as const; - -export type RunnerRoundTripPhase = (typeof RUNNER_ROUND_TRIP_PHASES)[number]; - -/** - * A single parsed line of the daemon `--debug` diagnostics ndjson stream. Only - * the fields the counter and its drift reporting need are retained; the full - * record carries more (ts/level/requestId/session/durationMs). - */ -export type ParsedDiagnosticEvent = { - phase: string; - command?: string; -}; - -export type RunnerRequestCounts = { - runnerRoundTrips: number; - byPhase: Record; -}; - -// The stderr fallback path in `emitDiagnostic` prefixes each ndjson line with -// this tag. The daemon-log path does not, but we tolerate both so the counter -// works against captured stderr too. -const STDERR_DIAGNOSTIC_PREFIX = '[agent-device][diag] '; - -/** - * Tolerant ndjson parser: the daemon log interleaves plain log text with the - * diagnostic ndjson lines, so non-JSON lines, blank lines, malformed JSON, and - * objects without a `phase` are skipped rather than throwing. - */ -export function parseDiagnosticNdjson(text: string): ParsedDiagnosticEvent[] { - const events: ParsedDiagnosticEvent[] = []; - for (const rawLine of text.split(/\r?\n/)) { - let line = rawLine.trim(); - if (line.length === 0) continue; - if (line.startsWith(STDERR_DIAGNOSTIC_PREFIX)) { - line = line.slice(STDERR_DIAGNOSTIC_PREFIX.length).trim(); - } - // Fast-skip plain daemon log lines that are not JSON objects. - if (!line.startsWith('{')) continue; - let parsed: unknown; - try { - parsed = JSON.parse(line); - } catch { - continue; - } - const event = toDiagnosticEvent(parsed); - if (event) events.push(event); - } - return events; -} - -function toDiagnosticEvent(value: unknown): ParsedDiagnosticEvent | null { - if (!value || typeof value !== 'object' || Array.isArray(value)) return null; - const record = value as Record; - const phase = record.phase; - if (typeof phase !== 'string') return null; - const command = record.command; - return typeof command === 'string' ? { phase, command } : { phase }; -} - -export function emptyRunnerRequestCounts(): RunnerRequestCounts { - return { - runnerRoundTrips: 0, - byPhase: { ios_runner_command_send: 0, ios_runner_readiness_preflight: 0 }, - }; -} - -/** - * Count iOS-runner round-trips the way the daemon itself does: tally events - * whose phase is one of `RUNNER_ROUND_TRIP_PHASES`. Accepts raw ndjson text or - * already-parsed events. - */ -export function countRunnerRequests( - input: string | readonly ParsedDiagnosticEvent[], -): RunnerRequestCounts { - const events = typeof input === 'string' ? parseDiagnosticNdjson(input) : input; - const counts = emptyRunnerRequestCounts(); - for (const event of events) { - if (isRunnerRoundTripPhase(event.phase)) { - counts.byPhase[event.phase] += 1; - counts.runnerRoundTrips += 1; - } - } - return counts; -} - -function isRunnerRoundTripPhase(phase: string): phase is RunnerRoundTripPhase { - return (RUNNER_ROUND_TRIP_PHASES as readonly string[]).includes(phase); -} - -// --------------------------------------------------------------------------- -// Committed baseline + assertion logic (pure, so the CI harness only does I/O) -// --------------------------------------------------------------------------- - -/** - * The committed expected-count baseline. `established: false` means the gate has - * not been armed yet (no real simulator run has recorded the counts), so the - * harness records the observed counts instead of asserting. - */ -export type RunnerRequestCountBaseline = RunnerRequestCounts & { - scenario: string; - established: boolean; -}; - -export type RunnerCountDifference = { - key: 'runnerRoundTrips' | RunnerRoundTripPhase; - expected: number; - actual: number; -}; - -export type RunnerCountComparison = - | { status: 'unarmed' } - | { status: 'match' } - | { status: 'mismatch'; differences: RunnerCountDifference[] }; - -export function buildRunnerRequestCountBaseline( - scenario: string, - counts: RunnerRequestCounts, -): RunnerRequestCountBaseline { - return { - scenario, - established: true, - runnerRoundTrips: counts.runnerRoundTrips, - byPhase: { ...counts.byPhase }, - }; -} - -/** - * Validate an untrusted baseline payload (read from disk) into a typed baseline. - * Unknown keys (e.g. a documentation `$comment`) are ignored. - */ -export function parseRunnerRequestCountBaseline(value: unknown): RunnerRequestCountBaseline { - if (!value || typeof value !== 'object' || Array.isArray(value)) { - throw new Error('Runner request-count baseline must be a JSON object.'); - } - const record = value as Record; - const scenario = record.scenario; - if (typeof scenario !== 'string' || scenario.length === 0) { - throw new Error('Runner request-count baseline is missing a "scenario" string.'); - } - const byPhaseRaw = record.byPhase; - if (!byPhaseRaw || typeof byPhaseRaw !== 'object' || Array.isArray(byPhaseRaw)) { - throw new Error('Runner request-count baseline is missing a "byPhase" object.'); - } - const byPhaseRecord = byPhaseRaw as Record; - const byPhase = emptyRunnerRequestCounts().byPhase; - for (const phase of RUNNER_ROUND_TRIP_PHASES) { - byPhase[phase] = asCount(byPhaseRecord[phase], `byPhase.${phase}`); - } - return { - scenario, - established: record.established === true, - runnerRoundTrips: asCount(record.runnerRoundTrips, 'runnerRoundTrips'), - byPhase, - }; -} - -function asCount(value: unknown, field: string): number { - if (typeof value !== 'number' || !Number.isInteger(value) || value < 0) { - throw new Error( - `Runner request-count baseline field "${field}" must be a non-negative integer.`, - ); - } - return value; -} - -/** - * Compare observed counts against the committed baseline. Returns `unarmed` - * when the baseline has not been established yet (the caller should record, not - * fail), `match` when every count is identical, or `mismatch` with the exact - * per-key differences a runner refactor introduced. - */ -export function compareRunnerCounts( - baseline: RunnerRequestCountBaseline, - observed: RunnerRequestCounts, -): RunnerCountComparison { - if (!baseline.established) return { status: 'unarmed' }; - const differences: RunnerCountDifference[] = []; - if (baseline.runnerRoundTrips !== observed.runnerRoundTrips) { - differences.push({ - key: 'runnerRoundTrips', - expected: baseline.runnerRoundTrips, - actual: observed.runnerRoundTrips, - }); - } - for (const phase of RUNNER_ROUND_TRIP_PHASES) { - if (baseline.byPhase[phase] !== observed.byPhase[phase]) { - differences.push({ - key: phase, - expected: baseline.byPhase[phase], - actual: observed.byPhase[phase], - }); - } - } - return differences.length === 0 ? { status: 'match' } : { status: 'mismatch', differences }; -} diff --git a/src/daemon/runtime-hints.ts b/src/daemon/runtime-hints.ts index c0ec9b737..d860a8931 100644 --- a/src/daemon/runtime-hints.ts +++ b/src/daemon/runtime-hints.ts @@ -10,8 +10,8 @@ import { classifyAndroidAppTarget, formatAndroidInstalledPackageRequiredMessage, } from '../platforms/android/open-target.ts'; -import { buildSimctlArgsForDevice } from '../platforms/ios/simctl.ts'; -import { runXcrun } from '../platforms/ios/tool-provider.ts'; +import { buildSimctlArgsForDevice } from '../platforms/apple/core/simctl.ts'; +import { runXcrun } from '../platforms/apple/core/tool-provider.ts'; import { isActiveProviderDevice } from '../provider-device-runtime.ts'; const ANDROID_DEV_PREFS_PATH = 'shared_prefs/ReactNativeDevPrefs.xml'; diff --git a/src/daemon/selector-runtime-backend.ts b/src/daemon/selector-runtime-backend.ts index c52f25fd1..357554ea6 100644 --- a/src/daemon/selector-runtime-backend.ts +++ b/src/daemon/selector-runtime-backend.ts @@ -9,7 +9,7 @@ import { isApplePlatform } 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'; -import { runIosRunnerCommand } from '../platforms/ios/runner-client.ts'; +import { runIosRunnerCommand } from '../platforms/apple/core/runner/runner-client.ts'; import { buildAppleRunnerRequestOptions } from './apple-runner-options.ts'; import { createDaemonRuntimePolicy } from './runtime-policy.ts'; import { createDaemonRuntimeSessionStore } from './runtime-session.ts'; diff --git a/src/daemon/selector-runtime.ts b/src/daemon/selector-runtime.ts index 60d6e018e..855aaccc4 100644 --- a/src/daemon/selector-runtime.ts +++ b/src/daemon/selector-runtime.ts @@ -2,7 +2,7 @@ import { parseWaitPositionals } from '../core/wait-positionals.ts'; import type { WaitParsed } from '../core/wait-positionals.ts'; import { AppError, asAppError, normalizeError } from '../kernel/errors.ts'; import type { SnapshotNode } from '../kernel/snapshot.ts'; -import { runIosRunnerCommand } from '../platforms/ios/runner-client.ts'; +import { runIosRunnerCommand } from '../platforms/apple/core/runner/runner-client.ts'; import { buildAppleRunnerRequestOptions } from './apple-runner-options.ts'; import type { DaemonRequest, DaemonResponse, SessionState } from './types.ts'; import { errorResponse, requireCommandSupported } from './handlers/response.ts'; diff --git a/src/daemon/session-teardown.ts b/src/daemon/session-teardown.ts index b1d405af2..76e3e1b6c 100644 --- a/src/daemon/session-teardown.ts +++ b/src/daemon/session-teardown.ts @@ -1,9 +1,9 @@ import { emitDiagnostic } from '../utils/diagnostics.ts'; import { isApplePlatform } from '../kernel/device.ts'; -import { runMacOsAlertAction } from '../platforms/ios/macos-helper.ts'; +import { runMacOsAlertAction } from '../platforms/apple/os/macos/helper.ts'; import { stopAppLog } from './app-log.ts'; -import { stopIosRunnerSession } from '../platforms/ios/runner-client.ts'; -import { cleanupAppleXctracePerfCapture } from '../platforms/ios/perf-xctrace.ts'; +import { stopIosRunnerSession } from '../platforms/apple/core/runner/runner-client.ts'; +import { cleanupAppleXctracePerfCapture } from '../platforms/apple/core/perf-xctrace.ts'; import { cleanupAndroidNativePerfSession } from '../platforms/android/perf.ts'; import { stopAndroidSnapshotHelperSessionForDevice } from '../platforms/android/snapshot-helper.ts'; import { cleanupRetainedMaterializedPathsForSession } from './materialized-path-registry.ts'; diff --git a/src/daemon/target-shutdown.ts b/src/daemon/target-shutdown.ts index 3ea21ca52..aa95c4316 100644 --- a/src/daemon/target-shutdown.ts +++ b/src/daemon/target-shutdown.ts @@ -1,5 +1,5 @@ import { runAndroidAdb } from '../platforms/android/adb.ts'; -import { getSimulatorState, shutdownSimulator } from '../platforms/ios/simulator.ts'; +import { getSimulatorState, shutdownSimulator } from '../platforms/apple/core/simulator.ts'; import type { TargetShutdownResult } from '../target-shutdown-contract.ts'; import type { DeviceInfo } from '../kernel/device.ts'; import { normalizeError } from '../kernel/errors.ts'; diff --git a/src/daemon/transport.ts b/src/daemon/transport.ts index 78bec820c..be62491bb 100644 --- a/src/daemon/transport.ts +++ b/src/daemon/transport.ts @@ -53,7 +53,8 @@ export function createSocketServer(handleRequest: DaemonInvokeFn): DaemonServer try { const deadline = Date.now() + disconnectAbortMaxWindowMs; while (inFlightRequests > 0 && Date.now() < deadline) { - const { abortAllIosRunnerSessions } = await import('../platforms/ios/runner-client.ts'); + const { abortAllIosRunnerSessions } = + await import('../platforms/apple/core/runner/runner-client.ts'); await abortAllIosRunnerSessions(); if (inFlightRequests <= 0) break; await sleep(disconnectAbortPollIntervalMs); diff --git a/src/daemon/types.ts b/src/daemon/types.ts index 19ac6e73d..87a7eb989 100644 --- a/src/daemon/types.ts +++ b/src/daemon/types.ts @@ -24,7 +24,7 @@ import type { AndroidNativePerfSession } from '../platforms/android/perf.ts'; import type { AppleXctracePerfCapture, AppleXctracePerfMode, -} from '../platforms/ios/perf-xctrace.ts'; +} from '../platforms/apple/core/perf-xctrace.ts'; import type { SnapshotDiagnosticsState, SnapshotDiagnosticsSummary, diff --git a/src/kernel/contracts.ts b/src/kernel/contracts.ts index bb9d2e03d..56a19a75d 100644 --- a/src/kernel/contracts.ts +++ b/src/kernel/contracts.ts @@ -120,7 +120,6 @@ export type DaemonArtifact = { export type ResponseCost = { wallClockMs: number; - runnerRoundTrips: number; // Number of UI/accessibility nodes in the response, when the command returns a // node tree (e.g. snapshot). Absent for commands that produce no nodes, so an // agent can size a snapshot before re-fetching at a different depth/scope. diff --git a/src/kernel/device.ts b/src/kernel/device.ts index df0eefec1..202f8bf9e 100644 --- a/src/kernel/device.ts +++ b/src/kernel/device.ts @@ -68,7 +68,7 @@ export function matchesPlatformSelector( export function resolveApplePlatformName( platformOrTarget: ApplePlatform | DeviceTarget | undefined, appleOs?: AppleOS, -): 'iOS' | 'tvOS' | 'macOS' { +): 'iOS' | 'tvOS' | 'macOS' | 'visionOS' { // Prefer the explicit, stored Apple OS when present; legacy records without // it keep resolving through the existing target-based inference below. if (appleOs) return resolveRunnerPlatformNameForAppleOs(appleOs); @@ -77,16 +77,20 @@ export function resolveApplePlatformName( return 'iOS'; } -function resolveRunnerPlatformNameForAppleOs(appleOs: AppleOS): 'iOS' | 'tvOS' | 'macOS' { +function resolveRunnerPlatformNameForAppleOs( + appleOs: AppleOS, +): 'iOS' | 'tvOS' | 'macOS' | 'visionOS' { switch (appleOs) { case 'tvos': return 'tvOS'; case 'macos': return 'macOS'; - // iOS and iPadOS share the single iOS runner profile/SDK. watchOS/visionOS - // are reserved in the type but never produced by discovery; defaulting them - // to the iOS profile keeps any future record on a valid runner profile - // without introducing a new one. + case 'visionos': + return 'visionOS'; + // iOS and iPadOS share the single iOS runner profile/SDK. watchOS remains + // reserved in the type but is never produced by discovery; defaulting it to + // iOS keeps any future record on a valid runner profile without introducing + // watchOS support. default: return 'iOS'; } diff --git a/src/platforms/__tests__/install-source.test.ts b/src/platforms/__tests__/install-source.test.ts index 545e7276e..67ae97508 100644 --- a/src/platforms/__tests__/install-source.test.ts +++ b/src/platforms/__tests__/install-source.test.ts @@ -16,7 +16,7 @@ import { } from '../install-source.ts'; import * as androidManifest from '../android/manifest.ts'; import { prepareAndroidInstallArtifact } from '../android/install-artifact.ts'; -import { prepareIosInstallArtifact } from '../ios/install-artifact.ts'; +import { prepareIosInstallArtifact } from '../apple/core/install-artifact.ts'; test('validateDownloadSourceUrl rejects localhost and private literal addresses by default', async () => { await assert.rejects( diff --git a/src/platforms/ios/app-filter.ts b/src/platforms/apple/core/app-filter.ts similarity index 100% rename from src/platforms/ios/app-filter.ts rename to src/platforms/apple/core/app-filter.ts diff --git a/src/platforms/ios/app-info.ts b/src/platforms/apple/core/app-info.ts similarity index 100% rename from src/platforms/ios/app-info.ts rename to src/platforms/apple/core/app-info.ts diff --git a/src/platforms/ios/apple-runner-platform.ts b/src/platforms/apple/core/apple-runner-platform.ts similarity index 83% rename from src/platforms/ios/apple-runner-platform.ts rename to src/platforms/apple/core/apple-runner-platform.ts index 36c67e462..8db9aeee9 100644 --- a/src/platforms/ios/apple-runner-platform.ts +++ b/src/platforms/apple/core/apple-runner-platform.ts @@ -1,7 +1,11 @@ -import { AppError } from '../../kernel/errors.ts'; -import { isApplePlatform, resolveApplePlatformName, type DeviceInfo } from '../../kernel/device.ts'; +import { AppError } from '../../../kernel/errors.ts'; +import { + isApplePlatform, + resolveApplePlatformName, + type DeviceInfo, +} from '../../../kernel/device.ts'; -export type RunnerApplePlatformName = 'iOS' | 'tvOS' | 'macOS'; +export type RunnerApplePlatformName = 'iOS' | 'tvOS' | 'macOS' | 'visionOS'; type RunnerPlatformDeviceKind = 'simulator' | 'device'; @@ -72,6 +76,40 @@ const RUNNER_PLATFORM_PROFILES: Record APPLE_VISION_PATTERN.test(descriptor))) return 'visionos'; if (descriptors.some((descriptor) => APPLE_IPAD_PATTERN.test(descriptor))) return 'ipados'; return 'ios'; } @@ -139,7 +150,9 @@ export function resolveAppleTargetFromDevicectlDevice(device: DevicectlAppleDevi export function isSupportedAppleDevicectlDevice(device: DevicectlAppleDevice): boolean { const platform = resolveDevicectlApplePlatform(device); - if (platform.includes('ios') || platform.includes('tvos')) return true; + if (platform.includes('ios') || platform.includes('tvos') || isAppleVisionPlatform(platform)) { + return true; + } const productType = resolveDevicectlAppleProductType(device); if (isAppleProductType(productType)) return true; return resolveDevicectlAppleLabels(device).some(isAppleTvLabel); diff --git a/src/platforms/ios/install-artifact.ts b/src/platforms/apple/core/install-artifact.ts similarity index 97% rename from src/platforms/ios/install-artifact.ts rename to src/platforms/apple/core/install-artifact.ts index b338d86dd..e0819861e 100644 --- a/src/platforms/ios/install-artifact.ts +++ b/src/platforms/apple/core/install-artifact.ts @@ -2,13 +2,13 @@ import { promises as fs } from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import { readInfoPlistString } from './plist.ts'; -import { AppError } from '../../kernel/errors.ts'; -import { runCmd } from '../../utils/exec.ts'; +import { AppError } from '../../../kernel/errors.ts'; +import { runCmd } from '../../../utils/exec.ts'; import { isTrustedInstallSourceUrl, materializeInstallablePath, type MaterializeInstallSource, -} from '../install-source.ts'; +} from '../../install-source.ts'; type InstallIosArtifactOptions = { appIdentifierHint?: string; diff --git a/src/platforms/ios/launch-diagnostics.ts b/src/platforms/apple/core/launch-diagnostics.ts similarity index 96% rename from src/platforms/ios/launch-diagnostics.ts rename to src/platforms/apple/core/launch-diagnostics.ts index bc776fd6b..e0fe48c78 100644 --- a/src/platforms/ios/launch-diagnostics.ts +++ b/src/platforms/apple/core/launch-diagnostics.ts @@ -1,5 +1,5 @@ -import { AppError } from '../../kernel/errors.ts'; -import type { DeviceInfo } from '../../kernel/device.ts'; +import { AppError } from '../../../kernel/errors.ts'; +import type { DeviceInfo } from '../../../kernel/device.ts'; import { buildSimctlArgsForDevice } from './simctl.ts'; import { runAppleToolCommand, runXcrun } from './tool-provider.ts'; diff --git a/src/platforms/ios/perf-frame.ts b/src/platforms/apple/core/perf-frame.ts similarity index 94% rename from src/platforms/ios/perf-frame.ts rename to src/platforms/apple/core/perf-frame.ts index 529481c7d..5d4065c9b 100644 --- a/src/platforms/ios/perf-frame.ts +++ b/src/platforms/apple/core/perf-frame.ts @@ -1,4 +1,4 @@ -import { roundOneDecimal, roundPercent } from '../perf-utils.ts'; +import { roundOneDecimal, roundPercent } from '../../perf-utils.ts'; import { parseXmlDocumentSync, type XmlNode } from './xml.ts'; import { findAllXmlNodes, @@ -188,15 +188,18 @@ function buildAppleWorstWindows( } if (current.length > 0) windows.push(current); - return windows - .map((rows) => buildAppleWorstWindow(rows, windowStartedAtMs)) - .sort( - (left, right) => - right.missedDeadlineFrameCount - left.missedDeadlineFrameCount || - right.worstFrameMs - left.worstFrameMs, - ) - .slice(0, MAX_WORST_WINDOWS) - .sort((left, right) => left.startOffsetMs - right.startOffsetMs); + return ( + windows + // fallow-ignore-next-line code-duplication + .map((rows) => buildAppleWorstWindow(rows, windowStartedAtMs)) + .sort( + (left, right) => + right.missedDeadlineFrameCount - left.missedDeadlineFrameCount || + right.worstFrameMs - left.worstFrameMs, + ) + .slice(0, MAX_WORST_WINDOWS) + .sort((left, right) => left.startOffsetMs - right.startOffsetMs) + ); } function buildAppleWorstWindow( diff --git a/src/platforms/ios/perf-xctrace.ts b/src/platforms/apple/core/perf-xctrace.ts similarity index 97% rename from src/platforms/ios/perf-xctrace.ts rename to src/platforms/apple/core/perf-xctrace.ts index c37dc1a89..762822d01 100644 --- a/src/platforms/ios/perf-xctrace.ts +++ b/src/platforms/apple/core/perf-xctrace.ts @@ -2,10 +2,14 @@ import { promises as fs } from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; -import { isApplePlatform, type DeviceInfo } from '../../kernel/device.ts'; -import { AppError } from '../../kernel/errors.ts'; -import { runCmdBackground, type ExecBackgroundResult, type ExecResult } from '../../utils/exec.ts'; -import { uniqueStrings } from '../../daemon/action-utils.ts'; +import { isApplePlatform, type DeviceInfo } from '../../../kernel/device.ts'; +import { AppError } from '../../../kernel/errors.ts'; +import { + runCmdBackground, + type ExecBackgroundResult, + type ExecResult, +} from '../../../utils/exec.ts'; +import { uniqueStrings } from '../../../daemon/action-utils.ts'; import { findAllXmlNodes } from './perf-xml.ts'; import { isRetryableIosDeviceTraceRecordFailure, diff --git a/src/platforms/ios/perf-xml.ts b/src/platforms/apple/core/perf-xml.ts similarity index 100% rename from src/platforms/ios/perf-xml.ts rename to src/platforms/apple/core/perf-xml.ts diff --git a/src/platforms/ios/perf.ts b/src/platforms/apple/core/perf.ts similarity index 98% rename from src/platforms/ios/perf.ts rename to src/platforms/apple/core/perf.ts index d1647f134..7463fafc4 100644 --- a/src/platforms/ios/perf.ts +++ b/src/platforms/apple/core/perf.ts @@ -2,12 +2,12 @@ import { promises as fs } from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; -import type { DeviceInfo } from '../../kernel/device.ts'; -import { AppError } from '../../kernel/errors.ts'; -import type { ExecResult } from '../../utils/exec.ts'; -import { splitNonEmptyTrimmedLines } from '../../utils/parsing.ts'; -import { roundPercent } from '../perf-utils.ts'; -import { uniqueStrings } from '../../daemon/action-utils.ts'; +import type { DeviceInfo } from '../../../kernel/device.ts'; +import { AppError } from '../../../kernel/errors.ts'; +import type { ExecResult } from '../../../utils/exec.ts'; +import { splitNonEmptyTrimmedLines } from '../../../utils/parsing.ts'; +import { roundPercent } from '../../perf-utils.ts'; +import { uniqueStrings } from '../../../daemon/action-utils.ts'; import { IOS_DEVICECTL_DEFAULT_HINT, listIosDeviceApps, @@ -181,6 +181,7 @@ export async function captureAppleMemorySnapshot( } if (result.exitCode !== 0) { await cleanupLocalArtifact(outPath, hadLocalArtifact); + // fallow-ignore-next-line code-duplication throw new AppError('COMMAND_FAILED', `Failed to capture Apple memgraph for ${appBundleId}`, { kind: 'memgraph', appBundleId, @@ -288,6 +289,7 @@ function annotateAppleMemorySnapshotToolError( ); } +// fallow-ignore-next-line code-duplication async function fileExists(filePath: string): Promise { return await fs .stat(filePath) diff --git a/src/platforms/ios/plist.ts b/src/platforms/apple/core/plist.ts similarity index 100% rename from src/platforms/ios/plist.ts rename to src/platforms/apple/core/plist.ts diff --git a/src/platforms/ios/runner-client.ts b/src/platforms/apple/core/runner/runner-client.ts similarity index 96% rename from src/platforms/ios/runner-client.ts rename to src/platforms/apple/core/runner/runner-client.ts index 392ea52c0..95f1a4692 100644 --- a/src/platforms/ios/runner-client.ts +++ b/src/platforms/apple/core/runner/runner-client.ts @@ -1,6 +1,6 @@ -import { withRetry } from '../../utils/retry.ts'; -import type { DeviceInfo } from '../../kernel/device.ts'; -import { emitDiagnostic } from '../../utils/diagnostics.ts'; +import { withRetry } from '../../../../utils/retry.ts'; +import type { DeviceInfo } from '../../../../kernel/device.ts'; +import { emitDiagnostic } from '../../../../utils/diagnostics.ts'; import { type RunnerSessionOptions, validateRunnerDevice } from './runner-session.ts'; import { assertRunnerRequestActive, diff --git a/src/platforms/ios/runner-command-manifest.ts b/src/platforms/apple/core/runner/runner-command-manifest.ts similarity index 100% rename from src/platforms/ios/runner-command-manifest.ts rename to src/platforms/apple/core/runner/runner-command-manifest.ts diff --git a/src/platforms/ios/runner-command-recovery.ts b/src/platforms/apple/core/runner/runner-command-recovery.ts similarity index 98% rename from src/platforms/ios/runner-command-recovery.ts rename to src/platforms/apple/core/runner/runner-command-recovery.ts index 89ed3003f..2c99d1e26 100644 --- a/src/platforms/ios/runner-command-recovery.ts +++ b/src/platforms/apple/core/runner/runner-command-recovery.ts @@ -1,6 +1,6 @@ -import { AppError, toAppErrorCode } from '../../kernel/errors.ts'; -import type { DeviceInfo } from '../../kernel/device.ts'; -import { emitDiagnostic } from '../../utils/diagnostics.ts'; +import { AppError, toAppErrorCode } from '../../../../kernel/errors.ts'; +import type { DeviceInfo } from '../../../../kernel/device.ts'; +import { emitDiagnostic } from '../../../../utils/diagnostics.ts'; import type { RunnerCommand } from './runner-contract.ts'; import { isReadOnlyRunnerCommand } from './runner-command-traits.ts'; import type { AppleRunnerCommandOptions } from './runner-provider.ts'; diff --git a/src/platforms/ios/runner-command-traits.ts b/src/platforms/apple/core/runner/runner-command-traits.ts similarity index 100% rename from src/platforms/ios/runner-command-traits.ts rename to src/platforms/apple/core/runner/runner-command-traits.ts diff --git a/src/platforms/ios/runner-contract.ts b/src/platforms/apple/core/runner/runner-contract.ts similarity index 93% rename from src/platforms/ios/runner-contract.ts rename to src/platforms/apple/core/runner/runner-contract.ts index b1e69e6ff..30adf5181 100644 --- a/src/platforms/ios/runner-contract.ts +++ b/src/platforms/apple/core/runner/runner-contract.ts @@ -1,11 +1,14 @@ import crypto from 'node:crypto'; -import { AppError } from '../../kernel/errors.ts'; -import type { ClickButton } from '../../core/click-button.ts'; -import type { DeviceRotation } from '../../core/device-rotation.ts'; -import type { ScrollDirection } from '../../core/scroll-gesture.ts'; -import type { ElementSelectorKey } from '../../core/interactor-types.ts'; -import { createRequestCanceledError, isRequestCanceled } from '../../daemon/request-cancel.ts'; -import { bootFailureHint, classifyBootFailure } from '../boot-diagnostics.ts'; +import { AppError } from '../../../../kernel/errors.ts'; +import type { ClickButton } from '../../../../core/click-button.ts'; +import type { DeviceRotation } from '../../../../core/device-rotation.ts'; +import type { ScrollDirection } from '../../../../core/scroll-gesture.ts'; +import type { ElementSelectorKey } from '../../../../core/interactor-types.ts'; +import { + createRequestCanceledError, + isRequestCanceled, +} from '../../../../daemon/request-cancel.ts'; +import { bootFailureHint, classifyBootFailure } from '../../../boot-diagnostics.ts'; import type { RunnerSession } from './runner-session-types.ts'; const RUNNER_CACHE_RECOVERY_HINT = diff --git a/src/platforms/ios/runner-disposal.ts b/src/platforms/apple/core/runner/runner-disposal.ts similarity index 95% rename from src/platforms/ios/runner-disposal.ts rename to src/platforms/apple/core/runner/runner-disposal.ts index bbc2a7704..e0848098a 100644 --- a/src/platforms/ios/runner-disposal.ts +++ b/src/platforms/apple/core/runner/runner-disposal.ts @@ -1,6 +1,6 @@ -import { emitDiagnostic } from '../../utils/diagnostics.ts'; -import type { DeviceInfo } from '../../kernel/device.ts'; -import { isProcessAlive, isProcessGroupAlive } from '../../utils/process-identity.ts'; +import { emitDiagnostic } from '../../../../utils/diagnostics.ts'; +import type { DeviceInfo } from '../../../../kernel/device.ts'; +import { isProcessAlive, isProcessGroupAlive } from '../../../../utils/process-identity.ts'; import { cleanupTempFile, waitForRunner } from './runner-transport.ts'; import { withRunnerCommandId, type RunnerCommand } from './runner-contract.ts'; import { @@ -9,8 +9,8 @@ import { type RunnerLeaseCleanupAdapter, } from './runner-lease.ts'; import { IOS_RUNNER_CONTAINER_BUNDLE_IDS, runnerPrepProcesses } from './runner-xctestrun.ts'; -import { buildSimctlArgsForDevice } from './simctl.ts'; -import { runAppleToolCommand, runXcrun } from './tool-provider.ts'; +import { buildSimctlArgsForDevice } from '../simctl.ts'; +import { runAppleToolCommand, runXcrun } from '../tool-provider.ts'; import type { RunnerSession } from './runner-session-types.ts'; export const RUNNER_INVALIDATE_WAIT_TIMEOUT_MS = 1_000; diff --git a/src/platforms/ios/runner-failure-diagnostics.ts b/src/platforms/apple/core/runner/runner-failure-diagnostics.ts similarity index 98% rename from src/platforms/ios/runner-failure-diagnostics.ts rename to src/platforms/apple/core/runner/runner-failure-diagnostics.ts index 69305baaa..6c6a7b10d 100644 --- a/src/platforms/ios/runner-failure-diagnostics.ts +++ b/src/platforms/apple/core/runner/runner-failure-diagnostics.ts @@ -1,6 +1,6 @@ import fs from 'node:fs/promises'; import type { FileHandle } from 'node:fs/promises'; -import { AppError, type AppErrorCode } from '../../kernel/errors.ts'; +import { AppError, type AppErrorCode } from '../../../../kernel/errors.ts'; const RUNNER_LOG_TAIL_BYTES = 64 * 1024; diff --git a/src/platforms/ios/runner-icon.ts b/src/platforms/apple/core/runner/runner-icon.ts similarity index 97% rename from src/platforms/ios/runner-icon.ts rename to src/platforms/apple/core/runner/runner-icon.ts index 9a987b157..5307b096f 100644 --- a/src/platforms/ios/runner-icon.ts +++ b/src/platforms/apple/core/runner/runner-icon.ts @@ -1,7 +1,7 @@ import fs from 'node:fs'; import path from 'node:path'; -import { AppError } from '../../kernel/errors.ts'; -import { readApplePlistJson, runAppleToolCommand } from './tool-provider.ts'; +import { AppError } from '../../../../kernel/errors.ts'; +import { readApplePlistJson, runAppleToolCommand } from '../tool-provider.ts'; const ICON_PLIST_KEYS = ['CFBundleIcons', 'CFBundleIcons~ipad'] as const; diff --git a/src/platforms/ios/runner-lease.ts b/src/platforms/apple/core/runner/runner-lease.ts similarity index 97% rename from src/platforms/ios/runner-lease.ts rename to src/platforms/apple/core/runner/runner-lease.ts index 963539d15..8ea083b46 100644 --- a/src/platforms/ios/runner-lease.ts +++ b/src/platforms/apple/core/runner/runner-lease.ts @@ -2,11 +2,11 @@ import crypto from 'node:crypto'; import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; -import { emitDiagnostic } from '../../utils/diagnostics.ts'; -import { AppError } from '../../kernel/errors.ts'; -import { acquireProcessLock } from '../../utils/process-lock.ts'; -import { isProcessAlive, readProcessStartTime } from '../../utils/process-identity.ts'; -import type { RunnerLogicalLeaseContext } from '../../core/runner-lease-context.ts'; +import { emitDiagnostic } from '../../../../utils/diagnostics.ts'; +import { AppError } from '../../../../kernel/errors.ts'; +import { acquireProcessLock } from '../../../../utils/process-lock.ts'; +import { isProcessAlive, readProcessStartTime } from '../../../../utils/process-identity.ts'; +import type { RunnerLogicalLeaseContext } from '../../../../core/runner-lease-context.ts'; const RUNNER_LEASE_SCHEMA_VERSION = 1; const RUNNER_LEASE_LOCK_TIMEOUT_MS = 30_000; diff --git a/src/platforms/ios/runner-lifecycle.ts b/src/platforms/apple/core/runner/runner-lifecycle.ts similarity index 98% rename from src/platforms/ios/runner-lifecycle.ts rename to src/platforms/apple/core/runner/runner-lifecycle.ts index d40f33128..2d419b01a 100644 --- a/src/platforms/ios/runner-lifecycle.ts +++ b/src/platforms/apple/core/runner/runner-lifecycle.ts @@ -1,7 +1,7 @@ -import { AppError } from '../../kernel/errors.ts'; -import type { DeviceInfo } from '../../kernel/device.ts'; -import { emitDiagnostic } from '../../utils/diagnostics.ts'; -import { getRequestSignal, isRequestCanceledError } from '../../daemon/request-cancel.ts'; +import { AppError } from '../../../../kernel/errors.ts'; +import type { DeviceInfo } from '../../../../kernel/device.ts'; +import { emitDiagnostic } from '../../../../utils/diagnostics.ts'; +import { getRequestSignal, isRequestCanceledError } from '../../../../daemon/request-cancel.ts'; import { RUNNER_COMMAND_TIMEOUT_MS, RUNNER_STARTUP_TIMEOUT_MS } from './runner-transport.ts'; import { type RunnerSession, diff --git a/src/platforms/ios/runner-macos-products.ts b/src/platforms/apple/core/runner/runner-macos-products.ts similarity index 92% rename from src/platforms/ios/runner-macos-products.ts rename to src/platforms/apple/core/runner/runner-macos-products.ts index 9754c4c12..6ae60ab54 100644 --- a/src/platforms/ios/runner-macos-products.ts +++ b/src/platforms/apple/core/runner/runner-macos-products.ts @@ -1,7 +1,7 @@ import fs from 'node:fs'; -import type { DeviceInfo } from '../../kernel/device.ts'; -import { AppError } from '../../kernel/errors.ts'; -import { runAppleToolCommand } from './tool-provider.ts'; +import type { DeviceInfo } from '../../../../kernel/device.ts'; +import { AppError } from '../../../../kernel/errors.ts'; +import { runAppleToolCommand } from '../tool-provider.ts'; const RUNNER_PRODUCT_REPAIR_FAILURE_REASONS = new Set([ 'RUNNER_PRODUCT_MISSING', diff --git a/src/platforms/ios/runner-provider.ts b/src/platforms/apple/core/runner/runner-provider.ts similarity index 95% rename from src/platforms/ios/runner-provider.ts rename to src/platforms/apple/core/runner/runner-provider.ts index b67f6ab19..07a95ba3d 100644 --- a/src/platforms/ios/runner-provider.ts +++ b/src/platforms/apple/core/runner/runner-provider.ts @@ -1,7 +1,7 @@ import { AsyncLocalStorage } from 'node:async_hooks'; -import type { RunnerLogicalLeaseContext } from '../../core/runner-lease-context.ts'; -import type { DeviceInfo } from '../../kernel/device.ts'; -import type { Deadline } from '../../utils/retry.ts'; +import type { RunnerLogicalLeaseContext } from '../../../../core/runner-lease-context.ts'; +import type { DeviceInfo } from '../../../../kernel/device.ts'; +import type { Deadline } from '../../../../utils/retry.ts'; import type { RunnerCommand } from './runner-contract.ts'; import type { RunnerXctestrunArtifactState, diff --git a/src/platforms/ios/runner-sequence.ts b/src/platforms/apple/core/runner/runner-sequence.ts similarity index 99% rename from src/platforms/ios/runner-sequence.ts rename to src/platforms/apple/core/runner/runner-sequence.ts index 7e03a0e63..c2692abd3 100644 --- a/src/platforms/ios/runner-sequence.ts +++ b/src/platforms/apple/core/runner/runner-sequence.ts @@ -1,4 +1,4 @@ -import { AppError, toAppErrorCode } from '../../kernel/errors.ts'; +import { AppError, toAppErrorCode } from '../../../../kernel/errors.ts'; import type { RunnerCommand, RunnerSequenceStep } from './runner-contract.ts'; export const SEQUENCEABLE_RUNNER_STEP_KINDS = ['tap', 'doubleTap', 'longPress', 'drag'] as const; diff --git a/src/platforms/ios/runner-session-types.ts b/src/platforms/apple/core/runner/runner-session-types.ts similarity index 80% rename from src/platforms/ios/runner-session-types.ts rename to src/platforms/apple/core/runner/runner-session-types.ts index 108a62ece..e29eb9a21 100644 --- a/src/platforms/ios/runner-session-types.ts +++ b/src/platforms/apple/core/runner/runner-session-types.ts @@ -1,6 +1,6 @@ -import type { RunnerLogicalLeaseContext } from '../../core/runner-lease-context.ts'; -import type { ExecResult, ExecBackgroundResult } from '../../utils/exec.ts'; -import type { DeviceInfo } from '../../kernel/device.ts'; +import type { RunnerLogicalLeaseContext } from '../../../../core/runner-lease-context.ts'; +import type { ExecResult, ExecBackgroundResult } from '../../../../utils/exec.ts'; +import type { DeviceInfo } from '../../../../kernel/device.ts'; import type { RunnerXctestrunArtifact } from './runner-xctestrun.ts'; import type { RunnerLease } from './runner-lease.ts'; diff --git a/src/platforms/ios/runner-session.ts b/src/platforms/apple/core/runner/runner-session.ts similarity index 97% rename from src/platforms/ios/runner-session.ts rename to src/platforms/apple/core/runner/runner-session.ts index 1cc40d5d1..02dcadf6f 100644 --- a/src/platforms/ios/runner-session.ts +++ b/src/platforms/apple/core/runner/runner-session.ts @@ -1,14 +1,18 @@ -import { AppError, toAppErrorCode } from '../../kernel/errors.ts'; -import { runCmdBackground, type ExecResult, type ExecBackgroundResult } from '../../utils/exec.ts'; -import { withKeyedLock } from '../../utils/keyed-lock.ts'; -import { Deadline } from '../../utils/retry.ts'; -import { isApplePlatform, type DeviceInfo } from '../../kernel/device.ts'; -import type { RunnerLogicalLeaseContext } from '../../core/runner-lease-context.ts'; +import { AppError, toAppErrorCode } from '../../../../kernel/errors.ts'; +import { + runCmdBackground, + type ExecResult, + type ExecBackgroundResult, +} from '../../../../utils/exec.ts'; +import { withKeyedLock } from '../../../../utils/keyed-lock.ts'; +import { Deadline } from '../../../../utils/retry.ts'; +import { isApplePlatform, type DeviceInfo } from '../../../../kernel/device.ts'; +import type { RunnerLogicalLeaseContext } from '../../../../core/runner-lease-context.ts'; import type { AppleRunnerLifecycleOptions } from './runner-provider.ts'; -import { emitRequestProgress } from '../../daemon/request-progress.ts'; -import { emitDiagnostic, withDiagnosticTimer } from '../../utils/diagnostics.ts'; -import { buildSimctlArgsForDevice } from './simctl.ts'; -import { runAppleToolCommand, runXcrun } from './tool-provider.ts'; +import { emitRequestProgress } from '../../../../daemon/request-progress.ts'; +import { emitDiagnostic, withDiagnosticTimer } from '../../../../utils/diagnostics.ts'; +import { buildSimctlArgsForDevice } from '../simctl.ts'; +import { runAppleToolCommand, runXcrun } from '../tool-provider.ts'; import { waitForRunner, sendRunnerCommandOnce, diff --git a/src/platforms/ios/runner-transport.ts b/src/platforms/apple/core/runner/runner-transport.ts similarity index 97% rename from src/platforms/ios/runner-transport.ts rename to src/platforms/apple/core/runner/runner-transport.ts index 3a8142d62..174aa625d 100644 --- a/src/platforms/ios/runner-transport.ts +++ b/src/platforms/apple/core/runner/runner-transport.ts @@ -2,13 +2,16 @@ import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import net from 'node:net'; -import { createRequestCanceledError, isRequestCanceledError } from '../../daemon/request-cancel.ts'; -import { AppError } from '../../kernel/errors.ts'; -import { Deadline, retryWithPolicy } from '../../utils/retry.ts'; -import type { DeviceInfo } from '../../kernel/device.ts'; -import { classifyBootFailure, bootFailureHint } from '../boot-diagnostics.ts'; -import { buildSimctlArgsForDevice } from './simctl.ts'; -import { runXcrun } from './tool-provider.ts'; +import { + createRequestCanceledError, + isRequestCanceledError, +} from '../../../../daemon/request-cancel.ts'; +import { AppError } from '../../../../kernel/errors.ts'; +import { Deadline, retryWithPolicy } from '../../../../utils/retry.ts'; +import type { DeviceInfo } from '../../../../kernel/device.ts'; +import { classifyBootFailure, bootFailureHint } from '../../../boot-diagnostics.ts'; +import { buildSimctlArgsForDevice } from '../simctl.ts'; +import { runXcrun } from '../tool-provider.ts'; import { buildRunnerConnectError, buildRunnerEarlyExitError, diff --git a/src/platforms/ios/runner-xctestrun-products.ts b/src/platforms/apple/core/runner/runner-xctestrun-products.ts similarity index 97% rename from src/platforms/ios/runner-xctestrun-products.ts rename to src/platforms/apple/core/runner/runner-xctestrun-products.ts index 80a0d0959..cddb1b561 100644 --- a/src/platforms/ios/runner-xctestrun-products.ts +++ b/src/platforms/apple/core/runner/runner-xctestrun-products.ts @@ -1,8 +1,8 @@ import fs from 'node:fs'; import path from 'node:path'; -import { readApplePlistJson } from './tool-provider.ts'; -import { parseXmlDocumentSync, visitXmlPlistEntries, type XmlNode } from './xml.ts'; -import { isRecord } from '../../utils/parsing.ts'; +import { readApplePlistJson } from '../tool-provider.ts'; +import { parseXmlDocumentSync, visitXmlPlistEntries, type XmlNode } from '../xml.ts'; +import { isRecord } from '../../../../utils/parsing.ts'; const XCTESTRUN_PRODUCT_REFERENCE_KEYS = new Set([ 'ProductPaths', diff --git a/src/platforms/ios/runner-xctestrun.ts b/src/platforms/apple/core/runner/runner-xctestrun.ts similarity index 98% rename from src/platforms/ios/runner-xctestrun.ts rename to src/platforms/apple/core/runner/runner-xctestrun.ts index b5feb09b3..e53ce7c6f 100644 --- a/src/platforms/ios/runner-xctestrun.ts +++ b/src/platforms/apple/core/runner/runner-xctestrun.ts @@ -2,21 +2,21 @@ import fs from 'node:fs'; import crypto from 'node:crypto'; import os from 'node:os'; import path from 'node:path'; -import { AppError } from '../../kernel/errors.ts'; -import { runCmdStreaming, runCmdSync, type ExecBackgroundResult } from '../../utils/exec.ts'; -import { resolveIosSimulatorDeviceSetPath } from '../../utils/device-isolation.ts'; -import { readProcessStartTime } from '../../utils/process-identity.ts'; -import { acquireProcessLock, type ProcessLockOwner } from '../../utils/process-lock.ts'; -import { isEnvTruthy } from '../../utils/retry.ts'; -import type { DeviceInfo } from '../../kernel/device.ts'; -import type { DefinedEnvMap as EnvMap } from '../../utils/env-map.ts'; -import { withKeyedLock } from '../../utils/keyed-lock.ts'; -import { emitRequestProgress } from '../../daemon/request-progress.ts'; -import { emitDiagnostic } from '../../utils/diagnostics.ts'; -import { findProjectRoot, readVersion } from '../../utils/version.ts'; +import { AppError } from '../../../../kernel/errors.ts'; +import { runCmdStreaming, runCmdSync, type ExecBackgroundResult } from '../../../../utils/exec.ts'; +import { resolveIosSimulatorDeviceSetPath } from '../../../../utils/device-isolation.ts'; +import { readProcessStartTime } from '../../../../utils/process-identity.ts'; +import { acquireProcessLock, type ProcessLockOwner } from '../../../../utils/process-lock.ts'; +import { isEnvTruthy } from '../../../../utils/retry.ts'; +import type { DeviceInfo } from '../../../../kernel/device.ts'; +import type { DefinedEnvMap as EnvMap } from '../../../../utils/env-map.ts'; +import { withKeyedLock } from '../../../../utils/keyed-lock.ts'; +import { emitRequestProgress } from '../../../../daemon/request-progress.ts'; +import { emitDiagnostic } from '../../../../utils/diagnostics.ts'; +import { findProjectRoot, readVersion } from '../../../../utils/version.ts'; import { resolveRunnerBuildFailureHint } from './runner-contract.ts'; import { logChunk } from './runner-transport.ts'; -import { runAppleToolCommand } from './tool-provider.ts'; +import { runAppleToolCommand } from '../tool-provider.ts'; import { repairMacOsRunnerProductsIfNeeded, isExpectedRunnerRepairFailure, @@ -30,11 +30,11 @@ import { resolveRunnerPlatformName, resolveRunnerSdkName, resolveRunnerXctestrunHints, -} from './apple-runner-platform.ts'; +} from '../apple-runner-platform.ts'; export { resolveRunnerBuildDestination, resolveRunnerDestination, -} from './apple-runner-platform.ts'; +} from '../apple-runner-platform.ts'; const DEFAULT_IOS_RUNNER_APP_BUNDLE_ID = 'com.callstack.agentdevice.runner'; const XCTEST_DEVICE_SET_BASE_NAME = 'XCTestDevices'; @@ -778,7 +778,7 @@ export function resolveExpectedRunnerCacheMetadata( } function resolveRunnerToolchainFingerprint( - platformName: 'iOS' | 'tvOS' | 'macOS', + platformName: ReturnType, deviceKind: DeviceInfo['kind'], ): { xcodeVersion: string; diff --git a/src/platforms/ios/screenshot-status-bar.ts b/src/platforms/apple/core/screenshot-status-bar.ts similarity index 96% rename from src/platforms/ios/screenshot-status-bar.ts rename to src/platforms/apple/core/screenshot-status-bar.ts index aad57b07a..3c884bc8f 100644 --- a/src/platforms/ios/screenshot-status-bar.ts +++ b/src/platforms/apple/core/screenshot-status-bar.ts @@ -1,7 +1,7 @@ -import type { DeviceInfo } from '../../kernel/device.ts'; -import { emitDiagnostic } from '../../utils/diagnostics.ts'; -import { AppError } from '../../kernel/errors.ts'; -import type { ExecOptions } from '../../utils/exec.ts'; +import type { DeviceInfo } from '../../../kernel/device.ts'; +import { emitDiagnostic } from '../../../utils/diagnostics.ts'; +import { AppError } from '../../../kernel/errors.ts'; +import type { ExecOptions } from '../../../utils/exec.ts'; import { runSimctlForDevice } from './simctl.ts'; import { extractAppleToolErrorMeta } from './tool-diagnostics.ts'; diff --git a/src/platforms/ios/screenshot.ts b/src/platforms/apple/core/screenshot.ts similarity index 97% rename from src/platforms/ios/screenshot.ts rename to src/platforms/apple/core/screenshot.ts index 35b0c8bba..a3b138d57 100644 --- a/src/platforms/ios/screenshot.ts +++ b/src/platforms/apple/core/screenshot.ts @@ -1,10 +1,10 @@ import { promises as fs } from 'node:fs'; import path from 'node:path'; -import type { DeviceInfo } from '../../kernel/device.ts'; -import { emitDiagnostic } from '../../utils/diagnostics.ts'; -import { AppError } from '../../kernel/errors.ts'; -import type { ExecOptions } from '../../utils/exec.ts'; -import { Deadline, retryWithPolicy } from '../../utils/retry.ts'; +import type { DeviceInfo } from '../../../kernel/device.ts'; +import { emitDiagnostic } from '../../../utils/diagnostics.ts'; +import { AppError } from '../../../kernel/errors.ts'; +import type { ExecOptions } from '../../../utils/exec.ts'; +import { Deadline, retryWithPolicy } from '../../../utils/retry.ts'; import { IOS_RUNNER_SCREENSHOT_COPY_TIMEOUT_MS, @@ -14,8 +14,8 @@ import { IOS_SIMULATOR_SCREENSHOT_TIMEOUT_MS, } from './config.ts'; import { runIosDevicectl } from './devicectl.ts'; -import { runIosRunnerCommand, IOS_RUNNER_CONTAINER_BUNDLE_IDS } from './runner-client.ts'; -import type { AppleRunnerCommandOptions } from './runner-provider.ts'; +import { runIosRunnerCommand, IOS_RUNNER_CONTAINER_BUNDLE_IDS } from './runner/runner-client.ts'; +import type { AppleRunnerCommandOptions } from './runner/runner-provider.ts'; import { prepareSimulatorStatusBarForScreenshot } from './screenshot-status-bar.ts'; import { ensureBootedSimulator } from './simulator.ts'; import { runSimctlForDevice } from './simctl.ts'; diff --git a/src/platforms/ios/scroll.ts b/src/platforms/apple/core/scroll.ts similarity index 99% rename from src/platforms/ios/scroll.ts rename to src/platforms/apple/core/scroll.ts index a002b9c68..318e68672 100644 --- a/src/platforms/ios/scroll.ts +++ b/src/platforms/apple/core/scroll.ts @@ -1,4 +1,4 @@ -import { buildScrollGesturePlan, type ScrollDirection } from '../../core/scroll-gesture.ts'; +import { buildScrollGesturePlan, type ScrollDirection } from '../../../core/scroll-gesture.ts'; export type NormalizedScrollOptions = { amount?: number; diff --git a/src/platforms/ios/simctl.ts b/src/platforms/apple/core/simctl.ts similarity index 79% rename from src/platforms/ios/simctl.ts rename to src/platforms/apple/core/simctl.ts index bfab84203..c21e4c66d 100644 --- a/src/platforms/ios/simctl.ts +++ b/src/platforms/apple/core/simctl.ts @@ -1,6 +1,6 @@ -import type { DeviceInfo } from '../../kernel/device.ts'; -import type { ExecOptions, ExecResult } from '../../utils/exec.ts'; -import { resolveIosSimulatorDeviceSetPath } from '../../utils/device-isolation.ts'; +import type { DeviceInfo } from '../../../kernel/device.ts'; +import type { ExecOptions, ExecResult } from '../../../utils/exec.ts'; +import { resolveIosSimulatorDeviceSetPath } from '../../../utils/device-isolation.ts'; import { runXcrun } from './tool-provider.ts'; type SimctlArgsOptions = { diff --git a/src/platforms/ios/simulator.ts b/src/platforms/apple/core/simulator.ts similarity index 96% rename from src/platforms/ios/simulator.ts rename to src/platforms/apple/core/simulator.ts index 0f72e728c..0e2df4681 100644 --- a/src/platforms/ios/simulator.ts +++ b/src/platforms/apple/core/simulator.ts @@ -1,7 +1,7 @@ -import type { DeviceInfo } from '../../kernel/device.ts'; -import { AppError } from '../../kernel/errors.ts'; -import { Deadline, retryWithPolicy } from '../../utils/retry.ts'; -import { bootFailureHint, classifyBootFailure } from '../boot-diagnostics.ts'; +import type { DeviceInfo } from '../../../kernel/device.ts'; +import { AppError } from '../../../kernel/errors.ts'; +import { Deadline, retryWithPolicy } from '../../../utils/retry.ts'; +import { bootFailureHint, classifyBootFailure } from '../../boot-diagnostics.ts'; import { IOS_BOOT_TIMEOUT_MS, diff --git a/src/platforms/ios/tool-diagnostics.ts b/src/platforms/apple/core/tool-diagnostics.ts similarity index 94% rename from src/platforms/ios/tool-diagnostics.ts rename to src/platforms/apple/core/tool-diagnostics.ts index dfefe26cf..d517408a9 100644 --- a/src/platforms/ios/tool-diagnostics.ts +++ b/src/platforms/apple/core/tool-diagnostics.ts @@ -1,4 +1,4 @@ -import { AppError } from '../../kernel/errors.ts'; +import { AppError } from '../../../kernel/errors.ts'; export function extractAppleToolErrorMeta(error: unknown): Record { if (!(error instanceof AppError)) { diff --git a/src/platforms/ios/tool-provider-types.ts b/src/platforms/apple/core/tool-provider-types.ts similarity index 87% rename from src/platforms/ios/tool-provider-types.ts rename to src/platforms/apple/core/tool-provider-types.ts index ac02a42cd..7796c3e54 100644 --- a/src/platforms/ios/tool-provider-types.ts +++ b/src/platforms/apple/core/tool-provider-types.ts @@ -1,5 +1,5 @@ -import type { AppsFilter } from '../../contracts/app-inventory.ts'; -import type { ExecOptions, ExecResult } from '../../utils/exec.ts'; +import type { AppsFilter } from '../../../contracts/app-inventory.ts'; +import type { ExecOptions, ExecResult } from '../../../utils/exec.ts'; import type { IosAppInfo } from './app-info.ts'; export type AppleToolCommandExecutor = ( diff --git a/src/platforms/ios/tool-provider.ts b/src/platforms/apple/core/tool-provider.ts similarity index 96% rename from src/platforms/ios/tool-provider.ts rename to src/platforms/apple/core/tool-provider.ts index 0c6d9a60e..01856afab 100644 --- a/src/platforms/ios/tool-provider.ts +++ b/src/platforms/apple/core/tool-provider.ts @@ -1,6 +1,6 @@ -import { runCmd, whichCmd, type ExecOptions, type ExecResult } from '../../utils/exec.ts'; -import { createScopedProvider } from '../../utils/scoped-provider.ts'; -import { createLocalAppleMacOsHostProvider } from './macos-host-provider.ts'; +import { runCmd, whichCmd, type ExecOptions, type ExecResult } from '../../../utils/exec.ts'; +import { createScopedProvider } from '../../../utils/scoped-provider.ts'; +import { createLocalAppleMacOsHostProvider } from '../os/macos/host-provider.ts'; import type { AppleMacOsHelperProvider, AppleMacOsHostProvider, diff --git a/src/platforms/ios/xml.ts b/src/platforms/apple/core/xml.ts similarity index 100% rename from src/platforms/ios/xml.ts rename to src/platforms/apple/core/xml.ts diff --git a/src/platforms/ios/macos-apps.ts b/src/platforms/apple/os/macos/apps.ts similarity index 86% rename from src/platforms/ios/macos-apps.ts rename to src/platforms/apple/os/macos/apps.ts index 39e83c693..f1344ece4 100644 --- a/src/platforms/ios/macos-apps.ts +++ b/src/platforms/apple/os/macos/apps.ts @@ -1,12 +1,15 @@ -import type { AppsFilter } from '../../contracts/app-inventory.ts'; -import { isDeepLinkTarget } from '../../core/open-target.ts'; -import type { DeviceInfo } from '../../kernel/device.ts'; -import { AppError } from '../../kernel/errors.ts'; -import { parseAppearanceAction } from '../appearance.ts'; -import { createAppResolutionCache, type AppResolutionCacheScope } from '../app-resolution-cache.ts'; -import { quitMacOsApp } from './macos-helper.ts'; -import { resolveAppleToolProvider, type AppleMacOsHostProvider } from './tool-provider.ts'; -import type { IosAppInfo } from './app-info.ts'; +import type { AppsFilter } from '../../../../contracts/app-inventory.ts'; +import { isDeepLinkTarget } from '../../../../core/open-target.ts'; +import type { DeviceInfo } from '../../../../kernel/device.ts'; +import { AppError } from '../../../../kernel/errors.ts'; +import { parseAppearanceAction } from '../../../appearance.ts'; +import { + createAppResolutionCache, + type AppResolutionCacheScope, +} from '../../../app-resolution-cache.ts'; +import { quitMacOsApp } from './helper.ts'; +import { resolveAppleToolProvider, type AppleMacOsHostProvider } from '../../core/tool-provider.ts'; +import type { IosAppInfo } from '../../core/app-info.ts'; const MACOS_ALIASES: Record = { settings: 'com.apple.systempreferences', diff --git a/src/platforms/ios/desktop-scroll.ts b/src/platforms/apple/os/macos/desktop-scroll.ts similarity index 72% rename from src/platforms/ios/desktop-scroll.ts rename to src/platforms/apple/os/macos/desktop-scroll.ts index f887a276a..dd831bf41 100644 --- a/src/platforms/ios/desktop-scroll.ts +++ b/src/platforms/apple/os/macos/desktop-scroll.ts @@ -1,12 +1,12 @@ -import type { DeviceInfo } from '../../kernel/device.ts'; -import type { RunnerCallOptions, RunnerContext } from '../../core/interactor-types.ts'; -import type { ScrollDirection } from '../../core/scroll-gesture.ts'; -import type { RunnerCommand } from './runner-contract.ts'; +import type { DeviceInfo } from '../../../../kernel/device.ts'; +import type { RunnerCallOptions, RunnerContext } from '../../../../core/interactor-types.ts'; +import type { ScrollDirection } from '../../../../core/scroll-gesture.ts'; +import type { RunnerCommand } from '../../core/runner/runner-contract.ts'; import { normalizeAppleScrollResultWithResolvedFrame, scrollRunnerFields, type AppleScrollOptions, -} from './scroll.ts'; +} from '../../core/scroll.ts'; type RunRunnerCommand = ( device: DeviceInfo, diff --git a/src/platforms/macos/devices.ts b/src/platforms/apple/os/macos/devices.ts similarity index 86% rename from src/platforms/macos/devices.ts rename to src/platforms/apple/os/macos/devices.ts index d49f3ca3c..cef5eb8c5 100644 --- a/src/platforms/macos/devices.ts +++ b/src/platforms/apple/os/macos/devices.ts @@ -1,5 +1,5 @@ import os from 'node:os'; -import type { DeviceInfo } from '../../kernel/device.ts'; +import type { DeviceInfo } from '../../../../kernel/device.ts'; const HOST_MAC_DEVICE_ID = 'host-macos-local'; diff --git a/src/platforms/ios/macos-helper.ts b/src/platforms/apple/os/macos/helper.ts similarity index 97% rename from src/platforms/ios/macos-helper.ts rename to src/platforms/apple/os/macos/helper.ts index 2de9bc346..9f9a487ae 100644 --- a/src/platforms/ios/macos-helper.ts +++ b/src/platforms/apple/os/macos/helper.ts @@ -3,14 +3,14 @@ import { createHash } from 'node:crypto'; import os from 'node:os'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; -import { AppError } from '../../kernel/errors.ts'; -import { resolveExecutableOverridePath } from '../../utils/exec.ts'; -import type { SessionSurface } from '../../core/session-surface.ts'; +import { AppError } from '../../../../kernel/errors.ts'; +import { resolveExecutableOverridePath } from '../../../../utils/exec.ts'; +import type { SessionSurface } from '../../../../core/session-surface.ts'; import { hasScopedAppleToolProvider, resolveAppleToolProvider, runAppleToolCommand, -} from './tool-provider.ts'; +} from '../../core/tool-provider.ts'; export type MacOsPermissionTarget = 'accessibility' | 'screen-recording' | 'input-monitoring'; diff --git a/src/platforms/ios/macos-host-provider.ts b/src/platforms/apple/os/macos/host-provider.ts similarity index 93% rename from src/platforms/ios/macos-host-provider.ts rename to src/platforms/apple/os/macos/host-provider.ts index c2c113868..bcde52098 100644 --- a/src/platforms/ios/macos-host-provider.ts +++ b/src/platforms/apple/os/macos/host-provider.ts @@ -1,11 +1,14 @@ import { promises as fs } from 'node:fs'; import os from 'node:os'; import path from 'node:path'; -import type { AppsFilter } from '../../contracts/app-inventory.ts'; -import { AppError } from '../../kernel/errors.ts'; -import { filterAppleAppsByBundlePrefix } from './app-filter.ts'; -import type { IosAppInfo } from './app-info.ts'; -import type { AppleMacOsHostProvider, AppleToolCommandExecutor } from './tool-provider-types.ts'; +import type { AppsFilter } from '../../../../contracts/app-inventory.ts'; +import { AppError } from '../../../../kernel/errors.ts'; +import { filterAppleAppsByBundlePrefix } from '../../core/app-filter.ts'; +import type { IosAppInfo } from '../../core/app-info.ts'; +import type { + AppleMacOsHostProvider, + AppleToolCommandExecutor, +} from '../../core/tool-provider-types.ts'; type ApplePlistJsonReader = (plistPath: string) => Promise | null>; diff --git a/src/platforms/ios/__tests__/apple-runner-platform.test.ts b/src/platforms/ios/__tests__/apple-runner-platform.test.ts index af00f9c93..aea4e792f 100644 --- a/src/platforms/ios/__tests__/apple-runner-platform.test.ts +++ b/src/platforms/ios/__tests__/apple-runner-platform.test.ts @@ -1,6 +1,11 @@ import { test } from 'vitest'; import assert from 'node:assert/strict'; -import { resolveRunnerDestination, resolveRunnerPlatformName } from '../apple-runner-platform.ts'; +import { + resolveRunnerDestination, + resolveRunnerPlatformName, + resolveRunnerSdkName, + resolveRunnerXctestrunHints, +} from '../../apple/core/apple-runner-platform.ts'; import type { DeviceInfo } from '../../../kernel/device.ts'; function iosSim(overrides: Partial = {}): DeviceInfo { @@ -27,6 +32,17 @@ test('resolveRunnerPlatformName maps tvOS appleOs to the tvOS profile', () => { ); }); +test('resolveRunnerPlatformName maps visionOS appleOs to the visionOS profile', () => { + const vision = iosSim({ + id: 'vision-sim-1', + name: 'Apple Vision Pro', + appleOs: 'visionos', + }); + assert.equal(resolveRunnerPlatformName(vision), 'visionOS'); + assert.equal(resolveRunnerDestination(vision), 'platform=visionOS Simulator,id=vision-sim-1'); + assert.equal(resolveRunnerSdkName('visionOS', 'simulator'), 'xrsimulator'); +}); + test('resolveRunnerPlatformName maps macOS appleOs to the macOS profile', () => { const mac: DeviceInfo = { platform: 'macos', @@ -58,3 +74,34 @@ test('iPadOS produces a byte-identical runner profile and destination to legacy assert.equal(resolveRunnerDestination(taggedIpad), resolveRunnerDestination(legacyIpad)); assert.equal(resolveRunnerDestination(taggedIpad), 'platform=iOS Simulator,id=sim-ipad'); }); + +test('existing platform xctestrun disallowed hints stay unchanged when visionOS is added', () => { + assert.deepEqual(resolveRunnerXctestrunHints(iosSim()).disallowed, [ + 'iphoneos', + 'appletvos', + 'appletvsimulator', + 'macos', + ]); + assert.deepEqual( + resolveRunnerXctestrunHints(iosSim({ target: 'tv', appleOs: 'tvos' })).disallowed, + ['appletvos', 'iphoneos', 'iphonesimulator', 'macos'], + ); + assert.deepEqual( + resolveRunnerXctestrunHints({ + platform: 'macos', + id: 'host-macos-local', + name: 'Studio Mac', + kind: 'device', + target: 'desktop', + appleOs: 'macos', + booted: true, + }).disallowed, + ['iphoneos', 'iphonesimulator', 'appletvos', 'appletvsimulator'], + ); + assert.deepEqual( + resolveRunnerXctestrunHints( + iosSim({ id: 'vision-sim-1', name: 'Apple Vision Pro', appleOs: 'visionos' }), + ).disallowed, + ['xros', 'iphoneos', 'iphonesimulator', 'appletvos', 'appletvsimulator', 'macos'], + ); +}); diff --git a/src/platforms/ios/__tests__/debug-symbols.test.ts b/src/platforms/ios/__tests__/debug-symbols.test.ts index c08fbf38f..948d9999c 100644 --- a/src/platforms/ios/__tests__/debug-symbols.test.ts +++ b/src/platforms/ios/__tests__/debug-symbols.test.ts @@ -3,7 +3,7 @@ import fs from 'node:fs/promises'; import os from 'node:os'; import path from 'node:path'; import { test } from 'vitest'; -import { symbolicateCrashArtifact } from '../debug-symbols.ts'; +import { symbolicateCrashArtifact } from '../../apple/core/debug-symbols.ts'; import { AppError } from '../../../kernel/errors.ts'; import { withCommandExecutorOverride } from '../../../utils/exec.ts'; diff --git a/src/platforms/ios/__tests__/devices.test.ts b/src/platforms/ios/__tests__/devices.test.ts index 405bbef6c..df4c5d9b4 100644 --- a/src/platforms/ios/__tests__/devices.test.ts +++ b/src/platforms/ios/__tests__/devices.test.ts @@ -10,7 +10,7 @@ import { parseXctracePhysicalAppleDevices, resolveAppleTargetFromDevicectlDevice, withAppleToolProvider, -} from '../devices.ts'; +} from '../../apple/core/devices.ts'; import type { ExecResult } from '../../../utils/exec.ts'; const toolCalls: Array<[string, string[]]> = []; @@ -79,6 +79,7 @@ test('isSupportedAppleDevicectlDevice handles renamed AppleTV devices', () => { test('apple product type helpers classify iOS and tvOS product families', () => { assert.equal(isAppleProductType('iPhone16,2'), true); assert.equal(isAppleProductType('AppleTV11,1'), true); + assert.equal(isAppleProductType('RealityDevice14,1'), true); assert.equal(isAppleTvProductType('AppleTV11,1'), true); assert.equal(isAppleTvProductType('iPhone16,2'), false); }); @@ -137,6 +138,23 @@ test('parseXctracePhysicalAppleDevices tags physical iPads as iPadOS', () => { ]); }); +test('parseXctracePhysicalAppleDevices tags Apple Vision devices as visionOS', () => { + const parsed = parseXctracePhysicalAppleDevices( + ['== Devices ==', 'Apple Vision Pro [vision-udid-1]'].join('\n'), + ); + assert.deepEqual(parsed, [ + { + platform: 'ios', + id: 'vision-udid-1', + name: 'Apple Vision Pro', + kind: 'device', + target: 'mobile', + appleOs: 'visionos', + booted: true, + }, + ]); +}); + test('listAppleDevices tags devicectl iPad product types as iPadOS', async () => { mockRunCommand = async (_cmd, args) => { if (args.join(' ') === 'simctl list devices -j') { @@ -221,6 +239,43 @@ test('listAppleDevices tags iPhone simulators and the host Mac with appleOs', as assert.equal(byId.get('host-macos-local'), 'macos'); }); +test('listAppleDevices tags visionOS simulators', async () => { + mockRunCommand = async (_cmd, args) => { + if (args.includes('simctl') && args.includes('list') && args.includes('devices')) { + return { + stdout: JSON.stringify({ + devices: { + 'com.apple.CoreSimulator.SimRuntime.xrOS-26-2': [ + { + name: 'Apple Vision Pro', + udid: 'sim-vision', + state: 'Shutdown', + isAvailable: true, + deviceTypeIdentifier: 'com.apple.CoreSimulator.SimDeviceType.Apple-Vision-Pro-4K', + }, + ], + }, + }), + stderr: '', + exitCode: 0, + }; + } + throw new Error(`unexpected xcrun args: ${args.join(' ')}`); + }; + + const devices = await withMockedPlatform( + 'darwin', + async () => + await withMockedAppleTools( + async () => await listAppleDevices({ simulatorSetPath: '/tmp/agent-device-sim-set' }), + ), + ); + + const vision = devices.find((device) => device.id === 'sim-vision'); + assert.equal(vision?.target, 'mobile'); + assert.equal(vision?.appleOs, 'visionos'); +}); + test('listAppleDevices tags renamed iPad simulators as iPadOS from deviceTypeIdentifier', async () => { mockRunCommand = async (_cmd, args) => { if (args.includes('simctl') && args.includes('list') && args.includes('devices')) { diff --git a/src/platforms/ios/__tests__/index.test.ts b/src/platforms/ios/__tests__/index.test.ts index 993ee20e7..8e410b30c 100644 --- a/src/platforms/ios/__tests__/index.test.ts +++ b/src/platforms/ios/__tests__/index.test.ts @@ -12,20 +12,20 @@ vi.mock('../../../utils/retry.ts', async (importOriginal) => { const actual = await importOriginal(); return { ...actual, retryWithPolicy: vi.fn(actual.retryWithPolicy) }; }); -vi.mock('../runner-client.ts', async (importOriginal) => { - const actual = await importOriginal(); +vi.mock('../../apple/core/runner/runner-client.ts', async (importOriginal) => { + const actual = await importOriginal(); return { ...actual, runIosRunnerCommand: vi.fn(actual.runIosRunnerCommand) }; }); -vi.mock('../simulator.ts', async (importOriginal) => { - const actual = await importOriginal(); +vi.mock('../../apple/core/simulator.ts', async (importOriginal) => { + const actual = await importOriginal(); return { ...actual, ensureBootedSimulator: vi.fn(actual.ensureBootedSimulator), openIosSimulatorApp: vi.fn(actual.openIosSimulatorApp), }; }); -vi.mock('../screenshot-status-bar.ts', async (importOriginal) => { - const actual = await importOriginal(); +vi.mock('../../apple/core/screenshot-status-bar.ts', async (importOriginal) => { + const actual = await importOriginal(); return { ...actual, prepareSimulatorStatusBarForScreenshot: vi.fn(actual.prepareSimulatorStatusBarForScreenshot), @@ -36,12 +36,15 @@ const execActual = await vi.importActual('../../../utils/exec.ts'); const retryActual = await vi.importActual('../../../utils/retry.ts'); -const runnerActual = - await vi.importActual('../runner-client.ts'); -const simulatorActual = await vi.importActual('../simulator.ts'); +const runnerActual = await vi.importActual< + typeof import('../../apple/core/runner/runner-client.ts') +>('../../apple/core/runner/runner-client.ts'); +const simulatorActual = await vi.importActual( + '../../apple/core/simulator.ts', +); const screenshotStatusBarActual = await vi.importActual< - typeof import('../screenshot-status-bar.ts') ->('../screenshot-status-bar.ts'); + typeof import('../../apple/core/screenshot-status-bar.ts') +>('../../apple/core/screenshot-status-bar.ts'); import { closeIosApp, @@ -57,30 +60,36 @@ import { setIosSetting, shouldFallbackToRunnerForIosScreenshot, shouldRetryIosSimulatorScreenshot, -} from '../apps.ts'; +} from '../../apple/core/apps.ts'; import { withMockedMacOsHelper } from './macos-helper-test-utils.ts'; -import { quitMacOsApp, resolveMacOsHelperPackageRootFrom } from '../macos-helper.ts'; +import { quitMacOsApp, resolveMacOsHelperPackageRootFrom } from '../../apple/os/macos/helper.ts'; import { captureSimulatorScreenshotWithFallback, captureSimulatorScreenshotWithRetry, captureScreenshotViaRunner, prepareSimulatorStatusBarForScreenshot, resolveSimulatorRunnerScreenshotCandidatePaths, -} from '../screenshot.ts'; -import { ensureBootedSimulator, openIosSimulatorApp } from '../simulator.ts'; +} from '../../apple/core/screenshot.ts'; +import { ensureBootedSimulator, openIosSimulatorApp } from '../../apple/core/simulator.ts'; import { invalidateSimulatorStatusBarOverrideCache, prepareSimulatorStatusBarForScreenshot as prepareStatusBarForScreenshot, -} from '../screenshot-status-bar.ts'; -import { runIosRunnerCommand } from '../runner-client.ts'; +} from '../../apple/core/screenshot-status-bar.ts'; +import { runIosRunnerCommand } from '../../apple/core/runner/runner-client.ts'; import { iosRunnerOverrides } from '../interactions.ts'; -import { IOS_DEVICE_INSTALL_TIMEOUT_MS, IOS_SIMULATOR_TERMINATE_TIMEOUT_MS } from '../config.ts'; +import { + IOS_DEVICE_INSTALL_TIMEOUT_MS, + IOS_SIMULATOR_TERMINATE_TIMEOUT_MS, +} from '../../apple/core/config.ts'; import type { DeviceInfo } from '../../../kernel/device.ts'; import { withDiagnosticsScope } from '../../../utils/diagnostics.ts'; import { AppError } from '../../../kernel/errors.ts'; import { runCmd } from '../../../utils/exec.ts'; import { retryWithPolicy } from '../../../utils/retry.ts'; -import { parseIosDeviceAppsPayload, parseIosDeviceProcessesPayload } from '../devicectl.ts'; +import { + parseIosDeviceAppsPayload, + parseIosDeviceProcessesPayload, +} from '../../apple/core/devicectl.ts'; const IOS_TEST_DEVICE: DeviceInfo = { platform: 'ios', diff --git a/src/platforms/ios/__tests__/launch-diagnostics.test.ts b/src/platforms/ios/__tests__/launch-diagnostics.test.ts index f5fb2c1af..44fb95567 100644 --- a/src/platforms/ios/__tests__/launch-diagnostics.test.ts +++ b/src/platforms/ios/__tests__/launch-diagnostics.test.ts @@ -4,7 +4,7 @@ import { isSimulatorLaunchFBSError, classifyLaunchFailure, launchFailureHint, -} from '../launch-diagnostics.ts'; +} from '../../apple/core/launch-diagnostics.ts'; import { AppError } from '../../../kernel/errors.ts'; test('isSimulatorLaunchFBSError identifies FBS code=4 errors', () => { diff --git a/src/platforms/ios/__tests__/perf.test.ts b/src/platforms/ios/__tests__/perf.test.ts index 455ccd245..8698be286 100644 --- a/src/platforms/ios/__tests__/perf.test.ts +++ b/src/platforms/ios/__tests__/perf.test.ts @@ -18,14 +18,14 @@ import { parseApplePsOutput, sampleAppleFramePerf, sampleApplePerfMetrics, -} from '../perf.ts'; +} from '../../apple/core/perf.ts'; import { startAppleXctracePerfCapture, stopAppleXctracePerfCapture, writeAppleXctracePerfReport, type AppleXctracePerfCapture, -} from '../perf-xctrace.ts'; -import { parseAppleFramePerfSample } from '../perf-frame.ts'; +} from '../../apple/core/perf-xctrace.ts'; +import { parseAppleFramePerfSample } from '../../apple/core/perf-frame.ts'; import { runCmd, runCmdBackground } from '../../../utils/exec.ts'; import type { DeviceInfo } from '../../../kernel/device.ts'; import { AppError } from '../../../kernel/errors.ts'; diff --git a/src/platforms/ios/__tests__/plist.test.ts b/src/platforms/ios/__tests__/plist.test.ts index 2b30225b2..242f29c84 100644 --- a/src/platforms/ios/__tests__/plist.test.ts +++ b/src/platforms/ios/__tests__/plist.test.ts @@ -10,7 +10,7 @@ vi.mock('../../../utils/exec.ts', async (importOriginal) => { }); import { runCmd } from '../../../utils/exec.ts'; -import { readInfoPlistString } from '../plist.ts'; +import { readInfoPlistString } from '../../apple/core/plist.ts'; const mockRunCmd = vi.mocked(runCmd); diff --git a/src/platforms/ios/__tests__/runner-client.test.ts b/src/platforms/ios/__tests__/runner-client.test.ts index 3545402c9..b8fd46261 100644 --- a/src/platforms/ios/__tests__/runner-client.test.ts +++ b/src/platforms/ios/__tests__/runner-client.test.ts @@ -19,10 +19,10 @@ vi.mock('../../../utils/exec.ts', async () => { }; }); -vi.mock('../runner-macos-products.ts', async () => { - const actual = await vi.importActual( - '../runner-macos-products.ts', - ); +vi.mock('../../apple/core/runner/runner-macos-products.ts', async () => { + const actual = await vi.importActual< + typeof import('../../apple/core/runner/runner-macos-products.ts') + >('../../apple/core/runner/runner-macos-products.ts'); return { ...actual, repairMacOsRunnerProductsIfNeeded: mockRepairMacOsRunnerProductsIfNeeded, @@ -36,8 +36,11 @@ import { } from '../../../daemon/request-progress.ts'; import { flushDiagnosticsToSessionFile, withDiagnosticsScope } from '../../../utils/diagnostics.ts'; import { AppError } from '../../../kernel/errors.ts'; -import { isReadOnlyRunnerCommand } from '../runner-command-traits.ts'; -import { withRunnerCommandId, type RunnerCommand } from '../runner-contract.ts'; +import { isReadOnlyRunnerCommand } from '../../apple/core/runner/runner-command-traits.ts'; +import { + withRunnerCommandId, + type RunnerCommand, +} from '../../apple/core/runner/runner-contract.ts'; import { assertSafeDerivedCleanup, isRetryableRunnerError, @@ -49,7 +52,7 @@ import { resolveRunnerMaxConcurrentDestinationsFlag, resolveRunnerSigningBuildSettings, shouldRetryRunnerConnectError, -} from '../runner-client.ts'; +} from '../../apple/core/runner/runner-client.ts'; import { acquireRunnerXctestrunCacheLock, ensureXctestrunArtifact, @@ -63,8 +66,8 @@ import { shouldDeleteRunnerDerivedRootEntry, writeRunnerCacheMetadata, xctestrunReferencesProjectRoot, -} from '../runner-xctestrun.ts'; -import { parseRunnerResponse } from '../runner-session.ts'; +} from '../../apple/core/runner/runner-xctestrun.ts'; +import { parseRunnerResponse } from '../../apple/core/runner/runner-session.ts'; const iosSimulator: DeviceInfo = { platform: 'ios', diff --git a/src/platforms/ios/__tests__/runner-command-retry.test.ts b/src/platforms/ios/__tests__/runner-command-retry.test.ts index b3c69c8c8..faa58258b 100644 --- a/src/platforms/ios/__tests__/runner-command-retry.test.ts +++ b/src/platforms/ios/__tests__/runner-command-retry.test.ts @@ -4,7 +4,7 @@ import { IOS_SIMULATOR } from '../../../__tests__/test-utils/index.ts'; import { clearRequestCanceled, markRequestCanceled } from '../../../daemon/request-cancel.ts'; import { AppError } from '../../../kernel/errors.ts'; import { Deadline } from '../../../utils/retry.ts'; -import type { RunnerSession } from '../runner-session-types.ts'; +import type { RunnerSession } from '../../apple/core/runner/runner-session-types.ts'; const { mockEnsureRunnerSession, @@ -30,9 +30,10 @@ vi.mock('../../../utils/diagnostics.ts', async () => { }; }); -vi.mock('../runner-session.ts', async () => { - const actual = - await vi.importActual('../runner-session.ts'); +vi.mock('../../apple/core/runner/runner-session.ts', async () => { + const actual = await vi.importActual( + '../../apple/core/runner/runner-session.ts', + ); return { ...actual, ensureRunnerSession: mockEnsureRunnerSession, @@ -41,9 +42,10 @@ vi.mock('../runner-session.ts', async () => { }; }); -vi.mock('../runner-xctestrun.ts', async () => { - const actual = - await vi.importActual('../runner-xctestrun.ts'); +vi.mock('../../apple/core/runner/runner-xctestrun.ts', async () => { + const actual = await vi.importActual< + typeof import('../../apple/core/runner/runner-xctestrun.ts') + >('../../apple/core/runner/runner-xctestrun.ts'); return { ...actual, markRunnerXctestrunArtifactBadForRun: mockMarkRunnerXctestrunArtifactBadForRun, @@ -54,8 +56,8 @@ import { prepareIosRunner, prewarmIosRunnerSession, runIosRunnerCommand, -} from '../runner-client.ts'; -import type { RunnerXctestrunArtifact } from '../runner-xctestrun.ts'; +} from '../../apple/core/runner/runner-client.ts'; +import type { RunnerXctestrunArtifact } from '../../apple/core/runner/runner-xctestrun.ts'; beforeEach(() => { vi.resetAllMocks(); diff --git a/src/platforms/ios/__tests__/runner-command-traits.test.ts b/src/platforms/ios/__tests__/runner-command-traits.test.ts index 731a17a3d..76f04a56a 100644 --- a/src/platforms/ios/__tests__/runner-command-traits.test.ts +++ b/src/platforms/ios/__tests__/runner-command-traits.test.ts @@ -1,14 +1,14 @@ import assert from 'node:assert/strict'; import { test } from 'vitest'; -import type { RunnerCommand } from '../runner-contract.ts'; +import type { RunnerCommand } from '../../apple/core/runner/runner-contract.ts'; import { canSkipRunnerReadinessPreflightAfterHealthyMutation, isReadOnlyRunnerCommand, isRunnerReadinessProbeCommand, readRunnerCommandTraits, type RunnerCommandTraits, -} from '../runner-command-traits.ts'; -import { RUNNER_COMMAND_TRAIT_MANIFEST } from '../runner-command-manifest.ts'; +} from '../../apple/core/runner/runner-command-traits.ts'; +import { RUNNER_COMMAND_TRAIT_MANIFEST } from '../../apple/core/runner/runner-command-manifest.ts'; const EXPECTED_RUNNER_COMMAND_TRAITS = Object.fromEntries( Object.entries(RUNNER_COMMAND_TRAIT_MANIFEST).map(([command, traitClass]) => [ diff --git a/src/platforms/ios/__tests__/runner-icon.test.ts b/src/platforms/ios/__tests__/runner-icon.test.ts index ea288665e..926e0c2df 100644 --- a/src/platforms/ios/__tests__/runner-icon.test.ts +++ b/src/platforms/ios/__tests__/runner-icon.test.ts @@ -4,11 +4,14 @@ import os from 'node:os'; import path from 'node:path'; import { test } from 'vitest'; -import { createLocalAppleToolProvider, withAppleToolProvider } from '../tool-provider.ts'; +import { + createLocalAppleToolProvider, + withAppleToolProvider, +} from '../../apple/core/tool-provider.ts'; import { applyXctestRunnerAppIcon, applyXctestRunnerAppIconFromDerivedPath, -} from '../runner-icon.ts'; +} from '../../apple/core/runner/runner-icon.ts'; type AppleToolCall = [string, string[]]; diff --git a/src/platforms/ios/__tests__/runner-provider.test.ts b/src/platforms/ios/__tests__/runner-provider.test.ts index 428b3a2f3..9ae9b884a 100644 --- a/src/platforms/ios/__tests__/runner-provider.test.ts +++ b/src/platforms/ios/__tests__/runner-provider.test.ts @@ -5,7 +5,7 @@ import { resolveAppleRunnerProvider, withAppleRunnerProvider, type AppleRunnerProvider, -} from '../runner-provider.ts'; +} from '../../apple/core/runner/runner-provider.ts'; test('scoped Apple runner provider requires matching request id when scoped by request', async () => { const calls: string[] = []; diff --git a/src/platforms/ios/__tests__/runner-sequence.test.ts b/src/platforms/ios/__tests__/runner-sequence.test.ts index 17c71eaac..8febf5491 100644 --- a/src/platforms/ios/__tests__/runner-sequence.test.ts +++ b/src/platforms/ios/__tests__/runner-sequence.test.ts @@ -7,8 +7,8 @@ import { buildRunnerSequenceCommand, parseRunnerSequenceResult, validateRunnerSequenceSteps, -} from '../runner-sequence.ts'; -import type { RunnerSequenceStep } from '../runner-contract.ts'; +} from '../../apple/core/runner/runner-sequence.ts'; +import type { RunnerSequenceStep } from '../../apple/core/runner/runner-contract.ts'; function tap(x: number, y: number): RunnerSequenceStep { return { kind: 'tap', x, y }; diff --git a/src/platforms/ios/__tests__/runner-session.test.ts b/src/platforms/ios/__tests__/runner-session.test.ts index 9f57b9ec6..aaf8da286 100644 --- a/src/platforms/ios/__tests__/runner-session.test.ts +++ b/src/platforms/ios/__tests__/runner-session.test.ts @@ -11,7 +11,7 @@ import { } from '../../../daemon/request-progress.ts'; import { AppError } from '../../../kernel/errors.ts'; import { flushDiagnosticsToSessionFile, withDiagnosticsScope } from '../../../utils/diagnostics.ts'; -import type { RunnerSession } from '../runner-session-types.ts'; +import type { RunnerSession } from '../../apple/core/runner/runner-session-types.ts'; const { mockAcquireXcodebuildSimulatorSetRedirect, @@ -67,8 +67,10 @@ vi.mock('../../../utils/process-identity.ts', async () => { }; }); -vi.mock('../tool-provider.ts', async () => { - const actual = await vi.importActual('../tool-provider.ts'); +vi.mock('../../apple/core/tool-provider.ts', async () => { + const actual = await vi.importActual( + '../../apple/core/tool-provider.ts', + ); return { ...actual, runAppleToolCommand: mockRunAppleToolCommand, @@ -76,9 +78,10 @@ vi.mock('../tool-provider.ts', async () => { }; }); -vi.mock('../runner-transport.ts', async () => { - const actual = - await vi.importActual('../runner-transport.ts'); +vi.mock('../../apple/core/runner/runner-transport.ts', async () => { + const actual = await vi.importActual< + typeof import('../../apple/core/runner/runner-transport.ts') + >('../../apple/core/runner/runner-transport.ts'); return { ...actual, cleanupTempFile: mockCleanupTempFile, @@ -88,9 +91,10 @@ vi.mock('../runner-transport.ts', async () => { }; }); -vi.mock('../runner-xctestrun.ts', async () => { - const actual = - await vi.importActual('../runner-xctestrun.ts'); +vi.mock('../../apple/core/runner/runner-xctestrun.ts', async () => { + const actual = await vi.importActual< + typeof import('../../apple/core/runner/runner-xctestrun.ts') + >('../../apple/core/runner/runner-xctestrun.ts'); return { ...actual, acquireXcodebuildSimulatorSetRedirect: mockAcquireXcodebuildSimulatorSetRedirect, @@ -109,7 +113,7 @@ import { invalidateRunnerSession, stopIosRunnerSession, validateRunnerDevice, -} from '../runner-session.ts'; +} from '../../apple/core/runner/runner-session.ts'; import { cleanupRunnerLeasesForOwner, RUNNER_OWNER_START_TIME, @@ -117,7 +121,7 @@ import { setRunnerLeaseOwnerStateDir, writeRunnerLease, type RunnerLease, -} from '../runner-lease.ts'; +} from '../../apple/core/runner/runner-lease.ts'; beforeEach(async () => { await abortAllIosRunnerSessions(); diff --git a/src/platforms/ios/__tests__/runner-transport.test.ts b/src/platforms/ios/__tests__/runner-transport.test.ts index dde87a276..04c171410 100644 --- a/src/platforms/ios/__tests__/runner-transport.test.ts +++ b/src/platforms/ios/__tests__/runner-transport.test.ts @@ -4,7 +4,7 @@ import assert from 'node:assert/strict'; import type { DeviceInfo } from '../../../kernel/device.ts'; import type { ExecBackgroundResult } from '../../../utils/exec.ts'; import { AppError } from '../../../kernel/errors.ts'; -import type { RunnerSession } from '../runner-session-types.ts'; +import type { RunnerSession } from '../../apple/core/runner/runner-session-types.ts'; const { mockRunCmd } = vi.hoisted(() => ({ mockRunCmd: vi.fn(), @@ -23,7 +23,7 @@ import { clearDeviceTunnelIpCache, sendRunnerCommandOnce, waitForRunner, -} from '../runner-transport.ts'; +} from '../../apple/core/runner/runner-transport.ts'; const iosSimulator: DeviceInfo = { platform: 'ios', diff --git a/src/platforms/ios/__tests__/runner-xctestrun.test.ts b/src/platforms/ios/__tests__/runner-xctestrun.test.ts index 919526596..1ffccfc06 100644 --- a/src/platforms/ios/__tests__/runner-xctestrun.test.ts +++ b/src/platforms/ios/__tests__/runner-xctestrun.test.ts @@ -18,7 +18,7 @@ import { resolveRunnerDerivedPath, resolveXcodebuildSimulatorDeviceSetPath, scoreXctestrunCandidate, -} from '../runner-xctestrun.ts'; +} from '../../apple/core/runner/runner-xctestrun.ts'; const iosSimulator: DeviceInfo = { platform: 'ios', diff --git a/src/platforms/ios/__tests__/simctl.test.ts b/src/platforms/ios/__tests__/simctl.test.ts index c16c62d0e..758cf0001 100644 --- a/src/platforms/ios/__tests__/simctl.test.ts +++ b/src/platforms/ios/__tests__/simctl.test.ts @@ -1,6 +1,6 @@ import { test } from 'vitest'; import assert from 'node:assert/strict'; -import { buildSimctlArgs, buildSimctlArgsForDevice } from '../simctl.ts'; +import { buildSimctlArgs, buildSimctlArgsForDevice } from '../../apple/core/simctl.ts'; import type { DeviceInfo } from '../../../kernel/device.ts'; const IOS_SIMULATOR: DeviceInfo = { diff --git a/src/platforms/ios/__tests__/tool-provider.test.ts b/src/platforms/ios/__tests__/tool-provider.test.ts index 70e21fa41..00f4c4c03 100644 --- a/src/platforms/ios/__tests__/tool-provider.test.ts +++ b/src/platforms/ios/__tests__/tool-provider.test.ts @@ -6,7 +6,7 @@ import { runAppleToolCommand, runXcrun, withAppleToolProvider, -} from '../tool-provider.ts'; +} from '../../apple/core/tool-provider.ts'; test('scoped Apple tool provider handles xcrun execution', async () => { const calls: Array<[string, string[]]> = []; diff --git a/src/platforms/ios/__tests__/xml.test.ts b/src/platforms/ios/__tests__/xml.test.ts index c498ea5dd..7045659b2 100644 --- a/src/platforms/ios/__tests__/xml.test.ts +++ b/src/platforms/ios/__tests__/xml.test.ts @@ -1,7 +1,7 @@ import assert from 'node:assert/strict'; import { test } from 'vitest'; -import { parseXmlDocumentSync } from '../xml.ts'; +import { parseXmlDocumentSync } from '../../apple/core/xml.ts'; test('parseXmlDocumentSync preserves ordered nodes with attributes and decoded text', () => { const nodes = parseXmlDocumentSync( diff --git a/src/platforms/ios/interactions.ts b/src/platforms/ios/interactions.ts index 9d73c188f..227048745 100644 --- a/src/platforms/ios/interactions.ts +++ b/src/platforms/ios/interactions.ts @@ -1,16 +1,19 @@ import type { DeviceInfo } from '../../kernel/device.ts'; import { assertScrollGestureInput, type ScrollDirection } from '../../core/scroll-gesture.ts'; import { normalizeScrollDurationMs, SCROLL_DURATION_MAX_MS } from '../../core/scroll-command.ts'; -import { runIosRunnerCommand } from './runner-client.ts'; -import { buildRunnerSequenceCommand, parseRunnerSequenceResult } from './runner-sequence.ts'; -import type { RunnerCommand } from './runner-contract.ts'; -import { runMacosDesktopScroll } from './desktop-scroll.ts'; +import { runIosRunnerCommand } from '../apple/core/runner/runner-client.ts'; +import { + buildRunnerSequenceCommand, + parseRunnerSequenceResult, +} from '../apple/core/runner/runner-sequence.ts'; +import type { RunnerCommand } from '../apple/core/runner/runner-contract.ts'; +import { runMacosDesktopScroll } from '../apple/os/macos/desktop-scroll.ts'; import { normalizeAppleScrollResult, normalizeAppleScrollResultWithResolvedFrame, scrollRunnerFields, type AppleScrollOptions, -} from './scroll.ts'; +} from '../apple/core/scroll.ts'; import type { BackMode, Interactor, diff --git a/src/utils/__tests__/device.test.ts b/src/utils/__tests__/device.test.ts index a6bdb8671..e3da7f144 100644 --- a/src/utils/__tests__/device.test.ts +++ b/src/utils/__tests__/device.test.ts @@ -41,6 +41,7 @@ test('resolveApplePlatformName prefers the explicit appleOs over target inferenc assert.equal(resolveApplePlatformName('mobile', 'ipados'), 'iOS'); assert.equal(resolveApplePlatformName('tv', 'tvos'), 'tvOS'); assert.equal(resolveApplePlatformName('desktop', 'macos'), 'macOS'); + assert.equal(resolveApplePlatformName('mobile', 'visionos'), 'visionOS'); }); test('resolveApplePlatformName appleOs wins even when it disagrees with the legacy target', () => { diff --git a/src/utils/__tests__/interactors.test.ts b/src/utils/__tests__/interactors.test.ts index 668def4ae..d65fcc0dc 100644 --- a/src/utils/__tests__/interactors.test.ts +++ b/src/utils/__tests__/interactors.test.ts @@ -1,17 +1,18 @@ import { beforeEach, test, vi } from 'vitest'; import assert from 'node:assert/strict'; -import type { RunnerCommand } from '../../platforms/ios/runner-client.ts'; +import type { RunnerCommand } from '../../platforms/apple/core/runner/runner-client.ts'; import type { DeviceInfo } from '../../kernel/device.ts'; import { AppError } from '../../kernel/errors.ts'; -vi.mock('../../platforms/ios/runner-client.ts', async (importOriginal) => { - const actual = await importOriginal(); +vi.mock('../../platforms/apple/core/runner/runner-client.ts', async (importOriginal) => { + const actual = + await importOriginal(); return { ...actual, runIosRunnerCommand: vi.fn() }; }); import { getInteractor } from '../../core/interactors.ts'; import { resolveAppleBackRunnerCommand } from '../../platforms/ios/interactions.ts'; -import { runIosRunnerCommand } from '../../platforms/ios/runner-client.ts'; +import { runIosRunnerCommand } from '../../platforms/apple/core/runner/runner-client.ts'; const iosSimulator: DeviceInfo = { platform: 'ios', diff --git a/src/utils/diagnostics.ts b/src/utils/diagnostics.ts index f3e2daf45..5e176e690 100644 --- a/src/utils/diagnostics.ts +++ b/src/utils/diagnostics.ts @@ -86,22 +86,6 @@ export function getDiagnosticsMeta(): { }; } -/** - * Sum the number of diagnostic events emitted in the current scope whose phase - * is one of `phases`. Backed by the flush-surviving `phaseCounts` tally, so it - * stays accurate for the whole request even under `--debug` (where `events` is - * streamed out and reset). Returns 0 when called outside a diagnostics scope. - */ -export function countDiagnosticEventsByPhase(phases: readonly string[]): number { - const scope = diagnosticsStorage.getStore(); - if (!scope) return 0; - let total = 0; - for (const phase of phases) { - total += scope.phaseCounts.get(phase) ?? 0; - } - return total; -} - export function emitDiagnostic(event: { level?: DiagnosticLevel; phase: string; diff --git a/test/integration/provider-scenarios/macos-desktop.test.ts b/test/integration/provider-scenarios/macos-desktop.test.ts index 59f14720f..67cc5d232 100644 --- a/test/integration/provider-scenarios/macos-desktop.test.ts +++ b/test/integration/provider-scenarios/macos-desktop.test.ts @@ -11,7 +11,7 @@ import { createProviderTranscript } from './transcript.ts'; import type { AppleRunnerPrepareResult, AppleRunnerProvider, -} from '../../../src/platforms/ios/runner-provider.ts'; +} from '../../../src/platforms/apple/core/runner/runner-provider.ts'; test('Provider-backed integration prepare uses the Apple runner lifecycle provider', async () => { const lifecycleCalls: string[] = []; diff --git a/test/integration/provider-scenarios/macos-world.ts b/test/integration/provider-scenarios/macos-world.ts index cae056588..4a6a86fac 100644 --- a/test/integration/provider-scenarios/macos-world.ts +++ b/test/integration/provider-scenarios/macos-world.ts @@ -1,5 +1,5 @@ import fs from 'node:fs'; -import type { AppleRunnerProvider } from '../../../src/platforms/ios/runner-provider.ts'; +import type { AppleRunnerProvider } from '../../../src/platforms/apple/core/runner/runner-provider.ts'; import { PROVIDER_SCENARIO_MACOS } from './fixtures.ts'; import { createProviderScenarioHarness, type ProviderScenarioHarness } from './harness.ts'; import { createRecordingAppleToolProvider, type FlatToolCall } from './providers.ts'; diff --git a/test/integration/provider-scenarios/providers.ts b/test/integration/provider-scenarios/providers.ts index 55a2e0d37..30a80f570 100644 --- a/test/integration/provider-scenarios/providers.ts +++ b/test/integration/provider-scenarios/providers.ts @@ -1,11 +1,11 @@ -import type { AppleRunnerProvider } from '../../../src/platforms/ios/runner-provider.ts'; -import type { RunnerCommand } from '../../../src/platforms/ios/runner-contract.ts'; +import type { AppleRunnerProvider } from '../../../src/platforms/apple/core/runner/runner-provider.ts'; +import type { RunnerCommand } from '../../../src/platforms/apple/core/runner/runner-contract.ts'; import type { AppleMacOsHostProvider, ApplePlistProvider, AppleToolProvider, AppleToolSubcommandExecutor, -} from '../../../src/platforms/ios/tool-provider.ts'; +} from '../../../src/platforms/apple/core/tool-provider.ts'; import type { ExecResult } from '../../../src/utils/exec.ts'; import type { ProviderScenarioTranscript } from './transcript.ts';