mirror of
https://github.com/callstack/agent-device.git
synced 2026-09-14 20:06:34 +08:00
refactor: consolidate Apple platform internals (#968)
This commit is contained in:
committed by
GitHub
parent
f4882bc706
commit
26ac865c63
@@ -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"],
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -82,6 +82,31 @@ int main(int argc, const char *argv[]) {
|
||||
|
||||
@end
|
||||
|
||||
#if defined(TARGET_OS_VISION) && TARGET_OS_VISION
|
||||
@interface AgentDeviceRunnerSceneDelegate : UIResponder <UIWindowSceneDelegate>
|
||||
@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 <UIApplicationDelegate>
|
||||
@property(nonatomic, strong) UIWindow *window;
|
||||
@end
|
||||
@@ -93,13 +118,33 @@ int main(int argc, const char *argv[]) {
|
||||
(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[]) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
+3
-1
@@ -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/**'",
|
||||
|
||||
@@ -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.
|
||||
@@ -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<P>` (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)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@@ -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)
|
||||
## Step (c) — Apple filesystem consolidation ✅ shipped in #968
|
||||
|
||||
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.
|
||||
This step retired the stale standalone Apple plan by landing the low-risk relocation it described:
|
||||
|
||||
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.
|
||||
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
|
||||
|
||||
@@ -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"
|
||||
;;
|
||||
*)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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 <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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
* `<state-dir>/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 <UDID> [--scenario <path>] [--artifacts-dir <dir>] \
|
||||
* [--prepare-timeout-ms <ms>] [--state-dir <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 <UDID> Simulator UDID to drive (required for a real run).
|
||||
--scenario <path> Replay scenario (default: ${DEFAULT_SCENARIO}).
|
||||
--artifacts-dir <dir> Where to write the replay + observed-count artifacts.
|
||||
--state-dir <dir> Reuse an existing daemon state dir instead of a temp one.
|
||||
--prepare-timeout-ms <n> 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 <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());
|
||||
@@ -9,7 +9,7 @@ const [platform, derivedPath, destination] = args;
|
||||
|
||||
if (!platform || !derivedPath || !destination) {
|
||||
console.error(
|
||||
'Usage: write-xcuitest-cache-metadata.mjs <ios|macos|tvos> <derived> <destination>',
|
||||
'Usage: write-xcuitest-cache-metadata.mjs <ios|macos|tvos|visionos> <derived> <destination>',
|
||||
);
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -5,8 +5,9 @@ const { mockRunIosRunnerCommand } = vi.hoisted(() => ({
|
||||
mockRunIosRunnerCommand: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../platforms/ios/runner-client.ts', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('../../platforms/ios/runner-client.ts')>();
|
||||
vi.mock('../../platforms/apple/core/runner/runner-client.ts', async (importOriginal) => {
|
||||
const actual =
|
||||
await importOriginal<typeof import('../../platforms/apple/core/runner/runner-client.ts')>();
|
||||
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<typeof import('../../platforms/ios/macos-helper.ts')>();
|
||||
vi.mock('../../platforms/apple/os/macos/helper.ts', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('../../platforms/apple/os/macos/helper.ts')>();
|
||||
return {
|
||||
...actual,
|
||||
runMacOsPressAction: vi.fn(async () => ({})),
|
||||
|
||||
@@ -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<typeof import('../../platforms/ios/runner-client.ts')>();
|
||||
vi.mock('../../platforms/apple/core/runner/runner-client.ts', async (importOriginal) => {
|
||||
const actual =
|
||||
await importOriginal<typeof import('../../platforms/apple/core/runner/runner-client.ts')>();
|
||||
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';
|
||||
|
||||
|
||||
@@ -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<typeof import('../../platforms/ios/apps.ts')>();
|
||||
vi.mock('../../platforms/apple/core/apps.ts', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('../../platforms/apple/core/apps.ts')>();
|
||||
return {
|
||||
...actual,
|
||||
openIosApp: vi.fn(async () => {}),
|
||||
|
||||
@@ -6,8 +6,8 @@ const { mockFindBootableIosSimulator, mockListAppleDevices } = vi.hoisted(() =>
|
||||
mockListAppleDevices: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../platforms/ios/devices.ts', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('../../platforms/ios/devices.ts')>();
|
||||
vi.mock('../../platforms/apple/core/devices.ts', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('../../platforms/apple/core/devices.ts')>();
|
||||
return {
|
||||
...actual,
|
||||
findBootableIosSimulator: mockFindBootableIosSimulator,
|
||||
|
||||
@@ -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<typeof import('../../platforms/ios/runner-client.ts')>();
|
||||
vi.mock('../../platforms/apple/core/runner/runner-client.ts', async (importOriginal) => {
|
||||
const actual =
|
||||
await importOriginal<typeof import('../../platforms/apple/core/runner/runner-client.ts')>();
|
||||
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';
|
||||
|
||||
|
||||
@@ -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<Record<string, unknown>> {
|
||||
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<string, unknown> | 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,
|
||||
{
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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([
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<typeof import('../../platforms/ios/runner-client.ts')>();
|
||||
vi.mock('../../platforms/apple/core/runner/runner-client.ts', async (importOriginal) => {
|
||||
const actual =
|
||||
await importOriginal<typeof import('../../platforms/apple/core/runner/runner-client.ts')>();
|
||||
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'));
|
||||
|
||||
|
||||
@@ -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<typeof import('../../platforms/ios/runner-client.ts')>();
|
||||
vi.mock('../../platforms/apple/core/runner/runner-client.ts', async (importOriginal) => {
|
||||
const actual =
|
||||
await importOriginal<typeof import('../../platforms/apple/core/runner/runner-client.ts')>();
|
||||
return { ...actual, stopIosRunnerSession: vi.fn(async () => {}) };
|
||||
});
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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<typeof import('../../platforms/ios/runner-client.ts')>();
|
||||
vi.mock('../../platforms/apple/core/runner/runner-client.ts', async (importOriginal) => {
|
||||
const actual =
|
||||
await importOriginal<typeof import('../../platforms/apple/core/runner/runner-client.ts')>();
|
||||
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 () => {
|
||||
|
||||
@@ -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<typeof import('../../platforms/ios/runner-client.ts')>();
|
||||
vi.mock('../../platforms/apple/core/runner/runner-client.ts', async (importOriginal) => {
|
||||
const actual =
|
||||
await importOriginal<typeof import('../../platforms/apple/core/runner/runner-client.ts')>();
|
||||
return { ...actual, stopIosRunnerSession: vi.fn(async () => {}) };
|
||||
});
|
||||
|
||||
|
||||
@@ -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, unknown> = {}): 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);
|
||||
});
|
||||
@@ -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<typeof import('../../platforms/ios/simulator.ts')>();
|
||||
vi.mock('../../platforms/apple/core/simulator.ts', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('../../platforms/apple/core/simulator.ts')>();
|
||||
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);
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -57,8 +57,9 @@ vi.mock('../interaction-snapshot.ts', async (importOriginal) => {
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('../../../platforms/ios/runner-client.ts', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('../../../platforms/ios/runner-client.ts')>();
|
||||
vi.mock('../../../platforms/apple/core/runner/runner-client.ts', async (importOriginal) => {
|
||||
const actual =
|
||||
await importOriginal<typeof import('../../../platforms/apple/core/runner/runner-client.ts')>();
|
||||
return {
|
||||
...actual,
|
||||
runIosRunnerCommand: mockRunIosRunnerCommand,
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -15,8 +15,9 @@ vi.mock('../../../utils/exec.ts', async (importOriginal) => {
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('../../../platforms/ios/runner-client.ts', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('../../../platforms/ios/runner-client.ts')>();
|
||||
vi.mock('../../../platforms/apple/core/runner/runner-client.ts', async (importOriginal) => {
|
||||
const actual =
|
||||
await importOriginal<typeof import('../../../platforms/apple/core/runner/runner-client.ts')>();
|
||||
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,
|
||||
|
||||
@@ -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<typeof import('../../../platforms/ios/simulator.ts')>();
|
||||
vi.mock('../../../platforms/apple/core/simulator.ts', async (importOriginal) => {
|
||||
const actual =
|
||||
await importOriginal<typeof import('../../../platforms/apple/core/simulator.ts')>();
|
||||
return { ...actual, shutdownSimulator: vi.fn() };
|
||||
});
|
||||
vi.mock('../../../utils/exec.ts', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('../../../utils/exec.ts')>();
|
||||
return { ...actual, runCmd: vi.fn() };
|
||||
});
|
||||
vi.mock('../../../platforms/ios/runner-client.ts', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('../../../platforms/ios/runner-client.ts')>();
|
||||
vi.mock('../../../platforms/apple/core/runner/runner-client.ts', async (importOriginal) => {
|
||||
const actual =
|
||||
await importOriginal<typeof import('../../../platforms/apple/core/runner/runner-client.ts')>();
|
||||
return { ...actual, stopIosRunnerSession: vi.fn(async () => {}) };
|
||||
});
|
||||
vi.mock('../../../platforms/ios/perf-xctrace.ts', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('../../../platforms/ios/perf-xctrace.ts')>();
|
||||
vi.mock('../../../platforms/apple/core/perf-xctrace.ts', async (importOriginal) => {
|
||||
const actual =
|
||||
await importOriginal<typeof import('../../../platforms/apple/core/perf-xctrace.ts')>();
|
||||
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<typeof import('../../../platforms/android/snapshot-helper.ts')>();
|
||||
return { ...actual, stopAndroidSnapshotHelperSessionForDevice: vi.fn(async () => {}) };
|
||||
});
|
||||
vi.mock('../../../platforms/ios/macos-helper.ts', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('../../../platforms/ios/macos-helper.ts')>();
|
||||
vi.mock('../../../platforms/apple/os/macos/helper.ts', async (importOriginal) => {
|
||||
const actual =
|
||||
await importOriginal<typeof import('../../../platforms/apple/os/macos/helper.ts')>();
|
||||
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';
|
||||
|
||||
@@ -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<typeof import('../../../platforms/ios/perf-xctrace.ts')>();
|
||||
vi.mock('../../../platforms/apple/core/perf-xctrace.ts', async (importOriginal) => {
|
||||
const actual =
|
||||
await importOriginal<typeof import('../../../platforms/apple/core/perf-xctrace.ts')>();
|
||||
return {
|
||||
...actual,
|
||||
startAppleXctracePerfCapture: applePerfMocks.startAppleXctracePerfCapture,
|
||||
|
||||
@@ -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<typeof import('../../../platforms/ios/runner-client.ts')>();
|
||||
vi.mock('../../../platforms/apple/core/runner/runner-client.ts', async (importOriginal) => {
|
||||
const actual =
|
||||
await importOriginal<typeof import('../../../platforms/apple/core/runner/runner-client.ts')>();
|
||||
return {
|
||||
...actual,
|
||||
prewarmIosRunnerSession: vi.fn(),
|
||||
stopIosRunnerSession: vi.fn(async () => {}),
|
||||
};
|
||||
});
|
||||
vi.mock('../../../platforms/ios/apps.ts', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('../../../platforms/ios/apps.ts')>();
|
||||
vi.mock('../../../platforms/apple/core/apps.ts', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('../../../platforms/apple/core/apps.ts')>();
|
||||
return {
|
||||
...actual,
|
||||
resolveIosApp: vi.fn(async () => 'com.example.demo'),
|
||||
|
||||
@@ -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<typeof import('../../../platforms/ios/runner-client.ts')>();
|
||||
vi.mock('../../../platforms/apple/core/runner/runner-client.ts', async (importOriginal) => {
|
||||
const actual =
|
||||
await importOriginal<typeof import('../../../platforms/apple/core/runner/runner-client.ts')>();
|
||||
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<typeof import('../../../platforms/ios/macos-helper.ts')>();
|
||||
vi.mock('../../../platforms/apple/os/macos/helper.ts', async (importOriginal) => {
|
||||
const actual =
|
||||
await importOriginal<typeof import('../../../platforms/apple/os/macos/helper.ts')>();
|
||||
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<typeof import('../session-open-target.ts')>();
|
||||
return { ...actual, resolveAndroidPackageForOpen: vi.fn(async () => undefined) };
|
||||
});
|
||||
vi.mock('../../../platforms/ios/simulator.ts', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('../../../platforms/ios/simulator.ts')>();
|
||||
vi.mock('../../../platforms/apple/core/simulator.ts', async (importOriginal) => {
|
||||
const actual =
|
||||
await importOriginal<typeof import('../../../platforms/apple/core/simulator.ts')>();
|
||||
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<typeof import('../../../platforms/ios/devices.ts')>();
|
||||
vi.mock('../../../platforms/apple/core/devices.ts', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('../../../platforms/apple/core/devices.ts')>();
|
||||
return { ...actual, listAppleDevices: vi.fn(async () => []) };
|
||||
});
|
||||
vi.mock('../../../platforms/ios/apps.ts', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('../../../platforms/ios/apps.ts')>();
|
||||
vi.mock('../../../platforms/apple/core/apps.ts', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('../../../platforms/apple/core/apps.ts')>();
|
||||
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';
|
||||
|
||||
@@ -23,8 +23,9 @@ vi.mock('../../../core/dispatch.ts', async (importOriginal) => {
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('../../../platforms/ios/runner-client.ts', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('../../../platforms/ios/runner-client.ts')>();
|
||||
vi.mock('../../../platforms/apple/core/runner/runner-client.ts', async (importOriginal) => {
|
||||
const actual =
|
||||
await importOriginal<typeof import('../../../platforms/apple/core/runner/runner-client.ts')>();
|
||||
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<typeof import('../../../platforms/ios/apps.ts')>();
|
||||
vi.mock('../../../platforms/apple/core/apps.ts', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('../../../platforms/apple/core/apps.ts')>();
|
||||
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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -31,7 +31,8 @@ async function tryResolveIosSimulatorDeepLinkBundleId(
|
||||
openTarget: string,
|
||||
): Promise<string | undefined> {
|
||||
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<string | undefined> {
|
||||
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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 (`<state-dir>/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<RunnerRoundTripPhase, number>;
|
||||
};
|
||||
|
||||
// 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<string, unknown>;
|
||||
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<string, unknown>;
|
||||
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<string, unknown>;
|
||||
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 };
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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);
|
||||
|
||||
+1
-1
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
+10
-6
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
+41
-3
@@ -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<RunnerApplePlatformName, RunnerPlatformPr
|
||||
},
|
||||
},
|
||||
},
|
||||
visionOS: {
|
||||
sdkName: {
|
||||
simulator: 'xrsimulator',
|
||||
device: 'xros',
|
||||
},
|
||||
derivedBaseName: {
|
||||
simulator: 'visionos-simulator',
|
||||
device: 'visionos-device',
|
||||
},
|
||||
xctestrunHints: {
|
||||
simulator: {
|
||||
preferred: ['xrsimulator'],
|
||||
disallowed: [
|
||||
'xros',
|
||||
'iphoneos',
|
||||
'iphonesimulator',
|
||||
'appletvos',
|
||||
'appletvsimulator',
|
||||
'macos',
|
||||
],
|
||||
},
|
||||
device: {
|
||||
preferred: ['xros'],
|
||||
disallowed: [
|
||||
'xrsimulator',
|
||||
'iphoneos',
|
||||
'iphonesimulator',
|
||||
'appletvos',
|
||||
'appletvsimulator',
|
||||
'macos',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export function resolveRunnerPlatformName(device: DeviceInfo): RunnerApplePlatformName {
|
||||
@@ -2,35 +2,38 @@ 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 { emitDiagnostic } from '../../utils/diagnostics.ts';
|
||||
import type { AppsFilter } from '../../contracts/app-inventory.ts';
|
||||
import type { DeviceInfo } from '../../../kernel/device.ts';
|
||||
import { AppError } from '../../../kernel/errors.ts';
|
||||
import { emitDiagnostic } from '../../../utils/diagnostics.ts';
|
||||
import type { AppsFilter } from '../../../contracts/app-inventory.ts';
|
||||
import {
|
||||
LAUNCH_CONSOLE_DIRECT_APP_ONLY_MESSAGE,
|
||||
LAUNCH_CONSOLE_IOS_SIMULATOR_ONLY_MESSAGE,
|
||||
} from '../../core/launch-console.ts';
|
||||
import { requireLocationCoordinates } from '../../utils/location-coordinates.ts';
|
||||
import { resolveIosSimulatorDeviceSetPath } from '../../utils/device-isolation.ts';
|
||||
import { Deadline, retryWithPolicy } from '../../utils/retry.ts';
|
||||
} from '../../../core/launch-console.ts';
|
||||
import { requireLocationCoordinates } from '../../../utils/location-coordinates.ts';
|
||||
import { resolveIosSimulatorDeviceSetPath } from '../../../utils/device-isolation.ts';
|
||||
import { Deadline, retryWithPolicy } from '../../../utils/retry.ts';
|
||||
import {
|
||||
isDeepLinkTarget,
|
||||
isWebUrl,
|
||||
resolveIosDeviceDeepLinkBundleId,
|
||||
} from '../../core/open-target.ts';
|
||||
import { getUnsupportedMacOsSettingMessage } from '../../core/settings-contract.ts';
|
||||
} from '../../../core/open-target.ts';
|
||||
import { getUnsupportedMacOsSettingMessage } from '../../../core/settings-contract.ts';
|
||||
import {
|
||||
parsePermissionAction,
|
||||
parsePermissionTarget,
|
||||
type SettingOptions,
|
||||
} from '../permission-utils.ts';
|
||||
import { parseAppearanceAction } from '../appearance.ts';
|
||||
import { parseSettingState } from '../setting-state.ts';
|
||||
import { createAppResolutionCache, type AppResolutionCacheScope } from '../app-resolution-cache.ts';
|
||||
} from '../../permission-utils.ts';
|
||||
import { parseAppearanceAction } from '../../appearance.ts';
|
||||
import { parseSettingState } from '../../setting-state.ts';
|
||||
import {
|
||||
createAppResolutionCache,
|
||||
type AppResolutionCacheScope,
|
||||
} from '../../app-resolution-cache.ts';
|
||||
import {
|
||||
summarizeCommandAttemptFailures,
|
||||
type CommandAttemptFailure,
|
||||
} from '../command-attempts.ts';
|
||||
} from '../../command-attempts.ts';
|
||||
|
||||
import {
|
||||
IOS_APP_LAUNCH_TIMEOUT_MS,
|
||||
@@ -64,8 +67,8 @@ import {
|
||||
resolveMacOsApp,
|
||||
setMacOsAppearance,
|
||||
writeMacOsClipboardText,
|
||||
} from './macos-apps.ts';
|
||||
import { runMacOsPermissionAction, type MacOsPermissionTarget } from './macos-helper.ts';
|
||||
} from '../os/macos/apps.ts';
|
||||
import { runMacOsPermissionAction, type MacOsPermissionTarget } from '../os/macos/helper.ts';
|
||||
import {
|
||||
invalidateSimulatorStatusBarOverrideCache,
|
||||
rememberClearedStatusBarOverrides,
|
||||
@@ -4,8 +4,8 @@ import { readAppleCrashArtifact } from './debug-symbols/crash-artifact.ts';
|
||||
import { matchImagesToDsyms, readDsymPaths, readDsymSlices } from './debug-symbols/dsym.ts';
|
||||
import { summarizeCrashArtifact } from './debug-symbols/report.ts';
|
||||
import { resolveAppleTools, symbolicateAddresses } from './debug-symbols/symbolication.ts';
|
||||
import type { DebugSymbolsOptions, DebugSymbolsResult } from '../../contracts/debug-symbols.ts';
|
||||
import { AppError } from '../../kernel/errors.ts';
|
||||
import type { DebugSymbolsOptions, DebugSymbolsResult } from '../../../contracts/debug-symbols.ts';
|
||||
import { AppError } from '../../../kernel/errors.ts';
|
||||
|
||||
export type {
|
||||
DebugSymbolsCrashFrame,
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import path from 'node:path';
|
||||
import { isRecord } from '../../../utils/parsing.ts';
|
||||
import { isRecord } from '../../../../utils/parsing.ts';
|
||||
import type {
|
||||
AppleImage,
|
||||
CrashArtifact,
|
||||
+2
-2
@@ -2,8 +2,8 @@ import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import type { AppleImage, DsymMatch, DsymSlice } from './types.ts';
|
||||
import { normalizeUuid, unique } from './utils.ts';
|
||||
import { runCmd } from '../../../utils/exec.ts';
|
||||
import { AppError } from '../../../kernel/errors.ts';
|
||||
import { runCmd } from '../../../../utils/exec.ts';
|
||||
import { AppError } from '../../../../kernel/errors.ts';
|
||||
|
||||
const MAX_SEARCH_ENTRIES = 10_000;
|
||||
const MAX_DSYM_CANDIDATES = 200;
|
||||
+1
-1
@@ -7,7 +7,7 @@ import type {
|
||||
SymbolicatedAddress,
|
||||
TextFrameMatch,
|
||||
} from './types.ts';
|
||||
import { isRecord } from '../../../utils/parsing.ts';
|
||||
import { isRecord } from '../../../../utils/parsing.ts';
|
||||
import {
|
||||
addressKey,
|
||||
compactJoin,
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
import type { DsymMatch, SymbolicatedAddress, SymbolicationGroup } from './types.ts';
|
||||
import { addressKey, hex, unique } from './utils.ts';
|
||||
import { runCmd } from '../../../utils/exec.ts';
|
||||
import { AppError } from '../../../kernel/errors.ts';
|
||||
import { runCmd } from '../../../../utils/exec.ts';
|
||||
import { AppError } from '../../../../kernel/errors.ts';
|
||||
|
||||
export async function symbolicateAddresses(
|
||||
addresses: SymbolicatedAddress[],
|
||||
+1
-1
@@ -4,7 +4,7 @@ export type {
|
||||
DebugSymbolsImage,
|
||||
DebugSymbolsOptions,
|
||||
DebugSymbolsResult,
|
||||
} from '../../../contracts/debug-symbols.ts';
|
||||
} from '../../../../contracts/debug-symbols.ts';
|
||||
|
||||
export type AppleImage = {
|
||||
index?: number;
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
import { AppError } from '../../../kernel/errors.ts';
|
||||
import { isRecord } from '../../../utils/parsing.ts';
|
||||
import { AppError } from '../../../../kernel/errors.ts';
|
||||
import { isRecord } from '../../../../utils/parsing.ts';
|
||||
|
||||
const UUID_RE = /^[0-9a-fA-F-]{32,36}$/;
|
||||
|
||||
@@ -2,8 +2,8 @@ import { promises as fs } from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
|
||||
import type { DeviceInfo } from '../../kernel/device.ts';
|
||||
import { AppError } from '../../kernel/errors.ts';
|
||||
import type { DeviceInfo } from '../../../kernel/device.ts';
|
||||
import { AppError } from '../../../kernel/errors.ts';
|
||||
|
||||
import { IOS_DEVICECTL_TIMEOUT_MS } from './config.ts';
|
||||
import { runXcrun } from './tool-provider.ts';
|
||||
@@ -1,18 +1,19 @@
|
||||
import { promises as fs } from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { AppError } from '../../kernel/errors.ts';
|
||||
import type { AppleOS, DeviceInfo, DeviceTarget } from '../../kernel/device.ts';
|
||||
import { resolveIosSimulatorDeviceSetPath } from '../../utils/device-isolation.ts';
|
||||
import { buildHostMacDevice } from '../macos/devices.ts';
|
||||
import { AppError } from '../../../kernel/errors.ts';
|
||||
import type { AppleOS, DeviceInfo, DeviceTarget } from '../../../kernel/device.ts';
|
||||
import { resolveIosSimulatorDeviceSetPath } from '../../../utils/device-isolation.ts';
|
||||
import { buildHostMacDevice } from '../os/macos/devices.ts';
|
||||
import { buildSimctlArgs } from './simctl.ts';
|
||||
import { resolveAppleToolProvider, runXcrun } from './tool-provider.ts';
|
||||
|
||||
export { createLocalAppleToolProvider, withAppleToolProvider } from './tool-provider.ts';
|
||||
|
||||
const IOS_DEVICECTL_LIST_TIMEOUT_MS = 8_000;
|
||||
const APPLE_PRODUCT_TYPE_PATTERN = /^(iphone|ipad|ipod|appletv)/i;
|
||||
const APPLE_PRODUCT_TYPE_PATTERN = /^(iphone|ipad|ipod|appletv|realitydevice)/i;
|
||||
const APPLE_IPAD_PATTERN = /ipad/i;
|
||||
const APPLE_VISION_PATTERN = /\b(apple vision|vision pro|xros|visionos|realitydevice)\b/i;
|
||||
const APPLE_MOBILE_LABEL_PATTERN = /\b(iphone|ipad|ipod)\b/i;
|
||||
const APPLE_TV_PRODUCT_TYPE_PATTERN = /^appletv/i;
|
||||
const APPLE_TV_LABEL_HINTS = ['apple tv', 'appletv', 'tvos'] as const;
|
||||
@@ -70,13 +71,22 @@ function isAppleTvPlatform(platform: string): boolean {
|
||||
return platform.includes('tvos');
|
||||
}
|
||||
|
||||
function isAppleVisionPlatform(platform: string): boolean {
|
||||
return platform.includes('xros') || platform.includes('visionos');
|
||||
}
|
||||
|
||||
function resolveAppleTargetFromRuntime(runtime: string): DeviceTarget {
|
||||
return isAppleTvPlatform(resolveAppleRuntime(runtime)) ? 'tv' : 'mobile';
|
||||
}
|
||||
|
||||
function isSupportedAppleRuntime(runtime: string): boolean {
|
||||
const normalized = resolveAppleRuntime(runtime);
|
||||
return normalized.includes('ios') || normalized.includes('tvos');
|
||||
return (
|
||||
normalized.includes('ios') ||
|
||||
normalized.includes('tvos') ||
|
||||
normalized.includes('xros') ||
|
||||
normalized.includes('visionos')
|
||||
);
|
||||
}
|
||||
|
||||
function isAppleTvLabel(value: string): boolean {
|
||||
@@ -90,7 +100,7 @@ function isAppleMobileLabel(value: string): boolean {
|
||||
|
||||
function resolveAppleTargetFromLabel(value: string): DeviceTarget | null {
|
||||
if (isAppleTvLabel(value)) return 'tv';
|
||||
if (isAppleMobileLabel(value)) return 'mobile';
|
||||
if (isAppleMobileLabel(value) || APPLE_VISION_PATTERN.test(value)) return 'mobile';
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -105,6 +115,7 @@ function resolveAppleTargetFromLabel(value: string): DeviceTarget | null {
|
||||
*/
|
||||
function resolveAppleOs(target: DeviceTarget, descriptors: string[]): AppleOS {
|
||||
if (target === 'tv') return 'tvos';
|
||||
if (descriptors.some((descriptor) => 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);
|
||||
+3
-3
@@ -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;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user