Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions src/__tests__/contracts-schema-public.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ import fs from 'node:fs';
import path from 'node:path';
import {
AppError,
type BackCommandResult,
type BootCommandResult,
type CommandResult,
type RotateCommandResult,
type ShutdownCommandResult,
type ViewportCommandResult,
} from '../index.ts';
Expand Down Expand Up @@ -126,9 +128,25 @@ test('public root exports typed command result contracts', () => {
} satisfies ViewportCommandResult;
const viewportFromMap: CommandResult<'viewport'> = viewport;

const back = {
action: 'back',
mode: 'in-app',
message: 'Back',
} satisfies BackCommandResult;
const backFromMap: CommandResult<'back'> = back;

const rotate = {
action: 'rotate',
orientation: 'portrait',
message: 'Rotated to portrait',
} satisfies RotateCommandResult;
const rotateFromMap: CommandResult<'rotate'> = rotate;

assert.equal(bootFromMap.booted, true);
assert.equal(shutdownFromMap.shutdown.success, true);
assert.equal(viewportFromMap.width, 390);
assert.equal(backFromMap.mode, 'in-app');
assert.equal(rotateFromMap.orientation, 'portrait');
});

test('public daemon request schema accepts GitHub Actions artifact install sources', () => {
Expand Down
30 changes: 10 additions & 20 deletions src/client-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,12 @@ export type { AlertAction, AlertInfo, AlertPlatform, AlertSource } from './alert
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 {
AppSwitcherCommandResult,
BackCommandResult,
HomeCommandResult,
RotateCommandResult,
} from './contracts/navigation.ts';

export type AgentDeviceDaemonTransport = (
req: Omit<DaemonRequest, 'token'>,
Expand Down Expand Up @@ -467,10 +473,6 @@ export type AlertCommandResult = DaemonResponseData & {
items?: string[];
};

type CommandActionResult<T extends string> = DaemonResponseData & {
action?: T;
};

export type AppStateCommandResult = DaemonResponseData & {
platform?: Platform;
appName?: string;
Expand All @@ -481,18 +483,6 @@ export type AppStateCommandResult = DaemonResponseData & {
surface?: SessionSurface;
};

export type BackCommandResult = CommandActionResult<'back'> & {
mode?: BackMode;
};

export type HomeCommandResult = CommandActionResult<'home'>;

export type RotateCommandResult = CommandActionResult<'rotate'> & {
orientation?: RotateCommandOptions['orientation'];
};

export type AppSwitcherCommandResult = CommandActionResult<'app-switcher'>;

export type KeyboardCommandResult = DaemonResponseData & {
platform?: 'android' | 'ios';
action?: 'status' | 'dismiss' | 'enter';
Expand Down Expand Up @@ -536,10 +526,10 @@ export type AgentDeviceCommandClient = {
wait: (options: WaitCommandOptions) => Promise<WaitCommandResult>;
alert: (options?: AlertCommandOptions) => Promise<AlertCommandResult>;
appState: (options?: AppStateCommandOptions) => Promise<AppStateCommandResult>;
back: (options?: BackCommandOptions) => Promise<BackCommandResult>;
home: (options?: HomeCommandOptions) => Promise<HomeCommandResult>;
rotate: (options: RotateCommandOptions) => Promise<RotateCommandResult>;
appSwitcher: (options?: AppSwitcherCommandOptions) => Promise<AppSwitcherCommandResult>;
back: (options?: BackCommandOptions) => Promise<CommandResult<'back'>>;
home: (options?: HomeCommandOptions) => Promise<CommandResult<'home'>>;
rotate: (options: RotateCommandOptions) => Promise<CommandResult<'rotate'>>;
appSwitcher: (options?: AppSwitcherCommandOptions) => Promise<CommandResult<'app-switcher'>>;
keyboard: (options?: KeyboardCommandOptions) => Promise<KeyboardCommandResult>;
clipboard: (options: ClipboardCommandOptions) => Promise<ClipboardCommandResult>;
reactNative: (options: ReactNativeCommandOptions) => Promise<CommandRequestResult>;
Expand Down
9 changes: 5 additions & 4 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,10 +104,11 @@ export function createAgentDeviceClient(
wait: async (options) => await executeCommand('wait', options),
alert: async (options = {}) => await executeCommand('alert', options),
appState: async (options = {}) => await executeCommand('appstate', options),
back: async (options = {}) => await executeCommand('back', options),
home: async (options = {}) => await executeCommand('home', options),
rotate: async (options) => await executeCommand('rotate', options),
appSwitcher: async (options = {}) => await executeCommand('app-switcher', options),
back: async (options = {}) => await executeCommand<CommandResult<'back'>>('back', options),
home: async (options = {}) => await executeCommand<CommandResult<'home'>>('home', options),
rotate: async (options) => await executeCommand<CommandResult<'rotate'>>('rotate', options),
appSwitcher: async (options = {}) =>
await executeCommand<CommandResult<'app-switcher'>>('app-switcher', options),
keyboard: async (options = {}) => await executeCommand('keyboard', options),
clipboard: async (options) => await executeCommand('clipboard', options),
reactNative: async (options) => await executeCommand('react-native', options),
Expand Down
38 changes: 38 additions & 0 deletions src/contracts/navigation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import type { BackMode } from '../core/back-mode.ts';
import type { DeviceRotation } from '../core/device-rotation.ts';

/**
* Closed results of the navigation/global action commands. Each mirrors the
* dispatch handler's literal return EXACTLY (src/core/dispatch.ts
* `DISPATCH_HANDLERS`): a fixed `action` discriminant plus the always-present
* `successText` message (the handlers always pass a non-empty message, so it is
* required here). The handlers spread nothing else, so the shapes are closed —
* consistent with the `viewport` contract, the generic-dispatch Android
* dialog-recovery `warning` annotation is intentionally not part of the contract.
*/

/** `home` — `{ action: 'home', message: 'Home' }`. */
export type HomeCommandResult = {
action: 'home';
message: string;
};

/** `back` — `{ action: 'back', mode, message: 'Back' }`; `mode` defaults to `'in-app'`. */
export type BackCommandResult = {
action: 'back';
mode: BackMode;
message: string;
};

/** `rotate` — `{ action: 'rotate', orientation, message: 'Rotated to <orientation>' }`. */
export type RotateCommandResult = {
action: 'rotate';
orientation: DeviceRotation;
message: string;
};

/** `app-switcher` — `{ action: 'app-switcher', message: 'Opened app switcher' }`. */
export type AppSwitcherCommandResult = {
action: 'app-switcher';
message: string;
};
41 changes: 32 additions & 9 deletions src/core/command-descriptor/__tests__/command-result.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ import type {
} from '../../../contracts/interaction.ts';
import type { BootCommandResult, ShutdownCommandResult } from '../../../contracts/device.ts';
import type { ViewportCommandResult } from '../../../contracts/viewport.ts';
import type {
AppSwitcherCommandResult,
BackCommandResult,
HomeCommandResult,
RotateCommandResult,
} from '../../../contracts/navigation.ts';
import type { CommandResult, CommandResultMap } from '../command-result.ts';

/**
Expand All @@ -24,14 +30,22 @@ test('seeded CommandResult entries resolve to their existing contract result typ
const boot: Equal<CommandResult<'boot'>, BootCommandResult> = true;
const shutdown: Equal<CommandResult<'shutdown'>, ShutdownCommandResult> = true;
const viewport: Equal<CommandResult<'viewport'>, ViewportCommandResult> = true;
expect([press, fill, longPress, boot, shutdown, viewport]).toEqual([
true,
true,
true,
true,
true,
true,
]);
const home: Equal<CommandResult<'home'>, HomeCommandResult> = true;
const back: Equal<CommandResult<'back'>, BackCommandResult> = true;
const rotate: Equal<CommandResult<'rotate'>, RotateCommandResult> = true;
const appSwitcher: Equal<CommandResult<'app-switcher'>, AppSwitcherCommandResult> = true;
expect([
press,
fill,
longPress,
boot,
shutdown,
viewport,
home,
back,
rotate,
appSwitcher,
]).toEqual([true, true, true, true, true, true, true, true, true, true]);
});

test('unmigrated commands fall back to the untyped Record bag, keeping the union total', () => {
Expand All @@ -44,7 +58,16 @@ 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<
keyof CommandResultMap,
'press' | 'fill' | 'longpress' | 'boot' | 'shutdown' | 'viewport'
| 'press'
| 'fill'
| 'longpress'
| 'boot'
| 'shutdown'
| 'viewport'
| 'home'
| 'back'
| 'rotate'
| 'app-switcher'
> = true;
expect(keys).toBe(true);
});
20 changes: 15 additions & 5 deletions src/core/command-descriptor/command-result.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ import type {
} from '../../contracts/interaction.ts';
import type { BootCommandResult, ShutdownCommandResult } from '../../contracts/device.ts';
import type { ViewportCommandResult } from '../../contracts/viewport.ts';
import type {
AppSwitcherCommandResult,
BackCommandResult,
HomeCommandResult,
RotateCommandResult,
} from '../../contracts/navigation.ts';

/**
* The additive typed-result spine (ADR-0008, Phase 1 step 6).
Expand All @@ -16,11 +22,11 @@ import type { ViewportCommandResult } from '../../contracts/viewport.ts';
* visualization, perf, logs, …) are deliberately omitted rather than given an
* invented shape.
*
* 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.
* Batch 1 wired `boot` / `shutdown` / `viewport` alongside the seed interaction
* trio (`press` / `fill` / `longpress`). Batch 2 adds the closed navigation/
* action commands `home` / `back` / `rotate` / `app-switcher` (each a fixed
* `{ action, …, message }`). 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;
Expand All @@ -29,6 +35,10 @@ export interface CommandResultMap {
boot: BootCommandResult;
shutdown: ShutdownCommandResult;
viewport: ViewportCommandResult;
home: HomeCommandResult;
back: BackCommandResult;
rotate: RotateCommandResult;
'app-switcher': AppSwitcherCommandResult;
}

/**
Expand Down
Loading