From cf1c5a2180940be4a186afebd59d7ecf4aadfabd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Thu, 5 Nov 2026 19:16:51 +0100 Subject: [PATCH 1/3] refactor(limrun): drop the unused iOS app-alias seam and stale AX-backend wording Limrun iOS runs the shared XCTest runner since #4224, so: - remove resolveAppAlias from the Limrun iOS adapter (no callers), its SDK implementation, fixtures, test, and the facade export it alone used; - wait on Limrun operations through the kernel awaitWithSignal and drop the awaitLimrunOperation wrapper; - fix help, docs and comments that still describe the old element-tree backend: alert language matching, settings --app resolution, the launch-console refusal, and the alert/fill/snapshot contract comments. Co-Authored-By: Claude Opus 5.5 --- packages/apple-runner/src/index.ts | 6 +++--- packages/capture-kit/src/alert-tree-driver.ts | 15 +++++++-------- packages/command-surface/src/capture/alert.ts | 2 +- packages/command-surface/src/capture/settings.ts | 2 +- packages/contracts/src/alert-contract.ts | 4 ++-- .../src/application-lifecycle-interaction.ts | 4 ++-- packages/contracts/src/client-settings.ts | 10 +++++----- packages/contracts/src/interactor-types.ts | 13 ++++++------- packages/contracts/src/settings-catalog.ts | 2 +- .../platform-apple/src/app-resolution-facade.ts | 1 - .../platform-apple/src/core/app-resolution.ts | 2 +- packages/provider-limrun/src/app-state-read.ts | 4 ++-- .../src/ios-interactor-snapshot.test.ts | 1 - .../provider-limrun/src/request-cancellation.ts | 16 ++-------------- .../src/runtime-dependencies.fixtures.ts | 1 - .../src/runtime-dependencies.test.ts | 1 - .../provider-limrun/src/runtime-dependencies.ts | 1 - packages/provider-revyl/src/snapshot.ts | 2 +- src/platform-runtime-gateway.fixtures.ts | 1 - src/sdk/limrun-runtime-dependencies.test.ts | 16 ---------------- src/sdk/limrun-runtime-dependencies.ts | 5 ----- website/docs/docs/commands.md | 2 +- 22 files changed, 35 insertions(+), 76 deletions(-) diff --git a/packages/apple-runner/src/index.ts b/packages/apple-runner/src/index.ts index 477f057334..4944af4950 100644 --- a/packages/apple-runner/src/index.ts +++ b/packages/apple-runner/src/index.ts @@ -3,9 +3,9 @@ * * The package entry names its whole surface. Host-bound operations evaluate the implementation, so * they reach their host capabilities through the `AppleRunnerHost` port, which `platform-apple` - * binds once through `bindAppleRunnerHost` before any operation runs. The one operation outside - * this closure, `runApplePressSeries`, and the legacy device-set restore stay behind function-scoped - * imports so the entry evaluates only what a host-bound operation needs. + * binds once through `bindAppleRunnerHost` before any operation runs. `runApplePressSeries`, + * `createLaunchedAppleRunnerProvider`, and the legacy device-set restore stay behind + * function-scoped imports so the entry evaluates only what a host-bound operation needs. */ export { RUNNER_STARTUP_TIMEOUT_MS } from './runner-startup-transport.ts'; diff --git a/packages/capture-kit/src/alert-tree-driver.ts b/packages/capture-kit/src/alert-tree-driver.ts index b9df86d5b6..bfe99addd0 100644 --- a/packages/capture-kit/src/alert-tree-driver.ts +++ b/packages/capture-kit/src/alert-tree-driver.ts @@ -18,17 +18,16 @@ import { AppError, createRequestCanceledError } from '@agent-device/kernel/error import { successText } from '@agent-device/kernel/success-text'; /** - * The one driver for every backend that reads alerts from a capture tree (Android ADB, Limrun - * iOS). A backend supplies only its observation (`capture` + `findCandidate`), its acting - * policy (`choose`), and its press seam (`press`). It lives in the capture kit: an iOS provider + * The one driver for every backend that reads alerts from a capture tree (Android ADB). A + * backend supplies only its observation (`capture` + `findCandidate`), its acting policy + * (`choose`), and its press seam (`press`). It lives in the capture kit: an iOS provider * must not depend on the Android package to poll a tree. * * The budget rule: `pollUntilSettled` owns it — every probe started inside the budget is * awaited and counts; the deadline ends only the LOOP, never a capture in flight. Only the * caller's cancellation stops a transport. A quiet screen therefore answers `alert-not-found` - * on every backend. The cost: a transport with no abort (Limrun's client) can overrun the - * budget by one call, and a capture that never returns keeps the leg open until the caller - * cancels it. + * on every backend. The cost: a transport with no abort can overrun the budget by one call, and + * a capture that never returns keeps the leg open until the caller cancels it. */ /** The point a press seam aims at: a button's center in the backend's own coordinate space. */ @@ -56,8 +55,8 @@ export type TreeAlertCandidate< /** * The seams one tree-based backend owns. `signal` is always the daemon request's own * cancellation, never a budget timer: it is what may stop a transport in flight. A transport - * with no abort (Limrun's client) cannot be stopped this way — its in-flight call keeps running - * and the leg waits for the answer it started (the overrun named above). The caller meanwhile + * with no abort cannot be stopped this way — its in-flight call keeps running and the leg waits + * for the answer it started (the overrun named above). The caller meanwhile * learns of its cancellation from its own client transport; the daemon's own answer lands once * the leg settles on the started probe's answer. */ diff --git a/packages/command-surface/src/capture/alert.ts b/packages/command-surface/src/capture/alert.ts index ef69de3bfd..2cf692fbad 100644 --- a/packages/command-surface/src/capture/alert.ts +++ b/packages/command-surface/src/capture/alert.ts @@ -17,7 +17,7 @@ import { AppError } from '@agent-device/kernel/errors'; const ALERT_COMMAND_NAME = 'alert'; const alertCommandDescription = - "Inspect, wait for, accept, or dismiss a platform alert. Use get before acting when the alert content matters; accept and dismiss change the active alert state. Each iOS XCTest execution activates once, then only observes. Local Apple XCTest and Limrun iOS choose recognized English labels by preference (Allow While Using App before Allow Once); on iOS simulators, local XCTest also recognizes the device's preferred languages' wording of those labels from the OS permission-prompt strings, on any alert, while Limrun iOS stays English; equal-ranked choices and unknown labels refuse, including a lone unknown button. Open, Done, and Confirm… accept; Keep Browsing, Deny, and Skip dismiss. On the Apple XCUITest runner, dismiss taps only a recognized decline label (Cancel, Don't Allow, Ask App Not to Track, and similar) or the close element of a popover whose accessibility marker reads exactly \"dismiss popup\"; when no button fits the action, accept and dismiss answer ALERT_BUTTON_NOT_FOUND having tapped nothing — read labels with alert get, then press the button by label. Inspect again after an unconfirmed action; never assume a timeout means nothing happened."; + "Inspect, wait for, accept, or dismiss a platform alert. Use get before acting when the alert content matters; accept and dismiss change the active alert state. Each iOS XCTest execution activates once, then only observes. Apple XCTest and Limrun iOS choose recognized English labels by preference (Allow While Using App before Allow Once); on iOS simulators, including Limrun, the runner also recognizes the device's preferred languages' wording of those labels from the OS permission-prompt strings, on any alert; equal-ranked choices and unknown labels refuse, including a lone unknown button. Open, Done, and Confirm… accept; Keep Browsing, Deny, and Skip dismiss. On the Apple XCUITest runner, dismiss taps only a recognized decline label (Cancel, Don't Allow, Ask App Not to Track, and similar) or the close element of a popover whose accessibility marker reads exactly \"dismiss popup\"; when no button fits the action, accept and dismiss answer ALERT_BUTTON_NOT_FOUND having tapped nothing — read labels with alert get, then press the button by label. Inspect again after an unconfirmed action; never assume a timeout means nothing happened."; const alertCommandMetadata = defineFieldCommandMetadata( ALERT_COMMAND_NAME, diff --git a/packages/command-surface/src/capture/settings.ts b/packages/command-surface/src/capture/settings.ts index b444cd9d1e..fe612841c1 100644 --- a/packages/command-surface/src/capture/settings.ts +++ b/packages/command-surface/src/capture/settings.ts @@ -35,7 +35,7 @@ const settingsCommandMetadata = defineFieldCommandMetadata( // A readable setting answers with no state; the parse decides per setting. state: stringField(), app: stringField( - 'App the change targets, by bundle id, package name, or app name: the app `clear-app-state` clears, or the app a `permission` change (or an iOS-simulator `location on|off` privacy change) lands on. A name resolves to the installed app as `open` resolves it: an iOS simulator matches display names and accepts only an installed bundle id, Android accepts only an installed package and refuses a name that resolves to an intent with `setting_app_not_package`, and a Limrun iOS session resolves only the `settings` alias. One that resolves to nothing is refused with `app_not_installed` before anything changes. The app need not be running. Without it the session app is used; a device-wide change (`location set` on any target, the Android `location on|off` toggle, `mobile-data`, macOS permissions) refuses one with `setting_app_not_consumed`. The CLI reads it only from this invocation, never from `AGENT_DEVICE_TARGET_APP` or config `targetApp`.', + 'App the change targets, by bundle id, package name, or app name: the app `clear-app-state` clears, or the app a `permission` change (or an iOS-simulator `location on|off` privacy change) lands on. A name resolves to the installed app as `open` resolves it: an iOS simulator, local or Limrun, matches display names and accepts only an installed bundle id, and Android accepts only an installed package and refuses a name that resolves to an intent with `setting_app_not_package`. One that resolves to nothing is refused with `app_not_installed` before anything changes. The app need not be running. Without it the session app is used; a device-wide change (`location set` on any target, the Android `location on|off` toggle, `mobile-data`, macOS permissions) refuses one with `setting_app_not_consumed`. The CLI reads it only from this invocation, never from `AGENT_DEVICE_TARGET_APP` or config `targetApp`.', ), latitude: numberField(), longitude: numberField(), diff --git a/packages/contracts/src/alert-contract.ts b/packages/contracts/src/alert-contract.ts index ecf4f6a764..72030c0d90 100644 --- a/packages/contracts/src/alert-contract.ts +++ b/packages/contracts/src/alert-contract.ts @@ -15,8 +15,8 @@ export const MAX_ALERT_WINDOW_MS = 2_147_483_647; * The one alert failure the family retries on: the backend looked and there was no alert *yet*. * Both Apple backends state it in typed form — the XCTest runner as this `ErrorPayload.code` * (surfacing as `details.runnerErrorCode`, like `RUNNER_BUSY`, without changing the wire error - * code), the macOS helper as details.reason — and so do the tree-based acting legs (Android and - * Limrun iOS, through the shared driver), as this `details.reason` paired with `dispatched: + * code), the macOS helper as details.reason — and so does the tree-based Android acting leg + * (through the shared driver), as this `details.reason` paired with `dispatched: * 'no'`. Retry and the fallback hint key on these, never on the message text: a transport, * runner or helper failure must never read as an absent alert. */ diff --git a/packages/contracts/src/application-lifecycle-interaction.ts b/packages/contracts/src/application-lifecycle-interaction.ts index 18eea109e8..5b0e5c085f 100644 --- a/packages/contracts/src/application-lifecycle-interaction.ts +++ b/packages/contracts/src/application-lifecycle-interaction.ts @@ -90,11 +90,11 @@ function bindApplicationLifecycleInteractor( if (execution.launchConsole) { throw new AppError( 'UNSUPPORTED_OPERATION', - '--launch-console is supported only for a local iOS simulator app launch', + '--launch-console is supported only for a local or Limrun iOS simulator app launch', { reason: 'launch-console-owner-unadmitted', deviceId: device.id, - hint: 'Remove --launch-console, or use a local iOS simulator for launch output.', + hint: 'Remove --launch-console, or use a local or Limrun iOS simulator for launch output.', }, ); } diff --git a/packages/contracts/src/client-settings.ts b/packages/contracts/src/client-settings.ts index 870ffd1a13..102b034243 100644 --- a/packages/contracts/src/client-settings.ts +++ b/packages/contracts/src/client-settings.ts @@ -82,11 +82,11 @@ export type SettingsUpdateOptions = mode?: PermissionMode; /** * The app the permission changes, by bundle id, package name, or app name, resolved as `open` - * resolves it: an iOS simulator matches display names and accepts only an installed bundle - * id, Android accepts only an installed package and refuses a name that resolves to an intent - * with `setting_app_not_package`, and a Limrun iOS session resolves only the `settings` - * alias. Without it the session app is used; no app has to be running or open. macOS - * permissions are host-level TCC grants, so naming an app there is refused. + * resolves it: an iOS simulator, local or Limrun, matches display names and accepts only an + * installed bundle id, and Android accepts only an installed package and refuses a name that + * resolves to an intent with `setting_app_not_package`. Without it the session app is used; + * no app has to be running or open. macOS permissions are host-level TCC grants, so naming an + * app there is refused. */ app?: string; }); diff --git a/packages/contracts/src/interactor-types.ts b/packages/contracts/src/interactor-types.ts index dce79ef80b..b396e34d01 100644 --- a/packages/contracts/src/interactor-types.ts +++ b/packages/contracts/src/interactor-types.ts @@ -172,13 +172,12 @@ export const CLOUD_TEXT_ENTRY_READINESS = ['focused-element'] as const; export type CloudTextEntryReadiness = (typeof CLOUD_TEXT_ENTRY_READINESS)[number]; /** - * What `Interactor.fill` reports back about the entry it performed. The cloud - * interactors (WebDriver and the Limrun iOS session) populate - * `textEntryReadiness`; Android and the Apple runner may return target-bound - * `verification: 'unconfirmed'` evidence when the field changed but its value - * cannot confirm the text (app-owned formatting, or an accessibility value that - * does not echo the entry). The Apple runner carries its own readiness - * equivalent in Swift. + * What `Interactor.fill` reports back about the entry it performed. The + * WebDriver interactor populates `textEntryReadiness`; Android and the Apple + * runner may return target-bound `verification: 'unconfirmed'` evidence when the + * field changed but its value cannot confirm the text (app-owned formatting, or + * an accessibility value that does not echo the entry). The Apple runner + * carries its own readiness equivalent in Swift. */ export type FillBackendResult = | { diff --git a/packages/contracts/src/settings-catalog.ts b/packages/contracts/src/settings-catalog.ts index 8506fc8817..f83332a4a4 100644 --- a/packages/contracts/src/settings-catalog.ts +++ b/packages/contracts/src/settings-catalog.ts @@ -173,7 +173,7 @@ export const SETTINGS = { ], app: 'input', appScope: { apple: 'app-scoped', mobile: 'app-scoped', 'macos-host': 'device-level' }, - help: 'Changes a permission of the --app app or the session app. An iOS simulator resolves --app to an installed bundle id or display name, Android to an installed package, and a Limrun iOS session only through the settings alias. On Android, deny|reset of a held permission kills a running app; the response reports priorGrantState and warns, and open --relaunch restores it. Android needs a resolvable foreground user and changes nothing without one. On the macOS host, grant|reset change the host TCC grants for the three desktop targets and take no --app.', + help: 'Changes a permission of the --app app or the session app. An iOS simulator, local or Limrun, resolves --app to an installed bundle id or display name, and Android to an installed package. On Android, deny|reset of a held permission kills a running app; the response reports priorGrantState and warns, and open --relaunch restores it. Android needs a resolvable foreground user and changes nothing without one. On the macOS host, grant|reset change the host TCC grants for the three desktop targets and take no --app.', }, } as const satisfies Record; diff --git a/packages/platform-apple/src/app-resolution-facade.ts b/packages/platform-apple/src/app-resolution-facade.ts index b772981807..bd566a016f 100644 --- a/packages/platform-apple/src/app-resolution-facade.ts +++ b/packages/platform-apple/src/app-resolution-facade.ts @@ -4,7 +4,6 @@ export { findIosSimulatorInstalledApp, invalidateIosAppResolutionCache, resolveIosApp, - resolveIosAppAlias, resolveIosSimulatorDeepLinkBundleId, } from './core/app-resolution.ts'; export { resolveMacOsAppDeviceOpenTarget } from './os/macos/apps.ts'; diff --git a/packages/platform-apple/src/core/app-resolution.ts b/packages/platform-apple/src/core/app-resolution.ts index 79d6f99110..f203fdd5dc 100644 --- a/packages/platform-apple/src/core/app-resolution.ts +++ b/packages/platform-apple/src/core/app-resolution.ts @@ -127,7 +127,7 @@ function isUnresolvedInstalledApp(error: unknown): boolean { ); } -export function resolveIosAppAlias(app: string): string { +function resolveIosAppAlias(app: string): string { const trimmed = app.trim(); return ALIASES[trimmed.toLowerCase()] ?? app; } diff --git a/packages/provider-limrun/src/app-state-read.ts b/packages/provider-limrun/src/app-state-read.ts index 9b3fea0db5..ce85bb9311 100644 --- a/packages/provider-limrun/src/app-state-read.ts +++ b/packages/provider-limrun/src/app-state-read.ts @@ -1,7 +1,7 @@ import type { AppStateRuntimeResult } from '@agent-device/contracts/app-state-runtime'; import { AppError } from '@agent-device/kernel/errors'; import type { LimrunDeviceSession } from './device-session.ts'; -import { awaitLimrunOperation } from './request-cancellation.ts'; +import { awaitWithSignal } from '@agent-device/kernel/await-with-signal'; /** * Android reads the foreground app. Limrun exposes no foreground bundle id on iOS, so an iOS read @@ -34,7 +34,7 @@ async function confirmLimrunIosInstanceAnswers( signal: AbortSignal, ): Promise { try { - await awaitLimrunOperation(session.listApps(), signal); + await awaitWithSignal(session.listApps(), signal); } catch (error) { signal.throwIfAborted(); throw new AppError( diff --git a/packages/provider-limrun/src/ios-interactor-snapshot.test.ts b/packages/provider-limrun/src/ios-interactor-snapshot.test.ts index cc2fe72bcc..abb8d05884 100644 --- a/packages/provider-limrun/src/ios-interactor-snapshot.test.ts +++ b/packages/provider-limrun/src/ios-interactor-snapshot.test.ts @@ -36,7 +36,6 @@ test('shared runner targets follow open, request overrides, home and close', asy device: iosRunnerDevice, runner, client: { elementTree, launchApp: async () => {} }, - dependencies: { ios: { resolveAppAlias: async () => 'com.apple.Preferences' } }, } as unknown as LimrunIosSession; const interactor = createLimrunIosInteractor(session); await interactor.open('settings'); diff --git a/packages/provider-limrun/src/request-cancellation.ts b/packages/provider-limrun/src/request-cancellation.ts index c21aa29753..474ad3436f 100644 --- a/packages/provider-limrun/src/request-cancellation.ts +++ b/packages/provider-limrun/src/request-cancellation.ts @@ -1,16 +1,4 @@ import { awaitWithSignal } from '@agent-device/kernel/await-with-signal'; -/** - * The instance-client operations wrapped here do not accept an AbortSignal. Keep the request-bound - * caller cancellable while retaining a handled continuation for the provider operation it started. - */ -export async function awaitLimrunOperation( - source: Promise, - signal?: AbortSignal, - onSettled?: () => void, -): Promise { - return await awaitWithSignal(source.finally(onSettled), signal); -} - /** * Request-local ownership for an opaque Limrun SDK operation. A caller can observe its own * abort immediately, but a bound runtime does not release the provider session until every @@ -32,7 +20,7 @@ export function createLimrunRequestOperationDrain(): LimrunRequestOperationDrain }; }); pending.add(completion); - return await awaitLimrunOperation(source, signal, settled); + return await awaitWithSignal(source.finally(settled), signal); }, [Symbol.asyncDispose]: async () => { while (pending.size > 0) { @@ -47,5 +35,5 @@ export async function awaitLimrunDeploymentOperation( source: Promise, signal?: AbortSignal, ): Promise { - return await (drain ? drain.wait(source, signal) : awaitLimrunOperation(source, signal)); + return await (drain ? drain.wait(source, signal) : awaitWithSignal(source, signal)); } diff --git a/packages/provider-limrun/src/runtime-dependencies.fixtures.ts b/packages/provider-limrun/src/runtime-dependencies.fixtures.ts index 427fc88aec..e87542e1f0 100644 --- a/packages/provider-limrun/src/runtime-dependencies.fixtures.ts +++ b/packages/provider-limrun/src/runtime-dependencies.fixtures.ts @@ -66,7 +66,6 @@ export function stubLimrunRuntimeDependencies(): LimrunRuntimeDependencies { createTestIosRunner(() => { throw new Error('Unexpected XCTest runner command'); }, session.device), - resolveAppAlias: async (app) => app, readBundleAppName: async () => undefined, }, }; diff --git a/packages/provider-limrun/src/runtime-dependencies.test.ts b/packages/provider-limrun/src/runtime-dependencies.test.ts index 6d32cebee0..817feb8840 100644 --- a/packages/provider-limrun/src/runtime-dependencies.test.ts +++ b/packages/provider-limrun/src/runtime-dependencies.test.ts @@ -365,7 +365,6 @@ function createContractFixture() { createRunner: async () => { return testIosRunner; }, - resolveAppAlias: async (app: string) => app, readBundleAppName: async () => undefined, }, } satisfies LimrunRuntimeDependencies; diff --git a/packages/provider-limrun/src/runtime-dependencies.ts b/packages/provider-limrun/src/runtime-dependencies.ts index 739ed6e594..75e700e6b5 100644 --- a/packages/provider-limrun/src/runtime-dependencies.ts +++ b/packages/provider-limrun/src/runtime-dependencies.ts @@ -202,7 +202,6 @@ export type LimrunIosRuntimeAdapter = { token: string; device: DeviceInfo; }): Promise; - resolveAppAlias(app: string): Promise; readBundleAppName(appPath: string): Promise; }; diff --git a/packages/provider-revyl/src/snapshot.ts b/packages/provider-revyl/src/snapshot.ts index fb720477b0..4274ae2cc9 100644 --- a/packages/provider-revyl/src/snapshot.ts +++ b/packages/provider-revyl/src/snapshot.ts @@ -47,7 +47,7 @@ export async function captureRevylSnapshot( } /** - * iOS: the AX node tree goes through the same reader and interpreter as Limrun's iOS tree, and comes + * iOS: the AX node tree goes through the capture kit's iOS AX tree reader and interpreter, and comes * back as an acquisition for the host to present. The viewport is the tree's own application root, * because that is the box its frames were measured in; without a usable root it is `health`'s screen, * which Revyl reports in the same points on iOS. diff --git a/src/platform-runtime-gateway.fixtures.ts b/src/platform-runtime-gateway.fixtures.ts index 7d296850b0..c9ae9dd81a 100644 --- a/src/platform-runtime-gateway.fixtures.ts +++ b/src/platform-runtime-gateway.fixtures.ts @@ -256,7 +256,6 @@ export const limrunTestDependencies: LimrunRuntimeDependencies = { [Symbol.asyncDispose]: async () => {}, }), createRunner: async (session) => createTestLimrunIosRunner(session.device), - resolveAppAlias: async (app: string) => app, readBundleAppName: async () => undefined, }, } satisfies LimrunRuntimeDependencies; diff --git a/src/sdk/limrun-runtime-dependencies.test.ts b/src/sdk/limrun-runtime-dependencies.test.ts index 5cadb9ca74..07e05ce4cb 100644 --- a/src/sdk/limrun-runtime-dependencies.test.ts +++ b/src/sdk/limrun-runtime-dependencies.test.ts @@ -4,7 +4,6 @@ import { expect, test, vi } from 'vitest'; const moduleLoads = vi.hoisted(() => ({ androidAppHelpers: 0, androidLogcat: 0, - appleAppResolution: 0, appleInstallArtifact: 0, })); @@ -27,14 +26,6 @@ vi.mock('@agent-device/platform-android/mechanics', async (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 () => { @@ -50,7 +41,6 @@ test('Limrun construction defers operation and opposite-platform helper modules' assert.deepEqual(moduleLoads, { androidAppHelpers: 0, androidLogcat: 0, - appleAppResolution: 0, appleInstallArtifact: 0, }); await runtime.shutdown(); @@ -88,16 +78,10 @@ test('Limrun construction defers operation and opposite-platform helper modules' ]); assert.equal(moduleLoads.androidAppHelpers, 1); assert.equal(moduleLoads.androidLogcat, 0); - assert.equal(moduleLoads.appleAppResolution, 0); assert.equal(moduleLoads.appleInstallArtifact, 0); assert.equal(await dependencies.android.readLogs(adb, 10), 'log line\n'); assert.equal(moduleLoads.androidLogcat, 1); - assert.equal(moduleLoads.appleAppResolution, 0); - assert.equal(moduleLoads.appleInstallArtifact, 0); - - assert.equal(await dependencies.ios.resolveAppAlias('settings'), 'resolved:settings'); - assert.equal(moduleLoads.appleAppResolution, 1); assert.equal(moduleLoads.appleInstallArtifact, 0); assert.equal(await dependencies.ios.readBundleAppName('/tmp/Example.app'), 'Example'); diff --git a/src/sdk/limrun-runtime-dependencies.ts b/src/sdk/limrun-runtime-dependencies.ts index 5f469f7b83..4eeddc54bf 100644 --- a/src/sdk/limrun-runtime-dependencies.ts +++ b/src/sdk/limrun-runtime-dependencies.ts @@ -121,11 +121,6 @@ export function createLimrunRuntimeDependencies(host: HostEnvironment): LimrunRu await import('../platform-runtime-apple-application-tools.ts'); return createLimrunIosRunner(session, host, createAppleApplicationTools(host.env)); }, - resolveAppAlias: async (app) => { - const { resolveIosAppAlias } = - await import('@agent-device/platform-apple/app-resolution'); - return resolveIosAppAlias(app); - }, readBundleAppName: async (appPath) => { const { readIosBundleInfo } = await import('@agent-device/platform-apple/install-artifact'); diff --git a/website/docs/docs/commands.md b/website/docs/docs/commands.md index a4231e5f7c..8322cc92d0 100644 --- a/website/docs/docs/commands.md +++ b/website/docs/docs/commands.md @@ -793,7 +793,7 @@ App state: Permissions: -- On iOS and Android, `permission` and iOS simulator `location on|off` apply to the session app. Pass `--app ` to target an installed app without opening it, for example to grant a permission before first launch. The app need not be running. Unlike `open`, which passes an unknown id through, settings resolve only installed apps, the same way `clear-app-state` does. On an iOS simulator, a dotted name is first matched against installed bundle ids, and any name against installed display names (`--app Maps` is `com.apple.Maps`). On Android, a package must be installed, and any other name is matched against installed package names. A Limrun iOS session resolves only the `settings` alias. +- On iOS and Android, `permission` and iOS simulator `location on|off` apply to the session app. Pass `--app ` to target an installed app without opening it, for example to grant a permission before first launch. The app need not be running. Unlike `open`, which passes an unknown id through, settings resolve only installed apps, the same way `clear-app-state` does. On an iOS simulator, including Limrun, a dotted name is first matched against installed bundle ids, and any name against installed display names (`--app Maps` is `com.apple.Maps`). On Android, a package must be installed, and any other name is matched against installed package names. - `--app` counts only when passed on the command line; `AGENT_DEVICE_TARGET_APP` and `targetApp` in `~/.agent-device/config.json` never retarget a settings change. - Refusals for `permission`, `location`, and `clear-app-state`, all with `dispatched: no` and nothing changed: - `app_not_installed` (`APP_NOT_INSTALLED`): the app resolves to nothing installed. From e958fe03fec9105b47ec8091b0f4f6267745b5c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Thu, 5 Nov 2026 19:53:20 +0100 Subject: [PATCH 2/3] test(provider): match the reworded launch-console refusal Co-Authored-By: Claude Opus 5.5 --- .../provider-scenarios/provider-device-runtime.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integration/provider-scenarios/provider-device-runtime.test.ts b/test/integration/provider-scenarios/provider-device-runtime.test.ts index 09dcbee769..98443f6abd 100644 --- a/test/integration/provider-scenarios/provider-device-runtime.test.ts +++ b/test/integration/provider-scenarios/provider-device-runtime.test.ts @@ -107,7 +107,7 @@ test('provider-owned relaunch refuses launch-console without closing the running ), 500, 'UNSUPPORTED_OPERATION', - /local iOS simulator/, + /local or Limrun iOS simulator/, ); assert.equal( (error.details as Record).reason, From f958c6354d482462400b0d1487621cbf1406dc3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Fri, 6 Nov 2026 05:44:32 +0100 Subject: [PATCH 3/3] test(limrun): pin iOS settings --app display-name resolution through the remote app list Co-Authored-By: Claude Opus 5.5 --- .../limrun-ios-settings.test.ts | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/test/integration/provider-scenarios/limrun-ios-settings.test.ts b/test/integration/provider-scenarios/limrun-ios-settings.test.ts index ae337c9a41..c17a788c93 100644 --- a/test/integration/provider-scenarios/limrun-ios-settings.test.ts +++ b/test/integration/provider-scenarios/limrun-ios-settings.test.ts @@ -115,3 +115,27 @@ test('a Limrun iOS simctl failure that is not a refusal stays a command failure' await dispose(); } }); + +test('a Limrun iOS permission resolves an app display name through the remote app list', async () => { + state.simctl.mockImplementation(() => ({ + wait: async () => ({ code: 0, stdout: '', stderr: '' }), + })); + const { binding, dispose } = await bindLimrunDeployment( + 'ios', + new AbortController().signal, + createLimrunRuntimeDependencies({ env: {}, cwd: process.cwd() }).ios, + ); + try { + await binding.operations.setSetting!({ + setting: 'permission', + state: 'grant', + appBundleId: 'Example', + options: { permissionTarget: 'camera' }, + }); + expect(state.simctl.mock.calls).toEqual([ + [['privacy', 'booted', 'grant', 'camera', 'com.example.ios']], + ]); + } finally { + await dispose(); + } +});