diff --git a/packages/contracts/src/app-inventory-runtime.ts b/packages/contracts/src/app-inventory-runtime.ts index 1b4179a89..df8ba0e5c 100644 --- a/packages/contracts/src/app-inventory-runtime.ts +++ b/packages/contracts/src/app-inventory-runtime.ts @@ -14,27 +14,3 @@ export type ListAppsInput = Readonly<{ export type AppInventoryRuntimeOperations = Readonly<{ listApps(input: ListAppsInput): Promise; }>; - -export type AppInventoryRuntimeHost = Readonly<{ - apple: Readonly<{ - listApps( - device: DeviceInfo, - filter: AppsFilter, - signal: AbortSignal, - ): Promise; - }>; - android: Readonly<{ - listApps( - device: DeviceInfo, - filter: AppsFilter, - signal: AbortSignal, - ): Promise; - }>; - harmonyos: Readonly<{ - listApps( - device: DeviceInfo, - filter: AppsFilter, - signal: AbortSignal, - ): Promise; - }>; -}>; diff --git a/packages/contracts/src/app-state-runtime.ts b/packages/contracts/src/app-state-runtime.ts index 41c998ae3..a19dac5d3 100644 --- a/packages/contracts/src/app-state-runtime.ts +++ b/packages/contracts/src/app-state-runtime.ts @@ -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; }>; - -export type AppStateRuntimeHost = Readonly<{ - android: Readonly<{ - run( - device: DeviceInfo, - command: AppStateRuntimeCommand, - signal: AbortSignal, - ): Promise; - }>; - harmonyos: Readonly<{ - run( - device: DeviceInfo, - command: AppStateRuntimeCommand, - signal: AbortSignal, - ): Promise; - }>; -}>; diff --git a/packages/contracts/src/platform-runtime-operations.ts b/packages/contracts/src/platform-runtime-operations.ts index 55ca69d27..3b9cff9f8 100644 --- a/packages/contracts/src/platform-runtime-operations.ts +++ b/packages/contracts/src/platform-runtime-operations.ts @@ -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; diff --git a/packages/platform-android/src/app-state.test.ts b/packages/platform-android/src/app-state.test.ts index 8601abe6f..737cb335e 100644 --- a/packages/platform-android/src/app-state.test.ts +++ b/packages/platform-android/src/app-state.test.ts @@ -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']]); +}); diff --git a/packages/platform-android/src/app-state.ts b/packages/platform-android/src/app-state.ts index 741b719fa..a3ca401ad 100644 --- a/packages/platform-android/src/app-state.ts +++ b/packages/platform-android/src/app-state.ts @@ -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; -}>; - 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 { - 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 { 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 { - 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 { - 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; diff --git a/packages/platform-android/src/index.ts b/packages/platform-android/src/index.ts index 1ca8a52e1..aa8f5e5ed 100644 --- a/packages/platform-android/src/index.ts +++ b/packages/platform-android/src/index.ts @@ -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 { - 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 { + signal?: AbortSignal, +): Promise { 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; +}>; + +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, diff --git a/packages/platform-android/src/mechanics.ts b/packages/platform-android/src/mechanics.ts index 7c68ee358..6e1899dc3 100644 --- a/packages/platform-android/src/mechanics.ts +++ b/packages/platform-android/src/mechanics.ts @@ -72,7 +72,6 @@ export async function listAndroidAppsWithAdb( export { closeAndroidApp, isAmStartError, - listAndroidApps, openAndroidApp, openAndroidDevice, parseAndroidLaunchComponent, diff --git a/packages/platform-android/src/network/runtime.test.ts b/packages/platform-android/src/network/runtime.test.ts index acf0ecb72..f56623eb7 100644 --- a/packages/platform-android/src/network/runtime.test.ts +++ b/packages/platform-android/src/network/runtime.test.ts @@ -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: { diff --git a/packages/platform-android/src/runtime-facade.test.ts b/packages/platform-android/src/runtime-facade.test.ts index 64a1686ce..4e4ad92c6 100644 --- a/packages/platform-android/src/runtime-facade.test.ts +++ b/packages/platform-android/src/runtime-facade.test.ts @@ -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'); +}); diff --git a/packages/platform-android/src/runtime.fixtures.ts b/packages/platform-android/src/runtime.fixtures.ts index 8c762f862..b781270f4 100644 --- a/packages/platform-android/src/runtime.fixtures.ts +++ b/packages/platform-android/src/runtime.fixtures.ts @@ -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 = {}): 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 } }, }); } diff --git a/packages/platform-android/src/runtime.test.ts b/packages/platform-android/src/runtime.test.ts index e283cc929..61adafcdd 100644 --- a/packages/platform-android/src/runtime.test.ts +++ b/packages/platform-android/src/runtime.test.ts @@ -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); diff --git a/packages/platform-android/src/runtime.ts b/packages/platform-android/src/runtime.ts index b1d0cb2b9..682677370 100644 --- a/packages/platform-android/src/runtime.ts +++ b/packages/platform-android/src/runtime.ts @@ -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, diff --git a/packages/platform-apple/src/app-resolution-facade.ts b/packages/platform-apple/src/app-resolution-facade.ts index a0bacaa0b..702daeab4 100644 --- a/packages/platform-apple/src/app-resolution-facade.ts +++ b/packages/platform-apple/src/app-resolution-facade.ts @@ -3,7 +3,6 @@ export { detectSoleRunningIosSimulatorApp, findIosSimulatorInstalledApp, invalidateIosAppResolutionCache, - listIosApps, resolveIosApp, resolveIosAppAlias, resolveIosSimulatorDeepLinkBundleId, diff --git a/packages/platform-apple/src/network/runtime.test.ts b/packages/platform-apple/src/network/runtime.test.ts index af241afa0..0523dd04f 100644 --- a/packages/platform-apple/src/network/runtime.test.ts +++ b/packages/platform-apple/src/network/runtime.test.ts @@ -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: { diff --git a/packages/platform-apple/src/runtime.fixtures.ts b/packages/platform-apple/src/runtime.fixtures.ts index 2efe5d48b..efb332a54 100644 --- a/packages/platform-apple/src/runtime.fixtures.ts +++ b/packages/platform-apple/src/runtime.fixtures.ts @@ -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: { diff --git a/packages/platform-apple/src/runtime.test.ts b/packages/platform-apple/src/runtime.test.ts index a2e59c7f0..e1cea0e10 100644 --- a/packages/platform-apple/src/runtime.test.ts +++ b/packages/platform-apple/src/runtime.test.ts @@ -1,4 +1,11 @@ import { expect, test, vi } from 'vitest'; + +vi.mock('./core/app-resolution.ts', async (importOriginal) => ({ + ...(await importOriginal()), + 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<{ diff --git a/packages/platform-apple/src/runtime.ts b/packages/platform-apple/src/runtime.ts index 1ba682404..ab9746a18 100644 --- a/packages/platform-apple/src/runtime.ts +++ b/packages/platform-apple/src/runtime.ts @@ -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({ diff --git a/packages/platform-harmonyos/src/app-state.ts b/packages/platform-harmonyos/src/app-state.ts index 3fe85f6c5..f9f995eea 100644 --- a/packages/platform-harmonyos/src/app-state.ts +++ b/packages/platform-harmonyos/src/app-state.ts @@ -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; -}>; - export async function readHarmonyAppState( - host: HarmonyAppStateHost, device: DeviceInfo, signal: AbortSignal, ): Promise { 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) { diff --git a/packages/platform-harmonyos/src/index.ts b/packages/platform-harmonyos/src/index.ts index bd04074ff..0250b1f9b 100644 --- a/packages/platform-harmonyos/src/index.ts +++ b/packages/platform-harmonyos/src/index.ts @@ -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, ); diff --git a/packages/platform-harmonyos/src/runtime.test.ts b/packages/platform-harmonyos/src/runtime.test.ts index 8b69c3d5d..e59d16238 100644 --- a/packages/platform-harmonyos/src/runtime.test.ts +++ b/packages/platform-harmonyos/src/runtime.test.ts @@ -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; } diff --git a/packages/platform-harmonyos/src/runtime.ts b/packages/platform-harmonyos/src/runtime.ts index 1507aab86..3ed3eed31 100644 --- a/packages/platform-harmonyos/src/runtime.ts +++ b/packages/platform-harmonyos/src/runtime.ts @@ -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, diff --git a/packages/platform-web/src/runtime.test.ts b/packages/platform-web/src/runtime.test.ts index de14d3b04..30841b731 100644 --- a/packages/platform-web/src/runtime.test.ts +++ b/packages/platform-web/src/runtime.test.ts @@ -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: { diff --git a/packages/provider-webdriver/src/platform-runtime.test.ts b/packages/provider-webdriver/src/platform-runtime.test.ts index 325a4ff49..37801f3e2 100644 --- a/packages/provider-webdriver/src/platform-runtime.test.ts +++ b/packages/provider-webdriver/src/platform-runtime.test.ts @@ -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: { diff --git a/scripts/layering/platform-composition-policy.ts b/scripts/layering/platform-composition-policy.ts index 0ca24960b..222413834 100644 --- a/scripts/layering/platform-composition-policy.ts +++ b/scripts/layering/platform-composition-policy.ts @@ -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' || diff --git a/scripts/layering/platform-package-policy.ts b/scripts/layering/platform-package-policy.ts index bffa1956f..0bda0c6c6 100644 --- a/scripts/layering/platform-package-policy.ts +++ b/scripts/layering/platform-package-policy.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', diff --git a/src/platform-runtime-android-adb-binding.test.ts b/src/platform-runtime-android-adb-binding.test.ts new file mode 100644 index 000000000..eb7190993 --- /dev/null +++ b/src/platform-runtime-android-adb-binding.test.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(); + 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(); +}); diff --git a/src/platform-runtime-app-inventory-host.ts b/src/platform-runtime-app-inventory-host.ts deleted file mode 100644 index b9921051f..000000000 --- a/src/platform-runtime-app-inventory-host.ts +++ /dev/null @@ -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 })); -} diff --git a/src/platform-runtime-app-state-host.test.ts b/src/platform-runtime-app-state-host.test.ts deleted file mode 100644 index e639fe9f2..000000000 --- a/src/platform-runtime-app-state-host.test.ts +++ /dev/null @@ -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((_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); -}); diff --git a/src/platform-runtime-app-state-host.ts b/src/platform-runtime-app-state-host.ts deleted file mode 100644 index 923854250..000000000 --- a/src/platform-runtime-app-state-host.ts +++ /dev/null @@ -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 }; - }, - }), - }); -} diff --git a/src/platform-runtime-operation-host.ts b/src/platform-runtime-operation-host.ts index 18b38edc9..eb6ea1f39 100644 --- a/src/platform-runtime-operation-host.ts +++ b/src/platform-runtime-operation-host.ts @@ -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(), diff --git a/src/platform-runtime.ts b/src/platform-runtime.ts index 2004a70cb..20076c51a 100644 --- a/src/platform-runtime.ts +++ b/src/platform-runtime.ts @@ -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[0], - signal: AbortSignal, -): Promise { - return await readAndroidPackageAppState(host, device, signal); -} - export async function getAndroidAppStateWithAdb( adb: Parameters[0], + signal?: AbortSignal, ): Promise { - 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 = new Map< Platform, diff --git a/src/sdk/android-adb.ts b/src/sdk/android-adb.ts index 84b6c75f7..ff180fad7 100644 --- a/src/sdk/android-adb.ts +++ b/src/sdk/android-adb.ts @@ -13,9 +13,10 @@ import type { AppStateRuntimeResult } from '@agent-device/contracts/app-state-ru export async function getAndroidAppStateWithAdb( adb: AndroidAdbExecutor, + signal?: AbortSignal, ): Promise { const { getAndroidAppStateWithAdb: read } = await import('../platform-runtime.ts'); - return await read(adb); + return await read(adb, signal); } export { diff --git a/src/sdk/limrun-runtime-dependencies.ts b/src/sdk/limrun-runtime-dependencies.ts index 6db3a5d04..66d1a3289 100644 --- a/src/sdk/limrun-runtime-dependencies.ts +++ b/src/sdk/limrun-runtime-dependencies.ts @@ -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; },