From 82594b1096c179a1670b317abec6a3f7dd8abb1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Wed, 7 Oct 2026 22:42:18 +0200 Subject: [PATCH 1/4] refactor(providers): build WebDriver and limrun refusals through unavailableFact Co-Authored-By: Claude Fable 5.1 --- .../src/interaction-operations.ts | 119 +++--- .../src/platform-runtime.ts | 339 ++++++++---------- 2 files changed, 201 insertions(+), 257 deletions(-) diff --git a/packages/provider-limrun/src/interaction-operations.ts b/packages/provider-limrun/src/interaction-operations.ts index f0a0c3e97e..004fc2dbe1 100644 --- a/packages/provider-limrun/src/interaction-operations.ts +++ b/packages/provider-limrun/src/interaction-operations.ts @@ -38,7 +38,10 @@ import { touchRuntimeOperationFacts, } from '@agent-device/contracts/touch-runtime'; import type { Interactor, RunnerContext } from '@agent-device/contracts/interactor-types'; -import type { RuntimeOperationUnavailability } from '@agent-device/contracts/platform-runtime'; +import { + type RuntimeOperationUnavailability, + unavailableFact, +} from '@agent-device/contracts/platform-runtime'; import { isIosFamily, type DeviceInfo } from '@agent-device/kernel/device'; import { setTimeout as sleep } from 'node:timers/promises'; @@ -48,31 +51,22 @@ const available = Object.freeze({ available: true } as const); * Stating it as a fact refuses at admission instead of mid-execution (ADR 0019 §6), and it is * the only refusal the caller sees: the leg's interactor carries no gesture member at all. */ -const iosGestureUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-provider-mode', - hint: 'Limrun iOS direct sessions do not expose portable gesture execution yet.', -} as const); +const iosGestureUnavailable = unavailableFact( + 'unsupported-provider-mode', + 'Limrun iOS direct sessions do not expose portable gesture execution yet.', +); /** What an unnamed gesture tier reports on a live Limrun session. */ -const gestureUnsupportedProviderMode = Object.freeze({ - available: false, - reason: 'unsupported-provider-mode', -} as const); +const gestureUnsupportedProviderMode = unavailableFact('unsupported-provider-mode'); /** What an unnamed clipboard half reports on a live Limrun session. */ -const clipboardUnsupportedProviderMode = Object.freeze({ - available: false, - reason: 'unsupported-provider-mode', -} as const); -const androidTvMultiTouchUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-platform-leaf', - hint: ANDROID_TV_MULTI_TOUCH_UNSUPPORTED_HINT, -} as const); -const androidTvDragUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-platform-leaf', - hint: TARGET_AUTHORED_DRAG_UNSUPPORTED_HINT, -} as const); +const clipboardUnsupportedProviderMode = unavailableFact('unsupported-provider-mode'); +const androidTvMultiTouchUnavailable = unavailableFact( + 'unsupported-platform-leaf', + ANDROID_TV_MULTI_TOUCH_UNSUPPORTED_HINT, +); +const androidTvDragUnavailable = unavailableFact( + 'unsupported-platform-leaf', + TARGET_AUTHORED_DRAG_UNSUPPORTED_HINT, +); /** * Gesture cells split by the interactor behind the session: an Android emulator session runs the @@ -99,16 +93,14 @@ function limrunGestureFacts( viewport: available, }); } -const tvRemoteUnavailableIos = Object.freeze({ - available: false, - reason: 'unsupported-provider-mode', - hint: 'Limrun iOS direct sessions do not expose tv remote control.', -} as const); -const tvRemoteUnavailableAndroid = Object.freeze({ - available: false, - reason: 'unsupported-device-kind', - hint: 'tv-remote is supported only on Android TV targets.', -} as const); +const tvRemoteUnavailableIos = unavailableFact( + 'unsupported-provider-mode', + 'Limrun iOS direct sessions do not expose tv remote control.', +); +const tvRemoteUnavailableAndroid = unavailableFact( + 'unsupported-device-kind', + 'tv-remote is supported only on Android TV targets.', +); /** * The retired leaf never routed `keyboard` through provider resolution at all — it dispatched * directly by device platform, bypassing the interactor/provider seam entirely. The Android leg @@ -116,27 +108,23 @@ const tvRemoteUnavailableAndroid = Object.freeze({ * iOS leg has no such reuse, so it stays honestly unavailable rather than guessing at untested * provider behavior. */ -const keyboardUnavailableIos = Object.freeze({ - available: false, - reason: 'unsupported-provider-mode', - hint: 'Limrun iOS direct sessions do not expose keyboard actions.', -} as const); -const settingsReadUnavailableIos = Object.freeze({ - available: false, - reason: 'unsupported-provider-mode', - hint: 'Limrun iOS direct sessions cannot read settings back.', -} as const); -const appSwitcherUnavailableIos = Object.freeze({ - available: false, - reason: 'unsupported-provider-mode', - hint: 'Limrun iOS direct sessions do not expose app switcher yet.', -} as const); +const keyboardUnavailableIos = unavailableFact( + 'unsupported-provider-mode', + 'Limrun iOS direct sessions do not expose keyboard actions.', +); +const settingsReadUnavailableIos = unavailableFact( + 'unsupported-provider-mode', + 'Limrun iOS direct sessions cannot read settings back.', +); +const appSwitcherUnavailableIos = unavailableFact( + 'unsupported-provider-mode', + 'Limrun iOS direct sessions do not expose app switcher yet.', +); /** No Limrun session exposes the action button; only `appSwitcher` still splits by leg. */ -const systemButtonUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-provider-mode', - hint: 'No Limrun session exposes this system button.', -} as const); +const systemButtonUnavailable = unavailableFact( + 'unsupported-provider-mode', + 'No Limrun session exposes this system button.', +); /** * The interactor-backed interaction cells a live Limrun session serves: everything here rides @@ -149,10 +137,7 @@ export function limrunInteractionOperationFacts( liveSessionUnavailable?: RuntimeOperationUnavailability, ) { const cell = liveSessionUnavailable ?? available; - const unsupportedTouch = Object.freeze({ - available: false, - reason: 'unsupported-provider-mode', - } as const); + const unsupportedTouch = unavailableFact('unsupported-provider-mode'); return Object.freeze({ ...focusRuntimeOperationFacts({ focus: cell }), ...typeTextRuntimeOperationFacts({ type: cell }), @@ -272,11 +257,10 @@ export function limrunClipboardOperationFacts( }); } -const alertUnavailableIos = Object.freeze({ - available: false, - reason: 'unsupported-provider-mode', - hint: 'Limrun iOS direct sessions do not expose alert inspection yet.', -} as const); +const alertUnavailableIos = unavailableFact( + 'unsupported-provider-mode', + 'Limrun iOS direct sessions do not expose alert inspection yet.', +); /** * `alert` splits like every other interaction leaf: the Android leg rides the local family's own @@ -324,11 +308,10 @@ export function limrunSystemButtonOperationFacts( * `fold` sends a HID hinge event through a helper the daemon host spawns inside one of its own iOS * simulators with `simctl spawn`; a Limrun session's device is not a host simulator. */ -export const LIMRUN_FOLD_UNAVAILABLE = Object.freeze({ - available: false, - reason: 'unsupported-provider-mode', - hint: 'fold runs a HID helper through simctl spawn inside a foldable iPhone simulator on the daemon host; a Limrun session has no such simulator.', -} as const); +export const LIMRUN_FOLD_UNAVAILABLE = unavailableFact( + 'unsupported-provider-mode', + 'fold runs a HID helper through simctl spawn inside a foldable iPhone simulator on the daemon host; a Limrun session has no such simulator.', +); /** The fold refusal both Limrun legs share. */ export function limrunFoldOperationFacts() { diff --git a/packages/provider-webdriver/src/platform-runtime.ts b/packages/provider-webdriver/src/platform-runtime.ts index 3af50b3564..626e4186c6 100644 --- a/packages/provider-webdriver/src/platform-runtime.ts +++ b/packages/provider-webdriver/src/platform-runtime.ts @@ -38,6 +38,7 @@ import { type RuntimeOperationUnavailability, type RuntimeOwnerRef, sameRuntimeOwner, + unavailableFact, whenAdmitted, } from '@agent-device/contracts/platform-runtime'; import type { @@ -82,60 +83,47 @@ type WebDriverPlatformDeploymentRuntime = Pick< >; const available = Object.freeze({ available: true } as const); -const appLogUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-provider-mode', -} as const); -const recordingUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-provider-mode', - hint: 'WebDriver provider runtimes do not expose screen recording.', -} as const); -const headlessUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-provider-mode', - hint: 'Headless boot is unavailable for provider-owned devices.', -} as const); -const appsUnavailable = Object.freeze({ - available: false, - reason: 'owner-capability-missing' as const, - hint: 'WebDriver provider runtimes do not expose app inventory.', -}); -const deploymentUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-provider-mode', - hint: 'This WebDriver provider runtime does not expose app deployment.', -} as const); -const pushUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-provider-mode', - hint: 'Push notifications are unavailable for WebDriver provider-owned devices.', -} as const); -const inactiveSession = Object.freeze({ - available: false, - reason: 'owner-capability-missing', - hint: 'The WebDriver provider session is no longer active for this device.', -} as const); -const snapshotUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-provider-mode', - hint: 'This WebDriver provider runtime does not expose snapshot capture for this device.', -} as const); -const screenshotUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-provider-mode', - hint: 'This WebDriver provider runtime does not expose screenshot capture for this device.', -} as const); -const snapshotCustomActionsUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-provider-mode', - hint: 'WebDriver provider runtimes do not expose iOS simulator custom snapshot actions.', -} as const); -const viewportUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-provider-mode', - hint: 'WebDriver provider runtimes do not expose viewport resizing.', -} as const); +const appLogUnavailable = unavailableFact('unsupported-provider-mode'); +const recordingUnavailable = unavailableFact( + 'unsupported-provider-mode', + 'WebDriver provider runtimes do not expose screen recording.', +); +const headlessUnavailable = unavailableFact( + 'unsupported-provider-mode', + 'Headless boot is unavailable for provider-owned devices.', +); +const appsUnavailable = unavailableFact( + 'owner-capability-missing', + 'WebDriver provider runtimes do not expose app inventory.', +); +const deploymentUnavailable = unavailableFact( + 'unsupported-provider-mode', + 'This WebDriver provider runtime does not expose app deployment.', +); +const pushUnavailable = unavailableFact( + 'unsupported-provider-mode', + 'Push notifications are unavailable for WebDriver provider-owned devices.', +); +const inactiveSession = unavailableFact( + 'owner-capability-missing', + 'The WebDriver provider session is no longer active for this device.', +); +const snapshotUnavailable = unavailableFact( + 'unsupported-provider-mode', + 'This WebDriver provider runtime does not expose snapshot capture for this device.', +); +const screenshotUnavailable = unavailableFact( + 'unsupported-provider-mode', + 'This WebDriver provider runtime does not expose screenshot capture for this device.', +); +const snapshotCustomActionsUnavailable = unavailableFact( + 'unsupported-provider-mode', + 'WebDriver provider runtimes do not expose iOS simulator custom snapshot actions.', +); +const viewportUnavailable = unavailableFact( + 'unsupported-provider-mode', + 'WebDriver provider runtimes do not expose viewport resizing.', +); /** * A point read is a local-tool operation (adb uiautomator, the XCUITest runner, the macOS @@ -143,69 +131,58 @@ const viewportUnavailable = Object.freeze({ * read and `get` answers from the captured tree; provider ownership never borrows the local * family read. */ -const focusUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-provider-mode', - hint: 'This WebDriver provider runtime does not expose focus for this device.', -} as const); -const typeUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-provider-mode', - hint: 'This WebDriver provider runtime does not expose text entry for this device.', -} as const); -const gestureUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-provider-mode', - hint: 'This WebDriver provider runtime does not expose gestures for this device.', -} as const); -const scrollUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-provider-mode', - hint: 'This WebDriver provider runtime does not expose scrolling for this device.', -} as const); -const physicalIosMultiTouchUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-device-kind', - hint: PHYSICAL_IOS_MULTI_TOUCH_UNSUPPORTED_HINT, -} as const); -const elementTextUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-provider-mode', - hint: 'WebDriver provider runtimes read element text from the captured tree only.', -} as const); -const backUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-provider-mode', - hint: 'This WebDriver provider runtime does not expose back for this device.', -} as const); -const homeUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-provider-mode', - hint: 'This WebDriver provider runtime does not expose home for this device.', -} as const); -const orientationUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-provider-mode', - hint: 'This WebDriver provider runtime does not expose orientation for this device.', -} as const); +const focusUnavailable = unavailableFact( + 'unsupported-provider-mode', + 'This WebDriver provider runtime does not expose focus for this device.', +); +const typeUnavailable = unavailableFact( + 'unsupported-provider-mode', + 'This WebDriver provider runtime does not expose text entry for this device.', +); +const gestureUnavailable = unavailableFact( + 'unsupported-provider-mode', + 'This WebDriver provider runtime does not expose gestures for this device.', +); +const scrollUnavailable = unavailableFact( + 'unsupported-provider-mode', + 'This WebDriver provider runtime does not expose scrolling for this device.', +); +const physicalIosMultiTouchUnavailable = unavailableFact( + 'unsupported-device-kind', + PHYSICAL_IOS_MULTI_TOUCH_UNSUPPORTED_HINT, +); +const elementTextUnavailable = unavailableFact( + 'unsupported-provider-mode', + 'WebDriver provider runtimes read element text from the captured tree only.', +); +const backUnavailable = unavailableFact( + 'unsupported-provider-mode', + 'This WebDriver provider runtime does not expose back for this device.', +); +const homeUnavailable = unavailableFact( + 'unsupported-provider-mode', + 'This WebDriver provider runtime does not expose home for this device.', +); +const orientationUnavailable = unavailableFact( + 'unsupported-provider-mode', + 'This WebDriver provider runtime does not expose orientation for this device.', +); /** The WebDriver interactor's own `tvRemote` always throws unsupported (no capability declares * it), so this cell is unavailable unconditionally rather than gated by interactor reachability. */ -const tvRemoteUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-provider-mode', - hint: 'WebDriver provider runtimes do not expose tv-remote.', -} as const); +const tvRemoteUnavailable = unavailableFact( + 'unsupported-provider-mode', + 'WebDriver provider runtimes do not expose tv-remote.', +); /** * `keyboardEnter`/`keyboardDismiss` call `requireSupport('keyboard')` inside the interactor, so a * provider whose declared capability map refuses them still refuses at call time; this cell states * whether the runtime has a reachable interactor to ask at all. `keyboardStatus` is iOS's gap: * the probe's result shape is Android's. */ -const keyboardUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-provider-mode', - hint: 'This WebDriver provider runtime does not expose this keyboard action for this device.', -} as const); +const keyboardUnavailable = unavailableFact( + 'unsupported-provider-mode', + 'This WebDriver provider runtime does not expose this keyboard action for this device.', +); /** * The interactor's own `readClipboard`/`writeClipboard` call `requireSupport('clipboard.read')` / @@ -213,22 +190,20 @@ const keyboardUnavailable = Object.freeze({ * refuses at call time. This cell states the seam the same way `back`/`home` do: what the fact * answers is whether this runtime has a reachable interactor to ask at all. */ -const clipboardUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-provider-mode', - hint: 'This WebDriver provider runtime does not expose clipboard access for this device.', -} as const); +const clipboardUnavailable = unavailableFact( + 'unsupported-provider-mode', + 'This WebDriver provider runtime does not expose clipboard access for this device.', +); /** * `appSwitcher` calls `requireSupport('appSwitcher')` inside the interactor, so a provider whose * declared capability map refuses the button still refuses at call time. This cell states the * seam the same way `back`/`home` do: whether this runtime has a reachable interactor to ask. */ -const appSwitcherUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-provider-mode', - hint: 'This WebDriver provider runtime does not expose the app switcher for this device.', -} as const); +const appSwitcherUnavailable = unavailableFact( + 'unsupported-provider-mode', + 'This WebDriver provider runtime does not expose the app switcher for this device.', +); /** * Unlike `home` and `appSwitcher` this is never a per-session capability question: no WebDriver @@ -236,18 +211,16 @@ const appSwitcherUnavailable = Object.freeze({ * unconditionally. There is deliberately no capability key for it, which would advertise a button * this provider can never press. */ -const systemButtonUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-provider-mode', - hint: 'No WebDriver backend presses this system button.', -} as const); +const systemButtonUnavailable = unavailableFact( + 'unsupported-provider-mode', + 'No WebDriver backend presses this system button.', +); /** No WebDriver `mobile:` script poses a foldable hinge; the refusal is unconditional like the Action Button's. */ -const foldUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-provider-mode', - hint: 'fold runs a HID helper through simctl spawn inside a foldable iPhone simulator on the daemon host; a WebDriver backend has no such simulator.', -} as const); +const foldUnavailable = unavailableFact( + 'unsupported-provider-mode', + 'fold runs a HID helper through simctl spawn inside a foldable iPhone simulator on the daemon host; a WebDriver backend has no such simulator.', +); /** * The WebDriver interactor supplies no alert members at all: ADR 0019 keeps a stub that throws @@ -255,64 +228,54 @@ const foldUnavailable = Object.freeze({ * `alert: unsupported`, so this cell is unavailable unconditionally rather than gated by * interactor reachability. */ -const alertUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-provider-mode', - hint: 'WebDriver provider runtimes do not expose native alert handling.', -} as const); +const alertUnavailable = unavailableFact( + 'unsupported-provider-mode', + 'WebDriver provider runtimes do not expose native alert handling.', +); /** * The WebDriver interactor's own `setSetting` always throws unsupported (its capability map * declares `settings: unsupported`), so this cell is unavailable unconditionally rather than * gated by interactor reachability — the same shape `tvRemote` takes. */ -const settingsUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-provider-mode', - hint: 'WebDriver provider runtimes do not expose device settings.', -} as const); +const settingsUnavailable = unavailableFact( + 'unsupported-provider-mode', + 'WebDriver provider runtimes do not expose device settings.', +); -const appEventUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-provider-mode', - hint: 'This WebDriver provider runtime does not expose app-event delivery for this device.', -} as const); +const appEventUnavailable = unavailableFact( + 'unsupported-provider-mode', + 'This WebDriver provider runtime does not expose app-event delivery for this device.', +); -const appStateUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-provider-mode', - hint: 'WebDriver provider runtimes do not expose a foreground app-state operation.', -} as const); -const audioProbeUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-provider-mode', - hint: 'This WebDriver provider runtime does not expose the audio probe.', -} as const); -const prepareUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-provider-mode', - hint: 'Apple runner preparation is unavailable for WebDriver-owned devices.', -} as const); -const openTargetUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-provider-mode', - hint: 'WebDriver open is supported only for its owned iOS or Android mobile-device sessions.', -} as const); -const closeTargetUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-provider-mode', - hint: 'WebDriver close is supported only for its owned iOS or Android mobile-device sessions.', -} as const); -const runtimeHintsUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-provider-mode', - hint: 'Runtime hints are not applied to provider-owned devices.', -} as const); -const portReverseUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-provider-mode', - hint: 'WebDriver provider runtimes do not expose port reverse.', -} as const); +const appStateUnavailable = unavailableFact( + 'unsupported-provider-mode', + 'WebDriver provider runtimes do not expose a foreground app-state operation.', +); +const audioProbeUnavailable = unavailableFact( + 'unsupported-provider-mode', + 'This WebDriver provider runtime does not expose the audio probe.', +); +const prepareUnavailable = unavailableFact( + 'unsupported-provider-mode', + 'Apple runner preparation is unavailable for WebDriver-owned devices.', +); +const openTargetUnavailable = unavailableFact( + 'unsupported-provider-mode', + 'WebDriver open is supported only for its owned iOS or Android mobile-device sessions.', +); +const closeTargetUnavailable = unavailableFact( + 'unsupported-provider-mode', + 'WebDriver close is supported only for its owned iOS or Android mobile-device sessions.', +); +const runtimeHintsUnavailable = unavailableFact( + 'unsupported-provider-mode', + 'Runtime hints are not applied to provider-owned devices.', +); +const portReverseUnavailable = unavailableFact( + 'unsupported-provider-mode', + 'WebDriver provider runtimes do not expose port reverse.', +); function webDriverLifecycleFacts(device: DeviceInfo) { const selectedMobileDevice = @@ -509,11 +472,10 @@ function declaredCapabilityCell( ): RuntimeOperationFact { if (!params.reachable) return params.whenUnreachable; if (capabilitySupported(params.capabilities, params.operation)) return available; - return Object.freeze({ - available: false, - reason: 'owner-capability-missing', - hint: unsupportedCapabilityMessage(params.capabilities, params.operation), - } as const); + return unavailableFact( + 'owner-capability-missing', + unsupportedCapabilityMessage(params.capabilities, params.operation), + ); } function webDriverFacts( @@ -720,11 +682,10 @@ function webDriverFacts( bootTarget: available, bootTargetHeadless: headlessUnavailable, listApps: appsUnavailable, - shutdownTarget: { - available: false, - reason: 'unsupported-provider-mode', - hint: 'WebDriver owns the target lifecycle for provider-owned devices.', - }, + shutdownTarget: unavailableFact( + 'unsupported-provider-mode', + 'WebDriver owns the target lifecycle for provider-owned devices.', + ), }, }); } From 62ec8067e95467f2d282306030ad8cb8b627a8ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Wed, 7 Oct 2026 22:42:18 +0200 Subject: [PATCH 2/4] refactor(provider-webdriver): drop restated facts and unused AWS options - build the inactive-session facts from the fully-unavailable table instead of restating thirty cells - remove AWS Device Farm options no caller supplies, and the LeaseValue helper that only served them Co-Authored-By: Claude Fable 5.1 --- .../provider-webdriver/src/aws-device-farm.ts | 54 +++++-------------- .../src/platform-runtime.ts | 44 ++------------- .../provider-webdriver/src/webdriver-utils.ts | 10 ---- 3 files changed, 19 insertions(+), 89 deletions(-) diff --git a/packages/provider-webdriver/src/aws-device-farm.ts b/packages/provider-webdriver/src/aws-device-farm.ts index 23e259d574..60ed2411f1 100644 --- a/packages/provider-webdriver/src/aws-device-farm.ts +++ b/packages/provider-webdriver/src/aws-device-farm.ts @@ -5,16 +5,12 @@ import { type AwsDeviceFarmArtifact, type AwsDeviceFarmArtifactGroup, } from './aws-device-farm-artifacts.ts'; -import type { - CloudWebDriverPlatform, - CloudWebDriverRuntimeOptions, - CloudWebDriverPrepareSession, -} from './runtime.ts'; -import type { DeviceLease, LeaseLifecycleContext } from '@agent-device/contracts/device'; +import type { CloudWebDriverPlatform, CloudWebDriverPrepareSession } from './runtime.ts'; +import type { LeaseLifecycleContext } from '@agent-device/contracts/device'; import { setTimeout as sleep } from 'node:timers/promises'; import { AppError } from '@agent-device/kernel/errors'; import type { RunHostCommand } from './dependencies.ts'; -import { releaseOnFailure, resolveLeaseValue, type LeaseValue } from './webdriver-utils.ts'; +import { releaseOnFailure } from './webdriver-utils.ts'; export const AWS_DEVICE_FARM_CAPABILITY_OVERRIDES = { install: { @@ -72,25 +68,16 @@ export type AwsCreateRemoteAccessSessionInput = { }; export type AwsDeviceFarmWebDriverRuntimeOptions = { - clientVersion: string; + client: AwsDeviceFarmClient; projectArn: string; deviceArn: string; - region?: string; - platform?: CloudWebDriverPlatform; - deviceName?: string; + platform: CloudWebDriverPlatform; + deviceName: string; appArn?: string; - sessionName?: LeaseValue; - webdriverCapabilities?: - | Record - | ((lease: DeviceLease) => Record); - client?: AwsDeviceFarmClient; + sessionName?: string; pollIntervalMs?: number; startupTimeoutMs?: number; interactionMode?: AwsCreateRemoteAccessSessionInput['interactionMode']; - configuration?: AwsCreateRemoteAccessSessionInput['configuration']; - deviceId?: CloudWebDriverRuntimeOptions['deviceId']; - requestPolicy?: CloudWebDriverRuntimeOptions['requestPolicy']; - prepareSession?: CloudWebDriverRuntimeOptions['prepareSession']; }; export type AwsCliDeviceFarmClientOptions = { @@ -134,25 +121,15 @@ export function createAwsCliDeviceFarmClient( } export function createAwsDeviceFarmPrepareSession( - options: Required< - Pick< - AwsDeviceFarmWebDriverRuntimeOptions, - 'client' | 'platform' | 'deviceName' | 'projectArn' | 'deviceArn' - > - > & - Omit< - AwsDeviceFarmWebDriverRuntimeOptions, - 'client' | 'platform' | 'deviceName' | 'clientVersion' - >, + options: AwsDeviceFarmWebDriverRuntimeOptions, ): CloudWebDriverPrepareSession { return async ({ lease, req, base }) => { const remoteAccess = await options.client.createRemoteAccessSession({ projectArn: options.projectArn, deviceArn: options.deviceArn, appArn: options.appArn, - name: resolveLeaseValue(options.sessionName, lease) ?? `agent-device-${lease.leaseId}`, + name: options.sessionName ?? `agent-device-${lease.leaseId}`, interactionMode: options.interactionMode, - configuration: options.configuration, }); // The ARN is a billed session from here on; any failure short of RUNNING // must stop it before surfacing, or it bills until AWS reaps it. @@ -173,20 +150,17 @@ export function createAwsDeviceFarmPrepareSession( throw error; } const deviceName = running.device?.name ?? options.deviceName; - const configured = - typeof options.webdriverCapabilities === 'function' - ? options.webdriverCapabilities(lease) - : (options.webdriverCapabilities ?? {}); const awsDefaults = options.platform === 'android' ? { 'appium:autoLaunch': false } : {}; return { ...base, endpoint, platform: options.platform, deviceName, - webdriverCapabilities: buildCloudWebDriverBaseCapabilities(options.platform, deviceName, { - ...awsDefaults, - ...configured, - }), + webdriverCapabilities: buildCloudWebDriverBaseCapabilities( + options.platform, + deviceName, + awsDefaults, + ), cleanup: async () => { await options.client.stopRemoteAccessSession(running.arn); return { awsDeviceFarmSessionArn: running.arn }; diff --git a/packages/provider-webdriver/src/platform-runtime.ts b/packages/provider-webdriver/src/platform-runtime.ts index 626e4186c6..4cf4064bf0 100644 --- a/packages/provider-webdriver/src/platform-runtime.ts +++ b/packages/provider-webdriver/src/platform-runtime.ts @@ -46,7 +46,10 @@ import type { PlatformRuntimeOperations, PlatformRuntimeOwner, } from '@agent-device/contracts/platform-runtime-operations'; -import { createUnavailablePlatformRuntimeFacts } from '@agent-device/contracts/platform-runtime-unavailable'; +import { + createFullyUnavailablePlatformRuntimeFacts, + createUnavailablePlatformRuntimeFacts, +} from '@agent-device/contracts/platform-runtime-unavailable'; import { bindProviderScreenshotInteractor, screenshotRuntimeOperationFacts, @@ -484,45 +487,8 @@ function webDriverFacts( ): RuntimeFacts { if (!webDriverSessionActive(options, device)) { return createUnavailablePlatformRuntimeFacts(device, options.owner, { - appLog: inactiveSession, - appDeployment: inactiveSession, - network: inactiveSession, - screenRecording: inactiveSession, - screenshot: inactiveSession, - viewport: inactiveSession, - focus: inactiveSession, - gesture: inactiveSession, - scroll: inactiveSession, - typeText: inactiveSession, - touch: inactiveSession, - elementText: inactiveSession, - back: inactiveSession, - orientation: inactiveSession, - tvRemote: inactiveSession, - keyboard: inactiveSession, - clipboard: inactiveSession, - systemButton: inactiveSession, + ...createFullyUnavailablePlatformRuntimeFacts(inactiveSession), fold: foldUnavailable, - triggerAppEvent: inactiveSession, - settings: inactiveSession, - readAlert: inactiveSession, - awaitAlert: inactiveSession, - acceptAlert: inactiveSession, - dismissAlert: inactiveSession, - audioProbeCapture: inactiveSession, - audioProbeQuery: inactiveSession, - perf: inactiveSession, - lifecycle: applicationLifecycleOperationFacts({ - resolveOpenTarget: inactiveSession, - prepareApplicationOpen: inactiveSession, - openApplication: inactiveSession, - applyRuntimeHints: inactiveSession, - clearRuntimeHints: inactiveSession, - closeApplication: inactiveSession, - finalizeApplicationClose: inactiveSession, - prepareAppleRunner: inactiveSession, - configureProviderPortReverse: inactiveSession, - }), }); } const deployment = options.deployment?.fact(device) ?? deploymentUnavailable; diff --git a/packages/provider-webdriver/src/webdriver-utils.ts b/packages/provider-webdriver/src/webdriver-utils.ts index c4dae20c1f..64351c4fbc 100644 --- a/packages/provider-webdriver/src/webdriver-utils.ts +++ b/packages/provider-webdriver/src/webdriver-utils.ts @@ -2,7 +2,6 @@ import fs from 'node:fs'; import { readFile, stat } from 'node:fs/promises'; import path from 'node:path'; import type { - DeviceLease, LeaseLifecycleContext, ProviderDeviceInstallOptions, ProviderDeviceInstallResult, @@ -14,8 +13,6 @@ import { import { AppError, errorMessage } from '@agent-device/kernel/errors'; import { agentDeviceRequestHeaders } from './request-headers.ts'; -export type LeaseValue = T | ((lease: DeviceLease) => T); - /** Best-effort release after a failure; a failed release rides along as `cleanupError`, never masks the primary. */ export async function releaseOnFailure( primaryError: unknown, @@ -30,13 +27,6 @@ export async function releaseOnFailure( } } -export function resolveLeaseValue( - value: LeaseValue | undefined, - lease: DeviceLease, -): T | undefined { - return typeof value === 'function' ? (value as (lease: DeviceLease) => T)(lease) : value; -} - export function basicAuthHeader(credentials: { username: string; accessKey: string }): string { return `Basic ${Buffer.from(`${credentials.username}:${credentials.accessKey}`).toString('base64')}`; } From c754951ec095b1b98015d4fdaf006961c6bb9aa1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Thu, 8 Oct 2026 10:03:32 +0200 Subject: [PATCH 3/4] refactor(provider-limrun): build the facts and deployment refusals through unavailableFact Co-Authored-By: Claude Fable 5.1 --- .../provider-limrun/src/deployment-runtime.ts | 23 ++-- packages/provider-limrun/src/facts-runtime.ts | 119 ++++++++---------- 2 files changed, 65 insertions(+), 77 deletions(-) diff --git a/packages/provider-limrun/src/deployment-runtime.ts b/packages/provider-limrun/src/deployment-runtime.ts index b56129f1ed..a03ffd1753 100644 --- a/packages/provider-limrun/src/deployment-runtime.ts +++ b/packages/provider-limrun/src/deployment-runtime.ts @@ -9,7 +9,10 @@ import type { PlatformRuntimeHost, PlatformRuntimeOperations, } from '@agent-device/contracts/platform-runtime-operations'; -import type { RuntimeOperationFact } from '@agent-device/contracts/platform-runtime'; +import { + type RuntimeOperationFact, + unavailableFact, +} from '@agent-device/contracts/platform-runtime'; import type { ProviderDeviceInstallResult } from '@agent-device/contracts/device'; import type { DeviceInfo } from '@agent-device/kernel/device'; import { AppError } from '@agent-device/kernel/errors'; @@ -17,16 +20,14 @@ import type { LimrunRequestOperationDrain } from './request-cancellation.ts'; import { isSupportedLimrunRuntimeDevice } from './runtime-device.ts'; const available = Object.freeze({ available: true } as const); -const deploymentUnavailable = Object.freeze({ - available: false, - reason: 'owner-capability-missing', - hint: 'The Limrun provider session is no longer active for this device.', -} as const); -const pushUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-provider-mode', - hint: 'Push notifications are unavailable for Limrun provider-owned devices.', -} as const); +const deploymentUnavailable = unavailableFact( + 'owner-capability-missing', + 'The Limrun provider session is no longer active for this device.', +); +const pushUnavailable = unavailableFact( + 'unsupported-provider-mode', + 'Push notifications are unavailable for Limrun provider-owned devices.', +); export type LimrunAppDeploymentRuntimeOptions = Readonly<{ host: PlatformRuntimeHost; diff --git a/packages/provider-limrun/src/facts-runtime.ts b/packages/provider-limrun/src/facts-runtime.ts index 8adff5cfd0..c7b6357567 100644 --- a/packages/provider-limrun/src/facts-runtime.ts +++ b/packages/provider-limrun/src/facts-runtime.ts @@ -2,7 +2,7 @@ import { isIosFamily, type DeviceInfo } from '@agent-device/kernel/device'; import { applicationLifecycleOperationFacts } from '@agent-device/contracts/application-lifecycle-runtime'; import { elementTextRuntimeOperationFacts } from '@agent-device/contracts/element-text-runtime'; import type { PlatformRuntimeOperations } from '@agent-device/contracts/platform-runtime-operations'; -import type { RuntimeFacts } from '@agent-device/contracts/platform-runtime'; +import { type RuntimeFacts, unavailableFact } from '@agent-device/contracts/platform-runtime'; import { screenshotRuntimeOperationFacts } from '@agent-device/contracts/screenshot-runtime'; import { selectorObservationRuntimeOperationFacts } from '@agent-device/contracts/selector-observation-runtime'; import { snapshotRuntimeOperationFacts } from '@agent-device/contracts/snapshot-runtime'; @@ -28,73 +28,61 @@ import { } from './interaction-operations.ts'; const available = Object.freeze({ available: true } as const); -const customSnapshotUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-provider-mode', - hint: 'Custom snapshot actions are available only for Limrun iOS simulator sessions.', -} as const); -const viewportUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-provider-mode', - hint: 'Limrun does not expose viewport resizing.', -} as const); +const customSnapshotUnavailable = unavailableFact( + 'unsupported-provider-mode', + 'Custom snapshot actions are available only for Limrun iOS simulator sessions.', +); +const viewportUnavailable = unavailableFact( + 'unsupported-provider-mode', + 'Limrun does not expose viewport resizing.', +); /** * A point read needs a local tool (adb uiautomator, the XCUITest runner). Limrun's transport * carries none of them, so the owner reports no live read and `get` answers from the captured * tree; provider ownership never borrows the local family read. */ -const elementTextUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-provider-mode', - hint: 'Limrun-owned devices read element text from the captured tree only.', -} as const); -const headlessUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-provider-mode', - hint: 'Headless boot is unavailable for provider-owned devices.', -} as const); +const elementTextUnavailable = unavailableFact( + 'unsupported-provider-mode', + 'Limrun-owned devices read element text from the captured tree only.', +); +const headlessUnavailable = unavailableFact( + 'unsupported-provider-mode', + 'Headless boot is unavailable for provider-owned devices.', +); /** Also read outside this module's own facts assembly: the owner's `inspectFacts` reports this * for every operation when the request names a device with no matching live session at all. */ -export const liveSessionUnavailable = Object.freeze({ - available: false, - reason: 'owner-capability-missing', - hint: 'Limrun requires a matching live provider session for this device.', -} as const); -const prepareUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-provider-mode', - hint: 'Apple runner preparation is unavailable for Limrun-owned devices.', -} as const); -const iosAppStateUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-provider-mode', - hint: 'Limrun iOS appstate is session-owned; no sessionless provider foreground probe is exposed.', -} as const); -const openTargetUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-provider-mode', - hint: 'Limrun open requires a Limrun-owned iOS simulator or Android emulator.', -} as const); -const closeTargetUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-provider-mode', - hint: 'Limrun close requires a Limrun-owned iOS simulator or Android emulator.', -} as const); -const runtimeHintsUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-provider-mode', - hint: 'Runtime hints are not applied to provider-owned devices.', -} as const); -const portReverseUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-provider-mode', - hint: 'Limrun port reverse requires an active Android Limrun session.', -} as const); -const audioProbeUnavailable = Object.freeze({ - available: false, - reason: 'unsupported-provider-mode', - hint: 'Limrun does not expose the audio probe.', -} as const); +export const liveSessionUnavailable = unavailableFact( + 'owner-capability-missing', + 'Limrun requires a matching live provider session for this device.', +); +const prepareUnavailable = unavailableFact( + 'unsupported-provider-mode', + 'Apple runner preparation is unavailable for Limrun-owned devices.', +); +const iosAppStateUnavailable = unavailableFact( + 'unsupported-provider-mode', + 'Limrun iOS appstate is session-owned; no sessionless provider foreground probe is exposed.', +); +const openTargetUnavailable = unavailableFact( + 'unsupported-provider-mode', + 'Limrun open requires a Limrun-owned iOS simulator or Android emulator.', +); +const closeTargetUnavailable = unavailableFact( + 'unsupported-provider-mode', + 'Limrun close requires a Limrun-owned iOS simulator or Android emulator.', +); +const runtimeHintsUnavailable = unavailableFact( + 'unsupported-provider-mode', + 'Runtime hints are not applied to provider-owned devices.', +); +const portReverseUnavailable = unavailableFact( + 'unsupported-provider-mode', + 'Limrun port reverse requires an active Android Limrun session.', +); +const audioProbeUnavailable = unavailableFact( + 'unsupported-provider-mode', + 'Limrun does not expose the audio probe.', +); /** Also read outside this module's own facts assembly: the owner's `bind` needs the same * deployment options its facts do. */ @@ -214,11 +202,10 @@ export function limrunAppLogFacts( bootTarget: available, bootTargetHeadless: headlessUnavailable, listApps: available, - shutdownTarget: { - available: false, - reason: 'unsupported-provider-mode', - hint: 'Limrun owns the target lifecycle for provider-owned devices.', - }, + shutdownTarget: unavailableFact( + 'unsupported-provider-mode', + 'Limrun owns the target lifecycle for provider-owned devices.', + ), ...limrunLifecycleFacts(device, true), }, }); From bf64256faea0e046368b210bd9a4e23ba253af90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Thu, 8 Oct 2026 12:20:14 +0200 Subject: [PATCH 4/4] refactor(provider-webdriver): one inactive-session fact shared by the runtime and deployment facts Co-Authored-By: Claude Fable 5.1 --- .../src/platform-runtime.ts | 7 ++--- .../src/runtime-deployment.ts | 28 ++++++++++--------- 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/packages/provider-webdriver/src/platform-runtime.ts b/packages/provider-webdriver/src/platform-runtime.ts index 4cf4064bf0..180b0b3c70 100644 --- a/packages/provider-webdriver/src/platform-runtime.ts +++ b/packages/provider-webdriver/src/platform-runtime.ts @@ -71,7 +71,7 @@ import type { Interactor, RunnerContext } from '@agent-device/contracts/interact import { readRecentNetworkTrafficFromText } from '@agent-device/capture-kit'; import type { DeviceInfo } from '@agent-device/kernel/device'; import { AppError } from '@agent-device/kernel/errors'; -import type { WebDriverDeploymentRuntime } from './runtime-deployment.ts'; +import { type WebDriverDeploymentRuntime, webDriverInactiveSession } from './runtime-deployment.ts'; import { bindWebDriverApplicationLifecycle } from './lifecycle.ts'; import { capabilitySupported, @@ -107,10 +107,7 @@ const pushUnavailable = unavailableFact( 'unsupported-provider-mode', 'Push notifications are unavailable for WebDriver provider-owned devices.', ); -const inactiveSession = unavailableFact( - 'owner-capability-missing', - 'The WebDriver provider session is no longer active for this device.', -); +const inactiveSession = webDriverInactiveSession; const snapshotUnavailable = unavailableFact( 'unsupported-provider-mode', 'This WebDriver provider runtime does not expose snapshot capture for this device.', diff --git a/packages/provider-webdriver/src/runtime-deployment.ts b/packages/provider-webdriver/src/runtime-deployment.ts index fb2a058b86..56489795b1 100644 --- a/packages/provider-webdriver/src/runtime-deployment.ts +++ b/packages/provider-webdriver/src/runtime-deployment.ts @@ -7,7 +7,16 @@ import type { AppDeploymentResult, DeployMaterializedAppInput, } from '@agent-device/contracts/app-deployment-runtime'; -import type { RuntimeOperationFact } from '@agent-device/contracts/platform-runtime'; +import { + type RuntimeOperationFact, + unavailableFact, +} from '@agent-device/contracts/platform-runtime'; + +/** The refusal every WebDriver cell reports once the provider session is gone. */ +export const webDriverInactiveSession = unavailableFact( + 'owner-capability-missing', + 'The WebDriver provider session is no longer active for this device.', +); import { publicPlatformString, type DeviceInfo } from '@agent-device/kernel/device'; import { AppError } from '@agent-device/kernel/errors'; import { capabilitySupported, unsupportedCapabilityMessage } from './capabilities.ts'; @@ -117,19 +126,12 @@ export function createWebDriverDeploymentRuntime( } function deploymentFact(session: WebDriverProviderSession | undefined): RuntimeOperationFact { - if (!session) { - return Object.freeze({ - available: false, - reason: 'owner-capability-missing', - hint: 'The WebDriver provider session is no longer active for this device.', - } as const); - } + if (!session) return webDriverInactiveSession; if (!capabilitySupported(session.capabilities, 'install')) { - return Object.freeze({ - available: false, - reason: 'owner-capability-missing', - hint: unsupportedCapabilityMessage(session.capabilities, 'install'), - } as const); + return unavailableFact( + 'owner-capability-missing', + unsupportedCapabilityMessage(session.capabilities, 'install'), + ); } return Object.freeze({ available: true } as const); }