diff --git a/package.json b/package.json index dbc19f6288..cc07b5c524 100644 --- a/package.json +++ b/package.json @@ -167,7 +167,7 @@ "check:unit": "pnpm test:unit && pnpm check:tmpdir-leaks && pnpm test:smoke", "check": "pnpm check:tooling && pnpm check:fallow && pnpm check:unit", "prepack": "pnpm check:mcp-metadata && pnpm package:npm", - "typecheck": "tsc -b packages/xml packages/kernel packages/contracts packages/host-kit packages/capture-kit packages/managed-allocation packages/provision-kit packages/platform-apple packages/platform-android packages/platform-harmonyos packages/platform-vega packages/platform-linux packages/platform-web packages/ad-script packages/selectors packages/command-registry packages/session-journal packages/ad-replay packages/maestro packages/replay-test packages/provider-webdriver packages/provider-limrun && tsc -p tsconfig.json && tsc -p examples/sdk/tsconfig.json", + "typecheck": "tsc -b packages/xml packages/kernel packages/contracts packages/device-selection packages/host-kit packages/capture-kit packages/managed-allocation packages/provision-kit packages/platform-apple packages/platform-android packages/platform-harmonyos packages/platform-vega packages/platform-linux packages/platform-web packages/ad-script packages/selectors packages/command-registry packages/session-journal packages/ad-replay packages/maestro packages/replay-test packages/provider-webdriver packages/provider-limrun && tsc -p tsconfig.json && tsc -p examples/sdk/tsconfig.json", "test-app:install": "pnpm install --dir examples/test-app", "test-app:start": "pnpm --dir examples/test-app start", "test-app:ios": "pnpm --dir examples/test-app ios", @@ -279,6 +279,7 @@ "@agent-device/capture-kit": "workspace:*", "@agent-device/command-registry": "workspace:*", "@agent-device/contracts": "workspace:*", + "@agent-device/device-selection": "workspace:*", "@agent-device/host-kit": "workspace:*", "@agent-device/kernel": "workspace:*", "@agent-device/managed-allocation": "workspace:*", diff --git a/packages/contracts/src/platform-module.ts b/packages/contracts/src/platform-module.ts index 2f4866b516..08fedca02f 100644 --- a/packages/contracts/src/platform-module.ts +++ b/packages/contracts/src/platform-module.ts @@ -98,9 +98,20 @@ export type ProviderAwareDeviceInventoryGateway = DeviceInventoryGateway & ): Promise; }>; +export type InstalledAppProbe = ( + device: DeviceInfo, + appTarget: string, +) => Promise; + export type ComposedDeviceInventoryGateways = Readonly<{ providerFirst: ProviderAwareDeviceInventoryGateway; localOnly: DeviceInventoryGateway; + /** + * Optional installed-app probe that device selection uses to narrow several + * booted simulators before committing to one. Absent where the host does not + * provide one; selection then falls back to the ordinary inventory rules. + */ + findInstalledApp?: InstalledAppProbe; }>; /** A family module gains this interface only with an honest package-owned source. */ diff --git a/packages/device-selection/package.json b/packages/device-selection/package.json new file mode 100644 index 0000000000..2fe1998b4e --- /dev/null +++ b/packages/device-selection/package.json @@ -0,0 +1,27 @@ +{ + "name": "@agent-device/device-selection", + "version": "0.0.0", + "private": true, + "sideEffects": false, + "type": "module", + "description": "Device selection for dispatched requests: flag-to-inventory request construction, the request-scoped device resolution cache, inventory-backed selection with installed-app narrowing, and the request-scoped device inventory context.", + "dependencies": { + "@agent-device/contracts": "workspace:*", + "@agent-device/host-kit": "workspace:*", + "@agent-device/kernel": "workspace:*" + }, + "exports": { + "./dispatch-resolve": { + "types": "./src/dispatch-resolve.ts", + "default": "./src/dispatch-resolve.ts" + }, + "./device-selection-resolver": { + "types": "./src/device-selection-resolver.ts", + "default": "./src/device-selection-resolver.ts" + }, + "./device-inventory-context": { + "types": "./src/device-inventory-context.ts", + "default": "./src/device-inventory-context.ts" + } + } +} diff --git a/packages/device-selection/src/__tests__/device-selection-fixtures.ts b/packages/device-selection/src/__tests__/device-selection-fixtures.ts new file mode 100644 index 0000000000..7ca2b8b287 --- /dev/null +++ b/packages/device-selection/src/__tests__/device-selection-fixtures.ts @@ -0,0 +1,46 @@ +import type { DeviceInfo } from '@agent-device/kernel/device'; + +export const STOPPED_ANDROID_EMULATOR: DeviceInfo = { + platform: 'android', + id: 'Pixel_9_Pro_XL', + name: 'Pixel 9 Pro XL', + kind: 'emulator', + target: 'mobile', + booted: false, +}; + +export const SECOND_BOOTED_ANDROID_EMULATOR: DeviceInfo = { + platform: 'android', + id: 'emulator-5556', + name: 'Pixel 8', + kind: 'emulator', + target: 'mobile', + booted: true, +}; + +export const ANDROID_EMULATOR: DeviceInfo = { + platform: 'android', + id: 'emulator-5554', + name: 'Pixel', + kind: 'emulator', + booted: true, +}; + +export const IOS_SIMULATOR: DeviceInfo = { + platform: 'apple', + id: 'sim-1', + name: 'iPhone 17 Pro', + kind: 'simulator', + appleOs: 'ios', + booted: true, +}; + +export const MACOS_DEVICE: DeviceInfo = { + platform: 'apple', + id: 'host-macos-local', + name: 'Mac', + kind: 'device', + target: 'desktop', + appleOs: 'macos', + booted: true, +}; diff --git a/src/core/__tests__/device-selection-resolver.test.ts b/packages/device-selection/src/__tests__/device-selection-resolver.test.ts similarity index 90% rename from src/core/__tests__/device-selection-resolver.test.ts rename to packages/device-selection/src/__tests__/device-selection-resolver.test.ts index 210c2dd69e..2d1840a0ea 100644 --- a/src/core/__tests__/device-selection-resolver.test.ts +++ b/packages/device-selection/src/__tests__/device-selection-resolver.test.ts @@ -1,27 +1,21 @@ import assert from 'node:assert/strict'; import { test, vi } from 'vitest'; import { AppError } from '@agent-device/kernel/errors'; -import { - ANDROID_EMULATOR, - IOS_SIMULATOR, - MACOS_DEVICE, -} from '../../__tests__/test-utils/device-fixtures.ts'; +import type { DeviceInfo } from '@agent-device/kernel/device'; import { markSelectionBootOccurred, resolveExistingSessionDeviceSelection, resolveInventoryDeviceSelection, } from '../device-selection-resolver.ts'; +import { withTestDeviceInventory } from './test-utils/device-inventory-gateways.ts'; import { + ANDROID_EMULATOR, + IOS_SIMULATOR, + MACOS_DEVICE, SECOND_BOOTED_ANDROID_EMULATOR, STOPPED_ANDROID_EMULATOR, } from './device-selection-fixtures.ts'; -const mockFindIosSimulatorInstalledApp = vi.hoisted(() => vi.fn()); - -vi.mock('@agent-device/platform-apple/app-resolution', () => ({ - findIosSimulatorInstalledApp: mockFindIosSimulatorInstalledApp, -})); - test('explicit identity selector wins before local inference', async () => { const selection = await resolveInventoryDeviceSelection({ devices: [ANDROID_EMULATOR, SECOND_BOOTED_ANDROID_EMULATOR], @@ -59,16 +53,20 @@ test('a single bootable local candidate is selectable without a preliminary devi test('the booted simulator with the app installed carries its own selected-by reason', async () => { const secondBootedSimulator = { ...IOS_SIMULATOR, id: 'sim-2', name: 'iPhone 17' }; - mockFindIosSimulatorInstalledApp.mockImplementation(async (device: { id: string }) => + const findInstalledApp = vi.fn(async (device: DeviceInfo) => device.id === secondBootedSimulator.id ? 'com.example.demo' : undefined, ); - const selection = await resolveInventoryDeviceSelection({ - devices: [IOS_SIMULATOR, secondBootedSimulator], - selector: { platform: 'ios' }, - source: 'local', - appleSimulatorAppTarget: 'com.example.demo', - }); + const selection = await withTestDeviceInventory( + { findInstalledApp }, + async () => + await resolveInventoryDeviceSelection({ + devices: [IOS_SIMULATOR, secondBootedSimulator], + selector: { platform: 'ios' }, + source: 'local', + appleSimulatorAppTarget: 'com.example.demo', + }), + ); assert.equal(selection.device.id, secondBootedSimulator.id); assert.equal(selection.reason, 'single-app-installed-local'); diff --git a/src/core/__tests__/dispatch-resolve.test.ts b/packages/device-selection/src/__tests__/dispatch-resolve.test.ts similarity index 97% rename from src/core/__tests__/dispatch-resolve.test.ts rename to packages/device-selection/src/__tests__/dispatch-resolve.test.ts index 218442ab21..f6821dd8fa 100644 --- a/src/core/__tests__/dispatch-resolve.test.ts +++ b/packages/device-selection/src/__tests__/dispatch-resolve.test.ts @@ -1,16 +1,8 @@ import { beforeEach, test, vi } from 'vitest'; import assert from 'node:assert/strict'; -const { mockFindIosSimulatorInstalledApp, mockListAppleDevices } = vi.hoisted(() => ({ - mockFindIosSimulatorInstalledApp: vi.fn(), - mockListAppleDevices: vi.fn(), -})); - -vi.mock('@agent-device/platform-apple/app-resolution', () => { - return { - findIosSimulatorInstalledApp: mockFindIosSimulatorInstalledApp, - }; -}); +const mockFindIosSimulatorInstalledApp = vi.fn(); +const mockListAppleDevices = vi.fn(); import { resolveTargetDevice as resolveTargetDeviceInContext, resolveTargetDeviceSelection as resolveTargetDeviceSelectionInContext, @@ -19,7 +11,7 @@ import { import { withTestDeviceInventory, withTestDeviceInventoryProvider as withDeviceInventoryProvider, -} from '../../__tests__/test-utils/device-inventory-gateways.ts'; +} from './test-utils/device-inventory-gateways.ts'; import type { DeviceInfo } from '@agent-device/kernel/device'; import type { DeviceInventoryRequest } from '@agent-device/contracts/device'; import { AppError } from '@agent-device/kernel/errors'; @@ -98,6 +90,7 @@ async function resolveTargetDevice( (request.platform === 'apple' && request.target === 'desktop') ? [macDesktop] : await mockListAppleDevices(request), + findInstalledApp: mockFindIosSimulatorInstalledApp, }, async () => await resolveTargetDeviceInContext(...args), ); @@ -205,7 +198,10 @@ test('app-narrowed selection reports its own typed provenance, not a generic loc ); const selection = await withTestDeviceInventory( - { local: async (request) => await mockListAppleDevices(request) }, + { + local: async (request) => await mockListAppleDevices(request), + findInstalledApp: mockFindIosSimulatorInstalledApp, + }, async () => await resolveTargetDeviceSelectionInContext( { platform: 'ios' }, diff --git a/src/core/__tests__/dispatch-target.test.ts b/packages/device-selection/src/__tests__/dispatch-target.test.ts similarity index 100% rename from src/core/__tests__/dispatch-target.test.ts rename to packages/device-selection/src/__tests__/dispatch-target.test.ts diff --git a/packages/device-selection/src/__tests__/test-utils/device-inventory-gateways.ts b/packages/device-selection/src/__tests__/test-utils/device-inventory-gateways.ts new file mode 100644 index 0000000000..f67f628b9d --- /dev/null +++ b/packages/device-selection/src/__tests__/test-utils/device-inventory-gateways.ts @@ -0,0 +1,148 @@ +import { + filterDeviceInventoryProjection, + LOCAL_DEVICE_INVENTORY_PLATFORM_SELECTORS, + projectProviderDeviceInventoryRequest, + type DeviceInventoryProvider, + type DeviceInventoryRequest, + type ProviderDeviceInventorySource, +} from '@agent-device/contracts/device'; +import type { + ComposedDeviceInventoryGateways, + DeviceInventoryGateway, + InstalledAppProbe, + ProviderAwareDeviceInventoryGateway, +} from '@agent-device/contracts/platform-module'; +import type { PlatformRequestScope } from '@agent-device/contracts/platform-runtime-host'; +import { + isApplePlatform, + type DeviceInfo, + type Platform, + type PlatformSelector, +} from '@agent-device/kernel/device'; +import { withDeviceInventoryContext } from '../../device-inventory-context.ts'; + +export type TestDeviceInventoryOptions = Readonly<{ + provider?: ProviderDeviceInventorySource; + local?: (request: Readonly) => Promise; + findInstalledApp?: InstalledAppProbe; +}>; + +const testRequestScope: PlatformRequestScope = Object.freeze({ + signal: new AbortController().signal, + diagnostics: Object.freeze({ emit: () => {} }), + progress: Object.freeze({ report: () => {} }), +}); + +type LocalDiscover = (request: Readonly) => Promise; + +async function discoverLocalFamily( + localDiscover: LocalDiscover, + platform: PlatformSelector, + request: Readonly, + scope: PlatformRequestScope, +): Promise { + scope.signal.throwIfAborted(); + const family: Platform = isApplePlatform(platform) ? 'apple' : platform; + const devices = (await localDiscover(request)).filter((device) => device.platform === family); + return filterDeviceInventoryProjection(devices, request); +} + +async function discoverLocal( + localDiscover: LocalDiscover, + request: Readonly, + scope: PlatformRequestScope, +): Promise { + scope.signal.throwIfAborted(); + if (request.platform) { + return await discoverLocalFamily(localDiscover, request.platform, request, scope); + } + const perFamily = await Promise.all( + LOCAL_DEVICE_INVENTORY_PLATFORM_SELECTORS.map(async (selector) => { + try { + return await discoverLocalFamily( + localDiscover, + selector, + { ...request, platform: selector }, + scope, + ); + } catch { + return []; + } + }), + ); + scope.signal.throwIfAborted(); + return perFamily.flat(); +} + +function createTestDeviceInventoryGateways( + options: TestDeviceInventoryOptions = {}, +): ComposedDeviceInventoryGateways { + const localDiscover = options.local ?? (async () => [] as readonly DeviceInfo[]); + const localOnly: DeviceInventoryGateway = Object.freeze({ + discover: (request, scope) => discoverLocal(localDiscover, request, scope), + }); + const provider = options.provider; + const discoverWithSource: ProviderAwareDeviceInventoryGateway['discoverWithSource'] = async ( + request, + scope, + ) => { + if (provider) { + scope.signal.throwIfAborted(); + const result = await provider.discover( + projectProviderDeviceInventoryRequest(request), + scope.signal, + ); + scope.signal.throwIfAborted(); + if (result.kind === 'inventory') { + return { + devices: filterDeviceInventoryProjection( + result.devices.map((device) => ({ ...device })), + request, + ), + source: 'provider', + }; + } + } + return { devices: await localOnly.discover(request, scope), source: 'local' }; + }; + const providerFirst: ProviderAwareDeviceInventoryGateway = Object.freeze({ + discover: async (request, scope) => (await discoverWithSource(request, scope)).devices, + discoverWithSource, + }); + return Object.freeze({ + providerFirst, + localOnly, + findInstalledApp: options.findInstalledApp, + }); +} + +export async function withTestDeviceInventory( + options: TestDeviceInventoryOptions, + task: () => Promise, +): Promise { + return await withDeviceInventoryContext( + { ...createTestDeviceInventoryGateways(options), requestScope: testRequestScope }, + task, + ); +} + +/** Test-only bridge for fixtures that still implement the public nullable provider port. */ +export async function withTestDeviceInventoryProvider( + provider: DeviceInventoryProvider, + task: () => Promise, +): Promise { + return await withTestDeviceInventory( + { + provider: { + discover: async (request, signal) => { + signal.throwIfAborted(); + const devices = await provider(request, signal); + return devices === null || devices === undefined + ? { kind: 'declined' } + : { kind: 'inventory', devices }; + }, + }, + }, + task, + ); +} diff --git a/src/request/device-inventory-context.ts b/packages/device-selection/src/device-inventory-context.ts similarity index 84% rename from src/request/device-inventory-context.ts rename to packages/device-selection/src/device-inventory-context.ts index 5770d52711..0311ce21aa 100644 --- a/src/request/device-inventory-context.ts +++ b/packages/device-selection/src/device-inventory-context.ts @@ -1,9 +1,8 @@ import { AppError, isRequestCanceledError } from '@agent-device/kernel/errors'; import type { DeviceInventoryRequest } from '@agent-device/contracts/device'; import type { + ComposedDeviceInventoryGateways, DeviceInventoryDiscovery, - DeviceInventoryGateway, - ProviderAwareDeviceInventoryGateway, } from '@agent-device/contracts/platform-module'; import type { PlatformRequestScope } from '@agent-device/contracts/platform-runtime-host'; import type { DeviceInfo } from '@agent-device/kernel/device'; @@ -11,11 +10,8 @@ import { AsyncLocalStorage } from 'node:async_hooks'; const DEVICE_INVENTORY_CONTEXT_UNAVAILABLE_REASON = 'device_inventory_context_unavailable'; -type DeviceInventoryContext = Readonly<{ - providerFirst: ProviderAwareDeviceInventoryGateway; - localOnly: DeviceInventoryGateway; - requestScope: PlatformRequestScope; -}>; +type DeviceInventoryContext = ComposedDeviceInventoryGateways & + Readonly<{ requestScope: PlatformRequestScope }>; const deviceInventoryContext = new AsyncLocalStorage(); @@ -26,6 +22,15 @@ export async function withDeviceInventoryContext( return await deviceInventoryContext.run(context, task); } +/** + * The composition root's installed-app probe, or undefined where no probe was installed: + * device selection then falls back to the ordinary inventory rules instead of narrowing + * by installed app. + */ +export function readInstalledAppProbe() { + return deviceInventoryContext.getStore()?.findInstalledApp; +} + export async function listDeviceInventory(request: DeviceInventoryRequest): Promise { return await discoverFrom('providerFirst', request); } diff --git a/src/core/device-selection-resolver.ts b/packages/device-selection/src/device-selection-resolver.ts similarity index 95% rename from src/core/device-selection-resolver.ts rename to packages/device-selection/src/device-selection-resolver.ts index 59c60e5200..4424958de6 100644 --- a/src/core/device-selection-resolver.ts +++ b/packages/device-selection/src/device-selection-resolver.ts @@ -157,6 +157,11 @@ async function resolveAppInstalledSimulatorSelection( const appTarget = appleSimulatorAppTarget?.trim(); if (!appTarget) return undefined; + // Function-scoped: keeps the request-context module out of this entry's eager closure. + const { readInstalledAppProbe } = await import('./device-inventory-context.ts'); + const findInstalledApp = readInstalledAppProbe(); + if (!findInstalledApp) return undefined; + const bootedSimulators = devices.filter( (device) => matchesDeviceSelector(device, selector) && @@ -166,12 +171,10 @@ async function resolveAppInstalledSimulatorSelection( ); if (bootedSimulators.length < 2) return undefined; - const { findIosSimulatorInstalledApp } = - await import('@agent-device/platform-apple/app-resolution'); const matches = ( await Promise.all( bootedSimulators.map(async (device) => - (await findIosSimulatorInstalledApp(device, appTarget)) ? device : undefined, + (await findInstalledApp(device, appTarget)) ? device : undefined, ), ) ).filter((device): device is DeviceInfo => device !== undefined); diff --git a/src/core/dispatch-resolve.ts b/packages/device-selection/src/dispatch-resolve.ts similarity index 99% rename from src/core/dispatch-resolve.ts rename to packages/device-selection/src/dispatch-resolve.ts index f0d8553a4d..3847b8764c 100644 --- a/src/core/dispatch-resolve.ts +++ b/packages/device-selection/src/dispatch-resolve.ts @@ -18,7 +18,7 @@ import { listLocalDeviceInventory, readDeviceInventory, shouldPropagateDeviceInventoryProbeError, -} from '../request/device-inventory-context.ts'; +} from './device-inventory-context.ts'; import type { DeviceSelectionResult, InventoryDeviceSelectionParams, diff --git a/packages/device-selection/tsconfig.json b/packages/device-selection/tsconfig.json new file mode 100644 index 0000000000..935c871a4d --- /dev/null +++ b/packages/device-selection/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "composite": true, + "noEmit": false, + "emitDeclarationOnly": true, + "declaration": true, + "declarationDir": "./dist-types", + "rootDir": "./src" + }, + "include": ["src"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a803c846eb..dc78d4f62c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -32,6 +32,9 @@ importers: '@agent-device/contracts': specifier: workspace:* version: link:packages/contracts + '@agent-device/device-selection': + specifier: workspace:* + version: link:packages/device-selection '@agent-device/host-kit': specifier: workspace:* version: link:packages/host-kit @@ -244,6 +247,18 @@ importers: specifier: workspace:* version: link:../kernel + packages/device-selection: + dependencies: + '@agent-device/contracts': + specifier: workspace:* + version: link:../contracts + '@agent-device/host-kit': + specifier: workspace:* + version: link:../host-kit + '@agent-device/kernel': + specifier: workspace:* + version: link:../kernel + packages/host-kit: dependencies: '@agent-device/contracts': diff --git a/scripts/layering/model.test.ts b/scripts/layering/model.test.ts index 9189058527..8456e0ee1c 100644 --- a/scripts/layering/model.test.ts +++ b/scripts/layering/model.test.ts @@ -160,7 +160,7 @@ test('neutral ownership zones reject value imports into higher layers', () => { new Map([ ['src/contracts/result.ts', "import '../core/result.ts';"], ['src/core/result.ts', 'export const result = true;'], - ['src/request/cancel.ts', "import '../commands/cancel.ts';"], + ['packages/device-selection/src/selection.ts', "import '@agent-device/commands/cancel';"], ['src/commands/cancel.ts', 'export const cancel = true;'], ['packages/selectors/src/internal/parse.ts', "import '../../../../src/client/client.ts';"], ['src/client/client.ts', 'export const client = true;'], @@ -172,7 +172,9 @@ test('neutral ownership zones reject value imports into higher layers', () => { assert.deepEqual(collectBackEdges(edges), { 'cli-schema -> cli': ['src/cli-schema/schema.ts -> src/cli/parser.ts'], 'contracts -> core': ['src/contracts/result.ts -> src/core/result.ts'], - 'request -> commands': ['src/request/cancel.ts -> src/commands/cancel.ts'], + 'device-selection -> commands': [ + 'packages/device-selection/src/selection.ts -> src/commands/cancel.ts', + ], 'selectors -> client': ['packages/selectors/src/internal/parse.ts -> src/client/client.ts'], }); }); diff --git a/scripts/layering/model.ts b/scripts/layering/model.ts index 841cf9de82..a0321ff5dc 100644 --- a/scripts/layering/model.ts +++ b/scripts/layering/model.ts @@ -40,10 +40,10 @@ const TARGET_DAG_RANK = new Map([ ['ad-script', 1], ['command-registry', 1], ['contracts', 1], + ['device-selection', 1], ['maestro', 1], ['recording', 1], ['replay-test', 1], - ['request', 1], ['screenshot-diff', 1], ['selectors', 1], ['session-journal', 1], diff --git a/scripts/layering/substrate-domain-shape.test.ts b/scripts/layering/substrate-domain-shape.test.ts index b2c49594ad..e80cf2dc5d 100644 --- a/scripts/layering/substrate-domain-shape.test.ts +++ b/scripts/layering/substrate-domain-shape.test.ts @@ -23,7 +23,7 @@ test('capture-kit rejects request-scoped async_hooks dispatch', () => { ); }); -test('capture-kit policy ignores types, prose, tests, and root-runtime ALS', () => { +test('capture-kit policy ignores types, prose, tests, and non-capture-kit ALS', () => { assert.deepEqual( messages( 'packages/capture-kit/src/app-log-live-handle.ts', @@ -44,7 +44,7 @@ test('capture-kit policy ignores types, prose, tests, and root-runtime ALS', () ); assert.deepEqual( messages( - 'src/request/device-inventory-context.ts', + 'packages/device-selection/src/device-inventory-context.ts', "import { AsyncLocalStorage } from 'node:async_hooks';\nconst store = new AsyncLocalStorage();\n", ), [], diff --git a/src/__tests__/test-utils/boundary-fault-matrix.ts b/src/__tests__/test-utils/boundary-fault-matrix.ts index 152f3fe161..1de46d21b8 100644 --- a/src/__tests__/test-utils/boundary-fault-matrix.ts +++ b/src/__tests__/test-utils/boundary-fault-matrix.ts @@ -198,7 +198,7 @@ export const BOUNDARY_FAULT_MATRIX = { }, read: { kind: 'covered', - evidence: ['src/core/__tests__/dispatch-resolve.test.ts'], + evidence: ['packages/device-selection/src/__tests__/dispatch-resolve.test.ts'], invariants: ['best-effort-degradation'], }, 'artifact-producing': { diff --git a/src/__tests__/test-utils/device-inventory-gateways.ts b/src/__tests__/test-utils/device-inventory-gateways.ts index 36982656d1..e8f49766b1 100644 --- a/src/__tests__/test-utils/device-inventory-gateways.ts +++ b/src/__tests__/test-utils/device-inventory-gateways.ts @@ -14,7 +14,7 @@ import type { PlatformRequestScope, } from '@agent-device/contracts/platform-runtime-host'; import type { DeviceInfo, Platform } from '@agent-device/kernel/device'; -import { withDeviceInventoryContext } from '../../request/device-inventory-context.ts'; +import { withDeviceInventoryContext } from '@agent-device/device-selection/device-inventory-context'; import { createComposedDeviceInventoryGateways } from '../../platform-runtime-device-inventory.ts'; type TestDeviceInventoryOptions = Readonly<{ diff --git a/src/cli/replay-test/__tests__/session-test-reporter-values-maestro.test.ts b/src/cli/replay-test/__tests__/session-test-reporter-values-maestro.test.ts index ab0295cb84..5fc643656e 100644 --- a/src/cli/replay-test/__tests__/session-test-reporter-values-maestro.test.ts +++ b/src/cli/replay-test/__tests__/session-test-reporter-values-maestro.test.ts @@ -21,8 +21,9 @@ import { expect, test, vi } from 'vitest'; import { mkdtempForTestSync } from '../../../__tests__/test-utils/tmp-dir.ts'; -vi.mock('../../../core/dispatch-resolve.ts', async (importOriginal) => { - const actual = await importOriginal(); +vi.mock('@agent-device/device-selection/dispatch-resolve', async (importOriginal) => { + const actual = + await importOriginal(); return { ...actual, resolveTargetDevice: vi.fn(async () => ({ diff --git a/src/commands/output/error.test.ts b/src/commands/output/error.test.ts index 791497b8de..fa490bd18b 100644 --- a/src/commands/output/error.test.ts +++ b/src/commands/output/error.test.ts @@ -125,7 +125,7 @@ test('printHumanError appends a "+N more" marker when candidates were capped', a }); // The device-domain resolvers (findBootedAppleSimulatorWithApp, -// src/core/dispatch-resolve.ts) key their candidate list `devices`, so the CLI +// -device/device-selection/dispatch-resolve) key their candidate list `devices`, so the CLI // renders the udids the "pass --udid" hint asks for. The structured candidate // view comes from @agent-device/kernel/errors and formatting stays local here. test('printHumanError lists device candidates for the device-domain resolvers', async () => { diff --git a/src/core/__tests__/device-selection-fixtures.ts b/src/core/__tests__/device-selection-fixtures.ts deleted file mode 100644 index b4e5838461..0000000000 --- a/src/core/__tests__/device-selection-fixtures.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { DeviceInfo } from '@agent-device/kernel/device'; - -export const STOPPED_ANDROID_EMULATOR: DeviceInfo = { - platform: 'android', - id: 'Pixel_9_Pro_XL', - name: 'Pixel 9 Pro XL', - kind: 'emulator', - target: 'mobile', - booted: false, -}; - -export const SECOND_BOOTED_ANDROID_EMULATOR: DeviceInfo = { - platform: 'android', - id: 'emulator-5556', - name: 'Pixel 8', - kind: 'emulator', - target: 'mobile', - booted: true, -}; diff --git a/src/daemon/__tests__/device-selection-stub.ts b/src/daemon/__tests__/device-selection-stub.ts index 1867d10440..88dec635f9 100644 --- a/src/daemon/__tests__/device-selection-stub.ts +++ b/src/daemon/__tests__/device-selection-stub.ts @@ -1,5 +1,5 @@ import type { DeviceInfo } from '@agent-device/kernel/device'; -import type { DeviceSelectionResult } from '../../core/device-selection-resolver.ts'; +import type { DeviceSelectionResult } from '@agent-device/device-selection/device-selection-resolver'; /** * Wraps a mocked `resolveTargetDevice` so a mocked `resolveTargetDeviceSelection` diff --git a/src/daemon/__tests__/interaction-get-runtime-fixture.ts b/src/daemon/__tests__/interaction-get-runtime-fixture.ts index 4c97c7aabe..26439faa33 100644 --- a/src/daemon/__tests__/interaction-get-runtime-fixture.ts +++ b/src/daemon/__tests__/interaction-get-runtime-fixture.ts @@ -33,7 +33,7 @@ import { createUnavailableRuntimeFactsForTest } from '../../__tests__/test-utils /** * The request-bound runtime seam `get` consumes, faked at `inspectFacts` / `bindDevice` — never - * at `core/dispatch-resolve.ts`. The bound capture still runs the interactor capture the surrounding + * at `-device/device-selection/dispatch-resolve`. The bound capture still runs the interactor capture the surrounding * interaction tests already mock, so only the two `get` operations are fixture-owned here. */ export const mockReadTextAtPoint = vi.fn( diff --git a/src/daemon/__tests__/is-runtime.test.ts b/src/daemon/__tests__/is-runtime.test.ts index a43d3309e5..e27afd8063 100644 --- a/src/daemon/__tests__/is-runtime.test.ts +++ b/src/daemon/__tests__/is-runtime.test.ts @@ -30,7 +30,7 @@ beforeEach(() => { // `is` answers every one of its eight predicates from the resolved capture — `isCommand` never // reaches `backend.readText`. So its whole platform execution is the request-bound capture, and -// these cases bind at `inspectFacts` / `bindDevice`, never at `core/dispatch-resolve.ts`. +// these cases bind at `inspectFacts` / `bindDevice`, never at `-device/device-selection/dispatch-resolve`. const unavailableCapture = { available: false, reason: 'unsupported-device-kind' } as const; const activeAppRequired = { available: false, reason: 'owner-capability-missing' } as const; diff --git a/src/daemon/__tests__/request-router-dispatch-mocks.ts b/src/daemon/__tests__/request-router-dispatch-mocks.ts index 63791b8101..aff97cea5c 100644 --- a/src/daemon/__tests__/request-router-dispatch-mocks.ts +++ b/src/daemon/__tests__/request-router-dispatch-mocks.ts @@ -4,8 +4,9 @@ const dispatchMocks = vi.hoisted(() => ({ resolveTargetDevice: vi.fn(), })); -vi.mock('../../core/dispatch-resolve.ts', async (importOriginal) => { - const actual = await importOriginal(); +vi.mock('@agent-device/device-selection/dispatch-resolve', async (importOriginal) => { + const actual = + await importOriginal(); const { selectionFromResolveTargetDevice } = await import('./device-selection-stub.ts'); return { ...actual, diff --git a/src/daemon/__tests__/request-router-record-runtime-lock.test.ts b/src/daemon/__tests__/request-router-record-runtime-lock.test.ts index d1f3fa26c5..b0c0f24064 100644 --- a/src/daemon/__tests__/request-router-record-runtime-lock.test.ts +++ b/src/daemon/__tests__/request-router-record-runtime-lock.test.ts @@ -25,8 +25,9 @@ const DEVICE: DeviceInfo = { kind: 'emulator', }; -vi.mock('../../core/dispatch-resolve.ts', async (importOriginal) => { - const actual = await importOriginal(); +vi.mock('@agent-device/device-selection/dispatch-resolve', async (importOriginal) => { + const actual = + await importOriginal(); return { ...actual, resolveTargetDevice: vi.fn(async () => DEVICE) }; }); diff --git a/src/daemon/__tests__/selector-text-runtime.test.ts b/src/daemon/__tests__/selector-text-runtime.test.ts index 395a7ed159..6d15c93d2f 100644 --- a/src/daemon/__tests__/selector-text-runtime.test.ts +++ b/src/daemon/__tests__/selector-text-runtime.test.ts @@ -9,7 +9,7 @@ import { readTextForNode } from '../selector-text-runtime.ts'; /** * Bound at the seam the handler consumes (the runtime's `readTextAtPoint` operation), never at - * `core/dispatch-resolve.ts`: `get` is migrated, so the live read reaches this fake through the request + * `-device/device-selection/dispatch-resolve`: `get` is migrated, so the live read reaches this fake through the request * binding rather than through the legacy dispatcher. */ const readTextAtPoint = vi.fn( diff --git a/src/daemon/__tests__/session-device-resolution.test.ts b/src/daemon/__tests__/session-device-resolution.test.ts index ca2fdbcbf8..7a3b6d3646 100644 --- a/src/daemon/__tests__/session-device-resolution.test.ts +++ b/src/daemon/__tests__/session-device-resolution.test.ts @@ -7,14 +7,14 @@ import { selectorTargetsSessionDevice, } from '../session-device-resolution.ts'; import { getRunnerSessionSnapshot } from '@agent-device/platform-apple/runner/operations'; -import { resolveTargetDevice } from '../../core/dispatch-resolve.ts'; +import { resolveTargetDevice } from '@agent-device/device-selection/dispatch-resolve'; import { isActiveProviderDevice } from '../../provider-device-runtime.ts'; import { ensureDeviceReady } from '../device-ready.ts'; vi.mock('@agent-device/platform-apple/runner/operations', () => ({ getRunnerSessionSnapshot: vi.fn(async () => null), })); -vi.mock('../../core/dispatch-resolve.ts', () => ({ +vi.mock('@agent-device/device-selection/dispatch-resolve', () => ({ resolveTargetDevice: vi.fn(), })); vi.mock('../../provider-device-runtime.ts', () => ({ diff --git a/src/daemon/__tests__/system-surface-disclosure.test.ts b/src/daemon/__tests__/system-surface-disclosure.test.ts index 04d4921563..b15e58ac1a 100644 --- a/src/daemon/__tests__/system-surface-disclosure.test.ts +++ b/src/daemon/__tests__/system-surface-disclosure.test.ts @@ -11,8 +11,9 @@ import { makeSessionStore } from '../../__tests__/test-utils/store-factory.ts'; import { makeAndroidSession } from '../../__tests__/test-utils/session-factories.ts'; import { platformResourceCleanup } from '../../platform-runtime-resource-cleanup.ts'; -vi.mock('../../core/dispatch-resolve.ts', async (importOriginal) => { - const actual = await importOriginal(); +vi.mock('@agent-device/device-selection/dispatch-resolve', async (importOriginal) => { + const actual = + await importOriginal(); return { ...actual, resolveTargetDevice: vi.fn(actual.resolveTargetDevice), @@ -28,7 +29,7 @@ vi.mock('../device-ready.ts', () => ({ ensureDeviceReady: vi.fn(async () => {}), })); -import { resolveTargetDevice } from '../../core/dispatch-resolve.ts'; +import { resolveTargetDevice } from '@agent-device/device-selection/dispatch-resolve'; import { ANDROID_EMULATOR } from '../../__tests__/test-utils/device-fixtures.ts'; import { withSystemSurfaceDisclosure } from '../system-surface-disclosure.ts'; diff --git a/src/daemon/__tests__/wait-runtime.test.ts b/src/daemon/__tests__/wait-runtime.test.ts index a558dab83b..6db505c920 100644 --- a/src/daemon/__tests__/wait-runtime.test.ts +++ b/src/daemon/__tests__/wait-runtime.test.ts @@ -61,7 +61,7 @@ type CaptureNode = { /** * Binds the fake at the seam the handler consumes — `inspectFacts` / `bindDevice` — never at - * `core/dispatch-resolve.ts`. `captureSnapshot` is the ONE operation `wait` declares, so this harness is + * `-device/device-selection/dispatch-resolve`. `captureSnapshot` is the ONE operation `wait` declares, so this harness is * also the proof that no sibling snapshot operation is reachable from wait's narrowed binding. */ function waitRuntimeHarness( diff --git a/src/daemon/handlers/__tests__/session-device-claims.test.ts b/src/daemon/handlers/__tests__/session-device-claims.test.ts index e7bf00f00c..5ad1583ae0 100644 --- a/src/daemon/handlers/__tests__/session-device-claims.test.ts +++ b/src/daemon/handlers/__tests__/session-device-claims.test.ts @@ -4,8 +4,9 @@ import path from 'node:path'; import { afterEach, test, vi } from 'vitest'; import { mkdtempForTestSync } from '../../../__tests__/test-utils/tmp-dir.ts'; -vi.mock('../../../core/dispatch-resolve.ts', async (importOriginal) => { - const actual = await importOriginal(); +vi.mock('@agent-device/device-selection/dispatch-resolve', async (importOriginal) => { + const actual = + await importOriginal(); const { selectionFromResolveTargetDevice } = await import('../../__tests__/device-selection-stub.ts'); const resolveTargetDevice = vi.fn(); @@ -36,7 +37,7 @@ vi.mock('@agent-device/host-kit/process', async (importOriginal) => ), ); -import { resolveTargetDevice } from '../../../core/dispatch-resolve.ts'; +import { resolveTargetDevice } from '@agent-device/device-selection/dispatch-resolve'; import { ensureDeviceReady } from '../../device-ready.ts'; import { applyRuntimeHintValues } from '../../../platform-runtime-runtime-hints.ts'; import { resolveAndroidPackageForOpen } from '../../../platform-runtime-open-target.ts'; diff --git a/src/daemon/handlers/__tests__/session-reinstall.test.ts b/src/daemon/handlers/__tests__/session-reinstall.test.ts index efaeebb5e1..a48adcfc7e 100644 --- a/src/daemon/handlers/__tests__/session-reinstall.test.ts +++ b/src/daemon/handlers/__tests__/session-reinstall.test.ts @@ -11,12 +11,13 @@ import { SessionStore } from '../../session-store.ts'; import type { DaemonRequest, DaemonResponse } from '../../daemon-request.ts'; import type { SessionState } from '../../session-state.ts'; -vi.mock('../../../core/dispatch-resolve.ts', async (importOriginal) => { - const actual = await importOriginal(); +vi.mock('@agent-device/device-selection/dispatch-resolve', async (importOriginal) => { + const actual = + await importOriginal(); return { ...actual, resolveTargetDevice: vi.fn() }; }); -import { resolveTargetDevice } from '../../../core/dispatch-resolve.ts'; +import { resolveTargetDevice } from '@agent-device/device-selection/dispatch-resolve'; import { handleSessionCommands, mockBindDeviceRuntime, diff --git a/src/daemon/handlers/__tests__/session-test-harness.ts b/src/daemon/handlers/__tests__/session-test-harness.ts index bbff237357..31fc7522dd 100644 --- a/src/daemon/handlers/__tests__/session-test-harness.ts +++ b/src/daemon/handlers/__tests__/session-test-harness.ts @@ -13,8 +13,9 @@ vi.mock('node:timers/promises', async (importOriginal) => { return { ...actual, setTimeout: vi.fn(async () => undefined) }; }); -vi.mock('../../../core/dispatch-resolve.ts', async (importOriginal) => { - const actual = await importOriginal(); +vi.mock('@agent-device/device-selection/dispatch-resolve', async (importOriginal) => { + const actual = + await importOriginal(); const { selectionFromResolveTargetDevice } = await import('../../__tests__/device-selection-stub.ts'); const resolveTargetDevice = vi.fn(); @@ -93,7 +94,7 @@ import { cleanupRetainedMaterializedPathsForSession } from '../../materialized-p import { SessionStore } from '../../session-store.ts'; import type { DaemonRequest, DaemonResponse } from '../../daemon-request.ts'; import type { SessionState } from '../../session-state.ts'; -import { resolveTargetDevice } from '../../../core/dispatch-resolve.ts'; +import { resolveTargetDevice } from '@agent-device/device-selection/dispatch-resolve'; import { ensureDeviceReady } from '../../device-ready.ts'; import { applyRuntimeHintValues, diff --git a/src/daemon/handlers/__tests__/wait-landmark-recording.test.ts b/src/daemon/handlers/__tests__/wait-landmark-recording.test.ts index 11a26d2ca8..6b9e9a6a4a 100644 --- a/src/daemon/handlers/__tests__/wait-landmark-recording.test.ts +++ b/src/daemon/handlers/__tests__/wait-landmark-recording.test.ts @@ -24,8 +24,9 @@ import { authoringPublication, } from '../../../__tests__/test-utils/session-factories.ts'; -vi.mock('../../../core/dispatch-resolve.ts', async (importOriginal) => { - const actual = await importOriginal(); +vi.mock('@agent-device/device-selection/dispatch-resolve', async (importOriginal) => { + const actual = + await importOriginal(); return { ...actual, resolveTargetDevice: vi.fn(actual.resolveTargetDevice), diff --git a/src/daemon/handlers/record-runtime.ts b/src/daemon/handlers/record-runtime.ts index e909b00a98..311a1d400d 100644 --- a/src/daemon/handlers/record-runtime.ts +++ b/src/daemon/handlers/record-runtime.ts @@ -10,7 +10,7 @@ import { import { isWholeScreenRecordingScope } from '@agent-device/contracts/recording'; import { deviceIdentity, sameDeviceIdentity } from '@agent-device/kernel/device'; import { AppError, normalizeError } from '@agent-device/kernel/errors'; -import { resolveTargetDevice } from '../../core/dispatch-resolve.ts'; +import { resolveTargetDevice } from '@agent-device/device-selection/dispatch-resolve'; import { ensureBoundDeviceReady } from '../request-runtime-binding.ts'; import type { ScreenRecordingAdmissionLedger } from '../screen-recording-admission-ledger.ts'; import { diff --git a/src/daemon/handlers/session-doctor-device.ts b/src/daemon/handlers/session-doctor-device.ts index 2ab09ed8d8..bc201d515b 100644 --- a/src/daemon/handlers/session-doctor-device.ts +++ b/src/daemon/handlers/session-doctor-device.ts @@ -1,5 +1,5 @@ -import { buildDeviceInventoryRequestFromFlags } from '../../core/dispatch-resolve.ts'; -import { listDeviceInventory } from '../../request/device-inventory-context.ts'; +import { buildDeviceInventoryRequestFromFlags } from '@agent-device/device-selection/dispatch-resolve'; +import { listDeviceInventory } from '@agent-device/device-selection/device-inventory-context'; import { countDeviceInventoryByGroup, LOCAL_DEVICE_INVENTORY_PLATFORM_SELECTORS, diff --git a/src/daemon/handlers/session-doctor.ts b/src/daemon/handlers/session-doctor.ts index 383c4780c6..d4d7eeab1d 100644 --- a/src/daemon/handlers/session-doctor.ts +++ b/src/daemon/handlers/session-doctor.ts @@ -7,7 +7,7 @@ import { isActiveProviderDevice } from '../../provider-device-runtime.ts'; import { listLocalDeviceInventory, shouldPropagateDeviceInventoryProbeError, -} from '../../request/device-inventory-context.ts'; +} from '@agent-device/device-selection/device-inventory-context'; import { readVersion } from '@agent-device/host-kit/version'; import type { DaemonRequest, DaemonResponse } from '../daemon-request.ts'; import type { SessionState } from '../session-state.ts'; diff --git a/src/daemon/handlers/session.ts b/src/daemon/handlers/session.ts index 0c23d11f89..3ec370b214 100644 --- a/src/daemon/handlers/session.ts +++ b/src/daemon/handlers/session.ts @@ -98,7 +98,7 @@ const handleSessionObservabilityCommandGroup: SessionCommandHandler = async ({ /** * Descriptor-driven exhaustive dispatch table for the daemon's `session` - * route (mirrors `DISPATCH_HANDLERS` in src/core/dispatch-resolve.ts and + * route (mirrors `DISPATCH_HANDLERS` in -device/device-selection/dispatch-resolve and * `SNAPSHOT_COMMAND_HANDLER_IMPLS` in src/daemon/handlers/snapshot.ts). The * `satisfies Record` check means a * session-routed descriptor added to the registry without a matching entry diff --git a/src/daemon/interaction/internal/__tests__/find.test.ts b/src/daemon/interaction/internal/__tests__/find.test.ts index dbe002eab7..cc5c356fe4 100644 --- a/src/daemon/interaction/internal/__tests__/find.test.ts +++ b/src/daemon/interaction/internal/__tests__/find.test.ts @@ -11,8 +11,8 @@ import { makeAuthoringSession, } from '../../../../__tests__/test-utils/session-factories.ts'; -vi.mock('../../../../core/dispatch-resolve.ts', async (importOriginal) => { - const actual = await importOriginal(); +vi.mock('@agent-device/device-selection/dispatch-resolve', async (importOriginal) => { + const actual = (await importOriginal()) as Record; return { ...actual, resolveTargetDevice: actual.resolveTargetDevice, diff --git a/src/daemon/ios-app-session-hint.test.ts b/src/daemon/ios-app-session-hint.test.ts index ad78b0f2d7..379dd28b5b 100644 --- a/src/daemon/ios-app-session-hint.test.ts +++ b/src/daemon/ios-app-session-hint.test.ts @@ -4,8 +4,10 @@ import { beforeEach, expect, test, vi } from 'vitest'; const listBootedIosSimulators = vi.hoisted(() => vi.fn()); const detectSoleRunningIosSimulatorApp = vi.hoisted(() => vi.fn()); -vi.mock('../request/device-inventory-context.ts', async (importOriginal) => ({ - ...(await importOriginal()), +vi.mock('@agent-device/device-selection/device-inventory-context', async (importOriginal) => ({ + ...(await importOriginal< + typeof import('@agent-device/device-selection/device-inventory-context') + >()), listLocalDeviceInventory: listBootedIosSimulators, })); vi.mock('@agent-device/platform-apple/app-resolution', () => ({ diff --git a/src/daemon/open-device-selection.ts b/src/daemon/open-device-selection.ts index 9cc7d4ac00..ff5c1066da 100644 --- a/src/daemon/open-device-selection.ts +++ b/src/daemon/open-device-selection.ts @@ -1,5 +1,5 @@ import { isDeepLinkTarget } from '@agent-device/contracts/command'; -import type { ResolveTargetDeviceOptions } from '../core/dispatch-resolve.ts'; +import type { ResolveTargetDeviceOptions } from '@agent-device/device-selection/dispatch-resolve'; export function buildOpenTargetDeviceResolutionOptions( openTarget: string | undefined, diff --git a/src/daemon/replay-device-selection.ts b/src/daemon/replay-device-selection.ts index fb84a9b255..458f3297ff 100644 --- a/src/daemon/replay-device-selection.ts +++ b/src/daemon/replay-device-selection.ts @@ -5,7 +5,7 @@ import { resolveDeclaredScriptPlatform, resolveReplayFormat, } from '@agent-device/ad-script'; -import type { ResolveTargetDeviceOptions } from '../core/dispatch-resolve.ts'; +import type { ResolveTargetDeviceOptions } from '@agent-device/device-selection/dispatch-resolve'; import { isDeepLinkTarget, type CommandFlags } from '@agent-device/contracts/command'; import { readReplayScriptSourceFile } from './replay-script-source.ts'; import { appleSimulatorAppTargetForOpenTarget } from './open-device-selection.ts'; diff --git a/src/daemon/replay/internal/__tests__/session-replay-dispatch-selector-miss.test.ts b/src/daemon/replay/internal/__tests__/session-replay-dispatch-selector-miss.test.ts index 389cf6d287..9ead4d6afa 100644 --- a/src/daemon/replay/internal/__tests__/session-replay-dispatch-selector-miss.test.ts +++ b/src/daemon/replay/internal/__tests__/session-replay-dispatch-selector-miss.test.ts @@ -22,8 +22,9 @@ import { test, expect, vi, beforeEach } from 'vitest'; import { mkdtempForTestSync } from '../../../../__tests__/test-utils/tmp-dir.ts'; -vi.mock('../../../../core/dispatch-resolve.ts', async (importOriginal) => { - const actual = await importOriginal(); +vi.mock('@agent-device/device-selection/dispatch-resolve', async (importOriginal) => { + const actual = + await importOriginal(); return { ...actual, resolveTargetDevice: vi.fn() }; }); vi.mock('../../../snapshot-interactor-capture.ts', () => ({ diff --git a/src/daemon/replay/internal/__tests__/session-replay-divergence-android-occlusion.test.ts b/src/daemon/replay/internal/__tests__/session-replay-divergence-android-occlusion.test.ts index 37dd2a7c7d..f430a5177a 100644 --- a/src/daemon/replay/internal/__tests__/session-replay-divergence-android-occlusion.test.ts +++ b/src/daemon/replay/internal/__tests__/session-replay-divergence-android-occlusion.test.ts @@ -16,8 +16,9 @@ import { import { buildReplayFailureDivergence } from '../session-replay-divergence.ts'; import { captureSnapshotWithInteractor } from '../../../snapshot-interactor-capture.ts'; -vi.mock('../../../../core/dispatch-resolve.ts', async (importOriginal) => { - const actual = await importOriginal(); +vi.mock('@agent-device/device-selection/dispatch-resolve', async (importOriginal) => { + const actual = + await importOriginal(); return { ...actual, resolveTargetDevice: vi.fn() }; }); vi.mock('../../../snapshot-interactor-capture.ts', () => ({ diff --git a/src/daemon/replay/internal/__tests__/session-replay-divergence.test.ts b/src/daemon/replay/internal/__tests__/session-replay-divergence.test.ts index 1626d11181..8ad0bf3073 100644 --- a/src/daemon/replay/internal/__tests__/session-replay-divergence.test.ts +++ b/src/daemon/replay/internal/__tests__/session-replay-divergence.test.ts @@ -1,8 +1,8 @@ import path from 'node:path'; import { beforeEach, expect, test, vi } from 'vitest'; import { mkdtempForTestSync } from '../../../../__tests__/test-utils/tmp-dir.ts'; -vi.mock('../../../../core/dispatch-resolve.ts', async (importOriginal) => { - const actual = await importOriginal(); +vi.mock('@agent-device/device-selection/dispatch-resolve', async (importOriginal) => { + const actual = (await importOriginal()) as Record; return { ...actual, resolveTargetDevice: vi.fn() }; }); vi.mock('../../../snapshot-interactor-capture.ts', () => ({ diff --git a/src/daemon/replay/internal/__tests__/session-replay-maestro-failure.test.ts b/src/daemon/replay/internal/__tests__/session-replay-maestro-failure.test.ts index fc6abbf834..73483b2dbe 100644 --- a/src/daemon/replay/internal/__tests__/session-replay-maestro-failure.test.ts +++ b/src/daemon/replay/internal/__tests__/session-replay-maestro-failure.test.ts @@ -2,8 +2,9 @@ import { noMaestroIncludeSources } from '../../../../__tests__/test-utils/replay import { beforeEach, expect, test, vi } from 'vitest'; import { mkdtempForTestSync } from '../../../../__tests__/test-utils/tmp-dir.ts'; -vi.mock('../../../../core/dispatch-resolve.ts', async (importOriginal) => { - const actual = await importOriginal(); +vi.mock('@agent-device/device-selection/dispatch-resolve', async (importOriginal) => { + const actual = + await importOriginal(); return { ...actual, resolveTargetDevice: vi.fn() }; }); vi.mock('../../../snapshot-interactor-capture.ts', () => ({ diff --git a/src/daemon/replay/internal/__tests__/session-replay-repair-acceptance.test.ts b/src/daemon/replay/internal/__tests__/session-replay-repair-acceptance.test.ts index 9b81993228..75a170fd84 100644 --- a/src/daemon/replay/internal/__tests__/session-replay-repair-acceptance.test.ts +++ b/src/daemon/replay/internal/__tests__/session-replay-repair-acceptance.test.ts @@ -9,8 +9,9 @@ import { test, expect, vi, beforeEach } from 'vitest'; import { markRepairTransactionComplete } from '../../../session-replay-transaction.ts'; import { mkdtempForTestSync } from '../../../../__tests__/test-utils/tmp-dir.ts'; -vi.mock('../../../../core/dispatch-resolve.ts', async (importOriginal) => { - const actual = await importOriginal(); +vi.mock('@agent-device/device-selection/dispatch-resolve', async (importOriginal) => { + const actual = + await importOriginal(); return { ...actual, resolveTargetDevice: vi.fn() }; }); vi.mock('../../../snapshot-interactor-capture.ts', () => ({ diff --git a/src/daemon/replay/internal/__tests__/session-replay-repair-empty-tail.test.ts b/src/daemon/replay/internal/__tests__/session-replay-repair-empty-tail.test.ts index 0aed12e68b..f81c573824 100644 --- a/src/daemon/replay/internal/__tests__/session-replay-repair-empty-tail.test.ts +++ b/src/daemon/replay/internal/__tests__/session-replay-repair-empty-tail.test.ts @@ -25,8 +25,9 @@ import { test, expect, vi, beforeEach } from 'vitest'; import { mkdtempForTestSync } from '../../../../__tests__/test-utils/tmp-dir.ts'; -vi.mock('../../../../core/dispatch-resolve.ts', async (importOriginal) => { - const actual = await importOriginal(); +vi.mock('@agent-device/device-selection/dispatch-resolve', async (importOriginal) => { + const actual = + await importOriginal(); return { ...actual, resolveTargetDevice: vi.fn() }; }); vi.mock('../../../snapshot-interactor-capture.ts', () => ({ diff --git a/src/daemon/replay/internal/__tests__/session-replay-repair-loop.test.ts b/src/daemon/replay/internal/__tests__/session-replay-repair-loop.test.ts index bfc89d7e76..82db4b55bb 100644 --- a/src/daemon/replay/internal/__tests__/session-replay-repair-loop.test.ts +++ b/src/daemon/replay/internal/__tests__/session-replay-repair-loop.test.ts @@ -13,8 +13,9 @@ import { isSessionRecording } from '../../../session-script-publication-capabili import { test, expect, vi, beforeEach } from 'vitest'; import { mkdtempForTestSync } from '../../../../__tests__/test-utils/tmp-dir.ts'; -vi.mock('../../../../core/dispatch-resolve.ts', async (importOriginal) => { - const actual = await importOriginal(); +vi.mock('@agent-device/device-selection/dispatch-resolve', async (importOriginal) => { + const actual = + await importOriginal(); return { ...actual, resolveTargetDevice: vi.fn() }; }); vi.mock('../../../snapshot-interactor-capture.ts', () => ({ diff --git a/src/daemon/replay/internal/__tests__/session-replay-repair-record-exclusion.test.ts b/src/daemon/replay/internal/__tests__/session-replay-repair-record-exclusion.test.ts index 51fbc5afe2..2b840c6ae8 100644 --- a/src/daemon/replay/internal/__tests__/session-replay-repair-record-exclusion.test.ts +++ b/src/daemon/replay/internal/__tests__/session-replay-repair-record-exclusion.test.ts @@ -37,8 +37,9 @@ vi.mock('../../../../platform-runtime-runtime-hints.ts', async (importOriginal) await importOriginal(); return { ...actual, clearRuntimeHintValues: vi.fn(async () => {}) }; }); -vi.mock('../../../../core/dispatch-resolve.ts', async (importOriginal) => { - const actual = await importOriginal(); +vi.mock('@agent-device/device-selection/dispatch-resolve', async (importOriginal) => { + const actual = + await importOriginal(); return { ...actual, resolveTargetDevice: vi.fn() }; }); vi.mock('../../../snapshot-interactor-capture.ts', () => ({ diff --git a/src/daemon/replay/internal/__tests__/session-replay-repair-transaction.test.ts b/src/daemon/replay/internal/__tests__/session-replay-repair-transaction.test.ts index 36be862cf6..5041d9d2b4 100644 --- a/src/daemon/replay/internal/__tests__/session-replay-repair-transaction.test.ts +++ b/src/daemon/replay/internal/__tests__/session-replay-repair-transaction.test.ts @@ -38,8 +38,8 @@ vi.mock('../../../../platform-runtime-runtime-hints.ts', async (importOriginal) await importOriginal(); return { ...actual, clearRuntimeHintValues: vi.fn(async () => {}) }; }); -vi.mock('../../../../core/dispatch-resolve.ts', async (importOriginal) => { - const actual = await importOriginal(); +vi.mock('@agent-device/device-selection/dispatch-resolve', async (importOriginal) => { + const actual = (await importOriginal()) as Record; return { ...actual, resolveTargetDevice: vi.fn() }; }); vi.mock('../../../snapshot-interactor-capture.ts', () => ({ diff --git a/src/daemon/replay/internal/__tests__/session-replay-runtime-binding.test.ts b/src/daemon/replay/internal/__tests__/session-replay-runtime-binding.test.ts index 9e6df16c3e..580a75601e 100644 --- a/src/daemon/replay/internal/__tests__/session-replay-runtime-binding.test.ts +++ b/src/daemon/replay/internal/__tests__/session-replay-runtime-binding.test.ts @@ -2,7 +2,7 @@ import fs from 'node:fs'; import path from 'node:path'; import { beforeEach, expect, test, vi } from 'vitest'; import { makeIosSession } from '../../../../__tests__/test-utils/session-factories.ts'; -import { resolveTargetDevice } from '../../../../core/dispatch-resolve.ts'; +import { resolveTargetDevice } from '@agent-device/device-selection/dispatch-resolve'; import { captureSnapshotThroughLegacyDispatchFixture } from '../../../__tests__/legacy-snapshot-capture-fixture.ts'; import { SessionStore } from '../../../session-store.ts'; import { runReplayForTest } from '../../__tests__/replay-command-fixture.ts'; @@ -10,8 +10,9 @@ import { captureSnapshotWithInteractor } from '../../../snapshot-interactor-capt import { baseReplayRequest as baseReq } from '../../__tests__/session-replay-runtime.fixtures.ts'; import { mkdtempForTestSync } from '../../../../__tests__/test-utils/tmp-dir.ts'; -vi.mock('../../../../core/dispatch-resolve.ts', async (importOriginal) => { - const actual = await importOriginal(); +vi.mock('@agent-device/device-selection/dispatch-resolve', async (importOriginal) => { + const actual = + await importOriginal(); return { ...actual, resolveTargetDevice: vi.fn() }; }); diff --git a/src/daemon/replay/internal/__tests__/session-replay-runtime-failure-response.test.ts b/src/daemon/replay/internal/__tests__/session-replay-runtime-failure-response.test.ts index 41c7d617e3..790876d0ba 100644 --- a/src/daemon/replay/internal/__tests__/session-replay-runtime-failure-response.test.ts +++ b/src/daemon/replay/internal/__tests__/session-replay-runtime-failure-response.test.ts @@ -1,8 +1,9 @@ import { test, expect, vi, beforeEach } from 'vitest'; import { mkdtempForTestSync } from '../../../../__tests__/test-utils/tmp-dir.ts'; -vi.mock('../../../../core/dispatch-resolve.ts', async (importOriginal) => { - const actual = await importOriginal(); +vi.mock('@agent-device/device-selection/dispatch-resolve', async (importOriginal) => { + const actual = + await importOriginal(); return { ...actual, resolveTargetDevice: vi.fn() }; }); vi.mock('../../../snapshot-interactor-capture.ts', () => ({ diff --git a/src/daemon/replay/internal/__tests__/session-replay-runtime-failure.test.ts b/src/daemon/replay/internal/__tests__/session-replay-runtime-failure.test.ts index 49c4a81383..d0994c3bbf 100644 --- a/src/daemon/replay/internal/__tests__/session-replay-runtime-failure.test.ts +++ b/src/daemon/replay/internal/__tests__/session-replay-runtime-failure.test.ts @@ -1,8 +1,9 @@ import { test, expect, vi, beforeEach } from 'vitest'; import { mkdtempForTestSync } from '../../../../__tests__/test-utils/tmp-dir.ts'; -vi.mock('../../../../core/dispatch-resolve.ts', async (importOriginal) => { - const actual = await importOriginal(); +vi.mock('@agent-device/device-selection/dispatch-resolve', async (importOriginal) => { + const actual = + await importOriginal(); return { ...actual, resolveTargetDevice: vi.fn() }; }); vi.mock('../../../snapshot-interactor-capture.ts', () => ({ diff --git a/src/daemon/replay/internal/__tests__/session-replay-runtime-keep-session.test.ts b/src/daemon/replay/internal/__tests__/session-replay-runtime-keep-session.test.ts index 765b95e1c8..e8fc3da496 100644 --- a/src/daemon/replay/internal/__tests__/session-replay-runtime-keep-session.test.ts +++ b/src/daemon/replay/internal/__tests__/session-replay-runtime-keep-session.test.ts @@ -32,8 +32,9 @@ import { mkdtempForTestSync } from '../../../../__tests__/test-utils/tmp-dir.ts' * `session-replay-runtime.test.ts`. */ -vi.mock('../../../../core/dispatch-resolve.ts', async (importOriginal) => { - const actual = await importOriginal(); +vi.mock('@agent-device/device-selection/dispatch-resolve', async (importOriginal) => { + const actual = + await importOriginal(); return { ...actual, resolveTargetDevice: vi.fn() }; }); diff --git a/src/daemon/replay/internal/__tests__/session-replay-runtime-maestro.test.ts b/src/daemon/replay/internal/__tests__/session-replay-runtime-maestro.test.ts index c8376c984c..7220a65817 100644 --- a/src/daemon/replay/internal/__tests__/session-replay-runtime-maestro.test.ts +++ b/src/daemon/replay/internal/__tests__/session-replay-runtime-maestro.test.ts @@ -15,11 +15,11 @@ import { mkdtempForTestSync } from '../../../../__tests__/test-utils/tmp-dir.ts' // runReplayCommand tests that happen to share the same runReplayFixture // helper and mock configuration below. It is a sibling of // session-replay-runtime.test.ts rather than a merge into it because that file -// mocks '../../../core/dispatch-resolve.ts' with its own device resolution — vitest +// mocks '@agent-device/device-selection/dispatch-resolve' with its own device resolution — vitest // allows only one vi.mock per module per file, so reconciling the two // configurations was out of scope for a pure test-file split (see #1460). -vi.mock('../../../../core/dispatch-resolve.ts', async (importOriginal) => { - const actual = await importOriginal(); +vi.mock('@agent-device/device-selection/dispatch-resolve', async (importOriginal) => { + const actual = (await importOriginal()) as Record; return { ...actual, resolveTargetDevice: vi.fn(async (flags) => diff --git a/src/daemon/replay/internal/__tests__/session-replay-runtime-plan.test.ts b/src/daemon/replay/internal/__tests__/session-replay-runtime-plan.test.ts index e6a1dc6314..a5d02e464d 100644 --- a/src/daemon/replay/internal/__tests__/session-replay-runtime-plan.test.ts +++ b/src/daemon/replay/internal/__tests__/session-replay-runtime-plan.test.ts @@ -1,8 +1,9 @@ import { test, expect, vi, beforeEach } from 'vitest'; import { mkdtempForTestSync } from '../../../../__tests__/test-utils/tmp-dir.ts'; -vi.mock('../../../../core/dispatch-resolve.ts', async (importOriginal) => { - const actual = await importOriginal(); +vi.mock('@agent-device/device-selection/dispatch-resolve', async (importOriginal) => { + const actual = + await importOriginal(); return { ...actual, resolveTargetDevice: vi.fn() }; }); @@ -14,7 +15,7 @@ import fs from 'node:fs'; import path from 'node:path'; import { runReplayForTest } from '../../__tests__/replay-command-fixture.ts'; import { SessionStore } from '../../../session-store.ts'; -import { resolveTargetDevice } from '../../../../core/dispatch-resolve.ts'; +import { resolveTargetDevice } from '@agent-device/device-selection/dispatch-resolve'; import { captureSnapshotThroughLegacyDispatchFixture, legacyDispatchCapture, diff --git a/src/daemon/replay/internal/__tests__/session-replay-runtime.test.ts b/src/daemon/replay/internal/__tests__/session-replay-runtime.test.ts index e2c6aca59e..c5f7978f96 100644 --- a/src/daemon/replay/internal/__tests__/session-replay-runtime.test.ts +++ b/src/daemon/replay/internal/__tests__/session-replay-runtime.test.ts @@ -2,8 +2,9 @@ import { isSessionRecording } from '../../../session-script-publication-capabili import { test, expect, vi, beforeEach } from 'vitest'; import { mkdtempForTestSync } from '../../../../__tests__/test-utils/tmp-dir.ts'; -vi.mock('../../../../core/dispatch-resolve.ts', async (importOriginal) => { - const actual = await importOriginal(); +vi.mock('@agent-device/device-selection/dispatch-resolve', async (importOriginal) => { + const actual = + await importOriginal(); return { ...actual, resolveTargetDevice: vi.fn() }; }); diff --git a/src/daemon/replay/internal/__tests__/session-replay-script-source.test.ts b/src/daemon/replay/internal/__tests__/session-replay-script-source.test.ts index 9419bffab9..97c31e20a4 100644 --- a/src/daemon/replay/internal/__tests__/session-replay-script-source.test.ts +++ b/src/daemon/replay/internal/__tests__/session-replay-script-source.test.ts @@ -8,8 +8,9 @@ import { beforeEach, expect, test, vi } from 'vitest'; import { mkdtempForTestSync } from '../../../../__tests__/test-utils/tmp-dir.ts'; -vi.mock('../../../../core/dispatch-resolve.ts', async (importOriginal) => { - const actual = await importOriginal(); +vi.mock('@agent-device/device-selection/dispatch-resolve', async (importOriginal) => { + const actual = + await importOriginal(); return { ...actual, resolveTargetDevice: vi.fn() }; }); vi.mock('../../../snapshot-interactor-capture.ts', () => ({ diff --git a/src/daemon/replay/internal/__tests__/session-replay-selector-routes.test.ts b/src/daemon/replay/internal/__tests__/session-replay-selector-routes.test.ts index bd762af277..9d2a985092 100644 --- a/src/daemon/replay/internal/__tests__/session-replay-selector-routes.test.ts +++ b/src/daemon/replay/internal/__tests__/session-replay-selector-routes.test.ts @@ -15,8 +15,9 @@ import { writeReplayFile, } from '../../__tests__/session-replay-runtime.fixtures.ts'; -vi.mock('../../../../core/dispatch-resolve.ts', async (importOriginal) => { - const actual = await importOriginal(); +vi.mock('@agent-device/device-selection/dispatch-resolve', async (importOriginal) => { + const actual = + await importOriginal(); return { ...actual, resolveTargetDevice: vi.fn() }; }); diff --git a/src/daemon/replay/internal/__tests__/session-replay-target-verification-runtime.test.ts b/src/daemon/replay/internal/__tests__/session-replay-target-verification-runtime.test.ts index a548d7995d..8650c32f4c 100644 --- a/src/daemon/replay/internal/__tests__/session-replay-target-verification-runtime.test.ts +++ b/src/daemon/replay/internal/__tests__/session-replay-target-verification-runtime.test.ts @@ -8,8 +8,9 @@ */ import { test, expect, vi, beforeEach } from 'vitest'; -vi.mock('../../../../core/dispatch-resolve.ts', async (importOriginal) => { - const actual = await importOriginal(); +vi.mock('@agent-device/device-selection/dispatch-resolve', async (importOriginal) => { + const actual = + await importOriginal(); return { ...actual, resolveTargetDevice: vi.fn() }; }); diff --git a/src/daemon/replay/internal/session-replay-maestro-runtime.ts b/src/daemon/replay/internal/session-replay-maestro-runtime.ts index a086b502f4..2e99491b38 100644 --- a/src/daemon/replay/internal/session-replay-maestro-runtime.ts +++ b/src/daemon/replay/internal/session-replay-maestro-runtime.ts @@ -7,7 +7,7 @@ import { type MaestroPlatform, } from '@agent-device/maestro'; import { AppError } from '@agent-device/kernel/errors'; -import { resolveTargetDevice } from '../../../core/dispatch-resolve.ts'; +import { resolveTargetDevice } from '@agent-device/device-selection/dispatch-resolve'; import { getRequestSignal } from '@agent-device/host-kit/request'; import { stripUndefined } from '@agent-device/kernel/record'; import { diff --git a/src/daemon/replay/internal/session-test-shard-devices.ts b/src/daemon/replay/internal/session-test-shard-devices.ts index c432210a77..d99caf3465 100644 --- a/src/daemon/replay/internal/session-test-shard-devices.ts +++ b/src/daemon/replay/internal/session-test-shard-devices.ts @@ -1,6 +1,6 @@ import type { CommandFlags } from '@agent-device/contracts/command'; import type { DeviceInventoryRequest } from '@agent-device/contracts/device'; -import { listDeviceInventory } from '../../../request/device-inventory-context.ts'; +import { listDeviceInventory } from '@agent-device/device-selection/device-inventory-context'; import { resolveAndroidSerialAllowlist, resolveIosSimulatorDeviceSetPath, diff --git a/src/daemon/request-binding.ts b/src/daemon/request-binding.ts index 67a0102725..43aa6becca 100644 --- a/src/daemon/request-binding.ts +++ b/src/daemon/request-binding.ts @@ -1,4 +1,4 @@ -import { resolveTargetDevice } from '../core/dispatch-resolve.ts'; +import { resolveTargetDevice } from '@agent-device/device-selection/dispatch-resolve'; import { hasDeviceSelectionInput } from './device-selector-intent.ts'; import { applyRequestLockPolicy } from './request-lock-policy.ts'; import { buildOpenTargetDeviceResolutionOptions } from './open-device-selection.ts'; diff --git a/src/daemon/request-platform-provider-context.ts b/src/daemon/request-platform-provider-context.ts index e710f8fd81..b67ad5d3bb 100644 --- a/src/daemon/request-platform-provider-context.ts +++ b/src/daemon/request-platform-provider-context.ts @@ -1,5 +1,5 @@ import type { PlatformProviderRequestContext } from '@agent-device/contracts/platform-providers'; -import { resolveTargetDevice } from '../core/dispatch-resolve.ts'; +import { resolveTargetDevice } from '@agent-device/device-selection/dispatch-resolve'; import { hasDeviceSelectionInput, hasExplicitDeviceSelector } from './device-selector-intent.ts'; import { buildOpenTargetDeviceResolutionOptions } from './open-device-selection.ts'; import { resolveProviderDeviceResolutionIntent } from './daemon-command-registry.ts'; diff --git a/src/daemon/request-router.ts b/src/daemon/request-router.ts index fcb1d0aee7..2b517da8a7 100644 --- a/src/daemon/request-router.ts +++ b/src/daemon/request-router.ts @@ -1,5 +1,5 @@ -import { withResolveTargetDeviceCacheScope } from '../core/dispatch-resolve.ts'; -import { withDeviceInventoryContext } from '../request/device-inventory-context.ts'; +import { withResolveTargetDeviceCacheScope } from '@agent-device/device-selection/dispatch-resolve'; +import { withDeviceInventoryContext } from '@agent-device/device-selection/device-inventory-context'; import type { LeaseLifecycleProvider, ProviderAppCatalog } from '@agent-device/contracts/device'; import type { ComposedDeviceInventoryGateways } from '@agent-device/contracts/platform-module'; import type { DeviceRuntimeGateway } from '@agent-device/contracts/platform-runtime'; diff --git a/src/daemon/selector-runtime-backend.ts b/src/daemon/selector-runtime-backend.ts index 55129016d6..3456396bae 100644 --- a/src/daemon/selector-runtime-backend.ts +++ b/src/daemon/selector-runtime-backend.ts @@ -3,7 +3,7 @@ import type { BackendCommandContext, BackendSnapshotResult, } from '../backend.ts'; -import { resolveTargetDevice } from '../core/dispatch-resolve.ts'; +import { resolveTargetDevice } from '@agent-device/device-selection/dispatch-resolve'; import { createAgentDevice } from '../runtime.ts'; import { publicPlatformString } from '@agent-device/kernel/device'; import { noActiveSessionError } from './response.ts'; diff --git a/src/daemon/session-device-resolution.ts b/src/daemon/session-device-resolution.ts index b4e142b5ce..3d4204acc9 100644 --- a/src/daemon/session-device-resolution.ts +++ b/src/daemon/session-device-resolution.ts @@ -2,7 +2,7 @@ import { isIosFamily, type DeviceInfo } from '@agent-device/kernel/device'; import { AppError } from '@agent-device/kernel/errors'; import { isActiveProviderDevice } from '../provider-device-runtime.ts'; import { inspectAppleRunnerSession } from '../platform-runtime-apple-resources.ts'; -import { resolveTargetDevice } from '../core/dispatch-resolve.ts'; +import { resolveTargetDevice } from '@agent-device/device-selection/dispatch-resolve'; import type { DaemonRequest, DaemonResponse } from './daemon-request.ts'; import type { SessionState } from './session-state.ts'; import { hasDeviceSelectionInput, hasExplicitDeviceSelector } from './device-selector-intent.ts'; diff --git a/src/daemon/session-lifecycle/__tests__/application.test.ts b/src/daemon/session-lifecycle/__tests__/application.test.ts index 8906291384..ddbbcf606c 100644 --- a/src/daemon/session-lifecycle/__tests__/application.test.ts +++ b/src/daemon/session-lifecycle/__tests__/application.test.ts @@ -2,9 +2,11 @@ import { beforeEach, expect, test, vi } from 'vitest'; import type { DaemonRequest, DaemonResponse } from '../../daemon-request.ts'; import { makeSessionStore } from '../../../__tests__/test-utils/store-factory.ts'; -vi.mock('../../../request/device-inventory-context.ts', async (importOriginal) => { +vi.mock('@agent-device/device-selection/device-inventory-context', async (importOriginal) => { const actual = - await importOriginal(); + await importOriginal< + typeof import('@agent-device/device-selection/device-inventory-context') + >(); return { ...actual, listDeviceInventory: vi.fn(async () => []) }; }); diff --git a/src/daemon/session-lifecycle/internal/__tests__/session-inventory-appleos.test.ts b/src/daemon/session-lifecycle/internal/__tests__/session-inventory-appleos.test.ts index 29dca832b9..64bd393aa2 100644 --- a/src/daemon/session-lifecycle/internal/__tests__/session-inventory-appleos.test.ts +++ b/src/daemon/session-lifecycle/internal/__tests__/session-inventory-appleos.test.ts @@ -3,14 +3,16 @@ import { test, expect, vi, beforeEach } from 'vitest'; // The `devices` handler resolves its inventory through listDeviceInventory; mocking it // lets us drive the additive `appleOs` projection off the shared device fixtures without // touching real local discovery. -vi.mock('../../../../request/device-inventory-context.ts', async (importOriginal) => { +vi.mock('@agent-device/device-selection/device-inventory-context', async (importOriginal) => { const actual = - await importOriginal(); + await importOriginal< + typeof import('@agent-device/device-selection/device-inventory-context') + >(); return { ...actual, listDeviceInventory: vi.fn(async () => []) }; }); import { handleSessionInventoryCommands } from '../inventory.ts'; -import { listDeviceInventory } from '../../../../request/device-inventory-context.ts'; +import { listDeviceInventory } from '@agent-device/device-selection/device-inventory-context'; import { makeSessionStore } from '../../../../__tests__/test-utils/store-factory.ts'; import type { DaemonRequest, DaemonResponse } from '../../../daemon-request.ts'; import type { AppleOS, DeviceInfo } from '@agent-device/kernel/device'; diff --git a/src/daemon/session-lifecycle/internal/__tests__/session-open-execution-runtime.test.ts b/src/daemon/session-lifecycle/internal/__tests__/session-open-execution-runtime.test.ts index c9fbcb0245..dad7ddb26d 100644 --- a/src/daemon/session-lifecycle/internal/__tests__/session-open-execution-runtime.test.ts +++ b/src/daemon/session-lifecycle/internal/__tests__/session-open-execution-runtime.test.ts @@ -6,8 +6,9 @@ import { AppError } from '@agent-device/kernel/errors'; const mockResolveTargetDevice = vi.hoisted(() => vi.fn()); -vi.mock('../../../../core/dispatch-resolve.ts', async (importOriginal) => { - const actual = await importOriginal(); +vi.mock('@agent-device/device-selection/dispatch-resolve', async (importOriginal) => { + const actual = + await importOriginal(); const { selectionFromResolveTargetDevice } = await import('../../../__tests__/device-selection-stub.ts'); return { diff --git a/src/daemon/session-lifecycle/internal/__tests__/session-open-runtime.test.ts b/src/daemon/session-lifecycle/internal/__tests__/session-open-runtime.test.ts index 1e5a8af773..d92d1895ae 100644 --- a/src/daemon/session-lifecycle/internal/__tests__/session-open-runtime.test.ts +++ b/src/daemon/session-lifecycle/internal/__tests__/session-open-runtime.test.ts @@ -4,8 +4,9 @@ import path from 'node:path'; const mockResolveTargetDevice = vi.hoisted(() => vi.fn()); -vi.mock('../../../../core/dispatch-resolve.ts', async (importOriginal) => { - const actual = await importOriginal(); +vi.mock('@agent-device/device-selection/dispatch-resolve', async (importOriginal) => { + const actual = + await importOriginal(); const { selectionFromResolveTargetDevice } = await import('../../../__tests__/device-selection-stub.ts'); return { diff --git a/src/daemon/session-lifecycle/internal/inventory.ts b/src/daemon/session-lifecycle/internal/inventory.ts index 835b844dce..4090515829 100644 --- a/src/daemon/session-lifecycle/internal/inventory.ts +++ b/src/daemon/session-lifecycle/internal/inventory.ts @@ -2,7 +2,7 @@ import { commandRuntimeUseRequirements, listRuntimeFactCommands, } from '@agent-device/command-registry/registry'; -import { listDeviceInventory } from '../../../request/device-inventory-context.ts'; +import { listDeviceInventory } from '@agent-device/device-selection/device-inventory-context'; import { assertResolvedAppsFilter } from '@agent-device/contracts/device'; import { AppError, asAppError } from '@agent-device/kernel/errors'; import { diff --git a/src/daemon/session-lifecycle/internal/session-open-execution.ts b/src/daemon/session-lifecycle/internal/session-open-execution.ts index 232e4a312b..b26acc5f4e 100644 --- a/src/daemon/session-lifecycle/internal/session-open-execution.ts +++ b/src/daemon/session-lifecycle/internal/session-open-execution.ts @@ -6,7 +6,7 @@ import { import { markSelectionBootOccurred, type DeviceSelectionResult, -} from '../../../core/device-selection-resolver.ts'; +} from '@agent-device/device-selection/device-selection-resolver'; import type { BoundDeviceRuntime } from '@agent-device/contracts/platform-runtime'; import type { SessionSurface } from '@agent-device/contracts/session'; import type { DeviceInfo } from '@agent-device/kernel/device'; diff --git a/src/daemon/session-lifecycle/internal/session-open-surface.ts b/src/daemon/session-lifecycle/internal/session-open-surface.ts index 53a989b7e7..e45de9b360 100644 --- a/src/daemon/session-lifecycle/internal/session-open-surface.ts +++ b/src/daemon/session-lifecycle/internal/session-open-surface.ts @@ -8,7 +8,7 @@ import { import type { SessionRuntimeHints, SessionScope, SessionState } from '../../session-state.ts'; import { successText } from '@agent-device/kernel/success-text'; import type { StartupPerfSample } from './session-startup-metrics.ts'; -import type { DeviceSelectionResult } from '../../../core/device-selection-resolver.ts'; +import type { DeviceSelectionResult } from '@agent-device/device-selection/device-selection-resolver'; export function buildOpenResult(params: { sessionName: string; diff --git a/src/daemon/session-lifecycle/internal/session-open.ts b/src/daemon/session-lifecycle/internal/session-open.ts index dd7235f703..07c035e7e7 100644 --- a/src/daemon/session-lifecycle/internal/session-open.ts +++ b/src/daemon/session-lifecycle/internal/session-open.ts @@ -1,4 +1,4 @@ -import { resolveTargetDeviceSelection } from '../../../core/dispatch-resolve.ts'; +import { resolveTargetDeviceSelection } from '@agent-device/device-selection/dispatch-resolve'; import { openApplicationRuntimeUse, openApplicationWithRuntimeHintApplyAndClearUse, @@ -34,7 +34,7 @@ import type { InspectDeviceRuntimeFacts, } from '../../request-runtime-binding.ts'; import { admitRuntimeOperations } from '../../runtime-admission.ts'; -import { resolveExistingSessionDeviceSelection } from '../../../core/device-selection-resolver.ts'; +import { resolveExistingSessionDeviceSelection } from '@agent-device/device-selection/device-selection-resolver'; import { requireRuntimeBinding, requireRuntimeFacts } from '../../session-runtime-admission.ts'; import { completeOpenCommand, diff --git a/src/daemon/snapshot-session.ts b/src/daemon/snapshot-session.ts index 8ba08d21aa..8b89f573a2 100644 --- a/src/daemon/snapshot-session.ts +++ b/src/daemon/snapshot-session.ts @@ -1,4 +1,4 @@ -import { resolveTargetDevice } from '../core/dispatch-resolve.ts'; +import { resolveTargetDevice } from '@agent-device/device-selection/dispatch-resolve'; import type { PlatformResourceCleanup } from './platform-resource-cleanup.ts'; import type { DaemonRequest } from './daemon-request.ts'; import type { SessionScope, SessionState } from './session-state.ts'; diff --git a/src/mcp/__tests__/tool-error.test.ts b/src/mcp/__tests__/tool-error.test.ts index 6f8d4eb8cc..7bad324aea 100644 --- a/src/mcp/__tests__/tool-error.test.ts +++ b/src/mcp/__tests__/tool-error.test.ts @@ -54,7 +54,7 @@ test('formatToolErrorText renders a structured cause', () => { }); // Device-domain AMBIGUOUS_MATCH (findBootedAppleSimulatorWithApp, -// src/core/dispatch-resolve.ts) keys its list `devices`, so the MCP text path +// -device/device-selection/dispatch-resolve) keys its list `devices`, so the MCP text path // carries the udids the hint asks for — same block as the CLI. test('formatToolErrorText lists device-domain candidates udid-first', () => { const err = new AppError( diff --git a/src/platform-runtime-android-emulator-host.ts b/src/platform-runtime-android-emulator-host.ts index 5670e44190..8d8961f263 100644 --- a/src/platform-runtime-android-emulator-host.ts +++ b/src/platform-runtime-android-emulator-host.ts @@ -1,5 +1,5 @@ import type { DeviceReadinessRuntimeHost } from '@agent-device/contracts/device-readiness-runtime'; -import { listLocalDeviceInventory } from './request/device-inventory-context.ts'; +import { listLocalDeviceInventory } from '@agent-device/device-selection/device-inventory-context'; import { runCmdDetached } from '@agent-device/host-kit/command'; import { stopPidsWithEscalation } from '@agent-device/host-kit/process'; diff --git a/src/platform-runtime-device-inventory.test.ts b/src/platform-runtime-device-inventory.test.ts index cf00729aec..95404b0e7c 100644 --- a/src/platform-runtime-device-inventory.test.ts +++ b/src/platform-runtime-device-inventory.test.ts @@ -11,10 +11,28 @@ import type { DeviceInventoryHost, PlatformRequestScope, } from '@agent-device/contracts/platform-runtime-host'; +import { resolveTargetDeviceSelection } from '@agent-device/device-selection/dispatch-resolve'; +import { withDeviceInventoryContext } from '@agent-device/device-selection/device-inventory-context'; import { PLATFORMS, type DeviceInfo, type Platform } from '@agent-device/kernel/device'; import { describe, expect, test, vi } from 'vitest'; import { createComposedDeviceInventoryGateways } from './platform-runtime-device-inventory.ts'; +const simctl = vi.hoisted(() => ({ + listapps: [] as Array<{ deviceId: string; apps: Record }>, + calls: [] as string[], +})); + +vi.mock('../packages/platform-apple/src/core/tool-provider.ts', () => ({ + runXcrun: async (args: string[]) => { + const idx = args.indexOf('listapps'); + const deviceId = idx >= 0 ? args[idx + 1] : undefined; + if (deviceId) simctl.calls.push(deviceId); + const entry = simctl.listapps.find((l) => l.deviceId === deviceId); + return { stdout: entry ? JSON.stringify(entry.apps) : '{}', stderr: '', exitCode: 0 }; + }, + runAppleToolCommand: async () => ({ stdout: '', stderr: '', exitCode: 1 }), +})); + const scope: PlatformRequestScope = Object.freeze({ signal: new AbortController().signal, diagnostics: Object.freeze({ emit: () => {} }), @@ -239,6 +257,39 @@ describe('composed device inventory gateway', () => { details: { expectedFamily: 'android', actualFamily: 'linux' }, }); }); + + test('narrows app-based simulator selection through the factory-installed probe', async () => { + // Guards the production wiring: the package-level selection tests inject their own + // probe, so only this path proves the factory attaches one to the request context. + simctl.listapps = [ + { + deviceId: 'sim-b', + apps: { 'com.example.demo': { Bundle: 'com.example.demo', CFBundleName: 'Demo' } }, + }, + ]; + const local = inventoryWorld({ + apple: async () => source([simulator('sim-a'), simulator('sim-b')]), + }); + const gateways = createComposedDeviceInventoryGateways({ + registry: local.registry, + loadHost: local.loadHost, + }); + + const selection = await withDeviceInventoryContext({ ...gateways, requestScope: scope }, () => + resolveTargetDeviceSelection( + { platform: 'ios' }, + { + appleSimulatorAppTarget: 'com.example.demo', + }, + ), + ); + + expect(selection.device.id).toBe('sim-b'); + expect(selection.reason).toBe('single-app-installed-local'); + expect(selection.source).toBe('local'); + expect(selection.candidateCount).toBe(1); + expect([...simctl.calls].sort()).toEqual(['sim-a', 'sim-b']); + }); }); function inventoryWorld( @@ -283,3 +334,15 @@ function device(platform: Platform, id = `${platform}-device`): DeviceInfo { booted: true, }; } + +function simulator(id: string): DeviceInfo { + return { + platform: 'apple', + id, + name: id, + kind: 'simulator', + appleOs: 'ios', + target: 'mobile', + booted: true, + }; +} diff --git a/src/platform-runtime-device-inventory.ts b/src/platform-runtime-device-inventory.ts index 906e88f85c..f6f3c22792 100644 --- a/src/platform-runtime-device-inventory.ts +++ b/src/platform-runtime-device-inventory.ts @@ -8,6 +8,7 @@ import type { ComposedDeviceInventoryGateways, DeviceInventoryGateway, DeviceInventorySource, + InstalledAppProbe, InventoryPlatformModule, PlatformModuleRegistry, ProviderAwareDeviceInventoryGateway, @@ -62,7 +63,14 @@ export function createComposedDeviceInventoryGateways( discover: async (request, scope) => (await discoverWithSource(request, scope)).devices, discoverWithSource, }); - return Object.freeze({ providerFirst, localOnly }); + // Function-scoped: device selection reads this probe from the request context, and loading it + // must not evaluate Apple app-resolution mechanics until a selection actually narrows. + const findInstalledApp: InstalledAppProbe = async (device, appTarget) => { + const { findIosSimulatorInstalledApp } = + await import('@agent-device/platform-apple/app-resolution'); + return await findIosSimulatorInstalledApp(device, appTarget); + }; + return Object.freeze({ providerFirst, localOnly, findInstalledApp }); } function createLocalDeviceInventoryGateway( diff --git a/src/platform-runtime-open-target.ts b/src/platform-runtime-open-target.ts index fe9da66fcd..de600d1b4d 100644 --- a/src/platform-runtime-open-target.ts +++ b/src/platform-runtime-open-target.ts @@ -23,7 +23,7 @@ export async function resolveSoleForegroundIosApp( options: Readonly<{ simulatorSetPath?: string }> = {}, ): Promise { const { listLocalDeviceInventory, shouldPropagateDeviceInventoryProbeError } = - await import('./request/device-inventory-context.ts'); + await import('@agent-device/device-selection/device-inventory-context'); try { const booted = await listLocalDeviceInventory({ platform: 'ios', diff --git a/test/integration/provider-scenarios/android-world.ts b/test/integration/provider-scenarios/android-world.ts index d8ebf72b1a..8d51b3f1b7 100644 --- a/test/integration/provider-scenarios/android-world.ts +++ b/test/integration/provider-scenarios/android-world.ts @@ -8,7 +8,7 @@ import type { AndroidAdbProcess, AndroidAdbProvider, } from '@agent-device/platform-android/mechanics'; -import type { DeviceInventoryRequest } from '../../../src/core/dispatch-resolve.ts'; +import type { DeviceInventoryRequest } from '@agent-device/device-selection/dispatch-resolve'; import { ANDROID_IME_HELPER_FIXTURE_ARTIFACT } from '../../../src/__tests__/test-utils/android-ime-helper.ts'; import { ANDROID_SNAPSHOT_HELPER_FIXTURE_ARTIFACT, diff --git a/test/integration/provider-scenarios/ios-world.ts b/test/integration/provider-scenarios/ios-world.ts index 52a21f2f39..a012acebb2 100644 --- a/test/integration/provider-scenarios/ios-world.ts +++ b/test/integration/provider-scenarios/ios-world.ts @@ -1,6 +1,6 @@ import fs from 'node:fs'; import path from 'node:path'; -import type { DeviceInventoryRequest } from '../../../src/core/dispatch-resolve.ts'; +import type { DeviceInventoryRequest } from '@agent-device/device-selection/dispatch-resolve'; import { buildGesturePlan } from '@agent-device/contracts/gesture-plan'; import type { RawSnapshotNode } from '@agent-device/kernel/snapshot'; import { type ProviderScenarioTranscript, createProviderTranscript } from './transcript.ts';