Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions packages/apple-runner/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
15 changes: 7 additions & 8 deletions packages/capture-kit/src/alert-tree-driver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down Expand Up @@ -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.
*/
Expand Down
2 changes: 1 addition & 1 deletion packages/command-surface/src/capture/alert.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion packages/command-surface/src/capture/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
4 changes: 2 additions & 2 deletions packages/contracts/src/alert-contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down
4 changes: 2 additions & 2 deletions packages/contracts/src/application-lifecycle-interaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.',
},
);
}
Expand Down
10 changes: 5 additions & 5 deletions packages/contracts/src/client-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
});
13 changes: 6 additions & 7 deletions packages/contracts/src/interactor-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
| {
Expand Down
2 changes: 1 addition & 1 deletion packages/contracts/src/settings-catalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 <app> --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 <app> --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.',
Comment thread
thymikee marked this conversation as resolved.
Resolved
},
} as const satisfies Record<string, SettingDefinition>;

Expand Down
1 change: 0 additions & 1 deletion packages/platform-apple/src/app-resolution-facade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ export {
findIosSimulatorInstalledApp,
invalidateIosAppResolutionCache,
resolveIosApp,
resolveIosAppAlias,
resolveIosSimulatorDeepLinkBundleId,
} from './core/app-resolution.ts';
export { resolveMacOsAppDeviceOpenTarget } from './os/macos/apps.ts';
2 changes: 1 addition & 1 deletion packages/platform-apple/src/core/app-resolution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
4 changes: 2 additions & 2 deletions packages/provider-limrun/src/app-state-read.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -34,7 +34,7 @@ async function confirmLimrunIosInstanceAnswers(
signal: AbortSignal,
): Promise<void> {
try {
await awaitLimrunOperation(session.listApps(), signal);
await awaitWithSignal(session.listApps(), signal);
} catch (error) {
signal.throwIfAborted();
throw new AppError(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
16 changes: 2 additions & 14 deletions packages/provider-limrun/src/request-cancellation.ts
Original file line number Diff line number Diff line change
@@ -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<Value>(
source: Promise<Value>,
signal?: AbortSignal,
onSettled?: () => void,
): Promise<Value> {
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
Expand All @@ -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) {
Expand All @@ -47,5 +35,5 @@ export async function awaitLimrunDeploymentOperation<Value>(
source: Promise<Value>,
signal?: AbortSignal,
): Promise<Value> {
return await (drain ? drain.wait(source, signal) : awaitLimrunOperation(source, signal));
return await (drain ? drain.wait(source, signal) : awaitWithSignal(source, signal));
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
};
Expand Down
1 change: 0 additions & 1 deletion packages/provider-limrun/src/runtime-dependencies.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -365,7 +365,6 @@ function createContractFixture() {
createRunner: async () => {
return testIosRunner;
},
resolveAppAlias: async (app: string) => app,
readBundleAppName: async () => undefined,
},
} satisfies LimrunRuntimeDependencies;
Expand Down
1 change: 0 additions & 1 deletion packages/provider-limrun/src/runtime-dependencies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,6 @@ export type LimrunIosRuntimeAdapter = {
token: string;
device: DeviceInfo;
}): Promise<LimrunIosRunner>;
resolveAppAlias(app: string): Promise<string>;
readBundleAppName(appPath: string): Promise<string | undefined>;
};

Expand Down
2 changes: 1 addition & 1 deletion packages/provider-revyl/src/snapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
1 change: 0 additions & 1 deletion src/platform-runtime-gateway.fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
16 changes: 0 additions & 16 deletions src/sdk/limrun-runtime-dependencies.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { expect, test, vi } from 'vitest';
const moduleLoads = vi.hoisted(() => ({
androidAppHelpers: 0,
androidLogcat: 0,
appleAppResolution: 0,
appleInstallArtifact: 0,
}));

Expand All @@ -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<typeof import('@agent-device/platform-apple/app-resolution')>()),
resolveIosAppAlias: vi.fn((app: string) => {
moduleLoads.appleAppResolution += 1;
return `resolved:${app}`;
}),
}));

vi.mock('@agent-device/platform-apple/install-artifact', async (importOriginal) => ({
...(await importOriginal<typeof import('@agent-device/platform-apple/install-artifact')>()),
readIosBundleInfo: vi.fn(async () => {
Expand All @@ -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();
Expand Down Expand Up @@ -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');
Expand Down
Loading
Loading