Skip to content

Commit 392dc1c

Browse files
authored
refactor: rename rotate command to orientation (rotate kept as deprecated alias) (#1252)
* refactor: rename rotate command to orientation, keep rotate as a deprecated alias The top-level `rotate` command (device orientation: portrait/landscape) shared a name with the `gesture rotate` two-finger rotation gesture. Rename the orientation command to `orientation` and keep `rotate` working as a minimal, silent CLI alias (same mechanism as `tap`->`press`) for a few versions. The rename is applied across every layer: - command-descriptor registry `name`, daemon dispatch handler, and the typed system facet (metadata/cliReader/daemonWriter/schema/output formatter) - navigation projection + `CommandResultMap` (`OrientationCommandResult`, `action: 'orientation'`), client types (`OrientationCommandOptions`), and the runtime family (`device.system.orientation`) - interactor + backend methods -> `setOrientation` (matching the backend's `setKeyboard`/`setClipboard` verb convention); Android helper `rotateAndroid` -> `setAndroidOrientation` - Apple/cloud-webdriver capability keys and plugin gate - user-facing docs (commands.md, client-api.md) Client SDK method is `orientation` (client convention = camelCase of the command name, matching `back`/`home`/`appSwitcher`); execution layers use the imperative `setOrientation`. Deliberately unchanged: - the Swift runner wire protocol keeps `command: 'rotate'` — the runner has its own command namespace with no gesture collision, so renaming it would only risk CLI<->installed-runner version skew on physical devices - the `DeviceRotation` value type / `parseDeviceRotation` (names the orientation values, no collision) Note: `client.command.rotate` / `device.system.rotate` and the `RotateCommand*` exported types are removed (the alias only rewrites CLI tokens); SDK consumers must use `orientation`. The JSON `action` value changes `rotate` -> `orientation`. * style: wrap long lines to satisfy oxfmt (orientation rename tests) * fix: add compatibility layer for the rotate->orientation rename Addresses review blockers on the CLI-only alias: `rotate` previously resolved only in CLI token parsing, so command-data/RPC paths that carry the wire command directly failed descriptor validation, and the removed typed SDK surface broke shipped consumers. Central command-alias boundary (was CLI-only): - Promote `cli-command-aliases.ts` to `command-aliases.ts` as the single alias source, applied at each command-name ingress that bypasses the CLI parser: the daemon request boundary (`handleRequest`, covering replay and older remote clients) and the batch step readers (CLI `batch-steps.ts` and daemon `batch-policy.ts`). No hand-synced command tables. Retain deprecated typed SDK surface (shipped v0.18/v0.19): - `RotateCommandOptions` / `RotateCommandResult` type aliases (legacy `action: 'rotate'` contract) and `SystemRotate*` runtime types. - `client.command.rotate` and `device.system.rotate` deprecated wrappers that delegate to `orientation` and restore the legacy response (`action: 'rotate'` / `kind: 'systemRotated'`). ADR 0014: rename `rotate` -> `orientation` in the invalidation guidance (lines 229, 237) so the accepted architecture doc matches the command name. Tests: daemon-boundary rewrite, CLI+daemon batch alias resolution, and the deprecated client/runtime wrappers preserving the legacy contract. Live emulator evidence (emulator-5554): - `orientation landscape-left` -> user_rotation=1 - `rotate portrait` (CLI alias) -> user_rotation=0 - batch step `{command:'rotate'}` (no CLI parser) -> user_rotation=1 * fix: preserve orientation rename compatibility * test: stabilize orientation compatibility formatting * style: format MCP compatibility test * revert: drop cross-surface rotate compatibility, keep the lean rename The rotate->orientation change is a bug fix (name collision with the `gesture rotate` two-finger gesture), not a compatibility feature. The cross-surface command-data compatibility added disproportionate weight (~480 B, dominated by the alias module inlined into the batch bundle) for a command that was only canonical for two minor versions, so shipped batch/ replay/MCP data carrying `rotate` is a rare, documentable break. Removed: - daemon request-boundary command normalization (`request-router.ts`) - batch step alias resolution (`batch-policy.ts`, `cli/batch-steps.ts`) - MCP tool-runner alias/legacy-result handling (`mcp/command-tools.ts`) - the `command-aliases.ts` module rename and cross-surface machinery (reverted to `cli-command-aliases.ts`) - the cross-surface tests Kept (cheap, high value — prevents build breaks for typed consumers): - CLI `rotate` alias (one line, same mechanism as `tap`/`launch`) - deprecated `RotateCommand*` / `SystemRotate*` type aliases and the `client.command.rotate` / `device.system.rotate` wrappers that delegate to `orientation` and restore the legacy response contract Net bundle vs main is now +473 B (was +952 B), almost all the kept SDK wrappers plus the unavoidable longer command name.
1 parent 62fd46a commit 392dc1c

50 files changed

Lines changed: 297 additions & 143 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/adr/0014-session-ref-frame-lifetime.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -226,15 +226,15 @@ classifying representative actions; it must not become a second prose registry:
226226

227227
| Effect | Commands/actions |
228228
| --- | --- |
229-
| `may-invalidate` | press, click, fill, longpress, type, focus, scroll, swipe, gesture, back, home, `tv-remote`, rotate, open/relaunch, trigger/push delivery, settings changes, install/reinstall, React Native overlay dismissal, and lifecycle operations that can replace the visible surface |
229+
| `may-invalidate` | press, click, fill, longpress, type, focus, scroll, swipe, gesture, back, home, `tv-remote`, orientation, open/relaunch, trigger/push delivery, settings changes, install/reinstall, React Native overlay dismissal, and lifecycle operations that can replace the visible surface |
230230
| Conditional resolver | keyboard status preserves while dismiss/return/input invalidate; alert get/wait preserve while accept/dismiss invalidate; find reads preserve while click/fill/focus/type delegate to their leaf mutation |
231231
| `delegated` | batch, replay, and test/suite orchestrators; each nested leaf owns its transition |
232232
| `preserve` | snapshots and other observation, assertion, screenshot, recording, trace, logs, events, network inspection, performance, inventory, capability, lease, and transport-management operations unless a selected subaction directly manipulates the visible surface |
233233

234234
Clipboard reads and writes preserve the frame because pasteboard state alone does not change element
235235
identity. A later paste/type action is independently invalidating.
236236

237-
Generic routing is not an exception to the policy. `back`, `home`, `rotate`, `scroll`, `tv-remote`,
237+
Generic routing is not an exception to the policy. `back`, `home`, `orientation`, `scroll`, `tv-remote`,
238238
and `app-switcher` all reach the generic daemon leaf and must cross the same transition there when
239239
they act. `app-switcher` is currently projected to the daemon by a direct writer but, unlike its
240240
generic-routed siblings, omits an explicit daemon descriptor facet and relies on the registry's

src/__tests__/cli-help.test.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,15 @@ test('launch dispatches as a plain open without forcing a relaunch', async () =>
254254
assert.notEqual(result.calls[0]?.flags?.relaunch, true);
255255
});
256256

257+
test('rotate dispatches as orientation (deprecated alias) with positionals preserved', async () => {
258+
const result = await runCliCapture(['rotate', 'landscape-left', '--json']);
259+
assert.doesNotMatch(result.stderr, /Unknown command/);
260+
// Canonicalization: the daemon call must record orientation, never rotate.
261+
assert.equal(result.calls.length, 1);
262+
assert.equal(result.calls[0]?.command, 'orientation');
263+
assert.deepEqual(result.calls[0]?.positionals, ['landscape-left']);
264+
});
265+
257266
// From #1052 (credit: @vku2018): the alias must compose with the bare-ref
258267
// hint — `tap e3` normalizes to press, then gets the @e3 suggestion.
259268
test('tap with a bare ref gets the @ref hint, not an unknown-command error', async () => {

src/__tests__/client.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,26 @@ test('client exposes narrowed result types for closed daemon projections', async
166166
assert.equal(triggerResult.transport, 'deep-link');
167167
});
168168

169+
test('deprecated client.command.rotate delegates to orientation and keeps the legacy action', async () => {
170+
const setup = createTransport(async () => ({
171+
ok: true,
172+
data: {
173+
action: 'orientation',
174+
orientation: 'landscape-left',
175+
message: 'Rotated to landscape-left',
176+
},
177+
}));
178+
const client = createAgentDeviceClient(setup.config, { transport: setup.transport });
179+
180+
const result = await client.command.rotate({ orientation: 'landscape-left' });
181+
182+
// The wrapper sends the canonical wire command...
183+
assert.equal(setup.calls.at(-1)?.command, 'orientation');
184+
// ...and restores the shipped v0.18/v0.19 response contract for old consumers.
185+
assert.equal(result.action, 'rotate');
186+
assert.equal(result.orientation, 'landscape-left');
187+
});
188+
169189
function closedProjectionResponse(command: string): DaemonResponse {
170190
const data = closedProjectionResponses[command];
171191
if (!data) throw new Error(`Unexpected command: ${command}`);

src/__tests__/contracts-schema-public.test.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import type {
1111
AppSwitcherCommandResult,
1212
BackCommandResult,
1313
HomeCommandResult,
14-
RotateCommandResult,
14+
OrientationCommandResult,
1515
TvRemoteCommandResult,
1616
} from '../contracts/navigation.ts';
1717
import type { ViewportCommandResult } from '../contracts/viewport.ts';
@@ -121,12 +121,12 @@ test('command result contracts are assignable to command result map', () => {
121121
} satisfies AppSwitcherCommandResult;
122122
const appSwitcherFromMap: CommandResult<'app-switcher'> = appSwitcher;
123123

124-
const rotate = {
125-
action: 'rotate',
124+
const orientation = {
125+
action: 'orientation',
126126
orientation: 'portrait',
127127
message: 'Rotated to portrait',
128-
} satisfies RotateCommandResult;
129-
const rotateFromMap: CommandResult<'rotate'> = rotate;
128+
} satisfies OrientationCommandResult;
129+
const orientationFromMap: CommandResult<'orientation'> = orientation;
130130

131131
const tvRemote = {
132132
action: 'tv-remote',
@@ -155,7 +155,7 @@ test('command result contracts are assignable to command result map', () => {
155155
assert.equal(backFromMap.mode, 'in-app');
156156
assert.equal(homeFromMap.action, 'home');
157157
assert.equal(appSwitcherFromMap.action, 'app-switcher');
158-
assert.equal(rotateFromMap.orientation, 'portrait');
158+
assert.equal(orientationFromMap.orientation, 'portrait');
159159
assert.equal(tvRemoteFromMap.button, 'select');
160160
assert.equal(clipboardFromMap.action === 'write' ? clipboardFromMap.textLength : -1, 11);
161161
assert.equal(

src/__tests__/runtime-public.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,7 @@ test('internal backend, commands, and io modules are usable', () => {
258258
assert.equal(typeof commands.interactions.gesture, 'function');
259259
assert.equal(typeof commands.system.back, 'function');
260260
assert.equal(typeof commands.system.home, 'function');
261+
assert.equal(typeof commands.system.orientation, 'function');
261262
assert.equal(typeof commands.system.rotate, 'function');
262263
assert.equal(typeof commands.system.keyboard, 'function');
263264
assert.equal(typeof commands.system.clipboard, 'function');

src/agent-device-client.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,10 @@ import type {
5353
MaterializationReleaseOptions,
5454
MetroPrepareOptions,
5555
MetroPrepareResult,
56+
OrientationCommandResult,
5657
PanOptions,
5758
FlingOptions,
59+
RotateCommandResult,
5860
SwipeGestureOptions,
5961
PinchOptions,
6062
RotateGestureOptions,
@@ -74,7 +76,7 @@ import type { ProjectedNavigationCommandClient } from './commands/system/navigat
7476
import { AppError } from './kernel/errors.ts';
7577

7678
type ProjectedSystemCommandClient = ProjectedNavigationCommandClient<InternalRequestOptions> &
77-
Pick<AgentDeviceCommandClient, 'appState' | 'keyboard' | 'clipboard'>;
79+
Pick<AgentDeviceCommandClient, 'appState' | 'keyboard' | 'clipboard' | 'rotate'>;
7880

7981
export function createAgentDeviceClient(
8082
config: AgentDeviceClientConfig = {},
@@ -520,6 +522,17 @@ function buildProjectedSystemCommandClient(
520522
methods[method] = async (options = {}) =>
521523
await executeCommand<CommandResult<typeof command>>(command as DaemonCommandName, options);
522524
}
525+
// Deprecated (v0.18/v0.19): `rotate` was renamed to `orientation`. Retain a
526+
// thin wrapper that delegates to `orientation` and restores the legacy
527+
// `action: 'rotate'` response contract for existing consumers.
528+
const orientation = methods.orientation;
529+
if (!orientation) {
530+
throw new Error('orientation client method missing from the system command family');
531+
}
532+
methods.rotate = async (options = {}) => {
533+
const result = (await orientation(options)) as OrientationCommandResult;
534+
return { ...result, action: 'rotate' } satisfies RotateCommandResult;
535+
};
523536
return methods as unknown as ProjectedSystemCommandClient;
524537
}
525538

src/backend.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -493,7 +493,7 @@ export type AgentDeviceBackend = {
493493
context: BackendCommandContext,
494494
options: BackendTvRemoteOptions,
495495
): Promise<BackendActionResult>;
496-
rotate?(
496+
setOrientation?(
497497
context: BackendCommandContext,
498498
orientation: BackendDeviceOrientation,
499499
): Promise<BackendActionResult>;

src/cli-command-aliases.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ const CLI_COMMAND_ALIASES: readonly CliCommandAlias[] = [
1616
{ alias: 'tap', command: 'press' },
1717
{ alias: 'launch', command: 'open' },
1818
{ alias: 'relaunch', command: 'open', impliedFlags: ['relaunch'] },
19+
// Deprecated: `rotate` was renamed to `orientation` (it collided with the
20+
// `gesture rotate` two-finger gesture). Kept working at the CLI for a few versions.
21+
{ alias: 'rotate', command: 'orientation' },
1922
];
2023

2124
const aliasByToken: ReadonlyMap<string, CliCommandAlias> = new Map(

src/cli/parser/__tests__/args-parse-interaction.test.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -305,9 +305,15 @@ test('parseArgs supports trigger-app-event payload argument', () => {
305305
assert.deepEqual(parsed.positionals, ['screenshot_taken', '{"source":"qa"}']);
306306
});
307307

308-
test('parseArgs accepts rotate orientation aliases', () => {
308+
test('parseArgs accepts the orientation command', () => {
309+
const parsed = parseArgs(['orientation', 'left'], { strictFlags: true });
310+
assert.equal(parsed.command, 'orientation');
311+
assert.deepEqual(parsed.positionals, ['left']);
312+
});
313+
314+
test('parseArgs normalizes the deprecated rotate alias to orientation', () => {
309315
const parsed = parseArgs(['rotate', 'left'], { strictFlags: true });
310-
assert.equal(parsed.command, 'rotate');
316+
assert.equal(parsed.command, 'orientation');
311317
assert.deepEqual(parsed.positionals, ['left']);
312318
});
313319

src/cli/parser/__tests__/cli-help-command-usage.test.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -372,11 +372,23 @@ test('keyboard command usage is documented', async () => {
372372
);
373373
});
374374

375-
test('rotate command usage is documented', async () => {
375+
test('orientation command usage is documented', async () => {
376+
const help = await usageForCommand('orientation');
377+
if (help === null) throw new Error('Expected command help text');
378+
assert.match(
379+
help,
380+
/orientation <portrait\|portrait-upside-down\|landscape-left\|landscape-right>/,
381+
);
382+
assert.match(help, /Set device orientation on iOS and Android/);
383+
});
384+
385+
test('deprecated rotate alias resolves to orientation usage', async () => {
376386
const help = await usageForCommand('rotate');
377387
if (help === null) throw new Error('Expected command help text');
378-
assert.match(help, /rotate <portrait\|portrait-upside-down\|landscape-left\|landscape-right>/);
379-
assert.match(help, /Rotate device orientation on iOS and Android/);
388+
assert.match(
389+
help,
390+
/orientation <portrait\|portrait-upside-down\|landscape-left\|landscape-right>/,
391+
);
380392
});
381393

382394
test('settings usage documents canonical faceid states', async () => {

0 commit comments

Comments
 (0)