diff --git a/src/core/__tests__/capability-plugin-routing-parity.test.ts b/src/core/__tests__/capability-plugin-routing-parity.test.ts index d06105088..91c010e3f 100644 --- a/src/core/__tests__/capability-plugin-routing-parity.test.ts +++ b/src/core/__tests__/capability-plugin-routing-parity.test.ts @@ -29,7 +29,7 @@ import { import { deriveCapabilityForPlatform } from '../platform-descriptor/derive.ts'; import { platformDescriptors } from '../platform-descriptor/registry.ts'; import { getPlugin } from '../platform-plugin/plugin.ts'; -import { registerBuiltinPlatformPlugins } from '../platform-plugin/register-builtins.ts'; +import { registerBuiltinPlatformPlugins } from '../interactors/register-builtins.ts'; // Phase 3 step (b) parity gate. Independent oracles pin that the migration is // byte-for-byte behaviorless: diff --git a/src/core/capabilities.ts b/src/core/capabilities.ts index d6947f96c..5b8087d76 100644 --- a/src/core/capabilities.ts +++ b/src/core/capabilities.ts @@ -1,7 +1,7 @@ import { deriveCapabilityMatrix } from './command-descriptor/derive.ts'; import { commandDescriptors } from './command-descriptor/registry.ts'; import { tryGetPlugin } from './platform-plugin/plugin.ts'; -import { registerBuiltinPlatformPlugins } from './platform-plugin/register-builtins.ts'; +import { registerBuiltinPlatformPlugins } from './interactors/register-builtins.ts'; import type { DeviceInfo } from '../kernel/device.ts'; // Populate the PlatformPlugin registry once at module load (idempotent; registers diff --git a/src/core/command-descriptor/registry.ts b/src/core/command-descriptor/registry.ts index 2d13436d1..99bdaee49 100644 --- a/src/core/command-descriptor/registry.ts +++ b/src/core/command-descriptor/registry.ts @@ -39,8 +39,10 @@ const isShardedTestRequest = (req: DaemonRequest): boolean => // The per-command `supports()` / `unsupportedHint()` device closures that used to // live here were RELOCATED VERBATIM onto the owning PlatformPlugin's // `capability.supportsByDefault` / `unsupportedHintByDefault` in Phase 3 step b.2 -// (src/core/platform-plugin/register-builtins.ts). The capability facet now carries -// platform/kind buckets only; admission reads the closure off the plugin. +// (the Apple family's closures live on the Apple plugin, src/platforms/apple/plugin.ts; +// android/linux/web plugins are wired in src/core/interactors/register-builtins.ts). The +// capability facet now carries platform/kind buckets only; admission reads the closure +// off the plugin. // --------------------------------------------------------------------------- const APPLE_SIM_AND_DEVICE = { simulator: true, device: true }; diff --git a/src/core/interactors.ts b/src/core/interactors.ts index 77e26f65f..dd23f9a04 100644 --- a/src/core/interactors.ts +++ b/src/core/interactors.ts @@ -3,7 +3,7 @@ import { AppError } from '../kernel/errors.ts'; import { getProviderDeviceInteractor, isActiveProviderDevice } from '../provider-device-runtime.ts'; import type { Interactor, RunnerContext } from './interactor-types.ts'; import { getPlugin } from './platform-plugin/plugin.ts'; -import { registerBuiltinPlatformPlugins } from './platform-plugin/register-builtins.ts'; +import { registerBuiltinPlatformPlugins } from './interactors/register-builtins.ts'; // Populate the platform-plugin registry once, at module load (only registers // lazy closures — no leaf code is imported here, so CLI cold-start is unaffected). diff --git a/src/core/interactors/register-builtins.ts b/src/core/interactors/register-builtins.ts new file mode 100644 index 000000000..d93214c22 --- /dev/null +++ b/src/core/interactors/register-builtins.ts @@ -0,0 +1,117 @@ +import { registerPlatformPlugin, type PlatformPlugin } from '../platform-plugin/plugin.ts'; +import { applePlugin } from '../../platforms/apple/plugin.ts'; +import { PUBLIC_COMMANDS } from '../../command-catalog.ts'; +import { isAudioProbeSupportedDevice } from '../../kernel/audio-probe-support.ts'; +import { WEB_DESKTOP_DEVICE } from '../platform-inventory.ts'; +import type { Platform, DeviceInfo } from '../../kernel/device.ts'; +import type { DeviceInventoryRequest } from '../platform-inventory.ts'; + +// The builtin-plugin wiring lives at the interactor seam (src/core/interactors/) — +// the one place R3 (see scripts/layering/check.ts) permits a STATIC value import of +// `platforms/`, so this module can pull the relocated `applePlugin` +// (src/platforms/apple/plugin.ts) into the registry while the generic registry + type +// stay in `core/` (src/core/platform-plugin/plugin.ts) where non-interactor core code +// like `core/capabilities.ts` may import them. The Apple plugin instance and its +// capability closures now live under `platforms/apple/`; the android/linux/web wiring +// stays here. Each plugin WRAPS today's existing factories (src/core/interactors/*) and +// the inventory if-chain (src/core/platform-inventory.ts) as LAZY methods: the dynamic +// `import()`s and per-platform list calls are byte-for-byte the same as the +// hand-authored `getInteractor` switch arms and `listLocalDeviceInventory` branches. +// `as const satisfies PlatformPlugin` preserves each plugin's literal `platforms` tuple +// so the totality assertion below is a real compile-time check. + +const androidPlugin = { + id: 'android', + platforms: ['android'], + capability: { + bucket: 'android', + supportsByDefault: { [PUBLIC_COMMANDS.audio]: isAudioProbeSupportedDevice }, + }, + // Wraps the Android arm of `resolveLogBackend`: every Android device -> 'android'. + appLog: { resolveBackend: () => 'android' }, + // Wraps the Android arm of `supportsPlatformPerfMetrics`: every Android device + // reports perf-metrics support. + perf: { supportsMetrics: () => true }, + createInteractor: async (device: DeviceInfo) => { + const { createAndroidInteractor } = await import('./android.ts'); + return createAndroidInteractor(device); + }, + discoverDevices: async (request: DeviceInventoryRequest) => { + const { listAndroidDevices } = await import('../../platforms/android/devices.ts'); + return await listAndroidDevices({ + serialAllowlist: request.androidSerialAllowlist + ? new Set(request.androidSerialAllowlist) + : undefined, + }); + }, +} as const satisfies PlatformPlugin; + +const linuxPlugin = { + id: 'linux', + platforms: ['linux'], + capability: { bucket: 'linux' }, + createInteractor: async () => { + const { createLinuxInteractor } = await import('./linux.ts'); + return createLinuxInteractor(); + }, + discoverDevices: async () => { + const { listLinuxDevices } = await import('../../platforms/linux/devices.ts'); + return await listLinuxDevices(); + }, +} as const satisfies PlatformPlugin; + +const webPlugin = { + id: 'web', + platforms: ['web'], + capability: { bucket: 'web' }, + createInteractor: async () => { + const { createWebInteractor } = await import('./web.ts'); + return createWebInteractor(); + }, + // Mirrors the `request.platform === 'web'` branch (the single static device). + discoverDevices: async () => [WEB_DESKTOP_DEVICE], +} as const satisfies PlatformPlugin; + +/** + * The builtin plugins, in `PLATFORMS` order so `registeredPlatforms()` derives + * the canonical tuple's order (asserted by the parity test). + */ +export const BUILTIN_PLATFORM_PLUGINS = [ + applePlugin, + androidPlugin, + linuxPlugin, + webPlugin, +] as const satisfies readonly PlatformPlugin[]; + +// The leaf platforms covered by at least one builtin plugin, recovered from the +// preserved literal `platforms` tuples. +type CoveredPlatform = (typeof BUILTIN_PLATFORM_PLUGINS)[number]['platforms'][number]; + +/** + * Compile-time EXHAUSTIVENESS: a new `Platform` literal added to `PLATFORMS` + * without a plugin makes `Platform` no longer extend `CoveredPlatform`, so this + * alias resolves to `false`, violating the `extends true` constraint and failing + * the build. This is the registry counterpart of the deleted `getInteractor` + * switch's exhaustive `never` default. (Equivalent in spirit to the §5.1 + * `Object.fromEntries(registeredPlatforms()...) satisfies Record` + * sketch, but type-level so it cannot be satisfied vacuously by a runtime map.) + */ +type AssertTrue = T; +export type BuiltinPluginsCoverAllPlatforms = AssertTrue< + [Platform] extends [CoveredPlatform] ? true : false +>; + +let registered = false; + +/** + * Registers every builtin plugin into the shared registry exactly once + * (idempotent). Called at the top of `core/interactors.ts` so the registry is + * populated before any `getPlugin` lookup; safe to call again from tests. + */ +export function registerBuiltinPlatformPlugins(): void { + if (registered) return; + for (const plugin of BUILTIN_PLATFORM_PLUGINS) { + registerPlatformPlugin(plugin); + } + registered = true; +} diff --git a/src/core/platform-plugin/__tests__/apple-os-capability-table-parity.test.ts b/src/core/platform-plugin/__tests__/apple-os-capability-table-parity.test.ts index 8b80f359c..dfbf14cec 100644 --- a/src/core/platform-plugin/__tests__/apple-os-capability-table-parity.test.ts +++ b/src/core/platform-plugin/__tests__/apple-os-capability-table-parity.test.ts @@ -23,7 +23,7 @@ import { } from '../../../__tests__/test-utils/index.ts'; import { APPLE_OS_CAPABILITIES, resolveDeviceAppleOs } from '../apple-os-capabilities.ts'; import { getPlugin } from '../plugin.ts'; -import { registerBuiltinPlatformPlugins } from '../register-builtins.ts'; +import { registerBuiltinPlatformPlugins } from '../../interactors/register-builtins.ts'; // Phase 3 step d.5 table-equivalence gate. The AppleOS-axis predicates // (`target !== 'tv'` / `platform !== 'macos'` / `isTvOsDevice`) that used to be diff --git a/src/core/platform-plugin/__tests__/parity.test.ts b/src/core/platform-plugin/__tests__/parity.test.ts index 96bb26e79..42d0c79c0 100644 --- a/src/core/platform-plugin/__tests__/parity.test.ts +++ b/src/core/platform-plugin/__tests__/parity.test.ts @@ -4,7 +4,10 @@ import { PLATFORMS, type Platform } from '../../../kernel/device.ts'; import { AppError } from '../../../kernel/errors.ts'; import { platformDescriptors } from '../../platform-descriptor/registry.ts'; import { getPlugin, registeredPlatforms, registerPlatformPlugin, tryGetPlugin } from '../plugin.ts'; -import { BUILTIN_PLATFORM_PLUGINS, registerBuiltinPlatformPlugins } from '../register-builtins.ts'; +import { + BUILTIN_PLATFORM_PLUGINS, + registerBuiltinPlatformPlugins, +} from '../../interactors/register-builtins.ts'; // Idempotently populate the registry for this test module. registerBuiltinPlatformPlugins(); diff --git a/src/daemon/__tests__/applog-plugin-routing-parity.test.ts b/src/daemon/__tests__/applog-plugin-routing-parity.test.ts index 5802b7476..41791e42b 100644 --- a/src/daemon/__tests__/applog-plugin-routing-parity.test.ts +++ b/src/daemon/__tests__/applog-plugin-routing-parity.test.ts @@ -18,7 +18,7 @@ import { WEB_DESKTOP_DEVICE, } from '../../__tests__/test-utils/index.ts'; import { getPlugin } from '../../core/platform-plugin/plugin.ts'; -import { registerBuiltinPlatformPlugins } from '../../core/platform-plugin/register-builtins.ts'; +import { registerBuiltinPlatformPlugins } from '../../core/interactors/register-builtins.ts'; import { resolveLogBackend } from '../app-log.ts'; import type { LogBackend } from '../network-log.ts'; diff --git a/src/daemon/__tests__/perf-plugin-routing-parity.test.ts b/src/daemon/__tests__/perf-plugin-routing-parity.test.ts index 734203afe..17b258f81 100644 --- a/src/daemon/__tests__/perf-plugin-routing-parity.test.ts +++ b/src/daemon/__tests__/perf-plugin-routing-parity.test.ts @@ -19,7 +19,7 @@ import { WEB_DESKTOP_DEVICE, } from '../../__tests__/test-utils/index.ts'; import { getPlugin } from '../../core/platform-plugin/plugin.ts'; -import { registerBuiltinPlatformPlugins } from '../../core/platform-plugin/register-builtins.ts'; +import { registerBuiltinPlatformPlugins } from '../../core/interactors/register-builtins.ts'; import { buildPerfResponseData } from '../handlers/session-perf.ts'; import { PERF_UNAVAILABLE_REASON } from '../handlers/session-startup-metrics.ts'; diff --git a/src/daemon/app-log.ts b/src/daemon/app-log.ts index 6d30f1d48..a62d928a0 100644 --- a/src/daemon/app-log.ts +++ b/src/daemon/app-log.ts @@ -3,7 +3,7 @@ import path from 'node:path'; import type { DeviceInfo } from '../kernel/device.ts'; import { AppError } from '../kernel/errors.ts'; import { tryGetPlugin } from '../core/platform-plugin/plugin.ts'; -import { registerBuiltinPlatformPlugins } from '../core/platform-plugin/register-builtins.ts'; +import { registerBuiltinPlatformPlugins } from '../core/interactors/register-builtins.ts'; import { runCmd } from '../utils/exec.ts'; import { runXcrun } from '../platforms/apple/core/tool-provider.ts'; import { runAndroidAdb } from '../platforms/android/adb.ts'; diff --git a/src/daemon/handlers/session-perf.ts b/src/daemon/handlers/session-perf.ts index 8813f375a..4f12532df 100644 --- a/src/daemon/handlers/session-perf.ts +++ b/src/daemon/handlers/session-perf.ts @@ -3,7 +3,7 @@ import type { SessionAction, SessionState } from '../types.ts'; import { AppError, normalizeError } from '../../kernel/errors.ts'; import { isApplePlatform } from '../../kernel/device.ts'; import { tryGetPlugin } from '../../core/platform-plugin/plugin.ts'; -import { registerBuiltinPlatformPlugins } from '../../core/platform-plugin/register-builtins.ts'; +import { registerBuiltinPlatformPlugins } from '../../core/interactors/register-builtins.ts'; import type { AndroidAdbExecutor } from '../../platforms/android/adb-executor.ts'; import { ANDROID_HPROF_SNAPSHOT_DESCRIPTION, diff --git a/src/core/interactors/__tests__/apple-watchos-sentinel.test.ts b/src/platforms/apple/__tests__/watchos-sentinel.test.ts similarity index 93% rename from src/core/interactors/__tests__/apple-watchos-sentinel.test.ts rename to src/platforms/apple/__tests__/watchos-sentinel.test.ts index 32279566b..6a70705ca 100644 --- a/src/core/interactors/__tests__/apple-watchos-sentinel.test.ts +++ b/src/platforms/apple/__tests__/watchos-sentinel.test.ts @@ -1,7 +1,7 @@ import { test, expect } from 'vitest'; -import { createAppleInteractor } from '../apple.ts'; +import { createAppleInteractor } from '../interactor.ts'; import type { DeviceInfo } from '../../../kernel/device.ts'; -import type { RunnerContext } from '../../interactor-types.ts'; +import type { RunnerContext } from '../../../core/interactor-types.ts'; import { AppError } from '../../../kernel/errors.ts'; // watchOS is an explicit unsupported sentinel: XCUITest cannot drive watchOS UI, diff --git a/src/platforms/apple/core/__tests__/index.test.ts b/src/platforms/apple/core/__tests__/index.test.ts index fccd51e93..61c35830a 100644 --- a/src/platforms/apple/core/__tests__/index.test.ts +++ b/src/platforms/apple/core/__tests__/index.test.ts @@ -76,7 +76,7 @@ import { prepareSimulatorStatusBarForScreenshot as prepareStatusBarForScreenshot, } from '../screenshot-status-bar.ts'; import { runAppleRunnerCommand } from '../runner/runner-client.ts'; -import { iosRunnerOverrides } from '../../../ios/interactions.ts'; +import { iosRunnerOverrides } from '../../interactions.ts'; import { IOS_DEVICE_INSTALL_TIMEOUT_MS, IOS_SIMULATOR_TERMINATE_TIMEOUT_MS } from '../config.ts'; import type { DeviceInfo } from '../../../../kernel/device.ts'; import { withDiagnosticsScope } from '../../../../utils/diagnostics.ts'; diff --git a/src/platforms/ios/interactions.ts b/src/platforms/apple/interactions.ts similarity index 96% rename from src/platforms/ios/interactions.ts rename to src/platforms/apple/interactions.ts index 9744f9692..8d2a9f63b 100644 --- a/src/platforms/ios/interactions.ts +++ b/src/platforms/apple/interactions.ts @@ -1,20 +1,20 @@ import { isTvOsDevice, type DeviceInfo } from '../../kernel/device.ts'; import { assertScrollGestureInput, type ScrollDirection } from '../../core/scroll-gesture.ts'; import { normalizeScrollDurationMs, SCROLL_DURATION_MAX_MS } from '../../core/scroll-command.ts'; -import { runAppleRunnerCommand } from '../apple/core/runner/runner-client.ts'; +import { runAppleRunnerCommand } from './core/runner/runner-client.ts'; import { buildRunnerSequenceCommand, parseRunnerSequenceResult, -} from '../apple/core/runner/runner-sequence.ts'; -import type { RunnerCommand } from '../apple/core/runner/runner-contract.ts'; -import { appleRemotePressCommand } from '../apple/os/tvos/remote.ts'; -import { runMacosDesktopScroll } from '../apple/os/macos/desktop-scroll.ts'; +} from './core/runner/runner-sequence.ts'; +import type { RunnerCommand } from './core/runner/runner-contract.ts'; +import { appleRemotePressCommand } from './os/tvos/remote.ts'; +import { runMacosDesktopScroll } from './os/macos/desktop-scroll.ts'; import { normalizeAppleScrollResult, normalizeAppleScrollResultWithResolvedFrame, scrollRunnerFields, type AppleScrollOptions, -} from '../apple/core/scroll.ts'; +} from './core/scroll.ts'; import type { BackMode, Interactor, diff --git a/src/core/interactors/apple.ts b/src/platforms/apple/interactor.ts similarity index 92% rename from src/core/interactors/apple.ts rename to src/platforms/apple/interactor.ts index 294d3ef05..165b8f382 100644 --- a/src/core/interactors/apple.ts +++ b/src/platforms/apple/interactor.ts @@ -6,19 +6,16 @@ import { screenshotIos, setIosSetting, writeIosClipboardText, -} from '../../platforms/apple/core/apps.ts'; -import { - iosRunnerOverrides, - resolveAppleBackRunnerCommand, -} from '../../platforms/ios/interactions.ts'; -import { appleRemotePressCommand } from '../../platforms/apple/os/tvos/remote.ts'; -import { runMacOsScreenshotAction } from '../../platforms/apple/os/macos/helper.ts'; -import { runAppleRunnerCommand } from '../../platforms/apple/core/runner/runner-client.ts'; +} from './core/apps.ts'; +import { iosRunnerOverrides, resolveAppleBackRunnerCommand } from './interactions.ts'; +import { appleRemotePressCommand } from './os/tvos/remote.ts'; +import { runMacOsScreenshotAction } from './os/macos/helper.ts'; +import { runAppleRunnerCommand } from './core/runner/runner-client.ts'; import { withDiagnosticTimer } from '../../utils/diagnostics.ts'; import { isTvOsDevice, type DeviceInfo } from '../../kernel/device.ts'; import { AppError } from '../../kernel/errors.ts'; import type { RawSnapshotNode } from '../../kernel/snapshot.ts'; -import type { Interactor, RunnerContext } from '../interactor-types.ts'; +import type { Interactor, RunnerContext } from '../../core/interactor-types.ts'; import { readSnapshotQualityVerdict, type SnapshotQualityVerdict, diff --git a/src/core/platform-plugin/register-builtins.ts b/src/platforms/apple/plugin.ts similarity index 58% rename from src/core/platform-plugin/register-builtins.ts rename to src/platforms/apple/plugin.ts index 7da4d20fb..54645aedf 100644 --- a/src/core/platform-plugin/register-builtins.ts +++ b/src/platforms/apple/plugin.ts @@ -1,11 +1,11 @@ -import { appleOsCapabilities } from './apple-os-capabilities.ts'; -import { registerPlatformPlugin, type PlatformPlugin } from './plugin.ts'; +import { appleOsCapabilities } from '../../core/platform-plugin/apple-os-capabilities.ts'; +import type { PlatformPlugin } from '../../core/platform-plugin/plugin.ts'; import { PUBLIC_COMMANDS } from '../../command-catalog.ts'; import { isAudioProbeSupportedDevice } from '../../kernel/audio-probe-support.ts'; -import { shouldUseHostMacFastPath, WEB_DESKTOP_DEVICE } from '../platform-inventory.ts'; -import type { Platform, DeviceInfo } from '../../kernel/device.ts'; -import type { DeviceInventoryRequest } from '../platform-inventory.ts'; -import type { RunnerContext } from '../interactor-types.ts'; +import { shouldUseHostMacFastPath } from '../../core/platform-inventory.ts'; +import type { DeviceInfo } from '../../kernel/device.ts'; +import type { DeviceInventoryRequest } from '../../core/platform-inventory.ts'; +import type { RunnerContext } from '../../core/interactor-types.ts'; // --------------------------------------------------------------------------- // Apple family per-command capability closures. Originally RELOCATED VERBATIM from @@ -108,15 +108,15 @@ const APPLE_UNSUPPORTED_HINT_BY_DEFAULT: Record< 'transform-gesture': synthesisGestureUnsupportedHint, }; -// Each plugin WRAPS today's existing factories (src/core/interactors/*) and the -// inventory if-chain (src/core/platform-inventory.ts) as LAZY methods. No leaf -// code is rewritten: the dynamic `import()`s and the per-platform list calls are -// byte-for-byte the same as the hand-authored `getInteractor` switch arms and -// `listLocalDeviceInventory` branches. `as const satisfies PlatformPlugin` -// preserves each plugin's literal `platforms` tuple so the totality assertion -// below is a real compile-time check. +// The Apple plugin WRAPS today's existing factories (its `createInteractor` in +// `./interactor.ts`) and the inventory if-chain (src/core/platform-inventory.ts) as +// LAZY methods. No leaf code is rewritten: the dynamic `import()`s and the per-platform +// list calls are byte-for-byte the same as the hand-authored `getInteractor` switch arm +// and `listLocalDeviceInventory` branch. `as const satisfies PlatformPlugin` preserves +// the plugin's literal `platforms` tuple so the registry totality assertion (in +// `core/interactors/register-builtins.ts`) is a real compile-time check. -const applePlugin = { +export const applePlugin = { id: 'apple', // Apple owns BOTH leaf platforms today — mirrors `case 'ios': case 'macos':`. platforms: ['ios', 'macos'], @@ -140,116 +140,20 @@ const applePlugin = { // (ios/macos, any kind/target) reports perf-metrics support. perf: { supportsMetrics: () => true }, createInteractor: async (device: DeviceInfo, runner: RunnerContext) => { - const { createAppleInteractor } = await import('../interactors/apple.ts'); + const { createAppleInteractor } = await import('./interactor.ts'); return createAppleInteractor(device, runner); }, // Reproduces the macOS host fast-path + Apple-simulator branch of the // inventory if-chain, reusing the SAME predicate (no divergent copy). discoverDevices: async (request: DeviceInventoryRequest) => { if (shouldUseHostMacFastPath(request)) { - const { listMacosDevices } = await import('../../platforms/apple/os/macos/devices.ts'); + const { listMacosDevices } = await import('./os/macos/devices.ts'); return await listMacosDevices(); } - const { listAppleDevices } = await import('../../platforms/apple/core/devices.ts'); + const { listAppleDevices } = await import('./core/devices.ts'); return await listAppleDevices({ simulatorSetPath: request.iosSimulatorSetPath, udid: request.udid, }); }, } as const satisfies PlatformPlugin; - -const androidPlugin = { - id: 'android', - platforms: ['android'], - capability: { - bucket: 'android', - supportsByDefault: { [PUBLIC_COMMANDS.audio]: isAudioProbeSupportedDevice }, - }, - // Wraps the Android arm of `resolveLogBackend`: every Android device -> 'android'. - appLog: { resolveBackend: () => 'android' }, - // Wraps the Android arm of `supportsPlatformPerfMetrics`: every Android device - // reports perf-metrics support. - perf: { supportsMetrics: () => true }, - createInteractor: async (device: DeviceInfo) => { - const { createAndroidInteractor } = await import('../interactors/android.ts'); - return createAndroidInteractor(device); - }, - discoverDevices: async (request: DeviceInventoryRequest) => { - const { listAndroidDevices } = await import('../../platforms/android/devices.ts'); - return await listAndroidDevices({ - serialAllowlist: request.androidSerialAllowlist - ? new Set(request.androidSerialAllowlist) - : undefined, - }); - }, -} as const satisfies PlatformPlugin; - -const linuxPlugin = { - id: 'linux', - platforms: ['linux'], - capability: { bucket: 'linux' }, - createInteractor: async () => { - const { createLinuxInteractor } = await import('../interactors/linux.ts'); - return createLinuxInteractor(); - }, - discoverDevices: async () => { - const { listLinuxDevices } = await import('../../platforms/linux/devices.ts'); - return await listLinuxDevices(); - }, -} as const satisfies PlatformPlugin; - -const webPlugin = { - id: 'web', - platforms: ['web'], - capability: { bucket: 'web' }, - createInteractor: async () => { - const { createWebInteractor } = await import('../interactors/web.ts'); - return createWebInteractor(); - }, - // Mirrors the `request.platform === 'web'` branch (the single static device). - discoverDevices: async () => [WEB_DESKTOP_DEVICE], -} as const satisfies PlatformPlugin; - -/** - * The builtin plugins, in `PLATFORMS` order so `registeredPlatforms()` derives - * the canonical tuple's order (asserted by the parity test). - */ -export const BUILTIN_PLATFORM_PLUGINS = [ - applePlugin, - androidPlugin, - linuxPlugin, - webPlugin, -] as const satisfies readonly PlatformPlugin[]; - -// The leaf platforms covered by at least one builtin plugin, recovered from the -// preserved literal `platforms` tuples. -type CoveredPlatform = (typeof BUILTIN_PLATFORM_PLUGINS)[number]['platforms'][number]; - -/** - * Compile-time EXHAUSTIVENESS: a new `Platform` literal added to `PLATFORMS` - * without a plugin makes `Platform` no longer extend `CoveredPlatform`, so this - * alias resolves to `false`, violating the `extends true` constraint and failing - * the build. This is the registry counterpart of the deleted `getInteractor` - * switch's exhaustive `never` default. (Equivalent in spirit to the §5.1 - * `Object.fromEntries(registeredPlatforms()...) satisfies Record` - * sketch, but type-level so it cannot be satisfied vacuously by a runtime map.) - */ -type AssertTrue = T; -export type BuiltinPluginsCoverAllPlatforms = AssertTrue< - [Platform] extends [CoveredPlatform] ? true : false ->; - -let registered = false; - -/** - * Registers every builtin plugin into the shared registry exactly once - * (idempotent). Called at the top of `core/interactors.ts` so the registry is - * populated before any `getPlugin` lookup; safe to call again from tests. - */ -export function registerBuiltinPlatformPlugins(): void { - if (registered) return; - for (const plugin of BUILTIN_PLATFORM_PLUGINS) { - registerPlatformPlugin(plugin); - } - registered = true; -} diff --git a/src/sdk/index.ts b/src/sdk/index.ts index ab79afe95..13cdbf39f 100644 --- a/src/sdk/index.ts +++ b/src/sdk/index.ts @@ -2,134 +2,3 @@ export { createAgentDeviceClient } from '../client/client.ts'; export { createLocalArtifactAdapter } from '../io.ts'; export { AppError, isAgentDeviceError, normalizeAgentDeviceError } from '../kernel/errors.ts'; export { centerOfRect } from '../kernel/snapshot.ts'; - -export type { - ArtifactAdapter, - ArtifactDescriptor, - CreateTempFileOptions, - FileInputRef, - FileOutputRef, - LocalArtifactAdapterOptions, - OutputVisibility, - ReserveOutputOptions, - ReservedOutputFile, - ResolveInputOptions, - ResolvedInputFile, - TemporaryFile, -} from './io.ts'; - -export type { AppErrorCode, NormalizedError } from '../kernel/errors.ts'; - -export type { - ReplayTestReporter, - ReplayTestReporterContext, - ReplayTestReporterFactory, - ReplayTestReporterLoadContext, -} from '../replay/test/reporters/types.ts'; - -export type { CommandResult } from '../core/command-descriptor/command-result.ts'; -export type { ResponseLevel } from '../kernel/contracts.ts'; -export type { BootCommandResult, ShutdownCommandResult } from '../contracts/device.ts'; -export type { ViewportCommandResult } from '../contracts/viewport.ts'; -export type { - CloudArtifact, - CloudArtifactAvailability, - CloudArtifactKind, - CloudArtifactsResult, - CloudArtifactsStatus, -} from '../cloud-artifacts.ts'; - -export type { - AgentDeviceClient, - AgentDeviceClientConfig, - AgentDeviceCommandClient, - AgentDeviceDaemonTransport, - AgentDeviceDevice, - AgentDeviceIdentifiers, - AgentDeviceRequestOverrides, - AgentDeviceSelectionOptions, - AgentDeviceSession, - AgentDeviceSessionDevice, - AlertAction, - AlertCommandOptions, - AlertInfo, - AlertPlatform, - AlertSource, - AppCloseOptions, - AppCloseResult, - AppDeployOptions, - AppDeployResult, - AppInstallFromSourceOptions, - AppInstallFromSourceResult, - AppListOptions, - AppOpenOptions, - AppOpenResult, - AppPushOptions, - AppStateCommandOptions, - AppStateCommandResult, - AppSwitcherCommandOptions, - AppSwitcherCommandResult, - AppTriggerEventOptions, - BackCommandOptions, - BackCommandResult, - BatchRunOptions, - BatchRunResult, - BatchStep, - CaptureDiffOptions, - CaptureScreenshotOptions, - CaptureScreenshotResult, - CaptureSnapshotOptions, - CaptureSnapshotResult, - ClickOptions, - ClipboardCommandOptions, - ClipboardCommandResult, - CloudArtifactsOptions, - CommandRequestResult, - DeviceBootOptions, - DeviceShutdownOptions, - ElementTarget, - FillOptions, - FindLocator, - FindOptions, - FocusOptions, - GetOptions, - HomeCommandResult, - InteractionTarget, - IsOptions, - KeyboardCommandOptions, - KeyboardCommandResult, - LogsOptions, - LongPressOptions, - MaterializationReleaseOptions, - MaterializationReleaseResult, - MetroPrepareOptions, - MetroPrepareResult, - NetworkOptions, - PerfOptions, - PermissionTarget, - PinchOptions, - PressOptions, - RecordOptions, - ReplayRunOptions, - ReplayTestOptions, - RotateCommandOptions, - RotateCommandResult, - ScrollOptions, - SessionCloseResult, - SettingsUpdateOptions, - StartupPerfSample, - SwipeOptions, - TargetShutdownResult, - TraceOptions, - TypeTextOptions, - WaitCommandOptions, -} from '../client/client.ts'; - -export type { - Point, - Rect, - ScreenshotOverlayRef, - SnapshotNode, - SnapshotVisibility, - SnapshotVisibilityReason, -} from '../kernel/snapshot.ts'; diff --git a/src/utils/__tests__/interactors.test.ts b/src/utils/__tests__/interactors.test.ts index e3ff2acf4..a26f148e5 100644 --- a/src/utils/__tests__/interactors.test.ts +++ b/src/utils/__tests__/interactors.test.ts @@ -11,7 +11,7 @@ vi.mock('../../platforms/apple/core/runner/runner-client.ts', async (importOrigi }); import { getInteractor } from '../../core/interactors.ts'; -import { resolveAppleBackRunnerCommand } from '../../platforms/ios/interactions.ts'; +import { resolveAppleBackRunnerCommand } from '../../platforms/apple/interactions.ts'; import { runAppleRunnerCommand } from '../../platforms/apple/core/runner/runner-client.ts'; const iosSimulator: DeviceInfo = { diff --git a/test/integration/installed-package-metro.test.ts b/test/integration/installed-package-metro.test.ts index 3209fd334..c01f3acc8 100644 --- a/test/integration/installed-package-metro.test.ts +++ b/test/integration/installed-package-metro.test.ts @@ -270,11 +270,11 @@ test('installed package exposes Node APIs and packaged companion tunnel entrypoi consumerRoot, ['--input-type=module', '-e'], ` - import { createAgentDeviceClient, createLocalArtifactAdapter } from 'agent-device'; + import * as agentDeviceRoot from 'agent-device'; import 'agent-device/contracts'; import { createLocalArtifactAdapter as createIoArtifactAdapter } from 'agent-device/io'; import { buildBundleUrl, normalizeBaseUrl } from 'agent-device/metro'; - const client = createAgentDeviceClient(); + const client = agentDeviceRoot.createAgentDeviceClient(); const removedSubpaths = await Promise.all([ 'agent-device/backend', 'agent-device/commands', @@ -290,8 +290,11 @@ test('installed package exposes Node APIs and packaged companion tunnel entrypoi })); console.log(JSON.stringify({ bundleUrl: buildBundleUrl('https://public.example.test', 'ios'), + rootExports: Object.keys(agentDeviceRoot).sort(), rootClientSnapshot: typeof client.capture.snapshot, - rootArtifactAdapter: typeof createLocalArtifactAdapter({ cwd: process.cwd() }).reserveOutput, + rootArtifactAdapter: typeof agentDeviceRoot.createLocalArtifactAdapter({ + cwd: process.cwd(), + }).reserveOutput, ioArtifactAdapter: typeof createIoArtifactAdapter({ cwd: process.cwd() }).reserveOutput, removedSubpathsBlocked: removedSubpaths.every(Boolean), normalizedBaseUrl: normalizeBaseUrl('https://public.example.test///'), @@ -303,6 +306,14 @@ test('installed package exposes Node APIs and packaged companion tunnel entrypoi imports.bundleUrl, 'https://public.example.test/index.bundle?platform=ios&dev=true&minify=false', ); + assert.deepEqual(imports.rootExports, [ + 'AppError', + 'centerOfRect', + 'createAgentDeviceClient', + 'createLocalArtifactAdapter', + 'isAgentDeviceError', + 'normalizeAgentDeviceError', + ]); assert.equal(imports.rootClientSnapshot, 'function'); assert.equal(imports.rootArtifactAdapter, 'function'); assert.equal(imports.ioArtifactAdapter, 'function');