refactor(runtime): let platform runtimes list apps and read app state directly (#2295)

* refactor(runtime): let platform runtimes list apps and read app state directly

The root host carried two adapters, appInventory and appState, that only
forwarded a platform call back into that platform's own package. Each platform
runtime now performs its own listApps and appState call through a lazy import
inside its package, keeping the deferred load, the AbortSignal threading, and
the package/bundleId -> id rename. PlatformRuntimeHost loses both keys, so
Android, Apple and Harmony fixtures no longer stub the two platforms they do
not own.

Android is the one platform runtime whose package now reaches adb directly.
The adb host that adb mechanics require is bound by a module side effect that
only the root can perform, so the Android runtime-module registration binds it
before the module loads. loadAndroidMechanics keeps its own binding import for
the root host ports that reach mechanics without binding a runtime; neither
binder subsumes the other.

Android appstate now runs one foreground-focus loop instead of two. The host
shaped readAndroidAppState/AndroidAppStateHost pair is gone: limrun's adapter
already closes over its own adb executor, so it calls the executor variant
directly, and that variant took the per-attempt abort check the host variant
had. AppStateRuntimeCommand and AppStateRuntimeCommandResult described the
deleted host port and go with it.

Tests: the new ordering test in
src/platform-runtime-android-adb-binding.test.ts was seen red by deleting the
binding import from that registration (order came back
["android-runtime", "adb-host"]); the composed-gateway listApps test in the
same file was seen red by reverting the Android runtime's inlined listApps to a
host.appInventory lookup (TypeError reading 'android'); the new abort test in
packages/platform-android/src/app-state.test.ts was seen red by removing both
signal?.throwIfAborted() calls from readAndroidFocusWithExecutor (the second
dumpsys was issued and the call resolved). All green after.

* chore(gates): drop the retired app-inventory/app-state host allowances

The two PLATFORM_RUNTIME_HOST_FILES rows point at host files this change
deletes, and the ./platform-runtime-app-state-host.ts composition allowance has
no importer left.

* refactor(runtime): construct the Android runtime module with its adb host binding

The Android runtime now calls adb from inside its package for listApps and
appState, which needs the process-wide adb host port bound. That dependency
was hidden in a registry wrapper doing a side-effect import, with a paragraph
explaining why it and loadAndroidMechanics did not subsume each other and an
import-order test pinning the ordering. The package now declares the
dependency: createAndroidRuntimeModule({ bindAdbHost }) awaits the binding
before the runtime loads, and the composition root supplies the one binding
implementation (evaluating its adb host module). The wrapper, the paragraph
and the import-order test are gone; the routed listApps test stays and a
routed appState test joins it.
This commit is contained in:
Michał Pierzchała
2026-09-05 22:40:42 +02:00
committed by GitHub
parent ba6c818d81
commit 0c8227e9b7
33 changed files with 286 additions and 516 deletions
@@ -14,27 +14,3 @@ export type ListAppsInput = Readonly<{
export type AppInventoryRuntimeOperations = Readonly<{
listApps(input: ListAppsInput): Promise<readonly InstalledAppInfo[]>;
}>;
export type AppInventoryRuntimeHost = Readonly<{
apple: Readonly<{
listApps(
device: DeviceInfo,
filter: AppsFilter,
signal: AbortSignal,
): Promise<readonly InstalledAppInfo[]>;
}>;
android: Readonly<{
listApps(
device: DeviceInfo,
filter: AppsFilter,
signal: AbortSignal,
): Promise<readonly InstalledAppInfo[]>;
}>;
harmonyos: Readonly<{
listApps(
device: DeviceInfo,
filter: AppsFilter,
signal: AbortSignal,
): Promise<readonly InstalledAppInfo[]>;
}>;
}>;
@@ -1,38 +1,9 @@
import type { DeviceInfo } from '@agent-device/kernel/device';
/** Neutral foreground identity returned by a selected platform/provider runtime. */
export type AppStateRuntimeResult = Readonly<{
package?: string;
activity?: string;
}>;
export type AppStateRuntimeCommand = Readonly<{
args: readonly string[];
allowFailure?: boolean;
timeoutMs?: number;
}>;
export type AppStateRuntimeCommandResult = Readonly<{
stdout: string;
}>;
export type AppStateRuntimeOperations = Readonly<{
appState(): Promise<AppStateRuntimeResult>;
}>;
export type AppStateRuntimeHost = Readonly<{
android: Readonly<{
run(
device: DeviceInfo,
command: AppStateRuntimeCommand,
signal: AbortSignal,
): Promise<AppStateRuntimeCommandResult>;
}>;
harmonyos: Readonly<{
run(
device: DeviceInfo,
command: AppStateRuntimeCommand,
signal: AbortSignal,
): Promise<AppStateRuntimeCommandResult>;
}>;
}>;
@@ -1,14 +1,11 @@
import type { AppLogRuntimeHost, AppLogRuntimeOperations } from './app-log-runtime.ts';
import type {
AppInventoryRuntimeHost,
AppInventoryRuntimeOperations,
} from './app-inventory-runtime.ts';
import type { AppInventoryRuntimeOperations } from './app-inventory-runtime.ts';
import type {
AndroidAppDeploymentExecutor,
AppDeploymentRuntimeOperations,
AppleAppDeploymentExecutor,
} from './app-deployment-runtime.ts';
import type { AppStateRuntimeHost, AppStateRuntimeOperations } from './app-state-runtime.ts';
import type { AppStateRuntimeOperations } from './app-state-runtime.ts';
import type { NetworkRuntimeHost, NetworkRuntimeOperations } from './network-runtime.ts';
import type { ScreenRecordingRuntimeHost } from './screen-recording-runtime-host.ts';
import type { ScreenRecordingRuntimeOperations } from './screen-recording-runtime.ts';
@@ -721,8 +718,6 @@ export const keyboardRuntimePlanUses = Object.freeze([
export type PlatformRuntimeHost = AppLogRuntimeHost &
NetworkRuntimeHost &
Readonly<{
appInventory: AppInventoryRuntimeHost;
appState: AppStateRuntimeHost;
/** Focused native ports; deployment semantics remain in the owning family packages. */
appleDeployment: AppleAppDeploymentExecutor;
androidDeployment: AndroidAppDeploymentExecutor;
@@ -1,5 +1,5 @@
import { expect, test } from 'vitest';
import { parseAndroidForegroundApp } from './app-state.ts';
import { parseAndroidForegroundApp, readAndroidAppStateWithExecutor } from './app-state.ts';
test('parses Android window and activity foreground markers', () => {
expect(
@@ -28,3 +28,18 @@ test('scans repeated uncontrolled focus text without regular-expression backtrac
parseAndroidForegroundApp(`ResumedActivity:${'ResumedActivity:a'.repeat(20_000)}`),
).toBeNull();
});
test('stops between dumpsys attempts once the request is aborted', async () => {
const controller = new AbortController();
const issued: string[][] = [];
const run = async (args: string[]) => {
issued.push(args);
controller.abort(new Error('request canceled'));
return { exitCode: 0, stdout: 'mCurrentFocus=Window{1 u0 StatusBar}', stderr: '' };
};
await expect(readAndroidAppStateWithExecutor(run, controller.signal)).rejects.toThrow(
'request canceled',
);
expect(issued).toEqual([['shell', 'dumpsys', 'window', 'windows']]);
});
+7 -45
View File
@@ -1,19 +1,6 @@
import type {
AppStateRuntimeCommand,
AppStateRuntimeCommandResult,
AppStateRuntimeResult,
} from '@agent-device/contracts/app-state-runtime';
import type { DeviceInfo } from '@agent-device/kernel/device';
import type { AppStateRuntimeResult } from '@agent-device/contracts/app-state-runtime';
import { parseAndroidFocusSegment } from './app-parsers.ts';
export type AndroidAppStateHost = Readonly<{
run(
device: DeviceInfo,
command: AppStateRuntimeCommand,
signal: AbortSignal,
): Promise<AppStateRuntimeCommandResult>;
}>;
const FOCUS_COMMANDS = [
['shell', 'dumpsys', 'window', 'windows'],
['shell', 'dumpsys', 'window'],
@@ -29,11 +16,12 @@ export type AndroidCommandExecutor = (
export async function readAndroidAppStateWithExecutor(
run: AndroidCommandExecutor,
signal?: AbortSignal,
): Promise<AppStateRuntimeResult> {
const windowFocus = await readAndroidFocusWithExecutor(run, FOCUS_COMMANDS);
const windowFocus = await readAndroidFocusWithExecutor(run, FOCUS_COMMANDS, signal);
if (windowFocus) return windowFocus;
const activityFocus = await readAndroidFocusWithExecutor(run, ACTIVITY_COMMANDS);
const activityFocus = await readAndroidFocusWithExecutor(run, ACTIVITY_COMMANDS, signal);
if (activityFocus) return activityFocus;
return {};
}
@@ -41,48 +29,22 @@ export async function readAndroidAppStateWithExecutor(
async function readAndroidFocusWithExecutor(
run: AndroidCommandExecutor,
commands: readonly (readonly string[])[],
signal?: AbortSignal,
): Promise<AppStateRuntimeResult | null> {
for (const args of commands) {
signal?.throwIfAborted();
const result = await run([...args], { allowFailure: true });
signal?.throwIfAborted();
const parsed = parseAndroidForegroundApp(result.stdout ?? '');
if (parsed) return parsed;
}
return null;
}
export async function readAndroidAppState(
host: AndroidAppStateHost,
device: DeviceInfo,
signal: AbortSignal,
): Promise<AppStateRuntimeResult> {
const windowFocus = await readAndroidFocus(host, device, FOCUS_COMMANDS, signal);
if (windowFocus) return windowFocus;
const activityFocus = await readAndroidFocus(host, device, ACTIVITY_COMMANDS, signal);
if (activityFocus) return activityFocus;
return {};
}
export function parseAndroidForegroundApp(text: string): AppStateRuntimeResult | null {
return parseAndroidFocusSegment(text, (segment) => parseAndroidComponentFromSegment(segment));
}
async function readAndroidFocus(
host: AndroidAppStateHost,
device: DeviceInfo,
commands: readonly (readonly string[])[],
signal: AbortSignal,
): Promise<AppStateRuntimeResult | null> {
for (const args of commands) {
signal.throwIfAborted();
const result = await host.run(device, { args, allowFailure: true }, signal);
signal.throwIfAborted();
const parsed = parseAndroidForegroundApp(result.stdout);
if (parsed) return parsed;
}
return null;
}
function parseAndroidComponentFromSegment(segment: string): AppStateRuntimeResult | null {
const match = segment.match(/\b([A-Za-z][A-Za-z0-9_]*(?:\.[A-Za-z0-9_]+)+)\/([A-Za-z0-9_.$]+)/);
return match?.[1] && match[2] ? { package: match[1], activity: match[2] } : null;
+25 -25
View File
@@ -1,16 +1,11 @@
import type {
AppStateRuntimeHost,
AppStateRuntimeResult,
} from '@agent-device/contracts/app-state-runtime';
import type { AppStateRuntimeResult } from '@agent-device/contracts/app-state-runtime';
import type {
InventoryPlatformModule,
PlatformModuleMetadata,
} from '@agent-device/contracts/platform-module';
import type { PlatformRuntimeModule } from '@agent-device/contracts/platform-runtime-operations';
import type { DeviceShutdownRuntimeDependencies } from '@agent-device/contracts/device-shutdown-runtime';
import type { DeviceInfo } from '@agent-device/kernel/device';
import type { AndroidInventoryConfig } from './inventory-config.ts';
import type { AndroidAppStateHost } from './app-state.ts';
import type {
AndroidObservationAdapter,
AndroidObservationHost,
@@ -21,7 +16,6 @@ const metadata = Object.freeze({
} satisfies PlatformModuleMetadata);
export type { AndroidInventoryConfig } from './inventory-config.ts';
export type { AndroidAppStateHost } from './app-state.ts';
/** Package-owned Android observation policy, loaded only when a daemon request needs it. */
export function createAndroidObservationAdapter(
@@ -46,29 +40,35 @@ export function createAndroidObservationAdapter(
});
}
export async function readAndroidAppState(
host: AndroidAppStateHost | AppStateRuntimeHost['android'],
device: DeviceInfo,
signal: AbortSignal,
): Promise<AppStateRuntimeResult> {
const { readAndroidAppState: read } = await import('./app-state.ts');
return await read(host, device, signal);
}
export async function readAndroidAppStateWithExecutor(
run: import('./app-state.ts').AndroidCommandExecutor,
): Promise<import('@agent-device/contracts/app-state-runtime').AppStateRuntimeResult> {
signal?: AbortSignal,
): Promise<AppStateRuntimeResult> {
const { readAndroidAppStateWithExecutor: read } = await import('./app-state.ts');
return await read(run);
return await read(run, signal);
}
export const runtimeModule = Object.freeze({
...metadata,
loadRuntime: async (host) => {
const { createAndroidPlatformRuntime } = await import('./runtime.ts');
return createAndroidPlatformRuntime(host);
},
} satisfies PlatformRuntimeModule);
/** What the composition root supplies before this package's runtime can reach a device. */
export type AndroidRuntimeModuleDependencies = Readonly<{
/**
* Binds the process-wide adb host port (`bindAndroidAdbHost`) the runtime's mechanics run
* through. Awaited before the runtime loads, so no caller has to import anything first.
*/
bindAdbHost(): Promise<void>;
}>;
export function createAndroidRuntimeModule(
dependencies: AndroidRuntimeModuleDependencies,
): PlatformRuntimeModule {
return Object.freeze({
...metadata,
loadRuntime: async (host) => {
await dependencies.bindAdbHost();
const { createAndroidPlatformRuntime } = await import('./runtime.ts');
return createAndroidPlatformRuntime(host);
},
} satisfies PlatformRuntimeModule);
}
export function createAndroidInventoryModule(
config: AndroidInventoryConfig,
@@ -72,7 +72,6 @@ export async function listAndroidAppsWithAdb(
export {
closeAndroidApp,
isAmStartError,
listAndroidApps,
openAndroidApp,
openAndroidDevice,
parseAndroidLaunchComponent,
@@ -150,11 +150,6 @@ function host(options: {
readProcessMarker: async () => options.marker,
},
networkTransports: { resolve: async () => ({ mode: 'local' }) },
appInventory: {
apple: { listApps: async () => [] },
android: { listApps: async () => [] },
harmonyos: { listApps: async () => [] },
},
};
}
@@ -192,16 +187,7 @@ function unusedAppLogHost(): Omit<
terminate: async () => 'already-missing',
},
processTransports: { resolve: async () => ({ mode: 'local' }) },
appInventory: {
apple: { listApps: async () => [] },
android: { listApps: async () => [] },
harmonyos: { listApps: async () => [] },
},
clock: { now: () => 1, sleep: async () => {} },
appState: {
android: { run: async () => ({ stdout: '' }) },
harmonyos: { run: async () => ({ stdout: '' }) },
},
deviceReadiness: {
applePhysical: { ensureConnected: async () => {} },
appleAutomation: {
@@ -7,11 +7,36 @@ vi.mock('./logs/runtime.ts', async (loadOriginal) => {
return await loadOriginal();
});
import { runtimeModule } from './index.ts';
import { createAndroidRuntimeModule } from './index.ts';
test('defers Android app-log mechanics until runtime load', async () => {
const runtimeModule = createAndroidRuntimeModule({ bindAdbHost: async () => {} });
expect(mechanics.evaluations).toBe(0);
expect(runtimeModule.family).toBe('android');
await runtimeModule.loadRuntime({} as never);
expect(mechanics.evaluations).toBe(1);
});
test('binds the adb host it was constructed with before the runtime loads', async () => {
const order: string[] = [];
const bindAdbHost = vi.fn(async () => {
order.push('bind-adb-host');
});
const runtimeModule = createAndroidRuntimeModule({ bindAdbHost });
expect(bindAdbHost).not.toHaveBeenCalled();
await runtimeModule.loadRuntime({} as never);
order.push('runtime-loaded');
expect(order).toEqual(['bind-adb-host', 'runtime-loaded']);
});
test('a binding that fails keeps the runtime unloaded', async () => {
const runtimeModule = createAndroidRuntimeModule({
bindAdbHost: async () => {
throw new Error('adb host unavailable');
},
});
await expect(runtimeModule.loadRuntime({} as never)).rejects.toThrow('adb host unavailable');
});
@@ -39,17 +39,6 @@ const audioProbeHost: PlatformRuntimeHost['audioProbe'] = {
ownedProcesses: { replace: () => {}, clear: () => {} },
};
export const emptyAppInventory = {
apple: { listApps: async () => [] },
android: { listApps: async () => [] },
harmonyos: { listApps: async () => [] },
};
const emptyAppState = {
android: { run: async () => ({ stdout: '' }) },
harmonyos: { run: async () => ({ stdout: '' }) },
};
function localAndroidScreenRecording() {
return {
mode: 'local' as const,
@@ -72,7 +61,6 @@ export function androidRuntimeHost(overrides: Record<string, unknown> = {}): Pla
return {
androidTools: { probeClipboardShellSupport: async () => 'supported' as const },
processTransports: { resolve: async () => ({ mode: 'local' as const }) },
appInventory: emptyAppInventory,
localInteractors: { resolve: async () => ({}) },
audioProbe: audioProbeHost,
screenRecording: { android: { resolve: async () => localAndroidScreenRecording() } },
@@ -89,7 +77,6 @@ export function androidNavigationHost(
probeClipboardShellSupport,
runAdb: async () => ({ stdout: '', stderr: '', exitCode: 0 }),
},
appState: emptyAppState,
deviceReadiness: { android: { ensureReady: async (selected: DeviceInfo) => selected } },
});
}
+24 -19
View File
@@ -7,13 +7,13 @@ import type {
} from '@agent-device/contracts/platform-runtime-operations';
import type { DeviceInfo } from '@agent-device/kernel/device';
import { createAndroidPlatformRuntime } from './runtime.ts';
import { bindAndroidAdbHostStub } from './adb-host.fixtures.ts';
import {
ANDROID_EMULATOR,
UNKNOWN_KIND_DEVICE,
androidNavigationHost,
androidRuntimeHost,
bindOrdinary,
emptyAppInventory,
} from './runtime.fixtures.ts';
const appStateUnavailable = {
@@ -27,10 +27,20 @@ test.each([
['device', { ...ANDROID_EMULATOR, kind: 'device' as const }],
['unknown', UNKNOWN_KIND_DEVICE],
])('classifies the Android %s runtime denominator', async (_name, runtimeDevice) => {
const listApps = vi.fn(async () => [{ id: 'com.example.app', name: 'Example' }]);
const appState = vi.fn(async () => ({
stdout: 'mCurrentFocus=Window{1 u0 com.example.app/.MainActivity}',
}));
const execSerialAdb = vi.fn(async (_serial: string, args: string[]) => {
if (args.includes('query-activities')) {
return { stdout: 'com.example.app/.MainActivity\n', stderr: '', exitCode: 0 };
}
if (args.includes('dumpsys')) {
return {
stdout: 'mCurrentFocus=Window{1 u0 com.example.app/.MainActivity}',
stderr: '',
exitCode: 0,
};
}
return { stdout: '', stderr: '', exitCode: 0 };
});
bindAndroidAdbHostStub({ execSerialAdb });
const host = androidRuntimeHost({
commands: {
which: async () => 'tool',
@@ -38,11 +48,6 @@ test.each([
},
toolchains: { prepare: async () => {} },
clock: { now: () => 1, sleep: async () => {} },
appInventory: { ...emptyAppInventory, android: { listApps } },
appState: {
android: { run: appState },
harmonyos: { run: async () => ({ stdout: '' }) },
},
deviceReadiness: {
applePhysical: { ensureConnected: async () => {} },
appleAutomation: {
@@ -95,7 +100,11 @@ test.each([
await expect(
binding.operations.listApps?.({ device: runtimeDevice, filter: 'all' }),
).resolves.toEqual([{ id: 'com.example.app', name: 'Example' }]);
expect(listApps).toHaveBeenCalledWith(runtimeDevice, 'all', expect.any(AbortSignal));
expect(execSerialAdb).toHaveBeenCalledWith(
runtimeDevice.id,
expect.arrayContaining(['query-activities']),
expect.objectContaining({ allowFailure: true }),
);
await expect(binding.operations.bootTarget?.({})).resolves.toMatchObject({
id: runtimeDevice.id,
@@ -105,10 +114,10 @@ test.each([
package: 'com.example.app',
activity: '.MainActivity',
});
expect(appState).toHaveBeenCalledWith(
runtimeDevice,
{ args: ['shell', 'dumpsys', 'window', 'windows'], allowFailure: true },
expect.any(AbortSignal),
expect(execSerialAdb).toHaveBeenCalledWith(
runtimeDevice.id,
['shell', 'dumpsys', 'window', 'windows'],
expect.objectContaining({ allowFailure: true }),
);
if (runtimeDevice.kind === 'emulator') {
@@ -124,10 +133,6 @@ test.each([
test('rejects the non-discovered Android simulator cell for appstate', async () => {
const runtimeDevice = { ...ANDROID_EMULATOR, kind: 'simulator' as const };
const host = androidRuntimeHost({
appState: {
android: { run: async () => ({ stdout: '' }) },
harmonyos: { run: async () => ({ stdout: '' }) },
},
deviceReadiness: { android: { ensureReady: async (selected: DeviceInfo) => selected } },
});
const binding = await bindOrdinary(createAndroidPlatformRuntime(host), runtimeDevice);
+20 -12
View File
@@ -53,7 +53,7 @@ import { createAndroidAppLogRuntime } from './logs/runtime.ts';
import { dumpAndroidNetworkTraffic } from './network/runtime.ts';
import { bindAndroidScreenRecordingRuntime } from './recording/runtime.ts';
import { ensureAndroidReady } from './readiness/runtime.ts';
import { readAndroidAppState } from './app-state.ts';
import { readAndroidAppStateWithExecutor } from './app-state.ts';
import { bindAndroidApplicationLifecycle } from './lifecycle.ts';
import type { AndroidClipboardShellSupport } from '@agent-device/contracts/android-clipboard-support';
import {
@@ -419,12 +419,18 @@ export function createAndroidPlatformRuntime(host: PlatformRuntimeHost): Platfor
}),
...(facts.operations.appState.available
? {
appState: async () =>
await readAndroidAppState(
host.appState.android,
request.device,
appState: async () => {
request.scope.signal.throwIfAborted();
const { runAndroidAdb } = await import('./adb.ts');
return await readAndroidAppStateWithExecutor(
async (args, options) =>
await runAndroidAdb(request.device, args, {
...options,
signal: request.scope.signal,
}),
request.scope.signal,
),
);
},
}
: {}),
networkDump: async (input: NetworkDumpInput) =>
@@ -470,12 +476,14 @@ export function createAndroidPlatformRuntime(host: PlatformRuntimeHost): Platfor
),
}
: {}),
listApps: async (input: { device: DeviceInfo; filter: 'all' | 'user-installed' }) =>
await host.appInventory.android.listApps(
input.device,
input.filter,
request.scope.signal,
),
listApps: async (input: { device: DeviceInfo; filter: 'all' | 'user-installed' }) => {
request.scope.signal.throwIfAborted();
const { listAndroidApps } = await import('./app-lifecycle.ts');
return (await listAndroidApps(input.device, input.filter)).map((app) => ({
id: app.package,
name: app.name,
}));
},
...availableApplicationLifecycleOperations(
bindAndroidApplicationLifecycle({
host,
@@ -3,7 +3,6 @@ export {
detectSoleRunningIosSimulatorApp,
findIosSimulatorInstalledApp,
invalidateIosAppResolutionCache,
listIosApps,
resolveIosApp,
resolveIosAppAlias,
resolveIosSimulatorDeepLinkBundleId,
@@ -123,11 +123,6 @@ function host(options: {
readProcessMarker: async () => ({ status: 'missing' }),
},
networkTransports: { resolve: async () => ({ mode: 'local' }) },
appInventory: {
apple: { listApps: async () => [] },
android: { listApps: async () => [] },
harmonyos: { listApps: async () => [] },
},
};
}
@@ -159,16 +154,7 @@ function unusedAppLogHost(): Omit<
terminate: async () => 'already-missing',
},
processTransports: { resolve: async () => ({ mode: 'local' }) },
appInventory: {
apple: { listApps: async () => [] },
android: { listApps: async () => [] },
harmonyos: { listApps: async () => [] },
},
clock: { now: () => 1, sleep: async () => {} },
appState: {
android: { run: async () => ({ stdout: '' }) },
harmonyos: { run: async () => ({ stdout: '' }) },
},
deviceReadiness: {
applePhysical: { ensureConnected: async () => {} },
appleAutomation: {
@@ -15,15 +15,6 @@ export function platformRuntimeHostFixture(): PlatformRuntimeHost {
readProcessMarker: async () => ({ status: 'missing' }),
},
networkTransports: { resolve: async () => ({ mode: 'local' }) },
appInventory: {
apple: { listApps: async () => [] },
android: { listApps: async () => [] },
harmonyos: { listApps: async () => [] },
},
appState: {
android: { run: async () => ({ stdout: '' }) },
harmonyos: { run: async () => ({ stdout: '' }) },
},
deviceReadiness: {
applePhysical: { ensureConnected: async () => {} },
appleAutomation: {
+12 -11
View File
@@ -1,4 +1,11 @@
import { expect, test, vi } from 'vitest';
vi.mock('./core/app-resolution.ts', async (importOriginal) => ({
...(await importOriginal<typeof import('./core/app-resolution.ts')>()),
listIosApps: vi.fn(async () => [{ bundleId: 'com.example.app', name: 'Example' }]),
}));
import { listIosApps } from './core/app-resolution.ts';
import type { DeviceBinding, RuntimeFacts } from '@agent-device/contracts/platform-runtime';
import type { PlatformRuntimeOperations } from '@agent-device/contracts/platform-runtime-operations';
import type { SnapshotRuntimeHost } from '@agent-device/contracts/snapshot-runtime';
@@ -398,16 +405,10 @@ test('macOS readiness is a no-op while boot remains unavailable', async () => {
expect(binding.operations.bootTarget).toBeUndefined();
});
test('routes Apple app inventory through the injected host facet', async () => {
const host = platformRuntimeHostFixture();
const listApps = vi.fn(async () => [{ id: 'com.example.app', name: 'Example' }]);
const runtime = createApplePlatformRuntime({
...host,
appInventory: {
...host.appInventory,
apple: { listApps },
},
});
test('lists Apple apps through the package-owned resolver', async () => {
const listApps = vi.mocked(listIosApps);
listApps.mockClear();
const runtime = createApplePlatformRuntime(platformRuntimeHostFixture());
const device = appleDevice();
const binding = await runtime.bind({
device,
@@ -422,7 +423,7 @@ test('routes Apple app inventory through the injected host facet', async () => {
await expect(binding.operations.listApps?.({ device, filter: 'all' })).resolves.toEqual([
{ id: 'com.example.app', name: 'Example' },
]);
expect(listApps).toHaveBeenCalledWith(device, 'all', expect.any(AbortSignal));
expect(listApps).toHaveBeenCalledWith(device, 'all');
});
type LegacyLifecycleCell = Readonly<{
+8 -6
View File
@@ -462,12 +462,14 @@ export function createApplePlatformRuntime(host: PlatformRuntimeHost): PlatformR
await ensureAppleReady(host, request.device, request.scope.signal),
})),
...whenAdmitted(facts.operations.listApps, () => ({
listApps: async (input: { device: DeviceInfo; filter: 'all' | 'user-installed' }) =>
await host.appInventory.apple.listApps(
input.device,
input.filter,
request.scope.signal,
),
listApps: async (input: { device: DeviceInfo; filter: 'all' | 'user-installed' }) => {
request.scope.signal.throwIfAborted();
const { listIosApps } = await import('./core/app-resolution.ts');
return (await listIosApps(input.device, input.filter)).map((app) => ({
id: app.bundleId,
name: app.name,
}));
},
})),
...availableApplicationLifecycleOperations(
bindAppleApplicationLifecycle({
+5 -18
View File
@@ -1,30 +1,17 @@
import { AppError } from '@agent-device/kernel/errors';
import type {
AppStateRuntimeCommand,
AppStateRuntimeCommandResult,
AppStateRuntimeResult,
} from '@agent-device/contracts/app-state-runtime';
import type { AppStateRuntimeResult } from '@agent-device/contracts/app-state-runtime';
import type { DeviceInfo } from '@agent-device/kernel/device';
export type HarmonyAppStateHost = Readonly<{
run(
device: DeviceInfo,
command: AppStateRuntimeCommand,
signal: AbortSignal,
): Promise<AppStateRuntimeCommandResult>;
}>;
export async function readHarmonyAppState(
host: HarmonyAppStateHost,
device: DeviceInfo,
signal: AbortSignal,
): Promise<AppStateRuntimeResult> {
signal.throwIfAborted();
const result = await host.run(
device,
{ args: ['shell', 'aa', 'dump', '-l'], timeoutMs: 15_000 },
const { runHarmonyHdc } = await import('./hdc.ts');
const result = await runHarmonyHdc(device, ['shell', 'aa', 'dump', '-l'], {
timeoutMs: 15_000,
signal,
);
});
signal.throwIfAborted();
const foreground = parseHarmonyForegroundApp(result.stdout);
if (!foreground) {
-3
View File
@@ -32,9 +32,6 @@ export const runtimeModule = Object.freeze({
export type { HarmonyInventoryConfig } from './inventory-config.ts';
export const listHarmonyApps = deferred<(typeof import('./app-lifecycle.ts'))['listHarmonyApps']>(
async () => (await import('./app-lifecycle.ts')).listHarmonyApps,
);
export const openHarmonyApp = deferred<(typeof import('./app-lifecycle.ts'))['openHarmonyApp']>(
async () => (await import('./app-lifecycle.ts')).openHarmonyApp,
);
+18 -19
View File
@@ -1,4 +1,8 @@
import { expect, test, vi } from 'vitest';
vi.mock('./hdc.ts', () => ({ runHarmonyHdc: vi.fn() }));
import { runHarmonyHdc } from './hdc.ts';
import type { DeviceBinding } from '@agent-device/contracts/platform-runtime';
import type {
PlatformRuntimeHost,
@@ -25,19 +29,17 @@ test.each([
['device', device],
['emulator', { ...device, kind: 'emulator' as const }],
])('classifies the HarmonyOS %s runtime denominator', async (_name, runtimeDevice) => {
const listApps = vi.fn(async () => [{ id: 'com.example.application', name: 'application' }]);
const hdc = vi.mocked(runHarmonyHdc);
hdc.mockReset();
hdc.mockImplementation(async (_device, args) => ({
exitCode: 0,
stderr: '',
stdout: args.includes('bm')
? 'com.example.application\n'
: 'Mission ID #76 mission name #[#com.example.harmony:entry:MainAbility]\nstate #FOREGROUND',
}));
const host = {
processTransports: { resolve: async () => ({ mode: 'local' as const }) },
appInventory: { harmonyos: { listApps } },
appState: {
android: { run: async () => ({ stdout: '' }) },
harmonyos: {
run: async () => ({
stdout:
'Mission ID #76 mission name #[#com.example.harmony:entry:MainAbility]\nstate #FOREGROUND',
}),
},
},
localInteractors: { resolve: async () => ({}) },
} as unknown as PlatformRuntimeHost;
const binding = await createHarmonyPlatformRuntime(host).bind({
@@ -134,7 +136,11 @@ test.each([
await expect(
binding.operations.listApps?.({ device: runtimeDevice, filter: 'all' }),
).resolves.toEqual([{ id: 'com.example.application', name: 'application' }]);
expect(listApps).toHaveBeenCalledWith(runtimeDevice, 'all', expect.any(AbortSignal));
expect(hdc).toHaveBeenCalledWith(
runtimeDevice,
['shell', 'bm', 'dump', '-a'],
expect.objectContaining({ timeoutMs: 15_000 }),
);
await expect(binding.operations.appState?.()).resolves.toEqual({
package: 'com.example.harmony',
activity: 'MainAbility',
@@ -146,10 +152,6 @@ test('rejects the non-discovered HarmonyOS simulator cell for appstate', async (
const host = {
processTransports: { resolve: async () => ({ mode: 'local' as const }) },
localInteractors: { resolve: async () => ({}) },
appState: {
android: { run: async () => ({ stdout: '' }) },
harmonyos: { run: async () => ({ stdout: '' }) },
},
} as unknown as PlatformRuntimeHost;
const binding = await createHarmonyPlatformRuntime(host).bind({
device: runtimeDevice,
@@ -241,7 +243,6 @@ test.each([
async ({ device: runtimeDevice, legacy }) => {
const host = {
processTransports: { resolve: async () => ({ mode: 'local' as const }) },
appInventory: { harmonyos: { listApps: async () => [] } },
localInteractors: { resolve: async () => ({}) },
} as unknown as PlatformRuntimeHost;
const binding = await createHarmonyPlatformRuntime(host).bind({
@@ -344,8 +345,6 @@ test('binds the HarmonyOS gesture tiers it admitted and omits the rest', async (
function gestureHost(): PlatformRuntimeHost {
return {
processTransports: { resolve: async () => ({ mode: 'local' as const }) },
appInventory: { harmonyos: { listApps: async () => [] } },
appState: { harmonyos: { run: async () => ({ stdout: '' }) } },
localInteractors: { resolve: async () => ({}) },
} as unknown as PlatformRuntimeHost;
}
+8 -11
View File
@@ -344,11 +344,7 @@ export function createHarmonyPlatformRuntime(host: PlatformRuntimeHost): Platfor
...(facts.operations.appState.available
? {
appState: async () =>
await readHarmonyAppState(
host.appState.harmonyos,
request.device,
request.scope.signal,
),
await readHarmonyAppState(request.device, request.scope.signal),
}
: {}),
ensureReady: async () => ({ ...request.device, booted: true }),
@@ -423,12 +419,13 @@ export function createHarmonyPlatformRuntime(host: PlatformRuntimeHost): Platfor
await host.clock.sleep(milliseconds, request.scope.signal),
}),
),
listApps: async (input: { device: DeviceInfo; filter: 'all' | 'user-installed' }) =>
await host.appInventory.harmonyos.listApps(
input.device,
input.filter,
request.scope.signal,
),
listApps: async (input: { device: DeviceInfo; filter: 'all' | 'user-installed' }) => {
request.scope.signal.throwIfAborted();
const { listHarmonyApps } = await import('./app-lifecycle.ts');
return (
await listHarmonyApps(input.device, input.filter, { signal: request.scope.signal })
).map((app) => ({ id: app.package, name: app.name }));
},
...availableApplicationLifecycleOperations(
bindHarmonyApplicationLifecycle({
host: host.localInteractors,
@@ -385,15 +385,6 @@ function host(
readProcessMarker: async () => ({ status: 'missing' }),
},
networkTransports: { resolve: async () => transport },
appInventory: {
apple: { listApps: async () => [] },
android: { listApps: async () => [] },
harmonyos: { listApps: async () => [] },
},
appState: {
android: { run: async () => ({ stdout: '' }) },
harmonyos: { run: async () => ({ stdout: '' }) },
},
deviceReadiness: {
applePhysical: { ensureConnected: async () => {} },
appleAutomation: {
@@ -407,10 +407,6 @@ function host(run: PlatformRuntimeHost['commands']['run']): PlatformRuntimeHost
},
androidEmulator: { discover: async () => [], launch: () => 1, terminate: async () => {} },
},
appState: {
android: { run: async () => ({ stdout: '' }) },
harmonyos: { run: async () => ({ stdout: '' }) },
},
deviceShutdown: {
apple: {
shutdownTarget: async () => ({ success: true, exitCode: 0, stdout: '', stderr: '' }),
@@ -470,11 +466,6 @@ function host(run: PlatformRuntimeHost['commands']['run']): PlatformRuntimeHost
}),
},
networkTransports: { resolve: async () => ({ mode: 'local' }) },
appInventory: {
apple: { listApps: async () => [] },
android: { listApps: async () => [] },
harmonyos: { listApps: async () => [] },
},
screenRecording: {
outputs: { prepare: async () => {} },
apple: {
@@ -76,7 +76,6 @@ function isAllowedCompositionImport(specifier: string): boolean {
specifier === './platform-runtime-android-adb-host.ts' ||
specifier === './platform-runtime-android-observation-host.ts' ||
specifier === './platform-runtime-operation-host.ts' ||
specifier === './platform-runtime-app-state-host.ts' ||
specifier === './platform-runtime-device-inventory.ts' ||
specifier === './platform-runtime-host.ts' ||
specifier === './platform-runtime/request-providers.ts' ||
@@ -36,8 +36,6 @@ const COMPOSITION_FILES = new Set([COMPOSITION_FILE, REQUEST_PROVIDER_COMPOSITIO
const RULE = 'R13 platform-package-substrate';
const RAW_PROCESS_SPECIFIERS = new Set(['child_process', 'node:child_process']);
const PLATFORM_RUNTIME_HOST_FILES = new Set([
'src/platform-runtime-app-inventory-host.ts',
'src/platform-runtime-app-state-host.ts',
'src/platform-runtime-audio-probe-host.ts',
'src/platform-runtime-host-diagnostics.ts',
'src/platform-runtime-managed-web-backend.ts',
@@ -0,0 +1,97 @@
import type { DeviceInfo } from '@agent-device/kernel/device';
import { expect, test, vi } from 'vitest';
const adb = vi.hoisted(() => ({ calls: [] as string[][] }));
vi.mock('@agent-device/host-kit/command', async (importOriginal) => {
const original = await importOriginal<typeof import('@agent-device/host-kit/command')>();
return {
...original,
whichCmd: async (executable: string) => `/usr/bin/${executable}`,
runCmd: async (cmd: string, args: string[]) => {
adb.calls.push([cmd, ...args]);
if (args.includes('query-activities')) {
return { stdout: 'com.example.app/.MainActivity\n', stderr: '', exitCode: 0 };
}
if (args.includes('dumpsys')) {
return {
stdout: 'mCurrentFocus=Window{1 u0 com.example.app/.MainActivity}\n',
stderr: '',
exitCode: 0,
};
}
return { stdout: '', stderr: '', exitCode: 0 };
},
};
});
import { createPlatformRuntimeGateway } from './platform-runtime.ts';
const sessionArtifacts = {
sessionsDir: '/sessions',
resolveSessionArtifacts: () => ({
outputPath: '/sessions/one/app.log',
pidPath: '/sessions/one/app-log.pid',
}),
};
const scope = {
signal: new AbortController().signal,
diagnostics: { emit: () => {} },
progress: { report: () => {} },
};
const device: DeviceInfo = {
platform: 'android',
id: 'emulator-5554',
name: 'Pixel',
kind: 'emulator',
target: 'mobile',
booted: true,
};
test('the composed gateway lists Android apps from inside the platform package', async () => {
const gateway = createPlatformRuntimeGateway(sessionArtifacts);
const binding = await gateway.bind({ device, intent: { kind: 'ordinary' }, scope });
await expect(binding.operations.listApps?.({ device, filter: 'all' })).resolves.toEqual([
{ id: 'com.example.app', name: 'Example' },
]);
expect(adb.calls).toContainEqual([
'adb',
'-s',
device.id,
'shell',
'cmd',
'package',
'query-activities',
'--brief',
'-a',
'android.intent.action.MAIN',
'-c',
'android.intent.category.LAUNCHER',
]);
await gateway.shutdown();
});
test('the composed gateway reads Android app state from inside the platform package', async () => {
const gateway = createPlatformRuntimeGateway(sessionArtifacts);
const binding = await gateway.bind({ device, intent: { kind: 'ordinary' }, scope });
await expect(binding.operations.appState?.()).resolves.toEqual({
package: 'com.example.app',
activity: '.MainActivity',
});
expect(adb.calls).toContainEqual([
'adb',
'-s',
device.id,
'shell',
'dumpsys',
'window',
'windows',
]);
await gateway.shutdown();
});
@@ -1,42 +0,0 @@
import type {
AppInventoryRuntimeHost,
InstalledAppInfo,
} from '@agent-device/contracts/app-inventory-runtime';
import type { AppsFilter } from '@agent-device/contracts/device';
import type { DeviceInfo } from '@agent-device/kernel/device';
import { loadAndroidMechanics } from './platform-runtime-android-mechanics.ts';
export function createAppInventoryRuntimeHost(): AppInventoryRuntimeHost {
return Object.freeze({
apple: Object.freeze({
listApps: async (device: DeviceInfo, filter: AppsFilter) => {
const { listIosApps } = await import('@agent-device/platform-apple/app-resolution');
return mapAppleApps(await listIosApps(device, filter));
},
}),
android: Object.freeze({
listApps: async (device: DeviceInfo, filter: AppsFilter) => {
const { listAndroidApps } = await loadAndroidMechanics();
return (await listAndroidApps(device, filter)).map((app) => ({
id: app.package,
name: app.name,
}));
},
}),
harmonyos: Object.freeze({
listApps: async (device: DeviceInfo, filter: AppsFilter, signal: AbortSignal) => {
const { listHarmonyApps } = await import('@agent-device/platform-harmonyos');
return (await listHarmonyApps(device, filter, { signal })).map((app) => ({
id: app.package,
name: app.name,
}));
},
}),
});
}
function mapAppleApps(
apps: readonly { bundleId: string; name: string }[],
): readonly InstalledAppInfo[] {
return apps.map((app) => ({ id: app.bundleId, name: app.name }));
}
@@ -1,98 +0,0 @@
import { expect, beforeEach, test, vi } from 'vitest';
const executors = vi.hoisted(() => ({
android: vi.fn(),
harmonyos: vi.fn(),
}));
vi.mock('@agent-device/platform-android/mechanics', () => ({
runAndroidAdb: executors.android,
}));
vi.mock('@agent-device/platform-harmonyos', () => ({
runHarmonyHdc: executors.harmonyos,
}));
import { createAppStateRuntimeHost } from './platform-runtime-app-state-host.ts';
const android = {
platform: 'android' as const,
id: 'emulator-5554',
name: 'Pixel',
kind: 'emulator' as const,
};
const harmony = {
platform: 'harmonyos' as const,
id: 'harmony-1',
name: 'Harmony',
kind: 'device' as const,
};
beforeEach(() => {
executors.android.mockReset();
executors.harmonyos.mockReset();
executors.android.mockResolvedValue({
stdout: 'mCurrentFocus=Window{1 u0 com.example.android/.MainActivity}',
});
executors.harmonyos.mockResolvedValue({
stdout:
'Mission ID #76 mission name #[#com.example.harmony:entry:MainAbility]\nstate #FOREGROUND',
});
});
test('keeps only focused cancellable command bridges in the root host', async () => {
const host = createAppStateRuntimeHost();
const signal = new AbortController().signal;
await expect(
host.android.run(android, { args: ['shell', 'dumpsys', 'window'], allowFailure: true }, signal),
).resolves.toEqual({
stdout: 'mCurrentFocus=Window{1 u0 com.example.android/.MainActivity}',
});
expect(executors.android).toHaveBeenCalledWith(
android,
['shell', 'dumpsys', 'window'],
expect.objectContaining({ allowFailure: true, signal }),
);
await expect(
host.harmonyos.run(harmony, { args: ['shell', 'aa', 'dump', '-l'], timeoutMs: 15_000 }, signal),
).resolves.toEqual({
stdout:
'Mission ID #76 mission name #[#com.example.harmony:entry:MainAbility]\nstate #FOREGROUND',
});
expect(executors.harmonyos).toHaveBeenCalledWith(
harmony,
['shell', 'aa', 'dump', '-l'],
expect.objectContaining({ timeoutMs: 15_000, signal }),
);
});
test('forwards an in-flight abort to the underlying Android executor', async () => {
const controller = new AbortController();
let observedSignal: AbortSignal | undefined;
executors.android.mockImplementationOnce(
async (_device, _args, options: { signal?: AbortSignal }) => {
observedSignal = options.signal;
await new Promise<never>((_resolve, reject) => {
options.signal?.addEventListener(
'abort',
() => reject(options.signal?.reason ?? new DOMException('Aborted', 'AbortError')),
{ once: true },
);
});
},
);
const pending = createAppStateRuntimeHost().android.run(
android,
{ args: ['shell', 'dumpsys', 'window'], allowFailure: true },
controller.signal,
);
await vi.waitFor(() => expect(executors.android).toHaveBeenCalledTimes(1));
controller.abort();
await expect(pending).rejects.toMatchObject({ name: 'AbortError' });
expect(observedSignal).toBe(controller.signal);
});
-37
View File
@@ -1,37 +0,0 @@
import type {
AppStateRuntimeCommand,
AppStateRuntimeHost,
} from '@agent-device/contracts/app-state-runtime';
import type { DeviceInfo } from '@agent-device/kernel/device';
import { loadAndroidMechanics } from './platform-runtime-android-mechanics.ts';
export function createAppStateRuntimeHost(): AppStateRuntimeHost {
return Object.freeze({
android: Object.freeze({
run: async (device: DeviceInfo, command: AppStateRuntimeCommand, signal: AbortSignal) => {
signal.throwIfAborted();
const { runAndroidAdb } = await loadAndroidMechanics();
const result = await runAndroidAdb(device, [...command.args], {
allowFailure: command.allowFailure,
timeoutMs: command.timeoutMs,
signal,
});
signal.throwIfAborted();
return { stdout: result.stdout };
},
}),
harmonyos: Object.freeze({
run: async (device: DeviceInfo, command: AppStateRuntimeCommand, signal: AbortSignal) => {
signal.throwIfAborted();
const { runHarmonyHdc } = await import('@agent-device/platform-harmonyos');
const result = await runHarmonyHdc(device, [...command.args], {
allowFailure: command.allowFailure,
timeoutMs: command.timeoutMs,
signal,
});
signal.throwIfAborted();
return { stdout: result.stdout };
},
}),
});
}
-4
View File
@@ -24,8 +24,6 @@ import { createPerfRuntimeHost } from './platform-runtime-perf-host.ts';
import { createApplePhysicalReadinessHost } from './platform-runtime-apple-physical-readiness.ts';
import { createAppleAutomationKeepHotHost } from './platform-runtime-apple-automation-keep-hot.ts';
import { createAndroidEmulatorHost } from './platform-runtime-android-emulator-host.ts';
import { createAppInventoryRuntimeHost } from './platform-runtime-app-inventory-host.ts';
import { createAppStateRuntimeHost } from './platform-runtime-app-state-host.ts';
import { createDeviceShutdownRuntimeHost } from './platform-runtime-device-shutdown-host.ts';
import { createAppleAppDeploymentExecutor } from './platform-runtime-apple-deployment-executor.ts';
import { createAndroidAppDeploymentExecutor } from './platform-runtime-android-deployment-executor.ts';
@@ -105,8 +103,6 @@ export function createPlatformRuntimeHost(options: {
},
}),
...network,
appInventory: createAppInventoryRuntimeHost(),
appState: createAppStateRuntimeHost(),
appleDeployment: createAppleAppDeploymentExecutor(),
androidDeployment: createAndroidAppDeploymentExecutor(),
androidTools: createAndroidToolHost(),
+11 -15
View File
@@ -4,10 +4,7 @@ import type {
} from '@agent-device/contracts/device';
import type { AppLogSessionArtifacts } from '@agent-device/contracts/app-log-runtime';
import type { OwnedProcessRecordWriter } from '@agent-device/contracts/platform-runtime-host';
import type {
AppStateRuntimeHost,
AppStateRuntimeResult,
} from '@agent-device/contracts/app-state-runtime';
import type { AppStateRuntimeResult } from '@agent-device/contracts/app-state-runtime';
import type { DeviceShutdownRuntimeDependencies } from '@agent-device/contracts/device-shutdown-runtime';
import {
type ComposedDeviceInventoryGateways,
@@ -31,10 +28,9 @@ import {
import {
createAndroidObservationAdapter as createPackageAndroidObservationAdapter,
createAndroidInventoryModule,
createAndroidRuntimeModule,
readAndroidAppStateWithExecutor,
readAndroidAppState as readAndroidPackageAppState,
loadShutdownRuntime as loadAndroidShutdownRuntime,
runtimeModule as androidRuntimeModule,
} from '@agent-device/platform-android';
import {
createHarmonyInventoryModule,
@@ -67,18 +63,11 @@ export type {
PlatformProviderResolvers,
} from './platform-runtime/request-providers.ts';
export async function readAndroidAppStateWithHost(
host: AppStateRuntimeHost['android'],
device: Parameters<AppStateRuntimeHost['android']['run']>[0],
signal: AbortSignal,
): Promise<AppStateRuntimeResult> {
return await readAndroidPackageAppState(host, device, signal);
}
export async function getAndroidAppStateWithAdb(
adb: Parameters<typeof readAndroidAppStateWithExecutor>[0],
signal?: AbortSignal,
): Promise<AppStateRuntimeResult> {
return await readAndroidAppStateWithExecutor(adb);
return await readAndroidAppStateWithExecutor(adb, signal);
}
const androidInventoryModule = createAndroidInventoryModule({
@@ -125,6 +114,13 @@ export function createPlatformDeviceInventoryGateways(
});
}
const androidRuntimeModule = createAndroidRuntimeModule({
// Evaluating the root's adb host module binds the process-wide port exactly once.
bindAdbHost: async () => {
await import('./platform-runtime-android-adb-host.ts');
},
});
/** The root composition registry shared by the gateway and bounded host-contract fixtures. */
export const platformRuntimeModules: ReadonlyMap<Platform, PlatformRuntimeModule> = new Map<
Platform,
+2 -1
View File
@@ -13,9 +13,10 @@ import type { AppStateRuntimeResult } from '@agent-device/contracts/app-state-ru
export async function getAndroidAppStateWithAdb(
adb: AndroidAdbExecutor,
signal?: AbortSignal,
): Promise<AppStateRuntimeResult> {
const { getAndroidAppStateWithAdb: read } = await import('../platform-runtime.ts');
return await read(adb);
return await read(adb, signal);
}
export {
+5 -15
View File
@@ -31,21 +31,11 @@ export function createLimrunRuntimeDependencies(): LimrunRuntimeDependencies {
})
).map((app) => ({ id: app.package, name: app.name }));
},
getForegroundApp: async (device, adb, signal) => {
const { readAndroidAppStateWithHost } = await import('../platform-runtime.ts');
const app = await readAndroidAppStateWithHost(
{
run: async (_device, command, commandSignal) => {
const result = await adb([...command.args], {
allowFailure: command.allowFailure,
timeoutMs: command.timeoutMs,
signal: commandSignal,
});
return { stdout: result.stdout };
},
},
device,
signal ?? new AbortController().signal,
getForegroundApp: async (_device, adb, signal) => {
const { getAndroidAppStateWithAdb } = await import('../platform-runtime.ts');
const app = await getAndroidAppStateWithAdb(
async (args, options) => await adb(args, { ...options, signal }),
signal,
);
return app.package ? { appId: app.package, activity: app.activity } : undefined;
},