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
23 changes: 12 additions & 11 deletions packages/provider-limrun/src/deployment-runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,24 +9,25 @@ 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';
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;
Expand Down
119 changes: 53 additions & 66 deletions packages/provider-limrun/src/facts-runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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. */
Expand Down Expand Up @@ -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),
},
});
Expand Down
119 changes: 51 additions & 68 deletions packages/provider-limrun/src/interaction-operations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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
Expand All @@ -99,44 +93,38 @@ 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
* genuinely carries it (the same `createAndroidInteractor` factory the local family binds); the
* 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
Expand All @@ -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 }),
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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() {
Expand Down
Loading
Loading