From cc1f3161a8b773b975bc5eacee9a2f4014aadc85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Fri, 28 Aug 2026 10:25:36 +0200 Subject: [PATCH 1/3] refactor: move Apple platform family into package --- .fallowrc.json | 111 ++++- docs/adr/0009-apple-platform-consolidation.md | 14 +- fallow-baselines/health.json | 18 +- packages/contracts/src/interactor-types.ts | 4 +- packages/host-kit/src/environment.ts | 30 ++ packages/host-kit/src/filesystem.ts | 1 + packages/platform-apple/package.json | 2 + .../src}/__tests__/alert.test.ts | 2 +- .../platform-apple/src/__tests__/app-error.ts | 31 ++ .../src/__tests__/device-fixtures.ts | 29 ++ .../src/__tests__/doctor.test.ts | 102 +++++ .../src/__tests__}/fake-apple-tool.ts | 60 +-- .../interactor-runner-provider.test.ts | 4 +- .../src}/__tests__/screenshot-backend.test.ts | 0 .../platform-apple/src/__tests__/tmp-dir.ts | 7 + .../src}/__tests__/watchos-sentinel.test.ts | 0 .../platform-apple/src}/alert.ts | 0 .../src}/core/__tests__/app-device-io.test.ts | 0 .../src}/core/__tests__/app-install.test.ts | 0 .../core/__tests__/app-launch-close.test.ts | 2 +- .../__tests__/app-launch-device-open.test.ts | 2 +- .../core/__tests__/app-launch-url.test.ts | 0 .../app-resolution-deep-link.test.ts | 7 +- .../core/__tests__/app-resolution.test.ts | 0 .../core/__tests__/apple-core-stub-helpers.ts | 36 ++ .../src}/core/__tests__/apps.test.ts | 9 +- .../src}/core/__tests__/debug-symbols.test.ts | 2 +- .../src}/core/__tests__/devicectl.test.ts | 0 .../__tests__/install-artifact.fixtures.ts | 0 .../src}/core/__tests__/interactions.test.ts | 2 +- .../core/__tests__/launch-diagnostics.test.ts | 0 .../core/__tests__/macos-helper-test-utils.ts | 2 +- .../core/__tests__/perf-time-profile.test.ts | 0 .../src}/core/__tests__/perf.test.ts | 2 +- .../__tests__/physical-device-control.test.ts | 6 +- .../physical-device-coredevice.test.ts | 0 .../physical-device-screenshot.test.ts | 2 +- .../src}/core/__tests__/plist.test.ts | 2 +- .../__tests__/runner-selector-query.test.ts | 2 +- .../core/__tests__/screenshot-density.test.ts | 2 +- .../__tests__/screenshot-status-bar.test.ts | 2 +- .../src}/core/__tests__/screenshot.test.ts | 2 +- .../core/__tests__/settings-parsing.test.ts | 2 +- .../src}/core/__tests__/simctl.test.ts | 0 .../__tests__/simulator-booted-memo.test.ts | 0 .../src}/core/__tests__/simulator.test.ts | 0 .../src}/core/__tests__/tool-provider.test.ts | 0 .../platform-apple/src}/core/app-device-io.ts | 6 +- .../platform-apple/src}/core/app-filter.ts | 0 .../platform-apple/src}/core/app-info.ts | 0 .../platform-apple/src}/core/app-install.ts | 0 .../platform-apple/src}/core/app-launch.ts | 4 +- .../src/core/app-resolution-error.ts | 5 + .../src}/core/app-resolution.ts | 10 +- .../platform-apple/src}/core/app-settings.ts | 5 +- .../platform-apple/src}/core/apps-simctl.ts | 0 .../platform-apple/src}/core/apps.ts | 7 +- .../platform-apple/src}/core/config.ts | 0 .../platform-apple/src}/core/debug-symbols.ts | 5 +- .../src}/core/debug-symbols/crash-artifact.ts | 0 .../src}/core/debug-symbols/dsym.ts | 2 +- .../src}/core/debug-symbols/report.ts | 4 +- .../src}/core/debug-symbols/symbolication.ts | 0 .../src}/core/debug-symbols/types.ts | 0 .../src}/core/debug-symbols/utils.ts | 0 .../platform-apple/src}/core/devicectl.ts | 22 +- .../src}/core/install-artifact.ts | 6 +- .../src}/core/launch-diagnostics.ts | 0 .../platform-apple/src}/core/perf-frame.ts | 4 +- .../src}/core/perf-time-profile.ts | 0 .../platform-apple/src}/core/perf-xctrace.ts | 14 +- .../platform-apple/src}/core/perf-xml.ts | 0 .../platform-apple/src}/core/perf.ts | 56 +-- .../src}/core/physical-device-apps.ts | 0 .../src}/core/physical-device-constants.ts | 0 .../src}/core/physical-device-control.ts | 5 +- .../src}/core/physical-device-coredevice.ts | 8 +- .../src}/core/physical-device-files.ts | 2 +- .../src}/core/physical-device-screenshot.ts | 7 +- .../platform-apple/src}/core/plist-xml.ts | 0 .../platform-apple/src}/core/plist.ts | 2 +- .../platform-apple/src}/core/runner-client.ts | 7 +- .../platform-apple/src}/core/runner-host.ts | 2 +- .../src}/core/runner-owner-state.ts | 0 .../src}/core/runner-selector-query.ts | 2 +- .../src}/core/screenshot-status-bar.ts | 0 .../platform-apple/src}/core/screenshot.ts | 4 +- .../platform-apple/src}/core/scroll.ts | 0 .../src}/core/settings-parsing.ts | 0 .../platform-apple/src}/core/simctl.ts | 0 .../platform-apple/src}/core/simulator.ts | 0 .../src}/core/tool-diagnostics.ts | 0 .../src}/core/tool-provider-types.ts | 0 .../platform-apple/src}/core/tool-provider.ts | 0 .../platform-apple/src}/doctor.ts | 3 +- packages/platform-apple/src/index.test.ts | 15 + packages/platform-apple/src/index.ts | 430 ++++++++++++++++++ .../platform-apple/src}/interactions.ts | 2 +- .../platform-apple/src}/interactor.ts | 2 +- .../platform-apple/src}/os/macos/apps.test.ts | 0 .../platform-apple/src}/os/macos/apps.ts | 0 .../src}/os/macos/desktop-scroll.ts | 2 +- .../src}/os/macos/helper.test.ts | 0 .../platform-apple/src}/os/macos/helper.ts | 19 +- .../src}/os/macos/host-provider.ts | 6 +- .../src}/os/macos/surface-snapshot.ts | 0 .../platform-apple/src}/os/tvos/remote.ts | 2 +- packages/platform-apple/src/runner/host.ts | 12 +- pnpm-lock.yaml | 6 + .../help-conformance-sample-outputs.test.ts | 4 +- .../help-conformance-sample-producers.ts | 6 +- .../__tests__/test-file-size-ratchet.test.ts | 10 +- scripts/check-affected/device-lanes.test.ts | 10 +- scripts/check-affected/device-lanes.ts | 4 +- scripts/check-affected/model.test.ts | 4 +- scripts/clean-daemon.ts | 8 +- scripts/gate/routing.test.ts | 4 +- scripts/help-conformance-sample-outputs.mjs | 2 +- scripts/layering/check.ts | 4 + scripts/layering/package-boundaries.test.ts | 3 + .../layering/platform-package-policy.test.ts | 23 +- scripts/layering/platform-package-policy.ts | 116 ++++- scripts/patch-xcuitest-runner-icon.ts | 3 +- scripts/vitest-apple-runner-host-setup.ts | 4 +- .../test-utils/boundary-fault-matrix.ts | 2 +- src/agent-device-client.ts | 3 +- .../device-selection-resolver.test.ts | 2 +- src/core/__tests__/dispatch-resolve.test.ts | 2 +- src/core/device-selection-resolver.ts | 2 +- src/core/interactors/register-builtins.ts | 2 +- .../application-lifecycle-recovery.test.ts | 8 +- src/daemon/__tests__/device-ready.test.ts | 5 +- src/daemon/__tests__/is-runtime.test.ts | 5 +- .../__tests__/offscreen-target-probe.test.ts | 99 ++-- .../request-platform-providers.test.ts | 5 +- .../request-recording-health.test.ts | 6 +- .../__tests__/request-router-cost.test.ts | 5 +- .../request-router-lock-policy.test.ts | 5 +- .../__tests__/request-router-open.test.ts | 12 +- .../request-router-recording-health.test.ts | 7 +- .../request-router-replay-scope.test.ts | 10 +- .../request-router-response-level.test.ts | 5 +- .../request-router-session-address.test.ts | 5 +- .../request-router-typed-error.test.ts | 5 +- src/daemon/__tests__/selector-runtime.test.ts | 39 +- .../__tests__/snapshot-runtime-fixture.ts | 2 +- ...nteraction-settle-private-ax-route.test.ts | 38 +- .../interaction-target-evidence.test.ts | 5 +- ...nteraction-touch-android-freshness.test.ts | 5 +- ...nteraction-touch-android-readiness.test.ts | 5 +- ...ction-touch-direct-ios-eligibility.test.ts | 5 +- .../__tests__/interaction-touch-fill.test.ts | 38 +- .../interaction-touch-payload.test.ts | 5 +- .../interaction-touch-press-admission.test.ts | 5 +- .../__tests__/interaction-touch-press.test.ts | 5 +- .../interaction-touch-response.test.ts | 5 +- .../interaction-touch-runtime.test.ts | 38 +- .../__tests__/interaction-touch.test.ts | 5 +- .../handlers/__tests__/interaction.test.ts | 5 +- .../session-close-shutdown.fixtures.ts | 36 +- .../__tests__/session-device-utils.test.ts | 12 +- .../__tests__/session-doctor-warmup.test.ts | 48 +- .../session-open-execution-runtime.test.ts | 11 +- .../__tests__/session-open-runtime.test.ts | 11 +- ...ion-replay-repair-record-exclusion.test.ts | 22 +- .../session-replay-repair-transaction.test.ts | 22 +- .../__tests__/session-test-harness.ts | 31 +- .../handlers/__tests__/snapshot-alert.test.ts | 119 +++++ .../__tests__/snapshot-handler.test.ts | 108 +---- .../snapshot-session-cleanup.test.ts | 10 +- src/daemon/ios-app-session-hint.test.ts | 2 +- src/platform-runtime-app-inventory-host.ts | 2 +- ...latform-runtime-apple-application-tools.ts | 24 +- ...tform-runtime-apple-automation-keep-hot.ts | 7 +- ...-runtime-apple-deployment-executor.test.ts | 2 +- ...tform-runtime-apple-deployment-executor.ts | 8 +- ...atform-runtime-apple-physical-readiness.ts | 6 +- src/platform-runtime-apple-resources.ts | 5 +- src/platform-runtime-apple-runner-owner.ts | 5 +- src/platform-runtime-apple-tool-host.test.ts | 2 +- src/platform-runtime-apple-tool-host.ts | 10 +- src/platform-runtime-audio-probe-host.ts | 3 +- ...tform-runtime-daemon-owner-cleanup.test.ts | 8 +- src/platform-runtime-daemon-owner-cleanup.ts | 5 +- src/platform-runtime-device-ready.ts | 8 +- src/platform-runtime-host-diagnostics.ts | 4 +- src/platform-runtime-host.test.ts | 2 +- src/platform-runtime-host.ts | 4 +- src/platform-runtime-open-target.ts | 7 +- src/platform-runtime-perf-capture-host.ts | 4 +- src/platform-runtime-perf-host.ts | 11 +- src/platform-runtime-resource-cleanup.ts | 6 +- src/platform-runtime-runtime-hints.ts | 6 +- ...screen-recording-apple-runner-host.test.ts | 2 +- ...time-screen-recording-apple-runner-host.ts | 8 +- ...n-recording-apple-runner-transport.test.ts | 2 +- ...screen-recording-apple-runner-transport.ts | 16 +- ...untime-screen-recording-apple-transport.ts | 4 +- src/platform-runtime/request-providers.ts | 4 +- .../__tests__/install-source.test.ts | 5 +- .../core/__tests__/apple-core-stub-helpers.ts | 101 ---- src/platforms/apple/plugin.ts | 23 - src/sdk/limrun-runtime-dependencies.test.ts | 24 +- src/sdk/limrun-runtime-dependencies.ts | 4 +- src/utils/__tests__/interactors.test.ts | 72 +-- .../ios-simulator-e2e/coverage-manifest.ts | 2 +- .../macos-e2e/coverage-manifest.ts | 2 +- .../ios-alert-settings.test.ts | 4 +- .../provider-ios-runner-transport.test.ts | 2 +- .../provider-scenarios/providers.ts | 2 +- 210 files changed, 1677 insertions(+), 963 deletions(-) create mode 100644 packages/host-kit/src/environment.ts create mode 100644 packages/host-kit/src/filesystem.ts rename {src/platforms/apple => packages/platform-apple/src}/__tests__/alert.test.ts (98%) create mode 100644 packages/platform-apple/src/__tests__/app-error.ts create mode 100644 packages/platform-apple/src/__tests__/device-fixtures.ts create mode 100644 packages/platform-apple/src/__tests__/doctor.test.ts rename {src/__tests__/test-utils => packages/platform-apple/src/__tests__}/fake-apple-tool.ts (57%) rename {src/platforms/apple => packages/platform-apple/src}/__tests__/interactor-runner-provider.test.ts (98%) rename {src/platforms/apple => packages/platform-apple/src}/__tests__/screenshot-backend.test.ts (100%) create mode 100644 packages/platform-apple/src/__tests__/tmp-dir.ts rename {src/platforms/apple => packages/platform-apple/src}/__tests__/watchos-sentinel.test.ts (100%) rename {src/platforms/apple => packages/platform-apple/src}/alert.ts (100%) rename {src/platforms/apple => packages/platform-apple/src}/core/__tests__/app-device-io.test.ts (100%) rename {src/platforms/apple => packages/platform-apple/src}/core/__tests__/app-install.test.ts (100%) rename {src/platforms/apple => packages/platform-apple/src}/core/__tests__/app-launch-close.test.ts (98%) rename {src/platforms/apple => packages/platform-apple/src}/core/__tests__/app-launch-device-open.test.ts (95%) rename {src/platforms/apple => packages/platform-apple/src}/core/__tests__/app-launch-url.test.ts (100%) rename {src/platforms/apple => packages/platform-apple/src}/core/__tests__/app-resolution-deep-link.test.ts (96%) rename {src/platforms/apple => packages/platform-apple/src}/core/__tests__/app-resolution.test.ts (100%) create mode 100644 packages/platform-apple/src/core/__tests__/apple-core-stub-helpers.ts rename {src/platforms/apple => packages/platform-apple/src}/core/__tests__/apps.test.ts (99%) rename {src/platforms/apple => packages/platform-apple/src}/core/__tests__/debug-symbols.test.ts (99%) rename {src/platforms/apple => packages/platform-apple/src}/core/__tests__/devicectl.test.ts (100%) rename {src/platforms/apple => packages/platform-apple/src}/core/__tests__/install-artifact.fixtures.ts (100%) rename {src/platforms/apple => packages/platform-apple/src}/core/__tests__/interactions.test.ts (99%) rename {src/platforms/apple => packages/platform-apple/src}/core/__tests__/launch-diagnostics.test.ts (100%) rename {src/platforms/apple => packages/platform-apple/src}/core/__tests__/macos-helper-test-utils.ts (92%) rename {src/platforms/apple => packages/platform-apple/src}/core/__tests__/perf-time-profile.test.ts (100%) rename {src/platforms/apple => packages/platform-apple/src}/core/__tests__/perf.test.ts (99%) rename {src/platforms/apple => packages/platform-apple/src}/core/__tests__/physical-device-control.test.ts (94%) rename {src/platforms/apple => packages/platform-apple/src}/core/__tests__/physical-device-coredevice.test.ts (100%) rename {src/platforms/apple => packages/platform-apple/src}/core/__tests__/physical-device-screenshot.test.ts (95%) rename {src/platforms/apple => packages/platform-apple/src}/core/__tests__/plist.test.ts (97%) rename {src/platforms/apple => packages/platform-apple/src}/core/__tests__/runner-selector-query.test.ts (91%) rename {src/platforms/apple => packages/platform-apple/src}/core/__tests__/screenshot-density.test.ts (96%) rename {src/platforms/apple => packages/platform-apple/src}/core/__tests__/screenshot-status-bar.test.ts (97%) rename {src/platforms/apple => packages/platform-apple/src}/core/__tests__/screenshot.test.ts (99%) rename {src/platforms/apple => packages/platform-apple/src}/core/__tests__/settings-parsing.test.ts (95%) rename {src/platforms/apple => packages/platform-apple/src}/core/__tests__/simctl.test.ts (100%) rename {src/platforms/apple => packages/platform-apple/src}/core/__tests__/simulator-booted-memo.test.ts (100%) rename {src/platforms/apple => packages/platform-apple/src}/core/__tests__/simulator.test.ts (100%) rename {src/platforms/apple => packages/platform-apple/src}/core/__tests__/tool-provider.test.ts (100%) rename {src/platforms/apple => packages/platform-apple/src}/core/app-device-io.ts (89%) rename {src/platforms/apple => packages/platform-apple/src}/core/app-filter.ts (100%) rename {src/platforms/apple => packages/platform-apple/src}/core/app-info.ts (100%) rename {src/platforms/apple => packages/platform-apple/src}/core/app-install.ts (100%) rename {src/platforms/apple => packages/platform-apple/src}/core/app-launch.ts (98%) create mode 100644 packages/platform-apple/src/core/app-resolution-error.ts rename {src/platforms/apple => packages/platform-apple/src}/core/app-resolution.ts (96%) rename {src/platforms/apple => packages/platform-apple/src}/core/app-settings.ts (98%) rename {src/platforms/apple => packages/platform-apple/src}/core/apps-simctl.ts (100%) rename {src/platforms/apple => packages/platform-apple/src}/core/apps.ts (70%) rename {src/platforms/apple => packages/platform-apple/src}/core/config.ts (100%) rename {src/platforms/apple => packages/platform-apple/src}/core/debug-symbols.ts (96%) rename {src/platforms/apple => packages/platform-apple/src}/core/debug-symbols/crash-artifact.ts (100%) rename {src/platforms/apple => packages/platform-apple/src}/core/debug-symbols/dsym.ts (99%) rename {src/platforms/apple => packages/platform-apple/src}/core/debug-symbols/report.ts (98%) rename {src/platforms/apple => packages/platform-apple/src}/core/debug-symbols/symbolication.ts (100%) rename {src/platforms/apple => packages/platform-apple/src}/core/debug-symbols/types.ts (100%) rename {src/platforms/apple => packages/platform-apple/src}/core/debug-symbols/utils.ts (100%) rename {src/platforms/apple => packages/platform-apple/src}/core/devicectl.ts (94%) rename {src/platforms/apple => packages/platform-apple/src}/core/install-artifact.ts (97%) rename {src/platforms/apple => packages/platform-apple/src}/core/launch-diagnostics.ts (100%) rename {src/platforms/apple => packages/platform-apple/src}/core/perf-frame.ts (98%) rename {src/platforms/apple => packages/platform-apple/src}/core/perf-time-profile.ts (100%) rename {src/platforms/apple => packages/platform-apple/src}/core/perf-xctrace.ts (96%) rename {src/platforms/apple => packages/platform-apple/src}/core/perf-xml.ts (100%) rename {src/platforms/apple => packages/platform-apple/src}/core/perf.ts (96%) rename {src/platforms/apple => packages/platform-apple/src}/core/physical-device-apps.ts (100%) rename {src/platforms/apple => packages/platform-apple/src}/core/physical-device-constants.ts (100%) rename {src/platforms/apple => packages/platform-apple/src}/core/physical-device-control.ts (98%) rename {src/platforms/apple => packages/platform-apple/src}/core/physical-device-coredevice.ts (96%) rename {src/platforms/apple => packages/platform-apple/src}/core/physical-device-files.ts (95%) rename {src/platforms/apple => packages/platform-apple/src}/core/physical-device-screenshot.ts (96%) rename {src/platforms/apple => packages/platform-apple/src}/core/plist-xml.ts (100%) rename {src/platforms/apple => packages/platform-apple/src}/core/plist.ts (96%) rename {src/platforms/apple => packages/platform-apple/src}/core/runner-client.ts (92%) rename {src/platforms/apple => packages/platform-apple/src}/core/runner-host.ts (97%) rename {src/platforms/apple => packages/platform-apple/src}/core/runner-owner-state.ts (100%) rename {src/platforms/apple => packages/platform-apple/src}/core/runner-selector-query.ts (89%) rename {src/platforms/apple => packages/platform-apple/src}/core/screenshot-status-bar.ts (100%) rename {src/platforms/apple => packages/platform-apple/src}/core/screenshot.ts (99%) rename {src/platforms/apple => packages/platform-apple/src}/core/scroll.ts (100%) rename {src/platforms/apple => packages/platform-apple/src}/core/settings-parsing.ts (100%) rename {src/platforms/apple => packages/platform-apple/src}/core/simctl.ts (100%) rename {src/platforms/apple => packages/platform-apple/src}/core/simulator.ts (100%) rename {src/platforms/apple => packages/platform-apple/src}/core/tool-diagnostics.ts (100%) rename {src/platforms/apple => packages/platform-apple/src}/core/tool-provider-types.ts (100%) rename {src/platforms/apple => packages/platform-apple/src}/core/tool-provider.ts (100%) rename {src/platforms/apple => packages/platform-apple/src}/doctor.ts (95%) create mode 100644 packages/platform-apple/src/index.test.ts rename {src/platforms/apple => packages/platform-apple/src}/interactions.ts (99%) rename {src/platforms/apple => packages/platform-apple/src}/interactor.ts (99%) rename {src/platforms/apple => packages/platform-apple/src}/os/macos/apps.test.ts (100%) rename {src/platforms/apple => packages/platform-apple/src}/os/macos/apps.ts (100%) rename {src/platforms/apple => packages/platform-apple/src}/os/macos/desktop-scroll.ts (94%) rename {src/platforms/apple => packages/platform-apple/src}/os/macos/helper.test.ts (100%) rename {src/platforms/apple => packages/platform-apple/src}/os/macos/helper.ts (95%) rename {src/platforms/apple => packages/platform-apple/src}/os/macos/host-provider.ts (96%) rename {src/platforms/apple => packages/platform-apple/src}/os/macos/surface-snapshot.ts (100%) rename {src/platforms/apple => packages/platform-apple/src}/os/tvos/remote.ts (93%) create mode 100644 src/daemon/handlers/__tests__/snapshot-alert.test.ts delete mode 100644 src/platforms/apple/core/__tests__/apple-core-stub-helpers.ts delete mode 100644 src/platforms/apple/plugin.ts diff --git a/.fallowrc.json b/.fallowrc.json index 6676b651c9..dde54f19ee 100644 --- a/.fallowrc.json +++ b/.fallowrc.json @@ -71,9 +71,118 @@ }, { "comment": "Apple perf sampling metadata is selected through the same lazy platform host; Fallow cannot follow the dynamic property read in src/platform-runtime-perf-host.ts.", - "file": "src/platforms/apple/core/perf.ts", + "file": "packages/platform-apple/src/core/perf.ts", "exports": ["buildAppleFrameSamplingMetadata", "buildAppleMemorySamplingMetadata"] }, + { + "comment": "Apple mechanics are reached through the named lazy façade in packages/platform-apple/src/index.ts. Fallow cannot connect dynamic import member reads to these source exports; keep this list limited to wrappers present in that façade.", + "file": "packages/platform-apple/src/core/app-resolution.ts", + "exports": ["buildAppNotInstalledError", "resolveIosAppAlias"] + }, + { + "comment": "Apple mechanics are reached through the named lazy façade in packages/platform-apple/src/index.ts. Fallow cannot connect dynamic import member reads to these source exports; keep this list limited to wrappers present in that façade.", + "file": "packages/platform-apple/src/core/install-artifact.ts", + "exports": ["prepareIosInstallArtifact", "readIosBundleInfo"] + }, + { + "comment": "Apple mechanics are reached through the named lazy façade in packages/platform-apple/src/index.ts. Fallow cannot connect dynamic import member reads to this source export; keep this list limited to wrappers present in that façade.", + "file": "packages/platform-apple/src/core/perf-xctrace.ts", + "exports": ["cleanupAppleXctracePerfCapture"] + }, + { + "comment": "Apple runner mechanics are reached through the named lazy façade in packages/platform-apple/src/index.ts. Fallow cannot connect dynamic import member reads to these source exports; keep this list limited to wrappers present in that façade or its cleanup composition.", + "file": "packages/platform-apple/src/core/runner-client.ts", + "exports": [ + "notifyIosRunnerAppRelaunched", + "prewarmIosRunnerSession", + "prepareIosRunner", + "resolveRunnerAppBundleId", + "detachIosSimulatorRunnerSessionsForShutdown", + "getRunnerSessionSnapshot", + "scheduleIosRunnerIdleStop", + "stopIosRunnerSession", + "stopAllIosRunnerSessions", + "readStaleRunnerLease", + "verifyLeaseRunnerPidIdentity", + "applyXctestRunnerAppIconFromDerivedPath" + ] + }, + { + "comment": "The Apple root façade owns the lazy runner-owner mutator; Fallow cannot connect its dynamic import member read to the moved source export.", + "file": "packages/platform-apple/src/core/runner-owner-state.ts", + "exports": ["setRunnerLeaseOwnerStateDir"] + }, + { + "comment": "Apple diagnostics are reached through the named lazy façade in packages/platform-apple/src/index.ts. Fallow cannot connect dynamic import member reads to these source exports; keep this list limited to wrappers present in that façade.", + "file": "packages/platform-apple/src/doctor.ts", + "exports": ["appleToolchainCheck", "appleRunnerWarmupCheck"] + }, + { + "comment": "macOS helper mechanics are reached through the named lazy façade in packages/platform-apple/src/index.ts. Fallow cannot connect dynamic import member reads to these source exports; keep this list limited to wrappers present in that façade.", + "file": "packages/platform-apple/src/os/macos/helper.ts", + "exports": ["startMacOsAudioProbeProcess", "resolveFrontmostMacOsApp"] + }, + { + "comment": "The Apple package root is a cold-start-preserving named façade: its production consumers load the value wrappers through dynamic imports. Fallow cannot connect those member reads, so this exact list records only the façade exports currently reported as unreachable; static consumers remain visible to the audit.", + "file": "packages/platform-apple/src/index.ts", + "exports": [ + "openIosApp", + "openIosDevice", + "screenshotIos", + "pushIosNotification", + "readIosClipboardText", + "writeIosClipboardText", + "buildSimctlArgs", + "buildSimctlArgsForDevice", + "resolveAppleToolProvider", + "runAppleToolCommand", + "runXcrun", + "createLocalAppleToolProvider", + "readApplePlistJson", + "hasScopedAppleToolProvider", + "buildAppNotInstalledError", + "setIosSetting", + "readAppleAlert", + "awaitAppleAlert", + "actOnAppleAlert", + "createAppleInteractor", + "resolveAppleBackRunnerCommand", + "shutdownSimulator", + "cleanupAppleXctracePerfCapture", + "sampleAppleMemoryPerf", + "captureAppleMemorySnapshot", + "sampleAppleFramePerf", + "buildAppleFrameSamplingMetadata", + "buildAppleMemorySamplingMetadata", + "buildAppleMemorySnapshotSupport", + "isRetryableIosDeviceTraceRecordFailure", + "prepareAppleTraceRecordRetry", + "resolveAppleExecutable", + "resolveIosDevicePerfTarget", + "readAppleProcessSamples", + "resolveIosDevicePerfHint", + "startAppleXctracePerfCapture", + "stopAppleXctracePerfCapture", + "writeAppleXctracePerfReport", + "runApplePressSeries", + "AppleMacOsHelperProvider", + "AppleToolAvailabilityChecker", + "AppleToolCommandExecutor", + "AppleXcrunToolProvider", + "IosPhysicalDeviceBackend", + "IosPhysicalDeviceControl", + "IosPhysicalDeviceTunnel", + "PreparedIosInstallArtifact", + "AppleMemoryPerfSample", + "AppleMemorySnapshotResult", + "AppleProcessSample", + "AppleXctraceCpuProfileReport", + "AppleXctracePerfMode", + "AppleXctracePerfResult", + "MacOsPermissionTarget", + "MacOsSnapshotNode" + ] + }, { "comment": "Daemon route handlers are reached only through the dynamic `import()` table in request-handler-chain.ts, which --production analysis cannot follow to a consumer.", "file": "src/daemon/handlers/{lease,session,snapshot,react-native,record-trace,find,interaction}.ts", diff --git a/docs/adr/0009-apple-platform-consolidation.md b/docs/adr/0009-apple-platform-consolidation.md index 77770b693d..a77ad70c29 100644 --- a/docs/adr/0009-apple-platform-consolidation.md +++ b/docs/adr/0009-apple-platform-consolidation.md @@ -27,7 +27,8 @@ is therefore costly out of proportion to the actual work, and iPadOS/visionOS/wa Model Apple OSes with an **`AppleOS` discriminant** (`ios | ipados | tvos | watchos | visionos | macos`) under a single `apple` Platform — **not** six `Platform` literals. The OS-agnostic Apple engine consolidates -under `src/platforms/apple/core/`, with genuinely per-OS code in `src/platforms/apple/os//` leaves; +under `packages/platform-apple/src/core/`, with genuinely per-OS code in +`packages/platform-apple/src/os//` leaves; the Apple plugin is the first instance of the platform-plugin registry (the platform axis of the completed perfect-shape refactor; the registry and enforcement paths below are authoritative). Per-OS capability differences become data keyed by `AppleOS`. The additive, @@ -62,11 +63,12 @@ This composes with ADR 0008 (the descriptor's capability facet) and ADR 0003. Implementation status as of 2026-08: - Shipped: the internal `Platform` collapse to `apple`; additive `appleOs` groundwork; the shared - Apple engine under `src/platforms/apple/core`; macOS leaf files under - `src/platforms/apple/os/macos`; a dedicated tvOS leaf under `src/platforms/apple/os/tvos`; - direct internal imports to the Apple modules; the per-`AppleOS` capability table - (`APPLE_OS_CAPABILITIES`, `src/platforms/apple/capabilities.ts`, parity-pinned against the pre-table - predicates); the watchOS **unsupported sentinel** (reserved in the `AppleOS` type and interactor-rejected — + Apple engine under `packages/platform-apple/src/core`; macOS leaf files under + `packages/platform-apple/src/os/macos`; a dedicated tvOS leaf under + `packages/platform-apple/src/os/tvos`; + direct internal imports to the Apple modules; the per-`AppleOS` runtime facts (including + `packages/platform-apple/src/gesture-facts.ts`; the former capability projection was retired after + its predicates moved into request-bound facts); the watchOS **unsupported sentinel** (reserved in the `AppleOS` type and interactor-rejected — XCUITest cannot drive watchOS UI — `isSupportedAppleDeploymentLeaf`, the Apple interactor, and gesture admission reject it — never produced by discovery); and visionOS profile/build/discovery plus simulator-deployment evidence. diff --git a/fallow-baselines/health.json b/fallow-baselines/health.json index 2e324e5f43..102a23fc54 100644 --- a/fallow-baselines/health.json +++ b/fallow-baselines/health.json @@ -351,17 +351,17 @@ "count": 2 } }, - "src/platforms/apple/core/app-resolution.ts": { + "packages/platform-apple/src/core/app-resolution.ts": { "crap_moderate": { "count": 1 } }, - "src/platforms/apple/core/app-settings.ts": { + "packages/platform-apple/src/core/app-settings.ts": { "crap_moderate": { "count": 1 } }, - "src/platforms/apple/core/perf.ts": { + "packages/platform-apple/src/core/perf.ts": { "complexity_high": { "count": 1 }, @@ -369,7 +369,7 @@ "count": 1 } }, - "src/platforms/apple/core/screenshot-status-bar.ts": { + "packages/platform-apple/src/core/screenshot-status-bar.ts": { "complexity_moderate": { "count": 1 }, @@ -377,12 +377,12 @@ "count": 2 } }, - "src/platforms/apple/core/screenshot.ts": { + "packages/platform-apple/src/core/screenshot.ts": { "complexity_moderate": { "count": 1 } }, - "src/platforms/apple/os/macos/helper.ts": { + "packages/platform-apple/src/os/macos/helper.ts": { "crap_high": { "count": 1 } @@ -578,12 +578,12 @@ "src/snapshot/snapshot-lines.ts:high impact", "src/utils/result-serialization.ts:high impact", "src/utils/rect-center.ts:high impact", - "src/platforms/apple/core/app-launch.ts:complexity", + "packages/platform-apple/src/core/app-launch.ts:complexity", "src/utils/parsing.ts:high impact", "src/daemon/daemon-command-registry.ts:high impact", "src/replay/script.ts:complexity", "src/daemon/handlers/session-doctor-output.ts:high impact", - "src/platforms/apple/core/perf-xml.ts:high impact", + "packages/platform-apple/src/core/perf-xml.ts:high impact", "src/daemon/daemon-process.ts:high impact", "src/utils/screenshot-result.ts:high impact", "packages/platform-web/src/json-utils.ts:high impact", @@ -591,7 +591,7 @@ "src/utils/text-surface.ts:high impact", "src/daemon/handlers/session-test-sharding.ts:high impact", "src/daemon/handlers/session-replay-runtime.ts:complexity", - "src/platforms/apple/core/debug-symbols/utils.ts:high impact", + "packages/platform-apple/src/core/debug-symbols/utils.ts:high impact", "packages/platform-linux/src/snapshot.ts:high impact", "src/core/interaction-targeting.ts:high impact", "packages/maestro/src/internal/runtime-targets.ts:high impact", diff --git a/packages/contracts/src/interactor-types.ts b/packages/contracts/src/interactor-types.ts index 27f8dd6378..ad4498a08e 100644 --- a/packages/contracts/src/interactor-types.ts +++ b/packages/contracts/src/interactor-types.ts @@ -87,9 +87,9 @@ export const MAESTRO_NON_HITTABLE_FALLBACK_MESSAGE = 'tapped via non-hittable co * `TextEntryResult.textEntryRoute` (RunnerTests+TextTyping.swift, * RunnerTests+SynthesizedTextEntry.swift). Closed set, because the runner is * its only producer and the boundary that narrows it - * (readTypeTextBackendResult, src/platforms/apple/interactions.ts) drops a + * (readTypeTextBackendResult, packages/platform-apple/src/interactions.ts) drops a * route it cannot name — so a Swift-side addition would silently vanish. The - * route-parity test in src/platforms/apple/core/__tests__/interactions.test.ts + * route-parity test in packages/platform-apple/src/core/__tests__/interactions.test.ts * reads the Swift sources and fails instead. */ export const TEXT_ENTRY_ROUTES = [ diff --git a/packages/host-kit/src/environment.ts b/packages/host-kit/src/environment.ts new file mode 100644 index 0000000000..cc84907cd8 --- /dev/null +++ b/packages/host-kit/src/environment.ts @@ -0,0 +1,30 @@ +import os from 'node:os'; +import process from 'node:process'; + +export function hostHomeDirectory(): string { + return os.homedir(); +} + +export function hostTemporaryDirectory(): string { + return os.tmpdir(); +} + +export function hostWorkingDirectory(): string { + return process.cwd(); +} + +export function hostProcessId(): number { + return process.pid; +} + +export function hostPlatform(): NodeJS.Platform { + return process.platform; +} + +export function readHostEnvironmentVariable(name: string): string | undefined { + return process.env[name]; +} + +export function writeHostStderr(value: string): void { + process.stderr.write(value); +} diff --git a/packages/host-kit/src/filesystem.ts b/packages/host-kit/src/filesystem.ts new file mode 100644 index 0000000000..d24bf4c273 --- /dev/null +++ b/packages/host-kit/src/filesystem.ts @@ -0,0 +1 @@ +export { existsSync, promises as fs } from 'node:fs'; diff --git a/packages/platform-apple/package.json b/packages/platform-apple/package.json index 0083726e18..51eb61ea62 100644 --- a/packages/platform-apple/package.json +++ b/packages/platform-apple/package.json @@ -7,7 +7,9 @@ "dependencies": { "@agent-device/capture-kit": "workspace:*", "@agent-device/contracts": "workspace:*", + "@agent-device/host-kit": "workspace:*", "@agent-device/kernel": "workspace:*", + "@agent-device/provision-kit": "workspace:*", "@agent-device/xml": "workspace:*" }, "exports": { diff --git a/src/platforms/apple/__tests__/alert.test.ts b/packages/platform-apple/src/__tests__/alert.test.ts similarity index 98% rename from src/platforms/apple/__tests__/alert.test.ts rename to packages/platform-apple/src/__tests__/alert.test.ts index 655d4e6437..edfe127ca7 100644 --- a/src/platforms/apple/__tests__/alert.test.ts +++ b/packages/platform-apple/src/__tests__/alert.test.ts @@ -9,7 +9,7 @@ import { ALERT_NOT_FOUND_RUNNER_CODE, } from '@agent-device/contracts/alert-contract'; import { AppError } from '@agent-device/kernel/errors'; -import { IOS_SIMULATOR, MACOS_DEVICE } from '../../../__tests__/test-utils/device-fixtures.ts'; +import { IOS_SIMULATOR, MACOS_DEVICE } from './device-fixtures.ts'; import { runAppleRunnerCommand } from '../core/runner-client.ts'; import { runMacOsAlertAction } from '../os/macos/helper.ts'; import { actOnAppleAlert, awaitAppleAlert, readAppleAlert } from '../alert.ts'; diff --git a/packages/platform-apple/src/__tests__/app-error.ts b/packages/platform-apple/src/__tests__/app-error.ts new file mode 100644 index 0000000000..1a35896125 --- /dev/null +++ b/packages/platform-apple/src/__tests__/app-error.ts @@ -0,0 +1,31 @@ +import assert from 'node:assert/strict'; +import { AppError, normalizeError } from '@agent-device/kernel/errors'; + +type ExpectedAppError = { code: string; message?: RegExp; hint?: string | RegExp }; + +function assertAppError(error: unknown, expected: ExpectedAppError): true { + assert.ok( + error instanceof AppError, + `expected AppError, got ${error?.constructor?.name ?? typeof error}: ${String(error)}`, + ); + assert.equal(error.code, expected.code); + if (expected.message) assert.match(error.message, expected.message); + if (expected.hint !== undefined) { + const { hint } = normalizeError(error); + assert.ok(typeof hint === 'string', `expected a hint on ${error.code}, got ${String(hint)}`); + if (typeof expected.hint === 'string') assert.equal(hint, expected.hint); + else assert.match(hint, expected.hint); + } + return true; +} + +export async function assertRejectsAppError( + run: () => Promise, + expected: ExpectedAppError, +): Promise { + await assert.rejects(run, (error: unknown) => assertAppError(error, expected)); +} + +export function assertThrowsAppError(fn: () => unknown, expected: ExpectedAppError): void { + assert.throws(fn, (error: unknown) => assertAppError(error, expected)); +} diff --git a/packages/platform-apple/src/__tests__/device-fixtures.ts b/packages/platform-apple/src/__tests__/device-fixtures.ts new file mode 100644 index 0000000000..242ccda327 --- /dev/null +++ b/packages/platform-apple/src/__tests__/device-fixtures.ts @@ -0,0 +1,29 @@ +import type { DeviceInfo } from '@agent-device/kernel/device'; + +export const IOS_SIMULATOR: DeviceInfo = { + platform: 'apple', + id: 'sim-1', + name: 'iPhone 17 Pro', + kind: 'simulator', + appleOs: 'ios', + booted: true, +}; + +export const IOS_DEVICE: DeviceInfo = { + platform: 'apple', + id: 'ios-device-1', + name: 'iPhone', + kind: 'device', + appleOs: 'ios', + booted: true, +}; + +export const MACOS_DEVICE: DeviceInfo = { + platform: 'apple', + id: 'host-macos-local', + name: 'Mac', + kind: 'device', + target: 'desktop', + appleOs: 'macos', + booted: true, +}; diff --git a/packages/platform-apple/src/__tests__/doctor.test.ts b/packages/platform-apple/src/__tests__/doctor.test.ts new file mode 100644 index 0000000000..5d8bbbcb45 --- /dev/null +++ b/packages/platform-apple/src/__tests__/doctor.test.ts @@ -0,0 +1,102 @@ +import assert from 'node:assert/strict'; +import { beforeEach, test, vi } from 'vitest'; + +const { mockHasCachedRunnerArtifact, mockPrewarmRunnerCache, mockHostPlatform } = vi.hoisted( + () => ({ + mockHasCachedRunnerArtifact: vi.fn(), + mockPrewarmRunnerCache: vi.fn(), + mockHostPlatform: vi.fn(), + }), +); + +vi.mock('../core/runner-client.ts', () => ({ + hasCachedAppleRunnerArtifact: mockHasCachedRunnerArtifact, + prewarmAppleRunnerCache: mockPrewarmRunnerCache, +})); +vi.mock('@agent-device/host-kit/environment', async (importOriginal) => ({ + ...(await importOriginal()), + hostPlatform: mockHostPlatform, +})); + +import type { HostDiagnosticsContext } from '@agent-device/contracts/host-diagnostics'; +import type { DeviceInfo } from '@agent-device/kernel/device'; +import { IOS_DEVICE, IOS_SIMULATOR, MACOS_DEVICE } from './device-fixtures.ts'; +import { appleRunnerWarmupCheck } from '../doctor.ts'; + +const ANDROID_EMULATOR: DeviceInfo = { + platform: 'android', + id: 'emulator-5554', + name: 'Pixel', + kind: 'emulator', + booted: true, +}; + +function contextWith(isProviderDevice = false): HostDiagnosticsContext { + return Object.freeze({ + stateDir: '/tmp/state', + metroPort: 8081, + shouldProbeMetro: false, + isProviderDevice: () => isProviderDevice, + emitProgress: vi.fn(), + listLocalDeviceInventory: async () => [], + shouldPropagateInventoryProbeError: () => false, + transportOverrides: Object.freeze({}), + }); +} + +beforeEach(() => { + mockHasCachedRunnerArtifact.mockReset(); + mockPrewarmRunnerCache.mockReset(); + mockPrewarmRunnerCache.mockResolvedValue(undefined); + mockHostPlatform.mockReset(); + mockHostPlatform.mockReturnValue('darwin'); +}); + +test('does not warm non-iOS, physical, or provider-backed devices', async () => { + const context = contextWith(); + + assert.equal(await appleRunnerWarmupCheck(ANDROID_EMULATOR, context), undefined); + assert.equal(await appleRunnerWarmupCheck(MACOS_DEVICE, context), undefined); + assert.equal(await appleRunnerWarmupCheck(IOS_DEVICE, contextWith(true)), undefined); + assert.equal(mockHostPlatform.mock.calls.length, 0); + assert.equal(mockHasCachedRunnerArtifact.mock.calls.length, 0); + assert.equal(mockPrewarmRunnerCache.mock.calls.length, 0); +}); + +test('does not warm an iOS simulator on a non-macOS host', async () => { + mockHostPlatform.mockReturnValue('linux'); + + assert.equal(await appleRunnerWarmupCheck(IOS_SIMULATOR, contextWith()), undefined); + assert.equal(mockHasCachedRunnerArtifact.mock.calls.length, 0); + assert.equal(mockPrewarmRunnerCache.mock.calls.length, 0); +}); + +test('reports a cached runner artifact without starting a build', async () => { + mockHasCachedRunnerArtifact.mockResolvedValue(true); + const context = contextWith(); + + const check = await appleRunnerWarmupCheck(IOS_SIMULATOR, context); + + assert.deepEqual(check, { + id: 'ios-runner-cache', + status: 'pass', + summary: 'iOS runner artifact cached; first open skips the runner build', + }); + assert.equal(mockPrewarmRunnerCache.mock.calls.length, 0); +}); + +test('starts a missing runner build opportunistically and reports the wait command', async () => { + mockHasCachedRunnerArtifact.mockResolvedValue(false); + const context = contextWith(); + + const check = await appleRunnerWarmupCheck(IOS_SIMULATOR, context); + + assert.equal(check?.id, 'ios-runner-cache'); + assert.equal(check?.status, 'pass'); + assert.match(check?.summary ?? '', /started in the background/); + assert.equal( + check?.hint, + 'Run `agent-device prepare ios-runner` to wait for a fully warmed runner instead.', + ); + assert.deepEqual(mockPrewarmRunnerCache.mock.calls, [[IOS_SIMULATOR, {}]]); +}); diff --git a/src/__tests__/test-utils/fake-apple-tool.ts b/packages/platform-apple/src/__tests__/fake-apple-tool.ts similarity index 57% rename from src/__tests__/test-utils/fake-apple-tool.ts rename to packages/platform-apple/src/__tests__/fake-apple-tool.ts index 384d65313b..506091a05b 100644 --- a/src/__tests__/test-utils/fake-apple-tool.ts +++ b/packages/platform-apple/src/__tests__/fake-apple-tool.ts @@ -1,33 +1,13 @@ import type { DeviceInfo } from '@agent-device/kernel/device'; import { AppError } from '@agent-device/kernel/errors'; -import type { AppleToolProvider } from '../../platforms/apple/core/tool-provider.ts'; -import { withAppleToolProvider } from '../../platforms/apple/core/tool-provider.ts'; +import type { AppleToolProvider } from '../core/tool-provider.ts'; +import { withAppleToolProvider } from '../core/tool-provider.ts'; import { execFailureDetails, type ExecResult } from '@agent-device/host-kit/command'; import { IOS_DEVICE } from './device-fixtures.ts'; -/** - * A scripted response for one fake Apple tool invocation: a string is - * shorthand for that stdout with exit 0, a partial result overrides the - * success defaults, an Error simulates a transport-level failure, and - * `undefined` falls back to empty success (the PATH-stub scripts' `exit 0` - * default). - */ export type FakeAppleToolResponse = string | Partial | Error; - -/** - * Receives the flat invocation the PATH-stub xcrun scripts saw: for - * `simctl`/`devicectl` calls the tool word is prepended - * (`['simctl', 'privacy', ...]`), for other `runCommand` binaries the command - * name leads (`['open', ...]`). - */ export type FakeAppleToolScript = (args: string[]) => FakeAppleToolResponse | undefined; -/** - * The `simctl privacy help` service listing production probes to enumerate - * privacy services — the same canned block `withMockedXcrun` injected into - * every PATH-stub script. Served when the test's script returns `undefined` - * for that probe, so scripts only model it to override it. - */ const SIMCTL_PRIVACY_HELP = `Usage: simctl privacy [] service @@ -45,36 +25,14 @@ const SIMCTL_PRIVACY_HELP = `Usage: simctl privacy [ motion - Allow access to motion and fitness data. reminders - Allow access to reminders. siri - Allow use of the app with Siri. - camera - Allow access to camera. + camera - Allow access to the camera. notifications - Allow access to notifications.`; -/** - * Runs `run` with a scripted in-process Apple tool provider installed through - * the production {@link withAppleToolProvider} scope — the seam the daemon - * installs per request and the provider-scenario lane exercises. Prefer this - * over PATH-stub xcrun scripts (`withMockedXcrun`): no PATH mutation, no - * spawns, no real subprocess waits. - * - * Mirrors the local provider's contract: a scripted nonzero exit throws the - * production-shaped COMMAND_FAILED (` exited with code N` with - * `execFailureDetails`) unless the call site passed `allowFailure` — the same - * path a PATH-stub `exit 1` took through exec.ts. - */ export async function withFakeAppleTool( script: FakeAppleToolScript, run: (ctx: { calls: string[][]; device: DeviceInfo }) => Promise, - options: { - device?: DeviceInfo; - /** - * Extra provider members (plist, macosHelper, macosHost, ...) merged into - * the installed fake. The scripted executors always win for - * runCommand/simctl/devicectl so `calls` keeps recording. - */ - provider?: Partial; - } = {}, + options: { device?: DeviceInfo; provider?: Partial } = {}, ): Promise { - // Fresh copy per call: tests may tailor the device without leaking - // mutations into the shared fixture. const device: DeviceInfo = { ...(options.device ?? IOS_DEVICE) }; const calls: string[][] = []; @@ -85,9 +43,13 @@ export async function withFakeAppleTool( ): Promise => { calls.push([...flat]); let response = script(flat); - if (response === undefined && flat[0] === 'simctl' && flat[1] === 'privacy') { - const probesHelp = flat.includes('help'); - if (probesHelp) response = SIMCTL_PRIVACY_HELP; + if ( + response === undefined && + flat[0] === 'simctl' && + flat[1] === 'privacy' && + flat.includes('help') + ) { + response = SIMCTL_PRIVACY_HELP; } if (response instanceof Error) throw response; const result: ExecResult = diff --git a/src/platforms/apple/__tests__/interactor-runner-provider.test.ts b/packages/platform-apple/src/__tests__/interactor-runner-provider.test.ts similarity index 98% rename from src/platforms/apple/__tests__/interactor-runner-provider.test.ts rename to packages/platform-apple/src/__tests__/interactor-runner-provider.test.ts index a19788f147..7bde3797a8 100644 --- a/src/platforms/apple/__tests__/interactor-runner-provider.test.ts +++ b/packages/platform-apple/src/__tests__/interactor-runner-provider.test.ts @@ -3,12 +3,12 @@ import type { Interactor, RunnerContext } from '@agent-device/contracts/interact import { AppError } from '@agent-device/kernel/errors'; import assert from 'node:assert/strict'; import { test } from 'vitest'; -import { IOS_SIMULATOR } from '../../../__tests__/test-utils/device-fixtures.ts'; +import { IOS_SIMULATOR } from './device-fixtures.ts'; import type { AppleRunnerCommandOptions, AppleRunnerProvider, RunnerCommand, -} from '@agent-device/platform-apple/runner'; +} from '../runner/index.ts'; import { createAppleInteractor } from '../interactor.ts'; type RecordedRunnerCall = { command: RunnerCommand; options: AppleRunnerCommandOptions }; diff --git a/src/platforms/apple/__tests__/screenshot-backend.test.ts b/packages/platform-apple/src/__tests__/screenshot-backend.test.ts similarity index 100% rename from src/platforms/apple/__tests__/screenshot-backend.test.ts rename to packages/platform-apple/src/__tests__/screenshot-backend.test.ts diff --git a/packages/platform-apple/src/__tests__/tmp-dir.ts b/packages/platform-apple/src/__tests__/tmp-dir.ts new file mode 100644 index 0000000000..0a8a60c897 --- /dev/null +++ b/packages/platform-apple/src/__tests__/tmp-dir.ts @@ -0,0 +1,7 @@ +import fsPromises from 'node:fs/promises'; +import os from 'node:os'; +import path from 'node:path'; + +export async function mkdtempForTest(prefix: string): Promise { + return fsPromises.mkdtemp(path.join(os.tmpdir(), prefix)); +} diff --git a/src/platforms/apple/__tests__/watchos-sentinel.test.ts b/packages/platform-apple/src/__tests__/watchos-sentinel.test.ts similarity index 100% rename from src/platforms/apple/__tests__/watchos-sentinel.test.ts rename to packages/platform-apple/src/__tests__/watchos-sentinel.test.ts diff --git a/src/platforms/apple/alert.ts b/packages/platform-apple/src/alert.ts similarity index 100% rename from src/platforms/apple/alert.ts rename to packages/platform-apple/src/alert.ts diff --git a/src/platforms/apple/core/__tests__/app-device-io.test.ts b/packages/platform-apple/src/core/__tests__/app-device-io.test.ts similarity index 100% rename from src/platforms/apple/core/__tests__/app-device-io.test.ts rename to packages/platform-apple/src/core/__tests__/app-device-io.test.ts diff --git a/src/platforms/apple/core/__tests__/app-install.test.ts b/packages/platform-apple/src/core/__tests__/app-install.test.ts similarity index 100% rename from src/platforms/apple/core/__tests__/app-install.test.ts rename to packages/platform-apple/src/core/__tests__/app-install.test.ts diff --git a/src/platforms/apple/core/__tests__/app-launch-close.test.ts b/packages/platform-apple/src/core/__tests__/app-launch-close.test.ts similarity index 98% rename from src/platforms/apple/core/__tests__/app-launch-close.test.ts rename to packages/platform-apple/src/core/__tests__/app-launch-close.test.ts index 43151ccdad..9471a4eda8 100644 --- a/src/platforms/apple/core/__tests__/app-launch-close.test.ts +++ b/packages/platform-apple/src/core/__tests__/app-launch-close.test.ts @@ -3,7 +3,7 @@ import { promises as fs } from 'node:fs'; import { test } from 'vitest'; -import { IOS_DEVICE } from '../../../../__tests__/test-utils/device-fixtures.ts'; +import { IOS_DEVICE } from '../../__tests__/device-fixtures.ts'; import { AppError } from '@agent-device/kernel/errors'; import { closeIosApp } from '../app-launch.ts'; import { createLocalAppleToolProvider, withAppleToolProvider } from '../tool-provider.ts'; diff --git a/src/platforms/apple/core/__tests__/app-launch-device-open.test.ts b/packages/platform-apple/src/core/__tests__/app-launch-device-open.test.ts similarity index 95% rename from src/platforms/apple/core/__tests__/app-launch-device-open.test.ts rename to packages/platform-apple/src/core/__tests__/app-launch-device-open.test.ts index 9c76966039..3af9ddbc09 100644 --- a/src/platforms/apple/core/__tests__/app-launch-device-open.test.ts +++ b/packages/platform-apple/src/core/__tests__/app-launch-device-open.test.ts @@ -1,6 +1,6 @@ import assert from 'node:assert/strict'; import { test } from 'vitest'; -import { IOS_SIMULATOR, MACOS_DEVICE } from '../../../../__tests__/test-utils/device-fixtures.ts'; +import { IOS_SIMULATOR, MACOS_DEVICE } from '../../__tests__/device-fixtures.ts'; import { openIosDevice } from '../app-launch.ts'; import { markSimulatorBooted } from '../simulator.ts'; import { createLocalAppleToolProvider, withAppleToolProvider } from '../tool-provider.ts'; diff --git a/src/platforms/apple/core/__tests__/app-launch-url.test.ts b/packages/platform-apple/src/core/__tests__/app-launch-url.test.ts similarity index 100% rename from src/platforms/apple/core/__tests__/app-launch-url.test.ts rename to packages/platform-apple/src/core/__tests__/app-launch-url.test.ts diff --git a/src/platforms/apple/core/__tests__/app-resolution-deep-link.test.ts b/packages/platform-apple/src/core/__tests__/app-resolution-deep-link.test.ts similarity index 96% rename from src/platforms/apple/core/__tests__/app-resolution-deep-link.test.ts rename to packages/platform-apple/src/core/__tests__/app-resolution-deep-link.test.ts index 4e261516ad..10aa23c5b7 100644 --- a/src/platforms/apple/core/__tests__/app-resolution-deep-link.test.ts +++ b/packages/platform-apple/src/core/__tests__/app-resolution-deep-link.test.ts @@ -4,11 +4,8 @@ import { test } from 'vitest'; import assert from 'node:assert/strict'; import { resolveIosSimulatorDeepLinkBundleId } from '../app-resolution.ts'; -import { - withFakeAppleTool, - type FakeAppleToolResponse, -} from '../../../../__tests__/test-utils/fake-apple-tool.ts'; -import { mkdtempForTest } from '../../../../__tests__/test-utils/tmp-dir.ts'; +import { withFakeAppleTool, type FakeAppleToolResponse } from '../../__tests__/fake-apple-tool.ts'; +import { mkdtempForTest } from '../../__tests__/tmp-dir.ts'; import { IOS_TEST_SIMULATOR } from './apple-core-stub-helpers.ts'; function unexpectedArgs(args: string[]): FakeAppleToolResponse { diff --git a/src/platforms/apple/core/__tests__/app-resolution.test.ts b/packages/platform-apple/src/core/__tests__/app-resolution.test.ts similarity index 100% rename from src/platforms/apple/core/__tests__/app-resolution.test.ts rename to packages/platform-apple/src/core/__tests__/app-resolution.test.ts diff --git a/packages/platform-apple/src/core/__tests__/apple-core-stub-helpers.ts b/packages/platform-apple/src/core/__tests__/apple-core-stub-helpers.ts new file mode 100644 index 0000000000..2b632fabb6 --- /dev/null +++ b/packages/platform-apple/src/core/__tests__/apple-core-stub-helpers.ts @@ -0,0 +1,36 @@ +import type { DeviceInfo } from '@agent-device/kernel/device'; + +export const IOS_TEST_DEVICE: DeviceInfo = { + platform: 'apple', + id: 'ios-device-1', + name: 'iPhone Device', + kind: 'device', + booted: true, +}; + +export const IOS_TEST_SIMULATOR: DeviceInfo = { + platform: 'apple', + id: 'sim-1', + name: 'iPhone 17 Pro', + kind: 'simulator', + booted: true, +}; + +export const MACOS_TEST_DEVICE: DeviceInfo = { + platform: 'apple', + appleOs: 'macos', + id: 'host-macos-local', + name: 'Mac', + kind: 'device', + target: 'desktop', + booted: true, +}; + +export const TVOS_TEST_SIMULATOR: DeviceInfo = { + platform: 'apple', + id: 'tvos-sim-1', + name: 'Apple TV', + kind: 'simulator', + target: 'tv', + booted: true, +}; diff --git a/src/platforms/apple/core/__tests__/apps.test.ts b/packages/platform-apple/src/core/__tests__/apps.test.ts similarity index 99% rename from src/platforms/apple/core/__tests__/apps.test.ts rename to packages/platform-apple/src/core/__tests__/apps.test.ts index 57da6fec95..504633cacd 100644 --- a/src/platforms/apple/core/__tests__/apps.test.ts +++ b/packages/platform-apple/src/core/__tests__/apps.test.ts @@ -2,7 +2,7 @@ import { beforeEach, test, vi } from 'vitest'; import assert from 'node:assert/strict'; import { promises as fs, readFileSync, writeFileSync } from 'node:fs'; import path from 'node:path'; -import { mkdtempForTest } from '../../../../__tests__/test-utils/tmp-dir.ts'; +import { mkdtempForTest } from '../../__tests__/tmp-dir.ts'; vi.mock('@agent-device/host-kit/command', async (importOriginal) => { const actual = await importOriginal(); @@ -45,11 +45,8 @@ import { AppError } from '@agent-device/kernel/errors'; import { runCmd } from '@agent-device/host-kit/command'; import { retryWithPolicy } from '@agent-device/host-kit/retry'; import { PNG } from '@agent-device/capture-kit/png'; -import { assertRejectsAppError } from '../../../../__tests__/test-utils/app-error.ts'; -import { - withFakeAppleTool, - type FakeAppleToolResponse, -} from '../../../../__tests__/test-utils/fake-apple-tool.ts'; +import { assertRejectsAppError } from '../../__tests__/app-error.ts'; +import { withFakeAppleTool, type FakeAppleToolResponse } from '../../__tests__/fake-apple-tool.ts'; import { IOS_TEST_DEVICE, IOS_TEST_SIMULATOR, diff --git a/src/platforms/apple/core/__tests__/debug-symbols.test.ts b/packages/platform-apple/src/core/__tests__/debug-symbols.test.ts similarity index 99% rename from src/platforms/apple/core/__tests__/debug-symbols.test.ts rename to packages/platform-apple/src/core/__tests__/debug-symbols.test.ts index 19f7ec6821..51b1d0f8c9 100644 --- a/src/platforms/apple/core/__tests__/debug-symbols.test.ts +++ b/packages/platform-apple/src/core/__tests__/debug-symbols.test.ts @@ -5,7 +5,7 @@ import { test } from 'vitest'; import { symbolicateCrashArtifact } from '../debug-symbols.ts'; import { AppError } from '@agent-device/kernel/errors'; import { withCommandExecutorOverride } from '@agent-device/host-kit/command'; -import { mkdtempForTest } from '../../../../__tests__/test-utils/tmp-dir.ts'; +import { mkdtempForTest } from '../../__tests__/tmp-dir.ts'; const UUID = 'ABCDEFAB-CDEF-ABCD-EFAB-CDEFABCDEFAB'; const NORMALIZED_UUID = 'ABCDEFABCDEFABCDEFABCDEFABCDEFAB'; diff --git a/src/platforms/apple/core/__tests__/devicectl.test.ts b/packages/platform-apple/src/core/__tests__/devicectl.test.ts similarity index 100% rename from src/platforms/apple/core/__tests__/devicectl.test.ts rename to packages/platform-apple/src/core/__tests__/devicectl.test.ts diff --git a/src/platforms/apple/core/__tests__/install-artifact.fixtures.ts b/packages/platform-apple/src/core/__tests__/install-artifact.fixtures.ts similarity index 100% rename from src/platforms/apple/core/__tests__/install-artifact.fixtures.ts rename to packages/platform-apple/src/core/__tests__/install-artifact.fixtures.ts diff --git a/src/platforms/apple/core/__tests__/interactions.test.ts b/packages/platform-apple/src/core/__tests__/interactions.test.ts similarity index 99% rename from src/platforms/apple/core/__tests__/interactions.test.ts rename to packages/platform-apple/src/core/__tests__/interactions.test.ts index 2314afed83..8f63bfde18 100644 --- a/src/platforms/apple/core/__tests__/interactions.test.ts +++ b/packages/platform-apple/src/core/__tests__/interactions.test.ts @@ -10,7 +10,7 @@ import { PHYSICAL_IOS_MULTI_TOUCH_UNSUPPORTED_HINT, } from '@agent-device/contracts/gesture-admission'; import type { GesturePlan } from '@agent-device/contracts/gesture-plan-types'; -import type { RunnerCommand } from '@agent-device/platform-apple/runner'; +import type { RunnerCommand } from '../../runner/index.ts'; import { TEXT_ENTRY_ROUTES } from '@agent-device/contracts/interactor-types'; import { IOS_TEST_DEVICE, diff --git a/src/platforms/apple/core/__tests__/launch-diagnostics.test.ts b/packages/platform-apple/src/core/__tests__/launch-diagnostics.test.ts similarity index 100% rename from src/platforms/apple/core/__tests__/launch-diagnostics.test.ts rename to packages/platform-apple/src/core/__tests__/launch-diagnostics.test.ts diff --git a/src/platforms/apple/core/__tests__/macos-helper-test-utils.ts b/packages/platform-apple/src/core/__tests__/macos-helper-test-utils.ts similarity index 92% rename from src/platforms/apple/core/__tests__/macos-helper-test-utils.ts rename to packages/platform-apple/src/core/__tests__/macos-helper-test-utils.ts index 1d810bd91b..768166e08c 100644 --- a/src/platforms/apple/core/__tests__/macos-helper-test-utils.ts +++ b/packages/platform-apple/src/core/__tests__/macos-helper-test-utils.ts @@ -1,6 +1,6 @@ import fs from 'node:fs/promises'; import path from 'node:path'; -import { mkdtempForTest } from '../../../../__tests__/test-utils/tmp-dir.ts'; +import { mkdtempForTest } from '../../__tests__/tmp-dir.ts'; export async function withMockedMacOsHelper( script: string, diff --git a/src/platforms/apple/core/__tests__/perf-time-profile.test.ts b/packages/platform-apple/src/core/__tests__/perf-time-profile.test.ts similarity index 100% rename from src/platforms/apple/core/__tests__/perf-time-profile.test.ts rename to packages/platform-apple/src/core/__tests__/perf-time-profile.test.ts diff --git a/src/platforms/apple/core/__tests__/perf.test.ts b/packages/platform-apple/src/core/__tests__/perf.test.ts similarity index 99% rename from src/platforms/apple/core/__tests__/perf.test.ts rename to packages/platform-apple/src/core/__tests__/perf.test.ts index 939040f4ce..cd2315741a 100644 --- a/src/platforms/apple/core/__tests__/perf.test.ts +++ b/packages/platform-apple/src/core/__tests__/perf.test.ts @@ -2,7 +2,7 @@ import { beforeEach, test, vi } from 'vitest'; import assert from 'node:assert/strict'; import { promises as fs } from 'node:fs'; import path from 'node:path'; -import { mkdtempForTest } from '../../../../__tests__/test-utils/tmp-dir.ts'; +import { mkdtempForTest } from '../../__tests__/tmp-dir.ts'; vi.mock('@agent-device/host-kit/command', async (importOriginal) => { const actual = await importOriginal(); diff --git a/src/platforms/apple/core/__tests__/physical-device-control.test.ts b/packages/platform-apple/src/core/__tests__/physical-device-control.test.ts similarity index 94% rename from src/platforms/apple/core/__tests__/physical-device-control.test.ts rename to packages/platform-apple/src/core/__tests__/physical-device-control.test.ts index 2b19bdd247..d9230011c2 100644 --- a/src/platforms/apple/core/__tests__/physical-device-control.test.ts +++ b/packages/platform-apple/src/core/__tests__/physical-device-control.test.ts @@ -2,7 +2,7 @@ import assert from 'node:assert/strict'; import { promises as fs } from 'node:fs'; import path from 'node:path'; import { test } from 'vitest'; -import { IOS_DEVICE as SHARED_IOS_DEVICE } from '../../../../__tests__/test-utils/device-fixtures.ts'; +import { IOS_DEVICE as SHARED_IOS_DEVICE } from '../../__tests__/device-fixtures.ts'; import type { DeviceInfo } from '@agent-device/kernel/device'; import type { AppError } from '@agent-device/kernel/errors'; import { createAppleInteractor } from '../../interactor.ts'; @@ -10,9 +10,9 @@ import { installIosInstallablePath } from '../app-install.ts'; import { closeIosApp, openIosApp } from '../app-launch.ts'; import { listIosApps } from '../app-resolution.ts'; import { resolveIosPhysicalDeviceControl } from '../physical-device-control.ts'; -import { withAppleRunnerProvider } from '@agent-device/platform-apple/runner'; +import { withAppleRunnerProvider } from '../../runner/index.ts'; import { createLocalAppleToolProvider, withAppleToolProvider } from '../tool-provider.ts'; -import { mkdtempForTest } from '../../../../__tests__/test-utils/tmp-dir.ts'; +import { mkdtempForTest } from '../../__tests__/tmp-dir.ts'; const IOS_DEVICE: DeviceInfo = { ...SHARED_IOS_DEVICE, diff --git a/src/platforms/apple/core/__tests__/physical-device-coredevice.test.ts b/packages/platform-apple/src/core/__tests__/physical-device-coredevice.test.ts similarity index 100% rename from src/platforms/apple/core/__tests__/physical-device-coredevice.test.ts rename to packages/platform-apple/src/core/__tests__/physical-device-coredevice.test.ts diff --git a/src/platforms/apple/core/__tests__/physical-device-screenshot.test.ts b/packages/platform-apple/src/core/__tests__/physical-device-screenshot.test.ts similarity index 95% rename from src/platforms/apple/core/__tests__/physical-device-screenshot.test.ts rename to packages/platform-apple/src/core/__tests__/physical-device-screenshot.test.ts index 55a4701549..0d8566c00a 100644 --- a/src/platforms/apple/core/__tests__/physical-device-screenshot.test.ts +++ b/packages/platform-apple/src/core/__tests__/physical-device-screenshot.test.ts @@ -1,6 +1,6 @@ import { test } from 'vitest'; import assert from 'node:assert/strict'; -import { IOS_DEVICE } from '../../../../__tests__/test-utils/device-fixtures.ts'; +import { IOS_DEVICE } from '../../__tests__/device-fixtures.ts'; import { captureCoreDeviceScreenshot, shouldFallbackToRunnerForIosScreenshot, diff --git a/src/platforms/apple/core/__tests__/plist.test.ts b/packages/platform-apple/src/core/__tests__/plist.test.ts similarity index 97% rename from src/platforms/apple/core/__tests__/plist.test.ts rename to packages/platform-apple/src/core/__tests__/plist.test.ts index a31dd608d0..564e53ed98 100644 --- a/src/platforms/apple/core/__tests__/plist.test.ts +++ b/packages/platform-apple/src/core/__tests__/plist.test.ts @@ -2,7 +2,7 @@ import { beforeEach, test, vi } from 'vitest'; import assert from 'node:assert/strict'; import { promises as fs } from 'node:fs'; import path from 'node:path'; -import { mkdtempForTest } from '../../../../__tests__/test-utils/tmp-dir.ts'; +import { mkdtempForTest } from '../../__tests__/tmp-dir.ts'; vi.mock('@agent-device/host-kit/command', async (importOriginal) => { const actual = await importOriginal(); diff --git a/src/platforms/apple/core/__tests__/runner-selector-query.test.ts b/packages/platform-apple/src/core/__tests__/runner-selector-query.test.ts similarity index 91% rename from src/platforms/apple/core/__tests__/runner-selector-query.test.ts rename to packages/platform-apple/src/core/__tests__/runner-selector-query.test.ts index 37e321ab77..5d9c864bfe 100644 --- a/src/platforms/apple/core/__tests__/runner-selector-query.test.ts +++ b/packages/platform-apple/src/core/__tests__/runner-selector-query.test.ts @@ -1,5 +1,5 @@ import { expect, test, vi } from 'vitest'; -import { IOS_SIMULATOR } from '../../../../__tests__/test-utils/device-fixtures.ts'; +import { IOS_SIMULATOR } from '../../__tests__/device-fixtures.ts'; const { runAppleRunnerCommand } = vi.hoisted(() => ({ runAppleRunnerCommand: vi.fn(async () => ({ found: true })), diff --git a/src/platforms/apple/core/__tests__/screenshot-density.test.ts b/packages/platform-apple/src/core/__tests__/screenshot-density.test.ts similarity index 96% rename from src/platforms/apple/core/__tests__/screenshot-density.test.ts rename to packages/platform-apple/src/core/__tests__/screenshot-density.test.ts index 7b77d40d24..ccda003702 100644 --- a/src/platforms/apple/core/__tests__/screenshot-density.test.ts +++ b/packages/platform-apple/src/core/__tests__/screenshot-density.test.ts @@ -6,7 +6,7 @@ import type { DeviceInfo } from '@agent-device/kernel/device'; import { PNG } from '@agent-device/capture-kit/png'; import { screenshotIos } from '../screenshot.ts'; import { createLocalAppleToolProvider, withAppleToolProvider } from '../tool-provider.ts'; -import { mkdtempForTest } from '../../../../__tests__/test-utils/tmp-dir.ts'; +import { mkdtempForTest } from '../../__tests__/tmp-dir.ts'; function startsWithArgs(args: string[], expected: string[]): boolean { return expected.every((value, index) => args[index] === value); diff --git a/src/platforms/apple/core/__tests__/screenshot-status-bar.test.ts b/packages/platform-apple/src/core/__tests__/screenshot-status-bar.test.ts similarity index 97% rename from src/platforms/apple/core/__tests__/screenshot-status-bar.test.ts rename to packages/platform-apple/src/core/__tests__/screenshot-status-bar.test.ts index 2bed87e41f..a2bfee803b 100644 --- a/src/platforms/apple/core/__tests__/screenshot-status-bar.test.ts +++ b/packages/platform-apple/src/core/__tests__/screenshot-status-bar.test.ts @@ -4,7 +4,7 @@ import { invalidateSimulatorStatusBarOverrideCache, prepareSimulatorStatusBarForScreenshot, } from '../screenshot-status-bar.ts'; -import { withFakeAppleTool } from '../../../../__tests__/test-utils/fake-apple-tool.ts'; +import { withFakeAppleTool } from '../../__tests__/fake-apple-tool.ts'; import { IOS_TEST_SIMULATOR } from './apple-core-stub-helpers.ts'; // The fake tool provider installs through the production withAppleToolProvider diff --git a/src/platforms/apple/core/__tests__/screenshot.test.ts b/packages/platform-apple/src/core/__tests__/screenshot.test.ts similarity index 99% rename from src/platforms/apple/core/__tests__/screenshot.test.ts rename to packages/platform-apple/src/core/__tests__/screenshot.test.ts index 1110e0c622..361ec2f14b 100644 --- a/src/platforms/apple/core/__tests__/screenshot.test.ts +++ b/packages/platform-apple/src/core/__tests__/screenshot.test.ts @@ -2,7 +2,7 @@ import { beforeEach, test, vi } from 'vitest'; import assert from 'node:assert/strict'; import { promises as fs } from 'node:fs'; import path from 'node:path'; -import { mkdtempForTest } from '../../../../__tests__/test-utils/tmp-dir.ts'; +import { mkdtempForTest } from '../../__tests__/tmp-dir.ts'; vi.mock('@agent-device/host-kit/command', async (importOriginal) => { const actual = await importOriginal(); diff --git a/src/platforms/apple/core/__tests__/settings-parsing.test.ts b/packages/platform-apple/src/core/__tests__/settings-parsing.test.ts similarity index 95% rename from src/platforms/apple/core/__tests__/settings-parsing.test.ts rename to packages/platform-apple/src/core/__tests__/settings-parsing.test.ts index df9f147d9f..a76b2bd2dc 100644 --- a/src/platforms/apple/core/__tests__/settings-parsing.test.ts +++ b/packages/platform-apple/src/core/__tests__/settings-parsing.test.ts @@ -5,7 +5,7 @@ import { parseSettingState, summarizeCommandAttemptFailures, } from '../settings-parsing.ts'; -import { assertThrowsAppError } from '../../../../__tests__/test-utils/app-error.ts'; +import { assertThrowsAppError } from '../../__tests__/app-error.ts'; test('parseAppearanceAction accepts the three actions in any casing or padding', () => { assert.equal(parseAppearanceAction('light'), 'light'); diff --git a/src/platforms/apple/core/__tests__/simctl.test.ts b/packages/platform-apple/src/core/__tests__/simctl.test.ts similarity index 100% rename from src/platforms/apple/core/__tests__/simctl.test.ts rename to packages/platform-apple/src/core/__tests__/simctl.test.ts diff --git a/src/platforms/apple/core/__tests__/simulator-booted-memo.test.ts b/packages/platform-apple/src/core/__tests__/simulator-booted-memo.test.ts similarity index 100% rename from src/platforms/apple/core/__tests__/simulator-booted-memo.test.ts rename to packages/platform-apple/src/core/__tests__/simulator-booted-memo.test.ts diff --git a/src/platforms/apple/core/__tests__/simulator.test.ts b/packages/platform-apple/src/core/__tests__/simulator.test.ts similarity index 100% rename from src/platforms/apple/core/__tests__/simulator.test.ts rename to packages/platform-apple/src/core/__tests__/simulator.test.ts diff --git a/src/platforms/apple/core/__tests__/tool-provider.test.ts b/packages/platform-apple/src/core/__tests__/tool-provider.test.ts similarity index 100% rename from src/platforms/apple/core/__tests__/tool-provider.test.ts rename to packages/platform-apple/src/core/__tests__/tool-provider.test.ts diff --git a/src/platforms/apple/core/app-device-io.ts b/packages/platform-apple/src/core/app-device-io.ts similarity index 89% rename from src/platforms/apple/core/app-device-io.ts rename to packages/platform-apple/src/core/app-device-io.ts index f29e9497e6..ead01ce6d2 100644 --- a/src/platforms/apple/core/app-device-io.ts +++ b/packages/platform-apple/src/core/app-device-io.ts @@ -1,8 +1,8 @@ -import { promises as fs } from 'node:fs'; -import os from 'node:os'; +import { fs } from '@agent-device/host-kit/filesystem'; import path from 'node:path'; import { isMacOs, type DeviceInfo } from '@agent-device/kernel/device'; import { requireExecSuccess } from '@agent-device/host-kit/command'; +import { hostTemporaryDirectory } from '@agent-device/host-kit/environment'; import { ensureBootedSimulator, requireSimulatorDevice } from './simulator.ts'; import { readMacOsClipboardText, writeMacOsClipboardText } from '../os/macos/apps.ts'; import { runSimctl } from './apps-simctl.ts'; @@ -45,7 +45,7 @@ export async function pushIosNotification( requireSimulatorDevice(device, 'push'); options.signal?.throwIfAborted(); await ensureBootedSimulator(device, { signal: options.signal }); - const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'agent-device-ios-push-')); + const tempDir = await fs.mkdtemp(path.join(hostTemporaryDirectory(), 'agent-device-ios-push-')); const payloadPath = path.join(tempDir, 'payload.apns'); try { await fs.writeFile(payloadPath, `${JSON.stringify(payload)}\n`, 'utf8'); diff --git a/src/platforms/apple/core/app-filter.ts b/packages/platform-apple/src/core/app-filter.ts similarity index 100% rename from src/platforms/apple/core/app-filter.ts rename to packages/platform-apple/src/core/app-filter.ts diff --git a/src/platforms/apple/core/app-info.ts b/packages/platform-apple/src/core/app-info.ts similarity index 100% rename from src/platforms/apple/core/app-info.ts rename to packages/platform-apple/src/core/app-info.ts diff --git a/src/platforms/apple/core/app-install.ts b/packages/platform-apple/src/core/app-install.ts similarity index 100% rename from src/platforms/apple/core/app-install.ts rename to packages/platform-apple/src/core/app-install.ts diff --git a/src/platforms/apple/core/app-launch.ts b/packages/platform-apple/src/core/app-launch.ts similarity index 98% rename from src/platforms/apple/core/app-launch.ts rename to packages/platform-apple/src/core/app-launch.ts index da5d8f6838..b07c71519d 100644 --- a/src/platforms/apple/core/app-launch.ts +++ b/packages/platform-apple/src/core/app-launch.ts @@ -1,4 +1,4 @@ -import { promises as fs } from 'node:fs'; +import { fs } from '@agent-device/host-kit/filesystem'; import path from 'node:path'; import { isIosFamily, isMacOs, type DeviceInfo } from '@agent-device/kernel/device'; import { AppError } from '@agent-device/kernel/errors'; @@ -19,7 +19,7 @@ import { import { IOS_APP_LAUNCH_TIMEOUT_MS, IOS_SIMULATOR_TERMINATE_TIMEOUT_MS } from './config.ts'; import { resolveIosPhysicalDeviceControl } from './physical-device-control.ts'; import { runAppleRunnerCommand } from './runner-client.ts'; -import type { AppleRunnerCommandOptions } from '@agent-device/platform-apple/runner'; +import type { AppleRunnerCommandOptions } from '../runner/index.ts'; import { isSimulatorLaunchFBSError, probeSimulatorLaunchContext, diff --git a/packages/platform-apple/src/core/app-resolution-error.ts b/packages/platform-apple/src/core/app-resolution-error.ts new file mode 100644 index 0000000000..318039ec08 --- /dev/null +++ b/packages/platform-apple/src/core/app-resolution-error.ts @@ -0,0 +1,5 @@ +import { AppError } from '@agent-device/kernel/errors'; + +export function buildAppNotInstalledError(app: string): AppError { + return new AppError('APP_NOT_INSTALLED', `No app found matching "${app}"`); +} diff --git a/src/platforms/apple/core/app-resolution.ts b/packages/platform-apple/src/core/app-resolution.ts similarity index 96% rename from src/platforms/apple/core/app-resolution.ts rename to packages/platform-apple/src/core/app-resolution.ts index a8df4a54dc..0bc98b967a 100644 --- a/src/platforms/apple/core/app-resolution.ts +++ b/packages/platform-apple/src/core/app-resolution.ts @@ -1,5 +1,5 @@ import path from 'node:path'; -import { promises as fs } from 'node:fs'; +import { fs } from '@agent-device/host-kit/filesystem'; import { fileURLToPath } from 'node:url'; import { isIosFamily, isMacOs, type DeviceInfo } from '@agent-device/kernel/device'; import { AppError } from '@agent-device/kernel/errors'; @@ -10,6 +10,7 @@ import { } from '@agent-device/provision-kit/app-resolution-cache'; import type { IosAppInfo } from './app-info.ts'; import { filterAppleAppsByBundlePrefix } from './app-filter.ts'; +import { buildAppNotInstalledError } from './app-resolution-error.ts'; import { listMacApps, resolveMacOsApp } from '../os/macos/apps.ts'; import { runAppleToolCommand } from './tool-provider.ts'; import { runSimctl } from './apps-simctl.ts'; @@ -63,12 +64,7 @@ export async function resolveIosApp(device: DeviceInfo, app: string): Promise> { const simulatorSetPath = resolveIosSimulatorDeviceSetPath(device.simulatorSetPath); - const currentCacheKey = `${process.env.PATH ?? ''}::${simulatorSetPath ?? ''}`; + const currentCacheKey = `${readHostEnvironmentVariable('PATH') ?? ''}::${simulatorSetPath ?? ''}`; if (cachedSimctlPrivacyServices && cachedSimctlPrivacyServicesCacheKey === currentCacheKey) { return cachedSimctlPrivacyServices; } diff --git a/src/platforms/apple/core/apps-simctl.ts b/packages/platform-apple/src/core/apps-simctl.ts similarity index 100% rename from src/platforms/apple/core/apps-simctl.ts rename to packages/platform-apple/src/core/apps-simctl.ts diff --git a/src/platforms/apple/core/apps.ts b/packages/platform-apple/src/core/apps.ts similarity index 70% rename from src/platforms/apple/core/apps.ts rename to packages/platform-apple/src/core/apps.ts index 0aebb2e47f..d43906241c 100644 --- a/src/platforms/apple/core/apps.ts +++ b/packages/platform-apple/src/core/apps.ts @@ -1,10 +1,5 @@ export { screenshotIos } from './screenshot.ts'; -export { - findIosSimulatorInstalledApp, - listIosApps, - resolveIosApp, - resolveIosSimulatorDeepLinkBundleId, -} from './app-resolution.ts'; +export { resolveIosApp, resolveIosSimulatorDeepLinkBundleId } from './app-resolution.ts'; export { closeIosApp, openIosApp, openIosDevice } from './app-launch.ts'; export { installIosInstallablePath } from './app-install.ts'; export { diff --git a/src/platforms/apple/core/config.ts b/packages/platform-apple/src/core/config.ts similarity index 100% rename from src/platforms/apple/core/config.ts rename to packages/platform-apple/src/core/config.ts diff --git a/src/platforms/apple/core/debug-symbols.ts b/packages/platform-apple/src/core/debug-symbols.ts similarity index 96% rename from src/platforms/apple/core/debug-symbols.ts rename to packages/platform-apple/src/core/debug-symbols.ts index 3a7038aee4..8891eb7b04 100644 --- a/src/platforms/apple/core/debug-symbols.ts +++ b/packages/platform-apple/src/core/debug-symbols.ts @@ -1,5 +1,6 @@ -import fs from 'node:fs/promises'; +import { fs } from '@agent-device/host-kit/filesystem'; import path from 'node:path'; +import { hostWorkingDirectory } from '@agent-device/host-kit/environment'; import { readAppleCrashArtifact } from './debug-symbols/crash-artifact.ts'; import { matchImagesToDsyms, readDsymPaths, readDsymSlices } from './debug-symbols/dsym.ts'; import { summarizeCrashArtifact } from './debug-symbols/report.ts'; @@ -20,7 +21,7 @@ export async function symbolicateCrashArtifact( hint: 'Use debug symbols --artifact --dsym or --search-path --out .', }); } - const cwd = options.cwd ?? process.cwd(); + const cwd = options.cwd ?? hostWorkingDirectory(); const artifactPath = resolvePath(cwd, options.artifact); const outPath = resolvePath(cwd, options.out ?? defaultOutPath(artifactPath)); const artifactText = await readTextFile(artifactPath, 'crash artifact'); diff --git a/src/platforms/apple/core/debug-symbols/crash-artifact.ts b/packages/platform-apple/src/core/debug-symbols/crash-artifact.ts similarity index 100% rename from src/platforms/apple/core/debug-symbols/crash-artifact.ts rename to packages/platform-apple/src/core/debug-symbols/crash-artifact.ts diff --git a/src/platforms/apple/core/debug-symbols/dsym.ts b/packages/platform-apple/src/core/debug-symbols/dsym.ts similarity index 99% rename from src/platforms/apple/core/debug-symbols/dsym.ts rename to packages/platform-apple/src/core/debug-symbols/dsym.ts index cbf835cb91..1f5c416b57 100644 --- a/src/platforms/apple/core/debug-symbols/dsym.ts +++ b/packages/platform-apple/src/core/debug-symbols/dsym.ts @@ -1,4 +1,4 @@ -import fs from 'node:fs/promises'; +import { fs } from '@agent-device/host-kit/filesystem'; import path from 'node:path'; import type { AppleImage, DsymMatch, DsymSlice } from './types.ts'; import { normalizeUuid, unique } from './utils.ts'; diff --git a/src/platforms/apple/core/debug-symbols/report.ts b/packages/platform-apple/src/core/debug-symbols/report.ts similarity index 98% rename from src/platforms/apple/core/debug-symbols/report.ts rename to packages/platform-apple/src/core/debug-symbols/report.ts index e4809d97c7..b3ff380faf 100644 --- a/src/platforms/apple/core/debug-symbols/report.ts +++ b/packages/platform-apple/src/core/debug-symbols/report.ts @@ -168,8 +168,8 @@ function summarizeTextCrash( } function readTextProcessName(lines: string[]): string | undefined { - const process = readTextField(lines, 'Process'); - return process?.replace(/\s+\[\d+\]$/, ''); + const processName = readTextField(lines, 'Process'); + return processName?.replace(/\s+\[\d+\]$/, ''); } function readTextField(lines: string[], label: string): string | undefined { diff --git a/src/platforms/apple/core/debug-symbols/symbolication.ts b/packages/platform-apple/src/core/debug-symbols/symbolication.ts similarity index 100% rename from src/platforms/apple/core/debug-symbols/symbolication.ts rename to packages/platform-apple/src/core/debug-symbols/symbolication.ts diff --git a/src/platforms/apple/core/debug-symbols/types.ts b/packages/platform-apple/src/core/debug-symbols/types.ts similarity index 100% rename from src/platforms/apple/core/debug-symbols/types.ts rename to packages/platform-apple/src/core/debug-symbols/types.ts diff --git a/src/platforms/apple/core/debug-symbols/utils.ts b/packages/platform-apple/src/core/debug-symbols/utils.ts similarity index 100% rename from src/platforms/apple/core/debug-symbols/utils.ts rename to packages/platform-apple/src/core/debug-symbols/utils.ts diff --git a/src/platforms/apple/core/devicectl.ts b/packages/platform-apple/src/core/devicectl.ts similarity index 94% rename from src/platforms/apple/core/devicectl.ts rename to packages/platform-apple/src/core/devicectl.ts index 8a709140af..6d67d3defd 100644 --- a/src/platforms/apple/core/devicectl.ts +++ b/packages/platform-apple/src/core/devicectl.ts @@ -1,10 +1,10 @@ -import { promises as fs } from 'node:fs'; -import os from 'node:os'; +import { fs } from '@agent-device/host-kit/filesystem'; import path from 'node:path'; import type { DeviceInfo } from '@agent-device/kernel/device'; import { AppError } from '@agent-device/kernel/errors'; import { execFailureDetails } from '@agent-device/host-kit/command'; +import { hostProcessId, hostTemporaryDirectory } from '@agent-device/host-kit/environment'; import { IOS_DEVICECTL_TIMEOUT_MS } from './config.ts'; import { runXcrun } from './tool-provider.ts'; @@ -112,11 +112,11 @@ async function listIosDeviceProcesses(device: DeviceInfo): Promise { const { appBundleUrl, processes } = await resolveIosDeviceAppProcesses(device, bundleId); // Extensions share the app bundle URL, but closing the app targets its shallowest main process. - const process = processes.sort( + const processInfo = processes.sort( (left, right) => processPathDepth(left, appBundleUrl) - processPathDepth(right, appBundleUrl), )[0]; // No match means the installed app is already closed. - if (!process) return; + if (!processInfo) return; await runIosDevicectlJsonCommand(device, { jsonPrefix: 'agent-device-ios-process-terminate', @@ -128,7 +128,7 @@ export async function terminateIosDeviceApp(device: DeviceInfo, bundleId: string '--device', device.id, '--pid', - String(process.pid), + String(processInfo.pid), '--kill', ], failureMessage: 'Failed to terminate iOS app', @@ -161,8 +161,8 @@ export async function resolveIosDeviceAppProcesses( const appBundleUrl = app.url.replace(/\/+$/, ''); // CoreDevice reports app URLs and process executables in the same file-URL form. - const processes = (await listIosDeviceProcesses(device)).filter((process) => - process.executable.startsWith(`${appBundleUrl}/`), + const processes = (await listIosDeviceProcesses(device)).filter((processInfo) => + processInfo.executable.startsWith(`${appBundleUrl}/`), ); return { appBundleUrl, processes }; } @@ -179,8 +179,8 @@ async function runIosDevicectlJsonCommand( }, ): Promise { const jsonPath = path.join( - os.tmpdir(), - `${options.jsonPrefix}-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}.json`, + hostTemporaryDirectory(), + `${options.jsonPrefix}-${hostProcessId()}-${Date.now()}-${Math.random().toString(36).slice(2)}.json`, ); const args = [...options.args, '--json-output', jsonPath]; const result = await runXcrun(args, { @@ -229,8 +229,8 @@ async function readJsonFile(jsonPath: string): Promise { return JSON.parse(await fs.readFile(jsonPath, 'utf8')); } -function processPathDepth(process: IosDeviceProcessInfo, appBundleUrl: string): number { - return process.executable.slice(appBundleUrl.length + 1).split('/').length; +function processPathDepth(processInfo: IosDeviceProcessInfo, appBundleUrl: string): number { + return processInfo.executable.slice(appBundleUrl.length + 1).split('/').length; } function isIosDeviceProcessesPayload(payload: unknown): payload is IosDeviceProcessesPayload { diff --git a/src/platforms/apple/core/install-artifact.ts b/packages/platform-apple/src/core/install-artifact.ts similarity index 97% rename from src/platforms/apple/core/install-artifact.ts rename to packages/platform-apple/src/core/install-artifact.ts index 5de599b0d5..c6f6c0e665 100644 --- a/src/platforms/apple/core/install-artifact.ts +++ b/packages/platform-apple/src/core/install-artifact.ts @@ -1,5 +1,4 @@ -import { promises as fs } from 'node:fs'; -import os from 'node:os'; +import { fs } from '@agent-device/host-kit/filesystem'; import path from 'node:path'; import type { LocalInstallSource } from '@agent-device/kernel/contracts'; import { readInfoPlistString } from './plist.ts'; @@ -16,6 +15,7 @@ import { isTrustedInstallSourceUrl, materializeInstallablePath, } from '@agent-device/provision-kit/install-source'; +import { hostTemporaryDirectory } from '@agent-device/host-kit/environment'; type InstallIosArtifactOptions = { appIdentifierHint?: string; @@ -128,7 +128,7 @@ async function resolveIosIpaInstallablePath( appPath: string, options?: InstallIosArtifactOptions, ): Promise<{ installPath: string; cleanup: () => Promise }> { - const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'agent-device-ios-ipa-')); + const tempDir = await fs.mkdtemp(path.join(hostTemporaryDirectory(), 'agent-device-ios-ipa-')); const cleanup = async () => { await fs.rm(tempDir, { recursive: true, force: true }); }; diff --git a/src/platforms/apple/core/launch-diagnostics.ts b/packages/platform-apple/src/core/launch-diagnostics.ts similarity index 100% rename from src/platforms/apple/core/launch-diagnostics.ts rename to packages/platform-apple/src/core/launch-diagnostics.ts diff --git a/src/platforms/apple/core/perf-frame.ts b/packages/platform-apple/src/core/perf-frame.ts similarity index 98% rename from src/platforms/apple/core/perf-frame.ts rename to packages/platform-apple/src/core/perf-frame.ts index b67686828b..dc8cbda192 100644 --- a/src/platforms/apple/core/perf-frame.ts +++ b/packages/platform-apple/src/core/perf-frame.ts @@ -153,8 +153,8 @@ function readAppleHitchRow( const startNs = resolveXmlNumber(row.children[indexes.start], references); const durationNs = resolveXmlNumber(row.children[indexes.duration], references); if (startNs === null || durationNs === null) return null; - const process = resolveXmlProcess(row.children[indexes.process], references); - return { startNs, durationNs, pid: process?.pid, processName: process?.name }; + const processInfo = resolveXmlProcess(row.children[indexes.process], references); + return { startNs, durationNs, pid: processInfo?.pid, processName: processInfo?.name }; } function matchesAppleFrameProcess( diff --git a/src/platforms/apple/core/perf-time-profile.ts b/packages/platform-apple/src/core/perf-time-profile.ts similarity index 100% rename from src/platforms/apple/core/perf-time-profile.ts rename to packages/platform-apple/src/core/perf-time-profile.ts diff --git a/src/platforms/apple/core/perf-xctrace.ts b/packages/platform-apple/src/core/perf-xctrace.ts similarity index 96% rename from src/platforms/apple/core/perf-xctrace.ts rename to packages/platform-apple/src/core/perf-xctrace.ts index b1a4fe0037..ef7e0016e1 100644 --- a/src/platforms/apple/core/perf-xctrace.ts +++ b/packages/platform-apple/src/core/perf-xctrace.ts @@ -1,5 +1,4 @@ -import { promises as fs } from 'node:fs'; -import os from 'node:os'; +import { fs } from '@agent-device/host-kit/filesystem'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { @@ -19,6 +18,7 @@ import { type ExecResult, } from '@agent-device/host-kit/command'; import { uniqueStrings } from '@agent-device/kernel/collections'; +import { hostTemporaryDirectory } from '@agent-device/host-kit/environment'; import { findAllXmlNodes } from './perf-xml.ts'; import { parseAppleTimeProfileSummary, @@ -183,7 +183,9 @@ export async function writeAppleXctracePerfReport(params: { template?: string; appBundleId?: string; }): Promise { - const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'agent-device-xctrace-report-')); + const tempDir = await fs.mkdtemp( + path.join(hostTemporaryDirectory(), 'agent-device-xctrace-report-'), + ); const tocPath = path.join(tempDir, 'trace-toc.xml'); const timeProfilePath = path.join(tempDir, 'time-profile.xml'); try { @@ -271,9 +273,9 @@ async function resolveAppleXctracePerfTarget( if (isIosFamily(device) && device.kind === 'device') { const processes = await resolveIosDevicePerfTarget(device, appBundleId); return { - pids: processes.map((process) => process.pid), + pids: processes.map((processInfo) => processInfo.pid), processNames: uniqueStrings( - processes.map((process) => path.basename(fileURLToPath(process.executable))), + processes.map((processInfo) => path.basename(fileURLToPath(processInfo.executable))), ), }; } @@ -288,7 +290,7 @@ async function resolveAppleXctracePerfTarget( }); } return { - pids: processes.map((process) => process.pid), + pids: processes.map((processInfo) => processInfo.pid), processNames: [executable.executableName], }; } diff --git a/src/platforms/apple/core/perf-xml.ts b/packages/platform-apple/src/core/perf-xml.ts similarity index 100% rename from src/platforms/apple/core/perf-xml.ts rename to packages/platform-apple/src/core/perf-xml.ts diff --git a/src/platforms/apple/core/perf.ts b/packages/platform-apple/src/core/perf.ts similarity index 96% rename from src/platforms/apple/core/perf.ts rename to packages/platform-apple/src/core/perf.ts index c9bcee516e..b2ade72368 100644 --- a/src/platforms/apple/core/perf.ts +++ b/packages/platform-apple/src/core/perf.ts @@ -1,5 +1,4 @@ -import { promises as fs } from 'node:fs'; -import os from 'node:os'; +import { fs } from '@agent-device/host-kit/filesystem'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { @@ -18,6 +17,7 @@ import { } from '@agent-device/host-kit/command'; import { splitNonEmptyTrimmedLines } from '@agent-device/kernel/record'; import { uniqueStrings } from '@agent-device/kernel/collections'; +import { hostTemporaryDirectory } from '@agent-device/host-kit/environment'; import { IOS_DEVICECTL_DEFAULT_HINT, resolveIosDevicectlHint } from './devicectl.ts'; import type { IosDeviceProcessInfo } from './app-info.ts'; import { resolveIosPhysicalDeviceControl } from './physical-device-control.ts'; @@ -135,7 +135,7 @@ export async function sampleAppleMemoryPerf( const measuredAt = new Date().toISOString(); return buildAppleMemoryPerfSample({ - residentMemoryKb: processes.reduce((total, process) => total + process.rssKb, 0), + residentMemoryKb: processes.reduce((total, processInfo) => total + processInfo.rssKb, 0), measuredAt, matchedProcesses: [executable.executableName], memoryMethod: APPLE_MEMORY_SAMPLE_METHOD, @@ -160,15 +160,15 @@ export async function captureAppleMemorySnapshot( const target = await resolveAppleMemorySnapshotTarget(device, appBundleId, support); if (target.available === false) return target; - const { process } = target; + const { process: processInfo } = target; await fs.mkdir(path.dirname(outPath), { recursive: true }); const hadLocalArtifact = await fileExists(outPath); let result: ExecResult; try { - result = await runAppleMemorySnapshotTool(device, outPath, process.pid); + result = await runAppleMemorySnapshotTool(device, outPath, processInfo.pid); } catch (error) { await cleanupLocalArtifact(outPath, hadLocalArtifact); - throw annotateAppleMemorySnapshotToolError(device, appBundleId, process, outPath, error); + throw annotateAppleMemorySnapshotToolError(device, appBundleId, processInfo, outPath, error); } if (result.exitCode !== 0) { await cleanupLocalArtifact(outPath, hadLocalArtifact); @@ -179,8 +179,8 @@ export async function captureAppleMemorySnapshot( execFailureDetails(result, { kind: 'memgraph', appBundleId, - pid: process.pid, - processName: path.basename(readProcessCommandToken(process.command)), + pid: processInfo.pid, + processName: path.basename(readProcessCommandToken(processInfo.command)), path: outPath, hint: resolveAppleMemorySnapshotHint(device, result.stdout, result.stderr), }), @@ -193,7 +193,7 @@ export async function captureAppleMemorySnapshot( throw new AppError('COMMAND_FAILED', 'Apple memgraph artifact is missing or empty', { kind: 'memgraph', appBundleId, - pid: process.pid, + pid: processInfo.pid, path: outPath, hint: 'Retry with a writable --out path. If the file is still empty, run with --debug and inspect leaks output.', }); @@ -207,8 +207,8 @@ export async function captureAppleMemorySnapshot( measuredAt: new Date().toISOString(), method: APPLE_MEMGRAPH_SNAPSHOT_METHOD, appBundleId, - pid: process.pid, - processName: path.basename(readProcessCommandToken(process.command)), + pid: processInfo.pid, + processName: path.basename(readProcessCommandToken(processInfo.command)), support, }; } @@ -239,7 +239,7 @@ async function runAppleMemorySnapshotTool( function annotateAppleMemorySnapshotToolError( device: DeviceInfo, appBundleId: string, - process: AppleProcessSample, + processInfo: AppleProcessSample, outPath: string, error: unknown, ): AppError { @@ -252,8 +252,8 @@ function annotateAppleMemorySnapshotToolError( ...details, kind: 'memgraph', appBundleId, - pid: process.pid, - processName: path.basename(readProcessCommandToken(process.command)), + pid: processInfo.pid, + processName: path.basename(readProcessCommandToken(processInfo.command)), path: outPath, hint: resolveAppleMemorySnapshotHint( device, @@ -272,8 +272,8 @@ function annotateAppleMemorySnapshotToolError( { kind: 'memgraph', appBundleId, - pid: process.pid, - processName: path.basename(readProcessCommandToken(process.command)), + pid: processInfo.pid, + processName: path.basename(readProcessCommandToken(processInfo.command)), path: outPath, hint: 'Retry perf memory snapshot. If it still fails, run with --debug and inspect leaks output.', }, @@ -316,9 +316,9 @@ export async function sampleAppleFramePerf( hitchesXml: capture.hitchesXml, frameLifetimesXml: capture.frameLifetimesXml, displayInfoXml: capture.displayInfoXml, - processIds: processes.map((process) => process.pid), + processIds: processes.map((processInfo) => processInfo.pid), processNames: uniqueStrings( - processes.map((process) => path.basename(fileURLToPath(process.executable))), + processes.map((processInfo) => path.basename(fileURLToPath(processInfo.executable))), ), windowStartedAt: capture.windowStartedAt, windowEndedAt: capture.windowEndedAt, @@ -420,7 +420,9 @@ async function captureIosDeviceFramePerf( appBundleId: string, processes: IosDeviceProcessInfo[], ): Promise { - const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'agent-device-ios-frame-perf-')); + const tempDir = await fs.mkdtemp( + path.join(hostTemporaryDirectory(), 'agent-device-ios-frame-perf-'), + ); const tracePath = path.join(tempDir, 'animation-hitches.trace'); const hitchesPath = path.join(tempDir, 'hitches.xml'); const frameLifetimesPath = path.join(tempDir, 'frame-lifetimes.xml'); @@ -432,7 +434,7 @@ async function captureIosDeviceFramePerf( tracePath, template: 'Animation Hitches', duration: IOS_DEVICE_FRAME_TRACE_DURATION, - targetPids: processes.map((process) => process.pid), + targetPids: processes.map((processInfo) => processInfo.pid), validateTraceOutput: true, failureMessage: `Failed to record iOS frame-health sample for ${appBundleId}`, }); @@ -803,7 +805,7 @@ async function captureIosDevicePerfTable( device: DeviceInfo, appBundleId: string, ): Promise { - const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'agent-device-ios-perf-')); + const tempDir = await fs.mkdtemp(path.join(hostTemporaryDirectory(), 'agent-device-ios-perf-')); const tracePath = path.join(tempDir, 'sample.trace'); const exportPath = path.join(tempDir, 'activity-monitor-process-live.xml'); try { @@ -841,9 +843,9 @@ function summarizeIosDeviceMemorySnapshot( residentMemoryBytes: number | null; matchedProcesses: string[]; } { - const processIds = new Set(processes.map((process) => process.pid)); + const processIds = new Set(processes.map((processInfo) => processInfo.pid)); const processNames = new Set( - processes.map((process) => path.basename(fileURLToPath(process.executable))), + processes.map((processInfo) => path.basename(fileURLToPath(processInfo.executable))), ); const matchedSamples = samples.filter( (sample) => processIds.has(sample.pid) || processNames.has(sample.processName), @@ -959,8 +961,8 @@ export async function readAppleProcessSamples( const result = isMacOs(device) ? await runAppleToolCommand('ps', args, { timeoutMs: APPLE_PERF_TIMEOUT_MS }) : await runAppleSimulatorProcessCommand(args); - return parseApplePsOutput(result.stdout).filter((process) => - matchesAppleExecutableProcess(process.command, executable), + return parseApplePsOutput(result.stdout).filter((processInfo) => + matchesAppleExecutableProcess(processInfo.command, executable), ); } @@ -970,8 +972,8 @@ async function resolveAppleMemorySnapshotProcess( executable: { executableName: string; executablePath?: string }, ): Promise { const processes = await readAppleProcessSamples(device, executable); - const process = processes.sort((left, right) => right.rssKb - left.rssKb)[0]; - if (process) return process; + const processInfo = processes.sort((left, right) => right.rssKb - left.rssKb)[0]; + if (processInfo) return processInfo; throw new AppError('COMMAND_FAILED', `No running process found for ${appBundleId}`, { kind: 'memgraph', appBundleId, diff --git a/src/platforms/apple/core/physical-device-apps.ts b/packages/platform-apple/src/core/physical-device-apps.ts similarity index 100% rename from src/platforms/apple/core/physical-device-apps.ts rename to packages/platform-apple/src/core/physical-device-apps.ts diff --git a/src/platforms/apple/core/physical-device-constants.ts b/packages/platform-apple/src/core/physical-device-constants.ts similarity index 100% rename from src/platforms/apple/core/physical-device-constants.ts rename to packages/platform-apple/src/core/physical-device-constants.ts diff --git a/src/platforms/apple/core/physical-device-control.ts b/packages/platform-apple/src/core/physical-device-control.ts similarity index 98% rename from src/platforms/apple/core/physical-device-control.ts rename to packages/platform-apple/src/core/physical-device-control.ts index 5cab541eaa..14416fae61 100644 --- a/src/platforms/apple/core/physical-device-control.ts +++ b/packages/platform-apple/src/core/physical-device-control.ts @@ -20,10 +20,7 @@ import { IOS_DEVICE_READY_COMMAND_TIMEOUT_BUFFER_MS, IOS_DEVICE_READY_TIMEOUT_MS, } from './physical-device-constants.ts'; -import type { - AppleRunnerCommandExecutor, - AppleRunnerCommandOptions, -} from '@agent-device/platform-apple/runner'; +import type { AppleRunnerCommandExecutor, AppleRunnerCommandOptions } from '../runner/index.ts'; import { captureCoreDeviceScreenshot, captureXctestDeviceScreenshot, diff --git a/src/platforms/apple/core/physical-device-coredevice.ts b/packages/platform-apple/src/core/physical-device-coredevice.ts similarity index 96% rename from src/platforms/apple/core/physical-device-coredevice.ts rename to packages/platform-apple/src/core/physical-device-coredevice.ts index 0dfebfa587..0cab7a7144 100644 --- a/src/platforms/apple/core/physical-device-coredevice.ts +++ b/packages/platform-apple/src/core/physical-device-coredevice.ts @@ -1,9 +1,9 @@ -import { promises as fs } from 'node:fs'; -import os from 'node:os'; +import { fs } from '@agent-device/host-kit/filesystem'; import path from 'node:path'; import type { DeviceInfo } from '@agent-device/kernel/device'; import { AppError } from '@agent-device/kernel/errors'; import { execFailureDetails } from '@agent-device/host-kit/command'; +import { hostProcessId, hostTemporaryDirectory } from '@agent-device/host-kit/environment'; import { IOS_DEVICECTL_DEFAULT_HINT, resolveIosDevicectlHint, @@ -156,8 +156,8 @@ async function runCoreDeviceDetails( parsed: { parsed: boolean; outcome?: string; tunnelState?: string; tunnelIp?: string }; }> { const jsonPath = path.join( - os.tmpdir(), - `agent-device-coredevice-info-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}.json`, + hostTemporaryDirectory(), + `agent-device-coredevice-info-${hostProcessId()}-${Date.now()}-${Math.random().toString(36).slice(2)}.json`, ); const timeoutSeconds = Math.max(1, Math.ceil(timeoutMs / 1000)); try { diff --git a/src/platforms/apple/core/physical-device-files.ts b/packages/platform-apple/src/core/physical-device-files.ts similarity index 95% rename from src/platforms/apple/core/physical-device-files.ts rename to packages/platform-apple/src/core/physical-device-files.ts index 2ccb3c49a9..d9ffd0978b 100644 --- a/src/platforms/apple/core/physical-device-files.ts +++ b/packages/platform-apple/src/core/physical-device-files.ts @@ -2,7 +2,7 @@ import type { DeviceInfo } from '@agent-device/kernel/device'; import { AppError } from '@agent-device/kernel/errors'; import { Deadline } from '@agent-device/host-kit/retry'; import { IOS_RUNNER_SCREENSHOT_COPY_TIMEOUT_MS } from './config.ts'; -import { IOS_RUNNER_CONTAINER_BUNDLE_IDS } from '@agent-device/platform-apple/runner'; +import { IOS_RUNNER_CONTAINER_BUNDLE_IDS } from '../runner/index.ts'; import { runXcrun } from './tool-provider.ts'; export async function copyCoreDeviceRunnerFile( diff --git a/src/platforms/apple/core/physical-device-screenshot.ts b/packages/platform-apple/src/core/physical-device-screenshot.ts similarity index 96% rename from src/platforms/apple/core/physical-device-screenshot.ts rename to packages/platform-apple/src/core/physical-device-screenshot.ts index 44f3d79b04..8d8c8c35c4 100644 --- a/src/platforms/apple/core/physical-device-screenshot.ts +++ b/packages/platform-apple/src/core/physical-device-screenshot.ts @@ -1,13 +1,10 @@ -import { promises as fs } from 'node:fs'; +import { fs } from '@agent-device/host-kit/filesystem'; import type { DeviceInfo } from '@agent-device/kernel/device'; import { AppError } from '@agent-device/kernel/errors'; import { emitDiagnostic } from '@agent-device/host-kit/diagnostics'; import { runIosDevicectl } from './devicectl.ts'; import { copyCoreDeviceRunnerFile } from './physical-device-files.ts'; -import type { - AppleRunnerCommandExecutor, - AppleRunnerCommandOptions, -} from '@agent-device/platform-apple/runner'; +import type { AppleRunnerCommandExecutor, AppleRunnerCommandOptions } from '../runner/index.ts'; import { appleToolFailureText, extractAppleToolErrorMeta } from './tool-diagnostics.ts'; export type IosPhysicalDeviceScreenshotOptions = { diff --git a/src/platforms/apple/core/plist-xml.ts b/packages/platform-apple/src/core/plist-xml.ts similarity index 100% rename from src/platforms/apple/core/plist-xml.ts rename to packages/platform-apple/src/core/plist-xml.ts diff --git a/src/platforms/apple/core/plist.ts b/packages/platform-apple/src/core/plist.ts similarity index 96% rename from src/platforms/apple/core/plist.ts rename to packages/platform-apple/src/core/plist.ts index 5ccdd24345..3d6d56c70b 100644 --- a/src/platforms/apple/core/plist.ts +++ b/packages/platform-apple/src/core/plist.ts @@ -1,4 +1,4 @@ -import { promises as fs } from 'node:fs'; +import { fs } from '@agent-device/host-kit/filesystem'; import { parseXmlDocumentSync } from '@agent-device/xml'; import { readApplePlistJson, runAppleToolCommand } from './tool-provider.ts'; import { visitXmlPlistEntries } from './plist-xml.ts'; diff --git a/src/platforms/apple/core/runner-client.ts b/packages/platform-apple/src/core/runner-client.ts similarity index 92% rename from src/platforms/apple/core/runner-client.ts rename to packages/platform-apple/src/core/runner-client.ts index 7174d604c4..5c1f1f8aab 100644 --- a/src/platforms/apple/core/runner-client.ts +++ b/packages/platform-apple/src/core/runner-client.ts @@ -1,7 +1,4 @@ -import { - createAppleRunnerClient, - type AppleRunnerClient, -} from '@agent-device/platform-apple/runner/client'; +import { createAppleRunnerClient, type AppleRunnerClient } from '../runner/client.ts'; import { appleRunnerHost } from './runner-host.ts'; /** @@ -50,7 +47,5 @@ export const readStaleRunnerLease: AppleRunnerClient['readStaleRunnerLease'] = client.readStaleRunnerLease; export const verifyLeaseRunnerPidIdentity: AppleRunnerClient['verifyLeaseRunnerPidIdentity'] = client.verifyLeaseRunnerPidIdentity; -export const runnerLeaseCleanupAdapter: AppleRunnerClient['runnerLeaseCleanupAdapter'] = - client.runnerLeaseCleanupAdapter; export const applyXctestRunnerAppIconFromDerivedPath: AppleRunnerClient['applyXctestRunnerAppIconFromDerivedPath'] = client.applyXctestRunnerAppIconFromDerivedPath; diff --git a/src/platforms/apple/core/runner-host.ts b/packages/platform-apple/src/core/runner-host.ts similarity index 97% rename from src/platforms/apple/core/runner-host.ts rename to packages/platform-apple/src/core/runner-host.ts index 2dd5a139cb..1f7ec6a930 100644 --- a/src/platforms/apple/core/runner-host.ts +++ b/packages/platform-apple/src/core/runner-host.ts @@ -1,4 +1,4 @@ -import type { AppleRunnerHost } from '@agent-device/platform-apple/runner'; +import type { AppleRunnerHost } from '../runner/index.ts'; import { publishFileSync, acquireProcessLock } from '@agent-device/host-kit/file'; import { resolveIosSimulatorDeviceSetPath } from '@agent-device/kernel/device-isolation'; diff --git a/src/platforms/apple/core/runner-owner-state.ts b/packages/platform-apple/src/core/runner-owner-state.ts similarity index 100% rename from src/platforms/apple/core/runner-owner-state.ts rename to packages/platform-apple/src/core/runner-owner-state.ts diff --git a/src/platforms/apple/core/runner-selector-query.ts b/packages/platform-apple/src/core/runner-selector-query.ts similarity index 89% rename from src/platforms/apple/core/runner-selector-query.ts rename to packages/platform-apple/src/core/runner-selector-query.ts index 539d0b76ca..f33da420b7 100644 --- a/src/platforms/apple/core/runner-selector-query.ts +++ b/packages/platform-apple/src/core/runner-selector-query.ts @@ -1,5 +1,5 @@ import type { ElementSelectorKey } from '@agent-device/contracts/interactor-types'; -import type { AppleRunnerCommandOptions } from '@agent-device/platform-apple/runner'; +import type { AppleRunnerCommandOptions } from '../runner/index.ts'; import type { DeviceInfo } from '@agent-device/kernel/device'; import { runAppleRunnerCommand } from './runner-client.ts'; diff --git a/src/platforms/apple/core/screenshot-status-bar.ts b/packages/platform-apple/src/core/screenshot-status-bar.ts similarity index 100% rename from src/platforms/apple/core/screenshot-status-bar.ts rename to packages/platform-apple/src/core/screenshot-status-bar.ts diff --git a/src/platforms/apple/core/screenshot.ts b/packages/platform-apple/src/core/screenshot.ts similarity index 99% rename from src/platforms/apple/core/screenshot.ts rename to packages/platform-apple/src/core/screenshot.ts index 658d0d8cbd..54c22078bc 100644 --- a/src/platforms/apple/core/screenshot.ts +++ b/packages/platform-apple/src/core/screenshot.ts @@ -1,4 +1,4 @@ -import { promises as fs } from 'node:fs'; +import { fs } from '@agent-device/host-kit/filesystem'; import path from 'node:path'; import { isMacOs, type DeviceInfo } from '@agent-device/kernel/device'; import { type ExecOptions } from '@agent-device/host-kit/command'; @@ -22,7 +22,7 @@ import { runAppleRunnerCommand } from './runner-client.ts'; import { IOS_RUNNER_CONTAINER_BUNDLE_IDS, type AppleRunnerCommandOptions, -} from '@agent-device/platform-apple/runner'; +} from '../runner/index.ts'; import { prepareSimulatorStatusBarForScreenshot } from './screenshot-status-bar.ts'; import { ensureBootedSimulator } from './simulator.ts'; import { runSimctlForDevice } from './simctl.ts'; diff --git a/src/platforms/apple/core/scroll.ts b/packages/platform-apple/src/core/scroll.ts similarity index 100% rename from src/platforms/apple/core/scroll.ts rename to packages/platform-apple/src/core/scroll.ts diff --git a/src/platforms/apple/core/settings-parsing.ts b/packages/platform-apple/src/core/settings-parsing.ts similarity index 100% rename from src/platforms/apple/core/settings-parsing.ts rename to packages/platform-apple/src/core/settings-parsing.ts diff --git a/src/platforms/apple/core/simctl.ts b/packages/platform-apple/src/core/simctl.ts similarity index 100% rename from src/platforms/apple/core/simctl.ts rename to packages/platform-apple/src/core/simctl.ts diff --git a/src/platforms/apple/core/simulator.ts b/packages/platform-apple/src/core/simulator.ts similarity index 100% rename from src/platforms/apple/core/simulator.ts rename to packages/platform-apple/src/core/simulator.ts diff --git a/src/platforms/apple/core/tool-diagnostics.ts b/packages/platform-apple/src/core/tool-diagnostics.ts similarity index 100% rename from src/platforms/apple/core/tool-diagnostics.ts rename to packages/platform-apple/src/core/tool-diagnostics.ts diff --git a/src/platforms/apple/core/tool-provider-types.ts b/packages/platform-apple/src/core/tool-provider-types.ts similarity index 100% rename from src/platforms/apple/core/tool-provider-types.ts rename to packages/platform-apple/src/core/tool-provider-types.ts diff --git a/src/platforms/apple/core/tool-provider.ts b/packages/platform-apple/src/core/tool-provider.ts similarity index 100% rename from src/platforms/apple/core/tool-provider.ts rename to packages/platform-apple/src/core/tool-provider.ts diff --git a/src/platforms/apple/doctor.ts b/packages/platform-apple/src/doctor.ts similarity index 95% rename from src/platforms/apple/doctor.ts rename to packages/platform-apple/src/doctor.ts index 0379c196e6..15104f988a 100644 --- a/src/platforms/apple/doctor.ts +++ b/packages/platform-apple/src/doctor.ts @@ -2,6 +2,7 @@ import { isIosFamily, type DeviceInfo } from '@agent-device/kernel/device'; import type { DoctorCheck } from '@agent-device/contracts/observability'; import type { HostDiagnosticsContext } from '@agent-device/contracts/host-diagnostics'; import { commandFirstLine } from '@agent-device/provision-kit/toolchain-probe'; +import { hostPlatform } from '@agent-device/host-kit/environment'; import { hasCachedAppleRunnerArtifact, prewarmAppleRunnerCache } from './core/runner-client.ts'; type AppleToolchainProbe = { @@ -31,7 +32,7 @@ export async function appleRunnerWarmupCheck( context: HostDiagnosticsContext, ): Promise { if (!isIosFamily(device) || device.kind !== 'simulator') return undefined; - if (process.platform !== 'darwin' || context.isProviderDevice(device)) return undefined; + if (hostPlatform() !== 'darwin' || context.isProviderDevice(device)) return undefined; context.emitProgress(`Checking iOS runner build cache (${device.name})...`); if (await hasCachedAppleRunnerArtifact(device)) { return { diff --git a/packages/platform-apple/src/index.test.ts b/packages/platform-apple/src/index.test.ts new file mode 100644 index 0000000000..a709942bfa --- /dev/null +++ b/packages/platform-apple/src/index.test.ts @@ -0,0 +1,15 @@ +import assert from 'node:assert/strict'; +import { test } from 'vitest'; +import { createLocalAppleToolProvider } from './index.ts'; + +test('lazy Apple tool provider preserves the local semantic operation shape', () => { + const provider = createLocalAppleToolProvider(); + + assert.equal(typeof provider.runCommand, 'function'); + assert.equal(typeof provider.whichCommand, 'function'); + assert.equal(typeof provider.simctl?.run, 'function'); + assert.equal(typeof provider.devicectl?.run, 'function'); + assert.equal(typeof provider.plist?.readJson, 'function'); + assert.equal(typeof provider.macosHost?.openBundle, 'function'); + assert.equal(typeof provider.macosHost?.listApps, 'function'); +}); diff --git a/packages/platform-apple/src/index.ts b/packages/platform-apple/src/index.ts index 1b292135cf..6527df284e 100644 --- a/packages/platform-apple/src/index.ts +++ b/packages/platform-apple/src/index.ts @@ -4,6 +4,9 @@ import type { } from '@agent-device/contracts/platform-module'; import type { PlatformRuntimeModule } from '@agent-device/contracts/platform-runtime-operations'; import type { DeviceShutdownRuntimeDependencies } from '@agent-device/contracts/device-shutdown-runtime'; +import type { PlatformPlugin } from '@agent-device/contracts/platform-plugin'; +import type { DeviceInfo } from '@agent-device/kernel/device'; +import type { RunnerContext } from '@agent-device/contracts/interactor-types'; const metadata = Object.freeze({ family: 'apple', @@ -32,3 +35,430 @@ export async function loadShutdownRuntime( const { createAppleShutdownRuntime } = await import('./shutdown/runtime.ts'); return createAppleShutdownRuntime(dependencies); } + +export const appleToolchainCheck = async ( + ...args: Parameters<(typeof import('./doctor.ts'))['appleToolchainCheck']> +) => (await import('./doctor.ts')).appleToolchainCheck(...args); +export const appleRunnerWarmupCheck = async ( + ...args: Parameters<(typeof import('./doctor.ts'))['appleRunnerWarmupCheck']> +) => (await import('./doctor.ts')).appleRunnerWarmupCheck(...args); + +export const applePlugin = { + id: 'apple', + platforms: ['apple'], + familySelector: 'apple', + providers: { platformGatedResolvers: ['appleRunnerProvider', 'appleToolProvider'] }, + createInteractor: async (device: DeviceInfo, runner: RunnerContext) => { + const { createAppleInteractor } = await import('./interactor.ts'); + return createAppleInteractor(device, runner); + }, +} as const satisfies PlatformPlugin; + +export type { + AppleMacOsHelperProvider, + AppleMacOsHostProvider, + ApplePlistProvider, + AppleToolAvailabilityChecker, + AppleToolCommandExecutor, + AppleToolProvider, + AppleToolSubcommandExecutor, + AppleXcrunToolProvider, +} from './core/tool-provider.ts'; +export type { + IosPhysicalDeviceBackend, + IosPhysicalDeviceControl, + IosPhysicalDeviceTunnel, +} from './core/physical-device-control.ts'; +export type { PreparedIosInstallArtifact } from './core/install-artifact.ts'; +export type { + AppleMemoryPerfSample, + AppleMemorySnapshotResult, + AppleProcessSample, +} from './core/perf.ts'; +export type { + AppleXctraceCpuProfileReport, + AppleXctracePerfCapture, + AppleXctracePerfMode, + AppleXctracePerfResult, +} from './core/perf-xctrace.ts'; +export type { MacOsPermissionTarget, MacOsSnapshotNode } from './os/macos/helper.ts'; + +type AppleToolProvider = import('./core/tool-provider.ts').AppleToolProvider; +type AppleInteractor = import('@agent-device/contracts/interactor-types').Interactor; +type AppleInteractorFactory = (typeof import('./interactor.ts'))['createAppleInteractor']; + +export const buildAppNotInstalledError = async ( + ...args: Parameters< + (typeof import('./core/app-resolution-error.ts'))['buildAppNotInstalledError'] + > +) => (await import('./core/app-resolution-error.ts')).buildAppNotInstalledError(...args); +export const detectSoleRunningIosSimulatorApp = async ( + ...args: Parameters< + (typeof import('./core/app-resolution.ts'))['detectSoleRunningIosSimulatorApp'] + > +) => (await import('./core/app-resolution.ts')).detectSoleRunningIosSimulatorApp(...args); +export const findIosSimulatorInstalledApp = async ( + ...args: Parameters<(typeof import('./core/app-resolution.ts'))['findIosSimulatorInstalledApp']> +) => (await import('./core/app-resolution.ts')).findIosSimulatorInstalledApp(...args); +export async function invalidateIosAppResolutionCache( + device: Parameters< + (typeof import('./core/app-resolution.ts'))['invalidateIosAppResolutionCache'] + >[0], + operation: () => Promise, +): Promise { + const { invalidateIosAppResolutionCache: invalidate } = await import('./core/app-resolution.ts'); + return await invalidate(device, operation); +} +export const listIosApps = async ( + ...args: Parameters<(typeof import('./core/app-resolution.ts'))['listIosApps']> +) => (await import('./core/app-resolution.ts')).listIosApps(...args); +export const resolveIosApp = async ( + ...args: Parameters<(typeof import('./core/app-resolution.ts'))['resolveIosApp']> +) => (await import('./core/app-resolution.ts')).resolveIosApp(...args); +export const resolveIosAppAlias = async ( + ...args: Parameters<(typeof import('./core/app-resolution.ts'))['resolveIosAppAlias']> +) => (await import('./core/app-resolution.ts')).resolveIosAppAlias(...args); +export const resolveIosSimulatorDeepLinkBundleId = async ( + ...args: Parameters< + (typeof import('./core/app-resolution.ts'))['resolveIosSimulatorDeepLinkBundleId'] + > +) => (await import('./core/app-resolution.ts')).resolveIosSimulatorDeepLinkBundleId(...args); + +export const closeIosApp = async ( + ...args: Parameters<(typeof import('./core/app-launch.ts'))['closeIosApp']> +) => (await import('./core/app-launch.ts')).closeIosApp(...args); +export const openIosApp = async ( + ...args: Parameters<(typeof import('./core/app-launch.ts'))['openIosApp']> +) => (await import('./core/app-launch.ts')).openIosApp(...args); +export const openIosDevice = async ( + ...args: Parameters<(typeof import('./core/app-launch.ts'))['openIosDevice']> +) => (await import('./core/app-launch.ts')).openIosDevice(...args); +export const screenshotIos = async ( + ...args: Parameters<(typeof import('./core/screenshot.ts'))['screenshotIos']> +) => (await import('./core/screenshot.ts')).screenshotIos(...args); +export const setIosSetting = async ( + ...args: Parameters<(typeof import('./core/app-settings.ts'))['setIosSetting']> +) => (await import('./core/app-settings.ts')).setIosSetting(...args); +export const pushIosNotification = async ( + ...args: Parameters<(typeof import('./core/app-device-io.ts'))['pushIosNotification']> +) => (await import('./core/app-device-io.ts')).pushIosNotification(...args); +export const readIosClipboardText = async ( + ...args: Parameters<(typeof import('./core/app-device-io.ts'))['readIosClipboardText']> +) => (await import('./core/app-device-io.ts')).readIosClipboardText(...args); +export const writeIosClipboardText = async ( + ...args: Parameters<(typeof import('./core/app-device-io.ts'))['writeIosClipboardText']> +) => (await import('./core/app-device-io.ts')).writeIosClipboardText(...args); + +export const readAppleAlert = async ( + ...args: Parameters<(typeof import('./alert.ts'))['readAppleAlert']> +) => (await import('./alert.ts')).readAppleAlert(...args); +export const awaitAppleAlert = async ( + ...args: Parameters<(typeof import('./alert.ts'))['awaitAppleAlert']> +) => (await import('./alert.ts')).awaitAppleAlert(...args); +export const actOnAppleAlert = async ( + ...args: Parameters<(typeof import('./alert.ts'))['actOnAppleAlert']> +) => (await import('./alert.ts')).actOnAppleAlert(...args); + +export function createAppleInteractor( + ...args: Parameters +): AppleInteractor { + let implementation: Promise | undefined; + const load = (): Promise => + (implementation ??= import('./interactor.ts').then(({ createAppleInteractor: create }) => + create(...args), + )); + const method = (name: K): AppleInteractor[K] => + ((...methodArgs: unknown[]) => + load().then((interactor) => { + const operation = interactor[name]; + if (typeof operation !== 'function') { + throw new TypeError(`Apple interactor method '${String(name)}' is unavailable`); + } + return Reflect.apply(operation, interactor, methodArgs); + })) as AppleInteractor[K]; + + return { + open: method('open'), + openDevice: method('openDevice'), + close: method('close'), + tap: method('tap'), + pressPoint: method('pressPoint'), + tapElementSelector: method('tapElementSelector'), + doubleTap: method('doubleTap'), + longPress: method('longPress'), + focus: method('focus'), + type: method('type'), + fill: method('fill'), + scroll: method('scroll'), + screenshot: method('screenshot'), + snapshot: method('snapshot'), + readTextAtPoint: method('readTextAtPoint'), + findText: method('findText'), + findSelector: method('findSelector'), + gestureViewport: method('gestureViewport'), + back: method('back'), + home: method('home'), + setOrientation: method('setOrientation'), + performGesture: method('performGesture'), + appSwitcher: method('appSwitcher'), + tvRemote: method('tvRemote'), + keyboardDismiss: method('keyboardDismiss'), + keyboardEnter: method('keyboardEnter'), + readClipboard: method('readClipboard'), + writeClipboard: method('writeClipboard'), + setSetting: method('setSetting'), + readAlert: method('readAlert'), + awaitAlert: method('awaitAlert'), + acceptAlert: method('acceptAlert'), + dismissAlert: method('dismissAlert'), + }; +} +export const resolveAppleBackRunnerCommand = async ( + ...args: Parameters<(typeof import('./interactions.ts'))['resolveAppleBackRunnerCommand']> +) => (await import('./interactions.ts')).resolveAppleBackRunnerCommand(...args); + +export const ensureBootedSimulator = async ( + ...args: Parameters<(typeof import('./core/simulator.ts'))['ensureBootedSimulator']> +) => (await import('./core/simulator.ts')).ensureBootedSimulator(...args); +export const shutdownSimulator = async ( + ...args: Parameters<(typeof import('./core/simulator.ts'))['shutdownSimulator']> +) => (await import('./core/simulator.ts')).shutdownSimulator(...args); +export const wasSimulatorRecentlyObservedBooted = async ( + ...args: Parameters<(typeof import('./core/simulator.ts'))['wasSimulatorRecentlyObservedBooted']> +) => (await import('./core/simulator.ts')).wasSimulatorRecentlyObservedBooted(...args); +export const markSimulatorBooted = async ( + ...args: Parameters<(typeof import('./core/simulator.ts'))['markSimulatorBooted']> +) => (await import('./core/simulator.ts')).markSimulatorBooted(...args); +export const buildSimctlArgs = async ( + ...args: Parameters<(typeof import('./core/simctl.ts'))['buildSimctlArgs']> +) => (await import('./core/simctl.ts')).buildSimctlArgs(...args); +export const buildSimctlArgsForDevice = async ( + ...args: Parameters<(typeof import('./core/simctl.ts'))['buildSimctlArgsForDevice']> +) => (await import('./core/simctl.ts')).buildSimctlArgsForDevice(...args); + +export function createLocalAppleToolProvider( + provider: Partial = {}, +): AppleToolProvider { + let implementation: Promise | undefined; + const load = (): Promise => + (implementation ??= import('./core/tool-provider.ts').then( + ({ createLocalAppleToolProvider: create }) => create(provider), + )); + + const delegate = unknown>( + select: (loaded: AppleToolProvider) => T | undefined, + override: T | undefined, + ): T => + (override ?? + ((...args: Parameters) => + load().then((loaded) => { + const operation = select(loaded); + if (!operation) { + throw new TypeError('Apple tool provider operation is unavailable'); + } + return operation(...args); + }))) as T; + + const runCommand = delegate((loaded) => loaded.runCommand, provider.runCommand); + const whichCommand = delegate((loaded) => loaded.whichCommand, provider.whichCommand); + const simctl = provider.simctl ?? { + run: delegate((loaded) => loaded.simctl?.run, undefined), + }; + const devicectl = provider.devicectl ?? { + run: delegate((loaded) => loaded.devicectl?.run, undefined), + }; + const plist = provider.plist ?? { + readJson: delegate((loaded) => loaded.plist?.readJson, undefined), + }; + const macosHost = provider.macosHost ?? { + openBundle: delegate((loaded) => loaded.macosHost?.openBundle, undefined), + openTarget: delegate((loaded) => loaded.macosHost?.openTarget, undefined), + readClipboard: delegate((loaded) => loaded.macosHost?.readClipboard, undefined), + writeClipboard: delegate((loaded) => loaded.macosHost?.writeClipboard, undefined), + readDarkMode: delegate((loaded) => loaded.macosHost?.readDarkMode, undefined), + setDarkMode: delegate((loaded) => loaded.macosHost?.setDarkMode, undefined), + listApps: delegate((loaded) => loaded.macosHost?.listApps, undefined), + }; + + return { + ...provider, + runCommand, + whichCommand, + simctl, + devicectl, + plist, + macosHost, + }; +} +export const resolveAppleToolProvider = async ( + ...args: Parameters<(typeof import('./core/tool-provider.ts'))['resolveAppleToolProvider']> +) => (await import('./core/tool-provider.ts')).resolveAppleToolProvider(...args); +export const runAppleToolCommand = async ( + ...args: Parameters<(typeof import('./core/tool-provider.ts'))['runAppleToolCommand']> +) => (await import('./core/tool-provider.ts')).runAppleToolCommand(...args); +export const runXcrun = async ( + ...args: Parameters<(typeof import('./core/tool-provider.ts'))['runXcrun']> +) => (await import('./core/tool-provider.ts')).runXcrun(...args); +export const readApplePlistJson = async ( + ...args: Parameters<(typeof import('./core/tool-provider.ts'))['readApplePlistJson']> +) => (await import('./core/tool-provider.ts')).readApplePlistJson(...args); +export const hasScopedAppleToolProvider = async ( + ...args: Parameters<(typeof import('./core/tool-provider.ts'))['hasScopedAppleToolProvider']> +) => (await import('./core/tool-provider.ts')).hasScopedAppleToolProvider(...args); +export async function withAppleToolProvider( + provider: Parameters<(typeof import('./core/tool-provider.ts'))['withAppleToolProvider']>[0], + task: () => Promise, +): Promise { + const { withAppleToolProvider: runWithAppleToolProvider } = + await import('./core/tool-provider.ts'); + return await runWithAppleToolProvider(provider, task); +} + +export const resolveIosPhysicalDeviceControl = async ( + ...args: Parameters< + (typeof import('./core/physical-device-control.ts'))['resolveIosPhysicalDeviceControl'] + > +) => (await import('./core/physical-device-control.ts')).resolveIosPhysicalDeviceControl(...args); +export const prepareIosInstallArtifact = async ( + ...args: Parameters<(typeof import('./core/install-artifact.ts'))['prepareIosInstallArtifact']> +) => (await import('./core/install-artifact.ts')).prepareIosInstallArtifact(...args); +export const readIosBundleInfo = async ( + ...args: Parameters<(typeof import('./core/install-artifact.ts'))['readIosBundleInfo']> +) => (await import('./core/install-artifact.ts')).readIosBundleInfo(...args); +export const symbolicateCrashArtifact = async ( + ...args: Parameters<(typeof import('./core/debug-symbols.ts'))['symbolicateCrashArtifact']> +) => (await import('./core/debug-symbols.ts')).symbolicateCrashArtifact(...args); +export const queryAppleRunnerSelector = async ( + ...args: Parameters< + (typeof import('./core/runner-selector-query.ts'))['queryAppleRunnerSelector'] + > +) => (await import('./core/runner-selector-query.ts')).queryAppleRunnerSelector(...args); +export const setRunnerLeaseOwnerStateDir = async ( + ...args: Parameters< + (typeof import('./core/runner-owner-state.ts'))['setRunnerLeaseOwnerStateDir'] + > +) => (await import('./core/runner-owner-state.ts')).setRunnerLeaseOwnerStateDir(...args); + +export const resolveFrontmostMacOsApp = async ( + ...args: Parameters<(typeof import('./os/macos/helper.ts'))['resolveFrontmostMacOsApp']> +) => (await import('./os/macos/helper.ts')).resolveFrontmostMacOsApp(...args); +export const runMacOsAlertAction = async ( + ...args: Parameters<(typeof import('./os/macos/helper.ts'))['runMacOsAlertAction']> +) => (await import('./os/macos/helper.ts')).runMacOsAlertAction(...args); +export const startMacOsAudioProbeProcess = async ( + ...args: Parameters<(typeof import('./os/macos/helper.ts'))['startMacOsAudioProbeProcess']> +) => (await import('./os/macos/helper.ts')).startMacOsAudioProbeProcess(...args); +export const captureMacOsSurfaceSnapshot = async ( + ...args: Parameters< + (typeof import('./os/macos/surface-snapshot.ts'))['captureMacOsSurfaceSnapshot'] + > +) => (await import('./os/macos/surface-snapshot.ts')).captureMacOsSurfaceSnapshot(...args); + +export const sampleAppleMemoryPerf = async ( + ...args: Parameters<(typeof import('./core/perf.ts'))['sampleAppleMemoryPerf']> +) => (await import('./core/perf.ts')).sampleAppleMemoryPerf(...args); +export const captureAppleMemorySnapshot = async ( + ...args: Parameters<(typeof import('./core/perf.ts'))['captureAppleMemorySnapshot']> +) => (await import('./core/perf.ts')).captureAppleMemorySnapshot(...args); +export const sampleAppleFramePerf = async ( + ...args: Parameters<(typeof import('./core/perf.ts'))['sampleAppleFramePerf']> +) => (await import('./core/perf.ts')).sampleAppleFramePerf(...args); +export const buildAppleFrameSamplingMetadata = async ( + ...args: Parameters<(typeof import('./core/perf.ts'))['buildAppleFrameSamplingMetadata']> +) => (await import('./core/perf.ts')).buildAppleFrameSamplingMetadata(...args); +export const buildAppleMemorySamplingMetadata = async ( + ...args: Parameters<(typeof import('./core/perf.ts'))['buildAppleMemorySamplingMetadata']> +) => (await import('./core/perf.ts')).buildAppleMemorySamplingMetadata(...args); +export const buildAppleMemorySnapshotSupport = async ( + ...args: Parameters<(typeof import('./core/perf.ts'))['buildAppleMemorySnapshotSupport']> +) => (await import('./core/perf.ts')).buildAppleMemorySnapshotSupport(...args); +export const isRetryableIosDeviceTraceRecordFailure = async ( + ...args: Parameters<(typeof import('./core/perf.ts'))['isRetryableIosDeviceTraceRecordFailure']> +) => (await import('./core/perf.ts')).isRetryableIosDeviceTraceRecordFailure(...args); +export const prepareAppleTraceRecordRetry = async ( + ...args: Parameters<(typeof import('./core/perf.ts'))['prepareAppleTraceRecordRetry']> +) => (await import('./core/perf.ts')).prepareAppleTraceRecordRetry(...args); +export const resolveAppleExecutable = async ( + ...args: Parameters<(typeof import('./core/perf.ts'))['resolveAppleExecutable']> +) => (await import('./core/perf.ts')).resolveAppleExecutable(...args); +export const resolveIosDevicePerfTarget = async ( + ...args: Parameters<(typeof import('./core/perf.ts'))['resolveIosDevicePerfTarget']> +) => (await import('./core/perf.ts')).resolveIosDevicePerfTarget(...args); +export const readAppleProcessSamples = async ( + ...args: Parameters<(typeof import('./core/perf.ts'))['readAppleProcessSamples']> +) => (await import('./core/perf.ts')).readAppleProcessSamples(...args); +export const resolveIosDevicePerfHint = async ( + ...args: Parameters<(typeof import('./core/perf.ts'))['resolveIosDevicePerfHint']> +) => (await import('./core/perf.ts')).resolveIosDevicePerfHint(...args); + +export const startAppleXctracePerfCapture = async ( + ...args: Parameters<(typeof import('./core/perf-xctrace.ts'))['startAppleXctracePerfCapture']> +) => (await import('./core/perf-xctrace.ts')).startAppleXctracePerfCapture(...args); +export const stopAppleXctracePerfCapture = async ( + ...args: Parameters<(typeof import('./core/perf-xctrace.ts'))['stopAppleXctracePerfCapture']> +) => (await import('./core/perf-xctrace.ts')).stopAppleXctracePerfCapture(...args); +export const cleanupAppleXctracePerfCapture = async ( + ...args: Parameters<(typeof import('./core/perf-xctrace.ts'))['cleanupAppleXctracePerfCapture']> +) => (await import('./core/perf-xctrace.ts')).cleanupAppleXctracePerfCapture(...args); +export const writeAppleXctracePerfReport = async ( + ...args: Parameters<(typeof import('./core/perf-xctrace.ts'))['writeAppleXctracePerfReport']> +) => (await import('./core/perf-xctrace.ts')).writeAppleXctracePerfReport(...args); + +export const runAppleRunnerCommand = async ( + ...args: Parameters<(typeof import('./core/runner-client.ts'))['runAppleRunnerCommand']> +) => (await import('./core/runner-client.ts')).runAppleRunnerCommand(...args); +export const notifyIosRunnerAppRelaunched = async ( + ...args: Parameters<(typeof import('./core/runner-client.ts'))['notifyIosRunnerAppRelaunched']> +) => (await import('./core/runner-client.ts')).notifyIosRunnerAppRelaunched(...args); +export const prewarmAppleRunnerCache = async ( + ...args: Parameters<(typeof import('./core/runner-client.ts'))['prewarmAppleRunnerCache']> +) => (await import('./core/runner-client.ts')).prewarmAppleRunnerCache(...args); +export const prewarmIosRunnerSession = async ( + ...args: Parameters<(typeof import('./core/runner-client.ts'))['prewarmIosRunnerSession']> +) => (await import('./core/runner-client.ts')).prewarmIosRunnerSession(...args); +export const prepareIosRunner = async ( + ...args: Parameters<(typeof import('./core/runner-client.ts'))['prepareIosRunner']> +) => (await import('./core/runner-client.ts')).prepareIosRunner(...args); +export const resolveRunnerAppBundleId = async ( + ...args: Parameters<(typeof import('./core/runner-client.ts'))['resolveRunnerAppBundleId']> +) => (await import('./core/runner-client.ts')).resolveRunnerAppBundleId(...args); +export const detachIosSimulatorRunnerSessionsForShutdown = async ( + ...args: Parameters< + (typeof import('./core/runner-client.ts'))['detachIosSimulatorRunnerSessionsForShutdown'] + > +) => (await import('./core/runner-client.ts')).detachIosSimulatorRunnerSessionsForShutdown(...args); +export const getRunnerSessionSnapshot = async ( + ...args: Parameters<(typeof import('./core/runner-client.ts'))['getRunnerSessionSnapshot']> +) => (await import('./core/runner-client.ts')).getRunnerSessionSnapshot(...args); +export const scheduleIosRunnerIdleStop = async ( + ...args: Parameters<(typeof import('./core/runner-client.ts'))['scheduleIosRunnerIdleStop']> +) => (await import('./core/runner-client.ts')).scheduleIosRunnerIdleStop(...args); +export const stopIosRunnerSession = async ( + ...args: Parameters<(typeof import('./core/runner-client.ts'))['stopIosRunnerSession']> +) => (await import('./core/runner-client.ts')).stopIosRunnerSession(...args); +export const stopAllIosRunnerSessions = async ( + ...args: Parameters<(typeof import('./core/runner-client.ts'))['stopAllIosRunnerSessions']> +) => (await import('./core/runner-client.ts')).stopAllIosRunnerSessions(...args); +export const cleanupRunnerLeasesForOwner = async ( + owner: Parameters<(typeof import('./runner/runner-lease.ts'))['cleanupRunnerLeasesForOwner']>[0], +): Promise => { + const { cleanupRunnerLeasesForOwner: cleanup } = await import('./core/runner-client.ts'); + const { runnerLeaseCleanupAdapter } = await import('./runner/runner-disposal.ts'); + await cleanup(owner, runnerLeaseCleanupAdapter); +}; +export const readStaleRunnerLease = async ( + ...args: Parameters<(typeof import('./core/runner-client.ts'))['readStaleRunnerLease']> +) => (await import('./core/runner-client.ts')).readStaleRunnerLease(...args); +export const verifyLeaseRunnerPidIdentity = async ( + ...args: Parameters<(typeof import('./core/runner-client.ts'))['verifyLeaseRunnerPidIdentity']> +) => (await import('./core/runner-client.ts')).verifyLeaseRunnerPidIdentity(...args); +export const runApplePressSeries = async ( + ...args: Parameters<(typeof import('./core/runner-client.ts'))['runApplePressSeries']> +) => (await import('./core/runner-client.ts')).runApplePressSeries(...args); +export const applyXctestRunnerAppIconFromDerivedPath = async ( + ...args: Parameters< + (typeof import('./core/runner-client.ts'))['applyXctestRunnerAppIconFromDerivedPath'] + > +) => (await import('./core/runner-client.ts')).applyXctestRunnerAppIconFromDerivedPath(...args); +export const loadAppleRunnerHost = async () => + (await import('./core/runner-host.ts')).appleRunnerHost; diff --git a/src/platforms/apple/interactions.ts b/packages/platform-apple/src/interactions.ts similarity index 99% rename from src/platforms/apple/interactions.ts rename to packages/platform-apple/src/interactions.ts index e03641a903..ddaada8c12 100644 --- a/src/platforms/apple/interactions.ts +++ b/packages/platform-apple/src/interactions.ts @@ -26,7 +26,7 @@ import { buildRunnerSequenceCommand, parseRunnerSequenceResult, type RunnerCommand, -} from '@agent-device/platform-apple/runner'; +} from './runner/index.ts'; import { materializeIosScrollOptions, normalizeAppleScrollResult, diff --git a/src/platforms/apple/interactor.ts b/packages/platform-apple/src/interactor.ts similarity index 99% rename from src/platforms/apple/interactor.ts rename to packages/platform-apple/src/interactor.ts index 5f95933438..709bb0d1bf 100644 --- a/src/platforms/apple/interactor.ts +++ b/packages/platform-apple/src/interactor.ts @@ -18,7 +18,7 @@ import { withAppleRunnerProvider, type AppleRunnerCommandExecutor, type AppleRunnerProvider, -} from '@agent-device/platform-apple/runner'; +} from './runner/index.ts'; import { toAppleTvRemoteButton } from '@agent-device/contracts/tv-remote'; import type { SessionSurface } from '@agent-device/contracts/session'; import { DEVICE_ROTATIONS, type DeviceRotation } from '@agent-device/contracts/device'; diff --git a/src/platforms/apple/os/macos/apps.test.ts b/packages/platform-apple/src/os/macos/apps.test.ts similarity index 100% rename from src/platforms/apple/os/macos/apps.test.ts rename to packages/platform-apple/src/os/macos/apps.test.ts diff --git a/src/platforms/apple/os/macos/apps.ts b/packages/platform-apple/src/os/macos/apps.ts similarity index 100% rename from src/platforms/apple/os/macos/apps.ts rename to packages/platform-apple/src/os/macos/apps.ts diff --git a/src/platforms/apple/os/macos/desktop-scroll.ts b/packages/platform-apple/src/os/macos/desktop-scroll.ts similarity index 94% rename from src/platforms/apple/os/macos/desktop-scroll.ts rename to packages/platform-apple/src/os/macos/desktop-scroll.ts index a99219958e..c8c7d335c8 100644 --- a/src/platforms/apple/os/macos/desktop-scroll.ts +++ b/packages/platform-apple/src/os/macos/desktop-scroll.ts @@ -1,7 +1,7 @@ import type { RunnerCallOptions, RunnerContext } from '@agent-device/contracts/interactor-types'; import type { ScrollDirection } from '@agent-device/contracts/scroll-gesture'; import type { DeviceInfo } from '@agent-device/kernel/device'; -import type { RunnerCommand } from '@agent-device/platform-apple/runner'; +import type { RunnerCommand } from '../../runner/index.ts'; import { normalizeAppleScrollResultWithResolvedFrame, scrollRunnerFields, diff --git a/src/platforms/apple/os/macos/helper.test.ts b/packages/platform-apple/src/os/macos/helper.test.ts similarity index 100% rename from src/platforms/apple/os/macos/helper.test.ts rename to packages/platform-apple/src/os/macos/helper.test.ts diff --git a/src/platforms/apple/os/macos/helper.ts b/packages/platform-apple/src/os/macos/helper.ts similarity index 95% rename from src/platforms/apple/os/macos/helper.ts rename to packages/platform-apple/src/os/macos/helper.ts index f1143c8450..0597ce4ba0 100644 --- a/src/platforms/apple/os/macos/helper.ts +++ b/packages/platform-apple/src/os/macos/helper.ts @@ -1,9 +1,14 @@ -import { existsSync, promises as fs } from 'node:fs'; +import { existsSync, fs } from '@agent-device/host-kit/filesystem'; import { createHash } from 'node:crypto'; -import os from 'node:os'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { AppError } from '@agent-device/kernel/errors'; +import { + hostHomeDirectory, + hostPlatform, + readHostEnvironmentVariable, + writeHostStderr, +} from '@agent-device/host-kit/environment'; import { resolveExecutableOverridePath, runCmdBackground, @@ -64,7 +69,7 @@ type HelperResult> = HelperSuccess | Helper const MACOS_HELPER_PRODUCT_NAME = 'agent-device-macos-helper'; const MACOS_HELPER_ENV_PATH = 'AGENT_DEVICE_MACOS_HELPER_BIN'; const MACOS_HELPER_INSTALL_ROOT = path.join( - os.homedir(), + hostHomeDirectory(), '.agent-device', 'macos-helper', 'current', @@ -177,7 +182,7 @@ async function readInstalledMacOsHelperFingerprint(): Promise { async function ensureMacOsHelperBinary(): Promise { const configuredPath = await resolveExecutableOverridePath( - process.env[MACOS_HELPER_ENV_PATH], + readHostEnvironmentVariable(MACOS_HELPER_ENV_PATH), MACOS_HELPER_ENV_PATH, ); if (configuredPath) { @@ -198,7 +203,7 @@ async function ensureMacOsHelperBinary(): Promise { } const sourceBinary = resolveMacOsHelperSourceBinaryPath(); - process.stderr.write('agent-device: building macOS helper (first run or helper update)\n'); + writeHostStderr('agent-device: building macOS helper (first run or helper update)\n'); await runAppleToolCommand('swift', ['build', '-c', 'release', '--package-path', packageRoot], { cwd: packageRoot, timeoutMs: 120_000, @@ -217,7 +222,7 @@ async function ensureMacOsHelperBinary(): Promise { } async function resolveMacOsHelperCommandPath(): Promise { - const configuredPath = process.env[MACOS_HELPER_ENV_PATH]?.trim(); + const configuredPath = readHostEnvironmentVariable(MACOS_HELPER_ENV_PATH)?.trim(); if (configuredPath) { const resolvedPath = await resolveExecutableOverridePath(configuredPath, MACOS_HELPER_ENV_PATH); if (resolvedPath) return resolvedPath; @@ -225,7 +230,7 @@ async function resolveMacOsHelperCommandPath(): Promise { if (hasScopedAppleToolProvider()) { return MACOS_HELPER_PRODUCT_NAME; } - if (process.platform !== 'darwin') { + if (hostPlatform() !== 'darwin') { throw new AppError('UNSUPPORTED_PLATFORM', 'macOS helper is only available on macOS'); } return await ensureMacOsHelperBinary(); diff --git a/src/platforms/apple/os/macos/host-provider.ts b/packages/platform-apple/src/os/macos/host-provider.ts similarity index 96% rename from src/platforms/apple/os/macos/host-provider.ts rename to packages/platform-apple/src/os/macos/host-provider.ts index 82f5b6b428..22a37eeaa7 100644 --- a/src/platforms/apple/os/macos/host-provider.ts +++ b/packages/platform-apple/src/os/macos/host-provider.ts @@ -1,5 +1,5 @@ -import { promises as fs } from 'node:fs'; -import os from 'node:os'; +import { fs } from '@agent-device/host-kit/filesystem'; +import { hostHomeDirectory } from '@agent-device/host-kit/environment'; import path from 'node:path'; import type { AppsFilter } from '@agent-device/contracts/device'; import { AppError } from '@agent-device/kernel/errors'; @@ -82,7 +82,7 @@ async function listLocalMacApps( const appRoots = [ '/Applications', '/System/Applications', - path.join(os.homedir(), 'Applications'), + path.join(hostHomeDirectory(), 'Applications'), ]; const appPaths = new Set(); diff --git a/src/platforms/apple/os/macos/surface-snapshot.ts b/packages/platform-apple/src/os/macos/surface-snapshot.ts similarity index 100% rename from src/platforms/apple/os/macos/surface-snapshot.ts rename to packages/platform-apple/src/os/macos/surface-snapshot.ts diff --git a/src/platforms/apple/os/tvos/remote.ts b/packages/platform-apple/src/os/tvos/remote.ts similarity index 93% rename from src/platforms/apple/os/tvos/remote.ts rename to packages/platform-apple/src/os/tvos/remote.ts index ca4fbd96c7..595d87055a 100644 --- a/src/platforms/apple/os/tvos/remote.ts +++ b/packages/platform-apple/src/os/tvos/remote.ts @@ -1,4 +1,4 @@ -import type { RunnerCommand } from '@agent-device/platform-apple/runner'; +import type { RunnerCommand } from '../../runner/index.ts'; // The tvOS Apple-OS leaf (ADR-0009). tvOS has no touch input: it is driven by the // Siri Remote's focus engine, so back/home/scroll navigate focus via XCUIRemote diff --git a/packages/platform-apple/src/runner/host.ts b/packages/platform-apple/src/runner/host.ts index 95af11fff8..23c5de98c5 100644 --- a/packages/platform-apple/src/runner/host.ts +++ b/packages/platform-apple/src/runner/host.ts @@ -10,7 +10,7 @@ import type { XmlNode } from '@agent-device/xml'; * enters through this object: process execution, diagnostics, retry, process * probes, locks, Apple foreground tooling, and physical-device control. The * package never imports root implementation files; the composition root - * (`src/platforms/apple/core/runner-client.ts`) constructs the client with the + * (`packages/platform-apple/src/core/runner-client.ts`) constructs the client with the * real implementations exactly once per process. * * Signatures mirror the root utilities structurally, narrowed to what the @@ -220,20 +220,20 @@ export type AppleRunnerHost = { context?: { platform?: 'ios' | 'android'; phase?: 'boot' | 'connect' | 'transport' }; }): BootFailureReason; bootFailureHint(reason: BootFailureReason): string; - // Apple foreground tooling (src/platforms/apple/core/tool-provider.ts) + // Apple foreground tooling (packages/platform-apple/src/core/tool-provider.ts) runAppleToolCommand(cmd: string, args: string[], options?: ExecOptions): Promise; runXcrun(args: string[], options?: ExecOptions): Promise; readApplePlistJson( plistPath: string, signal?: AbortSignal, ): Promise | null>; - // simctl argument shaping (src/platforms/apple/core/simctl.ts) + // simctl argument shaping (packages/platform-apple/src/core/simctl.ts) buildSimctlArgsForDevice(device: DeviceInfo, args: string[]): string[]; - // Physical-device control routing (src/platforms/apple/core/physical-device-control.ts) + // Physical-device control routing (packages/platform-apple/src/core/physical-device-control.ts) resolveIosPhysicalDeviceControl(device: DeviceInfo): IosPhysicalDeviceRunnerControl; - // XML plist traversal (src/platforms/apple/core/plist-xml.ts) + // XML plist traversal (packages/platform-apple/src/core/plist-xml.ts) visitXmlPlistEntries(nodes: XmlNode[], visitor: (key: string, valueNode: XmlNode) => void): void; - // Daemon-owned lease owner state directory (src/platforms/apple/core/runner-owner-state.ts) + // Daemon-owned lease owner state directory (packages/platform-apple/src/core/runner-owner-state.ts) leaseOwnerStateDir(): string | undefined; }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5a3baa87bb..ae15c68923 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -293,9 +293,15 @@ importers: '@agent-device/contracts': specifier: workspace:* version: link:../contracts + '@agent-device/host-kit': + specifier: workspace:* + version: link:../host-kit '@agent-device/kernel': specifier: workspace:* version: link:../kernel + '@agent-device/provision-kit': + specifier: workspace:* + version: link:../provision-kit '@agent-device/xml': specifier: workspace:* version: link:../xml diff --git a/scripts/__tests__/help-conformance-sample-outputs.test.ts b/scripts/__tests__/help-conformance-sample-outputs.test.ts index 39368a19ea..0d8ab5feef 100644 --- a/scripts/__tests__/help-conformance-sample-outputs.test.ts +++ b/scripts/__tests__/help-conformance-sample-outputs.test.ts @@ -13,7 +13,7 @@ import { SAMPLE_PRODUCERS } from './help-conformance-sample-producers.ts'; // (help-conformance-error-recovery-coverage.test.ts). for (const { name, producer, sample, render } of SAMPLE_PRODUCERS) { - test(`${name} matches ${producer}`, () => { - assert.equal(render(), sample.output); + test(`${name} matches ${producer}`, async () => { + assert.equal(await render(), sample.output); }); } diff --git a/scripts/__tests__/help-conformance-sample-producers.ts b/scripts/__tests__/help-conformance-sample-producers.ts index 80c1c08b28..88e9665d56 100644 --- a/scripts/__tests__/help-conformance-sample-producers.ts +++ b/scripts/__tests__/help-conformance-sample-producers.ts @@ -25,7 +25,7 @@ import { buildDeviceInUseBySessionError } from '../../src/daemon/handlers/sessio import { buildDeviceClaimConflictError } from '../../src/daemon/device-claim-conflict.ts'; import { resolveRefStalenessWarning } from '../../src/daemon/session-snapshot.ts'; import type { SessionState } from '../../src/daemon/types.ts'; -import { buildAppNotInstalledError } from '../../src/platforms/apple/core/app-resolution.ts'; +import { buildAppNotInstalledError } from '@agent-device/platform-apple'; import { presentConnectReadiness, renderConnectSuccess, @@ -53,7 +53,7 @@ export type SampleProducer = { producer: string; sample: CapturedSample; /** The sample's text rebuilt from production code, ready to compare. */ - render: () => string; + render: () => string | Promise; }; const formatPress = (result: Record) => @@ -450,7 +450,7 @@ export const SAMPLE_PRODUCERS: SampleProducer[] = [ name: 'APP_NOT_INSTALLED_SAMPLE', producer: 'the real app-resolution producer and the default hint', sample: APP_NOT_INSTALLED_SAMPLE, - render: () => renderHumanError(buildAppNotInstalledError('Shoply')), + render: async () => renderHumanError(await buildAppNotInstalledError('Shoply')), }, { name: 'BROWSERSTACK_CONNECT_SAMPLE', diff --git a/scripts/__tests__/test-file-size-ratchet.test.ts b/scripts/__tests__/test-file-size-ratchet.test.ts index a8dc1c9720..ce104d7fce 100644 --- a/scripts/__tests__/test-file-size-ratchet.test.ts +++ b/scripts/__tests__/test-file-size-ratchet.test.ts @@ -34,7 +34,7 @@ const TRIPWIRE_LINES = 1_000; // Exact current lengths. Lower a pin when its file shrinks; never raise one — extract instead. const PINNED_TEST_FILE_LINES: Readonly> = Object.freeze({ 'src/__tests__/remote-connection.test.ts': 2973, - 'src/daemon/handlers/__tests__/snapshot-handler.test.ts': 2242, + 'src/daemon/handlers/__tests__/snapshot-handler.test.ts': 2140, 'src/commands/interaction/runtime/settle.test.ts': 2359, 'src/daemon/handlers/__tests__/session-replay-runtime-maestro.test.ts': 1963, 'packages/platform-apple/src/runner/__tests__/runner-session.test.ts': 1957, @@ -49,11 +49,11 @@ const PINNED_TEST_FILE_LINES: Readonly> = Object.freeze({ 'src/__tests__/cli-client-commands.test.ts': 1317, 'src/__tests__/cli-config.test.ts': 1282, 'src/daemon/handlers/__tests__/find.test.ts': 1199, - 'src/platforms/apple/core/__tests__/perf.test.ts': 1222, + 'packages/platform-apple/src/core/__tests__/perf.test.ts': 1222, 'src/mcp/__tests__/command-tools.test.ts': 1216, 'src/daemon/handlers/__tests__/session-replay-divergence.test.ts': 1136, - 'src/platforms/apple/core/__tests__/apps.test.ts': 1210, - 'src/daemon/handlers/__tests__/session-replay-repair-transaction.test.ts': 1208, + 'packages/platform-apple/src/core/__tests__/apps.test.ts': 1207, + 'src/daemon/handlers/__tests__/session-replay-repair-transaction.test.ts': 1202, 'src/daemon/handlers/__tests__/session-replay-target-verification-runtime.test.ts': 1182, 'src/__tests__/client-metro.test.ts': 1105, 'src/__tests__/cli-network.test.ts': 1092, @@ -144,7 +144,7 @@ function baseLineCounts(paths: readonly string[]): ReadonlyMap(); const renames = runCmdSync( 'git', - ['diff', '--name-status', '--find-renames', '--diff-filter=R', base, 'HEAD', '--', '*.test.*'], + ['diff', '--name-status', '--find-renames', '--diff-filter=R', base, '--', '*.test.*'], { cwd: REPO_ROOT }, ); for (const line of renames.stdout.split('\n')) { diff --git a/scripts/check-affected/device-lanes.test.ts b/scripts/check-affected/device-lanes.test.ts index 0e3395645c..2c42c3d1b2 100644 --- a/scripts/check-affected/device-lanes.test.ts +++ b/scripts/check-affected/device-lanes.test.ts @@ -21,9 +21,9 @@ function lanes(file: string): CheckId[] { test('a TypeScript-only Apple change selects the iOS and macOS lanes without a Swift build', () => { for (const file of [ - 'src/platforms/apple/core/apps.ts', + 'packages/platform-apple/src/core/apps.ts', 'packages/platform-apple/src/runtime.ts', - 'src/platforms/apple/os/macos/desktop.ts', + 'packages/platform-apple/src/os/macos/desktop.ts', ]) { assert.deepEqual(lanes(file), ['replay-ios', 'replay-ios-device', 'replay-macos'], file); assert.ok( @@ -129,7 +129,7 @@ test('unit tests under src/ and packages/*/src own no lane', () => { for (const file of [ 'src/daemon/selectors.test.ts', 'src/__tests__/contracts/interaction-guarantees.test.ts', - 'src/platforms/apple/__tests__/apps.test.ts', + 'packages/platform-apple/src/__tests__/apps.test.ts', 'packages/platform-apple/src/runtime.test.ts', ]) { assert.ok(isUnitTest(file), file); @@ -147,9 +147,9 @@ test('unit tests under src/ and packages/*/src own no lane', () => { }); test('a mixed tree is shared, and two Apple leaves are still Apple', () => { - assert.equal(deviceLaneLeaf('src/platforms/apple/os/macos/x.ts'), 'apple'); + assert.equal(deviceLaneLeaf('packages/platform-apple/src/os/macos/x.ts'), 'apple'); assert.equal(deviceLaneLeaf('test/integration/android-emulator-e2e/ios/x.ts'), 'shared'); - assert.deepEqual(deviceLanesFor('src/platforms/apple/os/macos/x.ts').lanes, [ + assert.deepEqual(deviceLanesFor('packages/platform-apple/src/os/macos/x.ts').lanes, [ 'replay-ios', 'replay-ios-device', 'replay-macos', diff --git a/scripts/check-affected/device-lanes.ts b/scripts/check-affected/device-lanes.ts index 2fa34f9192..051f2a3922 100644 --- a/scripts/check-affected/device-lanes.ts +++ b/scripts/check-affected/device-lanes.ts @@ -3,7 +3,7 @@ // The live lanes (`replay-ios`, `replay-ios-device`, `replay-macos`, `replay-android`, // `replay-linux`, `linux-command-evidence`, `web-smoke`) drive the CLI and daemon against a real // simulator, emulator, desktop, or browser. Before this rule the selector could reach them only through a full -// fail-open, so a TypeScript-only Apple change (`src/platforms/apple/**`) produced a plan +// fail-open, so a TypeScript-only Apple change (`packages/platform-apple/src/**`) produced a plan // with no iOS check in it at all, and nothing could route `ios.yml` on the plan. // // What enumerates the surface. The import graph cannot: the daemon value-imports every @@ -12,7 +12,7 @@ // session close), so reachability from `src/bin.ts` is the whole tree, and the layering graph // does not resolve relative imports inside `packages/`. The repo's *enforced* platform partition // can: `CANONICAL_PLATFORM_FAMILIES` names the families, layering R13 pins each family's runtime -// to `packages/platform-/` and `src/platforms//`, and the remaining family-owned +// to `packages/platform-/`, and the remaining family-owned // trees are named by a family or Apple-leaf directory segment (`android/`, `linux/`, // `test/integration/replays//`, `src/snapshot/snapshot-presentation/ios/`) or, under // `test/integration/`, by the lane prefix of the smoke file. A path tagged with exactly one diff --git a/scripts/check-affected/model.test.ts b/scripts/check-affected/model.test.ts index f05ed64c1b..dedf1d66b6 100644 --- a/scripts/check-affected/model.test.ts +++ b/scripts/check-affected/model.test.ts @@ -40,7 +40,7 @@ test('production source selects static/build gates and delegates tests to Vitest }); test('platform source additionally selects provider-integration', () => { - const result = ids(['src/platforms/apple/core/apps.ts']); + const result = ids(['packages/platform-apple/src/core/apps.ts']); assert.ok(result.includes('provider-integration')); assert.ok(result.includes('coverage')); assert.ok(result.includes('vitest-related')); @@ -84,7 +84,7 @@ test('Swift runner change selects both XCUITest platform builds', () => { 'replay-ios-device', 'replay-macos', ]); - assert.ok(ids(['src/platforms/apple/core/Support.swift']).includes('swift-runner-ios')); + assert.ok(ids(['packages/platform-apple/src/core/Support.swift']).includes('swift-runner-ios')); }); test('a runner XCTest source also selects the test-list and package-source check', () => { diff --git a/scripts/clean-daemon.ts b/scripts/clean-daemon.ts index 5087d39e5d..d214197c6a 100644 --- a/scripts/clean-daemon.ts +++ b/scripts/clean-daemon.ts @@ -27,12 +27,8 @@ if (daemonPid !== null) { killTimeoutMs: DAEMON_KILL_TIMEOUT_MS, expectedStartTime: info?.processStartTime, }); - const { cleanupRunnerLeasesForOwner, runnerLeaseCleanupAdapter } = - await import('../src/platforms/apple/core/runner-client.ts'); - await cleanupRunnerLeasesForOwner( - { pid: daemonPid, startTime: info?.processStartTime }, - runnerLeaseCleanupAdapter, - ); + const { cleanupRunnerLeasesForOwner } = await import('@agent-device/platform-apple'); + await cleanupRunnerLeasesForOwner({ pid: daemonPid, startTime: info?.processStartTime }); } removeIfPresent(paths.infoPath); diff --git a/scripts/gate/routing.test.ts b/scripts/gate/routing.test.ts index fdb73d54ef..9c5a886a30 100644 --- a/scripts/gate/routing.test.ts +++ b/scripts/gate/routing.test.ts @@ -42,14 +42,14 @@ test('the routed lane derives its needs from its declared gate plus the sampled test('ignoring an Apple-owned tree is reported with the checks the selector routes it to', () => { const model = withIos((lane) => ({ ...lane, - pathsIgnore: [...lane.pathsIgnore, 'src/platforms/apple/**'], + pathsIgnore: [...lane.pathsIgnore, 'packages/platform-apple/src/**'], })); const found = messages(model); assert.ok(found.length > 0); assert.ok( found.some( (message) => - /ignores src\/platforms\/apple\//.test(message) && + /ignores packages\/platform-apple\/src\//.test(message) && /routes it to "replay-ios"/.test(message), ), found.slice(0, 3).join('\n'), diff --git a/scripts/help-conformance-sample-outputs.mjs b/scripts/help-conformance-sample-outputs.mjs index c098bdfd7b..f0ec95810a 100644 --- a/scripts/help-conformance-sample-outputs.mjs +++ b/scripts/help-conformance-sample-outputs.mjs @@ -131,7 +131,7 @@ Candidates: }; // APP_NOT_INSTALLED from buildAppNotInstalledError -// (src/platforms/apple/core/app-resolution.ts) — the parity test drives that +// (packages/platform-apple/src/core/app-resolution.ts) — the parity test drives that // exact producer; the hint is defaultHintForCode('APP_NOT_INSTALLED'). export const APP_NOT_INSTALLED_SAMPLE = { command: 'agent-device open Shoply --platform ios', diff --git a/scripts/layering/check.ts b/scripts/layering/check.ts index 920918ccc6..47c083f377 100644 --- a/scripts/layering/check.ts +++ b/scripts/layering/check.ts @@ -82,6 +82,7 @@ import { } from './package-boundaries.ts'; import { checkPlatformPackagePolicy, + checkRetiredAppleFamilySources, checkPlatformsRootShape, platformPackagePolicySummary, } from './platform-package-policy.ts'; @@ -546,6 +547,7 @@ export const LAYERING_RULE_IDS = [ 'bin-alias-fast-path', 'package-boundaries', 'platform-package-policy', + 'platform-family-package-ownership', 'platforms-root-shape', ] as const; @@ -584,6 +586,8 @@ export const LAYERING_RULES: Readonly> = { readTrackedPlatformPackageDeclarations(repoRoot), { untrackedProductionFiles: listUntrackedProductionTypeScriptFiles(repoRoot) }, ), + 'platform-family-package-ownership': (context) => + checkRetiredAppleFamilySources([...context.allTypeScriptSources.keys()]), 'platforms-root-shape': (context) => checkPlatformsRootShape([...context.allTypeScriptSources.keys()]), }; diff --git a/scripts/layering/package-boundaries.test.ts b/scripts/layering/package-boundaries.test.ts index db84e1fbc9..cc4191297d 100644 --- a/scripts/layering/package-boundaries.test.ts +++ b/scripts/layering/package-boundaries.test.ts @@ -510,6 +510,7 @@ test('the real tree parses, declares, and passes R11', () => { '@agent-device/host-kit/archive', '@agent-device/host-kit/command', '@agent-device/host-kit/diagnostics', + '@agent-device/host-kit/environment', '@agent-device/host-kit/file', '@agent-device/host-kit/host-file', '@agent-device/host-kit/process', @@ -535,7 +536,9 @@ test('the real tree parses, declares, and passes R11', () => { assert.deepEqual([...platformApplePackage.workspaceDependencies].sort(), [ '@agent-device/capture-kit', '@agent-device/contracts', + '@agent-device/host-kit', '@agent-device/kernel', + '@agent-device/provision-kit', '@agent-device/xml', ]); const platformAndroidPackage = packages.find( diff --git a/scripts/layering/platform-package-policy.test.ts b/scripts/layering/platform-package-policy.test.ts index 39cf1035fc..59541a67c2 100644 --- a/scripts/layering/platform-package-policy.test.ts +++ b/scripts/layering/platform-package-policy.test.ts @@ -30,6 +30,15 @@ function declarations(): PlatformPackageDeclaration[] { '@agent-device/platform-apple/runner', '@agent-device/platform-apple/runner/client', '@agent-device/platform-apple/runner/test-host', + '@agent-device/platform-apple/app-resolution', + '@agent-device/platform-apple/interactions', + '@agent-device/platform-apple/install-artifact', + '@agent-device/platform-apple/perf', + '@agent-device/platform-apple/physical-device', + '@agent-device/platform-apple/runner-owner', + '@agent-device/platform-apple/simctl', + '@agent-device/platform-apple/simulator', + '@agent-device/platform-apple/tool-provider', ] : family === 'android' ? [ @@ -87,8 +96,9 @@ test('the inventory substrate has six private lazy packages and one exact compos assert.deepEqual(checkPlatformPackagePolicy(validSources(), declarations()), []); }); -test('the W6 family implementations are retired from src/platforms', () => { +test('retired platform family implementations are rejected from src/platforms', () => { const violations = checkPlatformsRootShape([ + 'src/platforms/apple/core/apps.ts', 'src/platforms/harmonyos/app-lifecycle.ts', 'src/platforms/linux/snapshot.ts', 'src/platforms/vega/interactor.ts', @@ -98,6 +108,7 @@ test('the W6 family implementations are retired from src/platforms', () => { assert.deepEqual( violations.map(({ file, rule }) => ({ file, rule })), [ + { file: 'src/platforms/apple/core/apps.ts', rule: 'platforms-root-shape' }, { file: 'src/platforms/harmonyos/app-lifecycle.ts', rule: 'platforms-root-shape' }, { file: 'src/platforms/linux/snapshot.ts', rule: 'platforms-root-shape' }, { file: 'src/platforms/vega/interactor.ts', rule: 'platforms-root-shape' }, @@ -266,7 +277,7 @@ test('the apple runner mechanics facet subpaths are the enumerated exception', ( // The host-bound client factory has exactly one composition root. const clientSources = validSources(); clientSources.set( - 'src/platforms/apple/core/runner-client.ts', + 'packages/platform-apple/src/core/runner-client.ts', "import { createAppleRunnerClient } from '@agent-device/platform-apple/runner/client';", ); assert.deepEqual(checkPlatformPackagePolicy(clientSources, declarations()), []); @@ -276,7 +287,7 @@ test('the apple runner mechanics facet subpaths are the enumerated exception', ( ); assert.match( messages(clientSources).join('\n'), - /only the composition root src\/platforms\/apple\/core\/runner-client\.ts may construct/, + /only the composition root packages\/platform-apple\/src\/core\/runner-client\.ts may construct/, ); // The test-host installer is a single vitest setup file, dynamic imports included. @@ -556,10 +567,10 @@ test('Node resolves only each platform package root facade', () => { } }); -test('the src/platforms root holds only the remaining family directories and __tests__', () => { - const clean = ['apple'].map((family) => `src/platforms/${family}/doctor.ts`); +test('the src/platforms root holds only the shared __tests__ directory', () => { + const clean = ['src/platforms/__tests__/install-source.test.ts']; assert.deepEqual( - checkPlatformsRootShape([...clean, 'src/platforms/__tests__/install-source.test.ts']), + checkPlatformsRootShape(clean), [], ); }); diff --git a/scripts/layering/platform-package-policy.ts b/scripts/layering/platform-package-policy.ts index 58f61974f3..2d7b0f6e4b 100644 --- a/scripts/layering/platform-package-policy.ts +++ b/scripts/layering/platform-package-policy.ts @@ -11,7 +11,14 @@ export const CANONICAL_PLATFORM_FAMILIES = [ 'linux', 'web', ] as const; -const RETIRED_PLATFORM_FAMILIES = ['android', 'harmonyos', 'linux', 'vega', 'web'] as const; +const RETIRED_PLATFORM_FAMILIES = [ + 'apple', + 'android', + 'harmonyos', + 'linux', + 'vega', + 'web', +] as const; type PlatformFamily = (typeof CANONICAL_PLATFORM_FAMILIES)[number]; export type PlatformPackageDeclaration = { dir: string; @@ -49,7 +56,7 @@ const PLATFORM_RUNTIME_HOST_FILES = new Set([ export const APPLE_RUNNER_SUBTREE = 'packages/platform-apple/src/runner/'; export function checkPlatformsRootShape(files: readonly string[]): LayeringViolation[] { - const allowedChild = new RegExp(`^src/platforms/(?:apple|__tests__)/`); + const allowedChild = new RegExp(`^src/platforms/__tests__/`); const retiredFamily = new RegExp(`^src/platforms/(?:${RETIRED_PLATFORM_FAMILIES.join('|')})/`); return files .filter( @@ -62,19 +69,35 @@ export function checkPlatformsRootShape(files: readonly string[]): LayeringViola line: 1, message: file.startsWith('src/platforms/android/') ? 'the Android family has moved to packages/platform-android; remove the superseded src/platforms/android path' - : 'src/platforms may hold only the remaining apple family directory and __tests__; retired family code belongs in its platform package and shared code belongs in a substrate package', + : 'src/platforms may hold only the shared __tests__ directory; retired family code belongs in its platform package and shared code belongs in a substrate package', })); } const APPLE_RUNNER_FACADE = '@agent-device/platform-apple/runner'; const APPLE_RUNNER_CLIENT = '@agent-device/platform-apple/runner/client'; const APPLE_RUNNER_TEST_HOST = '@agent-device/platform-apple/runner/test-host'; -const APPLE_RUNNER_CLIENT_COMPOSITION = 'src/platforms/apple/core/runner-client.ts'; +const APPLE_RUNNER_CLIENT_COMPOSITION = 'packages/platform-apple/src/core/runner-client.ts'; const APPLE_RUNNER_TEST_HOST_INSTALLER = 'scripts/vitest-apple-runner-host-setup.ts'; const ANDROID_MECHANICS_FACADE = '@agent-device/platform-android/mechanics'; const ANDROID_HOST_FACET = '@agent-device/platform-android/adb-host'; const ANDROID_HOST_BINDING = 'src/platform-runtime-android-adb-host.ts'; +const APPLE_DOMAIN_FACADES = [ + '@agent-device/platform-apple/app-resolution', + '@agent-device/platform-apple/interactions', + '@agent-device/platform-apple/install-artifact', + '@agent-device/platform-apple/perf', + '@agent-device/platform-apple/physical-device', + '@agent-device/platform-apple/runner-owner', + '@agent-device/platform-apple/simctl', + '@agent-device/platform-apple/simulator', + '@agent-device/platform-apple/tool-provider', +] as const; const MECHANICS_FACET_SUBPATHS: Readonly>> = { - apple: [APPLE_RUNNER_FACADE, APPLE_RUNNER_CLIENT, APPLE_RUNNER_TEST_HOST], + apple: [ + APPLE_RUNNER_FACADE, + APPLE_RUNNER_CLIENT, + APPLE_RUNNER_TEST_HOST, + ...APPLE_DOMAIN_FACADES, + ], android: [ANDROID_HOST_FACET, ANDROID_MECHANICS_FACADE], }; @@ -87,6 +110,74 @@ function isAndroidMechanicsFacetImport(file: string, specifier: string): boolean return !file.startsWith('src/daemon/') || !isProductionSource(file); } +function isAppleFacadeSubpathImport(specifier: string): boolean { + return ( + specifier === APPLE_RUNNER_FACADE || + specifier === APPLE_RUNNER_CLIENT || + specifier === APPLE_RUNNER_TEST_HOST || + APPLE_DOMAIN_FACADES.includes(specifier as (typeof APPLE_DOMAIN_FACADES)[number]) + ); +} + +const APPLE_FACADE_CONSUMER_FILES = new Set([ + 'scripts/__tests__/help-conformance-sample-producers.ts', + 'scripts/clean-daemon.ts', + 'scripts/patch-xcuitest-runner-icon.ts', + 'scripts/vitest-apple-runner-host-setup.ts', + 'src/agent-device-client.ts', + 'src/core/__tests__/device-selection-resolver.test.ts', + 'src/core/__tests__/dispatch-resolve.test.ts', + 'src/core/device-selection-resolver.ts', + 'src/core/interactors/register-builtins.ts', + 'src/daemon/ios-app-session-hint.test.ts', + 'src/platform-runtime-apple-application-tools.ts', + 'src/platform-runtime-apple-automation-keep-hot.ts', + 'src/platform-runtime-apple-deployment-executor.test.ts', + 'src/platform-runtime-apple-deployment-executor.ts', + 'src/platform-runtime-apple-physical-readiness.ts', + 'src/platform-runtime-apple-resources.ts', + 'src/platform-runtime-apple-runner-owner.ts', + 'src/platform-runtime-app-inventory-host.ts', + 'src/platform-runtime-apple-tool-host.test.ts', + 'src/platform-runtime-apple-tool-host.ts', + 'src/platform-runtime-audio-probe-host.ts', + 'src/platform-runtime-daemon-owner-cleanup.test.ts', + 'src/platform-runtime-daemon-owner-cleanup.ts', + 'src/platform-runtime-device-ready.ts', + 'src/platform-runtime-host-diagnostics.ts', + 'src/platform-runtime-host.test.ts', + 'src/platform-runtime-host.ts', + 'src/platform-runtime-open-target.ts', + 'src/platform-runtime-perf-capture-host.ts', + 'src/platform-runtime-perf-host.ts', + 'src/platform-runtime-resource-cleanup.ts', + 'src/platform-runtime-runtime-hints.ts', + 'src/platform-runtime-screen-recording-apple-runner-host.test.ts', + 'src/platform-runtime-screen-recording-apple-runner-host.ts', + 'src/platform-runtime-screen-recording-apple-runner-transport.test.ts', + 'src/platform-runtime-screen-recording-apple-runner-transport.ts', + 'src/platform-runtime-screen-recording-apple-transport.ts', + 'src/platforms/__tests__/install-source.test.ts', + 'src/platform-runtime.ts', + 'src/platform-runtime/request-providers.ts', + 'src/sdk/limrun-runtime-dependencies.test.ts', + 'src/sdk/limrun-runtime-dependencies.ts', + 'src/snapshot/snapshot-desktop-surface.test.ts', + 'src/snapshot/snapshot-desktop-surface.ts', + 'src/utils/__tests__/interactors.test.ts', + 'test/integration/provider-scenarios/ios-alert-settings.test.ts', + 'test/integration/provider-scenarios/provider-ios-runner-transport.test.ts', + 'test/integration/provider-scenarios/providers.ts', +]); + +function isAppleFacadeConsumer(file: string): boolean { + return ( + APPLE_FACADE_CONSUMER_FILES.has(file) || + file.startsWith('src/daemon/__tests__/') || + file.startsWith('src/daemon/handlers/__tests__/') + ); +} + function violation(file: string, line: number, message: string): LayeringViolation { return { rule: RULE, file, line, message }; } @@ -261,16 +352,14 @@ function checkSource(file: string, source: string): LayeringViolation[] { } else if ( importedFamily && !COMPOSITION_FILES.has(file) && - // The runner façade subpath is the facet's consumer seam: root code - // that reaches runner mechanics directly imports its types and host-free - // helpers here. R11's workspace-dependency declarations bound the - // importer set to the root package. - site.spec !== APPLE_RUNNER_FACADE && - site.spec !== APPLE_RUNNER_CLIENT && - site.spec !== APPLE_RUNNER_TEST_HOST && + // Named Apple facades are the package's consumer seams. R11's + // workspace-dependency declarations bound the importer set to the root + // package. + !isAppleFacadeSubpathImport(site.spec) && !isAllowedPlatformRootImport(file, site, importedFamily) && !isPackageOwnedFacadeTest(file, importedFamily, site.spec) && - !isAndroidMechanicsFacetImport(file, site.spec) + !isAndroidMechanicsFacetImport(file, site.spec) && + !(site.spec === '@agent-device/platform-apple' && isAppleFacadeConsumer(file)) ) { violations.push( violation( @@ -292,6 +381,7 @@ function checkSource(file: string, source: string): LayeringViolation[] { !site.spec.startsWith('@agent-device/provision-kit/') && !site.spec.startsWith('@agent-device/kernel/') && site.spec !== '@agent-device/xml' && + !isAppleFacadeSubpathImport(site.spec) && !isPackageOwnedFacadeTest(file, ownerFamily, site.spec) && !isWebPackageTestSelectorImport(file, ownerFamily, site.spec) ) { diff --git a/scripts/patch-xcuitest-runner-icon.ts b/scripts/patch-xcuitest-runner-icon.ts index 044a92906a..df51a0c154 100644 --- a/scripts/patch-xcuitest-runner-icon.ts +++ b/scripts/patch-xcuitest-runner-icon.ts @@ -5,6 +5,5 @@ if (!derivedPath) { process.exit(1); } -const { applyXctestRunnerAppIconFromDerivedPath } = - await import('../src/platforms/apple/core/runner-client.ts'); +const { applyXctestRunnerAppIconFromDerivedPath } = await import('@agent-device/platform-apple'); await applyXctestRunnerAppIconFromDerivedPath(derivedPath); diff --git a/scripts/vitest-apple-runner-host-setup.ts b/scripts/vitest-apple-runner-host-setup.ts index 7107f7778c..79463687d8 100644 --- a/scripts/vitest-apple-runner-host-setup.ts +++ b/scripts/vitest-apple-runner-host-setup.ts @@ -3,13 +3,13 @@ import { appleRunnerTestHost, installAppleRunnerTestHost, } from '@agent-device/platform-apple/runner/test-host'; -import { appleRunnerHost } from '../src/platforms/apple/core/runner-host.ts'; +import { loadAppleRunnerHost } from '@agent-device/platform-apple'; // The apple-runner vitest project runs the package's own suites, which cannot // import root utilities directly (R11). This setup installs the real root host // capabilities as overridable defaults, so package tests exercise genuine // retry/lock/exec semantics and stub individual capabilities per test. -installAppleRunnerTestHost(appleRunnerHost); +installAppleRunnerTestHost(await loadAppleRunnerHost()); beforeEach(() => { appleRunnerTestHost.reset(); diff --git a/src/__tests__/test-utils/boundary-fault-matrix.ts b/src/__tests__/test-utils/boundary-fault-matrix.ts index 549a47c3f9..65a96b2413 100644 --- a/src/__tests__/test-utils/boundary-fault-matrix.ts +++ b/src/__tests__/test-utils/boundary-fault-matrix.ts @@ -203,7 +203,7 @@ export const BOUNDARY_FAULT_MATRIX = { }, 'artifact-producing': { kind: 'covered', - evidence: ['src/platforms/apple/core/__tests__/screenshot.test.ts'], + evidence: ['packages/platform-apple/src/core/__tests__/screenshot.test.ts'], invariants: ['best-effort-degradation'], }, 'session-lifecycle': { diff --git a/src/agent-device-client.ts b/src/agent-device-client.ts index 1793da8209..9778733498 100644 --- a/src/agent-device-client.ts +++ b/src/agent-device-client.ts @@ -440,8 +440,7 @@ export function createAgentDeviceClient( }, debug: { symbols: async (options) => { - const { symbolicateCrashArtifact } = - await import('./platforms/apple/core/debug-symbols.ts'); + const { symbolicateCrashArtifact } = await import('@agent-device/platform-apple'); return symbolicateCrashArtifact({ cwd: options.cwd ?? config.cwd, ...options }); }, }, diff --git a/src/core/__tests__/device-selection-resolver.test.ts b/src/core/__tests__/device-selection-resolver.test.ts index f365f97191..eb9ddba000 100644 --- a/src/core/__tests__/device-selection-resolver.test.ts +++ b/src/core/__tests__/device-selection-resolver.test.ts @@ -18,7 +18,7 @@ import { const mockFindIosSimulatorInstalledApp = vi.hoisted(() => vi.fn()); -vi.mock('../../platforms/apple/core/apps.ts', () => ({ +vi.mock('@agent-device/platform-apple', () => ({ findIosSimulatorInstalledApp: mockFindIosSimulatorInstalledApp, })); diff --git a/src/core/__tests__/dispatch-resolve.test.ts b/src/core/__tests__/dispatch-resolve.test.ts index 63b751609b..4050f2c81e 100644 --- a/src/core/__tests__/dispatch-resolve.test.ts +++ b/src/core/__tests__/dispatch-resolve.test.ts @@ -6,7 +6,7 @@ const { mockFindIosSimulatorInstalledApp, mockListAppleDevices } = vi.hoisted(() mockListAppleDevices: vi.fn(), })); -vi.mock('../../platforms/apple/core/apps.ts', () => { +vi.mock('@agent-device/platform-apple', () => { return { findIosSimulatorInstalledApp: mockFindIosSimulatorInstalledApp, }; diff --git a/src/core/device-selection-resolver.ts b/src/core/device-selection-resolver.ts index 4f23eb6fae..8165d81e9c 100644 --- a/src/core/device-selection-resolver.ts +++ b/src/core/device-selection-resolver.ts @@ -166,7 +166,7 @@ async function resolveAppInstalledSimulatorSelection( ); if (bootedSimulators.length < 2) return undefined; - const { findIosSimulatorInstalledApp } = await import('../platforms/apple/core/apps.ts'); + const { findIosSimulatorInstalledApp } = await import('@agent-device/platform-apple'); const matches = ( await Promise.all( bootedSimulators.map(async (device) => diff --git a/src/core/interactors/register-builtins.ts b/src/core/interactors/register-builtins.ts index 1214267ed6..4a96a7f8cb 100644 --- a/src/core/interactors/register-builtins.ts +++ b/src/core/interactors/register-builtins.ts @@ -1,7 +1,7 @@ import type { RunnerContext } from '@agent-device/contracts/interactor-types'; import type { PlatformPlugin } from '@agent-device/contracts/platform-plugin'; import { registerPlatformPlugin } from '../platform-plugin-registry.ts'; -import { applePlugin } from '../../platforms/apple/plugin.ts'; +import { applePlugin } from '@agent-device/platform-apple'; import { vegaPlugin } from '@agent-device/platform-vega'; import type { Platform, DeviceInfo } from '@agent-device/kernel/device'; diff --git a/src/daemon/__tests__/application-lifecycle-recovery.test.ts b/src/daemon/__tests__/application-lifecycle-recovery.test.ts index 811e119406..4111179773 100644 --- a/src/daemon/__tests__/application-lifecycle-recovery.test.ts +++ b/src/daemon/__tests__/application-lifecycle-recovery.test.ts @@ -27,14 +27,10 @@ vi.mock('@agent-device/platform-android/mechanics', () => { localMechanics.adbEvaluated = true; return { restoreAndroidTestIme: localMechanics.imeRestore }; }); -vi.mock('../../platforms/apple/core/simctl.ts', async (importOriginal) => { +vi.mock('@agent-device/platform-apple', async (importOriginal) => { localMechanics.simctlEvaluated = true; - return await importOriginal(); -}); -vi.mock('../../platforms/apple/core/runner-client.ts', async (importOriginal) => { localMechanics.runnerEvaluated = true; - const actual = - await importOriginal(); + const actual = await importOriginal(); return { ...actual, stopIosRunnerSession: localMechanics.runnerStop }; }); diff --git a/src/daemon/__tests__/device-ready.test.ts b/src/daemon/__tests__/device-ready.test.ts index d700999a5e..d6b749b698 100644 --- a/src/daemon/__tests__/device-ready.test.ts +++ b/src/daemon/__tests__/device-ready.test.ts @@ -11,7 +11,8 @@ vi.mock('@agent-device/host-kit/command', async (importOriginal) => { whichCmd: vi.fn(async () => true), }; }); -vi.mock('../../platforms/apple/core/simulator.ts', () => ({ +vi.mock('@agent-device/platform-apple', async (importOriginal) => ({ + ...(await importOriginal()), ensureBootedSimulator: vi.fn(async () => {}), })); vi.mock('@agent-device/platform-android/mechanics', () => ({ @@ -20,7 +21,7 @@ vi.mock('@agent-device/platform-android/mechanics', () => ({ import { runCmd } from '@agent-device/host-kit/command'; import { waitForAndroidBoot } from '@agent-device/platform-android/mechanics'; -import { ensureBootedSimulator } from '../../platforms/apple/core/simulator.ts'; +import { ensureBootedSimulator } from '@agent-device/platform-apple'; import { ANDROID_EMULATOR, IOS_DEVICE, diff --git a/src/daemon/__tests__/is-runtime.test.ts b/src/daemon/__tests__/is-runtime.test.ts index 1c0f3d3756..4974a3386a 100644 --- a/src/daemon/__tests__/is-runtime.test.ts +++ b/src/daemon/__tests__/is-runtime.test.ts @@ -13,9 +13,8 @@ import { selectorCaptureFixture } from './selector-capture-fixture.ts'; const { mockRunAppleRunnerCommand } = vi.hoisted(() => ({ mockRunAppleRunnerCommand: vi.fn() })); -vi.mock('../../platforms/apple/core/runner-client.ts', async (importOriginal) => { - const actual = - await importOriginal(); +vi.mock('@agent-device/platform-apple', async (importOriginal) => { + const actual = await importOriginal(); return { ...actual, runAppleRunnerCommand: mockRunAppleRunnerCommand }; }); diff --git a/src/daemon/__tests__/offscreen-target-probe.test.ts b/src/daemon/__tests__/offscreen-target-probe.test.ts index 5335f4c70b..d08a255d01 100644 --- a/src/daemon/__tests__/offscreen-target-probe.test.ts +++ b/src/daemon/__tests__/offscreen-target-probe.test.ts @@ -2,18 +2,13 @@ import { beforeEach, test, vi } from 'vitest'; import assert from 'node:assert/strict'; import { AppError } from '@agent-device/kernel/errors'; import { IOS_SIMULATOR } from '../../__tests__/test-utils/device-fixtures.ts'; +import { withAppleRunnerProvider } from '@agent-device/platform-apple/runner'; import type { SessionState } from '../types.ts'; const { mockRunAppleRunnerCommand } = vi.hoisted(() => ({ mockRunAppleRunnerCommand: vi.fn(), })); -vi.mock('../../platforms/apple/core/runner-client.ts', async (importOriginal) => { - const actual = - await importOriginal(); - return { ...actual, runAppleRunnerCommand: mockRunAppleRunnerCommand }; -}); - import { confirmIosOffscreenTargetVisible } from '../offscreen-target-probe.ts'; beforeEach(() => { @@ -24,6 +19,14 @@ function makeSession(): SessionState { return { name: 'default', device: IOS_SIMULATOR, createdAt: Date.now(), actions: [] }; } +async function withRunner(operation: () => Promise): Promise { + return await withAppleRunnerProvider( + mockRunAppleRunnerCommand, + { deviceId: IOS_SIMULATOR.id }, + operation, + ); +} + const ROOT_VIEWPORT = { x: 0, y: 0, width: 402, height: 874 }; test('confirmIosOffscreenTargetVisible: returns the live rect when the runner confirms hittable + inside the viewport', async () => { @@ -32,12 +35,14 @@ test('confirmIosOffscreenTargetVisible: returns the live rect when the runner co nodes: [{ index: 0, rect: { x: 126, y: 136, width: 75, height: 38 }, hittable: true }], }); - const rect = await confirmIosOffscreenTargetVisible({ - session: makeSession(), - node: { identifier: 'shipping-pickup' }, - rootViewport: ROOT_VIEWPORT, - requestOptions: {}, - }); + const rect = await withRunner(() => + confirmIosOffscreenTargetVisible({ + session: makeSession(), + node: { identifier: 'shipping-pickup' }, + rootViewport: ROOT_VIEWPORT, + requestOptions: {}, + }), + ); assert.deepEqual(rect, { x: 126, y: 136, width: 75, height: 38 }); assert.equal(mockRunAppleRunnerCommand.mock.calls[0]?.[1].selectorKey, 'id'); @@ -45,12 +50,14 @@ test('confirmIosOffscreenTargetVisible: returns the live rect when the runner co }); test('confirmIosOffscreenTargetVisible: null when the node has no usable id/label (never calls the runner)', async () => { - const rect = await confirmIosOffscreenTargetVisible({ - session: makeSession(), - node: {}, - rootViewport: ROOT_VIEWPORT, - requestOptions: {}, - }); + const rect = await withRunner(() => + confirmIosOffscreenTargetVisible({ + session: makeSession(), + node: {}, + rootViewport: ROOT_VIEWPORT, + requestOptions: {}, + }), + ); assert.equal(rect, null); assert.equal(mockRunAppleRunnerCommand.mock.calls.length, 0); @@ -59,12 +66,14 @@ test('confirmIosOffscreenTargetVisible: null when the node has no usable id/labe test('confirmIosOffscreenTargetVisible: null when the runner reports not found', async () => { mockRunAppleRunnerCommand.mockResolvedValue({ found: false, nodes: [] }); - const rect = await confirmIosOffscreenTargetVisible({ - session: makeSession(), - node: { label: 'Pickup' }, - rootViewport: ROOT_VIEWPORT, - requestOptions: {}, - }); + const rect = await withRunner(() => + confirmIosOffscreenTargetVisible({ + session: makeSession(), + node: { label: 'Pickup' }, + rootViewport: ROOT_VIEWPORT, + requestOptions: {}, + }), + ); assert.equal(rect, null); }); @@ -74,12 +83,14 @@ test('confirmIosOffscreenTargetVisible: null on an ambiguous match / any runner new AppError('AMBIGUOUS_MATCH', 'selector matched multiple elements'), ); - const rect = await confirmIosOffscreenTargetVisible({ - session: makeSession(), - node: { label: 'Checkout form' }, - rootViewport: ROOT_VIEWPORT, - requestOptions: {}, - }); + const rect = await withRunner(() => + confirmIosOffscreenTargetVisible({ + session: makeSession(), + node: { label: 'Checkout form' }, + rootViewport: ROOT_VIEWPORT, + requestOptions: {}, + }), + ); assert.equal(rect, null); }); @@ -90,12 +101,14 @@ test('confirmIosOffscreenTargetVisible: null when the live read is not hittable, nodes: [{ index: 0, rect: { x: 126, y: 136, width: 75, height: 38 }, hittable: false }], }); - const rect = await confirmIosOffscreenTargetVisible({ - session: makeSession(), - node: { identifier: 'shipping-pickup' }, - rootViewport: ROOT_VIEWPORT, - requestOptions: {}, - }); + const rect = await withRunner(() => + confirmIosOffscreenTargetVisible({ + session: makeSession(), + node: { identifier: 'shipping-pickup' }, + rootViewport: ROOT_VIEWPORT, + requestOptions: {}, + }), + ); assert.equal(rect, null); }); @@ -106,12 +119,14 @@ test('confirmIosOffscreenTargetVisible: null when the live rect is hittable but nodes: [{ index: 0, rect: { x: 5000, y: 5000, width: 75, height: 38 }, hittable: true }], }); - const rect = await confirmIosOffscreenTargetVisible({ - session: makeSession(), - node: { identifier: 'shipping-pickup' }, - rootViewport: ROOT_VIEWPORT, - requestOptions: {}, - }); + const rect = await withRunner(() => + confirmIosOffscreenTargetVisible({ + session: makeSession(), + node: { identifier: 'shipping-pickup' }, + rootViewport: ROOT_VIEWPORT, + requestOptions: {}, + }), + ); assert.equal(rect, null); }); diff --git a/src/daemon/__tests__/request-platform-providers.test.ts b/src/daemon/__tests__/request-platform-providers.test.ts index a25a279372..3b98756af9 100644 --- a/src/daemon/__tests__/request-platform-providers.test.ts +++ b/src/daemon/__tests__/request-platform-providers.test.ts @@ -13,10 +13,7 @@ import { makeSession, } from '../../__tests__/test-utils/session-factories.ts'; import { withTestDeviceInventoryProvider as withTargetDeviceResolutionScope } from '../../__tests__/test-utils/device-inventory-gateways.ts'; -import { - createLocalAppleToolProvider, - runXcrun, -} from '../../platforms/apple/core/tool-provider.ts'; +import { createLocalAppleToolProvider, runXcrun } from '@agent-device/platform-apple'; import type { AndroidAdbExecutor } from '@agent-device/platform-android/mechanics'; import { resolveWebProvider, type WebProvider } from '@agent-device/platform-web'; import { diff --git a/src/daemon/__tests__/request-recording-health.test.ts b/src/daemon/__tests__/request-recording-health.test.ts index f60c080261..e7c82d6663 100644 --- a/src/daemon/__tests__/request-recording-health.test.ts +++ b/src/daemon/__tests__/request-recording-health.test.ts @@ -2,11 +2,11 @@ import { test, expect, vi, beforeEach } from 'vitest'; import type { SessionState } from '../types.ts'; import { makeTestScreenRecordingResource } from '../../__tests__/test-utils/screen-recording-live-handle.ts'; -vi.mock('../../platforms/apple/core/runner-client.ts', () => ({ +vi.mock('@agent-device/platform-apple', () => ({ getRunnerSessionSnapshot: vi.fn(), })); -import { getRunnerSessionSnapshot } from '../../platforms/apple/core/runner-client.ts'; +import { getRunnerSessionSnapshot } from '@agent-device/platform-apple'; import { refreshRecordingHealth } from '../request-recording-health.ts'; const mockGetRunnerSessionSnapshot = vi.mocked(getRunnerSessionSnapshot); @@ -48,7 +48,7 @@ test('runner-backed iOS recordings still invalidate on runner restarts', async ( showTouches: true, runnerSessionId: 'runner-before', }); - mockGetRunnerSessionSnapshot.mockReturnValue({ + mockGetRunnerSessionSnapshot.mockResolvedValue({ alive: true, sessionId: 'runner-after', }); diff --git a/src/daemon/__tests__/request-router-cost.test.ts b/src/daemon/__tests__/request-router-cost.test.ts index 5d917a76a2..dbf814bb2b 100644 --- a/src/daemon/__tests__/request-router-cost.test.ts +++ b/src/daemon/__tests__/request-router-cost.test.ts @@ -3,9 +3,8 @@ import { test, expect, vi, beforeEach } from 'vitest'; import os from 'node:os'; import path from 'node:path'; -vi.mock('../../platforms/apple/core/runner-client.ts', async (importOriginal) => { - const actual = - await importOriginal(); +vi.mock('@agent-device/platform-apple', async (importOriginal) => { + const actual = await importOriginal(); return { ...actual, stopIosRunnerSession: vi.fn(async () => {}) }; }); diff --git a/src/daemon/__tests__/request-router-lock-policy.test.ts b/src/daemon/__tests__/request-router-lock-policy.test.ts index 400187b6f8..a4690a58b1 100644 --- a/src/daemon/__tests__/request-router-lock-policy.test.ts +++ b/src/daemon/__tests__/request-router-lock-policy.test.ts @@ -7,9 +7,8 @@ import { legacyDispatchCapture } from './legacy-snapshot-capture-fixture.ts'; import os from 'node:os'; import path from 'node:path'; -vi.mock('../../platforms/apple/core/runner-client.ts', async (importOriginal) => { - const actual = - await importOriginal(); +vi.mock('@agent-device/platform-apple', async (importOriginal) => { + const actual = await importOriginal(); return { ...actual, stopIosRunnerSession: vi.fn(async () => {}) }; }); diff --git a/src/daemon/__tests__/request-router-open.test.ts b/src/daemon/__tests__/request-router-open.test.ts index 4793489df4..521fd6e212 100644 --- a/src/daemon/__tests__/request-router-open.test.ts +++ b/src/daemon/__tests__/request-router-open.test.ts @@ -13,17 +13,11 @@ vi.mock('@agent-device/host-kit/process', async (importOriginal) => { const actual = await importOriginal(); return { ...actual, readProcessStartTime: vi.fn(() => 'test-process-start') }; }); -// Opening a session runs the owned-lease cleanup, which pattern-kills stale -// xcodebuild runners with a real `pkill -f`. The session id here is fabricated, -// so on a host with a live Apple runner that write would reach a process this -// test does not own; stub the tool seam the way the runner tests stub the -// signal seam (#1824). -vi.mock('../../platforms/apple/core/tool-provider.ts', async (importOriginal) => { - const actual = - await importOriginal(); +vi.mock('@agent-device/platform-apple', async (importOriginal) => { + const actual = await importOriginal(); return { ...actual, - runAppleToolCommand: vi.fn(async () => ({ exitCode: 0, stdout: '', stderr: '' })), + stopIosRunnerSession: vi.fn(async () => {}), }; }); diff --git a/src/daemon/__tests__/request-router-recording-health.test.ts b/src/daemon/__tests__/request-router-recording-health.test.ts index 26f12c812a..3c37554d81 100644 --- a/src/daemon/__tests__/request-router-recording-health.test.ts +++ b/src/daemon/__tests__/request-router-recording-health.test.ts @@ -4,11 +4,12 @@ import { test, expect, vi, beforeEach } from 'vitest'; import os from 'node:os'; import path from 'node:path'; -vi.mock('../../platforms/apple/core/runner-client.ts', () => ({ +vi.mock('@agent-device/platform-apple', async (importOriginal) => ({ + ...(await importOriginal()), getRunnerSessionSnapshot: vi.fn(), })); -import { getRunnerSessionSnapshot } from '../../platforms/apple/core/runner-client.ts'; +import { getRunnerSessionSnapshot } from '@agent-device/platform-apple'; import { createRequestHandler, gestureDeviceRuntimeGateway, @@ -103,7 +104,7 @@ test('router allows canonical iOS simulator gestures during overlay recording af runnerSessionId: 'runner-before', }); sessionStore.set('default', session); - mockGetRunnerSessionSnapshot.mockReturnValue({ + mockGetRunnerSessionSnapshot.mockResolvedValue({ alive: true, sessionId: 'runner-after', }); diff --git a/src/daemon/__tests__/request-router-replay-scope.test.ts b/src/daemon/__tests__/request-router-replay-scope.test.ts index bcccba1915..bf0f34bb6f 100644 --- a/src/daemon/__tests__/request-router-replay-scope.test.ts +++ b/src/daemon/__tests__/request-router-replay-scope.test.ts @@ -14,14 +14,8 @@ vi.mock('../../platform-runtime-runtime-hints.ts', async (importOriginal) => { return { ...actual, applyRuntimeHintValues: vi.fn(async () => {}) }; }); -vi.mock('../../platforms/apple/core/runner-client.ts', async (importOriginal) => { - const actual = - await importOriginal(); - return { ...actual, prewarmIosRunnerSession: vi.fn() }; -}); - -vi.mock('../../platforms/apple/core/apps.ts', async (importOriginal) => { - const actual = await importOriginal(); +vi.mock('@agent-device/platform-apple', async (importOriginal) => { + const actual = await importOriginal(); return { ...actual, resolveIosApp: vi.fn(async () => 'com.example.app'), diff --git a/src/daemon/__tests__/request-router-response-level.test.ts b/src/daemon/__tests__/request-router-response-level.test.ts index 252e283566..cc6835d65a 100644 --- a/src/daemon/__tests__/request-router-response-level.test.ts +++ b/src/daemon/__tests__/request-router-response-level.test.ts @@ -3,9 +3,8 @@ import { test, expect, vi, beforeEach } from 'vitest'; import os from 'node:os'; import path from 'node:path'; -vi.mock('../../platforms/apple/core/runner-client.ts', async (importOriginal) => { - const actual = - await importOriginal(); +vi.mock('@agent-device/platform-apple', async (importOriginal) => { + const actual = await importOriginal(); return { ...actual, stopIosRunnerSession: vi.fn(async () => {}) }; }); diff --git a/src/daemon/__tests__/request-router-session-address.test.ts b/src/daemon/__tests__/request-router-session-address.test.ts index a46a90dcdb..d80a56f742 100644 --- a/src/daemon/__tests__/request-router-session-address.test.ts +++ b/src/daemon/__tests__/request-router-session-address.test.ts @@ -13,9 +13,8 @@ vi.mock('@agent-device/host-kit/process', async (importOriginal) => { }); // Opening a session runs owned-lease cleanup, which pattern-kills stale xcodebuild runners with a // real `pkill -f`; the session ids here are fabricated, so stub the tool seam (#1824). -vi.mock('../../platforms/apple/core/tool-provider.ts', async (importOriginal) => { - const actual = - await importOriginal(); +vi.mock('@agent-device/platform-apple', async (importOriginal) => { + const actual = await importOriginal(); return { ...actual, runAppleToolCommand: vi.fn(async () => ({ exitCode: 0, stdout: '', stderr: '' })), diff --git a/src/daemon/__tests__/request-router-typed-error.test.ts b/src/daemon/__tests__/request-router-typed-error.test.ts index 659a50f3fa..b6e5b64749 100644 --- a/src/daemon/__tests__/request-router-typed-error.test.ts +++ b/src/daemon/__tests__/request-router-typed-error.test.ts @@ -5,9 +5,8 @@ import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; -vi.mock('../../platforms/apple/core/runner-client.ts', async (importOriginal) => { - const actual = - await importOriginal(); +vi.mock('@agent-device/platform-apple', async (importOriginal) => { + const actual = await importOriginal(); return { ...actual, stopIosRunnerSession: vi.fn(async () => {}) }; }); diff --git a/src/daemon/__tests__/selector-runtime.test.ts b/src/daemon/__tests__/selector-runtime.test.ts index 95957ad521..131a555f88 100644 --- a/src/daemon/__tests__/selector-runtime.test.ts +++ b/src/daemon/__tests__/selector-runtime.test.ts @@ -2,18 +2,13 @@ import { beforeEach, test, vi } from 'vitest'; import assert from 'node:assert/strict'; import { AppError } from '@agent-device/kernel/errors'; import { IOS_SIMULATOR } from '../../__tests__/test-utils/device-fixtures.ts'; +import { withAppleRunnerProvider } from '@agent-device/platform-apple/runner'; import type { SessionState } from '../types.ts'; const { mockRunAppleRunnerCommand } = vi.hoisted(() => ({ mockRunAppleRunnerCommand: vi.fn(), })); -vi.mock('../../platforms/apple/core/runner-client.ts', async (importOriginal) => { - const actual = - await importOriginal(); - return { ...actual, runAppleRunnerCommand: mockRunAppleRunnerCommand }; -}); - import { queryDirectIosSelector } from '../selector-runtime.ts'; beforeEach(() => { @@ -24,6 +19,14 @@ function makeSession(): SessionState { return { name: 'default', device: IOS_SIMULATOR, createdAt: Date.now(), actions: [] }; } +async function withRunner(operation: () => Promise): Promise { + return await withAppleRunnerProvider( + mockRunAppleRunnerCommand, + { deviceId: IOS_SIMULATOR.id }, + operation, + ); +} + // #1542: queryDirectIosSelector is the ONE querySelector client for the local // XCTest runner — the offscreen refusal double-check probe // (src/daemon/offscreen-target-probe.ts) reuses this exact function rather @@ -34,11 +37,12 @@ test('queryDirectIosSelector: builds the querySelector runner command from key/v mockRunAppleRunnerCommand.mockResolvedValue({ found: false, nodes: [] }); const session = { ...makeSession(), appBundleId: 'com.example.demo' }; - await queryDirectIosSelector(session, { key: 'id', value: 'submit-order' }, {}); + await withRunner(() => queryDirectIosSelector(session, { key: 'id', value: 'submit-order' }, {})); const [device, command] = mockRunAppleRunnerCommand.mock.calls[0] ?? []; assert.equal(device, session.device); - assert.deepEqual(command, { + const { commandId: _commandId, ...commandWithoutId } = command as Record; + assert.deepEqual(commandWithoutId, { command: 'querySelector', selectorKey: 'id', selectorValue: 'submit-order', @@ -55,7 +59,9 @@ test('queryDirectIosSelector: accepts a bare {key, value} selector — no `raw` nodes: [{ index: 0, rect: { x: 1, y: 2, width: 3, height: 4 }, hittable: true }], }); - const result = await queryDirectIosSelector(makeSession(), { key: 'label', value: 'Pickup' }, {}); + const result = await withRunner(() => + queryDirectIosSelector(makeSession(), { key: 'label', value: 'Pickup' }, {}), + ); assert.equal(result.found, true); assert.deepEqual(result.node, { @@ -68,7 +74,9 @@ test('queryDirectIosSelector: accepts a bare {key, value} selector — no `raw` test('queryDirectIosSelector: found:false with no nodes reports not-found without a node', async () => { mockRunAppleRunnerCommand.mockResolvedValue({ found: false, nodes: [] }); - const result = await queryDirectIosSelector(makeSession(), { key: 'id', value: 'missing' }, {}); + const result = await withRunner(() => + queryDirectIosSelector(makeSession(), { key: 'id', value: 'missing' }, {}), + ); assert.equal(result.found, false); assert.equal(result.node, undefined); @@ -81,10 +89,8 @@ test('queryDirectIosSelector: surfaces text when the runner includes it', async nodes: [{ index: 0 }], }); - const result = await queryDirectIosSelector( - makeSession(), - { key: 'id', value: 'field-name' }, - {}, + const result = await withRunner(() => + queryDirectIosSelector(makeSession(), { key: 'id', value: 'field-name' }, {}), ); assert.equal(result.text, 'Ada Lovelace'); @@ -96,7 +102,10 @@ test('queryDirectIosSelector: propagates a runner AMBIGUOUS_MATCH rather than sw ); await assert.rejects( - () => queryDirectIosSelector(makeSession(), { key: 'label', value: 'Checkout form' }, {}), + () => + withRunner(() => + queryDirectIosSelector(makeSession(), { key: 'label', value: 'Checkout form' }, {}), + ), (error: unknown) => { assert.ok(error instanceof AppError); assert.equal(error.code, 'AMBIGUOUS_MATCH'); diff --git a/src/daemon/__tests__/snapshot-runtime-fixture.ts b/src/daemon/__tests__/snapshot-runtime-fixture.ts index eab84d6cca..5c8c0a62fd 100644 --- a/src/daemon/__tests__/snapshot-runtime-fixture.ts +++ b/src/daemon/__tests__/snapshot-runtime-fixture.ts @@ -30,7 +30,7 @@ import { isMacOs, type DeviceInfo, } from '@agent-device/kernel/device'; -import { actOnAppleAlert, awaitAppleAlert, readAppleAlert } from '../../platforms/apple/alert.ts'; +import { actOnAppleAlert, awaitAppleAlert, readAppleAlert } from '@agent-device/platform-apple'; import { type DispatchContext } from '../../core/dispatch-context.ts'; import { getRequestSignal } from '@agent-device/host-kit/request'; import { isActiveProviderDevice } from '../../provider-device-runtime.ts'; diff --git a/src/daemon/handlers/__tests__/interaction-settle-private-ax-route.test.ts b/src/daemon/handlers/__tests__/interaction-settle-private-ax-route.test.ts index fa0491556e..f01031ac97 100644 --- a/src/daemon/handlers/__tests__/interaction-settle-private-ax-route.test.ts +++ b/src/daemon/handlers/__tests__/interaction-settle-private-ax-route.test.ts @@ -2,20 +2,20 @@ import { beforeEach, expect, test, vi } from 'vitest'; import { makeSnapshotState } from '../../../__tests__/test-utils/snapshot-builders.ts'; import { makeIosSession } from '../../../__tests__/test-utils/session-factories.ts'; import { makeSessionStore } from '../../../__tests__/test-utils/store-factory.ts'; +import { withAppleRunnerProvider } from '@agent-device/platform-apple/runner'; import { contextFromFlags as buildDaemonContext } from '../../context.ts'; import { handleInteractionCommands } from '../interaction.ts'; import { getRuntimeBindings } from './interaction-get-runtime-fixture.ts'; -vi.mock('../../../platforms/apple/core/runner-client.ts', async (importOriginal) => { - const actual = - await importOriginal(); +vi.mock('@agent-device/platform-apple', async (importOriginal) => { + const actual = await importOriginal(); return { ...actual, runAppleRunnerCommand: vi.fn(async () => ({})), }; }); -import { runAppleRunnerCommand } from '../../../platforms/apple/core/runner-client.ts'; +import { runAppleRunnerCommand } from '@agent-device/platform-apple'; const mockRunnerCommand = vi.mocked(runAppleRunnerCommand); const nodes = [ @@ -65,20 +65,22 @@ test('daemon press --settle pins private-ax on emitted snapshot runner requests' }), ); - const response = await handleInteractionCommands({ - req: { - token: 't', - session: sessionName, - command: 'press', - positionals: ['label=Continue'], - flags: { settle: true, settleQuietMs: 25, timeoutMs: 1_000 }, - }, - sessionName, - sessionStore, - contextFromFlags: (flags, appBundleId, traceLogPath) => - buildDaemonContext('', flags, appBundleId, traceLogPath), - ...getRuntimeBindings(), - }); + const response = await withAppleRunnerProvider(mockRunnerCommand, { deviceId: 'sim-1' }, () => + handleInteractionCommands({ + req: { + token: 't', + session: sessionName, + command: 'press', + positionals: ['label=Continue'], + flags: { settle: true, settleQuietMs: 25, timeoutMs: 1_000 }, + }, + sessionName, + sessionStore, + contextFromFlags: (flags, appBundleId, traceLogPath) => + buildDaemonContext('', flags, appBundleId, traceLogPath), + ...getRuntimeBindings(), + }), + ); expect(response?.ok).toBe(true); const snapshotCommands = mockRunnerCommand.mock.calls diff --git a/src/daemon/handlers/__tests__/interaction-target-evidence.test.ts b/src/daemon/handlers/__tests__/interaction-target-evidence.test.ts index ae2240bf5d..8c0496eaac 100644 --- a/src/daemon/handlers/__tests__/interaction-target-evidence.test.ts +++ b/src/daemon/handlers/__tests__/interaction-target-evidence.test.ts @@ -31,9 +31,8 @@ vi.mock('../snapshot-interactor-capture.ts', async () => { return { captureSnapshotWithInteractor: fixture.captureSnapshotThroughLegacyDispatchFixture }; }); -vi.mock('../../../platforms/apple/core/runner-client.ts', async (importOriginal) => { - const actual = - await importOriginal(); +vi.mock('@agent-device/platform-apple', async (importOriginal) => { + const actual = await importOriginal(); return { ...actual, runAppleRunnerCommand: mockRunAppleRunnerCommand, diff --git a/src/daemon/handlers/__tests__/interaction-touch-android-freshness.test.ts b/src/daemon/handlers/__tests__/interaction-touch-android-freshness.test.ts index a88067cbc3..0fbe728976 100644 --- a/src/daemon/handlers/__tests__/interaction-touch-android-freshness.test.ts +++ b/src/daemon/handlers/__tests__/interaction-touch-android-freshness.test.ts @@ -31,9 +31,8 @@ vi.mock('../snapshot-interactor-capture.ts', () => ({ captureSnapshotWithInteractor: vi.fn(), })); -vi.mock('../../../platforms/apple/core/runner-client.ts', async (importOriginal) => { - const actual = - await importOriginal(); +vi.mock('@agent-device/platform-apple', async (importOriginal) => { + const actual = await importOriginal(); return { ...actual, runAppleRunnerCommand: mockRunAppleRunnerCommand }; }); diff --git a/src/daemon/handlers/__tests__/interaction-touch-android-readiness.test.ts b/src/daemon/handlers/__tests__/interaction-touch-android-readiness.test.ts index 7978b88e52..647103bd3d 100644 --- a/src/daemon/handlers/__tests__/interaction-touch-android-readiness.test.ts +++ b/src/daemon/handlers/__tests__/interaction-touch-android-readiness.test.ts @@ -42,9 +42,8 @@ vi.mock('../snapshot-interactor-capture.ts', () => ({ captureSnapshotWithInteractor: vi.fn(), })); -vi.mock('../../../platforms/apple/core/runner-client.ts', async (importOriginal) => { - const actual = - await importOriginal(); +vi.mock('@agent-device/platform-apple', async (importOriginal) => { + const actual = await importOriginal(); return { ...actual, runAppleRunnerCommand: mockRunAppleRunnerCommand }; }); diff --git a/src/daemon/handlers/__tests__/interaction-touch-direct-ios-eligibility.test.ts b/src/daemon/handlers/__tests__/interaction-touch-direct-ios-eligibility.test.ts index 50527e13bf..29aca4bc6f 100644 --- a/src/daemon/handlers/__tests__/interaction-touch-direct-ios-eligibility.test.ts +++ b/src/daemon/handlers/__tests__/interaction-touch-direct-ios-eligibility.test.ts @@ -33,9 +33,8 @@ vi.mock('../snapshot-interactor-capture.ts', () => ({ captureSnapshotWithInteractor: vi.fn(), })); -vi.mock('../../../platforms/apple/core/runner-client.ts', async (importOriginal) => { - const actual = - await importOriginal(); +vi.mock('@agent-device/platform-apple', async (importOriginal) => { + const actual = await importOriginal(); return { ...actual, runAppleRunnerCommand: mockRunAppleRunnerCommand }; }); diff --git a/src/daemon/handlers/__tests__/interaction-touch-fill.test.ts b/src/daemon/handlers/__tests__/interaction-touch-fill.test.ts index c170ee04ac..92ff13f246 100644 --- a/src/daemon/handlers/__tests__/interaction-touch-fill.test.ts +++ b/src/daemon/handlers/__tests__/interaction-touch-fill.test.ts @@ -1,5 +1,6 @@ import { test, expect, vi, beforeEach } from 'vitest'; import { attachRefs } from '@agent-device/kernel/snapshot'; +import { withAppleRunnerProvider } from '@agent-device/platform-apple/runner'; import { makeSessionStore } from '../../../__tests__/test-utils/store-factory.ts'; import { activateCompleteRefFrame, expireRefFrame } from '../../ref-frame.ts'; import { STALE_SNAPSHOT_REFS_WARNING } from '../../session-snapshot.ts'; @@ -23,6 +24,10 @@ const { mockRunAppleRunnerCommand } = vi.hoisted(() => ({ mockRunAppleRunnerCommand: vi.fn(), })); +async function withRunner(operation: () => Promise): Promise { + return await withAppleRunnerProvider(mockRunAppleRunnerCommand, { deviceId: 'sim-1' }, operation); +} + vi.mock('@agent-device/platform-android/mechanics', async (importOriginal) => { const actual = await importOriginal(); return { @@ -37,9 +42,8 @@ vi.mock('../snapshot-interactor-capture.ts', () => ({ captureSnapshotWithInteractor: vi.fn(), })); -vi.mock('../../../platforms/apple/core/runner-client.ts', async (importOriginal) => { - const actual = - await importOriginal(); +vi.mock('@agent-device/platform-apple', async (importOriginal) => { + const actual = await importOriginal(); return { ...actual, runAppleRunnerCommand: mockRunAppleRunnerCommand }; }); @@ -93,19 +97,21 @@ test('fill @ref fails fast when the target is off-screen', async () => { }; sessionStore.set(sessionName, session); - const response = await handleInteractionCommands({ - req: { - token: 't', - session: sessionName, - command: 'fill', - positionals: ['@e2', 'hello@example.com'], - flags: {}, - }, - sessionName, - sessionStore, - contextFromFlags, - ...getRuntimeBindings(), - }); + const response = await withRunner(() => + handleInteractionCommands({ + req: { + token: 't', + session: sessionName, + command: 'fill', + positionals: ['@e2', 'hello@example.com'], + flags: {}, + }, + sessionName, + sessionStore, + contextFromFlags, + ...getRuntimeBindings(), + }), + ); expect(response).toBeTruthy(); expect(response?.ok).toBe(false); diff --git a/src/daemon/handlers/__tests__/interaction-touch-payload.test.ts b/src/daemon/handlers/__tests__/interaction-touch-payload.test.ts index b02a8e481e..a1c189b1c9 100644 --- a/src/daemon/handlers/__tests__/interaction-touch-payload.test.ts +++ b/src/daemon/handlers/__tests__/interaction-touch-payload.test.ts @@ -36,9 +36,8 @@ vi.mock('../snapshot-interactor-capture.ts', () => ({ captureSnapshotWithInteractor: vi.fn(), })); -vi.mock('../../../platforms/apple/core/runner-client.ts', async (importOriginal) => { - const actual = - await importOriginal(); +vi.mock('@agent-device/platform-apple', async (importOriginal) => { + const actual = await importOriginal(); return { ...actual, runAppleRunnerCommand: mockRunAppleRunnerCommand }; }); diff --git a/src/daemon/handlers/__tests__/interaction-touch-press-admission.test.ts b/src/daemon/handlers/__tests__/interaction-touch-press-admission.test.ts index 128aff329e..90b695f38c 100644 --- a/src/daemon/handlers/__tests__/interaction-touch-press-admission.test.ts +++ b/src/daemon/handlers/__tests__/interaction-touch-press-admission.test.ts @@ -43,9 +43,8 @@ vi.mock('../snapshot-interactor-capture.ts', () => ({ captureSnapshotWithInteractor: vi.fn(), })); -vi.mock('../../../platforms/apple/core/runner-client.ts', async (importOriginal) => { - const actual = - await importOriginal(); +vi.mock('@agent-device/platform-apple', async (importOriginal) => { + const actual = await importOriginal(); return { ...actual, runAppleRunnerCommand: mockRunAppleRunnerCommand }; }); diff --git a/src/daemon/handlers/__tests__/interaction-touch-press.test.ts b/src/daemon/handlers/__tests__/interaction-touch-press.test.ts index b42504eecd..089ed845f7 100644 --- a/src/daemon/handlers/__tests__/interaction-touch-press.test.ts +++ b/src/daemon/handlers/__tests__/interaction-touch-press.test.ts @@ -40,9 +40,8 @@ vi.mock('../snapshot-interactor-capture.ts', () => ({ captureSnapshotWithInteractor: vi.fn(), })); -vi.mock('../../../platforms/apple/core/runner-client.ts', async (importOriginal) => { - const actual = - await importOriginal(); +vi.mock('@agent-device/platform-apple', async (importOriginal) => { + const actual = await importOriginal(); return { ...actual, runAppleRunnerCommand: mockRunAppleRunnerCommand }; }); diff --git a/src/daemon/handlers/__tests__/interaction-touch-response.test.ts b/src/daemon/handlers/__tests__/interaction-touch-response.test.ts index 86b702b905..a6e76d3d8e 100644 --- a/src/daemon/handlers/__tests__/interaction-touch-response.test.ts +++ b/src/daemon/handlers/__tests__/interaction-touch-response.test.ts @@ -37,9 +37,8 @@ vi.mock('../snapshot-interactor-capture.ts', () => ({ captureSnapshotWithInteractor: vi.fn(), })); -vi.mock('../../../platforms/apple/core/runner-client.ts', async (importOriginal) => { - const actual = - await importOriginal(); +vi.mock('@agent-device/platform-apple', async (importOriginal) => { + const actual = await importOriginal(); return { ...actual, runAppleRunnerCommand: mockRunAppleRunnerCommand }; }); diff --git a/src/daemon/handlers/__tests__/interaction-touch-runtime.test.ts b/src/daemon/handlers/__tests__/interaction-touch-runtime.test.ts index c9787ce67c..0b2918cd17 100644 --- a/src/daemon/handlers/__tests__/interaction-touch-runtime.test.ts +++ b/src/daemon/handlers/__tests__/interaction-touch-runtime.test.ts @@ -1,5 +1,6 @@ import { test, expect, vi, beforeEach } from 'vitest'; import { attachRefs } from '@agent-device/kernel/snapshot'; +import { withAppleRunnerProvider } from '@agent-device/platform-apple/runner'; import { makeSessionStore } from '../../../__tests__/test-utils/store-factory.ts'; import { handleInteractionCommands } from '../interaction.ts'; import { @@ -17,6 +18,10 @@ const { mockRunAppleRunnerCommand } = vi.hoisted(() => ({ mockRunAppleRunnerCommand: vi.fn(), })); +async function withRunner(operation: () => Promise): Promise { + return await withAppleRunnerProvider(mockRunAppleRunnerCommand, { deviceId: 'sim-1' }, operation); +} + vi.mock('@agent-device/platform-android/mechanics', async (importOriginal) => { const actual = await importOriginal(); return { @@ -31,9 +36,8 @@ vi.mock('../snapshot-interactor-capture.ts', () => ({ captureSnapshotWithInteractor: vi.fn(), })); -vi.mock('../../../platforms/apple/core/runner-client.ts', async (importOriginal) => { - const actual = - await importOriginal(); +vi.mock('@agent-device/platform-apple', async (importOriginal) => { + const actual = await importOriginal(); return { ...actual, runAppleRunnerCommand: mockRunAppleRunnerCommand }; }); @@ -234,19 +238,21 @@ test('press @ref fails fast when the target is off-screen', async () => { }; sessionStore.set(sessionName, session); - const response = await handleInteractionCommands({ - req: { - token: 't', - session: sessionName, - command: 'press', - positionals: ['@e2'], - flags: {}, - }, - sessionName, - sessionStore, - contextFromFlags, - ...getRuntimeBindings(), - }); + const response = await withRunner(() => + handleInteractionCommands({ + req: { + token: 't', + session: sessionName, + command: 'press', + positionals: ['@e2'], + flags: {}, + }, + sessionName, + sessionStore, + contextFromFlags, + ...getRuntimeBindings(), + }), + ); expect(response).toBeTruthy(); expect(response?.ok).toBe(false); diff --git a/src/daemon/handlers/__tests__/interaction-touch.test.ts b/src/daemon/handlers/__tests__/interaction-touch.test.ts index 5a35f91336..78a79265fe 100644 --- a/src/daemon/handlers/__tests__/interaction-touch.test.ts +++ b/src/daemon/handlers/__tests__/interaction-touch.test.ts @@ -40,9 +40,8 @@ vi.mock('../snapshot-interactor-capture.ts', () => ({ captureSnapshotWithInteractor: vi.fn(), })); -vi.mock('../../../platforms/apple/core/runner-client.ts', async (importOriginal) => { - const actual = - await importOriginal(); +vi.mock('@agent-device/platform-apple', async (importOriginal) => { + const actual = await importOriginal(); return { ...actual, runAppleRunnerCommand: mockRunAppleRunnerCommand }; }); diff --git a/src/daemon/handlers/__tests__/interaction.test.ts b/src/daemon/handlers/__tests__/interaction.test.ts index e95eab3143..c0881f7a18 100644 --- a/src/daemon/handlers/__tests__/interaction.test.ts +++ b/src/daemon/handlers/__tests__/interaction.test.ts @@ -40,9 +40,8 @@ vi.mock('@agent-device/platform-android/mechanics', async (importOriginal) => { }; }); -vi.mock('../../../platforms/apple/core/runner-client.ts', async (importOriginal) => { - const actual = - await importOriginal(); +vi.mock('@agent-device/platform-apple', async (importOriginal) => { + const actual = await importOriginal(); return { ...actual, runAppleRunnerCommand: mockRunAppleRunnerCommand, diff --git a/src/daemon/handlers/__tests__/session-close-shutdown.fixtures.ts b/src/daemon/handlers/__tests__/session-close-shutdown.fixtures.ts index f1a1c12718..5458460114 100644 --- a/src/daemon/handlers/__tests__/session-close-shutdown.fixtures.ts +++ b/src/daemon/handlers/__tests__/session-close-shutdown.fixtures.ts @@ -7,25 +7,20 @@ import type { DaemonRequest, DaemonResponse, SessionState } from '../../types.ts import { AppError } from '@agent-device/kernel/errors'; import { mkdtempForTestSync } from '../../../__tests__/test-utils/tmp-dir.ts'; -vi.mock('../../../platforms/apple/core/simulator.ts', async (importOriginal) => { - const actual = - await importOriginal(); - return { ...actual, shutdownSimulator: vi.fn() }; +vi.mock('@agent-device/platform-apple', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + shutdownSimulator: vi.fn(), + stopIosRunnerSession: vi.fn(async () => {}), + cleanupAppleXctracePerfCapture: vi.fn(async () => ({})), + runMacOsAlertAction: vi.fn(async () => {}), + }; }); vi.mock('@agent-device/host-kit/command', async (importOriginal) => { const actual = await importOriginal(); return { ...actual, runCmd: vi.fn() }; }); -vi.mock('../../../platforms/apple/core/runner-client.ts', async (importOriginal) => { - const actual = - await importOriginal(); - return { ...actual, stopIosRunnerSession: vi.fn(async () => {}) }; -}); -vi.mock('../../../platforms/apple/core/perf-xctrace.ts', async (importOriginal) => { - const actual = - await importOriginal(); - return { ...actual, cleanupAppleXctracePerfCapture: vi.fn(async () => ({})) }; -}); vi.mock('@agent-device/platform-android/mechanics', async (importOriginal) => { const actual = await importOriginal(); return { @@ -34,11 +29,6 @@ vi.mock('@agent-device/platform-android/mechanics', async (importOriginal) => { stopAndroidSnapshotHelperSessionForDevice: vi.fn(async () => {}), }; }); -vi.mock('../../../platforms/apple/os/macos/helper.ts', async (importOriginal) => { - const actual = - await importOriginal(); - return { ...actual, runMacOsAlertAction: vi.fn(async () => {}) }; -}); vi.mock('../../../utils/video.ts', () => ({ waitForStableFile: vi.fn(async () => {}), isPlayableVideo: vi.fn(async () => true), @@ -52,18 +42,20 @@ import { import { teardownSessionResources as teardownProductionSessionResources } from '../../session-teardown.ts'; import { platformResourceCleanup } from '../../../platform-runtime-resource-cleanup.ts'; import { LeaseRegistry } from '../../lease-registry.ts'; -import { shutdownSimulator } from '../../../platforms/apple/core/simulator.ts'; import { runCmd } from '@agent-device/host-kit/command'; import { flushDiagnosticsToSessionFile, withDiagnosticsScope, } from '@agent-device/host-kit/diagnostics'; -import { cleanupAppleXctracePerfCapture } from '../../../platforms/apple/core/perf-xctrace.ts'; import { cleanupAndroidNativePerfSession, stopAndroidSnapshotHelperSessionForDevice, } from '@agent-device/platform-android/mechanics'; -import { stopIosRunnerSession } from '../../../platforms/apple/core/runner-client.ts'; +import { + cleanupAppleXctracePerfCapture, + shutdownSimulator, + stopIosRunnerSession, +} from '@agent-device/platform-apple'; import { WEB_DESKTOP_DEVICE } from '../../../__tests__/test-utils/device-fixtures.ts'; import { acquireDeviceClaim } from '../../device-claims.ts'; import { inspectDeviceClaims } from '../../device-claim-inspection.ts'; diff --git a/src/daemon/handlers/__tests__/session-device-utils.test.ts b/src/daemon/handlers/__tests__/session-device-utils.test.ts index 947aaa2067..ffd4452f8a 100644 --- a/src/daemon/handlers/__tests__/session-device-utils.test.ts +++ b/src/daemon/handlers/__tests__/session-device-utils.test.ts @@ -6,12 +6,12 @@ import { refreshSessionDeviceIfNeeded, selectorTargetsSessionDevice, } from '../session-device-utils.ts'; -import { getRunnerSessionSnapshot } from '../../../platforms/apple/core/runner-client.ts'; +import { getRunnerSessionSnapshot } from '@agent-device/platform-apple'; import { resolveTargetDevice } from '../../../core/dispatch-resolve.ts'; import { isActiveProviderDevice } from '../../../provider-device-runtime.ts'; -vi.mock('../../../platforms/apple/core/runner-client.ts', () => ({ - getRunnerSessionSnapshot: vi.fn(() => null), +vi.mock('@agent-device/platform-apple', () => ({ + getRunnerSessionSnapshot: vi.fn(async () => null), })); vi.mock('../../../core/dispatch-resolve.ts', () => ({ resolveTargetDevice: vi.fn(), @@ -29,7 +29,7 @@ const mockIsActiveProviderDevice = vi.mocked(isActiveProviderDevice); beforeEach(() => { mockGetRunnerSessionSnapshot.mockReset(); - mockGetRunnerSessionSnapshot.mockReturnValue(null); + mockGetRunnerSessionSnapshot.mockResolvedValue(null); mockResolveTargetDevice.mockReset(); mockIsActiveProviderDevice.mockReset(); mockIsActiveProviderDevice.mockReturnValue(false); @@ -96,7 +96,7 @@ test('refreshSessionDeviceIfNeeded keeps provider-owned iOS simulators out of lo }); test('refreshSessionDeviceIfNeeded skips re-resolve while the iOS runner session is alive', async () => { - mockGetRunnerSessionSnapshot.mockReturnValue({ sessionId: 'sim-1:1234:1', alive: true }); + mockGetRunnerSessionSnapshot.mockResolvedValue({ sessionId: 'sim-1:1234:1', alive: true }); const device = await withMockedPlatform('darwin', async () => refreshSessionDeviceIfNeeded(iosSimulatorSession.device), @@ -107,7 +107,7 @@ test('refreshSessionDeviceIfNeeded skips re-resolve while the iOS runner session }); test('refreshSessionDeviceIfNeeded re-resolves when the iOS runner session is gone', async () => { - mockGetRunnerSessionSnapshot.mockReturnValue({ sessionId: 'sim-1:1234:1', alive: false }); + mockGetRunnerSessionSnapshot.mockResolvedValue({ sessionId: 'sim-1:1234:1', alive: false }); const resolved = { ...iosSimulatorSession.device, booted: true, name: 'renamed' }; mockResolveTargetDevice.mockResolvedValue(resolved); diff --git a/src/daemon/handlers/__tests__/session-doctor-warmup.test.ts b/src/daemon/handlers/__tests__/session-doctor-warmup.test.ts index ad9c7361b2..2936a33c8e 100644 --- a/src/daemon/handlers/__tests__/session-doctor-warmup.test.ts +++ b/src/daemon/handlers/__tests__/session-doctor-warmup.test.ts @@ -1,18 +1,18 @@ import { beforeEach, expect, test, vi } from 'vitest'; import type { DeviceInfo } from '@agent-device/kernel/device'; -import { - hasCachedAppleRunnerArtifact, - prewarmAppleRunnerCache, -} from '../../../platforms/apple/core/runner-client.ts'; import { isActiveProviderDevice } from '../../../provider-device-runtime.ts'; import { handleDoctorCommand } from '../session-doctor.ts'; import { createHostDiagnostics } from '../../../platform-runtime-host-diagnostics.ts'; import { makeSessionStore } from '../../../__tests__/test-utils/store-factory.ts'; import type { DaemonResponse } from '../../types.ts'; -vi.mock('../../../platforms/apple/core/runner-client.ts', () => ({ - hasCachedAppleRunnerArtifact: vi.fn(async () => false), - prewarmAppleRunnerCache: vi.fn(), +const { mockAppleRunnerWarmupCheck } = vi.hoisted(() => ({ + mockAppleRunnerWarmupCheck: vi.fn(), +})); + +vi.mock('@agent-device/platform-apple', async (importOriginal) => ({ + ...(await importOriginal()), + appleRunnerWarmupCheck: mockAppleRunnerWarmupCheck, })); vi.mock('../session-doctor-device.ts', async (importOriginal) => { const actual = await importOriginal(); @@ -32,8 +32,6 @@ vi.mock('../../../provider-device-runtime.ts', () => ({ isActiveProviderDevice: vi.fn(() => false), })); -const mockHasCachedArtifact = vi.mocked(hasCachedAppleRunnerArtifact); -const mockPrewarmCache = vi.mocked(prewarmAppleRunnerCache); const mockIsActiveProviderDevice = vi.mocked(isActiveProviderDevice); async function withMockedPlatform(platform: NodeJS.Platform, fn: () => Promise): Promise { @@ -56,9 +54,8 @@ const IOS_SIMULATOR: DeviceInfo = { }; beforeEach(() => { - mockHasCachedArtifact.mockReset(); - mockHasCachedArtifact.mockResolvedValue(false); - mockPrewarmCache.mockReset(); + mockAppleRunnerWarmupCheck.mockReset(); + mockAppleRunnerWarmupCheck.mockResolvedValue(undefined); mockIsActiveProviderDevice.mockReset(); mockIsActiveProviderDevice.mockReturnValue(false); }); @@ -92,26 +89,35 @@ function readCheck(response: DaemonResponse | null, id: string): Record { + mockAppleRunnerWarmupCheck.mockResolvedValue({ + id: 'ios-runner-cache', + status: 'pass', + summary: 'iOS runner build started in the background', + }); const response = await withMockedPlatform('darwin', () => runDoctorWithSessionDevice(IOS_SIMULATOR), ); expect(response?.ok).toBe(true); - expect(mockPrewarmCache).toHaveBeenCalledTimes(1); + expect(mockAppleRunnerWarmupCheck).toHaveBeenCalledTimes(1); const check = readCheck(response, 'ios-runner-cache'); expect(check?.status).toBe('pass'); expect(String(check?.summary)).toMatch(/background/i); }); test('doctor reports a cached iOS runner artifact without rebuilding', async () => { - mockHasCachedArtifact.mockResolvedValue(true); + mockAppleRunnerWarmupCheck.mockResolvedValue({ + id: 'ios-runner-cache', + status: 'pass', + summary: 'iOS runner artifact cached', + }); const response = await withMockedPlatform('darwin', () => runDoctorWithSessionDevice(IOS_SIMULATOR), ); expect(response?.ok).toBe(true); - expect(mockPrewarmCache).not.toHaveBeenCalled(); + expect(mockAppleRunnerWarmupCheck).toHaveBeenCalledTimes(1); const check = readCheck(response, 'ios-runner-cache'); expect(String(check?.summary)).toMatch(/cached/i); }); @@ -126,7 +132,11 @@ test('doctor skips the runner warmup for non-simulator devices', async () => { ); expect(response?.ok).toBe(true); - expect(mockPrewarmCache).not.toHaveBeenCalled(); + expect(mockAppleRunnerWarmupCheck).toHaveBeenCalledTimes(1); + expect(mockAppleRunnerWarmupCheck).toHaveBeenCalledWith( + expect.objectContaining({ kind: 'device' }), + expect.any(Object), + ); expect(readCheck(response, 'ios-runner-cache')).toBeNull(); }); @@ -136,7 +146,8 @@ test('doctor skips the runner warmup on non-macOS hosts', async () => { ); expect(response?.ok).toBe(true); - expect(mockPrewarmCache).not.toHaveBeenCalled(); + expect(mockAppleRunnerWarmupCheck).toHaveBeenCalledTimes(1); + expect(mockAppleRunnerWarmupCheck).toHaveBeenCalledWith(IOS_SIMULATOR, expect.any(Object)); expect(readCheck(response, 'ios-runner-cache')).toBeNull(); }); @@ -148,6 +159,7 @@ test('doctor skips the runner warmup for provider-backed devices', async () => { ); expect(response?.ok).toBe(true); - expect(mockPrewarmCache).not.toHaveBeenCalled(); + expect(mockAppleRunnerWarmupCheck).toHaveBeenCalledTimes(1); + expect(mockAppleRunnerWarmupCheck).toHaveBeenCalledWith(IOS_SIMULATOR, expect.any(Object)); expect(readCheck(response, 'ios-runner-cache')).toBeNull(); }); diff --git a/src/daemon/handlers/__tests__/session-open-execution-runtime.test.ts b/src/daemon/handlers/__tests__/session-open-execution-runtime.test.ts index 89e152210f..8e9a748563 100644 --- a/src/daemon/handlers/__tests__/session-open-execution-runtime.test.ts +++ b/src/daemon/handlers/__tests__/session-open-execution-runtime.test.ts @@ -26,19 +26,12 @@ vi.mock('../../../platform-runtime-runtime-hints.ts', async (importOriginal) => clearRuntimeHintValues: vi.fn(async () => {}), }; }); -vi.mock('../../../platforms/apple/core/runner-client.ts', async (importOriginal) => { - const actual = - await importOriginal(); +vi.mock('@agent-device/platform-apple', async (importOriginal) => { + const actual = await importOriginal(); return { ...actual, prewarmIosRunnerSession: vi.fn(), stopIosRunnerSession: vi.fn(async () => {}), - }; -}); -vi.mock('../../../platforms/apple/core/apps.ts', async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, resolveIosApp: vi.fn(async () => 'com.example.demo'), }; }); diff --git a/src/daemon/handlers/__tests__/session-open-runtime.test.ts b/src/daemon/handlers/__tests__/session-open-runtime.test.ts index a2c2ee4839..00ffc098d4 100644 --- a/src/daemon/handlers/__tests__/session-open-runtime.test.ts +++ b/src/daemon/handlers/__tests__/session-open-runtime.test.ts @@ -24,19 +24,12 @@ vi.mock('../../../platform-runtime-runtime-hints.ts', async (importOriginal) => clearRuntimeHintValues: vi.fn(async () => {}), }; }); -vi.mock('../../../platforms/apple/core/runner-client.ts', async (importOriginal) => { - const actual = - await importOriginal(); +vi.mock('@agent-device/platform-apple', async (importOriginal) => { + const actual = await importOriginal(); return { ...actual, prewarmIosRunnerSession: vi.fn(), stopIosRunnerSession: vi.fn(async () => {}), - }; -}); -vi.mock('../../../platforms/apple/core/apps.ts', async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, resolveIosApp: vi.fn(async () => 'com.example.demo'), }; }); diff --git a/src/daemon/handlers/__tests__/session-replay-repair-record-exclusion.test.ts b/src/daemon/handlers/__tests__/session-replay-repair-record-exclusion.test.ts index 60cc9f6982..07ce132fd9 100644 --- a/src/daemon/handlers/__tests__/session-replay-repair-record-exclusion.test.ts +++ b/src/daemon/handlers/__tests__/session-replay-repair-record-exclusion.test.ts @@ -20,25 +20,19 @@ import { test, expect, vi, beforeEach } from 'vitest'; import { mkdtempForTestSync } from '../../../__tests__/test-utils/tmp-dir.ts'; -vi.mock('../../../platforms/apple/core/simulator.ts', async (importOriginal) => { - const actual = - await importOriginal(); - return { ...actual, shutdownSimulator: vi.fn() }; +vi.mock('@agent-device/platform-apple', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + shutdownSimulator: vi.fn(), + stopIosRunnerSession: vi.fn(async () => {}), + cleanupAppleXctracePerfCapture: vi.fn(async () => ({})), + }; }); vi.mock('@agent-device/host-kit/command', async (importOriginal) => { const actual = await importOriginal(); return { ...actual, runCmd: vi.fn() }; }); -vi.mock('../../../platforms/apple/core/runner-client.ts', async (importOriginal) => { - const actual = - await importOriginal(); - return { ...actual, stopIosRunnerSession: vi.fn(async () => {}) }; -}); -vi.mock('../../../platforms/apple/core/perf-xctrace.ts', async (importOriginal) => { - const actual = - await importOriginal(); - return { ...actual, cleanupAppleXctracePerfCapture: vi.fn(async () => ({})) }; -}); vi.mock('../../../platform-runtime-runtime-hints.ts', async (importOriginal) => { const actual = await importOriginal(); diff --git a/src/daemon/handlers/__tests__/session-replay-repair-transaction.test.ts b/src/daemon/handlers/__tests__/session-replay-repair-transaction.test.ts index 06d4bd6b1d..fbd75a1a49 100644 --- a/src/daemon/handlers/__tests__/session-replay-repair-transaction.test.ts +++ b/src/daemon/handlers/__tests__/session-replay-repair-transaction.test.ts @@ -20,25 +20,19 @@ import { test, expect, vi, beforeEach } from 'vitest'; import { mkdtempForTestSync } from '../../../__tests__/test-utils/tmp-dir.ts'; -vi.mock('../../../platforms/apple/core/simulator.ts', async (importOriginal) => { - const actual = - await importOriginal(); - return { ...actual, shutdownSimulator: vi.fn() }; +vi.mock('@agent-device/platform-apple', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + prewarmIosRunnerSession: vi.fn(async () => {}), + stopIosRunnerSession: vi.fn(async () => {}), + cleanupAppleXctracePerfCapture: vi.fn(async () => ({})), + }; }); vi.mock('@agent-device/host-kit/command', async (importOriginal) => { const actual = await importOriginal(); return { ...actual, runCmd: vi.fn() }; }); -vi.mock('../../../platforms/apple/core/runner-client.ts', async (importOriginal) => { - const actual = - await importOriginal(); - return { ...actual, stopIosRunnerSession: vi.fn(async () => {}) }; -}); -vi.mock('../../../platforms/apple/core/perf-xctrace.ts', async (importOriginal) => { - const actual = - await importOriginal(); - return { ...actual, cleanupAppleXctracePerfCapture: vi.fn(async () => ({})) }; -}); vi.mock('../../../platform-runtime-runtime-hints.ts', async (importOriginal) => { const actual = await importOriginal(); diff --git a/src/daemon/handlers/__tests__/session-test-harness.ts b/src/daemon/handlers/__tests__/session-test-harness.ts index 350aa21c82..06394c8e29 100644 --- a/src/daemon/handlers/__tests__/session-test-harness.ts +++ b/src/daemon/handlers/__tests__/session-test-harness.ts @@ -34,9 +34,8 @@ vi.mock('../../../platform-runtime-runtime-hints.ts', async (importOriginal) => clearRuntimeHintValues: vi.fn(async () => {}), }; }); -vi.mock('../../../platforms/apple/core/runner-client.ts', async (importOriginal) => { - const actual = - await importOriginal(); +vi.mock('@agent-device/platform-apple', async (importOriginal) => { + const actual = await importOriginal(); return { ...actual, prepareIosRunner: vi.fn(async () => ({ @@ -49,13 +48,11 @@ vi.mock('../../../platforms/apple/core/runner-client.ts', async (importOriginal) notifyIosRunnerAppRelaunched: vi.fn(async () => {}), scheduleIosRunnerIdleStop: vi.fn(), stopIosRunnerSession: vi.fn(async () => {}), + runMacOsAlertAction: vi.fn(async () => {}), + resolveIosApp: vi.fn(async () => undefined), + resolveIosSimulatorDeepLinkBundleId: vi.fn(async () => undefined), }; }); -vi.mock('../../../platforms/apple/os/macos/helper.ts', async (importOriginal) => { - const actual = - await importOriginal(); - return { ...actual, runMacOsAlertAction: vi.fn(async () => {}) }; -}); vi.mock('../../../platform-runtime-open-target.ts', async (importOriginal) => { const actual = await importOriginal(); return { ...actual, resolveAndroidPackageForOpen: vi.fn(async () => undefined) }; @@ -76,14 +73,6 @@ vi.mock('../../materialized-path-registry.ts', async (importOriginal) => { const actual = await importOriginal(); return { ...actual, cleanupRetainedMaterializedPathsForSession: vi.fn(async () => {}) }; }); -vi.mock('../../../platforms/apple/core/apps.ts', async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - resolveIosApp: vi.fn(async () => undefined), - resolveIosSimulatorDeepLinkBundleId: vi.fn(async () => undefined), - }; -}); import * as path from 'node:path'; import { cleanupRetainedMaterializedPathsForSession } from '../../materialized-path-registry.ts'; @@ -100,16 +89,14 @@ import { prewarmAppleRunnerCache, prewarmIosRunnerSession, notifyIosRunnerAppRelaunched, + runMacOsAlertAction, + resolveIosApp, + resolveIosSimulatorDeepLinkBundleId, scheduleIosRunnerIdleStop, stopIosRunnerSession, -} from '../../../platforms/apple/core/runner-client.ts'; -import { runMacOsAlertAction } from '../../../platforms/apple/os/macos/helper.ts'; +} from '@agent-device/platform-apple'; import { resolveAndroidPackageForOpen } from '../../../platform-runtime-open-target.ts'; import { runCmd } from '@agent-device/host-kit/command'; -import { - resolveIosApp, - resolveIosSimulatorDeepLinkBundleId, -} from '../../../platforms/apple/core/apps.ts'; import { dispatchApplicationLifecycleEffect } from '../../__tests__/application-lifecycle-runtime-fixture.ts'; export const mockLifecycleDispatch = vi.mocked(dispatchApplicationLifecycleEffect); diff --git a/src/daemon/handlers/__tests__/snapshot-alert.test.ts b/src/daemon/handlers/__tests__/snapshot-alert.test.ts new file mode 100644 index 0000000000..713f5017f5 --- /dev/null +++ b/src/daemon/handlers/__tests__/snapshot-alert.test.ts @@ -0,0 +1,119 @@ +import { test, expect, vi, beforeEach } from 'vitest'; +import { AppError } from '@agent-device/kernel/errors'; +import type { SessionState } from '../../types.ts'; +import { SessionStore } from '../../session-store.ts'; +import { handleSnapshotCommands as handleProductionSnapshotCommands } from '../snapshot.ts'; +import { snapshotRuntimeFixture } from '../../__tests__/snapshot-runtime-fixture.ts'; +import { withAppleRunnerProvider } from '@agent-device/platform-apple/runner'; +import { runAppleRunnerCommand } from '@agent-device/platform-apple'; +import { mkdtempForTestSync } from '../../../__tests__/test-utils/tmp-dir.ts'; +import path from 'node:path'; + +vi.mock('@agent-device/platform-apple', async (importOriginal) => { + const actual = await importOriginal(); + return { ...actual, runAppleRunnerCommand: vi.fn(async () => ({})) }; +}); + +const iosSimulatorDevice: SessionState['device'] = { + platform: 'apple', + id: 'sim-1', + name: 'My iPhone Simulator', + kind: 'simulator', + booted: true, +}; +const mockRunnerCommand = vi.mocked(runAppleRunnerCommand); + +function makeSessionStore(): SessionStore { + const root = mkdtempForTestSync('agent-device-snapshot-alert-'); + return new SessionStore(path.join(root, 'sessions')); +} + +function makeSession(name: string): SessionState { + return { name, device: iosSimulatorDevice, createdAt: Date.now(), actions: [] }; +} + +async function handleSnapshotCommands( + sessionName: string, + sessionStore: SessionStore, + positionals: string[], +): Promise>> { + return await withAppleRunnerProvider(mockRunnerCommand, { deviceId: iosSimulatorDevice.id }, () => + handleProductionSnapshotCommands({ + req: { token: 't', session: sessionName, command: 'alert', positionals, flags: {} }, + sessionName, + logPath: '/tmp/daemon.log', + sessionStore, + ...snapshotRuntimeFixture(), + }), + ); +} + +function alertAbsence(message = 'alert not found'): AppError { + return new AppError('COMMAND_FAILED', message, { runnerErrorCode: 'ALERT_NOT_FOUND' }); +} + +beforeEach(() => { + mockRunnerCommand.mockReset(); + mockRunnerCommand.mockResolvedValue({}); +}); + +test('alert accept retries a typed alert absence and succeeds on the second attempt', async () => { + const sessionStore = makeSessionStore(); + const sessionName = 'ios-sim'; + sessionStore.set(sessionName, makeSession(sessionName)); + + let calls = 0; + mockRunnerCommand.mockImplementation(async () => { + calls += 1; + if (calls === 1) throw alertAbsence(); + return { accepted: true }; + }); + + const response = await handleSnapshotCommands(sessionName, sessionStore, ['accept']); + + expect(response).toBeTruthy(); + expect(response?.ok).toBe(true); + expect(calls).toBe(2); + expect(mockRunnerCommand.mock.calls[0]?.[1]).toMatchObject({ + command: 'alert', + action: 'accept', + timeoutMs: 10_000, + }); +}); + +test('alert accept adds a scoped-snapshot hint after retrying alert-not-found failures', async () => { + const sessionStore = makeSessionStore(); + const sessionName = 'ios-sim'; + sessionStore.set(sessionName, makeSession(sessionName)); + mockRunnerCommand.mockRejectedValue(alertAbsence()); + + let thrown: unknown; + try { + await handleSnapshotCommands(sessionName, sessionStore, ['accept']); + } catch (error) { + thrown = error; + } + + expect(thrown).toBeInstanceOf(AppError); + expect((thrown as AppError).message).toBe('alert not found'); + expect((thrown as AppError).details?.hint).toMatch(/scoped snapshot/i); +}); + +test('alert dismiss retries a typed absence whatever the message says', async () => { + const sessionStore = makeSessionStore(); + const sessionName = 'ios-sim'; + sessionStore.set(sessionName, makeSession(sessionName)); + + let calls = 0; + mockRunnerCommand.mockImplementation(async () => { + calls += 1; + if (calls < 3) throw alertAbsence('no alert present'); + return { dismissed: true }; + }); + + const response = await handleSnapshotCommands(sessionName, sessionStore, ['dismiss']); + + expect(response).toBeTruthy(); + expect(response?.ok).toBe(true); + expect(calls).toBe(3); +}); diff --git a/src/daemon/handlers/__tests__/snapshot-handler.test.ts b/src/daemon/handlers/__tests__/snapshot-handler.test.ts index 0a05f8e320..57244f5777 100644 --- a/src/daemon/handlers/__tests__/snapshot-handler.test.ts +++ b/src/daemon/handlers/__tests__/snapshot-handler.test.ts @@ -34,9 +34,8 @@ vi.mock('../snapshot-interactor-capture.ts', async () => { return { captureSnapshotWithInteractor: fixture.captureSnapshotThroughLegacyDispatchFixture }; }); -vi.mock('../../../platforms/apple/core/runner-client.ts', async (importOriginal) => { - const actual = - await importOriginal(); +vi.mock('@agent-device/platform-apple', async (importOriginal) => { + const actual = await importOriginal(); return { ...actual, runAppleRunnerCommand: vi.fn(async () => ({})), @@ -51,7 +50,7 @@ vi.mock('../../ios-app-session-hint.ts', () => ({ buildIosOpenCommandHint: vi.fn(async () => undefined), })); -import { runAppleRunnerCommand } from '../../../platforms/apple/core/runner-client.ts'; +import { runAppleRunnerCommand } from '@agent-device/platform-apple'; import { buildIosOpenCommandHint } from '../../ios-app-session-hint.ts'; const mockRunnerCommand = vi.mocked(runAppleRunnerCommand); @@ -2118,107 +2117,6 @@ test('wait selector bypasses a fresh matching session snapshot', async () => { ); }); -/** - * Absence as the XCTest runner states it (`ALERT_NOT_FOUND` surfaces as `details.runnerErrorCode`). - * The retry and the fallback hint key on that evidence, never on the message text. - */ -function alertAbsence(message = 'alert not found'): AppError { - return new AppError('COMMAND_FAILED', message, { runnerErrorCode: 'ALERT_NOT_FOUND' }); -} - -test('alert accept retries a typed alert absence and succeeds on the second attempt', async () => { - const sessionStore = makeSessionStore(); - const sessionName = 'ios-sim'; - sessionStore.set(sessionName, makeSession(sessionName, iosSimulatorDevice)); - - let calls = 0; - mockRunnerCommand.mockImplementation(async () => { - calls += 1; - if (calls === 1) throw alertAbsence(); - return { accepted: true }; - }); - - const response = await handleSnapshotCommands({ - req: { token: 't', session: sessionName, command: 'alert', positionals: ['accept'], flags: {} }, - sessionName, - logPath: '/tmp/daemon.log', - sessionStore, - }); - - expect(response).toBeTruthy(); - expect(response?.ok).toBe(true); - expect(calls).toBe(2); - expect(mockRunnerCommand.mock.calls[0]?.[1]).toMatchObject({ - command: 'alert', - action: 'accept', - timeoutMs: 10_000, - }); -}); - -// The non-absence case moved to `src/platforms/apple/__tests__/alert.test.ts` with the retry policy -// itself (R59), where it also covers a failure whose message merely reads like an absence — the -// case this daemon-altitude copy could not distinguish. - -test('alert accept adds a scoped-snapshot hint after retrying alert-not-found failures', async () => { - const sessionStore = makeSessionStore(); - const sessionName = 'ios-sim'; - sessionStore.set(sessionName, makeSession(sessionName, iosSimulatorDevice)); - - mockRunnerCommand.mockRejectedValue(alertAbsence()); - - let thrown: unknown; - try { - await handleSnapshotCommands({ - req: { - token: 't', - session: sessionName, - command: 'alert', - positionals: ['accept'], - flags: {}, - }, - sessionName, - logPath: '/tmp/daemon.log', - sessionStore, - }); - } catch (error) { - thrown = error; - } - - expect(thrown).toBeInstanceOf(AppError); - expect((thrown as AppError).message).toBe('alert not found'); - expect((thrown as AppError).details?.hint).toMatch(/scoped snapshot/i); -}); - -test('alert dismiss retries a typed absence whatever the message says', async () => { - const sessionStore = makeSessionStore(); - const sessionName = 'ios-sim'; - sessionStore.set(sessionName, makeSession(sessionName, iosSimulatorDevice)); - - let calls = 0; - mockRunnerCommand.mockImplementation(async () => { - calls += 1; - if (calls < 3) throw alertAbsence('no alert present'); - return { dismissed: true }; - }); - - const response = await handleSnapshotCommands({ - req: { - token: 't', - session: sessionName, - command: 'alert', - positionals: ['dismiss'], - flags: {}, - }, - sessionName, - logPath: '/tmp/daemon.log', - sessionStore, - }); - - expect(response).toBeTruthy(); - expect(response?.ok).toBe(true); - expect(calls).toBe(3); -}); - test('wait sleep bypasses sessionless runner cleanup wrapper', async () => { const sessionStore = makeSessionStore(); const sessionName = 'ios-sim'; diff --git a/src/daemon/handlers/__tests__/snapshot-session-cleanup.test.ts b/src/daemon/handlers/__tests__/snapshot-session-cleanup.test.ts index 2c5a85c326..83c14def90 100644 --- a/src/daemon/handlers/__tests__/snapshot-session-cleanup.test.ts +++ b/src/daemon/handlers/__tests__/snapshot-session-cleanup.test.ts @@ -1,18 +1,14 @@ import { beforeEach, expect, test, vi } from 'vitest'; -vi.mock('../../../platforms/apple/core/runner-client.ts', async (importOriginal) => ({ - ...(await importOriginal()), +vi.mock('@agent-device/platform-apple', async (importOriginal) => ({ + ...(await importOriginal()), stopIosRunnerSession: vi.fn(async () => {}), -})); -vi.mock('../../../platforms/apple/core/apps.ts', async (importOriginal) => ({ - ...(await importOriginal()), closeIosApp: vi.fn(async () => {}), })); import { withSessionlessRunnerCleanup } from '../snapshot-session.ts'; import { platformResourceCleanup } from '../../../platform-runtime-resource-cleanup.ts'; -import { stopIosRunnerSession } from '../../../platforms/apple/core/runner-client.ts'; -import { closeIosApp } from '../../../platforms/apple/core/apps.ts'; +import { closeIosApp, stopIosRunnerSession } from '@agent-device/platform-apple'; import { IOS_SIMULATOR } from '../../../__tests__/test-utils/device-fixtures.ts'; const mockStopIosRunnerSession = vi.mocked(stopIosRunnerSession); diff --git a/src/daemon/ios-app-session-hint.test.ts b/src/daemon/ios-app-session-hint.test.ts index 9bf11e352a..650a0fa8bb 100644 --- a/src/daemon/ios-app-session-hint.test.ts +++ b/src/daemon/ios-app-session-hint.test.ts @@ -8,7 +8,7 @@ vi.mock('../request/device-inventory-context.ts', async (importOriginal) => ({ ...(await importOriginal()), listLocalDeviceInventory: listBootedIosSimulators, })); -vi.mock('../platforms/apple/core/app-resolution.ts', () => ({ detectSoleRunningIosSimulatorApp })); +vi.mock('@agent-device/platform-apple', () => ({ detectSoleRunningIosSimulatorApp })); import { IOS_DEVICE, IOS_SIMULATOR } from '../__tests__/test-utils/device-fixtures.ts'; import { buildIosOpenCommandHint, resolveSoleForegroundIosApp } from './ios-app-session-hint.ts'; diff --git a/src/platform-runtime-app-inventory-host.ts b/src/platform-runtime-app-inventory-host.ts index 4e6aa20895..45acf8726c 100644 --- a/src/platform-runtime-app-inventory-host.ts +++ b/src/platform-runtime-app-inventory-host.ts @@ -10,7 +10,7 @@ export function createAppInventoryRuntimeHost(): AppInventoryRuntimeHost { return Object.freeze({ apple: Object.freeze({ listApps: async (device: DeviceInfo, filter: AppsFilter) => { - const { listIosApps } = await import('./platforms/apple/core/apps.ts'); + const { listIosApps } = await import('@agent-device/platform-apple'); return mapAppleApps(await listIosApps(device, filter)); }, }), diff --git a/src/platform-runtime-apple-application-tools.ts b/src/platform-runtime-apple-application-tools.ts index 57dd1087dd..4aa7fed59d 100644 --- a/src/platform-runtime-apple-application-tools.ts +++ b/src/platform-runtime-apple-application-tools.ts @@ -13,33 +13,32 @@ export function createAppleApplicationTools(): AppleApplicationTools { return Object.freeze({ resolveOpenTarget: async (device, input) => await resolveAppleOpenTarget(device, input), prewarmRunnerCache: async (device, execution, signal) => { - const { prewarmAppleRunnerCache } = await import('./platforms/apple/core/runner-client.ts'); + const { prewarmAppleRunnerCache } = await import('@agent-device/platform-apple'); await prewarmAppleRunnerCache(device, appleRunnerOptions(execution, signal)); }, prewarmRunnerSession: async (device, execution, signal, propagateError) => { - const { prewarmIosRunnerSession } = await import('./platforms/apple/core/runner-client.ts'); + const { prewarmIosRunnerSession } = await import('@agent-device/platform-apple'); await prewarmIosRunnerSession(device, { ...appleRunnerOptions(execution, signal), propagateError, }); }, notifyRunnerAppRelaunched: async (device, execution, signal) => { - const { notifyIosRunnerAppRelaunched } = - await import('./platforms/apple/core/runner-client.ts'); + const { notifyIosRunnerAppRelaunched } = await import('@agent-device/platform-apple'); await notifyIosRunnerAppRelaunched(device, appleRunnerOptions(execution, signal)); }, stopRunnerSession: async (deviceId) => { - const { stopIosRunnerSession } = await import('./platforms/apple/core/runner-client.ts'); + const { stopIosRunnerSession } = await import('@agent-device/platform-apple'); await stopIosRunnerSession(deviceId); }, scheduleRunnerIdleStop: (deviceId) => { - void import('./platforms/apple/core/runner-client.ts').then(({ scheduleIosRunnerIdleStop }) => + void import('@agent-device/platform-apple').then(({ scheduleIosRunnerIdleStop }) => scheduleIosRunnerIdleStop(deviceId), ); }, prepareRunner: async (device, input, signal) => { const { Deadline } = await import('@agent-device/host-kit/retry'); - const { prepareIosRunner } = await import('./platforms/apple/core/runner-client.ts'); + const { prepareIosRunner } = await import('@agent-device/platform-apple'); const startedAtMs = Date.now(); return await prepareIosRunner(device, { ...appleRunnerOptions(input.execution, signal), @@ -61,11 +60,11 @@ export function createAppleApplicationTools(): AppleApplicationTools { dismissCloseAlerts: async (device, input) => await dismissMacOsCloseAlerts(device, input), detachRunnerSessionsForShutdown: async () => { const { detachIosSimulatorRunnerSessionsForShutdown } = - await import('./platforms/apple/core/runner-client.ts'); + await import('@agent-device/platform-apple'); await detachIosSimulatorRunnerSessionsForShutdown(); }, finalizeRunnerSessionsForShutdown: async () => { - const { stopAllIosRunnerSessions } = await import('./platforms/apple/core/runner-client.ts'); + const { stopAllIosRunnerSessions } = await import('@agent-device/platform-apple'); await stopAllIosRunnerSessions(); }, }); @@ -106,8 +105,7 @@ async function resolveAppleForegroundTarget( device: DeviceInfo, ): Promise { if (!isIosFamily(device) || device.kind !== 'simulator') return undefined; - const { detectSoleRunningIosSimulatorApp } = - await import('./platforms/apple/core/app-resolution.ts'); + const { detectSoleRunningIosSimulatorApp } = await import('@agent-device/platform-apple'); const app = await detectSoleRunningIosSimulatorApp(device); return app ? { appBundleId: app.bundleId, appName: app.name } : undefined; } @@ -119,7 +117,7 @@ async function resolveMacOsSurface( if (!isMacOs(device) || surface === 'app' || surface === 'desktop' || surface === 'menubar') { return {}; } - const { resolveFrontmostMacOsApp } = await import('./platforms/apple/os/macos/helper.ts'); + const { resolveFrontmostMacOsApp } = await import('@agent-device/platform-apple'); const frontmost = await resolveFrontmostMacOsApp(); return { appBundleId: frontmost.bundleId, appName: frontmost.appName }; } @@ -143,7 +141,7 @@ async function dismissMacOsCloseAlerts( input: CloseApplicationFinalizationInput, ): Promise { if (!isMacOs(device)) return; - const { runMacOsAlertAction } = await import('./platforms/apple/os/macos/helper.ts'); + const { runMacOsAlertAction } = await import('@agent-device/platform-apple'); const dismissOptions = input.surface === 'frontmost-app' ? { surface: 'frontmost-app' as const } diff --git a/src/platform-runtime-apple-automation-keep-hot.ts b/src/platform-runtime-apple-automation-keep-hot.ts index d6c4bf4f23..736a82d561 100644 --- a/src/platform-runtime-apple-automation-keep-hot.ts +++ b/src/platform-runtime-apple-automation-keep-hot.ts @@ -3,17 +3,16 @@ import type { DeviceReadinessRuntimeHost } from '@agent-device/contracts/device- export function createAppleAutomationKeepHotHost(): DeviceReadinessRuntimeHost['appleAutomation'] { return Object.freeze({ keepHot: (device) => { - void import('./platforms/apple/core/runner-client.ts') + void import('@agent-device/platform-apple') .then(({ prewarmAppleRunnerCache }) => prewarmAppleRunnerCache(device, {})) .catch(() => {}); }, wasRecentlyObservedBooted: async (device) => { - const { wasSimulatorRecentlyObservedBooted } = - await import('./platforms/apple/core/simulator.ts'); + const { wasSimulatorRecentlyObservedBooted } = await import('@agent-device/platform-apple'); return wasSimulatorRecentlyObservedBooted(device); }, markBooted: (device) => { - void import('./platforms/apple/core/simulator.ts') + void import('@agent-device/platform-apple') .then(({ markSimulatorBooted }) => markSimulatorBooted(device)) .catch(() => {}); }, diff --git a/src/platform-runtime-apple-deployment-executor.test.ts b/src/platform-runtime-apple-deployment-executor.test.ts index 6e9723fd09..ea1c3417ba 100644 --- a/src/platform-runtime-apple-deployment-executor.test.ts +++ b/src/platform-runtime-apple-deployment-executor.test.ts @@ -6,7 +6,7 @@ const { invalidateIosAppResolutionCache, resolveIosApp } = vi.hoisted(() => ({ resolveIosApp: vi.fn(), })); -vi.mock('./platforms/apple/core/app-resolution.ts', () => ({ +vi.mock('@agent-device/platform-apple', () => ({ invalidateIosAppResolutionCache, resolveIosApp, })); diff --git a/src/platform-runtime-apple-deployment-executor.ts b/src/platform-runtime-apple-deployment-executor.ts index 86ae08346e..a77fc99127 100644 --- a/src/platform-runtime-apple-deployment-executor.ts +++ b/src/platform-runtime-apple-deployment-executor.ts @@ -10,17 +10,15 @@ import type { export function createAppleAppDeploymentExecutor(): AppleAppDeploymentExecutor { return Object.freeze({ withInvalidatedAppResolutionCache: async (device, operation) => { - const { invalidateIosAppResolutionCache } = - await import('./platforms/apple/core/app-resolution.ts'); + const { invalidateIosAppResolutionCache } = await import('@agent-device/platform-apple'); return await invalidateIosAppResolutionCache(device, operation); }, prepareArtifact: async (input: MaterializeAppSourceInput, options) => { - const { prepareIosInstallArtifact } = - await import('./platforms/apple/core/install-artifact.ts'); + const { prepareIosInstallArtifact } = await import('@agent-device/platform-apple'); return await prepareIosInstallArtifact(input.source, options); }, resolveAppBundleId: async (device, app) => { - const { resolveIosApp } = await import('./platforms/apple/core/app-resolution.ts'); + const { resolveIosApp } = await import('@agent-device/platform-apple'); return await resolveIosApp(device, app); }, }); diff --git a/src/platform-runtime-apple-physical-readiness.ts b/src/platform-runtime-apple-physical-readiness.ts index 16a1ed9511..e8c73a6714 100644 --- a/src/platform-runtime-apple-physical-readiness.ts +++ b/src/platform-runtime-apple-physical-readiness.ts @@ -5,9 +5,9 @@ export function createApplePhysicalReadinessHost(): DeviceReadinessRuntimeHost[' ensureConnected: async (device, signal) => { signal.throwIfAborted(); try { - const { resolveIosPhysicalDeviceControl } = - await import('./platforms/apple/core/physical-device-control.ts'); - await resolveIosPhysicalDeviceControl(device).ensureReady(device, signal); + const { resolveIosPhysicalDeviceControl } = await import('@agent-device/platform-apple'); + const control = await resolveIosPhysicalDeviceControl(device); + await control.ensureReady(device, signal); signal.throwIfAborted(); } catch (error) { signal.throwIfAborted(); diff --git a/src/platform-runtime-apple-resources.ts b/src/platform-runtime-apple-resources.ts index 50fa0d02db..569098cf6b 100644 --- a/src/platform-runtime-apple-resources.ts +++ b/src/platform-runtime-apple-resources.ts @@ -3,7 +3,7 @@ import type { ElementSelectorKey } from '@agent-device/contracts/interactor-type import type { DeviceInfo } from '@agent-device/kernel/device'; export async function inspectAppleRunnerSession(deviceId: string) { - const { getRunnerSessionSnapshot } = await import('./platforms/apple/core/runner-client.ts'); + const { getRunnerSessionSnapshot } = await import('@agent-device/platform-apple'); return getRunnerSessionSnapshot(deviceId); } @@ -13,7 +13,6 @@ export async function queryAppleRuntimeSelector( appBundleId: string | undefined, options: AppleRunnerRequestOptions, ): Promise> { - const { queryAppleRunnerSelector } = - await import('./platforms/apple/core/runner-selector-query.ts'); + const { queryAppleRunnerSelector } = await import('@agent-device/platform-apple'); return await queryAppleRunnerSelector(device, selector, appBundleId, options); } diff --git a/src/platform-runtime-apple-runner-owner.ts b/src/platform-runtime-apple-runner-owner.ts index 58ce45d2aa..33b4e8fc1c 100644 --- a/src/platform-runtime-apple-runner-owner.ts +++ b/src/platform-runtime-apple-runner-owner.ts @@ -2,7 +2,6 @@ export async function configureAppleRunnerLeaseOwnerStateDir( stateDir: string | undefined, ): Promise { - const { setRunnerLeaseOwnerStateDir } = - await import('./platforms/apple/core/runner-owner-state.ts'); - setRunnerLeaseOwnerStateDir(stateDir); + const { setRunnerLeaseOwnerStateDir } = await import('@agent-device/platform-apple'); + await setRunnerLeaseOwnerStateDir(stateDir); } diff --git a/src/platform-runtime-apple-tool-host.test.ts b/src/platform-runtime-apple-tool-host.test.ts index 64290622d3..4343ee6efc 100644 --- a/src/platform-runtime-apple-tool-host.test.ts +++ b/src/platform-runtime-apple-tool-host.test.ts @@ -3,7 +3,7 @@ import { createLocalAppleToolProvider, type AppleToolProvider, withAppleToolProvider, -} from './platforms/apple/core/tool-provider.ts'; +} from '@agent-device/platform-apple'; import { createAppleToolHost } from './platform-runtime-apple-tool-host.ts'; test('Apple tool host uses a full scoped provider when local xcrun is unavailable', async () => { diff --git a/src/platform-runtime-apple-tool-host.ts b/src/platform-runtime-apple-tool-host.ts index 73dc42a932..b81c7e759b 100644 --- a/src/platform-runtime-apple-tool-host.ts +++ b/src/platform-runtime-apple-tool-host.ts @@ -4,18 +4,22 @@ export function createAppleToolHost(): AppleToolHost { return Object.freeze({ isXcrunAvailable: async (signal?: AbortSignal) => { const { resolveAppleToolProvider } = await awaitPreservingAbortReason( - async () => await import('./platforms/apple/core/tool-provider.ts'), + async () => await import('@agent-device/platform-apple'), + signal, + ); + const provider = await awaitPreservingAbortReason( + async () => await resolveAppleToolProvider(), signal, ); const available = await awaitPreservingAbortReason( - async () => await resolveAppleToolProvider().whichCommand('xcrun'), + async () => await provider.whichCommand('xcrun'), signal, ); return available; }, run: async (request, signal) => { const { runXcrun } = await awaitPreservingAbortReason( - async () => await import('./platforms/apple/core/tool-provider.ts'), + async () => await import('@agent-device/platform-apple'), signal, ); const result = await awaitPreservingAbortReason( diff --git a/src/platform-runtime-audio-probe-host.ts b/src/platform-runtime-audio-probe-host.ts index 3bd307714c..fb641b4207 100644 --- a/src/platform-runtime-audio-probe-host.ts +++ b/src/platform-runtime-audio-probe-host.ts @@ -43,8 +43,7 @@ export function createAudioProbeRuntimeHost( bucketMs: number; statusPath: string; }): Promise => { - const { startMacOsAudioProbeProcess } = - await import('./platforms/apple/os/macos/helper.ts'); + const { startMacOsAudioProbeProcess } = await import('@agent-device/platform-apple'); const probe = await startMacOsAudioProbeProcess(input); const marker = await resolveManagedProcessIdentity(probe.child.pid ?? undefined); return Object.freeze({ diff --git a/src/platform-runtime-daemon-owner-cleanup.test.ts b/src/platform-runtime-daemon-owner-cleanup.test.ts index 3f11a9ceac..949567d3ed 100644 --- a/src/platform-runtime-daemon-owner-cleanup.test.ts +++ b/src/platform-runtime-daemon-owner-cleanup.test.ts @@ -1,21 +1,19 @@ import { expect, test, vi } from 'vitest'; const mocks = vi.hoisted(() => ({ - adapter: { terminateProcess: vi.fn() }, cleanupRunnerLeasesForOwner: vi.fn(async () => undefined), })); -vi.mock('./platforms/apple/core/runner-client.ts', () => ({ +vi.mock('@agent-device/platform-apple', () => ({ cleanupRunnerLeasesForOwner: mocks.cleanupRunnerLeasesForOwner, - runnerLeaseCleanupAdapter: mocks.adapter, })); import { createDaemonOwnerCleanup } from './platform-runtime-daemon-owner-cleanup.ts'; -test('composes owner cleanup with the Apple runner lease adapter', async () => { +test('composes owner cleanup with the Apple runner lease facade', async () => { const owner = { pid: 42, startTime: 'process-start' }; await createDaemonOwnerCleanup().cleanup(owner); - expect(mocks.cleanupRunnerLeasesForOwner).toHaveBeenCalledWith(owner, mocks.adapter); + expect(mocks.cleanupRunnerLeasesForOwner).toHaveBeenCalledWith(owner); }); diff --git a/src/platform-runtime-daemon-owner-cleanup.ts b/src/platform-runtime-daemon-owner-cleanup.ts index a1f974508f..a500948975 100644 --- a/src/platform-runtime-daemon-owner-cleanup.ts +++ b/src/platform-runtime-daemon-owner-cleanup.ts @@ -7,9 +7,8 @@ import type { DaemonOwnerCleanup } from '@agent-device/contracts/daemon-owner-cl export function createDaemonOwnerCleanup(): DaemonOwnerCleanup { return Object.freeze({ cleanup: async (owner) => { - const { cleanupRunnerLeasesForOwner, runnerLeaseCleanupAdapter } = - await import('./platforms/apple/core/runner-client.ts'); - await cleanupRunnerLeasesForOwner(owner, runnerLeaseCleanupAdapter); + const { cleanupRunnerLeasesForOwner } = await import('@agent-device/platform-apple'); + await cleanupRunnerLeasesForOwner(owner); }, }); } diff --git a/src/platform-runtime-device-ready.ts b/src/platform-runtime-device-ready.ts index 3657d8fc11..2029d8be6f 100644 --- a/src/platform-runtime-device-ready.ts +++ b/src/platform-runtime-device-ready.ts @@ -17,7 +17,7 @@ export async function ensureLocalPlatformDeviceReady( ): Promise { if (isIosFamily(device)) { if (device.kind === 'simulator') { - const { ensureBootedSimulator } = await import('./platforms/apple/core/simulator.ts'); + const { ensureBootedSimulator } = await import('@agent-device/platform-apple'); await ensureBootedSimulator(device, { deviceHub: options.deviceHub, focusExisting: options.focusExisting, @@ -26,9 +26,9 @@ export async function ensureLocalPlatformDeviceReady( return true; } if (device.kind === 'device') { - const { resolveIosPhysicalDeviceControl } = - await import('./platforms/apple/core/physical-device-control.ts'); - await resolveIosPhysicalDeviceControl(device).ensureReady(device); + const { resolveIosPhysicalDeviceControl } = await import('@agent-device/platform-apple'); + const control = await resolveIosPhysicalDeviceControl(device); + await control.ensureReady(device); return true; } } diff --git a/src/platform-runtime-host-diagnostics.ts b/src/platform-runtime-host-diagnostics.ts index 01aa33cc42..f38ae7249a 100644 --- a/src/platform-runtime-host-diagnostics.ts +++ b/src/platform-runtime-host-diagnostics.ts @@ -30,7 +30,7 @@ export function createHostDiagnostics(): HostDiagnostics { return await harmonyToolchainCheck(); } if (platform === 'ios' || platform === 'macos' || platform === 'apple') { - const { appleToolchainCheck } = await import('./platforms/apple/doctor.ts'); + const { appleToolchainCheck } = await import('@agent-device/platform-apple'); return await appleToolchainCheck(); } return undefined; @@ -52,7 +52,7 @@ export function createHostDiagnostics(): HostDiagnostics { context: HostDiagnosticsContext, ): Promise => { if (device.platform !== 'apple') return undefined; - const { appleRunnerWarmupCheck } = await import('./platforms/apple/doctor.ts'); + const { appleRunnerWarmupCheck } = await import('@agent-device/platform-apple'); return await appleRunnerWarmupCheck(device, context); }, }); diff --git a/src/platform-runtime-host.test.ts b/src/platform-runtime-host.test.ts index 1634c0aeb6..aea485d063 100644 --- a/src/platform-runtime-host.test.ts +++ b/src/platform-runtime-host.test.ts @@ -6,7 +6,7 @@ const toolchains = vi.hoisted(() => ({ appleRuns: [] as Array<{ args: string[]; signal?: AbortSignal }>, })); -vi.mock('./platforms/apple/core/tool-provider.ts', () => { +vi.mock('@agent-device/platform-apple', () => { toolchains.appleEvaluations += 1; return { resolveAppleToolProvider: () => ({ diff --git a/src/platform-runtime-host.ts b/src/platform-runtime-host.ts index 812167b24f..269c3568ea 100644 --- a/src/platform-runtime-host.ts +++ b/src/platform-runtime-host.ts @@ -44,8 +44,8 @@ export function createDeviceInventoryHost(): DeviceInventoryHost { homeDirectory: os.homedir(), observations: Object.freeze({ deviceBooted: async (device: DeviceInfo) => { - const { markSimulatorBooted } = await import('./platforms/apple/core/simulator.ts'); - markSimulatorBooted(device); + const { markSimulatorBooted } = await import('@agent-device/platform-apple'); + await markSimulatorBooted(device); }, }), }); diff --git a/src/platform-runtime-open-target.ts b/src/platform-runtime-open-target.ts index 6f6d922188..935686542b 100644 --- a/src/platform-runtime-open-target.ts +++ b/src/platform-runtime-open-target.ts @@ -35,8 +35,7 @@ export async function resolveSoleForegroundIosApp( const [soleBootedDevice] = booted; if (!soleBootedDevice) return undefined; - const { detectSoleRunningIosSimulatorApp } = - await import('./platforms/apple/core/app-resolution.ts'); + const { detectSoleRunningIosSimulatorApp } = await import('@agent-device/platform-apple'); const app = await detectSoleRunningIosSimulatorApp(soleBootedDevice); return app ? { device: soleBootedDevice, app } : undefined; } catch (error) { @@ -160,7 +159,7 @@ async function tryResolveIosSimulatorDeepLinkBundleId( openTarget: string, ): Promise { try { - const { resolveIosSimulatorDeepLinkBundleId } = await import('./platforms/apple/core/apps.ts'); + const { resolveIosSimulatorDeepLinkBundleId } = await import('@agent-device/platform-apple'); return await resolveIosSimulatorDeepLinkBundleId(device, openTarget); } catch { return undefined; @@ -172,7 +171,7 @@ async function tryResolveIosAppBundleId( openTarget: string, ): Promise { try { - const { resolveIosApp } = await import('./platforms/apple/core/apps.ts'); + const { resolveIosApp } = await import('@agent-device/platform-apple'); return await resolveIosApp(device, openTarget); } catch { return undefined; diff --git a/src/platform-runtime-perf-capture-host.ts b/src/platform-runtime-perf-capture-host.ts index 8774f9e8d2..eeff821084 100644 --- a/src/platform-runtime-perf-capture-host.ts +++ b/src/platform-runtime-perf-capture-host.ts @@ -23,7 +23,7 @@ import type { AndroidNativePerfStartResult, AndroidNativePerfStopResult, } from '@agent-device/platform-android/mechanics'; -import type { AppleXctracePerfCapture } from './platforms/apple/core/perf-xctrace.ts'; +import type { AppleXctracePerfCapture } from '@agent-device/platform-apple'; import { inspectManagedProcess, resolveManagedProcessIdentity, @@ -32,7 +32,7 @@ import { import { loadAndroidMechanics } from './platform-runtime-android-mechanics.ts'; const loadAndroidPerf = loadAndroidMechanics; -const loadAppleXctrace = async () => await import('./platforms/apple/core/perf-xctrace.ts'); +const loadAppleXctrace = async () => await import('@agent-device/platform-apple'); type PerfCaptureDescriptor = | Readonly<{ diff --git a/src/platform-runtime-perf-host.ts b/src/platform-runtime-perf-host.ts index e01cf133d8..5defa73dcb 100644 --- a/src/platform-runtime-perf-host.ts +++ b/src/platform-runtime-perf-host.ts @@ -15,8 +15,9 @@ import { loadAndroidMechanics } from './platform-runtime-android-mechanics.ts'; const loadAndroidPerf = loadAndroidMechanics; const loadAndroidFramePerf = loadAndroidMechanics; const loadAndroidNativePerf = loadAndroidMechanics; -const loadApplePerf = async () => await import('./platforms/apple/core/perf.ts'); -const loadAppleXctrace = async () => await import('./platforms/apple/core/perf-xctrace.ts'); +const loadApplePerf = async () => await import('@agent-device/platform-apple'); +const loadApplePerfMechanics = async () => await import('@agent-device/platform-apple/perf'); +const loadAppleXctrace = async () => await import('@agent-device/platform-apple'); const loadHarmonyPerf = async () => await import('@agent-device/platform-harmonyos'); /** Lazy host-only mechanics. Platform packages own facts, policy, and operation construction. */ @@ -25,13 +26,13 @@ export function createPerfRuntimeHost(): PerfRuntimeHost { sampleFrames: async (device, appId) => await (await loadApplePerf()).sampleAppleFramePerf(device, appId), frameSampling: async (device) => - (await loadApplePerf()).buildAppleFrameSamplingMetadata(device), + (await loadApplePerfMechanics()).buildAppleFrameSamplingMetadata(device), sampleMemory: async (device, appId) => await (await loadApplePerf()).sampleAppleMemoryPerf(device, appId), memorySampling: async (device) => - (await loadApplePerf()).buildAppleMemorySamplingMetadata(device), + (await loadApplePerfMechanics()).buildAppleMemorySamplingMetadata(device), memorySnapshotSupport: async (device) => - (await loadApplePerf()).buildAppleMemorySnapshotSupport(device), + (await loadApplePerfMechanics()).buildAppleMemorySnapshotSupport(device), captureMemorySnapshot: async (device, appId, outputPath) => await (await loadApplePerf()).captureAppleMemorySnapshot(device, appId, outputPath), start: async (device, owner, input) => await startApplePerfCapture(device, owner, input), diff --git a/src/platform-runtime-resource-cleanup.ts b/src/platform-runtime-resource-cleanup.ts index d16a1651d5..a6353b6f29 100644 --- a/src/platform-runtime-resource-cleanup.ts +++ b/src/platform-runtime-resource-cleanup.ts @@ -63,10 +63,10 @@ export const platformResourceCleanup: PlatformResourceCleanup = Object.freeze({ async cleanupSessionlessExecutionHost(device) { if (!isIosFamily(device)) return; const { resolveRunnerAppBundleId, stopIosRunnerSession } = - await import('./platforms/apple/core/runner-client.ts'); + await import('@agent-device/platform-apple'); await stopIosRunnerSession(device.id); - const bundleId = resolveRunnerAppBundleId(); - const { closeIosApp } = await import('./platforms/apple/core/apps.ts'); + const bundleId = await resolveRunnerAppBundleId(); + const { closeIosApp } = await import('@agent-device/platform-apple'); await closeIosApp(device, bundleId).catch((error) => { emitDiagnostic({ level: 'debug', diff --git a/src/platform-runtime-runtime-hints.ts b/src/platform-runtime-runtime-hints.ts index 6a12be8a68..3b2a4b3da2 100644 --- a/src/platform-runtime-runtime-hints.ts +++ b/src/platform-runtime-runtime-hints.ts @@ -313,10 +313,10 @@ async function runIosSimulatorRuntimeHintCommand( options?: Readonly<{ allowFailure?: boolean }>, ): Promise { const [{ buildSimctlArgsForDevice }, { runXcrun }] = await Promise.all([ - import('./platforms/apple/core/simctl.ts'), - import('./platforms/apple/core/tool-provider.ts'), + import('@agent-device/platform-apple'), + import('@agent-device/platform-apple'), ]); - await runXcrun(buildSimctlArgsForDevice(device, args), options); + await runXcrun(await buildSimctlArgsForDevice(device, args), options); } function normalizeAndroidPrefsXml(xml: string): string { diff --git a/src/platform-runtime-screen-recording-apple-runner-host.test.ts b/src/platform-runtime-screen-recording-apple-runner-host.test.ts index 8e480f83cf..22faa7450a 100644 --- a/src/platform-runtime-screen-recording-apple-runner-host.test.ts +++ b/src/platform-runtime-screen-recording-apple-runner-host.test.ts @@ -7,7 +7,7 @@ import { withAppleRunnerScreenRecordingTransport } from './platform-runtime-scre import { withAppleSimulatorScreenRecordingTransport } from './platform-runtime-screen-recording-apple-transport.ts'; const runnerClient = vi.hoisted(() => ({ run: vi.fn() })); -vi.mock('./platforms/apple/core/runner-client.ts', () => ({ +vi.mock('@agent-device/platform-apple', () => ({ runAppleRunnerCommand: runnerClient.run, IOS_RUNNER_CONTAINER_BUNDLE_IDS: ['com.callstack.agentdevice.runner'], })); diff --git a/src/platform-runtime-screen-recording-apple-runner-host.ts b/src/platform-runtime-screen-recording-apple-runner-host.ts index a361cf55bf..376ebf4f1a 100644 --- a/src/platform-runtime-screen-recording-apple-runner-host.ts +++ b/src/platform-runtime-screen-recording-apple-runner-host.ts @@ -62,9 +62,9 @@ export async function retrieveAppleRunnerRecording( signal?: AbortSignal, ): Promise { signal?.throwIfAborted(); - const { resolveIosPhysicalDeviceControl } = - await import('./platforms/apple/core/physical-device-control.ts'); - await resolveIosPhysicalDeviceControl(device).copyRunnerFile(device, remotePath, outputPath); + const { resolveIosPhysicalDeviceControl } = await import('@agent-device/platform-apple'); + const control = await resolveIosPhysicalDeviceControl(device); + await control.copyRunnerFile(device, remotePath, outputPath); signal?.throwIfAborted(); } @@ -88,7 +88,7 @@ export async function captureAppleClockAnchor( ) { return undefined; } - const { runAppleRunnerCommand } = await import('./platforms/apple/core/runner-client.ts'); + const { runAppleRunnerCommand } = await import('@agent-device/platform-apple'); const result = await runAppleRunnerCommand( device, { command: 'snapshot', appBundleId, interactiveOnly: true, depth: 1 }, diff --git a/src/platform-runtime-screen-recording-apple-runner-transport.test.ts b/src/platform-runtime-screen-recording-apple-runner-transport.test.ts index 984bc2e830..4904b6b8d5 100644 --- a/src/platform-runtime-screen-recording-apple-runner-transport.test.ts +++ b/src/platform-runtime-screen-recording-apple-runner-transport.test.ts @@ -9,7 +9,7 @@ const runner = vi.hoisted(() => ({ snapshot: vi.fn(), })); -vi.mock('./platforms/apple/core/runner-client.ts', () => ({ +vi.mock('@agent-device/platform-apple', () => ({ runAppleRunnerCommand: runner.run, getRunnerSessionSnapshot: runner.snapshot, })); diff --git a/src/platform-runtime-screen-recording-apple-runner-transport.ts b/src/platform-runtime-screen-recording-apple-runner-transport.ts index 22cfe768da..2ce24933ba 100644 --- a/src/platform-runtime-screen-recording-apple-runner-transport.ts +++ b/src/platform-runtime-screen-recording-apple-runner-transport.ts @@ -42,7 +42,7 @@ const localTransport: AppleRunnerScreenRecordingTransport = Object.freeze({ start: startLocalAppleRunnerRecording, inspect: async (device, runnerSessionId) => await inspectLocalRunner(device, runnerSessionId), stop: async ({ device, runnerSessionId, appBundleId, signal }) => { - const { runAppleRunnerCommand } = await import('./platforms/apple/core/runner-client.ts'); + const { runAppleRunnerCommand } = await import('@agent-device/platform-apple'); await runAppleRunnerCommand( device, { command: 'recordStop', appBundleId }, @@ -59,7 +59,7 @@ async function startLocalAppleRunnerRecording({ signal, }: AppleRunnerScreenRecordingStartRequest): Promise { const { getRunnerSessionSnapshot, runAppleRunnerCommand } = - await import('./platforms/apple/core/runner-client.ts'); + await import('@agent-device/platform-apple'); const recordingFileName = `agent-device-recording-${Date.now()}.mp4`; const remotePath = device.appleOs === 'macos' || device.kind !== 'device' ? undefined : `tmp/${recordingFileName}`; @@ -73,7 +73,7 @@ async function startLocalAppleRunnerRecording({ }, { signal }, ); - const session = getRunnerSessionSnapshot(device.id); + const session = await getRunnerSessionSnapshot(device.id); if (!session?.alive) { throw new Error('Apple runner recording did not expose a durable runner session identity'); } @@ -147,15 +147,17 @@ async function inspectLocalRunner( runnerSessionId: string, ): Promise { const { getRunnerSessionSnapshot, readStaleRunnerLease, verifyLeaseRunnerPidIdentity } = - await import('./platforms/apple/core/runner-client.ts'); - const active = getRunnerSessionSnapshot(device.id); + await import('@agent-device/platform-apple'); + const active = await getRunnerSessionSnapshot(device.id); if (active) { if (!active.alive) return 'missing'; return active.sessionId === runnerSessionId ? 'owned-alive' : 'ownership-lost'; } - const lease = readStaleRunnerLease(device.id); + const lease = await readStaleRunnerLease(device.id); if (!lease) return 'missing'; if (lease.sessionId !== runnerSessionId) return 'ownership-lost'; if (lease.runnerPid === null || !isProcessAlive(lease.runnerPid)) return 'missing'; - return verifyLeaseRunnerPidIdentity(lease, lease.runnerPid) ? 'owned-alive' : 'ownership-lost'; + return (await verifyLeaseRunnerPidIdentity(lease, lease.runnerPid)) + ? 'owned-alive' + : 'ownership-lost'; } diff --git a/src/platform-runtime-screen-recording-apple-transport.ts b/src/platform-runtime-screen-recording-apple-transport.ts index 4ef8f91c32..2579a5157f 100644 --- a/src/platform-runtime-screen-recording-apple-transport.ts +++ b/src/platform-runtime-screen-recording-apple-transport.ts @@ -26,13 +26,13 @@ const localTransport: AppleSimulatorScreenRecordingTransport = Object.freeze({ mode: 'local', async start({ device, outputPath, signal }) { const [{ buildSimctlArgsForDevice }, { runCmdBackground }] = await Promise.all([ - import('./platforms/apple/core/simctl.ts'), + import('@agent-device/platform-apple'), import('@agent-device/host-kit/command'), ]); signal?.throwIfAborted(); return runCmdBackground( 'xcrun', - buildSimctlArgsForDevice(device, ['io', device.id, 'recordVideo', outputPath]), + await buildSimctlArgsForDevice(device, ['io', device.id, 'recordVideo', outputPath]), { allowFailure: true }, ); }, diff --git a/src/platform-runtime/request-providers.ts b/src/platform-runtime/request-providers.ts index 05d12b2f93..3c1a022875 100644 --- a/src/platform-runtime/request-providers.ts +++ b/src/platform-runtime/request-providers.ts @@ -13,7 +13,7 @@ import type { AppleRunnerProvider, } from '@agent-device/platform-apple/runner'; import type { WebProvider } from '@agent-device/platform-web'; -import type { AppleToolProvider } from '../platforms/apple/core/tool-provider.ts'; +import type { AppleToolProvider } from '@agent-device/platform-apple'; import type { LinuxToolProvider } from '@agent-device/platform-linux'; import type { VegaToolProvider } from '@agent-device/platform-vega'; import type { DeviceInfo } from '@agent-device/kernel/device'; @@ -224,7 +224,7 @@ const REQUEST_PLATFORM_PROVIDER_DESCRIPTORS = [ }, async appendWrapper(scopedProviders, wrappers) { if (!scopedProviders.appleTool?.provider) return; - const { withAppleToolProvider } = await import('../platforms/apple/core/tool-provider.ts'); + const { withAppleToolProvider } = await import('@agent-device/platform-apple'); appendRequestProviderWrapper(wrappers, scopedProviders.appleTool, withAppleToolProvider); }, }, diff --git a/src/platforms/__tests__/install-source.test.ts b/src/platforms/__tests__/install-source.test.ts index 6e2c5c4e75..cb594c6856 100644 --- a/src/platforms/__tests__/install-source.test.ts +++ b/src/platforms/__tests__/install-source.test.ts @@ -17,12 +17,13 @@ import { isBlockedIpAddress, isBlockedSourceHostname, } from '@agent-device/provision-kit/install-source-network'; +import * as androidManifest from '../android/manifest.ts'; import { prepareAndroidInstallArtifact } from '@agent-device/platform-android/mechanics'; -import { prepareIosInstallArtifact } from '../apple/core/install-artifact.ts'; import { createLocalAppleToolProvider, + prepareIosInstallArtifact, withAppleToolProvider, -} from '../apple/core/tool-provider.ts'; +} from '@agent-device/platform-apple'; import { ANDROID_INSTALL_SOURCE_CONTRACT_EVIDENCE } from './install-source.coverage.ts'; import { mkdtempForTest } from '../../__tests__/test-utils/tmp-dir.ts'; import * as networkTransport from '@agent-device/provision-kit/install-source-network-transport'; diff --git a/src/platforms/apple/core/__tests__/apple-core-stub-helpers.ts b/src/platforms/apple/core/__tests__/apple-core-stub-helpers.ts deleted file mode 100644 index be05d03e83..0000000000 --- a/src/platforms/apple/core/__tests__/apple-core-stub-helpers.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { promises as fs } from 'node:fs'; -import path from 'node:path'; -import type { DeviceInfo } from '@agent-device/kernel/device'; -import { mkdtempForTest } from '../../../../__tests__/test-utils/tmp-dir.ts'; - -export const IOS_TEST_DEVICE: DeviceInfo = { - platform: 'apple', - id: 'ios-device-1', - name: 'iPhone Device', - kind: 'device', - booted: true, -}; - -export const IOS_TEST_SIMULATOR: DeviceInfo = { - platform: 'apple', - id: 'sim-1', - name: 'iPhone 17 Pro', - kind: 'simulator', - booted: true, -}; - -export const MACOS_TEST_DEVICE: DeviceInfo = { - platform: 'apple', - appleOs: 'macos', - id: 'host-macos-local', - name: 'Mac', - kind: 'device', - target: 'desktop', - booted: true, -}; - -export const TVOS_TEST_SIMULATOR: DeviceInfo = { - platform: 'apple', - id: 'tvos-sim-1', - name: 'Apple TV', - kind: 'simulator', - target: 'tv', - booted: true, -}; - -export async function withMockedXcrun( - tempPrefix: string, - script: string, - run: (ctx: { tmpDir: string; argsLogPath: string; device: DeviceInfo }) => Promise, -): Promise { - const tmpDir = await mkdtempForTest(tempPrefix); - const xcrunPath = path.join(tmpDir, 'xcrun'); - const argsLogPath = path.join(tmpDir, 'args.log'); - const scriptWithPrivacyHelp = injectDefaultPrivacyHelp(script); - await fs.writeFile(xcrunPath, scriptWithPrivacyHelp, 'utf8'); - await fs.chmod(xcrunPath, 0o755); - - const previousPath = process.env.PATH; - const previousArgsFile = process.env.AGENT_DEVICE_TEST_ARGS_FILE; - process.env.PATH = `${tmpDir}${path.delimiter}${previousPath ?? ''}`; - process.env.AGENT_DEVICE_TEST_ARGS_FILE = argsLogPath; - - try { - await run({ tmpDir, argsLogPath, device: IOS_TEST_DEVICE }); - } finally { - process.env.PATH = previousPath; - if (previousArgsFile === undefined) { - delete process.env.AGENT_DEVICE_TEST_ARGS_FILE; - } else { - process.env.AGENT_DEVICE_TEST_ARGS_FILE = previousArgsFile; - } - await fs.rm(tmpDir, { recursive: true, force: true }); - } -} - -function injectDefaultPrivacyHelp(script: string): string { - if (script.includes('AGENT_DEVICE_CUSTOM_PRIVACY_HELP')) return script; - const helpBlock = `if [ "$1" = "simctl" ] && [ "$2" = "privacy" ] && [ "$3" = "help" ]; then - cat <<'HELP' -Usage: simctl privacy [] - - service - The service: - all - Apply the action to all services. - calendar - Allow access to calendar. - contacts-limited - Allow access to basic contact info. - contacts - Allow access to full contact details. - location - Allow access to location services when app is in use. - location-always - Allow access to location services at all times. - photos-add - Allow adding photos to the photo library. - photos - Allow full access to the photo library. - media-library - Allow access to the media library. - microphone - Allow access to audio input. - motion - Allow access to motion and fitness data. - reminders - Allow access to reminders. - siri - Allow use of the app with Siri. - camera - Allow access to camera. - notifications - Allow access to notifications. -HELP - exit 0 -fi -`; - const shebang = '#!/bin/sh\n'; - if (!script.startsWith(shebang)) return `${shebang}${helpBlock}${script}`; - return `${shebang}${helpBlock}${script.slice(shebang.length)}`; -} diff --git a/src/platforms/apple/plugin.ts b/src/platforms/apple/plugin.ts deleted file mode 100644 index 70f036655c..0000000000 --- a/src/platforms/apple/plugin.ts +++ /dev/null @@ -1,23 +0,0 @@ -import type { PlatformPlugin } from '@agent-device/contracts/platform-plugin'; -import type { DeviceInfo } from '@agent-device/kernel/device'; -import type { RunnerContext } from '@agent-device/contracts/interactor-types'; - -// The Apple plugin wraps today's existing interactor factory lazily. -// `as const satisfies PlatformPlugin` preserves -// the plugin's literal `platforms` tuple so the registry totality assertion (in -// `core/interactors/register-builtins.ts`) is a real compile-time check. - -export const applePlugin = { - id: 'apple', - // Apple owns the single collapsed `apple` platform; the `appleOs` field - // discriminates the OS (ADR-0009 / issue #979). - platforms: ['apple'], - familySelector: 'apple', - // Declares the platform-gated request provider resolvers the Apple family owns: the - // runner + tool providers (formerly gated by `isApplePlatform(device.platform)`). - providers: { platformGatedResolvers: ['appleRunnerProvider', 'appleToolProvider'] }, - createInteractor: async (device: DeviceInfo, runner: RunnerContext) => { - const { createAppleInteractor } = await import('./interactor.ts'); - return createAppleInteractor(device, runner); - }, -} as const satisfies PlatformPlugin; diff --git a/src/sdk/limrun-runtime-dependencies.test.ts b/src/sdk/limrun-runtime-dependencies.test.ts index 2a2f39794e..4d446bf185 100644 --- a/src/sdk/limrun-runtime-dependencies.test.ts +++ b/src/sdk/limrun-runtime-dependencies.test.ts @@ -27,19 +27,17 @@ vi.mock('@agent-device/platform-android/mechanics', async (importOriginal) => { }; }); -vi.mock('../platforms/apple/core/app-resolution.ts', () => { - moduleLoads.appleAppResolution += 1; - return { - resolveIosAppAlias: vi.fn((app: string) => `resolved:${app}`), - }; -}); - -vi.mock('../platforms/apple/core/install-artifact.ts', () => { - moduleLoads.appleInstallArtifact += 1; - return { - readIosBundleInfo: vi.fn(async () => ({ appName: 'Example' })), - }; -}); +vi.mock('@agent-device/platform-apple', async (importOriginal) => ({ + ...(await importOriginal()), + resolveIosAppAlias: vi.fn((app: string) => { + moduleLoads.appleAppResolution += 1; + return `resolved:${app}`; + }), + readIosBundleInfo: vi.fn(async () => { + moduleLoads.appleInstallArtifact += 1; + return { appName: 'Example' }; + }), +})); test('Limrun construction defers operation and opposite-platform helper modules', async () => { const { LimrunRuntime } = await import('../provider-limrun-runtime.ts'); diff --git a/src/sdk/limrun-runtime-dependencies.ts b/src/sdk/limrun-runtime-dependencies.ts index cf4cf6d657..526bb774b7 100644 --- a/src/sdk/limrun-runtime-dependencies.ts +++ b/src/sdk/limrun-runtime-dependencies.ts @@ -91,11 +91,11 @@ export function createLimrunRuntimeDependencies(): LimrunRuntimeDependencies { }, ios: { resolveAppAlias: async (app) => { - const { resolveIosAppAlias } = await import('../platforms/apple/core/app-resolution.ts'); + const { resolveIosAppAlias } = await import('@agent-device/platform-apple'); return resolveIosAppAlias(app); }, readBundleAppName: async (appPath) => { - const { readIosBundleInfo } = await import('../platforms/apple/core/install-artifact.ts'); + const { readIosBundleInfo } = await import('@agent-device/platform-apple'); return (await readIosBundleInfo(appPath)).appName; }, }, diff --git a/src/utils/__tests__/interactors.test.ts b/src/utils/__tests__/interactors.test.ts index 27385808a2..ee37320a49 100644 --- a/src/utils/__tests__/interactors.test.ts +++ b/src/utils/__tests__/interactors.test.ts @@ -1,21 +1,14 @@ import { afterEach, beforeEach, test, vi } from 'vitest'; import assert from 'node:assert/strict'; -import type { RunnerCommand } from '@agent-device/platform-apple/runner'; +import { withAppleRunnerProvider, type RunnerCommand } from '@agent-device/platform-apple/runner'; import type { DeviceInfo } from '@agent-device/kernel/device'; import { AppError } from '@agent-device/kernel/errors'; import type { Interactor, RunnerContext } from '@agent-device/contracts/interactor-types'; import { setActiveProviderDeviceRuntimes } from '../../provider-device-runtime.ts'; import type { ProviderDeviceRuntime } from '@agent-device/contracts/device'; -vi.mock('../../platforms/apple/core/runner-client.ts', async (importOriginal) => { - const actual = - await importOriginal(); - return { ...actual, runAppleRunnerCommand: vi.fn() }; -}); - import { getInteractor, getLocalInteractor } from '../../core/interactors.ts'; -import { resolveAppleBackRunnerCommand } from '../../platforms/apple/interactions.ts'; -import { runAppleRunnerCommand } from '../../platforms/apple/core/runner-client.ts'; +import { resolveAppleBackRunnerCommand } from '@agent-device/platform-apple'; const iosSimulator: DeviceInfo = { platform: 'apple', @@ -34,7 +27,20 @@ const tvOsSimulator: DeviceInfo = { booted: true, }; -const mockRunAppleRunnerCommand = vi.mocked(runAppleRunnerCommand); +const mockRunAppleRunnerCommand = vi.fn(); + +function withoutRunnerCommandId(command: RunnerCommand): Omit { + const { commandId: _commandId, ...withoutId } = command; + return withoutId; +} + +async function withRunner(operation: () => Promise, device: DeviceInfo = iosSimulator) { + return await withAppleRunnerProvider( + mockRunAppleRunnerCommand, + { deviceId: device.id }, + operation, + ); +} beforeEach(() => { vi.restoreAllMocks(); @@ -45,13 +51,13 @@ afterEach(() => { setActiveProviderDeviceRuntimes([]); }); -test('resolveAppleBackRunnerCommand defaults plain back to in-app navigation', () => { - assert.equal(resolveAppleBackRunnerCommand(), 'backInApp'); +test('resolveAppleBackRunnerCommand defaults plain back to in-app navigation', async () => { + assert.equal(await resolveAppleBackRunnerCommand(), 'backInApp'); }); -test('resolveAppleBackRunnerCommand maps explicit back modes to runner commands', () => { - assert.equal(resolveAppleBackRunnerCommand('in-app'), 'backInApp'); - assert.equal(resolveAppleBackRunnerCommand('system'), 'backSystem'); +test('resolveAppleBackRunnerCommand maps explicit back modes to runner commands', async () => { + assert.equal(await resolveAppleBackRunnerCommand('in-app'), 'backInApp'); + assert.equal(await resolveAppleBackRunnerCommand('system'), 'backSystem'); }); test('provider device interactor receives runner context from core resolution', async () => { @@ -102,7 +108,7 @@ test('local interactor resolution ignores an ambient provider owner', async () = test('ios scroll sends a single fused scroll command and reports planned pixels', async () => { const commands: RunnerCommand[] = []; mockRunAppleRunnerCommand.mockImplementation(async (_device, command) => { - commands.push(command); + commands.push(withoutRunnerCommandId(command)); if (command.command === 'scroll') { // x2/y2 endpoint travel is 119 here; planned pixels (120) must be preferred. return { @@ -119,7 +125,7 @@ test('ios scroll sends a single fused scroll command and reports planned pixels' throw new Error(`Unexpected runner command: ${command.command}`); }); const interactor = await getInteractor(iosSimulator, { appBundleId: 'com.example.app' }); - const result = await interactor.scroll('down', { pixels: 120 }); + const result = await withRunner(() => interactor.scroll('down', { pixels: 120 })); // The common iOS scroll path issues exactly one lifecycle command and NO 'interactionFrame'. assert.deepEqual(commands, [ @@ -147,7 +153,7 @@ test('ios scroll sends a single fused scroll command and reports planned pixels' test('ios amount-based scroll recomputes pixels from the runner reference frame', async () => { const commands: RunnerCommand[] = []; mockRunAppleRunnerCommand.mockImplementation(async (_device, command) => { - commands.push(command); + commands.push(withoutRunnerCommandId(command)); if (command.command === 'scroll') { return { x: 150, @@ -161,7 +167,7 @@ test('ios amount-based scroll recomputes pixels from the runner reference frame' throw new Error(`Unexpected runner command: ${command.command}`); }); const interactor = await getInteractor(iosSimulator, { appBundleId: 'com.example.app' }); - const result = await interactor.scroll('down', { amount: 0.5 }); + const result = await withRunner(() => interactor.scroll('down', { amount: 0.5 })); assert.deepEqual(commands, [ { @@ -185,12 +191,12 @@ test('ios amount-based scroll recomputes pixels from the runner reference frame' test('tvOS scroll sends only a remotePress command (behavior unchanged)', async () => { const commands: RunnerCommand[] = []; mockRunAppleRunnerCommand.mockImplementation(async (_device, command) => { - commands.push(command); + commands.push(withoutRunnerCommandId(command)); return {}; }); const interactor = await getInteractor(tvOsSimulator, { appBundleId: 'com.example.app' }); - await interactor.scroll('down'); + await withRunner(() => interactor.scroll('down'), tvOsSimulator); assert.deepEqual(commands, [ { command: 'remotePress', remoteButton: 'down', appBundleId: 'com.example.app' }, @@ -200,12 +206,12 @@ test('tvOS scroll sends only a remotePress command (behavior unchanged)', async test('tvOS back navigates focus via the remote Menu button, not a coordinate tap', async () => { const commands: RunnerCommand[] = []; mockRunAppleRunnerCommand.mockImplementation(async (_device, command) => { - commands.push(command); + commands.push(withoutRunnerCommandId(command)); return {}; }); const interactor = await getInteractor(tvOsSimulator, { appBundleId: 'com.example.app' }); - await interactor.back(); + await withRunner(() => interactor.back(), tvOsSimulator); // tvOS is focus-only: back pops focus via the Siri Remote Menu button (XCUIRemote). assert.deepEqual(commands, [ @@ -216,12 +222,12 @@ test('tvOS back navigates focus via the remote Menu button, not a coordinate tap test('tvOS home navigates focus via the remote Home button', async () => { const commands: RunnerCommand[] = []; mockRunAppleRunnerCommand.mockImplementation(async (_device, command) => { - commands.push(command); + commands.push(withoutRunnerCommandId(command)); return {}; }); const interactor = await getInteractor(tvOsSimulator, { appBundleId: 'com.example.app' }); - await interactor.home(); + await withRunner(() => interactor.home(), tvOsSimulator); assert.deepEqual(commands, [ { command: 'remotePress', remoteButton: 'home', appBundleId: 'com.example.app' }, @@ -231,12 +237,12 @@ test('tvOS home navigates focus via the remote Home button', async () => { test('iOS back keeps the in-app navigation path, not the tvOS remote', async () => { const commands: RunnerCommand[] = []; mockRunAppleRunnerCommand.mockImplementation(async (_device, command) => { - commands.push(command); + commands.push(withoutRunnerCommandId(command)); return {}; }); const interactor = await getInteractor(iosSimulator, { appBundleId: 'com.example.app' }); - await interactor.back(); + await withRunner(() => interactor.back()); // The per-OS gate must not flatten: touch-input iOS never uses the XCUIRemote path. assert.deepEqual(commands, [{ command: 'backInApp', appBundleId: 'com.example.app' }]); @@ -247,7 +253,7 @@ test('ios scroll rejects non-positive amount before sending any runner command', const interactor = await getInteractor(iosSimulator, { appBundleId: 'com.example.app' }); await assert.rejects( - () => interactor.scroll('down', { amount: 0 }), + () => withRunner(() => interactor.scroll('down', { amount: 0 })), (error: unknown) => error instanceof AppError && error.code === 'INVALID_ARGS' && @@ -264,7 +270,7 @@ test('ios scroll without reference dims derives pixels from endpoint travel', as throw new Error(`Unexpected runner command: ${command.command}`); }); const interactor = await getInteractor(iosSimulator, { appBundleId: 'com.example.app' }); - const result = await interactor.scroll('down', { pixels: 120 }); + const result = await withRunner(() => interactor.scroll('down', { pixels: 120 })); const pixels = result && typeof result === 'object' && 'pixels' in result ? result.pixels : undefined; @@ -275,12 +281,12 @@ test('ios scroll without reference dims derives pixels from endpoint travel', as test('ios coordinate fill uses replacement text-entry mode', async () => { const commands: RunnerCommand[] = []; mockRunAppleRunnerCommand.mockImplementation(async (_device, command) => { - commands.push(command); + commands.push(withoutRunnerCommandId(command)); return {}; }); const interactor = await getInteractor(iosSimulator, { appBundleId: 'com.example.app' }); - await interactor.fill(120, 240, 'hunter2'); + await withRunner(() => interactor.fill(120, 240, 'hunter2')); assert.deepEqual(commands, [ { @@ -298,12 +304,12 @@ test('ios coordinate fill uses replacement text-entry mode', async () => { test('ios type uses verified append text-entry mode', async () => { const commands: RunnerCommand[] = []; mockRunAppleRunnerCommand.mockImplementation(async (_device, command) => { - commands.push(command); + commands.push(withoutRunnerCommandId(command)); return { textEntryRoute: 'synthesized-first-responder' }; }); const interactor = await getInteractor(iosSimulator, { appBundleId: 'com.example.app' }); - const result = await interactor.type('hello', 25); + const result = await withRunner(() => interactor.type('hello', 25)); assert.deepEqual(commands, [ { diff --git a/test/integration/ios-simulator-e2e/coverage-manifest.ts b/test/integration/ios-simulator-e2e/coverage-manifest.ts index 528c58e784..ee992722b1 100644 --- a/test/integration/ios-simulator-e2e/coverage-manifest.ts +++ b/test/integration/ios-simulator-e2e/coverage-manifest.ts @@ -139,7 +139,7 @@ export const IOS_SIMULATOR_E2E_COVERAGE = { }, [C.press]: live('smoke:automation-input', 'semantic press updates a durable input canary'), [C.push]: contract( - 'src/platforms/apple/core/__tests__/apps.test.ts', + 'packages/platform-apple/src/core/__tests__/apps.test.ts', 'pushIosNotification uses simctl push with temporary payload file', 'simctl push dispatch; fixture has no notification entitlement or UI oracle', ), diff --git a/test/integration/macos-e2e/coverage-manifest.ts b/test/integration/macos-e2e/coverage-manifest.ts index 5036db5b18..5165d35522 100644 --- a/test/integration/macos-e2e/coverage-manifest.ts +++ b/test/integration/macos-e2e/coverage-manifest.ts @@ -194,7 +194,7 @@ export const MACOS_PLATFORM_COVERAGE = { 'the System Settings replay returns from the About pane', ), [C.gesture]: contract( - 'src/platforms/apple/core/__tests__/interactions.test.ts', + 'packages/platform-apple/src/core/__tests__/interactions.test.ts', 'performGestureApple composes macOS one-contact plans with the drag executor', 'macOS gesture dispatch preserves the one-contact drag plan', ), diff --git a/test/integration/provider-scenarios/ios-alert-settings.test.ts b/test/integration/provider-scenarios/ios-alert-settings.test.ts index a1d467d678..3ae6cac222 100644 --- a/test/integration/provider-scenarios/ios-alert-settings.test.ts +++ b/test/integration/provider-scenarios/ios-alert-settings.test.ts @@ -13,12 +13,12 @@ import { unavailableDeploymentSnapshotAndShutdownOperationFacts } from '../../.. import { createAppLogStartResult, createDurableResourceEnvelope } from '@agent-device/capture-kit'; import type { DeviceInfo } from '@agent-device/kernel/device'; import { createTestAppLogLiveHandle } from '../../../src/__tests__/test-utils/app-log-live-handle.ts'; -import { setIosSetting } from '../../../src/platforms/apple/core/app-settings.ts'; import { actOnAppleAlert, awaitAppleAlert, readAppleAlert, -} from '../../../src/platforms/apple/alert.ts'; + setIosSetting, +} from '@agent-device/platform-apple'; import type { AlertRuntimeInput } from '@agent-device/contracts/alert-runtime'; import { assertFlatToolCall } from './assertions.ts'; import { PROVIDER_SCENARIO_IOS_SIMULATOR } from './fixtures.ts'; diff --git a/test/integration/provider-scenarios/provider-ios-runner-transport.test.ts b/test/integration/provider-scenarios/provider-ios-runner-transport.test.ts index 43c781af95..0abf68a1a1 100644 --- a/test/integration/provider-scenarios/provider-ios-runner-transport.test.ts +++ b/test/integration/provider-scenarios/provider-ios-runner-transport.test.ts @@ -10,7 +10,7 @@ import type { import type { Interactor, RunnerContext } from '@agent-device/contracts/interactor-types'; import type { DaemonRequest } from '../../../src/daemon/types.ts'; import type { DeviceInfo } from '@agent-device/kernel/device'; -import { createAppleInteractor } from '../../../src/platforms/apple/interactor.ts'; +import { createAppleInteractor } from '@agent-device/platform-apple'; import type { AppleRunnerCommandOptions, AppleRunnerProvider, diff --git a/test/integration/provider-scenarios/providers.ts b/test/integration/provider-scenarios/providers.ts index 1da7e848f9..299e1623dd 100644 --- a/test/integration/provider-scenarios/providers.ts +++ b/test/integration/provider-scenarios/providers.ts @@ -5,7 +5,7 @@ import type { ApplePlistProvider, AppleToolProvider, AppleToolSubcommandExecutor, -} from '../../../src/platforms/apple/core/tool-provider.ts'; +} from '@agent-device/platform-apple'; import { type ExecResult } from '@agent-device/host-kit/command'; import type { ProviderScenarioTranscript } from './transcript.ts'; From 923591ff43387faaee1c59995d3f462014f29bb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Fri, 28 Aug 2026 12:22:40 +0200 Subject: [PATCH 2/3] fix: preserve Apple facade sync contracts --- .fallowrc.json | 46 ++++++++++----- packages/platform-apple/package.json | 36 ++++++++++++ .../src/app-resolution-facade.ts | 2 + .../platform-apple/src/core/bundle-info.ts | 18 ++++++ .../src/core/install-artifact.ts | 18 +----- packages/platform-apple/src/index.test.ts | 12 ++++ packages/platform-apple/src/index.ts | 57 ------------------- .../src/install-artifact-facade.ts | 1 + .../platform-apple/src/interactions-facade.ts | 1 + packages/platform-apple/src/perf-facade.ts | 7 +++ .../src/physical-device-facade.ts | 1 + .../platform-apple/src/runner-owner-facade.ts | 1 + packages/platform-apple/src/simctl-facade.ts | 1 + .../platform-apple/src/simulator-facade.ts | 1 + .../src/tool-provider-facade.ts | 1 + .../help-conformance-sample-producers.ts | 4 +- scripts/layering/package-boundaries.test.ts | 15 ++++- .../layering/platform-package-policy.test.ts | 36 +++++++++++- src/__tests__/eager-closure-budgets.ts | 28 ++++++++- ...tform-runtime-apple-automation-keep-hot.ts | 5 +- ...atform-runtime-apple-physical-readiness.ts | 5 +- src/platform-runtime-apple-runner-owner.ts | 4 +- src/platform-runtime-apple-tool-host.ts | 7 +-- src/platform-runtime-device-ready.ts | 5 +- src/platform-runtime-host.test.ts | 13 +++-- src/platform-runtime-host.ts | 4 +- src/platform-runtime-runtime-hints.ts | 4 +- ...time-screen-recording-apple-runner-host.ts | 5 +- ...untime-screen-recording-apple-transport.ts | 4 +- src/sdk/limrun-runtime-dependencies.test.ts | 8 ++- src/sdk/limrun-runtime-dependencies.ts | 4 +- src/utils/__tests__/interactors.test.ts | 12 ++-- 32 files changed, 233 insertions(+), 133 deletions(-) create mode 100644 packages/platform-apple/src/app-resolution-facade.ts create mode 100644 packages/platform-apple/src/core/bundle-info.ts create mode 100644 packages/platform-apple/src/install-artifact-facade.ts create mode 100644 packages/platform-apple/src/interactions-facade.ts create mode 100644 packages/platform-apple/src/perf-facade.ts create mode 100644 packages/platform-apple/src/physical-device-facade.ts create mode 100644 packages/platform-apple/src/runner-owner-facade.ts create mode 100644 packages/platform-apple/src/simctl-facade.ts create mode 100644 packages/platform-apple/src/simulator-facade.ts create mode 100644 packages/platform-apple/src/tool-provider-facade.ts diff --git a/.fallowrc.json b/.fallowrc.json index dde54f19ee..099b43e78e 100644 --- a/.fallowrc.json +++ b/.fallowrc.json @@ -75,15 +75,41 @@ "exports": ["buildAppleFrameSamplingMetadata", "buildAppleMemorySamplingMetadata"] }, { - "comment": "Apple mechanics are reached through the named lazy façade in packages/platform-apple/src/index.ts. Fallow cannot connect dynamic import member reads to these source exports; keep this list limited to wrappers present in that façade.", + "comment": "Apple app-resolution helpers are reached through the named lazy façade in packages/platform-apple/src/app-resolution-facade.ts. Fallow cannot connect dynamic import member reads to these source exports; keep this list limited to the facade's re-exports.", "file": "packages/platform-apple/src/core/app-resolution.ts", "exports": ["buildAppNotInstalledError", "resolveIosAppAlias"] }, { - "comment": "Apple mechanics are reached through the named lazy façade in packages/platform-apple/src/index.ts. Fallow cannot connect dynamic import member reads to these source exports; keep this list limited to wrappers present in that façade.", + "comment": "Apple install mechanics are reached through the root lazy façade, while bundle-info consumers use the named install-artifact façade. Fallow cannot connect dynamic import member reads to these source exports; keep this list limited to the actual facade re-exports.", "file": "packages/platform-apple/src/core/install-artifact.ts", "exports": ["prepareIosInstallArtifact", "readIosBundleInfo"] }, + { + "comment": "Apple perf mechanics are reached through the named lazy perf façade. Fallow cannot connect dynamic import member reads to these source exports; keep this list limited to the facade's re-exports.", + "file": "packages/platform-apple/src/perf-facade.ts", + "exports": [ + "buildAppleFrameSamplingMetadata", + "buildAppleMemorySamplingMetadata", + "buildAppleMemorySnapshotSupport", + "isRetryableIosDeviceTraceRecordFailure", + "resolveIosDevicePerfHint" + ] + }, + { + "comment": "Apple tool-provider mechanics are reached through the named lazy tool-provider façade. Fallow cannot connect dynamic import member reads to these source exports; keep this list limited to the facade's re-exports.", + "file": "packages/platform-apple/src/tool-provider-facade.ts", + "exports": ["hasScopedAppleToolProvider", "resolveAppleToolProvider"] + }, + { + "comment": "The device-aware simctl argument builder is reached through the named lazy simctl façade. Fallow cannot connect the dynamic member read to this source export; keep the suppression limited to that re-export.", + "file": "packages/platform-apple/src/core/simctl.ts", + "exports": ["buildSimctlArgsForDevice"] + }, + { + "comment": "The device-aware simctl argument builder is a named package entrypoint reached through dynamic platform-runtime imports. Fallow cannot connect those member reads to the re-export; keep the suppression limited to this one facade symbol.", + "file": "packages/platform-apple/src/simctl-facade.ts", + "exports": ["buildSimctlArgsForDevice"] + }, { "comment": "Apple mechanics are reached through the named lazy façade in packages/platform-apple/src/index.ts. Fallow cannot connect dynamic import member reads to this source export; keep this list limited to wrappers present in that façade.", "file": "packages/platform-apple/src/core/perf-xctrace.ts", @@ -132,35 +158,24 @@ "pushIosNotification", "readIosClipboardText", "writeIosClipboardText", - "buildSimctlArgs", - "buildSimctlArgsForDevice", - "resolveAppleToolProvider", "runAppleToolCommand", "runXcrun", "createLocalAppleToolProvider", "readApplePlistJson", - "hasScopedAppleToolProvider", - "buildAppNotInstalledError", "setIosSetting", "readAppleAlert", "awaitAppleAlert", "actOnAppleAlert", "createAppleInteractor", - "resolveAppleBackRunnerCommand", "shutdownSimulator", "cleanupAppleXctracePerfCapture", "sampleAppleMemoryPerf", "captureAppleMemorySnapshot", "sampleAppleFramePerf", - "buildAppleFrameSamplingMetadata", - "buildAppleMemorySamplingMetadata", - "buildAppleMemorySnapshotSupport", - "isRetryableIosDeviceTraceRecordFailure", "prepareAppleTraceRecordRetry", "resolveAppleExecutable", "resolveIosDevicePerfTarget", "readAppleProcessSamples", - "resolveIosDevicePerfHint", "startAppleXctracePerfCapture", "stopAppleXctracePerfCapture", "writeAppleXctracePerfReport", @@ -183,6 +198,11 @@ "MacOsSnapshotNode" ] }, + { + "comment": "The legacy Maestro fallback wording remains contracts vocabulary for wire/fixture compatibility, but has no in-repo production consumer. The Apple migration changes this file's documentation path, so keep the existing declaration explicit without treating it as a new W4 export leak.", + "file": "packages/contracts/src/interactor-types.ts", + "exports": ["MAESTRO_NON_HITTABLE_FALLBACK_MESSAGE"] + }, { "comment": "Daemon route handlers are reached only through the dynamic `import()` table in request-handler-chain.ts, which --production analysis cannot follow to a consumer.", "file": "src/daemon/handlers/{lease,session,snapshot,react-native,record-trace,find,interaction}.ts", diff --git a/packages/platform-apple/package.json b/packages/platform-apple/package.json index 51eb61ea62..34c089f9d4 100644 --- a/packages/platform-apple/package.json +++ b/packages/platform-apple/package.json @@ -17,6 +17,26 @@ "types": "./src/index.ts", "default": "./src/index.ts" }, + "./app-resolution": { + "types": "./src/app-resolution-facade.ts", + "default": "./src/app-resolution-facade.ts" + }, + "./interactions": { + "types": "./src/interactions-facade.ts", + "default": "./src/interactions-facade.ts" + }, + "./install-artifact": { + "types": "./src/install-artifact-facade.ts", + "default": "./src/install-artifact-facade.ts" + }, + "./perf": { + "types": "./src/perf-facade.ts", + "default": "./src/perf-facade.ts" + }, + "./physical-device": { + "types": "./src/physical-device-facade.ts", + "default": "./src/physical-device-facade.ts" + }, "./runner": { "types": "./src/runner/index.ts", "default": "./src/runner/index.ts" @@ -28,6 +48,22 @@ "./runner/test-host": { "types": "./src/runner/test-host.ts", "default": "./src/runner/test-host.ts" + }, + "./runner-owner": { + "types": "./src/runner-owner-facade.ts", + "default": "./src/runner-owner-facade.ts" + }, + "./simctl": { + "types": "./src/simctl-facade.ts", + "default": "./src/simctl-facade.ts" + }, + "./simulator": { + "types": "./src/simulator-facade.ts", + "default": "./src/simulator-facade.ts" + }, + "./tool-provider": { + "types": "./src/tool-provider-facade.ts", + "default": "./src/tool-provider-facade.ts" } } } diff --git a/packages/platform-apple/src/app-resolution-facade.ts b/packages/platform-apple/src/app-resolution-facade.ts new file mode 100644 index 0000000000..10462ec42d --- /dev/null +++ b/packages/platform-apple/src/app-resolution-facade.ts @@ -0,0 +1,2 @@ +export { buildAppNotInstalledError } from './core/app-resolution-error.ts'; +export { resolveIosAppAlias } from './core/app-resolution.ts'; diff --git a/packages/platform-apple/src/core/bundle-info.ts b/packages/platform-apple/src/core/bundle-info.ts new file mode 100644 index 0000000000..fbd48cc71e --- /dev/null +++ b/packages/platform-apple/src/core/bundle-info.ts @@ -0,0 +1,18 @@ +import path from 'node:path'; +import { readInfoPlistString } from './plist.ts'; + +export async function readIosBundleInfo( + appBundlePath: string, + signal?: AbortSignal, +): Promise<{ bundleId?: string; appName?: string }> { + const infoPlistPath = path.join(appBundlePath, 'Info.plist'); + const [bundleId, displayName, bundleName] = await Promise.all([ + readInfoPlistString(infoPlistPath, 'CFBundleIdentifier', signal), + readInfoPlistString(infoPlistPath, 'CFBundleDisplayName', signal), + readInfoPlistString(infoPlistPath, 'CFBundleName', signal), + ]); + return { + bundleId, + appName: displayName ?? bundleName, + }; +} diff --git a/packages/platform-apple/src/core/install-artifact.ts b/packages/platform-apple/src/core/install-artifact.ts index c6f6c0e665..8fd5483514 100644 --- a/packages/platform-apple/src/core/install-artifact.ts +++ b/packages/platform-apple/src/core/install-artifact.ts @@ -1,7 +1,7 @@ import { fs } from '@agent-device/host-kit/filesystem'; import path from 'node:path'; import type { LocalInstallSource } from '@agent-device/kernel/contracts'; -import { readInfoPlistString } from './plist.ts'; +import { readIosBundleInfo } from './bundle-info.ts'; import { AppError } from '@agent-device/kernel/errors'; import { extractArchiveSafely, ArchiveBudget } from '@agent-device/host-kit/archive'; @@ -97,21 +97,7 @@ async function prepareIosInstallArtifactInScope( } } -export async function readIosBundleInfo( - appBundlePath: string, - signal?: AbortSignal, -): Promise<{ bundleId?: string; appName?: string }> { - const infoPlistPath = path.join(appBundlePath, 'Info.plist'); - const [bundleId, displayName, bundleName] = await Promise.all([ - readInfoPlistString(infoPlistPath, 'CFBundleIdentifier', signal), - readInfoPlistString(infoPlistPath, 'CFBundleDisplayName', signal), - readInfoPlistString(infoPlistPath, 'CFBundleName', signal), - ]); - return { - bundleId, - appName: displayName ?? bundleName, - }; -} +export { readIosBundleInfo } from './bundle-info.ts'; async function resolveIosInstallablePath( appPath: string, diff --git a/packages/platform-apple/src/index.test.ts b/packages/platform-apple/src/index.test.ts index a709942bfa..6bf1fbfb3b 100644 --- a/packages/platform-apple/src/index.test.ts +++ b/packages/platform-apple/src/index.test.ts @@ -1,6 +1,9 @@ import assert from 'node:assert/strict'; import { test } from 'vitest'; +import { buildAppNotInstalledError } from '@agent-device/platform-apple/app-resolution'; +import { resolveAppleBackRunnerCommand } from '@agent-device/platform-apple/interactions'; import { createLocalAppleToolProvider } from './index.ts'; +import { buildSimctlArgs } from '@agent-device/platform-apple/simctl'; test('lazy Apple tool provider preserves the local semantic operation shape', () => { const provider = createLocalAppleToolProvider(); @@ -13,3 +16,12 @@ test('lazy Apple tool provider preserves the local semantic operation shape', () assert.equal(typeof provider.macosHost?.openBundle, 'function'); assert.equal(typeof provider.macosHost?.listApps, 'function'); }); + +test('Apple domain facades preserve synchronous helper contracts', () => { + const appError = buildAppNotInstalledError('Shoply'); + + assert.equal(appError.code, 'APP_NOT_INSTALLED'); + assert.equal(resolveAppleBackRunnerCommand(), 'backInApp'); + assert.deepEqual(buildSimctlArgs(['list']), ['simctl', 'list']); + assert.equal(appError instanceof Promise, false); +}); diff --git a/packages/platform-apple/src/index.ts b/packages/platform-apple/src/index.ts index 6527df284e..61a3d6a1a4 100644 --- a/packages/platform-apple/src/index.ts +++ b/packages/platform-apple/src/index.ts @@ -87,11 +87,6 @@ type AppleToolProvider = import('./core/tool-provider.ts').AppleToolProvider; type AppleInteractor = import('@agent-device/contracts/interactor-types').Interactor; type AppleInteractorFactory = (typeof import('./interactor.ts'))['createAppleInteractor']; -export const buildAppNotInstalledError = async ( - ...args: Parameters< - (typeof import('./core/app-resolution-error.ts'))['buildAppNotInstalledError'] - > -) => (await import('./core/app-resolution-error.ts')).buildAppNotInstalledError(...args); export const detectSoleRunningIosSimulatorApp = async ( ...args: Parameters< (typeof import('./core/app-resolution.ts'))['detectSoleRunningIosSimulatorApp'] @@ -115,9 +110,6 @@ export const listIosApps = async ( export const resolveIosApp = async ( ...args: Parameters<(typeof import('./core/app-resolution.ts'))['resolveIosApp']> ) => (await import('./core/app-resolution.ts')).resolveIosApp(...args); -export const resolveIosAppAlias = async ( - ...args: Parameters<(typeof import('./core/app-resolution.ts'))['resolveIosAppAlias']> -) => (await import('./core/app-resolution.ts')).resolveIosAppAlias(...args); export const resolveIosSimulatorDeepLinkBundleId = async ( ...args: Parameters< (typeof import('./core/app-resolution.ts'))['resolveIosSimulatorDeepLinkBundleId'] @@ -213,9 +205,6 @@ export function createAppleInteractor( dismissAlert: method('dismissAlert'), }; } -export const resolveAppleBackRunnerCommand = async ( - ...args: Parameters<(typeof import('./interactions.ts'))['resolveAppleBackRunnerCommand']> -) => (await import('./interactions.ts')).resolveAppleBackRunnerCommand(...args); export const ensureBootedSimulator = async ( ...args: Parameters<(typeof import('./core/simulator.ts'))['ensureBootedSimulator']> @@ -223,18 +212,6 @@ export const ensureBootedSimulator = async ( export const shutdownSimulator = async ( ...args: Parameters<(typeof import('./core/simulator.ts'))['shutdownSimulator']> ) => (await import('./core/simulator.ts')).shutdownSimulator(...args); -export const wasSimulatorRecentlyObservedBooted = async ( - ...args: Parameters<(typeof import('./core/simulator.ts'))['wasSimulatorRecentlyObservedBooted']> -) => (await import('./core/simulator.ts')).wasSimulatorRecentlyObservedBooted(...args); -export const markSimulatorBooted = async ( - ...args: Parameters<(typeof import('./core/simulator.ts'))['markSimulatorBooted']> -) => (await import('./core/simulator.ts')).markSimulatorBooted(...args); -export const buildSimctlArgs = async ( - ...args: Parameters<(typeof import('./core/simctl.ts'))['buildSimctlArgs']> -) => (await import('./core/simctl.ts')).buildSimctlArgs(...args); -export const buildSimctlArgsForDevice = async ( - ...args: Parameters<(typeof import('./core/simctl.ts'))['buildSimctlArgsForDevice']> -) => (await import('./core/simctl.ts')).buildSimctlArgsForDevice(...args); export function createLocalAppleToolProvider( provider: Partial = {}, @@ -290,9 +267,6 @@ export function createLocalAppleToolProvider( macosHost, }; } -export const resolveAppleToolProvider = async ( - ...args: Parameters<(typeof import('./core/tool-provider.ts'))['resolveAppleToolProvider']> -) => (await import('./core/tool-provider.ts')).resolveAppleToolProvider(...args); export const runAppleToolCommand = async ( ...args: Parameters<(typeof import('./core/tool-provider.ts'))['runAppleToolCommand']> ) => (await import('./core/tool-provider.ts')).runAppleToolCommand(...args); @@ -302,9 +276,6 @@ export const runXcrun = async ( export const readApplePlistJson = async ( ...args: Parameters<(typeof import('./core/tool-provider.ts'))['readApplePlistJson']> ) => (await import('./core/tool-provider.ts')).readApplePlistJson(...args); -export const hasScopedAppleToolProvider = async ( - ...args: Parameters<(typeof import('./core/tool-provider.ts'))['hasScopedAppleToolProvider']> -) => (await import('./core/tool-provider.ts')).hasScopedAppleToolProvider(...args); export async function withAppleToolProvider( provider: Parameters<(typeof import('./core/tool-provider.ts'))['withAppleToolProvider']>[0], task: () => Promise, @@ -314,17 +285,9 @@ export async function withAppleToolProvider( return await runWithAppleToolProvider(provider, task); } -export const resolveIosPhysicalDeviceControl = async ( - ...args: Parameters< - (typeof import('./core/physical-device-control.ts'))['resolveIosPhysicalDeviceControl'] - > -) => (await import('./core/physical-device-control.ts')).resolveIosPhysicalDeviceControl(...args); export const prepareIosInstallArtifact = async ( ...args: Parameters<(typeof import('./core/install-artifact.ts'))['prepareIosInstallArtifact']> ) => (await import('./core/install-artifact.ts')).prepareIosInstallArtifact(...args); -export const readIosBundleInfo = async ( - ...args: Parameters<(typeof import('./core/install-artifact.ts'))['readIosBundleInfo']> -) => (await import('./core/install-artifact.ts')).readIosBundleInfo(...args); export const symbolicateCrashArtifact = async ( ...args: Parameters<(typeof import('./core/debug-symbols.ts'))['symbolicateCrashArtifact']> ) => (await import('./core/debug-symbols.ts')).symbolicateCrashArtifact(...args); @@ -333,11 +296,6 @@ export const queryAppleRunnerSelector = async ( (typeof import('./core/runner-selector-query.ts'))['queryAppleRunnerSelector'] > ) => (await import('./core/runner-selector-query.ts')).queryAppleRunnerSelector(...args); -export const setRunnerLeaseOwnerStateDir = async ( - ...args: Parameters< - (typeof import('./core/runner-owner-state.ts'))['setRunnerLeaseOwnerStateDir'] - > -) => (await import('./core/runner-owner-state.ts')).setRunnerLeaseOwnerStateDir(...args); export const resolveFrontmostMacOsApp = async ( ...args: Parameters<(typeof import('./os/macos/helper.ts'))['resolveFrontmostMacOsApp']> @@ -363,18 +321,6 @@ export const captureAppleMemorySnapshot = async ( export const sampleAppleFramePerf = async ( ...args: Parameters<(typeof import('./core/perf.ts'))['sampleAppleFramePerf']> ) => (await import('./core/perf.ts')).sampleAppleFramePerf(...args); -export const buildAppleFrameSamplingMetadata = async ( - ...args: Parameters<(typeof import('./core/perf.ts'))['buildAppleFrameSamplingMetadata']> -) => (await import('./core/perf.ts')).buildAppleFrameSamplingMetadata(...args); -export const buildAppleMemorySamplingMetadata = async ( - ...args: Parameters<(typeof import('./core/perf.ts'))['buildAppleMemorySamplingMetadata']> -) => (await import('./core/perf.ts')).buildAppleMemorySamplingMetadata(...args); -export const buildAppleMemorySnapshotSupport = async ( - ...args: Parameters<(typeof import('./core/perf.ts'))['buildAppleMemorySnapshotSupport']> -) => (await import('./core/perf.ts')).buildAppleMemorySnapshotSupport(...args); -export const isRetryableIosDeviceTraceRecordFailure = async ( - ...args: Parameters<(typeof import('./core/perf.ts'))['isRetryableIosDeviceTraceRecordFailure']> -) => (await import('./core/perf.ts')).isRetryableIosDeviceTraceRecordFailure(...args); export const prepareAppleTraceRecordRetry = async ( ...args: Parameters<(typeof import('./core/perf.ts'))['prepareAppleTraceRecordRetry']> ) => (await import('./core/perf.ts')).prepareAppleTraceRecordRetry(...args); @@ -387,9 +333,6 @@ export const resolveIosDevicePerfTarget = async ( export const readAppleProcessSamples = async ( ...args: Parameters<(typeof import('./core/perf.ts'))['readAppleProcessSamples']> ) => (await import('./core/perf.ts')).readAppleProcessSamples(...args); -export const resolveIosDevicePerfHint = async ( - ...args: Parameters<(typeof import('./core/perf.ts'))['resolveIosDevicePerfHint']> -) => (await import('./core/perf.ts')).resolveIosDevicePerfHint(...args); export const startAppleXctracePerfCapture = async ( ...args: Parameters<(typeof import('./core/perf-xctrace.ts'))['startAppleXctracePerfCapture']> diff --git a/packages/platform-apple/src/install-artifact-facade.ts b/packages/platform-apple/src/install-artifact-facade.ts new file mode 100644 index 0000000000..117acc1b83 --- /dev/null +++ b/packages/platform-apple/src/install-artifact-facade.ts @@ -0,0 +1 @@ +export { readIosBundleInfo } from './core/bundle-info.ts'; diff --git a/packages/platform-apple/src/interactions-facade.ts b/packages/platform-apple/src/interactions-facade.ts new file mode 100644 index 0000000000..ac34036125 --- /dev/null +++ b/packages/platform-apple/src/interactions-facade.ts @@ -0,0 +1 @@ +export { resolveAppleBackRunnerCommand } from './interactions.ts'; diff --git a/packages/platform-apple/src/perf-facade.ts b/packages/platform-apple/src/perf-facade.ts new file mode 100644 index 0000000000..241b8d4d64 --- /dev/null +++ b/packages/platform-apple/src/perf-facade.ts @@ -0,0 +1,7 @@ +export { + buildAppleFrameSamplingMetadata, + buildAppleMemorySamplingMetadata, + buildAppleMemorySnapshotSupport, + isRetryableIosDeviceTraceRecordFailure, + resolveIosDevicePerfHint, +} from './core/perf.ts'; diff --git a/packages/platform-apple/src/physical-device-facade.ts b/packages/platform-apple/src/physical-device-facade.ts new file mode 100644 index 0000000000..ff9c2d8c4e --- /dev/null +++ b/packages/platform-apple/src/physical-device-facade.ts @@ -0,0 +1 @@ +export { resolveIosPhysicalDeviceControl } from './core/physical-device-control.ts'; diff --git a/packages/platform-apple/src/runner-owner-facade.ts b/packages/platform-apple/src/runner-owner-facade.ts new file mode 100644 index 0000000000..4ef1e2ef45 --- /dev/null +++ b/packages/platform-apple/src/runner-owner-facade.ts @@ -0,0 +1 @@ +export { setRunnerLeaseOwnerStateDir } from './core/runner-owner-state.ts'; diff --git a/packages/platform-apple/src/simctl-facade.ts b/packages/platform-apple/src/simctl-facade.ts new file mode 100644 index 0000000000..7646ac177f --- /dev/null +++ b/packages/platform-apple/src/simctl-facade.ts @@ -0,0 +1 @@ +export { buildSimctlArgs, buildSimctlArgsForDevice } from './core/simctl.ts'; diff --git a/packages/platform-apple/src/simulator-facade.ts b/packages/platform-apple/src/simulator-facade.ts new file mode 100644 index 0000000000..af85432354 --- /dev/null +++ b/packages/platform-apple/src/simulator-facade.ts @@ -0,0 +1 @@ +export { markSimulatorBooted, wasSimulatorRecentlyObservedBooted } from './core/simulator.ts'; diff --git a/packages/platform-apple/src/tool-provider-facade.ts b/packages/platform-apple/src/tool-provider-facade.ts new file mode 100644 index 0000000000..16f3196fe1 --- /dev/null +++ b/packages/platform-apple/src/tool-provider-facade.ts @@ -0,0 +1 @@ +export { hasScopedAppleToolProvider, resolveAppleToolProvider } from './core/tool-provider.ts'; diff --git a/scripts/__tests__/help-conformance-sample-producers.ts b/scripts/__tests__/help-conformance-sample-producers.ts index 88e9665d56..016cf520c2 100644 --- a/scripts/__tests__/help-conformance-sample-producers.ts +++ b/scripts/__tests__/help-conformance-sample-producers.ts @@ -25,7 +25,7 @@ import { buildDeviceInUseBySessionError } from '../../src/daemon/handlers/sessio import { buildDeviceClaimConflictError } from '../../src/daemon/device-claim-conflict.ts'; import { resolveRefStalenessWarning } from '../../src/daemon/session-snapshot.ts'; import type { SessionState } from '../../src/daemon/types.ts'; -import { buildAppNotInstalledError } from '@agent-device/platform-apple'; +import { buildAppNotInstalledError } from '@agent-device/platform-apple/app-resolution'; import { presentConnectReadiness, renderConnectSuccess, @@ -450,7 +450,7 @@ export const SAMPLE_PRODUCERS: SampleProducer[] = [ name: 'APP_NOT_INSTALLED_SAMPLE', producer: 'the real app-resolution producer and the default hint', sample: APP_NOT_INSTALLED_SAMPLE, - render: async () => renderHumanError(await buildAppNotInstalledError('Shoply')), + render: () => renderHumanError(buildAppNotInstalledError('Shoply')), }, { name: 'BROWSERSTACK_CONNECT_SAMPLE', diff --git a/scripts/layering/package-boundaries.test.ts b/scripts/layering/package-boundaries.test.ts index cc4191297d..aea103b80e 100644 --- a/scripts/layering/package-boundaries.test.ts +++ b/scripts/layering/package-boundaries.test.ts @@ -524,14 +524,23 @@ test('the real tree parses, declares, and passes R11', () => { ]); const platformApplePackage = packages.find((pkg) => pkg.name === '@agent-device/platform-apple'); assert.ok(platformApplePackage, 'platform-apple package must exist'); - // The root façade plus the runner mechanics facet's enumerated subpaths - // (#2040); any further subpath widens this key list and fails the - // assertion. R13 pins the same list from the manifest side. + // The root façade plus the named Apple domain/mechanics subpaths; any further + // subpath widens this key list and fails the assertion. R13 pins the same + // list from the manifest side. assert.deepEqual([...platformApplePackage.exportTargets.keys()].sort(), [ '@agent-device/platform-apple', + '@agent-device/platform-apple/app-resolution', + '@agent-device/platform-apple/install-artifact', + '@agent-device/platform-apple/interactions', + '@agent-device/platform-apple/perf', + '@agent-device/platform-apple/physical-device', '@agent-device/platform-apple/runner', + '@agent-device/platform-apple/runner-owner', '@agent-device/platform-apple/runner/client', '@agent-device/platform-apple/runner/test-host', + '@agent-device/platform-apple/simctl', + '@agent-device/platform-apple/simulator', + '@agent-device/platform-apple/tool-provider', ]); assert.deepEqual([...platformApplePackage.workspaceDependencies].sort(), [ '@agent-device/capture-kit', diff --git a/scripts/layering/platform-package-policy.test.ts b/scripts/layering/platform-package-policy.test.ts index 59541a67c2..c8b0d1ee80 100644 --- a/scripts/layering/platform-package-policy.test.ts +++ b/scripts/layering/platform-package-policy.test.ts @@ -27,14 +27,14 @@ function declarations(): PlatformPackageDeclaration[] { family === 'apple' ? [ '@agent-device/platform-apple', - '@agent-device/platform-apple/runner', - '@agent-device/platform-apple/runner/client', - '@agent-device/platform-apple/runner/test-host', '@agent-device/platform-apple/app-resolution', '@agent-device/platform-apple/interactions', '@agent-device/platform-apple/install-artifact', '@agent-device/platform-apple/perf', '@agent-device/platform-apple/physical-device', + '@agent-device/platform-apple/runner', + '@agent-device/platform-apple/runner/client', + '@agent-device/platform-apple/runner/test-host', '@agent-device/platform-apple/runner-owner', '@agent-device/platform-apple/simctl', '@agent-device/platform-apple/simulator', @@ -319,6 +319,36 @@ test('the apple runner mechanics facet subpaths are the enumerated exception', ( ); }); +test('the Apple domain facades preserve synchronous helpers without widening the root facade', () => { + const sources = validSources(); + for (const specifier of [ + '@agent-device/platform-apple/app-resolution', + '@agent-device/platform-apple/interactions', + '@agent-device/platform-apple/install-artifact', + '@agent-device/platform-apple/perf', + '@agent-device/platform-apple/physical-device', + '@agent-device/platform-apple/runner-owner', + '@agent-device/platform-apple/simctl', + '@agent-device/platform-apple/simulator', + '@agent-device/platform-apple/tool-provider', + ]) { + sources.set('src/platform-runtime-domain-facade.ts', `import '${specifier}';`); + assert.deepEqual(checkPlatformPackagePolicy(sources, declarations()), []); + sources.delete('src/platform-runtime-domain-facade.ts'); + } + + const widened = declarations(); + const apple = widened.find((pkg) => pkg.family === 'apple')!; + widened[widened.indexOf(apple)] = { + ...apple, + exportedSubpaths: [...apple.exportedSubpaths, '@agent-device/platform-apple/internal'], + }; + assert.match( + messages(validSources(), widened).join('\n'), + /platform-apple must export exactly its root façade/, + ); +}); + test('the runner mechanics facet is exempt from ambient-host and raw-process rules the right way around', () => { // Production runner mechanics may own files/sockets (fs/net/os) directly... const sources = validSources(); diff --git a/src/__tests__/eager-closure-budgets.ts b/src/__tests__/eager-closure-budgets.ts index b574d4f4a7..b5ddd0eee1 100644 --- a/src/__tests__/eager-closure-budgets.ts +++ b/src/__tests__/eager-closure-budgets.ts @@ -64,9 +64,9 @@ export type EagerClosureBudget = { * excluding just the entry turns the assertion into "this façade evaluates none of its own * mechanics", which is the actual ADR-0019 property. * - * Every package entry surface sets this true except the runner mechanics - * facet entries (see PLATFORM_MECHANICS_ENTRY_PREFIX below), whose closure - * IS the implementation. + * Every package entry surface sets this true except the runner mechanics and named Apple + * domain/mechanics facet entries (see the exceptions below), whose closure IS the implementation + * intentionally exposed through that subpath. * Hub rows set it false -- a hub is a CONSUMER of façades, not neutral vocabulary, and three of * them legitimately hold the R3-permitted static platform seam that has not migrated yet. */ @@ -298,6 +298,15 @@ export const FACADE_BUDGETS: Readonly> = Object.freeze({ // --- @agent-device/platform-apple --- 'packages/platform-apple/src/index.ts': 1, + 'packages/platform-apple/src/app-resolution-facade.ts': 55, + 'packages/platform-apple/src/interactions-facade.ts': 117, + 'packages/platform-apple/src/install-artifact-facade.ts': 20, + 'packages/platform-apple/src/perf-facade.ts': 52, + 'packages/platform-apple/src/physical-device-facade.ts': 41, + 'packages/platform-apple/src/runner-owner-facade.ts': 2, + 'packages/platform-apple/src/simctl-facade.ts': 18, + 'packages/platform-apple/src/simulator-facade.ts': 26, + 'packages/platform-apple/src/tool-provider-facade.ts': 14, // --- @agent-device/platform-harmonyos --- 'packages/platform-harmonyos/src/index.ts': 1, @@ -400,6 +409,18 @@ const PLATFORM_MECHANICS_ENTRY_PREFIXES = [ 'packages/platform-android/src/mechanics.ts', ] as const; +const APPLE_DOMAIN_MECHANICS_ENTRY_FILES: ReadonlySet = new Set([ + 'packages/platform-apple/src/app-resolution-facade.ts', + 'packages/platform-apple/src/interactions-facade.ts', + 'packages/platform-apple/src/install-artifact-facade.ts', + 'packages/platform-apple/src/perf-facade.ts', + 'packages/platform-apple/src/physical-device-facade.ts', + 'packages/platform-apple/src/runner-owner-facade.ts', + 'packages/platform-apple/src/simctl-facade.ts', + 'packages/platform-apple/src/simulator-facade.ts', + 'packages/platform-apple/src/tool-provider-facade.ts', +]); + function toRows( budgets: Readonly>, kind: 'facade' | 'hub', @@ -414,6 +435,7 @@ function toRows( !PLATFORM_MECHANICS_ENTRY_PREFIXES.some((prefix) => prefix.endsWith('/') ? entryFile.startsWith(prefix) : entryFile === prefix, ), + !APPLE_DOMAIN_MECHANICS_ENTRY_FILES.has(entryFile), })); } diff --git a/src/platform-runtime-apple-automation-keep-hot.ts b/src/platform-runtime-apple-automation-keep-hot.ts index 736a82d561..f11f0bb998 100644 --- a/src/platform-runtime-apple-automation-keep-hot.ts +++ b/src/platform-runtime-apple-automation-keep-hot.ts @@ -8,11 +8,12 @@ export function createAppleAutomationKeepHotHost(): DeviceReadinessRuntimeHost[' .catch(() => {}); }, wasRecentlyObservedBooted: async (device) => { - const { wasSimulatorRecentlyObservedBooted } = await import('@agent-device/platform-apple'); + const { wasSimulatorRecentlyObservedBooted } = + await import('@agent-device/platform-apple/simulator'); return wasSimulatorRecentlyObservedBooted(device); }, markBooted: (device) => { - void import('@agent-device/platform-apple') + void import('@agent-device/platform-apple/simulator') .then(({ markSimulatorBooted }) => markSimulatorBooted(device)) .catch(() => {}); }, diff --git a/src/platform-runtime-apple-physical-readiness.ts b/src/platform-runtime-apple-physical-readiness.ts index e8c73a6714..b76a7e0383 100644 --- a/src/platform-runtime-apple-physical-readiness.ts +++ b/src/platform-runtime-apple-physical-readiness.ts @@ -5,8 +5,9 @@ export function createApplePhysicalReadinessHost(): DeviceReadinessRuntimeHost[' ensureConnected: async (device, signal) => { signal.throwIfAborted(); try { - const { resolveIosPhysicalDeviceControl } = await import('@agent-device/platform-apple'); - const control = await resolveIosPhysicalDeviceControl(device); + const { resolveIosPhysicalDeviceControl } = + await import('@agent-device/platform-apple/physical-device'); + const control = resolveIosPhysicalDeviceControl(device); await control.ensureReady(device, signal); signal.throwIfAborted(); } catch (error) { diff --git a/src/platform-runtime-apple-runner-owner.ts b/src/platform-runtime-apple-runner-owner.ts index 33b4e8fc1c..b604fbfa8c 100644 --- a/src/platform-runtime-apple-runner-owner.ts +++ b/src/platform-runtime-apple-runner-owner.ts @@ -2,6 +2,6 @@ export async function configureAppleRunnerLeaseOwnerStateDir( stateDir: string | undefined, ): Promise { - const { setRunnerLeaseOwnerStateDir } = await import('@agent-device/platform-apple'); - await setRunnerLeaseOwnerStateDir(stateDir); + const { setRunnerLeaseOwnerStateDir } = await import('@agent-device/platform-apple/runner-owner'); + setRunnerLeaseOwnerStateDir(stateDir); } diff --git a/src/platform-runtime-apple-tool-host.ts b/src/platform-runtime-apple-tool-host.ts index b81c7e759b..8c530eddb0 100644 --- a/src/platform-runtime-apple-tool-host.ts +++ b/src/platform-runtime-apple-tool-host.ts @@ -4,13 +4,10 @@ export function createAppleToolHost(): AppleToolHost { return Object.freeze({ isXcrunAvailable: async (signal?: AbortSignal) => { const { resolveAppleToolProvider } = await awaitPreservingAbortReason( - async () => await import('@agent-device/platform-apple'), - signal, - ); - const provider = await awaitPreservingAbortReason( - async () => await resolveAppleToolProvider(), + async () => await import('@agent-device/platform-apple/tool-provider'), signal, ); + const provider = resolveAppleToolProvider(); const available = await awaitPreservingAbortReason( async () => await provider.whichCommand('xcrun'), signal, diff --git a/src/platform-runtime-device-ready.ts b/src/platform-runtime-device-ready.ts index 2029d8be6f..5eaaad2904 100644 --- a/src/platform-runtime-device-ready.ts +++ b/src/platform-runtime-device-ready.ts @@ -26,8 +26,9 @@ export async function ensureLocalPlatformDeviceReady( return true; } if (device.kind === 'device') { - const { resolveIosPhysicalDeviceControl } = await import('@agent-device/platform-apple'); - const control = await resolveIosPhysicalDeviceControl(device); + const { resolveIosPhysicalDeviceControl } = + await import('@agent-device/platform-apple/physical-device'); + const control = resolveIosPhysicalDeviceControl(device); await control.ensureReady(device); return true; } diff --git a/src/platform-runtime-host.test.ts b/src/platform-runtime-host.test.ts index aea485d063..0f1c5e2452 100644 --- a/src/platform-runtime-host.test.ts +++ b/src/platform-runtime-host.test.ts @@ -6,7 +6,7 @@ const toolchains = vi.hoisted(() => ({ appleRuns: [] as Array<{ args: string[]; signal?: AbortSignal }>, })); -vi.mock('@agent-device/platform-apple', () => { +vi.mock('@agent-device/platform-apple/tool-provider', () => { toolchains.appleEvaluations += 1; return { resolveAppleToolProvider: () => ({ @@ -15,13 +15,16 @@ vi.mock('@agent-device/platform-apple', () => { return true; }, }), - runXcrun: async (args: string[], options: { signal?: AbortSignal }) => { - toolchains.appleRuns.push({ args, signal: options.signal }); - return { stdout: 'ok', stderr: '', exitCode: 0 }; - }, }; }); +vi.mock('@agent-device/platform-apple', () => ({ + runXcrun: async (args: string[], options: { signal?: AbortSignal }) => { + toolchains.appleRuns.push({ args, signal: options.signal }); + return { stdout: 'ok', stderr: '', exitCode: 0 }; + }, +})); + import { createDeviceInventoryHost } from './platform-runtime-host.ts'; test('inventory host loads scoped Apple mechanics only on first Apple tool use', async () => { diff --git a/src/platform-runtime-host.ts b/src/platform-runtime-host.ts index 269c3568ea..1f19e63529 100644 --- a/src/platform-runtime-host.ts +++ b/src/platform-runtime-host.ts @@ -44,8 +44,8 @@ export function createDeviceInventoryHost(): DeviceInventoryHost { homeDirectory: os.homedir(), observations: Object.freeze({ deviceBooted: async (device: DeviceInfo) => { - const { markSimulatorBooted } = await import('@agent-device/platform-apple'); - await markSimulatorBooted(device); + const { markSimulatorBooted } = await import('@agent-device/platform-apple/simulator'); + markSimulatorBooted(device); }, }), }); diff --git a/src/platform-runtime-runtime-hints.ts b/src/platform-runtime-runtime-hints.ts index 3b2a4b3da2..6447599929 100644 --- a/src/platform-runtime-runtime-hints.ts +++ b/src/platform-runtime-runtime-hints.ts @@ -313,10 +313,10 @@ async function runIosSimulatorRuntimeHintCommand( options?: Readonly<{ allowFailure?: boolean }>, ): Promise { const [{ buildSimctlArgsForDevice }, { runXcrun }] = await Promise.all([ - import('@agent-device/platform-apple'), + import('@agent-device/platform-apple/simctl'), import('@agent-device/platform-apple'), ]); - await runXcrun(await buildSimctlArgsForDevice(device, args), options); + await runXcrun(buildSimctlArgsForDevice(device, args), options); } function normalizeAndroidPrefsXml(xml: string): string { diff --git a/src/platform-runtime-screen-recording-apple-runner-host.ts b/src/platform-runtime-screen-recording-apple-runner-host.ts index 376ebf4f1a..c8e24ee8a7 100644 --- a/src/platform-runtime-screen-recording-apple-runner-host.ts +++ b/src/platform-runtime-screen-recording-apple-runner-host.ts @@ -62,8 +62,9 @@ export async function retrieveAppleRunnerRecording( signal?: AbortSignal, ): Promise { signal?.throwIfAborted(); - const { resolveIosPhysicalDeviceControl } = await import('@agent-device/platform-apple'); - const control = await resolveIosPhysicalDeviceControl(device); + const { resolveIosPhysicalDeviceControl } = + await import('@agent-device/platform-apple/physical-device'); + const control = resolveIosPhysicalDeviceControl(device); await control.copyRunnerFile(device, remotePath, outputPath); signal?.throwIfAborted(); } diff --git a/src/platform-runtime-screen-recording-apple-transport.ts b/src/platform-runtime-screen-recording-apple-transport.ts index 2579a5157f..e5a0e97932 100644 --- a/src/platform-runtime-screen-recording-apple-transport.ts +++ b/src/platform-runtime-screen-recording-apple-transport.ts @@ -26,13 +26,13 @@ const localTransport: AppleSimulatorScreenRecordingTransport = Object.freeze({ mode: 'local', async start({ device, outputPath, signal }) { const [{ buildSimctlArgsForDevice }, { runCmdBackground }] = await Promise.all([ - import('@agent-device/platform-apple'), + import('@agent-device/platform-apple/simctl'), import('@agent-device/host-kit/command'), ]); signal?.throwIfAborted(); return runCmdBackground( 'xcrun', - await buildSimctlArgsForDevice(device, ['io', device.id, 'recordVideo', outputPath]), + buildSimctlArgsForDevice(device, ['io', device.id, 'recordVideo', outputPath]), { allowFailure: true }, ); }, diff --git a/src/sdk/limrun-runtime-dependencies.test.ts b/src/sdk/limrun-runtime-dependencies.test.ts index 4d446bf185..26f2bf77c8 100644 --- a/src/sdk/limrun-runtime-dependencies.test.ts +++ b/src/sdk/limrun-runtime-dependencies.test.ts @@ -27,12 +27,16 @@ vi.mock('@agent-device/platform-android/mechanics', async (importOriginal) => { }; }); -vi.mock('@agent-device/platform-apple', async (importOriginal) => ({ - ...(await importOriginal()), +vi.mock('@agent-device/platform-apple/app-resolution', async (importOriginal) => ({ + ...(await importOriginal()), resolveIosAppAlias: vi.fn((app: string) => { moduleLoads.appleAppResolution += 1; return `resolved:${app}`; }), +})); + +vi.mock('@agent-device/platform-apple/install-artifact', async (importOriginal) => ({ + ...(await importOriginal()), readIosBundleInfo: vi.fn(async () => { moduleLoads.appleInstallArtifact += 1; return { appName: 'Example' }; diff --git a/src/sdk/limrun-runtime-dependencies.ts b/src/sdk/limrun-runtime-dependencies.ts index 526bb774b7..6db3a5d04d 100644 --- a/src/sdk/limrun-runtime-dependencies.ts +++ b/src/sdk/limrun-runtime-dependencies.ts @@ -91,11 +91,11 @@ export function createLimrunRuntimeDependencies(): LimrunRuntimeDependencies { }, ios: { resolveAppAlias: async (app) => { - const { resolveIosAppAlias } = await import('@agent-device/platform-apple'); + const { resolveIosAppAlias } = await import('@agent-device/platform-apple/app-resolution'); return resolveIosAppAlias(app); }, readBundleAppName: async (appPath) => { - const { readIosBundleInfo } = await import('@agent-device/platform-apple'); + const { readIosBundleInfo } = await import('@agent-device/platform-apple/install-artifact'); return (await readIosBundleInfo(appPath)).appName; }, }, diff --git a/src/utils/__tests__/interactors.test.ts b/src/utils/__tests__/interactors.test.ts index ee37320a49..069617add9 100644 --- a/src/utils/__tests__/interactors.test.ts +++ b/src/utils/__tests__/interactors.test.ts @@ -8,7 +8,7 @@ import { setActiveProviderDeviceRuntimes } from '../../provider-device-runtime.t import type { ProviderDeviceRuntime } from '@agent-device/contracts/device'; import { getInteractor, getLocalInteractor } from '../../core/interactors.ts'; -import { resolveAppleBackRunnerCommand } from '@agent-device/platform-apple'; +import { resolveAppleBackRunnerCommand } from '@agent-device/platform-apple/interactions'; const iosSimulator: DeviceInfo = { platform: 'apple', @@ -51,13 +51,13 @@ afterEach(() => { setActiveProviderDeviceRuntimes([]); }); -test('resolveAppleBackRunnerCommand defaults plain back to in-app navigation', async () => { - assert.equal(await resolveAppleBackRunnerCommand(), 'backInApp'); +test('resolveAppleBackRunnerCommand defaults plain back to in-app navigation', () => { + assert.equal(resolveAppleBackRunnerCommand(), 'backInApp'); }); -test('resolveAppleBackRunnerCommand maps explicit back modes to runner commands', async () => { - assert.equal(await resolveAppleBackRunnerCommand('in-app'), 'backInApp'); - assert.equal(await resolveAppleBackRunnerCommand('system'), 'backSystem'); +test('resolveAppleBackRunnerCommand maps explicit back modes to runner commands', () => { + assert.equal(resolveAppleBackRunnerCommand('in-app'), 'backInApp'); + assert.equal(resolveAppleBackRunnerCommand('system'), 'backSystem'); }); test('provider device interactor receives runner context from core resolution', async () => { From a3c9d57f2426579ca9a0545b6f32d6dfb8631239 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Fri, 28 Aug 2026 14:51:52 +0200 Subject: [PATCH 3/3] fix: complete Apple W4 rebase review fixes --- .fallowrc.json | 95 +++++++++++++++++- packages/host-kit/src/environment.ts | 30 ------ packages/host-kit/src/filesystem.ts | 1 - packages/host-kit/src/host-file.ts | 12 +++ .../host-kit/src/internal/host-file.test.ts | 43 ++++++++ packages/host-kit/src/internal/host-file.ts | 78 ++++++++++++++- .../host-kit/src/internal/host-process.ts | 8 ++ packages/host-kit/src/process.ts | 2 + packages/platform-apple/package.json | 20 ++++ .../src/__tests__/doctor.test.ts | 4 +- .../src/app-lifecycle-facade.ts | 1 + .../src/app-resolution-facade.ts | 10 +- .../src/core/__tests__/app-install.test.ts | 57 ----------- .../src/core/__tests__/apps.test.ts | 73 ++------------ .../__tests__/physical-device-control.test.ts | 7 +- .../platform-apple/src/core/app-device-io.ts | 13 ++- .../platform-apple/src/core/app-install.ts | 30 ------ .../platform-apple/src/core/app-launch.ts | 6 +- .../platform-apple/src/core/app-resolution.ts | 4 +- .../platform-apple/src/core/app-settings.ts | 15 +-- packages/platform-apple/src/core/apps.ts | 10 -- .../platform-apple/src/core/debug-symbols.ts | 19 ++-- .../src/core/debug-symbols/dsym.ts | 10 +- packages/platform-apple/src/core/devicectl.ts | 12 ++- .../src/core/install-artifact.ts | 19 ++-- .../platform-apple/src/core/perf-xctrace.ts | 41 ++++---- packages/platform-apple/src/core/perf.ts | 43 ++++---- .../src/core/physical-device-coredevice.ts | 12 ++- .../src/core/physical-device-screenshot.ts | 4 +- packages/platform-apple/src/core/plist.ts | 4 +- .../platform-apple/src/core/screenshot.ts | 6 +- .../src/debug-symbols-facade.ts | 1 + packages/platform-apple/src/doctor-facade.ts | 1 + packages/platform-apple/src/doctor.ts | 2 +- packages/platform-apple/src/index.ts | 2 - .../src/install-artifact-facade.ts | 1 + packages/platform-apple/src/interactor.ts | 14 +-- packages/platform-apple/src/macos-facade.ts | 6 ++ .../platform-apple/src/os/macos/helper.ts | 36 ++++--- .../src/os/macos/host-provider.ts | 5 +- packages/platform-apple/src/perf-facade.ts | 16 +++ .../src/runner-operations-facade.ts | 27 +++++ .../platform-apple/src/runner/test-host.ts | 4 + .../platform-apple/src/simulator-facade.ts | 7 +- .../src/tool-provider-facade.ts | 11 ++- .../help-conformance-sample-outputs.test.ts | 4 +- .../help-conformance-sample-producers.ts | 2 +- .../__tests__/test-file-size-ratchet.test.ts | 2 +- scripts/check-affected/device-lanes.test.ts | 6 +- scripts/check-affected/model.test.ts | 2 +- scripts/clean-daemon.ts | 3 +- scripts/layering/check.ts | 4 - scripts/layering/model.test.ts | 7 +- scripts/layering/model.ts | 1 - scripts/layering/package-boundaries.test.ts | 6 +- .../layering/platform-package-policy.test.ts | 10 +- scripts/layering/platform-package-policy.ts | 98 ++++--------------- scripts/patch-xcuitest-runner-icon.ts | 3 +- scripts/vitest-apple-runner-host-setup.ts | 2 +- src/__tests__/eager-closure-budgets.ts | 20 +++- src/agent-device-client.ts | 3 +- .../device-selection-resolver.test.ts | 2 +- src/core/__tests__/dispatch-resolve.test.ts | 2 +- src/core/device-selection-resolver.ts | 3 +- src/daemon/__tests__/device-ready.test.ts | 6 +- .../request-recording-health.test.ts | 4 +- .../__tests__/request-router-open.test.ts | 30 +++++- .../request-router-replay-scope.test.ts | 11 ++- .../session-close-shutdown.fixtures.ts | 33 +++++-- .../__tests__/session-device-utils.test.ts | 4 +- .../__tests__/session-doctor-warmup.test.ts | 4 +- ...ion-replay-repair-record-exclusion.test.ts | 18 ++-- .../session-replay-repair-transaction.test.ts | 18 ++-- .../__tests__/session-test-harness.ts | 28 ++++-- .../snapshot-session-cleanup.test.ts | 10 +- src/daemon/ios-app-session-hint.test.ts | 4 +- src/platform-runtime-app-inventory-host.ts | 2 +- ...latform-runtime-apple-application-tools.ts | 30 +++--- ...tform-runtime-apple-automation-keep-hot.ts | 2 +- ...-runtime-apple-deployment-executor.test.ts | 2 +- ...tform-runtime-apple-deployment-executor.ts | 8 +- src/platform-runtime-apple-resources.ts | 6 +- src/platform-runtime-apple-tool-host.ts | 2 +- src/platform-runtime-audio-probe-host.ts | 2 +- ...tform-runtime-daemon-owner-cleanup.test.ts | 2 +- src/platform-runtime-daemon-owner-cleanup.ts | 3 +- src/platform-runtime-device-ready.ts | 2 +- src/platform-runtime-host-diagnostics.ts | 4 +- src/platform-runtime-host.test.ts | 11 +-- src/platform-runtime-open-target.ts | 8 +- src/platform-runtime-operation-host.ts | 3 +- src/platform-runtime-perf-capture-host.ts | 4 +- src/platform-runtime-perf-host.ts | 4 +- src/platform-runtime-resource-cleanup.ts | 4 +- src/platform-runtime-runtime-hints.ts | 2 +- ...time-screen-recording-apple-runner-host.ts | 3 +- ...n-recording-apple-runner-transport.test.ts | 2 +- ...screen-recording-apple-runner-transport.ts | 7 +- src/platform-runtime/request-providers.ts | 4 +- .../__tests__/install-source.test.ts | 1 - 100 files changed, 739 insertions(+), 551 deletions(-) delete mode 100644 packages/host-kit/src/environment.ts delete mode 100644 packages/host-kit/src/filesystem.ts create mode 100644 packages/host-kit/src/internal/host-file.test.ts create mode 100644 packages/platform-apple/src/app-lifecycle-facade.ts delete mode 100644 packages/platform-apple/src/core/__tests__/app-install.test.ts delete mode 100644 packages/platform-apple/src/core/app-install.ts delete mode 100644 packages/platform-apple/src/core/apps.ts create mode 100644 packages/platform-apple/src/debug-symbols-facade.ts create mode 100644 packages/platform-apple/src/doctor-facade.ts create mode 100644 packages/platform-apple/src/macos-facade.ts create mode 100644 packages/platform-apple/src/runner-operations-facade.ts diff --git a/.fallowrc.json b/.fallowrc.json index 099b43e78e..ab2831e29c 100644 --- a/.fallowrc.json +++ b/.fallowrc.json @@ -74,11 +74,31 @@ "file": "packages/platform-apple/src/core/perf.ts", "exports": ["buildAppleFrameSamplingMetadata", "buildAppleMemorySamplingMetadata"] }, + { + "comment": "These host-process helpers are consumed only by integration leak-oracle support and host-process unit tests; Fallow's production graph intentionally excludes both consumers.", + "file": "packages/host-kit/src/process.ts", + "exports": ["uniquePositivePids", "readOwnedProcessRecordFile"] + }, { "comment": "Apple app-resolution helpers are reached through the named lazy façade in packages/platform-apple/src/app-resolution-facade.ts. Fallow cannot connect dynamic import member reads to these source exports; keep this list limited to the facade's re-exports.", "file": "packages/platform-apple/src/core/app-resolution.ts", "exports": ["buildAppNotInstalledError", "resolveIosAppAlias"] }, + { + "comment": "The app-resolution package facade is consumed through workspace package exports. Fallow does not follow that package boundary, so keep only its externally consumed helper that has no visible production edge here.", + "file": "packages/platform-apple/src/app-resolution-facade.ts", + "exports": ["buildAppNotInstalledError"] + }, + { + "comment": "The legacy push helper remains a package-root operation reached through a dynamic import; Fallow cannot connect that root wrapper to this source export.", + "file": "packages/platform-apple/src/core/app-device-io.ts", + "exports": ["pushIosNotification"] + }, + { + "comment": "The lifecycle package facade is consumed through workspace package exports. Fallow does not follow that package boundary, so keep only the two root lifecycle helpers without visible production edges here.", + "file": "packages/platform-apple/src/app-lifecycle-facade.ts", + "exports": ["openIosApp", "openIosDevice"] + }, { "comment": "Apple install mechanics are reached through the root lazy façade, while bundle-info consumers use the named install-artifact façade. Fallow cannot connect dynamic import member reads to these source exports; keep this list limited to the actual facade re-exports.", "file": "packages/platform-apple/src/core/install-artifact.ts", @@ -92,13 +112,43 @@ "buildAppleMemorySamplingMetadata", "buildAppleMemorySnapshotSupport", "isRetryableIosDeviceTraceRecordFailure", - "resolveIosDevicePerfHint" + "resolveIosDevicePerfHint", + "captureAppleMemorySnapshot", + "readAppleProcessSamples", + "resolveAppleExecutable", + "resolveIosDevicePerfTarget", + "sampleAppleFramePerf", + "sampleAppleMemoryPerf", + "cleanupAppleXctracePerfCapture", + "startAppleXctracePerfCapture", + "stopAppleXctracePerfCapture", + "writeAppleXctracePerfReport", + "AppleXctraceCpuProfileReport", + "AppleXctracePerfMode", + "AppleXctracePerfResult" ] }, { "comment": "Apple tool-provider mechanics are reached through the named lazy tool-provider façade. Fallow cannot connect dynamic import member reads to these source exports; keep this list limited to the facade's re-exports.", "file": "packages/platform-apple/src/tool-provider-facade.ts", - "exports": ["hasScopedAppleToolProvider", "resolveAppleToolProvider"] + "exports": [ + "hasScopedAppleToolProvider", + "resolveAppleToolProvider", + "createLocalAppleToolProvider", + "readApplePlistJson", + "runAppleToolCommand", + "runXcrun" + ] + }, + { + "comment": "The interactions package facade is consumed through workspace package exports. Fallow does not follow that package boundary, so keep the synchronous helper explicitly accounted for.", + "file": "packages/platform-apple/src/interactions-facade.ts", + "exports": ["resolveAppleBackRunnerCommand"] + }, + { + "comment": "The runner-operations package facade is consumed through workspace package exports. Fallow does not follow that package boundary, so keep the two operations without visible production edges here explicitly accounted for.", + "file": "packages/platform-apple/src/runner-operations-facade.ts", + "exports": ["hasCachedAppleRunnerArtifact", "runApplePressSeries"] }, { "comment": "The device-aware simctl argument builder is reached through the named lazy simctl façade. Fallow cannot connect the dynamic member read to this source export; keep the suppression limited to that re-export.", @@ -108,7 +158,12 @@ { "comment": "The device-aware simctl argument builder is a named package entrypoint reached through dynamic platform-runtime imports. Fallow cannot connect those member reads to the re-export; keep the suppression limited to this one facade symbol.", "file": "packages/platform-apple/src/simctl-facade.ts", - "exports": ["buildSimctlArgsForDevice"] + "exports": ["buildSimctlArgs", "buildSimctlArgsForDevice"] + }, + { + "comment": "The simulator package facade is consumed through workspace package exports. Fallow does not follow that package boundary, so keep the legacy shutdown operation explicitly accounted for.", + "file": "packages/platform-apple/src/simulator-facade.ts", + "exports": ["shutdownSimulator"] }, { "comment": "Apple mechanics are reached through the named lazy façade in packages/platform-apple/src/index.ts. Fallow cannot connect dynamic import member reads to this source export; keep this list limited to wrappers present in that façade.", @@ -152,6 +207,15 @@ "comment": "The Apple package root is a cold-start-preserving named façade: its production consumers load the value wrappers through dynamic imports. Fallow cannot connect those member reads, so this exact list records only the façade exports currently reported as unreachable; static consumers remain visible to the audit.", "file": "packages/platform-apple/src/index.ts", "exports": [ + "appleToolchainCheck", + "appleRunnerWarmupCheck", + "detectSoleRunningIosSimulatorApp", + "findIosSimulatorInstalledApp", + "invalidateIosAppResolutionCache", + "listIosApps", + "resolveIosApp", + "resolveIosSimulatorDeepLinkBundleId", + "closeIosApp", "openIosApp", "openIosDevice", "screenshotIos", @@ -167,7 +231,16 @@ "awaitAppleAlert", "actOnAppleAlert", "createAppleInteractor", + "ensureBootedSimulator", "shutdownSimulator", + "withAppleToolProvider", + "prepareIosInstallArtifact", + "symbolicateCrashArtifact", + "queryAppleRunnerSelector", + "resolveFrontmostMacOsApp", + "runMacOsAlertAction", + "startMacOsAudioProbeProcess", + "captureMacOsSurfaceSnapshot", "cleanupAppleXctracePerfCapture", "sampleAppleMemoryPerf", "captureAppleMemorySnapshot", @@ -180,6 +253,21 @@ "stopAppleXctracePerfCapture", "writeAppleXctracePerfReport", "runApplePressSeries", + "runAppleRunnerCommand", + "notifyIosRunnerAppRelaunched", + "prewarmAppleRunnerCache", + "prewarmIosRunnerSession", + "prepareIosRunner", + "resolveRunnerAppBundleId", + "detachIosSimulatorRunnerSessionsForShutdown", + "getRunnerSessionSnapshot", + "scheduleIosRunnerIdleStop", + "stopIosRunnerSession", + "stopAllIosRunnerSessions", + "cleanupRunnerLeasesForOwner", + "readStaleRunnerLease", + "verifyLeaseRunnerPidIdentity", + "applyXctestRunnerAppIconFromDerivedPath", "AppleMacOsHelperProvider", "AppleToolAvailabilityChecker", "AppleToolCommandExecutor", @@ -192,6 +280,7 @@ "AppleMemorySnapshotResult", "AppleProcessSample", "AppleXctraceCpuProfileReport", + "AppleXctracePerfCapture", "AppleXctracePerfMode", "AppleXctracePerfResult", "MacOsPermissionTarget", diff --git a/packages/host-kit/src/environment.ts b/packages/host-kit/src/environment.ts deleted file mode 100644 index cc84907cd8..0000000000 --- a/packages/host-kit/src/environment.ts +++ /dev/null @@ -1,30 +0,0 @@ -import os from 'node:os'; -import process from 'node:process'; - -export function hostHomeDirectory(): string { - return os.homedir(); -} - -export function hostTemporaryDirectory(): string { - return os.tmpdir(); -} - -export function hostWorkingDirectory(): string { - return process.cwd(); -} - -export function hostProcessId(): number { - return process.pid; -} - -export function hostPlatform(): NodeJS.Platform { - return process.platform; -} - -export function readHostEnvironmentVariable(name: string): string | undefined { - return process.env[name]; -} - -export function writeHostStderr(value: string): void { - process.stderr.write(value); -} diff --git a/packages/host-kit/src/filesystem.ts b/packages/host-kit/src/filesystem.ts deleted file mode 100644 index d24bf4c273..0000000000 --- a/packages/host-kit/src/filesystem.ts +++ /dev/null @@ -1 +0,0 @@ -export { existsSync, promises as fs } from 'node:fs'; diff --git a/packages/host-kit/src/host-file.ts b/packages/host-kit/src/host-file.ts index efb2f2d661..2e69d1f65d 100644 --- a/packages/host-kit/src/host-file.ts +++ b/packages/host-kit/src/host-file.ts @@ -1,18 +1,30 @@ export { createHostDirectoryLinkSync, + accessHostFile, + chmodHostFile, + copyHostFile, + copyHostPath, + ensureHostDirectory, ensureHostDirectorySync, hostFileExistsSync, hostFileLstatSync, + hostFileStat, hostFileStatSync, hostHomeDirectory, hostTemporaryDirectory, makeHostTemporaryDirectory, readHostBinaryFile, + readHostDirectory, readHostDirectorySync, readHostSymbolicLinkSync, readHostTextFile, readHostTextFileSync, removeHostDirectory, + removeHostPath, removeHostFileSync, + renameHostPath, + unlinkHostFile, + writeHostBinaryFile, + writeHostTextFile, writeHostTextFileSync, } from './internal/host-file.ts'; diff --git a/packages/host-kit/src/internal/host-file.test.ts b/packages/host-kit/src/internal/host-file.test.ts new file mode 100644 index 0000000000..ece0033f06 --- /dev/null +++ b/packages/host-kit/src/internal/host-file.test.ts @@ -0,0 +1,43 @@ +import assert from 'node:assert/strict'; +import path from 'node:path'; +import { test } from 'vitest'; +import { + accessHostFile, + copyHostFile, + ensureHostDirectory, + hostFileStat, + makeHostTemporaryDirectory, + readHostBinaryFile, + readHostDirectory, + readHostTextFile, + removeHostPath, + renameHostPath, + writeHostBinaryFile, + writeHostTextFile, +} from './host-file.ts'; + +test('host-file ports preserve text, binary, directory, and path operations', async () => { + const root = await makeHostTemporaryDirectory('agent-device-host-file-'); + try { + const nested = path.join(root, 'nested'); + const source = path.join(nested, 'source.txt'); + const copy = path.join(root, 'copy.txt'); + const renamed = path.join(root, 'renamed.txt'); + await ensureHostDirectory(nested); + await writeHostTextFile(source, 'host-file'); + const bytes = path.join(nested, 'bytes.bin'); + await writeHostBinaryFile(bytes, Buffer.from([1, 2, 3])); + + assert.equal(await readHostTextFile(source), 'host-file'); + assert.deepEqual(await readHostBinaryFile(bytes), Buffer.from([1, 2, 3])); + assert.equal((await hostFileStat(source)).isFile(), true); + assert.deepEqual((await readHostDirectory(nested)).sort(), ['bytes.bin', 'source.txt']); + assert.equal((await readHostDirectory(root, { withFileTypes: true })).length, 1); + + await copyHostFile(source, copy); + await renameHostPath(copy, renamed); + await accessHostFile(renamed); + } finally { + await removeHostPath(root); + } +}); diff --git a/packages/host-kit/src/internal/host-file.ts b/packages/host-kit/src/internal/host-file.ts index c0ffd2090f..297df12a0a 100644 --- a/packages/host-kit/src/internal/host-file.ts +++ b/packages/host-kit/src/internal/host-file.ts @@ -15,16 +15,84 @@ export async function makeHostTemporaryDirectory(prefix: string): Promise { - return await fsPromises.readFile(filePath, 'utf8'); +export async function ensureHostDirectory(directoryPath: string): Promise { + await fsPromises.mkdir(directoryPath, { recursive: true }); } -export async function readHostBinaryFile(filePath: string): Promise { - return await fsPromises.readFile(filePath); +export async function readHostTextFile( + filePath: string, + options: { signal?: AbortSignal } = {}, +): Promise { + return await fsPromises.readFile(filePath, { encoding: 'utf8', signal: options.signal }); +} + +export async function readHostBinaryFile( + filePath: string, + options: { signal?: AbortSignal } = {}, +): Promise { + return await fsPromises.readFile(filePath, { signal: options.signal }); +} + +export async function readHostDirectory( + directoryPath: string, + options: { withFileTypes: true }, +): Promise; +export async function readHostDirectory( + directoryPath: string, + options?: { withFileTypes?: false }, +): Promise; +export async function readHostDirectory( + directoryPath: string, + options: { withFileTypes?: boolean } = {}, +): Promise { + if (options.withFileTypes === true) { + return await fsPromises.readdir(directoryPath, { withFileTypes: true }); + } + return await fsPromises.readdir(directoryPath); +} + +export async function hostFileStat(filePath: string): Promise { + return await fsPromises.stat(filePath); +} + +export async function writeHostTextFile(filePath: string, contents: string): Promise { + await fsPromises.writeFile(filePath, contents, 'utf8'); +} + +export async function writeHostBinaryFile(filePath: string, contents: Uint8Array): Promise { + await fsPromises.writeFile(filePath, contents); +} + +export async function removeHostPath(filePath: string): Promise { + await fsPromises.rm(filePath, { recursive: true, force: true }); +} + +export async function unlinkHostFile(filePath: string): Promise { + await fsPromises.unlink(filePath); +} + +export async function copyHostFile(sourcePath: string, destinationPath: string): Promise { + await fsPromises.copyFile(sourcePath, destinationPath); +} + +export async function copyHostPath(sourcePath: string, destinationPath: string): Promise { + await fsPromises.cp(sourcePath, destinationPath, { recursive: true }); +} + +export async function renameHostPath(sourcePath: string, destinationPath: string): Promise { + await fsPromises.rename(sourcePath, destinationPath); +} + +export async function chmodHostFile(filePath: string, mode: number): Promise { + await fsPromises.chmod(filePath, mode); +} + +export async function accessHostFile(filePath: string): Promise { + await fsPromises.access(filePath); } export async function removeHostDirectory(directoryPath: string): Promise { - await fsPromises.rm(directoryPath, { recursive: true, force: true }); + await removeHostPath(directoryPath); } export function ensureHostDirectorySync(directoryPath: string): void { diff --git a/packages/host-kit/src/internal/host-process.ts b/packages/host-kit/src/internal/host-process.ts index 415d8665b9..dba0c2dee7 100644 --- a/packages/host-kit/src/internal/host-process.ts +++ b/packages/host-kit/src/internal/host-process.ts @@ -26,6 +26,14 @@ export function hostEnvironment(): NodeJS.ProcessEnv { return process.env; } +export function readHostEnvironmentVariable(name: string): string | undefined { + return hostEnvironment()[name]; +} + +export function writeHostStderr(value: string): void { + process.stderr.write(value); +} + export function hostCurrentWorkingDirectory(): string { return process.cwd(); } diff --git a/packages/host-kit/src/process.ts b/packages/host-kit/src/process.ts index 76f986b8f1..12fa617e0b 100644 --- a/packages/host-kit/src/process.ts +++ b/packages/host-kit/src/process.ts @@ -12,6 +12,7 @@ export { isProcessGroupAlive, isProcessZombie, listHostProcesses, + readHostEnvironmentVariable, readHostProcessIdentityObservations, readProcessCommand, readProcessStartTime, @@ -20,6 +21,7 @@ export { stopPidsWithEscalation, uniquePositivePids, waitForProcessExit, + writeHostStderr, } from './internal/host-process.ts'; export { reapOwnedProcessRecordsAtStartup } from './internal/owned-process-reaper.ts'; export { diff --git a/packages/platform-apple/package.json b/packages/platform-apple/package.json index 34c089f9d4..44078d6112 100644 --- a/packages/platform-apple/package.json +++ b/packages/platform-apple/package.json @@ -17,10 +17,22 @@ "types": "./src/index.ts", "default": "./src/index.ts" }, + "./app-lifecycle": { + "types": "./src/app-lifecycle-facade.ts", + "default": "./src/app-lifecycle-facade.ts" + }, "./app-resolution": { "types": "./src/app-resolution-facade.ts", "default": "./src/app-resolution-facade.ts" }, + "./debug-symbols": { + "types": "./src/debug-symbols-facade.ts", + "default": "./src/debug-symbols-facade.ts" + }, + "./doctor": { + "types": "./src/doctor-facade.ts", + "default": "./src/doctor-facade.ts" + }, "./interactions": { "types": "./src/interactions-facade.ts", "default": "./src/interactions-facade.ts" @@ -29,6 +41,10 @@ "types": "./src/install-artifact-facade.ts", "default": "./src/install-artifact-facade.ts" }, + "./macos": { + "types": "./src/macos-facade.ts", + "default": "./src/macos-facade.ts" + }, "./perf": { "types": "./src/perf-facade.ts", "default": "./src/perf-facade.ts" @@ -41,6 +57,10 @@ "types": "./src/runner/index.ts", "default": "./src/runner/index.ts" }, + "./runner/operations": { + "types": "./src/runner-operations-facade.ts", + "default": "./src/runner-operations-facade.ts" + }, "./runner/client": { "types": "./src/runner/client.ts", "default": "./src/runner/client.ts" diff --git a/packages/platform-apple/src/__tests__/doctor.test.ts b/packages/platform-apple/src/__tests__/doctor.test.ts index 5d8bbbcb45..bafcc02e16 100644 --- a/packages/platform-apple/src/__tests__/doctor.test.ts +++ b/packages/platform-apple/src/__tests__/doctor.test.ts @@ -13,8 +13,8 @@ vi.mock('../core/runner-client.ts', () => ({ hasCachedAppleRunnerArtifact: mockHasCachedRunnerArtifact, prewarmAppleRunnerCache: mockPrewarmRunnerCache, })); -vi.mock('@agent-device/host-kit/environment', async (importOriginal) => ({ - ...(await importOriginal()), +vi.mock('@agent-device/host-kit/process', async (importOriginal) => ({ + ...(await importOriginal()), hostPlatform: mockHostPlatform, })); diff --git a/packages/platform-apple/src/app-lifecycle-facade.ts b/packages/platform-apple/src/app-lifecycle-facade.ts new file mode 100644 index 0000000000..1093443fed --- /dev/null +++ b/packages/platform-apple/src/app-lifecycle-facade.ts @@ -0,0 +1 @@ +export { closeIosApp, openIosApp, openIosDevice } from './core/app-launch.ts'; diff --git a/packages/platform-apple/src/app-resolution-facade.ts b/packages/platform-apple/src/app-resolution-facade.ts index 10462ec42d..a0bacaa0bb 100644 --- a/packages/platform-apple/src/app-resolution-facade.ts +++ b/packages/platform-apple/src/app-resolution-facade.ts @@ -1,2 +1,10 @@ export { buildAppNotInstalledError } from './core/app-resolution-error.ts'; -export { resolveIosAppAlias } from './core/app-resolution.ts'; +export { + detectSoleRunningIosSimulatorApp, + findIosSimulatorInstalledApp, + invalidateIosAppResolutionCache, + listIosApps, + resolveIosApp, + resolveIosAppAlias, + resolveIosSimulatorDeepLinkBundleId, +} from './core/app-resolution.ts'; diff --git a/packages/platform-apple/src/core/__tests__/app-install.test.ts b/packages/platform-apple/src/core/__tests__/app-install.test.ts deleted file mode 100644 index 5fbebdc4b5..0000000000 --- a/packages/platform-apple/src/core/__tests__/app-install.test.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { beforeEach, expect, test, vi } from 'vitest'; -import type { DeviceInfo } from '@agent-device/kernel/device'; - -const { ensureBootedSimulator, runSimctl } = vi.hoisted(() => ({ - ensureBootedSimulator: vi.fn(), - runSimctl: vi.fn(), -})); - -vi.mock('../simulator.ts', () => ({ ensureBootedSimulator })); -vi.mock('../apps-simctl.ts', () => ({ - isMissingAppErrorOutput: () => false, - runSimctl, -})); - -import { installIosInstallablePath } from '../app-install.ts'; - -const device: DeviceInfo = { - platform: 'apple', - appleOs: 'ios', - id: 'apple-abort', - name: 'Apple abort', - kind: 'simulator', - target: 'mobile', - booted: true, -}; - -beforeEach(() => { - ensureBootedSimulator.mockReset(); - ensureBootedSimulator.mockResolvedValue(undefined); - runSimctl.mockReset(); -}); - -test('aborts an in-flight simctl install with the deployment binding signal', async () => { - const controller = new AbortController(); - const aborted = new Error('request aborted'); - runSimctl.mockImplementation( - async (_device: DeviceInfo, _args: string[], options: Readonly<{ signal?: AbortSignal }>) => - await rejectWhenAborted(options.signal), - ); - - const pending = installIosInstallablePath(device, '/tmp/App.app', { - signal: controller.signal, - }); - await vi.waitFor(() => expect(runSimctl).toHaveBeenCalledOnce()); - expect(runSimctl).toHaveBeenCalledWith(device, ['install', device.id, '/tmp/App.app'], { - signal: controller.signal, - }); - controller.abort(aborted); - - await expect(pending).rejects.toBe(aborted); -}); - -async function rejectWhenAborted(signal: AbortSignal | undefined): Promise { - return await new Promise((_resolve, reject) => { - signal?.addEventListener('abort', () => reject(signal.reason), { once: true }); - }); -} diff --git a/packages/platform-apple/src/core/__tests__/apps.test.ts b/packages/platform-apple/src/core/__tests__/apps.test.ts index 504633cacd..30dda18f7d 100644 --- a/packages/platform-apple/src/core/__tests__/apps.test.ts +++ b/packages/platform-apple/src/core/__tests__/apps.test.ts @@ -25,21 +25,15 @@ const retryActual = await vi.importActual('../simulator.ts'); -import { - closeIosApp, - installIosInstallablePath, - openIosApp, - pushIosNotification, - readIosClipboardText, - resolveIosApp, - resolveIosSimulatorDeepLinkBundleId, - screenshotIos, - setIosSetting, -} from '../apps.ts'; +import { closeIosApp, openIosApp } from '../app-launch.ts'; +import { pushIosNotification, readIosClipboardText } from '../app-device-io.ts'; +import { resolveIosApp, resolveIosSimulatorDeepLinkBundleId } from '../app-resolution.ts'; +import { screenshotIos } from '../screenshot.ts'; +import { setIosSetting } from '../app-settings.ts'; import { withMockedMacOsHelper } from './macos-helper-test-utils.ts'; import { quitMacOsApp, resolveMacOsHelperPackageRootFrom } from '../../os/macos/helper.ts'; import { ensureBootedSimulator } from '../simulator.ts'; -import { IOS_DEVICE_INSTALL_TIMEOUT_MS, IOS_SIMULATOR_TERMINATE_TIMEOUT_MS } from '../config.ts'; +import { IOS_SIMULATOR_TERMINATE_TIMEOUT_MS } from '../config.ts'; import type { DeviceInfo } from '@agent-device/kernel/device'; import { AppError } from '@agent-device/kernel/errors'; import { runCmd } from '@agent-device/host-kit/command'; @@ -546,26 +540,6 @@ test('quitMacOsApp rejects invalid bundle identifiers before invoking helper', a await assert.rejects(() => quitMacOsApp('not a bundle id'), /reverse-DNS form/i); }); -test('installIosInstallablePath on iOS physical device uses extended devicectl install timeout', async () => { - mockRunCmd.mockResolvedValue({ stdout: '', stderr: '', exitCode: 0 }); - - await installIosInstallablePath(IOS_TEST_DEVICE, '/tmp/Sample.app'); - - assert.equal(mockRunCmd.mock.calls.length, 1); - assert.equal(mockRunCmd.mock.calls[0]?.[0], 'xcrun'); - assert.deepEqual(mockRunCmd.mock.calls[0]?.[1], [ - 'devicectl', - 'device', - 'install', - 'app', - '--device', - 'ios-device-1', - '/tmp/Sample.app', - ]); - assert.equal(mockRunCmd.mock.calls[0]?.[2]?.allowFailure, true); - assert.equal(mockRunCmd.mock.calls[0]?.[2]?.timeoutMs, IOS_DEVICE_INSTALL_TIMEOUT_MS); -}); - test('openIosApp with app and URL on iOS device launches app bundle with payload URL', async () => { const device: DeviceInfo = { platform: 'apple', @@ -732,41 +706,6 @@ test('resolveIosSimulatorDeepLinkBundleId maps custom URL scheme to installed us ); }); -test('installIosInstallablePath invalidates cached display-name bundle matches', async () => { - const device: DeviceInfo = { - platform: 'apple', - id: 'sim-cache-install-1', - name: 'iPhone Cache', - kind: 'simulator', - booted: true, - }; - mockEnsureBootedSimulator.mockResolvedValue(undefined); - - let installed = false; - await withFakeAppleTool( - (args) => { - if (args[0] === 'simctl' && args[1] === 'listapps') { - return installed - ? '{"com.example.installedcachemaps":{"CFBundleDisplayName":"Cache Maps"}}' - : '{"com.example.cachemaps":{"CFBundleDisplayName":"Cache Maps"}}'; - } - if (args[0] === 'simctl' && args[1] === 'install') { - installed = true; - return ''; - } - return unexpectedArgs(args); - }, - async () => { - const before = await resolveIosApp(device, 'Cache Maps'); - await installIosInstallablePath(device, '/fake/Cache.app'); - const after = await resolveIosApp(device, 'Cache Maps'); - - assert.equal(before, 'com.example.cachemaps'); - assert.equal(after, 'com.example.installedcachemaps'); - }, - ); -}); - test('setIosSetting faceid match uses simctl biometric match', async () => { await withFakeAppleTool( (args) => { diff --git a/packages/platform-apple/src/core/__tests__/physical-device-control.test.ts b/packages/platform-apple/src/core/__tests__/physical-device-control.test.ts index d9230011c2..f155fd6c90 100644 --- a/packages/platform-apple/src/core/__tests__/physical-device-control.test.ts +++ b/packages/platform-apple/src/core/__tests__/physical-device-control.test.ts @@ -6,7 +6,6 @@ import { IOS_DEVICE as SHARED_IOS_DEVICE } from '../../__tests__/device-fixtures import type { DeviceInfo } from '@agent-device/kernel/device'; import type { AppError } from '@agent-device/kernel/errors'; import { createAppleInteractor } from '../../interactor.ts'; -import { installIosInstallablePath } from '../app-install.ts'; import { closeIosApp, openIosApp } from '../app-launch.ts'; import { listIosApps } from '../app-resolution.ts'; import { resolveIosPhysicalDeviceControl } from '../physical-device-control.ts'; @@ -127,13 +126,9 @@ test('default interactor screenshots stay in-band without invoking devicectl', a } }); -test('CoreDevice-only app inventory and install fail with targeted XCTest guidance', async () => { +test('CoreDevice-only app inventory fails with targeted XCTest guidance', async () => { await assert.rejects( () => listIosApps(XCTEST_IOS_DEVICE, 'all'), /App inventory is unavailable on this XCTest-backed physical iOS device/, ); - await assert.rejects( - () => installIosInstallablePath(XCTEST_IOS_DEVICE, '/missing/example.app'), - /Installing apps is unavailable on this XCTest-backed physical iOS device/, - ); }); diff --git a/packages/platform-apple/src/core/app-device-io.ts b/packages/platform-apple/src/core/app-device-io.ts index ead01ce6d2..99aaca3d4f 100644 --- a/packages/platform-apple/src/core/app-device-io.ts +++ b/packages/platform-apple/src/core/app-device-io.ts @@ -1,8 +1,11 @@ -import { fs } from '@agent-device/host-kit/filesystem'; import path from 'node:path'; import { isMacOs, type DeviceInfo } from '@agent-device/kernel/device'; import { requireExecSuccess } from '@agent-device/host-kit/command'; -import { hostTemporaryDirectory } from '@agent-device/host-kit/environment'; +import { + makeHostTemporaryDirectory, + removeHostPath, + writeHostTextFile, +} from '@agent-device/host-kit/host-file'; import { ensureBootedSimulator, requireSimulatorDevice } from './simulator.ts'; import { readMacOsClipboardText, writeMacOsClipboardText } from '../os/macos/apps.ts'; import { runSimctl } from './apps-simctl.ts'; @@ -45,14 +48,14 @@ export async function pushIosNotification( requireSimulatorDevice(device, 'push'); options.signal?.throwIfAborted(); await ensureBootedSimulator(device, { signal: options.signal }); - const tempDir = await fs.mkdtemp(path.join(hostTemporaryDirectory(), 'agent-device-ios-push-')); + const tempDir = await makeHostTemporaryDirectory('agent-device-ios-push-'); const payloadPath = path.join(tempDir, 'payload.apns'); try { - await fs.writeFile(payloadPath, `${JSON.stringify(payload)}\n`, 'utf8'); + await writeHostTextFile(payloadPath, `${JSON.stringify(payload)}\n`); await runSimctl(device, ['push', device.id, bundleId, payloadPath], { signal: options.signal, }); } finally { - await fs.rm(tempDir, { recursive: true, force: true }); + await removeHostPath(tempDir); } } diff --git a/packages/platform-apple/src/core/app-install.ts b/packages/platform-apple/src/core/app-install.ts deleted file mode 100644 index 0b7683020f..0000000000 --- a/packages/platform-apple/src/core/app-install.ts +++ /dev/null @@ -1,30 +0,0 @@ -import type { DeviceInfo } from '@agent-device/kernel/device'; -import { ensureBootedSimulator } from './simulator.ts'; -import { invalidateIosAppResolutionCache } from './app-resolution.ts'; -import { runSimctl } from './apps-simctl.ts'; -import { resolveIosPhysicalDeviceControl } from './physical-device-control.ts'; - -type IosInstallSignalOptions = { - signal?: AbortSignal; -}; - -export async function installIosInstallablePath( - device: DeviceInfo, - installablePath: string, - options: IosInstallSignalOptions = {}, -): Promise { - await invalidateIosAppResolutionCache(device, async () => { - options.signal?.throwIfAborted(); - if (device.kind !== 'simulator') { - await resolveIosPhysicalDeviceControl(device).installApp( - device, - installablePath, - options.signal, - ); - return; - } - - await ensureBootedSimulator(device, { signal: options.signal }); - await runSimctl(device, ['install', device.id, installablePath], { signal: options.signal }); - }); -} diff --git a/packages/platform-apple/src/core/app-launch.ts b/packages/platform-apple/src/core/app-launch.ts index b07c71519d..6a7398b644 100644 --- a/packages/platform-apple/src/core/app-launch.ts +++ b/packages/platform-apple/src/core/app-launch.ts @@ -1,9 +1,9 @@ -import { fs } from '@agent-device/host-kit/filesystem'; import path from 'node:path'; import { isIosFamily, isMacOs, type DeviceInfo } from '@agent-device/kernel/device'; import { AppError } from '@agent-device/kernel/errors'; import { execFailureDetails } from '@agent-device/host-kit/command'; import { emitDiagnostic } from '@agent-device/host-kit/diagnostics'; +import { ensureHostDirectory, writeHostTextFile } from '@agent-device/host-kit/host-file'; import { Deadline, retryWithPolicy } from '@agent-device/host-kit/retry'; import { @@ -282,7 +282,7 @@ async function runIosSimulatorConsoleLaunch( launchArgs: string[], logPath: string, ): Promise>> { - await fs.mkdir(path.dirname(logPath), { recursive: true }); + await ensureHostDirectory(path.dirname(logPath)); try { const result = await runXcrun(launchArgs, { allowFailure: true, @@ -318,7 +318,7 @@ async function writeIosSimulatorConsoleLog( stdout: string, stderr: string, ): Promise { - await fs.writeFile(logPath, joinProcessOutput(stdout, stderr), 'utf8'); + await writeHostTextFile(logPath, joinProcessOutput(stdout, stderr)); } function joinProcessOutput(stdout: string, stderr: string): string { diff --git a/packages/platform-apple/src/core/app-resolution.ts b/packages/platform-apple/src/core/app-resolution.ts index 0bc98b967a..2e7cda261b 100644 --- a/packages/platform-apple/src/core/app-resolution.ts +++ b/packages/platform-apple/src/core/app-resolution.ts @@ -1,5 +1,5 @@ import path from 'node:path'; -import { fs } from '@agent-device/host-kit/filesystem'; +import { hostFileStat } from '@agent-device/host-kit/host-file'; import { fileURLToPath } from 'node:url'; import { isIosFamily, isMacOs, type DeviceInfo } from '@agent-device/kernel/device'; import { AppError } from '@agent-device/kernel/errors'; @@ -325,7 +325,7 @@ const iosSimulatorAppUrlSchemeMemo = createTtlMemo>({ async function readIosSimulatorAppUrlSchemes(infoPlistPath: string): Promise> { let cacheKey: string | undefined; try { - cacheKey = `${infoPlistPath}:${(await fs.stat(infoPlistPath)).mtimeMs}`; + cacheKey = `${infoPlistPath}:${(await hostFileStat(infoPlistPath)).mtimeMs}`; } catch { cacheKey = undefined; } diff --git a/packages/platform-apple/src/core/app-settings.ts b/packages/platform-apple/src/core/app-settings.ts index 39e884259a..56acf8a4b9 100644 --- a/packages/platform-apple/src/core/app-settings.ts +++ b/packages/platform-apple/src/core/app-settings.ts @@ -6,8 +6,8 @@ import { } from '@agent-device/contracts/settings'; import { isIosFamily, isMacOs, type DeviceInfo } from '@agent-device/kernel/device'; import { AppError } from '@agent-device/kernel/errors'; -import { fs } from '@agent-device/host-kit/filesystem'; -import { readHostEnvironmentVariable } from '@agent-device/host-kit/environment'; +import { readHostDirectory, removeHostPath } from '@agent-device/host-kit/host-file'; +import { readHostEnvironmentVariable } from '@agent-device/host-kit/process'; import path from 'node:path'; import { resolveIosSimulatorDeviceSetPath } from '@agent-device/kernel/device-isolation'; import { requireExecSuccess } from '@agent-device/host-kit/command'; @@ -184,15 +184,8 @@ async function clearIosSimulatorAppState( ); } - const entries = await fs.readdir(containerPath); - await Promise.all( - entries.map((entry) => - fs.rm(path.join(containerPath, entry), { - recursive: true, - force: true, - }), - ), - ); + const entries = await readHostDirectory(containerPath); + await Promise.all(entries.map((entry) => removeHostPath(path.join(containerPath, entry)))); return { bundleId, containerPath }; } diff --git a/packages/platform-apple/src/core/apps.ts b/packages/platform-apple/src/core/apps.ts deleted file mode 100644 index d43906241c..0000000000 --- a/packages/platform-apple/src/core/apps.ts +++ /dev/null @@ -1,10 +0,0 @@ -export { screenshotIos } from './screenshot.ts'; -export { resolveIosApp, resolveIosSimulatorDeepLinkBundleId } from './app-resolution.ts'; -export { closeIosApp, openIosApp, openIosDevice } from './app-launch.ts'; -export { installIosInstallablePath } from './app-install.ts'; -export { - pushIosNotification, - readIosClipboardText, - writeIosClipboardText, -} from './app-device-io.ts'; -export { setIosSetting } from './app-settings.ts'; diff --git a/packages/platform-apple/src/core/debug-symbols.ts b/packages/platform-apple/src/core/debug-symbols.ts index 8891eb7b04..b70a0251ad 100644 --- a/packages/platform-apple/src/core/debug-symbols.ts +++ b/packages/platform-apple/src/core/debug-symbols.ts @@ -1,6 +1,11 @@ -import { fs } from '@agent-device/host-kit/filesystem'; import path from 'node:path'; -import { hostWorkingDirectory } from '@agent-device/host-kit/environment'; +import { + ensureHostDirectory, + hostFileStat, + readHostTextFile, + writeHostTextFile, +} from '@agent-device/host-kit/host-file'; +import { hostCurrentWorkingDirectory } from '@agent-device/host-kit/process'; import { readAppleCrashArtifact } from './debug-symbols/crash-artifact.ts'; import { matchImagesToDsyms, readDsymPaths, readDsymSlices } from './debug-symbols/dsym.ts'; import { summarizeCrashArtifact } from './debug-symbols/report.ts'; @@ -21,7 +26,7 @@ export async function symbolicateCrashArtifact( hint: 'Use debug symbols --artifact --dsym or --search-path --out .', }); } - const cwd = options.cwd ?? hostWorkingDirectory(); + const cwd = options.cwd ?? hostCurrentWorkingDirectory(); const artifactPath = resolvePath(cwd, options.artifact); const outPath = resolvePath(cwd, options.out ?? defaultOutPath(artifactPath)); const artifactText = await readTextFile(artifactPath, 'crash artifact'); @@ -45,8 +50,8 @@ export async function symbolicateCrashArtifact( const addressMap = await symbolicateAddresses(crash.addresses, matched, tools.atos); const output = crash.write(addressMap); - await fs.mkdir(path.dirname(outPath), { recursive: true }); - await fs.writeFile(outPath, output, 'utf8'); + await ensureHostDirectory(path.dirname(outPath)); + await writeHostTextFile(outPath, output); const matchedImages = [...matched.values()].map(({ image, dsym }) => ({ name: image.name, @@ -91,7 +96,7 @@ function throwUnsupportedArtifact(): never { async function readTextFile(filePath: string, label: string): Promise { await assertTextFileWithinLimit(filePath, label); try { - return await fs.readFile(filePath, 'utf8'); + return await readHostTextFile(filePath); } catch (error) { const message = error instanceof Error ? error.message : String(error); throw new AppError('INVALID_ARGS', `Failed to read ${label}: ${filePath}`, { message }); @@ -100,7 +105,7 @@ async function readTextFile(filePath: string, label: string): Promise { async function assertTextFileWithinLimit(filePath: string, label: string): Promise { try { - const stats = await fs.stat(filePath); + const stats = await hostFileStat(filePath); if (stats.size <= MAX_CRASH_ARTIFACT_BYTES) return; throw new AppError('INVALID_ARGS', `${label} is too large: ${filePath}`, { actualBytes: stats.size, diff --git a/packages/platform-apple/src/core/debug-symbols/dsym.ts b/packages/platform-apple/src/core/debug-symbols/dsym.ts index 1f5c416b57..174ea3b4eb 100644 --- a/packages/platform-apple/src/core/debug-symbols/dsym.ts +++ b/packages/platform-apple/src/core/debug-symbols/dsym.ts @@ -1,5 +1,5 @@ -import { fs } from '@agent-device/host-kit/filesystem'; import path from 'node:path'; +import { hostFileStat, readHostDirectory } from '@agent-device/host-kit/host-file'; import type { AppleImage, DsymMatch, DsymSlice } from './types.ts'; import { normalizeUuid, unique } from './utils.ts'; import { requireExecSuccess, runCmd } from '@agent-device/host-kit/command'; @@ -48,7 +48,7 @@ async function findDsymBundles(root: string): Promise { found.push(current); return; } - const entries = await fs.readdir(current, { withFileTypes: true }); + const entries = await readHostDirectory(current, { withFileTypes: true }); for (const entry of entries) { if (!entry.isDirectory()) continue; await walk(path.join(current, entry.name)); @@ -61,9 +61,9 @@ async function findDsymBundles(root: string): Promise { async function readSearchPathStat( current: string, root: string, -): Promise>> { +): Promise>> { try { - return await fs.stat(current); + return await hostFileStat(current); } catch { throw new AppError('INVALID_ARGS', `debug symbols search path does not exist: ${root}`, { hint: 'Pass an existing build products directory to --search-path, or pass --dsym directly.', @@ -104,7 +104,7 @@ async function readDsymBundleSlices(dsymPath: string, dwarfdump: string): Promis } async function assertDsymBundlePath(dsymPath: string): Promise { - const stat = await fs.stat(dsymPath).catch(() => null); + const stat = await hostFileStat(dsymPath).catch(() => null); if (stat?.isDirectory() && dsymPath.endsWith('.dSYM')) return; throw new AppError('INVALID_ARGS', `Not a .dSYM bundle: ${dsymPath}`, { hint: 'Pass the .dSYM bundle path, not the DWARF executable inside it.', diff --git a/packages/platform-apple/src/core/devicectl.ts b/packages/platform-apple/src/core/devicectl.ts index 6d67d3defd..dfdbf3628e 100644 --- a/packages/platform-apple/src/core/devicectl.ts +++ b/packages/platform-apple/src/core/devicectl.ts @@ -1,10 +1,14 @@ -import { fs } from '@agent-device/host-kit/filesystem'; import path from 'node:path'; import type { DeviceInfo } from '@agent-device/kernel/device'; import { AppError } from '@agent-device/kernel/errors'; import { execFailureDetails } from '@agent-device/host-kit/command'; -import { hostProcessId, hostTemporaryDirectory } from '@agent-device/host-kit/environment'; +import { + hostTemporaryDirectory, + readHostTextFile, + unlinkHostFile, +} from '@agent-device/host-kit/host-file'; +import { hostProcessId } from '@agent-device/host-kit/process'; import { IOS_DEVICECTL_TIMEOUT_MS } from './config.ts'; import { runXcrun } from './tool-provider.ts'; @@ -221,12 +225,12 @@ async function runIosDevicectlJsonCommand( cause: String(error), }); } finally { - await fs.unlink(jsonPath).catch(() => {}); + await unlinkHostFile(jsonPath).catch(() => {}); } } async function readJsonFile(jsonPath: string): Promise { - return JSON.parse(await fs.readFile(jsonPath, 'utf8')); + return JSON.parse(await readHostTextFile(jsonPath)); } function processPathDepth(processInfo: IosDeviceProcessInfo, appBundleUrl: string): number { diff --git a/packages/platform-apple/src/core/install-artifact.ts b/packages/platform-apple/src/core/install-artifact.ts index 8fd5483514..4217ed13b3 100644 --- a/packages/platform-apple/src/core/install-artifact.ts +++ b/packages/platform-apple/src/core/install-artifact.ts @@ -1,4 +1,3 @@ -import { fs } from '@agent-device/host-kit/filesystem'; import path from 'node:path'; import type { LocalInstallSource } from '@agent-device/kernel/contracts'; import { readIosBundleInfo } from './bundle-info.ts'; @@ -15,7 +14,11 @@ import { isTrustedInstallSourceUrl, materializeInstallablePath, } from '@agent-device/provision-kit/install-source'; -import { hostTemporaryDirectory } from '@agent-device/host-kit/environment'; +import { + makeHostTemporaryDirectory, + readHostDirectory, + removeHostPath, +} from '@agent-device/host-kit/host-file'; type InstallIosArtifactOptions = { appIdentifierHint?: string; @@ -114,9 +117,9 @@ async function resolveIosIpaInstallablePath( appPath: string, options?: InstallIosArtifactOptions, ): Promise<{ installPath: string; cleanup: () => Promise }> { - const tempDir = await fs.mkdtemp(path.join(hostTemporaryDirectory(), 'agent-device-ios-ipa-')); + const tempDir = await makeHostTemporaryDirectory('agent-device-ios-ipa-'); const cleanup = async () => { - await fs.rm(tempDir, { recursive: true, force: true }); + await removeHostPath(tempDir); }; try { const outputRoot = path.join(tempDir, 'extracted'); @@ -132,9 +135,11 @@ async function resolveIosIpaInstallablePath( }); noteInstallArtifactArchiveDepth(depth); const payloadDir = path.join(outputRoot, 'Payload'); - const payloadEntries = await fs.readdir(payloadDir, { withFileTypes: true }).catch(() => { - throw new AppError('INVALID_ARGS', 'Invalid IPA: missing Payload directory'); - }); + const payloadEntries = await readHostDirectory(payloadDir, { withFileTypes: true }).catch( + () => { + throw new AppError('INVALID_ARGS', 'Invalid IPA: missing Payload directory'); + }, + ); const appBundles: IosPayloadAppBundle[] = payloadEntries .filter((entry) => entry.isDirectory() && entry.name.toLowerCase().endsWith('.app')) .map((entry) => ({ diff --git a/packages/platform-apple/src/core/perf-xctrace.ts b/packages/platform-apple/src/core/perf-xctrace.ts index ef7e0016e1..87a622f1ab 100644 --- a/packages/platform-apple/src/core/perf-xctrace.ts +++ b/packages/platform-apple/src/core/perf-xctrace.ts @@ -1,4 +1,3 @@ -import { fs } from '@agent-device/host-kit/filesystem'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { @@ -18,7 +17,17 @@ import { type ExecResult, } from '@agent-device/host-kit/command'; import { uniqueStrings } from '@agent-device/kernel/collections'; -import { hostTemporaryDirectory } from '@agent-device/host-kit/environment'; +import { + copyHostPath, + ensureHostDirectory, + hostFileStat, + makeHostTemporaryDirectory, + readHostDirectory, + readHostTextFile, + removeHostPath, + renameHostPath, + writeHostTextFile, +} from '@agent-device/host-kit/host-file'; import { findAllXmlNodes } from './perf-xml.ts'; import { parseAppleTimeProfileSummary, @@ -99,7 +108,7 @@ export async function startAppleXctracePerfCapture(params: { outPath: string; }): Promise { const target = await resolveAppleXctracePerfTarget(params.device, params.appBundleId); - await fs.mkdir(path.dirname(params.outPath), { recursive: true }); + await ensureHostDirectory(path.dirname(params.outPath)); const args = buildAppleXctraceRecordArgs({ device: params.device, template: params.template, @@ -133,7 +142,7 @@ export async function stopAppleXctracePerfCapture( outPath = capture.outPath, ): Promise { if (outPath !== capture.outPath) { - await fs.mkdir(path.dirname(outPath), { recursive: true }); + await ensureHostDirectory(path.dirname(outPath)); } const result = requireExecSuccess( await stopAppleXctraceProcess(capture, { failOnForcedKill: true }), @@ -145,9 +154,9 @@ export async function stopAppleXctracePerfCapture( }), ); if (outPath !== capture.outPath) { - await fs.rename(capture.outPath, outPath).catch(async () => { - await fs.cp(capture.outPath, outPath, { recursive: true }); - await fs.rm(capture.outPath, { recursive: true, force: true }); + await renameHostPath(capture.outPath, outPath).catch(async () => { + await copyHostPath(capture.outPath, outPath); + await removeHostPath(capture.outPath); }); } await assertTracePathHasData(outPath, { @@ -183,9 +192,7 @@ export async function writeAppleXctracePerfReport(params: { template?: string; appBundleId?: string; }): Promise { - const tempDir = await fs.mkdtemp( - path.join(hostTemporaryDirectory(), 'agent-device-xctrace-report-'), - ); + const tempDir = await makeHostTemporaryDirectory('agent-device-xctrace-report-'); const tocPath = path.join(tempDir, 'trace-toc.xml'); const timeProfilePath = path.join(tempDir, 'time-profile.xml'); try { @@ -211,7 +218,7 @@ export async function writeAppleXctracePerfReport(params: { hint: resolveIosDevicePerfHint(exportResult.stdout, exportResult.stderr), }), ); - const tocXml = await fs.readFile(tocPath, 'utf8'); + const tocXml = await readHostTextFile(tocPath); const timeProfileXml = await exportAppleTimeProfile(params.tracePath, timeProfilePath); const report = buildAppleXctracePerfReport({ ...params, @@ -225,11 +232,11 @@ export async function writeAppleXctracePerfReport(params: { hint: 'Keep the app active while recording, then retry. Open the raw trace in Instruments if it still contains no Time Profiler samples.', }); } - await fs.mkdir(path.dirname(params.outPath), { recursive: true }); - await fs.writeFile(params.outPath, `${JSON.stringify(report, null, 2)}\n`, 'utf8'); + await ensureHostDirectory(path.dirname(params.outPath)); + await writeHostTextFile(params.outPath, `${JSON.stringify(report, null, 2)}\n`); return report; } finally { - await fs.rm(tempDir, { recursive: true, force: true }).catch(() => {}); + await removeHostPath(tempDir).catch(() => {}); } } @@ -257,7 +264,7 @@ async function exportAppleTimeProfile(tracePath: string, outPath: string): Promi hint: resolveIosDevicePerfHint(exportResult.stdout, exportResult.stderr), }), ); - return await fs.readFile(outPath, 'utf8'); + return await readHostTextFile(outPath); } async function resolveAppleXctracePerfTarget( @@ -414,10 +421,10 @@ async function assertTracePathHasData( stderr: string; }, ): Promise { - const stat = await fs.stat(tracePath).catch(() => null); + const stat = await hostFileStat(tracePath).catch(() => null); const hasTrace = stat?.isDirectory() === true - ? (await fs.readdir(tracePath).catch(() => [])).length > 0 + ? (await readHostDirectory(tracePath).catch(() => [])).length > 0 : (stat?.size ?? 0) > 0; if (hasTrace) return; throw new AppError('COMMAND_FAILED', 'xctrace produced no trace data', { diff --git a/packages/platform-apple/src/core/perf.ts b/packages/platform-apple/src/core/perf.ts index b2ade72368..d73ba40860 100644 --- a/packages/platform-apple/src/core/perf.ts +++ b/packages/platform-apple/src/core/perf.ts @@ -1,4 +1,3 @@ -import { fs } from '@agent-device/host-kit/filesystem'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { @@ -17,7 +16,14 @@ import { } from '@agent-device/host-kit/command'; import { splitNonEmptyTrimmedLines } from '@agent-device/kernel/record'; import { uniqueStrings } from '@agent-device/kernel/collections'; -import { hostTemporaryDirectory } from '@agent-device/host-kit/environment'; +import { + ensureHostDirectory, + hostFileStat, + makeHostTemporaryDirectory, + readHostDirectory, + readHostTextFile, + removeHostPath, +} from '@agent-device/host-kit/host-file'; import { IOS_DEVICECTL_DEFAULT_HINT, resolveIosDevicectlHint } from './devicectl.ts'; import type { IosDeviceProcessInfo } from './app-info.ts'; import { resolveIosPhysicalDeviceControl } from './physical-device-control.ts'; @@ -161,7 +167,7 @@ export async function captureAppleMemorySnapshot( const target = await resolveAppleMemorySnapshotTarget(device, appBundleId, support); if (target.available === false) return target; const { process: processInfo } = target; - await fs.mkdir(path.dirname(outPath), { recursive: true }); + await ensureHostDirectory(path.dirname(outPath)); const hadLocalArtifact = await fileExists(outPath); let result: ExecResult; try { @@ -187,7 +193,7 @@ export async function captureAppleMemorySnapshot( ); } - const stat = await fs.stat(outPath).catch(() => null); + const stat = await hostFileStat(outPath).catch(() => null); if (!stat?.isFile() || stat.size <= 0) { await cleanupLocalArtifact(outPath, hadLocalArtifact); throw new AppError('COMMAND_FAILED', 'Apple memgraph artifact is missing or empty', { @@ -283,15 +289,14 @@ function annotateAppleMemorySnapshotToolError( // fallow-ignore-next-line code-duplication async function fileExists(filePath: string): Promise { - return await fs - .stat(filePath) + return await hostFileStat(filePath) .then((stat) => stat.isFile()) .catch(() => false); } async function cleanupLocalArtifact(filePath: string, existedBefore: boolean): Promise { if (existedBefore) return; - await fs.rm(filePath, { force: true }).catch(() => {}); + await removeHostPath(filePath).catch(() => {}); } export async function sampleAppleFramePerf( @@ -420,9 +425,7 @@ async function captureIosDeviceFramePerf( appBundleId: string, processes: IosDeviceProcessInfo[], ): Promise { - const tempDir = await fs.mkdtemp( - path.join(hostTemporaryDirectory(), 'agent-device-ios-frame-perf-'), - ); + const tempDir = await makeHostTemporaryDirectory('agent-device-ios-frame-perf-'); const tracePath = path.join(tempDir, 'animation-hitches.trace'); const hitchesPath = path.join(tempDir, 'hitches.xml'); const frameLifetimesPath = path.join(tempDir, 'frame-lifetimes.xml'); @@ -456,12 +459,12 @@ async function captureIosDeviceFramePerf( return { windowStartedAt: record.startedAt, windowEndedAt: record.endedAt, - hitchesXml: await fs.readFile(hitchesPath, 'utf8'), - frameLifetimesXml: await fs.readFile(frameLifetimesPath, 'utf8'), - displayInfoXml: hasDisplayInfo ? await fs.readFile(displayInfoPath, 'utf8') : undefined, + hitchesXml: await readHostTextFile(hitchesPath), + frameLifetimesXml: await readHostTextFile(frameLifetimesPath), + displayInfoXml: hasDisplayInfo ? await readHostTextFile(displayInfoPath) : undefined, }; } finally { - await fs.rm(tempDir, { recursive: true, force: true }).catch(() => {}); + await removeHostPath(tempDir).catch(() => {}); } } @@ -562,7 +565,7 @@ export async function prepareAppleTraceRecordRetry( retryDelayMs: number, ): Promise { if (attempt <= 1) return; - await fs.rm(tracePath, { recursive: true, force: true }).catch(() => {}); + await removeHostPath(tracePath).catch(() => {}); await new Promise((resolve) => setTimeout(resolve, retryDelayMs)); } @@ -576,10 +579,10 @@ async function assertUsableTraceOutput( stdout: string, stderr: string, ): Promise { - const stat = await fs.stat(params.tracePath).catch(() => null); + const stat = await hostFileStat(params.tracePath).catch(() => null); const hasTrace = stat?.isDirectory() === true - ? (await fs.readdir(params.tracePath).catch(() => [])).length > 0 + ? (await readHostDirectory(params.tracePath).catch(() => [])).length > 0 : (stat?.size ?? 0) > 0; if (hasTrace) return; throw new AppError('COMMAND_FAILED', `${params.failureMessage}: xctrace produced no trace data`, { @@ -805,7 +808,7 @@ async function captureIosDevicePerfTable( device: DeviceInfo, appBundleId: string, ): Promise { - const tempDir = await fs.mkdtemp(path.join(hostTemporaryDirectory(), 'agent-device-ios-perf-')); + const tempDir = await makeHostTemporaryDirectory('agent-device-ios-perf-'); const tracePath = path.join(tempDir, 'sample.trace'); const exportPath = path.join(tempDir, 'activity-monitor-process-live.xml'); try { @@ -827,10 +830,10 @@ async function captureIosDevicePerfTable( ); return { capturedAtMs: record.capturedAtMs, - xml: await fs.readFile(exportPath, 'utf8'), + xml: await readHostTextFile(exportPath), }; } finally { - await fs.rm(tempDir, { recursive: true, force: true }).catch(() => {}); + await removeHostPath(tempDir).catch(() => {}); } } diff --git a/packages/platform-apple/src/core/physical-device-coredevice.ts b/packages/platform-apple/src/core/physical-device-coredevice.ts index 0cab7a7144..72918637f6 100644 --- a/packages/platform-apple/src/core/physical-device-coredevice.ts +++ b/packages/platform-apple/src/core/physical-device-coredevice.ts @@ -1,9 +1,13 @@ -import { fs } from '@agent-device/host-kit/filesystem'; import path from 'node:path'; import type { DeviceInfo } from '@agent-device/kernel/device'; import { AppError } from '@agent-device/kernel/errors'; import { execFailureDetails } from '@agent-device/host-kit/command'; -import { hostProcessId, hostTemporaryDirectory } from '@agent-device/host-kit/environment'; +import { + hostTemporaryDirectory, + readHostTextFile, + removeHostPath, +} from '@agent-device/host-kit/host-file'; +import { hostProcessId } from '@agent-device/host-kit/process'; import { IOS_DEVICECTL_DEFAULT_HINT, resolveIosDevicectlHint, @@ -182,7 +186,7 @@ async function runCoreDeviceDetails( ); return { result, parsed: await readCoreDeviceDetails(jsonPath) }; } finally { - await fs.rm(jsonPath, { force: true }).catch(() => {}); + await removeHostPath(jsonPath).catch(() => {}); } } @@ -190,7 +194,7 @@ async function readCoreDeviceDetails( jsonPath: string, ): Promise<{ parsed: boolean; outcome?: string; tunnelState?: string; tunnelIp?: string }> { try { - const payload = JSON.parse(await fs.readFile(jsonPath, 'utf8')) as unknown; + const payload = JSON.parse(await readHostTextFile(jsonPath)) as unknown; const details = parseIosDeviceDetailsPayload(payload); return { parsed: true, ...details }; } catch { diff --git a/packages/platform-apple/src/core/physical-device-screenshot.ts b/packages/platform-apple/src/core/physical-device-screenshot.ts index 8d8c8c35c4..0e45408ce4 100644 --- a/packages/platform-apple/src/core/physical-device-screenshot.ts +++ b/packages/platform-apple/src/core/physical-device-screenshot.ts @@ -1,7 +1,7 @@ -import { fs } from '@agent-device/host-kit/filesystem'; import type { DeviceInfo } from '@agent-device/kernel/device'; import { AppError } from '@agent-device/kernel/errors'; import { emitDiagnostic } from '@agent-device/host-kit/diagnostics'; +import { writeHostBinaryFile } from '@agent-device/host-kit/host-file'; import { runIosDevicectl } from './devicectl.ts'; import { copyCoreDeviceRunnerFile } from './physical-device-files.ts'; import type { AppleRunnerCommandExecutor, AppleRunnerCommandOptions } from '../runner/index.ts'; @@ -83,7 +83,7 @@ async function writeInlineRunnerScreenshot( ): Promise { const inlineImage = result['imageBase64']; if (typeof inlineImage !== 'string' || inlineImage.length === 0) return false; - await fs.writeFile(outPath, Buffer.from(inlineImage, 'base64')); + await writeHostBinaryFile(outPath, Buffer.from(inlineImage, 'base64')); return true; } diff --git a/packages/platform-apple/src/core/plist.ts b/packages/platform-apple/src/core/plist.ts index 3d6d56c70b..09c6bd416a 100644 --- a/packages/platform-apple/src/core/plist.ts +++ b/packages/platform-apple/src/core/plist.ts @@ -1,4 +1,4 @@ -import { fs } from '@agent-device/host-kit/filesystem'; +import { readHostTextFile } from '@agent-device/host-kit/host-file'; import { parseXmlDocumentSync } from '@agent-device/xml'; import { readApplePlistJson, runAppleToolCommand } from './tool-provider.ts'; import { visitXmlPlistEntries } from './plist-xml.ts'; @@ -40,7 +40,7 @@ export async function readInfoPlistString( } try { - const plist = await fs.readFile(infoPlistPath, { encoding: 'utf8', signal }); + const plist = await readHostTextFile(infoPlistPath, { signal }); return readXmlPlistString(plist, key); } catch { signal?.throwIfAborted(); diff --git a/packages/platform-apple/src/core/screenshot.ts b/packages/platform-apple/src/core/screenshot.ts index 54c22078bc..ac6d98c574 100644 --- a/packages/platform-apple/src/core/screenshot.ts +++ b/packages/platform-apple/src/core/screenshot.ts @@ -1,8 +1,8 @@ -import { fs } from '@agent-device/host-kit/filesystem'; import path from 'node:path'; import { isMacOs, type DeviceInfo } from '@agent-device/kernel/device'; import { type ExecOptions } from '@agent-device/host-kit/command'; import { emitDiagnostic } from '@agent-device/host-kit/diagnostics'; +import { copyHostFile } from '@agent-device/host-kit/host-file'; import { Deadline, retryWithPolicy } from '@agent-device/host-kit/retry'; import { AppError } from '@agent-device/kernel/errors'; @@ -229,7 +229,7 @@ export async function captureScreenshotViaRunner( } if (isMacOs(device)) { - await fs.copyFile(remoteFileName, outPath); + await copyHostFile(remoteFileName, outPath); return; } @@ -304,7 +304,7 @@ async function tryCopySimulatorRunnerScreenshot( remoteFileName, )) { try { - await fs.copyFile(sourcePath, outPath); + await copyHostFile(sourcePath, outPath); return { copied: true }; } catch (error) { lastError = error instanceof Error ? error.message : String(error); diff --git a/packages/platform-apple/src/debug-symbols-facade.ts b/packages/platform-apple/src/debug-symbols-facade.ts new file mode 100644 index 0000000000..54e2a485ae --- /dev/null +++ b/packages/platform-apple/src/debug-symbols-facade.ts @@ -0,0 +1 @@ +export { symbolicateCrashArtifact } from './core/debug-symbols.ts'; diff --git a/packages/platform-apple/src/doctor-facade.ts b/packages/platform-apple/src/doctor-facade.ts new file mode 100644 index 0000000000..a59a7dca77 --- /dev/null +++ b/packages/platform-apple/src/doctor-facade.ts @@ -0,0 +1 @@ +export { appleRunnerWarmupCheck, appleToolchainCheck } from './doctor.ts'; diff --git a/packages/platform-apple/src/doctor.ts b/packages/platform-apple/src/doctor.ts index 15104f988a..f6b093bcf3 100644 --- a/packages/platform-apple/src/doctor.ts +++ b/packages/platform-apple/src/doctor.ts @@ -2,7 +2,7 @@ import { isIosFamily, type DeviceInfo } from '@agent-device/kernel/device'; import type { DoctorCheck } from '@agent-device/contracts/observability'; import type { HostDiagnosticsContext } from '@agent-device/contracts/host-diagnostics'; import { commandFirstLine } from '@agent-device/provision-kit/toolchain-probe'; -import { hostPlatform } from '@agent-device/host-kit/environment'; +import { hostPlatform } from '@agent-device/host-kit/process'; import { hasCachedAppleRunnerArtifact, prewarmAppleRunnerCache } from './core/runner-client.ts'; type AppleToolchainProbe = { diff --git a/packages/platform-apple/src/index.ts b/packages/platform-apple/src/index.ts index 61a3d6a1a4..c17d9c299b 100644 --- a/packages/platform-apple/src/index.ts +++ b/packages/platform-apple/src/index.ts @@ -403,5 +403,3 @@ export const applyXctestRunnerAppIconFromDerivedPath = async ( (typeof import('./core/runner-client.ts'))['applyXctestRunnerAppIconFromDerivedPath'] > ) => (await import('./core/runner-client.ts')).applyXctestRunnerAppIconFromDerivedPath(...args); -export const loadAppleRunnerHost = async () => - (await import('./core/runner-host.ts')).appleRunnerHost; diff --git a/packages/platform-apple/src/install-artifact-facade.ts b/packages/platform-apple/src/install-artifact-facade.ts index 117acc1b83..f9826d7432 100644 --- a/packages/platform-apple/src/install-artifact-facade.ts +++ b/packages/platform-apple/src/install-artifact-facade.ts @@ -1 +1,2 @@ export { readIosBundleInfo } from './core/bundle-info.ts'; +export { prepareIosInstallArtifact } from './core/install-artifact.ts'; diff --git a/packages/platform-apple/src/interactor.ts b/packages/platform-apple/src/interactor.ts index 709bb0d1bf..f860de748d 100644 --- a/packages/platform-apple/src/interactor.ts +++ b/packages/platform-apple/src/interactor.ts @@ -1,13 +1,7 @@ -import { - closeIosApp, - openIosApp, - openIosDevice, - readIosClipboardText, - screenshotIos, - setIosSetting, - writeIosClipboardText, -} from './core/apps.ts'; -import { captureScreenshotViaRunner } from './core/screenshot.ts'; +import { closeIosApp, openIosApp, openIosDevice } from './core/app-launch.ts'; +import { readIosClipboardText, writeIosClipboardText } from './core/app-device-io.ts'; +import { setIosSetting } from './core/app-settings.ts'; +import { captureScreenshotViaRunner, screenshotIos } from './core/screenshot.ts'; import { iosRunnerOverrides, resolveAppleBackRunnerCommand } from './interactions.ts'; import { appleRemotePressCommand } from './os/tvos/remote.ts'; import { runMacOsScreenshotAction } from './os/macos/helper.ts'; diff --git a/packages/platform-apple/src/macos-facade.ts b/packages/platform-apple/src/macos-facade.ts new file mode 100644 index 0000000000..d33d7dff3b --- /dev/null +++ b/packages/platform-apple/src/macos-facade.ts @@ -0,0 +1,6 @@ +export { + resolveFrontmostMacOsApp, + runMacOsAlertAction, + startMacOsAudioProbeProcess, +} from './os/macos/helper.ts'; +export { captureMacOsSurfaceSnapshot } from './os/macos/surface-snapshot.ts'; diff --git a/packages/platform-apple/src/os/macos/helper.ts b/packages/platform-apple/src/os/macos/helper.ts index 0597ce4ba0..1b5e660f1a 100644 --- a/packages/platform-apple/src/os/macos/helper.ts +++ b/packages/platform-apple/src/os/macos/helper.ts @@ -1,14 +1,25 @@ -import { existsSync, fs } from '@agent-device/host-kit/filesystem'; import { createHash } from 'node:crypto'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { AppError } from '@agent-device/kernel/errors'; import { + accessHostFile, + chmodHostFile, + copyHostFile, + ensureHostDirectory, + hostFileExistsSync, hostHomeDirectory, + readHostBinaryFile, + readHostDirectory, + readHostTextFile, + renameHostPath, + writeHostTextFile, +} from '@agent-device/host-kit/host-file'; +import { hostPlatform, readHostEnvironmentVariable, writeHostStderr, -} from '@agent-device/host-kit/environment'; +} from '@agent-device/host-kit/process'; import { resolveExecutableOverridePath, runCmdBackground, @@ -105,7 +116,7 @@ export function resolveMacOsHelperPackageRootFrom(modulePath: string): string { let currentDir = path.dirname(modulePath); while (true) { const candidate = path.join(currentDir, 'apple', 'macos-helper'); - if (existsSync(path.join(candidate, 'Package.swift'))) { + if (hostFileExistsSync(path.join(candidate, 'Package.swift'))) { return candidate; } const parentDir = path.dirname(currentDir); @@ -132,7 +143,7 @@ function resolveInstalledMacOsHelperPath(): string { } async function listMacOsHelperSourceFiles(root: string): Promise { - const entries = await fs.readdir(root, { withFileTypes: true }); + const entries = await readHostDirectory(root, { withFileTypes: true }); const files = await Promise.all( entries.map(async (entry) => { const entryPath = path.join(root, entry.name); @@ -154,7 +165,7 @@ async function computeMacOsHelperFingerprint(packageRoot: string): Promise { try { - const data = JSON.parse(await fs.readFile(MACOS_HELPER_MANIFEST_PATH, 'utf8')) as { + const data = JSON.parse(await readHostTextFile(MACOS_HELPER_MANIFEST_PATH)) as { fingerprint?: unknown; }; return typeof data.fingerprint === 'string' ? data.fingerprint : null; @@ -195,7 +206,7 @@ async function ensureMacOsHelperBinary(): Promise { try { const installedFingerprint = await readInstalledMacOsHelperFingerprint(); if (installedFingerprint === sourceFingerprint) { - await fs.access(installedPath); + await accessHostFile(installedPath); return installedPath; } } catch { @@ -208,15 +219,14 @@ async function ensureMacOsHelperBinary(): Promise { cwd: packageRoot, timeoutMs: 120_000, }); - await fs.mkdir(MACOS_HELPER_INSTALL_ROOT, { recursive: true }); + await ensureHostDirectory(MACOS_HELPER_INSTALL_ROOT); const tempInstalledPath = `${installedPath}.tmp`; - await fs.copyFile(sourceBinary, tempInstalledPath); - await fs.rename(tempInstalledPath, installedPath); - await fs.chmod(installedPath, 0o755); - await fs.writeFile( + await copyHostFile(sourceBinary, tempInstalledPath); + await renameHostPath(tempInstalledPath, installedPath); + await chmodHostFile(installedPath, 0o755); + await writeHostTextFile( MACOS_HELPER_MANIFEST_PATH, `${JSON.stringify({ fingerprint: sourceFingerprint }, null, 2)}\n`, - 'utf8', ); return installedPath; } diff --git a/packages/platform-apple/src/os/macos/host-provider.ts b/packages/platform-apple/src/os/macos/host-provider.ts index 22a37eeaa7..710fb25a34 100644 --- a/packages/platform-apple/src/os/macos/host-provider.ts +++ b/packages/platform-apple/src/os/macos/host-provider.ts @@ -1,5 +1,4 @@ -import { fs } from '@agent-device/host-kit/filesystem'; -import { hostHomeDirectory } from '@agent-device/host-kit/environment'; +import { hostFileStat, hostHomeDirectory } from '@agent-device/host-kit/host-file'; import path from 'node:path'; import type { AppsFilter } from '@agent-device/contracts/device'; import { AppError } from '@agent-device/kernel/errors'; @@ -87,7 +86,7 @@ async function listLocalMacApps( const appPaths = new Set(); for (const root of appRoots) { - const stat = await fs.stat(root).catch(() => null); + const stat = await hostFileStat(root).catch(() => null); if (!stat?.isDirectory()) continue; const result = await runCommand( 'find', diff --git a/packages/platform-apple/src/perf-facade.ts b/packages/platform-apple/src/perf-facade.ts index 241b8d4d64..a3afca34b1 100644 --- a/packages/platform-apple/src/perf-facade.ts +++ b/packages/platform-apple/src/perf-facade.ts @@ -2,6 +2,22 @@ export { buildAppleFrameSamplingMetadata, buildAppleMemorySamplingMetadata, buildAppleMemorySnapshotSupport, + captureAppleMemorySnapshot, isRetryableIosDeviceTraceRecordFailure, + readAppleProcessSamples, + resolveAppleExecutable, + resolveIosDevicePerfTarget, resolveIosDevicePerfHint, + sampleAppleFramePerf, + sampleAppleMemoryPerf, } from './core/perf.ts'; +export { + cleanupAppleXctracePerfCapture, + startAppleXctracePerfCapture, + stopAppleXctracePerfCapture, + writeAppleXctracePerfReport, + type AppleXctraceCpuProfileReport, + type AppleXctracePerfCapture, + type AppleXctracePerfMode, + type AppleXctracePerfResult, +} from './core/perf-xctrace.ts'; diff --git a/packages/platform-apple/src/runner-operations-facade.ts b/packages/platform-apple/src/runner-operations-facade.ts new file mode 100644 index 0000000000..9bbb31d90e --- /dev/null +++ b/packages/platform-apple/src/runner-operations-facade.ts @@ -0,0 +1,27 @@ +export { + applyXctestRunnerAppIconFromDerivedPath, + detachIosSimulatorRunnerSessionsForShutdown, + getRunnerSessionSnapshot, + hasCachedAppleRunnerArtifact, + notifyIosRunnerAppRelaunched, + prepareIosRunner, + prewarmAppleRunnerCache, + prewarmIosRunnerSession, + readStaleRunnerLease, + resolveRunnerAppBundleId, + runApplePressSeries, + runAppleRunnerCommand, + scheduleIosRunnerIdleStop, + stopAllIosRunnerSessions, + stopIosRunnerSession, + verifyLeaseRunnerPidIdentity, +} from './core/runner-client.ts'; +export { queryAppleRunnerSelector } from './core/runner-selector-query.ts'; + +export async function cleanupRunnerLeasesForOwner( + owner: Parameters<(typeof import('./core/runner-client.ts'))['cleanupRunnerLeasesForOwner']>[0], +): Promise { + const { cleanupRunnerLeasesForOwner: cleanup } = await import('./core/runner-client.ts'); + const { runnerLeaseCleanupAdapter } = await import('./runner/runner-disposal.ts'); + await cleanup(owner, runnerLeaseCleanupAdapter); +} diff --git a/packages/platform-apple/src/runner/test-host.ts b/packages/platform-apple/src/runner/test-host.ts index 6c165302e2..b19e06ec8d 100644 --- a/packages/platform-apple/src/runner/test-host.ts +++ b/packages/platform-apple/src/runner/test-host.ts @@ -13,6 +13,10 @@ let defaults: AppleRunnerHost | undefined; let overrides: Partial = {}; let bound = false; +export async function loadAppleRunnerHost(): Promise { + return (await import('../core/runner-host.ts')).appleRunnerHost; +} + export function installAppleRunnerTestHost(realHost: AppleRunnerHost): void { defaults = realHost; overrides = {}; diff --git a/packages/platform-apple/src/simulator-facade.ts b/packages/platform-apple/src/simulator-facade.ts index af85432354..af21d19b3d 100644 --- a/packages/platform-apple/src/simulator-facade.ts +++ b/packages/platform-apple/src/simulator-facade.ts @@ -1 +1,6 @@ -export { markSimulatorBooted, wasSimulatorRecentlyObservedBooted } from './core/simulator.ts'; +export { + ensureBootedSimulator, + markSimulatorBooted, + shutdownSimulator, + wasSimulatorRecentlyObservedBooted, +} from './core/simulator.ts'; diff --git a/packages/platform-apple/src/tool-provider-facade.ts b/packages/platform-apple/src/tool-provider-facade.ts index 16f3196fe1..731e42fc28 100644 --- a/packages/platform-apple/src/tool-provider-facade.ts +++ b/packages/platform-apple/src/tool-provider-facade.ts @@ -1 +1,10 @@ -export { hasScopedAppleToolProvider, resolveAppleToolProvider } from './core/tool-provider.ts'; +export { + createLocalAppleToolProvider, + hasScopedAppleToolProvider, + readApplePlistJson, + resolveAppleToolProvider, + runAppleToolCommand, + runXcrun, + withAppleToolProvider, +} from './core/tool-provider.ts'; +export type { AppleToolProvider } from './core/tool-provider.ts'; diff --git a/scripts/__tests__/help-conformance-sample-outputs.test.ts b/scripts/__tests__/help-conformance-sample-outputs.test.ts index 0d8ab5feef..39368a19ea 100644 --- a/scripts/__tests__/help-conformance-sample-outputs.test.ts +++ b/scripts/__tests__/help-conformance-sample-outputs.test.ts @@ -13,7 +13,7 @@ import { SAMPLE_PRODUCERS } from './help-conformance-sample-producers.ts'; // (help-conformance-error-recovery-coverage.test.ts). for (const { name, producer, sample, render } of SAMPLE_PRODUCERS) { - test(`${name} matches ${producer}`, async () => { - assert.equal(await render(), sample.output); + test(`${name} matches ${producer}`, () => { + assert.equal(render(), sample.output); }); } diff --git a/scripts/__tests__/help-conformance-sample-producers.ts b/scripts/__tests__/help-conformance-sample-producers.ts index 016cf520c2..1ff90fb414 100644 --- a/scripts/__tests__/help-conformance-sample-producers.ts +++ b/scripts/__tests__/help-conformance-sample-producers.ts @@ -53,7 +53,7 @@ export type SampleProducer = { producer: string; sample: CapturedSample; /** The sample's text rebuilt from production code, ready to compare. */ - render: () => string | Promise; + render: () => string; }; const formatPress = (result: Record) => diff --git a/scripts/__tests__/test-file-size-ratchet.test.ts b/scripts/__tests__/test-file-size-ratchet.test.ts index ce104d7fce..442b970c78 100644 --- a/scripts/__tests__/test-file-size-ratchet.test.ts +++ b/scripts/__tests__/test-file-size-ratchet.test.ts @@ -52,7 +52,7 @@ const PINNED_TEST_FILE_LINES: Readonly> = Object.freeze({ 'packages/platform-apple/src/core/__tests__/perf.test.ts': 1222, 'src/mcp/__tests__/command-tools.test.ts': 1216, 'src/daemon/handlers/__tests__/session-replay-divergence.test.ts': 1136, - 'packages/platform-apple/src/core/__tests__/apps.test.ts': 1207, + 'packages/platform-apple/src/core/__tests__/apps.test.ts': 1146, 'src/daemon/handlers/__tests__/session-replay-repair-transaction.test.ts': 1202, 'src/daemon/handlers/__tests__/session-replay-target-verification-runtime.test.ts': 1182, 'src/__tests__/client-metro.test.ts': 1105, diff --git a/scripts/check-affected/device-lanes.test.ts b/scripts/check-affected/device-lanes.test.ts index 2c42c3d1b2..59acf4f439 100644 --- a/scripts/check-affected/device-lanes.test.ts +++ b/scripts/check-affected/device-lanes.test.ts @@ -21,9 +21,9 @@ function lanes(file: string): CheckId[] { test('a TypeScript-only Apple change selects the iOS and macOS lanes without a Swift build', () => { for (const file of [ - 'packages/platform-apple/src/core/apps.ts', + 'packages/platform-apple/src/core/app-resolution.ts', 'packages/platform-apple/src/runtime.ts', - 'packages/platform-apple/src/os/macos/desktop.ts', + 'packages/platform-apple/src/os/macos/desktop-scroll.ts', ]) { assert.deepEqual(lanes(file), ['replay-ios', 'replay-ios-device', 'replay-macos'], file); assert.ok( @@ -129,7 +129,7 @@ test('unit tests under src/ and packages/*/src own no lane', () => { for (const file of [ 'src/daemon/selectors.test.ts', 'src/__tests__/contracts/interaction-guarantees.test.ts', - 'packages/platform-apple/src/__tests__/apps.test.ts', + 'packages/platform-apple/src/core/__tests__/apps.test.ts', 'packages/platform-apple/src/runtime.test.ts', ]) { assert.ok(isUnitTest(file), file); diff --git a/scripts/check-affected/model.test.ts b/scripts/check-affected/model.test.ts index dedf1d66b6..7bdf3bdc71 100644 --- a/scripts/check-affected/model.test.ts +++ b/scripts/check-affected/model.test.ts @@ -40,7 +40,7 @@ test('production source selects static/build gates and delegates tests to Vitest }); test('platform source additionally selects provider-integration', () => { - const result = ids(['packages/platform-apple/src/core/apps.ts']); + const result = ids(['packages/platform-apple/src/core/app-resolution.ts']); assert.ok(result.includes('provider-integration')); assert.ok(result.includes('coverage')); assert.ok(result.includes('vitest-related')); diff --git a/scripts/clean-daemon.ts b/scripts/clean-daemon.ts index d214197c6a..ff534eab37 100644 --- a/scripts/clean-daemon.ts +++ b/scripts/clean-daemon.ts @@ -27,7 +27,8 @@ if (daemonPid !== null) { killTimeoutMs: DAEMON_KILL_TIMEOUT_MS, expectedStartTime: info?.processStartTime, }); - const { cleanupRunnerLeasesForOwner } = await import('@agent-device/platform-apple'); + const { cleanupRunnerLeasesForOwner } = + await import('@agent-device/platform-apple/runner/operations'); await cleanupRunnerLeasesForOwner({ pid: daemonPid, startTime: info?.processStartTime }); } diff --git a/scripts/layering/check.ts b/scripts/layering/check.ts index 47c083f377..920918ccc6 100644 --- a/scripts/layering/check.ts +++ b/scripts/layering/check.ts @@ -82,7 +82,6 @@ import { } from './package-boundaries.ts'; import { checkPlatformPackagePolicy, - checkRetiredAppleFamilySources, checkPlatformsRootShape, platformPackagePolicySummary, } from './platform-package-policy.ts'; @@ -547,7 +546,6 @@ export const LAYERING_RULE_IDS = [ 'bin-alias-fast-path', 'package-boundaries', 'platform-package-policy', - 'platform-family-package-ownership', 'platforms-root-shape', ] as const; @@ -586,8 +584,6 @@ export const LAYERING_RULES: Readonly> = { readTrackedPlatformPackageDeclarations(repoRoot), { untrackedProductionFiles: listUntrackedProductionTypeScriptFiles(repoRoot) }, ), - 'platform-family-package-ownership': (context) => - checkRetiredAppleFamilySources([...context.allTypeScriptSources.keys()]), 'platforms-root-shape': (context) => checkPlatformsRootShape([...context.allTypeScriptSources.keys()]), }; diff --git a/scripts/layering/model.test.ts b/scripts/layering/model.test.ts index 091c30b132..662113b4b5 100644 --- a/scripts/layering/model.test.ts +++ b/scripts/layering/model.test.ts @@ -75,7 +75,7 @@ test('value cycles fail while type-only and dynamic cycles stay outside the grap test('back-edge identities follow the documented target spine', () => { const edges = resolveImportEdges( new Map([ - ['src/platforms/apple.ts', "import '../core/platform-plugin.ts';"], + ['src/contracts/result.ts', "import '../core/platform-plugin.ts';"], ['src/core/platform-plugin.ts', 'export const plugin = true;'], ['src/commands/help.ts', "import '../cli/parser.ts';"], ['src/cli/parser.ts', 'export const parser = true;'], @@ -85,7 +85,7 @@ test('back-edge identities follow the documented target spine', () => { const actual = collectBackEdges(edges); assert.deepEqual(actual, { 'commands -> cli': ['src/commands/help.ts -> src/cli/parser.ts'], - 'platforms -> core': ['src/platforms/apple.ts -> src/core/platform-plugin.ts'], + 'contracts -> core': ['src/contracts/result.ts -> src/core/platform-plugin.ts'], }); }); @@ -197,7 +197,8 @@ test('every production zone is deliberately classified as ranked or unranked', ( // deliberate ranked-vs-peripheral decision here instead of silently escaping // spine back-edge detection. If this fails, add the new zone to TARGET_DAG_RANK // (ranked spine) or UNRANKED_ZONES (root/peripheral) in model.ts. - assert.deepEqual(unclassifiedZones(listSourceFiles()), []); + const productionFiles = listSourceFiles(); + assert.deepEqual(unclassifiedZones(productionFiles), []); // The classification must also stay honest to the tree: every zone the model // names is a real production zone, so the docs cannot list a spine or peripheral diff --git a/scripts/layering/model.ts b/scripts/layering/model.ts index 7cbca94d79..b730fc8eaa 100644 --- a/scripts/layering/model.ts +++ b/scripts/layering/model.ts @@ -35,7 +35,6 @@ const TARGET_DAG_RANK = new Map([ ['ad-script', 1], ['contracts', 1], ['maestro', 1], - ['platforms', 1], ['recording', 1], ['replay', 1], ['replay-test', 1], diff --git a/scripts/layering/package-boundaries.test.ts b/scripts/layering/package-boundaries.test.ts index aea103b80e..cdbf40b402 100644 --- a/scripts/layering/package-boundaries.test.ts +++ b/scripts/layering/package-boundaries.test.ts @@ -510,7 +510,6 @@ test('the real tree parses, declares, and passes R11', () => { '@agent-device/host-kit/archive', '@agent-device/host-kit/command', '@agent-device/host-kit/diagnostics', - '@agent-device/host-kit/environment', '@agent-device/host-kit/file', '@agent-device/host-kit/host-file', '@agent-device/host-kit/process', @@ -529,14 +528,19 @@ test('the real tree parses, declares, and passes R11', () => { // list from the manifest side. assert.deepEqual([...platformApplePackage.exportTargets.keys()].sort(), [ '@agent-device/platform-apple', + '@agent-device/platform-apple/app-lifecycle', '@agent-device/platform-apple/app-resolution', + '@agent-device/platform-apple/debug-symbols', + '@agent-device/platform-apple/doctor', '@agent-device/platform-apple/install-artifact', '@agent-device/platform-apple/interactions', + '@agent-device/platform-apple/macos', '@agent-device/platform-apple/perf', '@agent-device/platform-apple/physical-device', '@agent-device/platform-apple/runner', '@agent-device/platform-apple/runner-owner', '@agent-device/platform-apple/runner/client', + '@agent-device/platform-apple/runner/operations', '@agent-device/platform-apple/runner/test-host', '@agent-device/platform-apple/simctl', '@agent-device/platform-apple/simulator', diff --git a/scripts/layering/platform-package-policy.test.ts b/scripts/layering/platform-package-policy.test.ts index c8b0d1ee80..eacfb123cf 100644 --- a/scripts/layering/platform-package-policy.test.ts +++ b/scripts/layering/platform-package-policy.test.ts @@ -27,14 +27,19 @@ function declarations(): PlatformPackageDeclaration[] { family === 'apple' ? [ '@agent-device/platform-apple', + '@agent-device/platform-apple/app-lifecycle', '@agent-device/platform-apple/app-resolution', + '@agent-device/platform-apple/debug-symbols', + '@agent-device/platform-apple/doctor', '@agent-device/platform-apple/interactions', '@agent-device/platform-apple/install-artifact', + '@agent-device/platform-apple/macos', '@agent-device/platform-apple/perf', '@agent-device/platform-apple/physical-device', '@agent-device/platform-apple/runner', '@agent-device/platform-apple/runner/client', '@agent-device/platform-apple/runner/test-host', + '@agent-device/platform-apple/runner/operations', '@agent-device/platform-apple/runner-owner', '@agent-device/platform-apple/simctl', '@agent-device/platform-apple/simulator', @@ -599,10 +604,7 @@ test('Node resolves only each platform package root facade', () => { test('the src/platforms root holds only the shared __tests__ directory', () => { const clean = ['src/platforms/__tests__/install-source.test.ts']; - assert.deepEqual( - checkPlatformsRootShape(clean), - [], - ); + assert.deepEqual(checkPlatformsRootShape(clean), []); }); test('a moved Android family cannot leave production or test files under the old root', () => { diff --git a/scripts/layering/platform-package-policy.ts b/scripts/layering/platform-package-policy.ts index 2d7b0f6e4b..869cf3710c 100644 --- a/scripts/layering/platform-package-policy.ts +++ b/scripts/layering/platform-package-policy.ts @@ -39,6 +39,7 @@ const PLATFORM_RUNTIME_HOST_FILES = new Set([ 'src/platform-runtime-host-diagnostics.ts', 'src/platform-runtime-managed-web-backend.ts', 'src/platform-runtime-network-web-transport.ts', + 'src/platform-runtime-operation-host.ts', 'src/platform-runtime-perf-host.ts', 'src/platform-runtime-resource-cleanup.ts', 'src/platform-runtime-screen-recording-harmony-host.ts', @@ -80,23 +81,25 @@ const APPLE_RUNNER_TEST_HOST_INSTALLER = 'scripts/vitest-apple-runner-host-setup const ANDROID_MECHANICS_FACADE = '@agent-device/platform-android/mechanics'; const ANDROID_HOST_FACET = '@agent-device/platform-android/adb-host'; const ANDROID_HOST_BINDING = 'src/platform-runtime-android-adb-host.ts'; -const APPLE_DOMAIN_FACADES = [ - '@agent-device/platform-apple/app-resolution', - '@agent-device/platform-apple/interactions', - '@agent-device/platform-apple/install-artifact', - '@agent-device/platform-apple/perf', - '@agent-device/platform-apple/physical-device', - '@agent-device/platform-apple/runner-owner', - '@agent-device/platform-apple/simctl', - '@agent-device/platform-apple/simulator', - '@agent-device/platform-apple/tool-provider', -] as const; const MECHANICS_FACET_SUBPATHS: Readonly>> = { apple: [ APPLE_RUNNER_FACADE, APPLE_RUNNER_CLIENT, APPLE_RUNNER_TEST_HOST, - ...APPLE_DOMAIN_FACADES, + '@agent-device/platform-apple/app-lifecycle', + '@agent-device/platform-apple/app-resolution', + '@agent-device/platform-apple/debug-symbols', + '@agent-device/platform-apple/doctor', + '@agent-device/platform-apple/interactions', + '@agent-device/platform-apple/install-artifact', + '@agent-device/platform-apple/macos', + '@agent-device/platform-apple/perf', + '@agent-device/platform-apple/physical-device', + '@agent-device/platform-apple/runner-owner', + '@agent-device/platform-apple/runner/operations', + '@agent-device/platform-apple/simctl', + '@agent-device/platform-apple/simulator', + '@agent-device/platform-apple/tool-provider', ], android: [ANDROID_HOST_FACET, ANDROID_MECHANICS_FACADE], }; @@ -111,71 +114,7 @@ function isAndroidMechanicsFacetImport(file: string, specifier: string): boolean } function isAppleFacadeSubpathImport(specifier: string): boolean { - return ( - specifier === APPLE_RUNNER_FACADE || - specifier === APPLE_RUNNER_CLIENT || - specifier === APPLE_RUNNER_TEST_HOST || - APPLE_DOMAIN_FACADES.includes(specifier as (typeof APPLE_DOMAIN_FACADES)[number]) - ); -} - -const APPLE_FACADE_CONSUMER_FILES = new Set([ - 'scripts/__tests__/help-conformance-sample-producers.ts', - 'scripts/clean-daemon.ts', - 'scripts/patch-xcuitest-runner-icon.ts', - 'scripts/vitest-apple-runner-host-setup.ts', - 'src/agent-device-client.ts', - 'src/core/__tests__/device-selection-resolver.test.ts', - 'src/core/__tests__/dispatch-resolve.test.ts', - 'src/core/device-selection-resolver.ts', - 'src/core/interactors/register-builtins.ts', - 'src/daemon/ios-app-session-hint.test.ts', - 'src/platform-runtime-apple-application-tools.ts', - 'src/platform-runtime-apple-automation-keep-hot.ts', - 'src/platform-runtime-apple-deployment-executor.test.ts', - 'src/platform-runtime-apple-deployment-executor.ts', - 'src/platform-runtime-apple-physical-readiness.ts', - 'src/platform-runtime-apple-resources.ts', - 'src/platform-runtime-apple-runner-owner.ts', - 'src/platform-runtime-app-inventory-host.ts', - 'src/platform-runtime-apple-tool-host.test.ts', - 'src/platform-runtime-apple-tool-host.ts', - 'src/platform-runtime-audio-probe-host.ts', - 'src/platform-runtime-daemon-owner-cleanup.test.ts', - 'src/platform-runtime-daemon-owner-cleanup.ts', - 'src/platform-runtime-device-ready.ts', - 'src/platform-runtime-host-diagnostics.ts', - 'src/platform-runtime-host.test.ts', - 'src/platform-runtime-host.ts', - 'src/platform-runtime-open-target.ts', - 'src/platform-runtime-perf-capture-host.ts', - 'src/platform-runtime-perf-host.ts', - 'src/platform-runtime-resource-cleanup.ts', - 'src/platform-runtime-runtime-hints.ts', - 'src/platform-runtime-screen-recording-apple-runner-host.test.ts', - 'src/platform-runtime-screen-recording-apple-runner-host.ts', - 'src/platform-runtime-screen-recording-apple-runner-transport.test.ts', - 'src/platform-runtime-screen-recording-apple-runner-transport.ts', - 'src/platform-runtime-screen-recording-apple-transport.ts', - 'src/platforms/__tests__/install-source.test.ts', - 'src/platform-runtime.ts', - 'src/platform-runtime/request-providers.ts', - 'src/sdk/limrun-runtime-dependencies.test.ts', - 'src/sdk/limrun-runtime-dependencies.ts', - 'src/snapshot/snapshot-desktop-surface.test.ts', - 'src/snapshot/snapshot-desktop-surface.ts', - 'src/utils/__tests__/interactors.test.ts', - 'test/integration/provider-scenarios/ios-alert-settings.test.ts', - 'test/integration/provider-scenarios/provider-ios-runner-transport.test.ts', - 'test/integration/provider-scenarios/providers.ts', -]); - -function isAppleFacadeConsumer(file: string): boolean { - return ( - APPLE_FACADE_CONSUMER_FILES.has(file) || - file.startsWith('src/daemon/__tests__/') || - file.startsWith('src/daemon/handlers/__tests__/') - ); + return MECHANICS_FACET_SUBPATHS.apple?.includes(specifier) ?? false; } function violation(file: string, line: number, message: string): LayeringViolation { @@ -352,14 +291,13 @@ function checkSource(file: string, source: string): LayeringViolation[] { } else if ( importedFamily && !COMPOSITION_FILES.has(file) && - // Named Apple facades are the package's consumer seams. R11's + // Named platform facades are the package's consumer seams. R11's // workspace-dependency declarations bound the importer set to the root // package. !isAppleFacadeSubpathImport(site.spec) && !isAllowedPlatformRootImport(file, site, importedFamily) && !isPackageOwnedFacadeTest(file, importedFamily, site.spec) && - !isAndroidMechanicsFacetImport(file, site.spec) && - !(site.spec === '@agent-device/platform-apple' && isAppleFacadeConsumer(file)) + !isAndroidMechanicsFacetImport(file, site.spec) ) { violations.push( violation( diff --git a/scripts/patch-xcuitest-runner-icon.ts b/scripts/patch-xcuitest-runner-icon.ts index df51a0c154..cef4ef653b 100644 --- a/scripts/patch-xcuitest-runner-icon.ts +++ b/scripts/patch-xcuitest-runner-icon.ts @@ -5,5 +5,6 @@ if (!derivedPath) { process.exit(1); } -const { applyXctestRunnerAppIconFromDerivedPath } = await import('@agent-device/platform-apple'); +const { applyXctestRunnerAppIconFromDerivedPath } = + await import('@agent-device/platform-apple/runner/operations'); await applyXctestRunnerAppIconFromDerivedPath(derivedPath); diff --git a/scripts/vitest-apple-runner-host-setup.ts b/scripts/vitest-apple-runner-host-setup.ts index 79463687d8..679753e171 100644 --- a/scripts/vitest-apple-runner-host-setup.ts +++ b/scripts/vitest-apple-runner-host-setup.ts @@ -2,8 +2,8 @@ import { beforeEach } from 'vitest'; import { appleRunnerTestHost, installAppleRunnerTestHost, + loadAppleRunnerHost, } from '@agent-device/platform-apple/runner/test-host'; -import { loadAppleRunnerHost } from '@agent-device/platform-apple'; // The apple-runner vitest project runs the package's own suites, which cannot // import root utilities directly (R11). This setup installs the real root host diff --git a/src/__tests__/eager-closure-budgets.ts b/src/__tests__/eager-closure-budgets.ts index b5ddd0eee1..c5e1e414bb 100644 --- a/src/__tests__/eager-closure-budgets.ts +++ b/src/__tests__/eager-closure-budgets.ts @@ -298,11 +298,16 @@ export const FACADE_BUDGETS: Readonly> = Object.freeze({ // --- @agent-device/platform-apple --- 'packages/platform-apple/src/index.ts': 1, - 'packages/platform-apple/src/app-resolution-facade.ts': 55, + 'packages/platform-apple/src/app-lifecycle-facade.ts': 120, + 'packages/platform-apple/src/app-resolution-facade.ts': 61, + 'packages/platform-apple/src/debug-symbols-facade.ts': 24, + 'packages/platform-apple/src/doctor-facade.ts': 101, 'packages/platform-apple/src/interactions-facade.ts': 117, - 'packages/platform-apple/src/install-artifact-facade.ts': 20, - 'packages/platform-apple/src/perf-facade.ts': 52, - 'packages/platform-apple/src/physical-device-facade.ts': 41, + 'packages/platform-apple/src/install-artifact-facade.ts': 41, + 'packages/platform-apple/src/macos-facade.ts': 25, + 'packages/platform-apple/src/perf-facade.ts': 60, + 'packages/platform-apple/src/physical-device-facade.ts': 47, + 'packages/platform-apple/src/runner-operations-facade.ts': 100, 'packages/platform-apple/src/runner-owner-facade.ts': 2, 'packages/platform-apple/src/simctl-facade.ts': 18, 'packages/platform-apple/src/simulator-facade.ts': 26, @@ -410,11 +415,16 @@ const PLATFORM_MECHANICS_ENTRY_PREFIXES = [ ] as const; const APPLE_DOMAIN_MECHANICS_ENTRY_FILES: ReadonlySet = new Set([ + 'packages/platform-apple/src/app-lifecycle-facade.ts', 'packages/platform-apple/src/app-resolution-facade.ts', + 'packages/platform-apple/src/debug-symbols-facade.ts', + 'packages/platform-apple/src/doctor-facade.ts', 'packages/platform-apple/src/interactions-facade.ts', 'packages/platform-apple/src/install-artifact-facade.ts', + 'packages/platform-apple/src/macos-facade.ts', 'packages/platform-apple/src/perf-facade.ts', 'packages/platform-apple/src/physical-device-facade.ts', + 'packages/platform-apple/src/runner-operations-facade.ts', 'packages/platform-apple/src/runner-owner-facade.ts', 'packages/platform-apple/src/simctl-facade.ts', 'packages/platform-apple/src/simulator-facade.ts', @@ -434,7 +444,7 @@ function toRows( kind === 'facade' && !PLATFORM_MECHANICS_ENTRY_PREFIXES.some((prefix) => prefix.endsWith('/') ? entryFile.startsWith(prefix) : entryFile === prefix, - ), + ) && !APPLE_DOMAIN_MECHANICS_ENTRY_FILES.has(entryFile), })); } diff --git a/src/agent-device-client.ts b/src/agent-device-client.ts index 9778733498..52fe665730 100644 --- a/src/agent-device-client.ts +++ b/src/agent-device-client.ts @@ -440,7 +440,8 @@ export function createAgentDeviceClient( }, debug: { symbols: async (options) => { - const { symbolicateCrashArtifact } = await import('@agent-device/platform-apple'); + const { symbolicateCrashArtifact } = + await import('@agent-device/platform-apple/debug-symbols'); return symbolicateCrashArtifact({ cwd: options.cwd ?? config.cwd, ...options }); }, }, diff --git a/src/core/__tests__/device-selection-resolver.test.ts b/src/core/__tests__/device-selection-resolver.test.ts index eb9ddba000..210c2dd69e 100644 --- a/src/core/__tests__/device-selection-resolver.test.ts +++ b/src/core/__tests__/device-selection-resolver.test.ts @@ -18,7 +18,7 @@ import { const mockFindIosSimulatorInstalledApp = vi.hoisted(() => vi.fn()); -vi.mock('@agent-device/platform-apple', () => ({ +vi.mock('@agent-device/platform-apple/app-resolution', () => ({ findIosSimulatorInstalledApp: mockFindIosSimulatorInstalledApp, })); diff --git a/src/core/__tests__/dispatch-resolve.test.ts b/src/core/__tests__/dispatch-resolve.test.ts index 4050f2c81e..0e8669fe91 100644 --- a/src/core/__tests__/dispatch-resolve.test.ts +++ b/src/core/__tests__/dispatch-resolve.test.ts @@ -6,7 +6,7 @@ const { mockFindIosSimulatorInstalledApp, mockListAppleDevices } = vi.hoisted(() mockListAppleDevices: vi.fn(), })); -vi.mock('@agent-device/platform-apple', () => { +vi.mock('@agent-device/platform-apple/app-resolution', () => { return { findIosSimulatorInstalledApp: mockFindIosSimulatorInstalledApp, }; diff --git a/src/core/device-selection-resolver.ts b/src/core/device-selection-resolver.ts index 8165d81e9c..ed7e878e99 100644 --- a/src/core/device-selection-resolver.ts +++ b/src/core/device-selection-resolver.ts @@ -166,7 +166,8 @@ async function resolveAppInstalledSimulatorSelection( ); if (bootedSimulators.length < 2) return undefined; - const { findIosSimulatorInstalledApp } = await import('@agent-device/platform-apple'); + const { findIosSimulatorInstalledApp } = + await import('@agent-device/platform-apple/app-resolution'); const matches = ( await Promise.all( bootedSimulators.map(async (device) => diff --git a/src/daemon/__tests__/device-ready.test.ts b/src/daemon/__tests__/device-ready.test.ts index d6b749b698..a85abf427f 100644 --- a/src/daemon/__tests__/device-ready.test.ts +++ b/src/daemon/__tests__/device-ready.test.ts @@ -11,8 +11,8 @@ vi.mock('@agent-device/host-kit/command', async (importOriginal) => { whichCmd: vi.fn(async () => true), }; }); -vi.mock('@agent-device/platform-apple', async (importOriginal) => ({ - ...(await importOriginal()), +vi.mock('@agent-device/platform-apple/simulator', async (importOriginal) => ({ + ...(await importOriginal()), ensureBootedSimulator: vi.fn(async () => {}), })); vi.mock('@agent-device/platform-android/mechanics', () => ({ @@ -21,7 +21,7 @@ vi.mock('@agent-device/platform-android/mechanics', () => ({ import { runCmd } from '@agent-device/host-kit/command'; import { waitForAndroidBoot } from '@agent-device/platform-android/mechanics'; -import { ensureBootedSimulator } from '@agent-device/platform-apple'; +import { ensureBootedSimulator } from '@agent-device/platform-apple/simulator'; import { ANDROID_EMULATOR, IOS_DEVICE, diff --git a/src/daemon/__tests__/request-recording-health.test.ts b/src/daemon/__tests__/request-recording-health.test.ts index e7c82d6663..ce7c9c3096 100644 --- a/src/daemon/__tests__/request-recording-health.test.ts +++ b/src/daemon/__tests__/request-recording-health.test.ts @@ -2,11 +2,11 @@ import { test, expect, vi, beforeEach } from 'vitest'; import type { SessionState } from '../types.ts'; import { makeTestScreenRecordingResource } from '../../__tests__/test-utils/screen-recording-live-handle.ts'; -vi.mock('@agent-device/platform-apple', () => ({ +vi.mock('@agent-device/platform-apple/runner/operations', () => ({ getRunnerSessionSnapshot: vi.fn(), })); -import { getRunnerSessionSnapshot } from '@agent-device/platform-apple'; +import { getRunnerSessionSnapshot } from '@agent-device/platform-apple/runner/operations'; import { refreshRecordingHealth } from '../request-recording-health.ts'; const mockGetRunnerSessionSnapshot = vi.mocked(getRunnerSessionSnapshot); diff --git a/src/daemon/__tests__/request-router-open.test.ts b/src/daemon/__tests__/request-router-open.test.ts index 521fd6e212..eb196ba2bb 100644 --- a/src/daemon/__tests__/request-router-open.test.ts +++ b/src/daemon/__tests__/request-router-open.test.ts @@ -13,11 +13,37 @@ vi.mock('@agent-device/host-kit/process', async (importOriginal) => { const actual = await importOriginal(); return { ...actual, readProcessStartTime: vi.fn(() => 'test-process-start') }; }); -vi.mock('@agent-device/platform-apple', async (importOriginal) => { - const actual = await importOriginal(); +vi.mock('@agent-device/platform-apple/runner/operations', async (importOriginal) => { + const actual = + await importOriginal(); return { ...actual, + detachIosSimulatorRunnerSessionsForShutdown: vi.fn(async () => {}), + notifyIosRunnerAppRelaunched: vi.fn(async () => {}), + prewarmAppleRunnerCache: vi.fn(async () => {}), + prewarmIosRunnerSession: vi.fn(async () => {}), + prepareIosRunner: vi.fn(async () => ({ + runner: { currentUptimeMs: 42 }, + connectMs: 0, + healthCheckMs: 0, + })), + resolveRunnerAppBundleId: vi.fn(() => 'com.callstack.agentdevice.runner'), + scheduleIosRunnerIdleStop: vi.fn(), stopIosRunnerSession: vi.fn(async () => {}), + stopAllIosRunnerSessions: vi.fn(async () => {}), + }; +}); +vi.mock('@agent-device/platform-apple/app-lifecycle', async (importOriginal) => { + const actual = + await importOriginal(); + return { ...actual, closeIosApp: vi.fn(async () => {}) }; +}); +vi.mock('@agent-device/platform-apple/app-resolution', async (importOriginal) => { + const actual = + await importOriginal(); + return { + ...actual, + resolveIosApp: vi.fn(async (_device, app) => app), }; }); diff --git a/src/daemon/__tests__/request-router-replay-scope.test.ts b/src/daemon/__tests__/request-router-replay-scope.test.ts index bf0f34bb6f..c835c73e93 100644 --- a/src/daemon/__tests__/request-router-replay-scope.test.ts +++ b/src/daemon/__tests__/request-router-replay-scope.test.ts @@ -14,8 +14,15 @@ vi.mock('../../platform-runtime-runtime-hints.ts', async (importOriginal) => { return { ...actual, applyRuntimeHintValues: vi.fn(async () => {}) }; }); -vi.mock('@agent-device/platform-apple', async (importOriginal) => { - const actual = await importOriginal(); +vi.mock('@agent-device/platform-apple/runner/operations', async (importOriginal) => { + const actual = + await importOriginal(); + return { ...actual, prewarmIosRunnerSession: vi.fn() }; +}); + +vi.mock('@agent-device/platform-apple/app-resolution', async (importOriginal) => { + const actual = + await importOriginal(); return { ...actual, resolveIosApp: vi.fn(async () => 'com.example.app'), diff --git a/src/daemon/handlers/__tests__/session-close-shutdown.fixtures.ts b/src/daemon/handlers/__tests__/session-close-shutdown.fixtures.ts index 5458460114..e347297b25 100644 --- a/src/daemon/handlers/__tests__/session-close-shutdown.fixtures.ts +++ b/src/daemon/handlers/__tests__/session-close-shutdown.fixtures.ts @@ -7,13 +7,32 @@ import type { DaemonRequest, DaemonResponse, SessionState } from '../../types.ts import { AppError } from '@agent-device/kernel/errors'; import { mkdtempForTestSync } from '../../../__tests__/test-utils/tmp-dir.ts'; -vi.mock('@agent-device/platform-apple', async (importOriginal) => { - const actual = await importOriginal(); +vi.mock('@agent-device/platform-apple/runner/operations', async (importOriginal) => { + const actual = + await importOriginal(); return { ...actual, - shutdownSimulator: vi.fn(), stopIosRunnerSession: vi.fn(async () => {}), + }; +}); +vi.mock('@agent-device/platform-apple/simulator', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + shutdownSimulator: vi.fn(), + }; +}); +vi.mock('@agent-device/platform-apple/perf', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, cleanupAppleXctracePerfCapture: vi.fn(async () => ({})), + }; +}); +vi.mock('@agent-device/platform-apple/macos', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, runMacOsAlertAction: vi.fn(async () => {}), }; }); @@ -51,11 +70,9 @@ import { cleanupAndroidNativePerfSession, stopAndroidSnapshotHelperSessionForDevice, } from '@agent-device/platform-android/mechanics'; -import { - cleanupAppleXctracePerfCapture, - shutdownSimulator, - stopIosRunnerSession, -} from '@agent-device/platform-apple'; +import { stopIosRunnerSession } from '@agent-device/platform-apple/runner/operations'; +import { shutdownSimulator } from '@agent-device/platform-apple/simulator'; +import { cleanupAppleXctracePerfCapture } from '@agent-device/platform-apple/perf'; import { WEB_DESKTOP_DEVICE } from '../../../__tests__/test-utils/device-fixtures.ts'; import { acquireDeviceClaim } from '../../device-claims.ts'; import { inspectDeviceClaims } from '../../device-claim-inspection.ts'; diff --git a/src/daemon/handlers/__tests__/session-device-utils.test.ts b/src/daemon/handlers/__tests__/session-device-utils.test.ts index ffd4452f8a..bddd8cf905 100644 --- a/src/daemon/handlers/__tests__/session-device-utils.test.ts +++ b/src/daemon/handlers/__tests__/session-device-utils.test.ts @@ -6,11 +6,11 @@ import { refreshSessionDeviceIfNeeded, selectorTargetsSessionDevice, } from '../session-device-utils.ts'; -import { getRunnerSessionSnapshot } from '@agent-device/platform-apple'; +import { getRunnerSessionSnapshot } from '@agent-device/platform-apple/runner/operations'; import { resolveTargetDevice } from '../../../core/dispatch-resolve.ts'; import { isActiveProviderDevice } from '../../../provider-device-runtime.ts'; -vi.mock('@agent-device/platform-apple', () => ({ +vi.mock('@agent-device/platform-apple/runner/operations', () => ({ getRunnerSessionSnapshot: vi.fn(async () => null), })); vi.mock('../../../core/dispatch-resolve.ts', () => ({ diff --git a/src/daemon/handlers/__tests__/session-doctor-warmup.test.ts b/src/daemon/handlers/__tests__/session-doctor-warmup.test.ts index 2936a33c8e..89fda2156e 100644 --- a/src/daemon/handlers/__tests__/session-doctor-warmup.test.ts +++ b/src/daemon/handlers/__tests__/session-doctor-warmup.test.ts @@ -10,8 +10,8 @@ const { mockAppleRunnerWarmupCheck } = vi.hoisted(() => ({ mockAppleRunnerWarmupCheck: vi.fn(), })); -vi.mock('@agent-device/platform-apple', async (importOriginal) => ({ - ...(await importOriginal()), +vi.mock('@agent-device/platform-apple/doctor', async (importOriginal) => ({ + ...(await importOriginal()), appleRunnerWarmupCheck: mockAppleRunnerWarmupCheck, })); vi.mock('../session-doctor-device.ts', async (importOriginal) => { diff --git a/src/daemon/handlers/__tests__/session-replay-repair-record-exclusion.test.ts b/src/daemon/handlers/__tests__/session-replay-repair-record-exclusion.test.ts index 07ce132fd9..f089bb4990 100644 --- a/src/daemon/handlers/__tests__/session-replay-repair-record-exclusion.test.ts +++ b/src/daemon/handlers/__tests__/session-replay-repair-record-exclusion.test.ts @@ -20,15 +20,15 @@ import { test, expect, vi, beforeEach } from 'vitest'; import { mkdtempForTestSync } from '../../../__tests__/test-utils/tmp-dir.ts'; -vi.mock('@agent-device/platform-apple', async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - shutdownSimulator: vi.fn(), - stopIosRunnerSession: vi.fn(async () => {}), - cleanupAppleXctracePerfCapture: vi.fn(async () => ({})), - }; -}); +vi.mock('@agent-device/platform-apple/runner/operations', () => ({ + resolveRunnerAppBundleId: vi.fn(), + scheduleIosRunnerIdleStop: vi.fn(), + stopIosRunnerSession: vi.fn(), +})); +vi.mock('@agent-device/platform-apple/simulator', () => ({ shutdownSimulator: vi.fn() })); +vi.mock('@agent-device/platform-apple/perf', () => ({ + cleanupAppleXctracePerfCapture: vi.fn(async () => ({})), +})); vi.mock('@agent-device/host-kit/command', async (importOriginal) => { const actual = await importOriginal(); return { ...actual, runCmd: vi.fn() }; diff --git a/src/daemon/handlers/__tests__/session-replay-repair-transaction.test.ts b/src/daemon/handlers/__tests__/session-replay-repair-transaction.test.ts index fbd75a1a49..a18fdf9827 100644 --- a/src/daemon/handlers/__tests__/session-replay-repair-transaction.test.ts +++ b/src/daemon/handlers/__tests__/session-replay-repair-transaction.test.ts @@ -20,15 +20,15 @@ import { test, expect, vi, beforeEach } from 'vitest'; import { mkdtempForTestSync } from '../../../__tests__/test-utils/tmp-dir.ts'; -vi.mock('@agent-device/platform-apple', async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - prewarmIosRunnerSession: vi.fn(async () => {}), - stopIosRunnerSession: vi.fn(async () => {}), - cleanupAppleXctracePerfCapture: vi.fn(async () => ({})), - }; -}); +vi.mock('@agent-device/platform-apple/runner/operations', () => ({ + prewarmIosRunnerSession: vi.fn(), + resolveRunnerAppBundleId: vi.fn(), + scheduleIosRunnerIdleStop: vi.fn(), + stopIosRunnerSession: vi.fn(), +})); +vi.mock('@agent-device/platform-apple/perf', () => ({ + cleanupAppleXctracePerfCapture: vi.fn(async () => ({})), +})); vi.mock('@agent-device/host-kit/command', async (importOriginal) => { const actual = await importOriginal(); return { ...actual, runCmd: vi.fn() }; diff --git a/src/daemon/handlers/__tests__/session-test-harness.ts b/src/daemon/handlers/__tests__/session-test-harness.ts index 06394c8e29..c0c1b0b70e 100644 --- a/src/daemon/handlers/__tests__/session-test-harness.ts +++ b/src/daemon/handlers/__tests__/session-test-harness.ts @@ -34,8 +34,9 @@ vi.mock('../../../platform-runtime-runtime-hints.ts', async (importOriginal) => clearRuntimeHintValues: vi.fn(async () => {}), }; }); -vi.mock('@agent-device/platform-apple', async (importOriginal) => { - const actual = await importOriginal(); +vi.mock('@agent-device/platform-apple/runner/operations', async (importOriginal) => { + const actual = + await importOriginal(); return { ...actual, prepareIosRunner: vi.fn(async () => ({ @@ -48,7 +49,20 @@ vi.mock('@agent-device/platform-apple', async (importOriginal) => { notifyIosRunnerAppRelaunched: vi.fn(async () => {}), scheduleIosRunnerIdleStop: vi.fn(), stopIosRunnerSession: vi.fn(async () => {}), + }; +}); +vi.mock('@agent-device/platform-apple/macos', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, runMacOsAlertAction: vi.fn(async () => {}), + }; +}); +vi.mock('@agent-device/platform-apple/app-resolution', async (importOriginal) => { + const actual = + await importOriginal(); + return { + ...actual, resolveIosApp: vi.fn(async () => undefined), resolveIosSimulatorDeepLinkBundleId: vi.fn(async () => undefined), }; @@ -89,12 +103,14 @@ import { prewarmAppleRunnerCache, prewarmIosRunnerSession, notifyIosRunnerAppRelaunched, - runMacOsAlertAction, - resolveIosApp, - resolveIosSimulatorDeepLinkBundleId, scheduleIosRunnerIdleStop, stopIosRunnerSession, -} from '@agent-device/platform-apple'; +} from '@agent-device/platform-apple/runner/operations'; +import { runMacOsAlertAction } from '@agent-device/platform-apple/macos'; +import { + resolveIosApp, + resolveIosSimulatorDeepLinkBundleId, +} from '@agent-device/platform-apple/app-resolution'; import { resolveAndroidPackageForOpen } from '../../../platform-runtime-open-target.ts'; import { runCmd } from '@agent-device/host-kit/command'; import { dispatchApplicationLifecycleEffect } from '../../__tests__/application-lifecycle-runtime-fixture.ts'; diff --git a/src/daemon/handlers/__tests__/snapshot-session-cleanup.test.ts b/src/daemon/handlers/__tests__/snapshot-session-cleanup.test.ts index 83c14def90..b9b34ef313 100644 --- a/src/daemon/handlers/__tests__/snapshot-session-cleanup.test.ts +++ b/src/daemon/handlers/__tests__/snapshot-session-cleanup.test.ts @@ -1,14 +1,18 @@ import { beforeEach, expect, test, vi } from 'vitest'; -vi.mock('@agent-device/platform-apple', async (importOriginal) => ({ - ...(await importOriginal()), +vi.mock('@agent-device/platform-apple/runner/operations', async (importOriginal) => ({ + ...(await importOriginal()), stopIosRunnerSession: vi.fn(async () => {}), +})); +vi.mock('@agent-device/platform-apple/app-lifecycle', async (importOriginal) => ({ + ...(await importOriginal()), closeIosApp: vi.fn(async () => {}), })); import { withSessionlessRunnerCleanup } from '../snapshot-session.ts'; import { platformResourceCleanup } from '../../../platform-runtime-resource-cleanup.ts'; -import { closeIosApp, stopIosRunnerSession } from '@agent-device/platform-apple'; +import { closeIosApp } from '@agent-device/platform-apple/app-lifecycle'; +import { stopIosRunnerSession } from '@agent-device/platform-apple/runner/operations'; import { IOS_SIMULATOR } from '../../../__tests__/test-utils/device-fixtures.ts'; const mockStopIosRunnerSession = vi.mocked(stopIosRunnerSession); diff --git a/src/daemon/ios-app-session-hint.test.ts b/src/daemon/ios-app-session-hint.test.ts index 650a0fa8bb..ad78b0f2d7 100644 --- a/src/daemon/ios-app-session-hint.test.ts +++ b/src/daemon/ios-app-session-hint.test.ts @@ -8,7 +8,9 @@ vi.mock('../request/device-inventory-context.ts', async (importOriginal) => ({ ...(await importOriginal()), listLocalDeviceInventory: listBootedIosSimulators, })); -vi.mock('@agent-device/platform-apple', () => ({ detectSoleRunningIosSimulatorApp })); +vi.mock('@agent-device/platform-apple/app-resolution', () => ({ + detectSoleRunningIosSimulatorApp, +})); import { IOS_DEVICE, IOS_SIMULATOR } from '../__tests__/test-utils/device-fixtures.ts'; import { buildIosOpenCommandHint, resolveSoleForegroundIosApp } from './ios-app-session-hint.ts'; diff --git a/src/platform-runtime-app-inventory-host.ts b/src/platform-runtime-app-inventory-host.ts index 45acf8726c..b9921051ff 100644 --- a/src/platform-runtime-app-inventory-host.ts +++ b/src/platform-runtime-app-inventory-host.ts @@ -10,7 +10,7 @@ export function createAppInventoryRuntimeHost(): AppInventoryRuntimeHost { return Object.freeze({ apple: Object.freeze({ listApps: async (device: DeviceInfo, filter: AppsFilter) => { - const { listIosApps } = await import('@agent-device/platform-apple'); + const { listIosApps } = await import('@agent-device/platform-apple/app-resolution'); return mapAppleApps(await listIosApps(device, filter)); }, }), diff --git a/src/platform-runtime-apple-application-tools.ts b/src/platform-runtime-apple-application-tools.ts index 4aa7fed59d..ee189dd4b2 100644 --- a/src/platform-runtime-apple-application-tools.ts +++ b/src/platform-runtime-apple-application-tools.ts @@ -13,32 +13,36 @@ export function createAppleApplicationTools(): AppleApplicationTools { return Object.freeze({ resolveOpenTarget: async (device, input) => await resolveAppleOpenTarget(device, input), prewarmRunnerCache: async (device, execution, signal) => { - const { prewarmAppleRunnerCache } = await import('@agent-device/platform-apple'); + const { prewarmAppleRunnerCache } = + await import('@agent-device/platform-apple/runner/operations'); await prewarmAppleRunnerCache(device, appleRunnerOptions(execution, signal)); }, prewarmRunnerSession: async (device, execution, signal, propagateError) => { - const { prewarmIosRunnerSession } = await import('@agent-device/platform-apple'); + const { prewarmIosRunnerSession } = + await import('@agent-device/platform-apple/runner/operations'); await prewarmIosRunnerSession(device, { ...appleRunnerOptions(execution, signal), propagateError, }); }, notifyRunnerAppRelaunched: async (device, execution, signal) => { - const { notifyIosRunnerAppRelaunched } = await import('@agent-device/platform-apple'); + const { notifyIosRunnerAppRelaunched } = + await import('@agent-device/platform-apple/runner/operations'); await notifyIosRunnerAppRelaunched(device, appleRunnerOptions(execution, signal)); }, stopRunnerSession: async (deviceId) => { - const { stopIosRunnerSession } = await import('@agent-device/platform-apple'); + const { stopIosRunnerSession } = + await import('@agent-device/platform-apple/runner/operations'); await stopIosRunnerSession(deviceId); }, scheduleRunnerIdleStop: (deviceId) => { - void import('@agent-device/platform-apple').then(({ scheduleIosRunnerIdleStop }) => - scheduleIosRunnerIdleStop(deviceId), + void import('@agent-device/platform-apple/runner/operations').then( + ({ scheduleIosRunnerIdleStop }) => scheduleIosRunnerIdleStop(deviceId), ); }, prepareRunner: async (device, input, signal) => { const { Deadline } = await import('@agent-device/host-kit/retry'); - const { prepareIosRunner } = await import('@agent-device/platform-apple'); + const { prepareIosRunner } = await import('@agent-device/platform-apple/runner/operations'); const startedAtMs = Date.now(); return await prepareIosRunner(device, { ...appleRunnerOptions(input.execution, signal), @@ -60,11 +64,12 @@ export function createAppleApplicationTools(): AppleApplicationTools { dismissCloseAlerts: async (device, input) => await dismissMacOsCloseAlerts(device, input), detachRunnerSessionsForShutdown: async () => { const { detachIosSimulatorRunnerSessionsForShutdown } = - await import('@agent-device/platform-apple'); + await import('@agent-device/platform-apple/runner/operations'); await detachIosSimulatorRunnerSessionsForShutdown(); }, finalizeRunnerSessionsForShutdown: async () => { - const { stopAllIosRunnerSessions } = await import('@agent-device/platform-apple'); + const { stopAllIosRunnerSessions } = + await import('@agent-device/platform-apple/runner/operations'); await stopAllIosRunnerSessions(); }, }); @@ -105,7 +110,8 @@ async function resolveAppleForegroundTarget( device: DeviceInfo, ): Promise { if (!isIosFamily(device) || device.kind !== 'simulator') return undefined; - const { detectSoleRunningIosSimulatorApp } = await import('@agent-device/platform-apple'); + const { detectSoleRunningIosSimulatorApp } = + await import('@agent-device/platform-apple/app-resolution'); const app = await detectSoleRunningIosSimulatorApp(device); return app ? { appBundleId: app.bundleId, appName: app.name } : undefined; } @@ -117,7 +123,7 @@ async function resolveMacOsSurface( if (!isMacOs(device) || surface === 'app' || surface === 'desktop' || surface === 'menubar') { return {}; } - const { resolveFrontmostMacOsApp } = await import('@agent-device/platform-apple'); + const { resolveFrontmostMacOsApp } = await import('@agent-device/platform-apple/macos'); const frontmost = await resolveFrontmostMacOsApp(); return { appBundleId: frontmost.bundleId, appName: frontmost.appName }; } @@ -141,7 +147,7 @@ async function dismissMacOsCloseAlerts( input: CloseApplicationFinalizationInput, ): Promise { if (!isMacOs(device)) return; - const { runMacOsAlertAction } = await import('@agent-device/platform-apple'); + const { runMacOsAlertAction } = await import('@agent-device/platform-apple/macos'); const dismissOptions = input.surface === 'frontmost-app' ? { surface: 'frontmost-app' as const } diff --git a/src/platform-runtime-apple-automation-keep-hot.ts b/src/platform-runtime-apple-automation-keep-hot.ts index f11f0bb998..2257c38aa9 100644 --- a/src/platform-runtime-apple-automation-keep-hot.ts +++ b/src/platform-runtime-apple-automation-keep-hot.ts @@ -3,7 +3,7 @@ import type { DeviceReadinessRuntimeHost } from '@agent-device/contracts/device- export function createAppleAutomationKeepHotHost(): DeviceReadinessRuntimeHost['appleAutomation'] { return Object.freeze({ keepHot: (device) => { - void import('@agent-device/platform-apple') + void import('@agent-device/platform-apple/runner/operations') .then(({ prewarmAppleRunnerCache }) => prewarmAppleRunnerCache(device, {})) .catch(() => {}); }, diff --git a/src/platform-runtime-apple-deployment-executor.test.ts b/src/platform-runtime-apple-deployment-executor.test.ts index ea1c3417ba..5a0ddf3b29 100644 --- a/src/platform-runtime-apple-deployment-executor.test.ts +++ b/src/platform-runtime-apple-deployment-executor.test.ts @@ -6,7 +6,7 @@ const { invalidateIosAppResolutionCache, resolveIosApp } = vi.hoisted(() => ({ resolveIosApp: vi.fn(), })); -vi.mock('@agent-device/platform-apple', () => ({ +vi.mock('@agent-device/platform-apple/app-resolution', () => ({ invalidateIosAppResolutionCache, resolveIosApp, })); diff --git a/src/platform-runtime-apple-deployment-executor.ts b/src/platform-runtime-apple-deployment-executor.ts index a77fc99127..94d962aeb5 100644 --- a/src/platform-runtime-apple-deployment-executor.ts +++ b/src/platform-runtime-apple-deployment-executor.ts @@ -10,15 +10,17 @@ import type { export function createAppleAppDeploymentExecutor(): AppleAppDeploymentExecutor { return Object.freeze({ withInvalidatedAppResolutionCache: async (device, operation) => { - const { invalidateIosAppResolutionCache } = await import('@agent-device/platform-apple'); + const { invalidateIosAppResolutionCache } = + await import('@agent-device/platform-apple/app-resolution'); return await invalidateIosAppResolutionCache(device, operation); }, prepareArtifact: async (input: MaterializeAppSourceInput, options) => { - const { prepareIosInstallArtifact } = await import('@agent-device/platform-apple'); + const { prepareIosInstallArtifact } = + await import('@agent-device/platform-apple/install-artifact'); return await prepareIosInstallArtifact(input.source, options); }, resolveAppBundleId: async (device, app) => { - const { resolveIosApp } = await import('@agent-device/platform-apple'); + const { resolveIosApp } = await import('@agent-device/platform-apple/app-resolution'); return await resolveIosApp(device, app); }, }); diff --git a/src/platform-runtime-apple-resources.ts b/src/platform-runtime-apple-resources.ts index 569098cf6b..fc71ae29d6 100644 --- a/src/platform-runtime-apple-resources.ts +++ b/src/platform-runtime-apple-resources.ts @@ -3,7 +3,8 @@ import type { ElementSelectorKey } from '@agent-device/contracts/interactor-type import type { DeviceInfo } from '@agent-device/kernel/device'; export async function inspectAppleRunnerSession(deviceId: string) { - const { getRunnerSessionSnapshot } = await import('@agent-device/platform-apple'); + const { getRunnerSessionSnapshot } = + await import('@agent-device/platform-apple/runner/operations'); return getRunnerSessionSnapshot(deviceId); } @@ -13,6 +14,7 @@ export async function queryAppleRuntimeSelector( appBundleId: string | undefined, options: AppleRunnerRequestOptions, ): Promise> { - const { queryAppleRunnerSelector } = await import('@agent-device/platform-apple'); + const { queryAppleRunnerSelector } = + await import('@agent-device/platform-apple/runner/operations'); return await queryAppleRunnerSelector(device, selector, appBundleId, options); } diff --git a/src/platform-runtime-apple-tool-host.ts b/src/platform-runtime-apple-tool-host.ts index 8c530eddb0..5d013ef4b9 100644 --- a/src/platform-runtime-apple-tool-host.ts +++ b/src/platform-runtime-apple-tool-host.ts @@ -16,7 +16,7 @@ export function createAppleToolHost(): AppleToolHost { }, run: async (request, signal) => { const { runXcrun } = await awaitPreservingAbortReason( - async () => await import('@agent-device/platform-apple'), + async () => await import('@agent-device/platform-apple/tool-provider'), signal, ); const result = await awaitPreservingAbortReason( diff --git a/src/platform-runtime-audio-probe-host.ts b/src/platform-runtime-audio-probe-host.ts index fb641b4207..c9a2e11857 100644 --- a/src/platform-runtime-audio-probe-host.ts +++ b/src/platform-runtime-audio-probe-host.ts @@ -43,7 +43,7 @@ export function createAudioProbeRuntimeHost( bucketMs: number; statusPath: string; }): Promise => { - const { startMacOsAudioProbeProcess } = await import('@agent-device/platform-apple'); + const { startMacOsAudioProbeProcess } = await import('@agent-device/platform-apple/macos'); const probe = await startMacOsAudioProbeProcess(input); const marker = await resolveManagedProcessIdentity(probe.child.pid ?? undefined); return Object.freeze({ diff --git a/src/platform-runtime-daemon-owner-cleanup.test.ts b/src/platform-runtime-daemon-owner-cleanup.test.ts index 949567d3ed..303e890adf 100644 --- a/src/platform-runtime-daemon-owner-cleanup.test.ts +++ b/src/platform-runtime-daemon-owner-cleanup.test.ts @@ -4,7 +4,7 @@ const mocks = vi.hoisted(() => ({ cleanupRunnerLeasesForOwner: vi.fn(async () => undefined), })); -vi.mock('@agent-device/platform-apple', () => ({ +vi.mock('@agent-device/platform-apple/runner/operations', () => ({ cleanupRunnerLeasesForOwner: mocks.cleanupRunnerLeasesForOwner, })); diff --git a/src/platform-runtime-daemon-owner-cleanup.ts b/src/platform-runtime-daemon-owner-cleanup.ts index a500948975..c6a98c8f72 100644 --- a/src/platform-runtime-daemon-owner-cleanup.ts +++ b/src/platform-runtime-daemon-owner-cleanup.ts @@ -7,7 +7,8 @@ import type { DaemonOwnerCleanup } from '@agent-device/contracts/daemon-owner-cl export function createDaemonOwnerCleanup(): DaemonOwnerCleanup { return Object.freeze({ cleanup: async (owner) => { - const { cleanupRunnerLeasesForOwner } = await import('@agent-device/platform-apple'); + const { cleanupRunnerLeasesForOwner } = + await import('@agent-device/platform-apple/runner/operations'); await cleanupRunnerLeasesForOwner(owner); }, }); diff --git a/src/platform-runtime-device-ready.ts b/src/platform-runtime-device-ready.ts index 5eaaad2904..dd66b0cef2 100644 --- a/src/platform-runtime-device-ready.ts +++ b/src/platform-runtime-device-ready.ts @@ -17,7 +17,7 @@ export async function ensureLocalPlatformDeviceReady( ): Promise { if (isIosFamily(device)) { if (device.kind === 'simulator') { - const { ensureBootedSimulator } = await import('@agent-device/platform-apple'); + const { ensureBootedSimulator } = await import('@agent-device/platform-apple/simulator'); await ensureBootedSimulator(device, { deviceHub: options.deviceHub, focusExisting: options.focusExisting, diff --git a/src/platform-runtime-host-diagnostics.ts b/src/platform-runtime-host-diagnostics.ts index f38ae7249a..2e5349dd7b 100644 --- a/src/platform-runtime-host-diagnostics.ts +++ b/src/platform-runtime-host-diagnostics.ts @@ -30,7 +30,7 @@ export function createHostDiagnostics(): HostDiagnostics { return await harmonyToolchainCheck(); } if (platform === 'ios' || platform === 'macos' || platform === 'apple') { - const { appleToolchainCheck } = await import('@agent-device/platform-apple'); + const { appleToolchainCheck } = await import('@agent-device/platform-apple/doctor'); return await appleToolchainCheck(); } return undefined; @@ -52,7 +52,7 @@ export function createHostDiagnostics(): HostDiagnostics { context: HostDiagnosticsContext, ): Promise => { if (device.platform !== 'apple') return undefined; - const { appleRunnerWarmupCheck } = await import('@agent-device/platform-apple'); + const { appleRunnerWarmupCheck } = await import('@agent-device/platform-apple/doctor'); return await appleRunnerWarmupCheck(device, context); }, }); diff --git a/src/platform-runtime-host.test.ts b/src/platform-runtime-host.test.ts index 0f1c5e2452..5b92b9af6e 100644 --- a/src/platform-runtime-host.test.ts +++ b/src/platform-runtime-host.test.ts @@ -15,16 +15,13 @@ vi.mock('@agent-device/platform-apple/tool-provider', () => { return true; }, }), + runXcrun: async (args: string[], options: { signal?: AbortSignal }) => { + toolchains.appleRuns.push({ args, signal: options.signal }); + return { stdout: 'ok', stderr: '', exitCode: 0 }; + }, }; }); -vi.mock('@agent-device/platform-apple', () => ({ - runXcrun: async (args: string[], options: { signal?: AbortSignal }) => { - toolchains.appleRuns.push({ args, signal: options.signal }); - return { stdout: 'ok', stderr: '', exitCode: 0 }; - }, -})); - import { createDeviceInventoryHost } from './platform-runtime-host.ts'; test('inventory host loads scoped Apple mechanics only on first Apple tool use', async () => { diff --git a/src/platform-runtime-open-target.ts b/src/platform-runtime-open-target.ts index 935686542b..fe9da66fcd 100644 --- a/src/platform-runtime-open-target.ts +++ b/src/platform-runtime-open-target.ts @@ -35,7 +35,8 @@ export async function resolveSoleForegroundIosApp( const [soleBootedDevice] = booted; if (!soleBootedDevice) return undefined; - const { detectSoleRunningIosSimulatorApp } = await import('@agent-device/platform-apple'); + const { detectSoleRunningIosSimulatorApp } = + await import('@agent-device/platform-apple/app-resolution'); const app = await detectSoleRunningIosSimulatorApp(soleBootedDevice); return app ? { device: soleBootedDevice, app } : undefined; } catch (error) { @@ -159,7 +160,8 @@ async function tryResolveIosSimulatorDeepLinkBundleId( openTarget: string, ): Promise { try { - const { resolveIosSimulatorDeepLinkBundleId } = await import('@agent-device/platform-apple'); + const { resolveIosSimulatorDeepLinkBundleId } = + await import('@agent-device/platform-apple/app-resolution'); return await resolveIosSimulatorDeepLinkBundleId(device, openTarget); } catch { return undefined; @@ -171,7 +173,7 @@ async function tryResolveIosAppBundleId( openTarget: string, ): Promise { try { - const { resolveIosApp } = await import('@agent-device/platform-apple'); + const { resolveIosApp } = await import('@agent-device/platform-apple/app-resolution'); return await resolveIosApp(device, openTarget); } catch { return undefined; diff --git a/src/platform-runtime-operation-host.ts b/src/platform-runtime-operation-host.ts index 4e8e962bd6..18b38edc99 100644 --- a/src/platform-runtime-operation-host.ts +++ b/src/platform-runtime-operation-host.ts @@ -42,8 +42,7 @@ export async function loadMacOsSurfaceSnapshot( options: CaptureSnapshotInput['options'], signal?: AbortSignal, ): Promise { - const { captureMacOsSurfaceSnapshot } = - await import('./platforms/apple/os/macos/surface-snapshot.ts'); + const { captureMacOsSurfaceSnapshot } = await import('@agent-device/platform-apple/macos'); return await captureMacOsSurfaceSnapshot(options ?? {}, signal); } diff --git a/src/platform-runtime-perf-capture-host.ts b/src/platform-runtime-perf-capture-host.ts index eeff821084..a8a5b8b0a5 100644 --- a/src/platform-runtime-perf-capture-host.ts +++ b/src/platform-runtime-perf-capture-host.ts @@ -23,7 +23,7 @@ import type { AndroidNativePerfStartResult, AndroidNativePerfStopResult, } from '@agent-device/platform-android/mechanics'; -import type { AppleXctracePerfCapture } from '@agent-device/platform-apple'; +import type { AppleXctracePerfCapture } from '@agent-device/platform-apple/perf'; import { inspectManagedProcess, resolveManagedProcessIdentity, @@ -32,7 +32,7 @@ import { import { loadAndroidMechanics } from './platform-runtime-android-mechanics.ts'; const loadAndroidPerf = loadAndroidMechanics; -const loadAppleXctrace = async () => await import('@agent-device/platform-apple'); +const loadAppleXctrace = async () => await import('@agent-device/platform-apple/perf'); type PerfCaptureDescriptor = | Readonly<{ diff --git a/src/platform-runtime-perf-host.ts b/src/platform-runtime-perf-host.ts index 5defa73dcb..6e64811d01 100644 --- a/src/platform-runtime-perf-host.ts +++ b/src/platform-runtime-perf-host.ts @@ -15,9 +15,9 @@ import { loadAndroidMechanics } from './platform-runtime-android-mechanics.ts'; const loadAndroidPerf = loadAndroidMechanics; const loadAndroidFramePerf = loadAndroidMechanics; const loadAndroidNativePerf = loadAndroidMechanics; -const loadApplePerf = async () => await import('@agent-device/platform-apple'); +const loadApplePerf = async () => await import('@agent-device/platform-apple/perf'); const loadApplePerfMechanics = async () => await import('@agent-device/platform-apple/perf'); -const loadAppleXctrace = async () => await import('@agent-device/platform-apple'); +const loadAppleXctrace = async () => await import('@agent-device/platform-apple/perf'); const loadHarmonyPerf = async () => await import('@agent-device/platform-harmonyos'); /** Lazy host-only mechanics. Platform packages own facts, policy, and operation construction. */ diff --git a/src/platform-runtime-resource-cleanup.ts b/src/platform-runtime-resource-cleanup.ts index a6353b6f29..8d0f6b1b88 100644 --- a/src/platform-runtime-resource-cleanup.ts +++ b/src/platform-runtime-resource-cleanup.ts @@ -63,10 +63,10 @@ export const platformResourceCleanup: PlatformResourceCleanup = Object.freeze({ async cleanupSessionlessExecutionHost(device) { if (!isIosFamily(device)) return; const { resolveRunnerAppBundleId, stopIosRunnerSession } = - await import('@agent-device/platform-apple'); + await import('@agent-device/platform-apple/runner/operations'); await stopIosRunnerSession(device.id); const bundleId = await resolveRunnerAppBundleId(); - const { closeIosApp } = await import('@agent-device/platform-apple'); + const { closeIosApp } = await import('@agent-device/platform-apple/app-lifecycle'); await closeIosApp(device, bundleId).catch((error) => { emitDiagnostic({ level: 'debug', diff --git a/src/platform-runtime-runtime-hints.ts b/src/platform-runtime-runtime-hints.ts index 6447599929..e9af3155d8 100644 --- a/src/platform-runtime-runtime-hints.ts +++ b/src/platform-runtime-runtime-hints.ts @@ -314,7 +314,7 @@ async function runIosSimulatorRuntimeHintCommand( ): Promise { const [{ buildSimctlArgsForDevice }, { runXcrun }] = await Promise.all([ import('@agent-device/platform-apple/simctl'), - import('@agent-device/platform-apple'), + import('@agent-device/platform-apple/tool-provider'), ]); await runXcrun(buildSimctlArgsForDevice(device, args), options); } diff --git a/src/platform-runtime-screen-recording-apple-runner-host.ts b/src/platform-runtime-screen-recording-apple-runner-host.ts index c8e24ee8a7..b30164e393 100644 --- a/src/platform-runtime-screen-recording-apple-runner-host.ts +++ b/src/platform-runtime-screen-recording-apple-runner-host.ts @@ -89,7 +89,8 @@ export async function captureAppleClockAnchor( ) { return undefined; } - const { runAppleRunnerCommand } = await import('@agent-device/platform-apple'); + const { runAppleRunnerCommand } = + await import('@agent-device/platform-apple/runner/operations'); const result = await runAppleRunnerCommand( device, { command: 'snapshot', appBundleId, interactiveOnly: true, depth: 1 }, diff --git a/src/platform-runtime-screen-recording-apple-runner-transport.test.ts b/src/platform-runtime-screen-recording-apple-runner-transport.test.ts index 4904b6b8d5..fa5edaecd0 100644 --- a/src/platform-runtime-screen-recording-apple-runner-transport.test.ts +++ b/src/platform-runtime-screen-recording-apple-runner-transport.test.ts @@ -9,7 +9,7 @@ const runner = vi.hoisted(() => ({ snapshot: vi.fn(), })); -vi.mock('@agent-device/platform-apple', () => ({ +vi.mock('@agent-device/platform-apple/runner/operations', () => ({ runAppleRunnerCommand: runner.run, getRunnerSessionSnapshot: runner.snapshot, })); diff --git a/src/platform-runtime-screen-recording-apple-runner-transport.ts b/src/platform-runtime-screen-recording-apple-runner-transport.ts index 2ce24933ba..70bef2b35d 100644 --- a/src/platform-runtime-screen-recording-apple-runner-transport.ts +++ b/src/platform-runtime-screen-recording-apple-runner-transport.ts @@ -42,7 +42,8 @@ const localTransport: AppleRunnerScreenRecordingTransport = Object.freeze({ start: startLocalAppleRunnerRecording, inspect: async (device, runnerSessionId) => await inspectLocalRunner(device, runnerSessionId), stop: async ({ device, runnerSessionId, appBundleId, signal }) => { - const { runAppleRunnerCommand } = await import('@agent-device/platform-apple'); + const { runAppleRunnerCommand } = + await import('@agent-device/platform-apple/runner/operations'); await runAppleRunnerCommand( device, { command: 'recordStop', appBundleId }, @@ -59,7 +60,7 @@ async function startLocalAppleRunnerRecording({ signal, }: AppleRunnerScreenRecordingStartRequest): Promise { const { getRunnerSessionSnapshot, runAppleRunnerCommand } = - await import('@agent-device/platform-apple'); + await import('@agent-device/platform-apple/runner/operations'); const recordingFileName = `agent-device-recording-${Date.now()}.mp4`; const remotePath = device.appleOs === 'macos' || device.kind !== 'device' ? undefined : `tmp/${recordingFileName}`; @@ -147,7 +148,7 @@ async function inspectLocalRunner( runnerSessionId: string, ): Promise { const { getRunnerSessionSnapshot, readStaleRunnerLease, verifyLeaseRunnerPidIdentity } = - await import('@agent-device/platform-apple'); + await import('@agent-device/platform-apple/runner/operations'); const active = await getRunnerSessionSnapshot(device.id); if (active) { if (!active.alive) return 'missing'; diff --git a/src/platform-runtime/request-providers.ts b/src/platform-runtime/request-providers.ts index 3c1a022875..9567159d18 100644 --- a/src/platform-runtime/request-providers.ts +++ b/src/platform-runtime/request-providers.ts @@ -13,7 +13,7 @@ import type { AppleRunnerProvider, } from '@agent-device/platform-apple/runner'; import type { WebProvider } from '@agent-device/platform-web'; -import type { AppleToolProvider } from '@agent-device/platform-apple'; +import type { AppleToolProvider } from '@agent-device/platform-apple/tool-provider'; import type { LinuxToolProvider } from '@agent-device/platform-linux'; import type { VegaToolProvider } from '@agent-device/platform-vega'; import type { DeviceInfo } from '@agent-device/kernel/device'; @@ -224,7 +224,7 @@ const REQUEST_PLATFORM_PROVIDER_DESCRIPTORS = [ }, async appendWrapper(scopedProviders, wrappers) { if (!scopedProviders.appleTool?.provider) return; - const { withAppleToolProvider } = await import('@agent-device/platform-apple'); + const { withAppleToolProvider } = await import('@agent-device/platform-apple/tool-provider'); appendRequestProviderWrapper(wrappers, scopedProviders.appleTool, withAppleToolProvider); }, }, diff --git a/src/platforms/__tests__/install-source.test.ts b/src/platforms/__tests__/install-source.test.ts index cb594c6856..b10c5a46d3 100644 --- a/src/platforms/__tests__/install-source.test.ts +++ b/src/platforms/__tests__/install-source.test.ts @@ -17,7 +17,6 @@ import { isBlockedIpAddress, isBlockedSourceHostname, } from '@agent-device/provision-kit/install-source-network'; -import * as androidManifest from '../android/manifest.ts'; import { prepareAndroidInstallArtifact } from '@agent-device/platform-android/mechanics'; import { createLocalAppleToolProvider,