diff --git a/src/__tests__/contracts-schema-public.test.ts b/src/__tests__/contracts-schema-public.test.ts index 3cf8a8cee..69d04a6f5 100644 --- a/src/__tests__/contracts-schema-public.test.ts +++ b/src/__tests__/contracts-schema-public.test.ts @@ -2,7 +2,13 @@ import { test } from 'vitest'; import assert from 'node:assert/strict'; import fs from 'node:fs'; import path from 'node:path'; -import { AppError } from '../index.ts'; +import { + AppError, + type BootCommandResult, + type CommandResult, + type ShutdownCommandResult, + type ViewportCommandResult, +} from '../index.ts'; import { defaultHintForCode, daemonCommandRequestSchema, @@ -87,6 +93,44 @@ test('public contract schemas validate daemon requests and lease payloads', () = assert.equal(node.ref, 'e1'); }); +test('public root exports typed command result contracts', () => { + const boot = { + platform: 'ios', + target: 'mobile', + device: 'iPhone 17', + id: 'booted-device', + kind: 'simulator', + booted: true, + } satisfies BootCommandResult; + const bootFromMap: CommandResult<'boot'> = boot; + + const shutdown = { + platform: 'ios', + target: 'mobile', + device: 'iPhone 17', + id: 'shutdown-device', + kind: 'simulator', + shutdown: { + success: true, + exitCode: 0, + stdout: '', + stderr: '', + }, + } satisfies ShutdownCommandResult; + const shutdownFromMap: CommandResult<'shutdown'> = shutdown; + + const viewport = { + width: 390, + height: 844, + message: 'Viewport is 390x844', + } satisfies ViewportCommandResult; + const viewportFromMap: CommandResult<'viewport'> = viewport; + + assert.equal(bootFromMap.booted, true); + assert.equal(shutdownFromMap.shutdown.success, true); + assert.equal(viewportFromMap.width, 390); +}); + test('public daemon request schema accepts GitHub Actions artifact install sources', () => { const artifactIdRequest = daemonCommandRequestSchema.parse({ command: 'install_source', diff --git a/src/client-types.ts b/src/client-types.ts index ef62703eb..1831bb14c 100644 --- a/src/client-types.ts +++ b/src/client-types.ts @@ -44,12 +44,15 @@ import type { PerfAction, PerfArea, PerfKind, PerfSubject } from './contracts/pe import type { AlertAction, AlertInfo } from './alert-contract.ts'; import type { DebugSymbolsOptions, DebugSymbolsResult } from './contracts/debug-symbols.ts'; import type { RemoteConnectionProfileFields } from './remote-config-schema.ts'; +import type { CommandResult } from './core/command-descriptor/command-result.ts'; export type { FindLocator } from './utils/finders.ts'; export type { CompanionTunnelScope, MetroBridgeScope } from './client-companion-tunnel-contract.ts'; export type { AppsFilter } from './contracts/app-inventory.ts'; export type { AlertAction, AlertInfo, AlertPlatform, AlertSource } from './alert-contract.ts'; export type { DebugSymbolsOptions, DebugSymbolsResult } from './contracts/debug-symbols.ts'; +export type { BootCommandResult, ShutdownCommandResult } from './contracts/device.ts'; +export type { ViewportCommandResult } from './contracts/viewport.ts'; export type AgentDeviceDaemonTransport = ( req: Omit, @@ -520,11 +523,6 @@ export type ViewportCommandOptions = DeviceCommandBaseOptions & { height: number; }; -export type ViewportCommandResult = CommandRequestResult & { - width: number; - height: number; -}; - export type AgentDeviceCommandClient = { wait: (options: WaitCommandOptions) => Promise; alert: (options?: AlertCommandOptions) => Promise; @@ -537,7 +535,7 @@ export type AgentDeviceCommandClient = { clipboard: (options: ClipboardCommandOptions) => Promise; reactNative: (options: ReactNativeCommandOptions) => Promise; prepare: (options: PrepareCommandOptions) => Promise; - viewport: (options: ViewportCommandOptions) => Promise; + viewport: (options: ViewportCommandOptions) => Promise>; }; type SelectorSnapshotCommandOptions = Pick; @@ -942,8 +940,8 @@ export type AgentDeviceClient = { list: ( options?: AgentDeviceRequestOverrides & AgentDeviceSelectionOptions, ) => Promise; - boot: (options?: DeviceBootOptions) => Promise; - shutdown: (options?: DeviceShutdownOptions) => Promise; + boot: (options?: DeviceBootOptions) => Promise>; + shutdown: (options?: DeviceShutdownOptions) => Promise>; }; sessions: { list: (options?: AgentDeviceRequestOverrides) => Promise; diff --git a/src/client.ts b/src/client.ts index 9f5d6d6c3..f4248fb8a 100644 --- a/src/client.ts +++ b/src/client.ts @@ -43,8 +43,8 @@ import type { Lease, MaterializationReleaseOptions, MetroPrepareOptions, - ViewportCommandResult, } from './client-types.ts'; +import type { CommandResult } from './core/command-descriptor/command-result.ts'; import { readSerializedSnapshotCaptureAnnotations } from './snapshot-capture-annotations.ts'; import { readSnapshotDiagnosticsSummary } from './snapshot-diagnostics.ts'; import type { CommandFlags } from './core/dispatch-context.ts'; @@ -111,7 +111,8 @@ export function createAgentDeviceClient( clipboard: async (options) => await executeCommand('clipboard', options), reactNative: async (options) => await executeCommand('react-native', options), prepare: async (options) => await executeCommand('prepare', options), - viewport: async (options) => await executeCommand('viewport', options), + viewport: async (options) => + await executeCommand>('viewport', options), }, devices: { list: async (options = {}) => { @@ -119,8 +120,9 @@ export function createAgentDeviceClient( const devices = Array.isArray(data.devices) ? data.devices : []; return devices.map(normalizeDevice); }, - boot: async (options = {}) => await executeCommand('boot', options), - shutdown: async (options = {}) => await executeCommand('shutdown', options), + boot: async (options = {}) => await executeCommand>('boot', options), + shutdown: async (options = {}) => + await executeCommand>('shutdown', options), }, sessions: { list: async (options = {}) => await listSessions(options), diff --git a/src/contracts/device.ts b/src/contracts/device.ts new file mode 100644 index 000000000..2ae2bcbc1 --- /dev/null +++ b/src/contracts/device.ts @@ -0,0 +1,37 @@ +import type { DeviceKind, DeviceTarget, Platform } from '../utils/device.ts'; +import type { TargetShutdownResult } from '../target-shutdown-contract.ts'; + +/** + * Closed result of the `boot` command. Mirrors the daemon handler's only + * success return EXACTLY (src/daemon/handlers/session-state.ts) — the fixed + * object literal `{ platform, target, device, id, kind, booted }`. The handler + * spreads nothing, so this shape is intentionally closed. + */ +export type BootCommandResult = { + platform: Platform; + target: DeviceTarget; + /** Human-readable device name (`device.name`). */ + device: string; + /** Stable device id (`device.id`). */ + id: string; + kind: DeviceKind; + /** Always `true` on the success path. */ + booted: true; +}; + +/** + * Closed result of the `shutdown` command. Mirrors the daemon handler's success + * return EXACTLY (src/daemon/handlers/session-state.ts) — the fixed object + * literal `{ platform, target, device, id, kind, shutdown }`. The `shutdown` + * field is the raw {@link TargetShutdownResult} from `shutdownDeviceTarget`. + */ +export type ShutdownCommandResult = { + platform: Platform; + target: DeviceTarget; + /** Human-readable device name (`device.name`). */ + device: string; + /** Stable device id (`device.id`). */ + id: string; + kind: DeviceKind; + shutdown: TargetShutdownResult; +}; diff --git a/src/contracts/viewport.ts b/src/contracts/viewport.ts new file mode 100644 index 000000000..2301361a7 --- /dev/null +++ b/src/contracts/viewport.ts @@ -0,0 +1,12 @@ +/** + * Closed result of the `viewport` command. Mirrors the dispatch handler's return + * EXACTLY (src/core/dispatch.ts `handleViewportCommand`) — `{ width, height }` + * plus the always-present `successText` message. The generic dispatch path + * returns this object unchanged (viewport has no Android dialog guard, so no + * `warning` is ever appended), so the shape is intentionally closed. + */ +export type ViewportCommandResult = { + width: number; + height: number; + message: string; +}; diff --git a/src/core/command-descriptor/__tests__/command-result.test.ts b/src/core/command-descriptor/__tests__/command-result.test.ts index 380f64c6d..8aab6cd79 100644 --- a/src/core/command-descriptor/__tests__/command-result.test.ts +++ b/src/core/command-descriptor/__tests__/command-result.test.ts @@ -4,6 +4,8 @@ import type { LongPressCommandResult, PressCommandResult, } from '../../../contracts/interaction.ts'; +import type { BootCommandResult, ShutdownCommandResult } from '../../../contracts/device.ts'; +import type { ViewportCommandResult } from '../../../contracts/viewport.ts'; import type { CommandResult, CommandResultMap } from '../command-result.ts'; /** @@ -19,7 +21,17 @@ test('seeded CommandResult entries resolve to their existing contract result typ const press: Equal, PressCommandResult> = true; const fill: Equal, FillCommandResult> = true; const longPress: Equal, LongPressCommandResult> = true; - expect([press, fill, longPress]).toEqual([true, true, true]); + const boot: Equal, BootCommandResult> = true; + const shutdown: Equal, ShutdownCommandResult> = true; + const viewport: Equal, ViewportCommandResult> = true; + expect([press, fill, longPress, boot, shutdown, viewport]).toEqual([ + true, + true, + true, + true, + true, + true, + ]); }); test('unmigrated commands fall back to the untyped Record bag, keeping the union total', () => { @@ -30,6 +42,9 @@ test('unmigrated commands fall back to the untyped Record bag, keeping the union }); test('CommandResultMap is seeded only from already-existing contract result types', () => { - const keys: Equal = true; + const keys: Equal< + keyof CommandResultMap, + 'press' | 'fill' | 'longpress' | 'boot' | 'shutdown' | 'viewport' + > = true; expect(keys).toBe(true); }); diff --git a/src/core/command-descriptor/command-result.ts b/src/core/command-descriptor/command-result.ts index 38f2817b6..ace5177be 100644 --- a/src/core/command-descriptor/command-result.ts +++ b/src/core/command-descriptor/command-result.ts @@ -3,26 +3,32 @@ import type { LongPressCommandResult, PressCommandResult, } from '../../contracts/interaction.ts'; +import type { BootCommandResult, ShutdownCommandResult } from '../../contracts/device.ts'; +import type { ViewportCommandResult } from '../../contracts/viewport.ts'; /** * The additive typed-result spine (ADR-0008, Phase 1 step 6). * - * Maps a command name to the *already-existing* per-command result type from - * `src/contracts/*`. It is SEEDED, not exhaustive: only commands whose accurate - * result shape already lives in the contracts layer are listed here. Today that - * is the interaction trio (`press` / `fill` / `longpress`); screenshot, perf, - * logs and friends have no contracts-layer result type yet, so they are - * deliberately omitted rather than given an invented shape. + * Maps a command name to the per-command result type from `src/contracts/*`. It + * is SEEDED, not exhaustive: a command is listed here only once its accurate, + * closed result shape lives in the contracts layer. Commands whose daemon + * handler spreads dynamic/Record data (screenshot overlays, gesture + * visualization, perf, logs, …) are deliberately omitted rather than given an + * invented shape. * - * This map is dormant: nothing reads it yet. It exists as the foundation that - * later slices consume to derive `client-types.ts` and delete the hand-authored - * `*Result` mirror — the same dormant-but-proven pattern as the #906 descriptor - * registry and the #910 dispatch map this slice is stacked on. + * Phase 2 batch 1 wires the first map entries into the public client return + * types: `boot` / `shutdown` (closed device-lifecycle results) and `viewport` + * (closed `{ width, height, message }`) join the seed interaction trio + * (`press` / `fill` / `longpress`). Each entry is grounded in a re-read of the + * handler's literal return; see the per-type docstrings for the file source. */ export interface CommandResultMap { press: PressCommandResult; fill: FillCommandResult; longpress: LongPressCommandResult; + boot: BootCommandResult; + shutdown: ShutdownCommandResult; + viewport: ViewportCommandResult; } /** diff --git a/src/index.ts b/src/index.ts index c5c188869..f48ac0866 100644 --- a/src/index.ts +++ b/src/index.ts @@ -20,6 +20,10 @@ export type { export type { AppErrorCode, NormalizedError } from './utils/errors.ts'; +export type { CommandResult } from './core/command-descriptor/command-result.ts'; +export type { BootCommandResult, ShutdownCommandResult } from './contracts/device.ts'; +export type { ViewportCommandResult } from './contracts/viewport.ts'; + export type { AgentDeviceClient, AgentDeviceClientConfig,