From cdd1efb3230091cd5008e2e2743ef2ea7e2c2ea9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Mon, 7 Sep 2026 13:19:49 +0200 Subject: [PATCH 1/4] refactor(daemon): move the device selection cluster into a workspace package git-rename src/core/dispatch-resolve.ts, src/core/device-selection-resolver.ts and src/request/device-inventory-context.ts (plus their tests and fixtures) into a new private package @agent-device/device-selection (deps: contracts, host-kit, kernel). One subpath per module points straight at the moved file; no index.ts, no re-export at the old path. Consumers switch to the owning specifier in the follow-up commit. --- package.json | 3 ++- packages/device-selection/package.json | 27 +++++++++++++++++++ .../__tests__/device-selection-fixtures.ts | 0 .../device-selection-resolver.test.ts | 0 .../src}/__tests__/dispatch-resolve.test.ts | 0 .../src}/__tests__/dispatch-target.test.ts | 0 .../src}/device-inventory-context.ts | 0 .../src}/device-selection-resolver.ts | 0 .../device-selection/src}/dispatch-resolve.ts | 0 packages/device-selection/tsconfig.json | 12 +++++++++ pnpm-lock.yaml | 15 +++++++++++ 11 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 packages/device-selection/package.json rename {src/core => packages/device-selection/src}/__tests__/device-selection-fixtures.ts (100%) rename {src/core => packages/device-selection/src}/__tests__/device-selection-resolver.test.ts (100%) rename {src/core => packages/device-selection/src}/__tests__/dispatch-resolve.test.ts (100%) rename {src/core => packages/device-selection/src}/__tests__/dispatch-target.test.ts (100%) rename {src/request => packages/device-selection/src}/device-inventory-context.ts (100%) rename {src/core => packages/device-selection/src}/device-selection-resolver.ts (100%) rename {src/core => packages/device-selection/src}/dispatch-resolve.ts (100%) create mode 100644 packages/device-selection/tsconfig.json 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/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/src/core/__tests__/device-selection-fixtures.ts b/packages/device-selection/src/__tests__/device-selection-fixtures.ts similarity index 100% rename from src/core/__tests__/device-selection-fixtures.ts rename to packages/device-selection/src/__tests__/device-selection-fixtures.ts diff --git a/src/core/__tests__/device-selection-resolver.test.ts b/packages/device-selection/src/__tests__/device-selection-resolver.test.ts similarity index 100% rename from src/core/__tests__/device-selection-resolver.test.ts rename to packages/device-selection/src/__tests__/device-selection-resolver.test.ts diff --git a/src/core/__tests__/dispatch-resolve.test.ts b/packages/device-selection/src/__tests__/dispatch-resolve.test.ts similarity index 100% rename from src/core/__tests__/dispatch-resolve.test.ts rename to packages/device-selection/src/__tests__/dispatch-resolve.test.ts 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/src/request/device-inventory-context.ts b/packages/device-selection/src/device-inventory-context.ts similarity index 100% rename from src/request/device-inventory-context.ts rename to packages/device-selection/src/device-inventory-context.ts diff --git a/src/core/device-selection-resolver.ts b/packages/device-selection/src/device-selection-resolver.ts similarity index 100% rename from src/core/device-selection-resolver.ts rename to packages/device-selection/src/device-selection-resolver.ts diff --git a/src/core/dispatch-resolve.ts b/packages/device-selection/src/dispatch-resolve.ts similarity index 100% rename from src/core/dispatch-resolve.ts rename to packages/device-selection/src/dispatch-resolve.ts 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': From dcc325e74c48113e597ac6028830592ff7a22ba4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Tue, 8 Sep 2026 09:31:46 +0200 Subject: [PATCH 2/4] refactor(daemon): install the installed-app probe on the inventory gateways and switch consumers to the device-selection package The composition root (src/platform-runtime-device-inventory.ts) now attaches an optional findInstalledApp probe to the composed device inventory gateways, lazily importing the Apple simulator app-resolution mechanics. Device selection reads the probe from the request context instead of importing a platform package directly, so absent probes fall back to the ordinary inventory rules. All consumers switch to the @agent-device/device-selection/{dispatch-resolve,device-selection-resolver,device-inventory-context} subpaths; the moved tests carry a package-local inventory test util. --- packages/contracts/src/platform-module.ts | 11 ++ .../__tests__/device-selection-fixtures.ts | 27 ++++ .../device-selection-resolver.test.ts | 34 ++-- .../src/__tests__/dispatch-resolve.test.ts | 20 +-- .../test-utils/device-inventory-gateways.ts | 148 ++++++++++++++++++ .../src/device-inventory-context.ts | 19 ++- .../src/device-selection-resolver.ts | 9 +- .../device-selection/src/dispatch-resolve.ts | 2 +- .../test-utils/boundary-fault-matrix.ts | 2 +- .../test-utils/device-inventory-gateways.ts | 2 +- ...ssion-test-reporter-values-maestro.test.ts | 5 +- src/commands/output/error.test.ts | 2 +- src/daemon/__tests__/device-selection-stub.ts | 2 +- .../interaction-get-runtime-fixture.ts | 2 +- src/daemon/__tests__/is-runtime.test.ts | 2 +- .../request-router-dispatch-mocks.ts | 5 +- ...request-router-record-runtime-lock.test.ts | 5 +- .../__tests__/selector-text-runtime.test.ts | 2 +- .../session-device-resolution.test.ts | 4 +- .../system-surface-disclosure.test.ts | 7 +- src/daemon/__tests__/wait-runtime.test.ts | 2 +- .../__tests__/session-device-claims.test.ts | 7 +- .../__tests__/session-reinstall.test.ts | 7 +- .../__tests__/session-test-harness.ts | 7 +- .../__tests__/wait-landmark-recording.test.ts | 5 +- src/daemon/handlers/record-runtime.ts | 2 +- src/daemon/handlers/session-doctor-device.ts | 4 +- src/daemon/handlers/session-doctor.ts | 2 +- src/daemon/handlers/session.ts | 2 +- .../internal/__tests__/find.test.ts | 4 +- src/daemon/ios-app-session-hint.test.ts | 6 +- src/daemon/open-device-selection.ts | 2 +- src/daemon/replay-device-selection.ts | 2 +- ...sion-replay-dispatch-selector-miss.test.ts | 5 +- ...eplay-divergence-android-occlusion.test.ts | 5 +- .../session-replay-divergence.test.ts | 4 +- .../session-replay-maestro-failure.test.ts | 5 +- .../session-replay-repair-acceptance.test.ts | 5 +- .../session-replay-repair-empty-tail.test.ts | 5 +- .../session-replay-repair-loop.test.ts | 5 +- ...ion-replay-repair-record-exclusion.test.ts | 5 +- .../session-replay-repair-transaction.test.ts | 4 +- .../session-replay-runtime-binding.test.ts | 7 +- ...on-replay-runtime-failure-response.test.ts | 5 +- .../session-replay-runtime-failure.test.ts | 5 +- ...ession-replay-runtime-keep-session.test.ts | 5 +- .../session-replay-runtime-maestro.test.ts | 6 +- .../session-replay-runtime-plan.test.ts | 7 +- .../__tests__/session-replay-runtime.test.ts | 5 +- .../session-replay-script-source.test.ts | 5 +- .../session-replay-selector-routes.test.ts | 5 +- ...replay-target-verification-runtime.test.ts | 5 +- .../session-replay-maestro-runtime.ts | 2 +- .../internal/session-test-shard-devices.ts | 2 +- src/daemon/request-binding.ts | 2 +- .../request-platform-provider-context.ts | 2 +- src/daemon/request-router.ts | 4 +- src/daemon/selector-runtime-backend.ts | 2 +- src/daemon/session-device-resolution.ts | 2 +- .../__tests__/application.test.ts | 6 +- .../session-inventory-appleos.test.ts | 8 +- .../session-open-execution-runtime.test.ts | 5 +- .../__tests__/session-open-runtime.test.ts | 5 +- .../session-lifecycle/internal/inventory.ts | 2 +- .../internal/session-open-execution.ts | 2 +- .../internal/session-open-surface.ts | 2 +- .../internal/session-open.ts | 4 +- src/daemon/snapshot-session.ts | 2 +- src/mcp/__tests__/tool-error.test.ts | 2 +- src/platform-runtime-android-emulator-host.ts | 2 +- src/platform-runtime-device-inventory.ts | 10 +- src/platform-runtime-open-target.ts | 2 +- .../provider-scenarios/android-world.ts | 2 +- .../provider-scenarios/ios-world.ts | 2 +- 74 files changed, 380 insertions(+), 152 deletions(-) create mode 100644 packages/device-selection/src/__tests__/test-utils/device-inventory-gateways.ts 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/src/__tests__/device-selection-fixtures.ts b/packages/device-selection/src/__tests__/device-selection-fixtures.ts index b4e5838461..7ca2b8b287 100644 --- a/packages/device-selection/src/__tests__/device-selection-fixtures.ts +++ b/packages/device-selection/src/__tests__/device-selection-fixtures.ts @@ -17,3 +17,30 @@ export const SECOND_BOOTED_ANDROID_EMULATOR: DeviceInfo = { 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/packages/device-selection/src/__tests__/device-selection-resolver.test.ts b/packages/device-selection/src/__tests__/device-selection-resolver.test.ts index 210c2dd69e..2d1840a0ea 100644 --- a/packages/device-selection/src/__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/packages/device-selection/src/__tests__/dispatch-resolve.test.ts b/packages/device-selection/src/__tests__/dispatch-resolve.test.ts index 218442ab21..f6821dd8fa 100644 --- a/packages/device-selection/src/__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/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/packages/device-selection/src/device-inventory-context.ts b/packages/device-selection/src/device-inventory-context.ts index 5770d52711..0311ce21aa 100644 --- a/packages/device-selection/src/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/packages/device-selection/src/device-selection-resolver.ts b/packages/device-selection/src/device-selection-resolver.ts index 59c60e5200..4424958de6 100644 --- a/packages/device-selection/src/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/packages/device-selection/src/dispatch-resolve.ts b/packages/device-selection/src/dispatch-resolve.ts index f0d8553a4d..3847b8764c 100644 --- a/packages/device-selection/src/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/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/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.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'; From ee11cdb56ce267e3e2b7eb2a18eeb11bf5b4f809 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Tue, 8 Sep 2026 09:50:33 +0200 Subject: [PATCH 3/4] chore(gates): re-point the layering gates at the device-selection zone TARGET_DAG_RANK gains the device-selection leaf zone and drops the retired request zone; the back-edge fixture and the capture-kit ALS substrate fixture move to the package path. --- scripts/layering/model.test.ts | 6 ++++-- scripts/layering/model.ts | 2 +- scripts/layering/substrate-domain-shape.test.ts | 4 ++-- 3 files changed, 7 insertions(+), 5 deletions(-) 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", ), [], From b93bfe1e4a021990550bed6c59936d8ab55f81c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Tue, 8 Sep 2026 15:02:23 +0200 Subject: [PATCH 4/4] test(daemon): prove the factory-installed app probe narrows simulator selection The moved selection tests inject their own probe, so nothing covered the composition root actually attaching findInstalledApp to the gateways: omitting it would leave those tests green while app-based selection silently fell back to the generic local rules. The new case runs two booted simulators through createComposedDeviceInventoryGateways and the request context, fakes only the leaf xcrun spawn (core tool-provider), and asserts the single-app-installed-local selection plus both probe consults. --- src/platform-runtime-device-inventory.test.ts | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) 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, + }; +}