Files
callstack__agent-device/src/cli/parser/__tests__/args-parse-interaction.test.ts
T
devin-ai-integration[bot] 668b64f3da chore: remove deprecated rotate CLI command alias (#1277) (#1283)
* chore: remove deprecated rotate CLI command alias (#1277)

The rotate CLI command alias was renamed to orientation a few versions
ago and is now removed at the next minor, aligned with the gesture-shim
deprecation window.

- Remove the rotate -> orientation entry from src/cli-command-aliases.ts.
- Add an actionable parser error: invoking rotate now fails with
  "rotate was renamed to orientation".
- Update the command-suggestion guard comments and the true-alias list
  in the curated suggestion map test.
- Update CLI parser/help usage tests and src/__tests__/cli-help.test.ts
  to assert the migration error.
- Remove the rotate deprecation note from the commands doc and add a
  breaking migration note to the changelog.

No other command-name aliases are marked deprecated; long-press,
metrics, tap, launch, and relaunch remain supported true aliases.

Fixes #1277

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* chore: disambiguate removed rotate alias error message (#1277)

Update the migration error so users who meant the two-finger gesture are
pointed to `gesture rotate` instead of `orientation`.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* chore: align CHANGELOG with runtime rotate migration message (#1277)

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

---------

Co-authored-by: Michał Pierzchała <thymikee@gmail.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-07-16 11:44:20 +02:00

377 lines
13 KiB
TypeScript

import { test } from 'vitest';
import assert from 'node:assert/strict';
import { AppError } from '../../../kernel/errors.ts';
import { parseArgs } from '../args.ts';
test('parseArgs accepts clipboard subcommands', () => {
const read = parseArgs(['clipboard', 'read'], { strictFlags: true });
assert.equal(read.command, 'clipboard');
assert.deepEqual(read.positionals, ['read']);
const write = parseArgs(['clipboard', 'write', 'otp', '123456'], { strictFlags: true });
assert.equal(write.command, 'clipboard');
assert.deepEqual(write.positionals, ['write', 'otp', '123456']);
});
test('parseArgs accepts keyboard subcommands', () => {
const status = parseArgs(['keyboard', 'status'], { strictFlags: true });
assert.equal(status.command, 'keyboard');
assert.deepEqual(status.positionals, ['status']);
const dismiss = parseArgs(['keyboard', 'dismiss'], { strictFlags: true });
assert.equal(dismiss.command, 'keyboard');
assert.deepEqual(dismiss.positionals, ['dismiss']);
const enter = parseArgs(['keyboard', 'enter'], { strictFlags: true });
assert.equal(enter.command, 'keyboard');
assert.deepEqual(enter.positionals, ['enter']);
});
test('parseArgs accepts tv-remote', () => {
const remote = parseArgs(['tv-remote', 'press', 'select', '--duration-ms', '250'], {
strictFlags: true,
});
assert.equal(remote.command, 'tv-remote');
assert.deepEqual(remote.positionals, ['press', 'select']);
assert.equal(remote.flags.durationMs, 250);
const longpress = parseArgs(['tv-remote', 'longpress', 'select'], {
strictFlags: true,
});
assert.equal(longpress.command, 'tv-remote');
assert.deepEqual(longpress.positionals, ['longpress', 'select']);
});
test('parseArgs accepts scroll pixel distance and duration flags', () => {
const parsed = parseArgs(['scroll', 'down', '--pixels', '240', '--duration-ms', '50'], {
strictFlags: true,
});
assert.equal(parsed.command, 'scroll');
assert.deepEqual(parsed.positionals, ['down']);
assert.equal(parsed.flags.pixels, 240);
assert.equal(parsed.flags.durationMs, 50);
});
test('parseArgs keeps no-record accepted on recordable commands', () => {
const press = parseArgs(['press', '10', '10', '--no-record'], { strictFlags: true });
assert.equal(press.flags.noRecord, true);
const swipe = parseArgs(['swipe', '0', '0', '10', '10', '--no-record'], {
strictFlags: true,
});
assert.equal(swipe.flags.noRecord, true);
});
test('parseArgs recognizes press series flags', () => {
const parsed = parseArgs([
'press',
'300',
'500',
'--count',
'12',
'--interval-ms=45',
'--hold-ms',
'120',
'--jitter-px',
'3',
]);
assert.equal(parsed.command, 'press');
assert.deepEqual(parsed.positionals, ['300', '500']);
assert.equal(parsed.flags.count, 12);
assert.equal(parsed.flags.intervalMs, 45);
assert.equal(parsed.flags.holdMs, 120);
assert.equal(parsed.flags.jitterPx, 3);
});
test('parseArgs recognizes press selector + snapshot flags', () => {
const parsed = parseArgs(['press', '@e2', '--depth', '3', '--scope', 'Sign In', '--raw'], {
strictFlags: true,
});
assert.equal(parsed.command, 'press');
assert.deepEqual(parsed.positionals, ['@e2']);
assert.equal(parsed.flags.snapshotDepth, 3);
assert.equal(parsed.flags.snapshotScope, 'Sign In');
assert.equal(parsed.flags.snapshotRaw, true);
});
test('parseArgs recognizes click series flags', () => {
const parsed = parseArgs(['click', '@e5', '--count', '4', '--interval-ms', '10'], {
strictFlags: true,
});
assert.equal(parsed.command, 'click');
assert.deepEqual(parsed.positionals, ['@e5']);
assert.equal(parsed.flags.count, 4);
assert.equal(parsed.flags.intervalMs, 10);
});
test('parseArgs recognizes click button flag', () => {
const parsed = parseArgs(['click', '@e5', '--button', 'secondary'], {
strictFlags: true,
});
assert.equal(parsed.command, 'click');
assert.deepEqual(parsed.positionals, ['@e5']);
assert.equal(parsed.flags.clickButton, 'secondary');
});
test('parseArgs recognizes double-tap flag for repeated press', () => {
const parsed = parseArgs(['press', '201', '545', '--count', '5', '--double-tap'], {
strictFlags: true,
});
assert.equal(parsed.command, 'press');
assert.deepEqual(parsed.positionals, ['201', '545']);
assert.equal(parsed.flags.count, 5);
assert.equal(parsed.flags.doubleTap, true);
});
test('parseArgs recognizes swipe positional + pattern flags', () => {
const parsed = parseArgs([
'swipe',
'540',
'1500',
'540',
'500',
'120',
'--count',
'8',
'--pause-ms',
'30',
'--pattern',
'ping-pong',
]);
assert.equal(parsed.command, 'swipe');
assert.deepEqual(parsed.positionals, ['540', '1500', '540', '500', '120']);
assert.equal(parsed.flags.count, 8);
assert.equal(parsed.flags.pauseMs, 30);
assert.equal(parsed.flags.pattern, 'ping-pong');
});
test('parseArgs recognizes gesture subcommand positionals', () => {
const pan = parseArgs(
['gesture', 'pan', '200', '420', '0', '-80', '500', '--pointer-count', '2'],
{
strictFlags: true,
},
);
assert.equal(pan.command, 'gesture');
assert.deepEqual(pan.positionals, ['pan', '200', '420', '0', '-80', '500']);
assert.equal(pan.flags.pointerCount, 2);
const fling = parseArgs(['gesture', 'fling', 'right', '200', '420', '180'], {
strictFlags: true,
});
assert.equal(fling.command, 'gesture');
assert.deepEqual(fling.positionals, ['fling', 'right', '200', '420', '180']);
const rotate = parseArgs(['gesture', 'rotate', '35', '200', '420'], {
strictFlags: true,
});
assert.equal(rotate.command, 'gesture');
assert.deepEqual(rotate.positionals, ['rotate', '35', '200', '420']);
const transform = parseArgs(['gesture', 'transform', '200', '420', '80', '-40', '2', '35'], {
strictFlags: true,
});
assert.equal(transform.command, 'gesture');
assert.deepEqual(transform.positionals, ['transform', '200', '420', '80', '-40', '2', '35']);
});
test('parseArgs recognizes type and fill delay flags', () => {
const typeParsed = parseArgs(['type', 'hello', '--delay-ms', '75'], {
strictFlags: true,
});
assert.equal(typeParsed.command, 'type');
assert.deepEqual(typeParsed.positionals, ['hello']);
assert.equal(typeParsed.flags.delayMs, 75);
const fillParsed = parseArgs(['fill', '@e5', 'search', '--delay-ms', '40'], {
strictFlags: true,
});
assert.equal(fillParsed.command, 'fill');
assert.deepEqual(fillParsed.positionals, ['@e5', 'search']);
assert.equal(fillParsed.flags.delayMs, 40);
});
test('parseArgs recognizes record --fps flag', () => {
const parsed = parseArgs(['record', 'start', './capture.mp4', '--fps', '30'], {
strictFlags: true,
});
assert.equal(parsed.command, 'record');
assert.deepEqual(parsed.positionals, ['start', './capture.mp4']);
assert.equal(parsed.flags.fps, 30);
});
test('parseArgs recognizes record --quality flag', () => {
const parsed = parseArgs(['record', 'start', './capture.mp4', '--quality', 'high'], {
strictFlags: true,
});
assert.equal(parsed.command, 'record');
assert.deepEqual(parsed.positionals, ['start', './capture.mp4']);
assert.equal(parsed.flags.quality, 'high');
});
test('parseArgs recognizes record --max-size flag', () => {
const parsed = parseArgs(['record', 'start', './capture.mp4', '--max-size', '1024'], {
strictFlags: true,
});
assert.equal(parsed.command, 'record');
assert.deepEqual(parsed.positionals, ['start', './capture.mp4']);
assert.equal(parsed.flags.screenshotMaxSize, 1024);
});
test('parseArgs recognizes record --hide-touches flag', () => {
const parsed = parseArgs(['record', 'start', './capture.mp4', '--hide-touches'], {
strictFlags: true,
});
assert.equal(parsed.command, 'record');
assert.deepEqual(parsed.positionals, ['start', './capture.mp4']);
assert.equal(parsed.flags.hideTouches, true);
});
test('parseArgs recognizes record --scope flag', () => {
const parsed = parseArgs(['record', 'start', './capture.mp4', '--scope', 'device'], {
strictFlags: true,
});
assert.equal(parsed.command, 'record');
assert.deepEqual(parsed.positionals, ['start', './capture.mp4']);
assert.equal(parsed.flags.recordingScope, 'device');
});
test('parseArgs recognizes screenshot flags', () => {
const parsed = parseArgs(
[
'screenshot',
'page.png',
'--pixel-density',
'2',
'--full',
'-f',
'--fullscreen',
'--max-size',
'1024',
'--no-stabilize',
'--normalize-status-bar',
],
{
strictFlags: true,
},
);
assert.equal(parsed.command, 'screenshot');
assert.deepEqual(parsed.positionals, ['page.png']);
assert.equal(parsed.flags.screenshotPixelDensity, 2);
assert.equal(parsed.flags.screenshotFullscreen, true);
assert.equal(parsed.flags.screenshotMaxSize, 1024);
assert.equal(parsed.flags.screenshotNoStabilize, true);
assert.equal(parsed.flags.screenshotNormalizeStatusBar, true);
});
test('parseArgs recognizes viewport command', () => {
const parsed = parseArgs(['viewport', '1280', '900', '--platform', 'web'], {
strictFlags: true,
});
assert.equal(parsed.command, 'viewport');
assert.deepEqual(parsed.positionals, ['1280', '900']);
assert.equal(parsed.flags.platform, 'web');
});
test('parseArgs recognizes longpress command', () => {
const parsed = parseArgs(['longpress', '300', '500', '800'], { strictFlags: true });
assert.equal(parsed.command, 'longpress');
assert.deepEqual(parsed.positionals, ['300', '500', '800']);
});
test('parseArgs supports legacy long-press alias', () => {
const parsed = parseArgs(['long-press', '300', '500', '800'], { strictFlags: true });
assert.equal(parsed.command, 'longpress');
assert.deepEqual(parsed.positionals, ['300', '500', '800']);
});
test('parseArgs supports tap alias for press', () => {
const parsed = parseArgs(['tap', '@e3'], { strictFlags: true });
assert.equal(parsed.command, 'press');
assert.deepEqual(parsed.positionals, ['@e3']);
});
test('parseArgs preserves flags when tap is aliased to press', () => {
const parsed = parseArgs(['tap', '@e3', '--json'], { strictFlags: true });
assert.equal(parsed.command, 'press');
assert.deepEqual(parsed.positionals, ['@e3']);
assert.equal(parsed.flags.json, true);
});
test('parseArgs supports trigger-app-event payload argument', () => {
const parsed = parseArgs(['trigger-app-event', 'screenshot_taken', '{"source":"qa"}'], {
strictFlags: true,
});
assert.equal(parsed.command, 'trigger-app-event');
assert.deepEqual(parsed.positionals, ['screenshot_taken', '{"source":"qa"}']);
});
test('parseArgs accepts the orientation command', () => {
const parsed = parseArgs(['orientation', 'left'], { strictFlags: true });
assert.equal(parsed.command, 'orientation');
assert.deepEqual(parsed.positionals, ['left']);
});
test('parseArgs rejects the removed rotate alias with a migration error', () => {
assert.throws(
() => parseArgs(['rotate', 'left'], { strictFlags: true }),
(error: unknown) =>
error instanceof AppError &&
error.code === 'INVALID_ARGS' &&
/rotate was renamed to orientation/.test(String(error.message)),
);
});
test('parseArgs recognizes test --record-video flag', () => {
const parsed = parseArgs(['test', './suite', '--record-video'], { strictFlags: true });
assert.equal(parsed.command, 'test');
assert.equal(parsed.flags.recordVideo, true);
});
test('snapshot command accepts command-specific flags', () => {
const ignoredLegacyFlag = '-' + 'c';
const parsed = parseArgs(
['snapshot', '-i', ignoredLegacyFlag, '--depth', '3', '-s', 'Login', '--timeout', '120000'],
{
strictFlags: true,
},
);
assert.equal(parsed.command, 'snapshot');
assert.equal(parsed.flags.snapshotInteractiveOnly, true);
assert.equal(parsed.flags.snapshotDepth, 3);
assert.equal(parsed.flags.snapshotScope, 'Login');
assert.equal(parsed.flags.timeoutMs, 120000);
});
test('snapshot command accepts diff alias flag', () => {
const parsed = parseArgs(['snapshot', '--diff', '-i', '--depth', '4', '--scope', 'Counter'], {
strictFlags: true,
});
assert.equal(parsed.command, 'diff');
assert.deepEqual(parsed.positionals, ['snapshot']);
assert.equal(parsed.flags.snapshotDiff, undefined);
assert.equal(parsed.flags.snapshotInteractiveOnly, true);
assert.equal(parsed.flags.snapshotDepth, 4);
assert.equal(parsed.flags.snapshotScope, 'Counter');
});
test('snapshot --diff --help stays on snapshot command help', () => {
const parsed = parseArgs(['snapshot', '--diff', '--help'], { strictFlags: true });
assert.equal(parsed.command, 'snapshot');
assert.equal(parsed.flags.snapshotDiff, true);
assert.equal(parsed.flags.help, true);
});
test('diff snapshot command accepts snapshot flags', () => {
const parsed = parseArgs(
['diff', 'snapshot', '-i', '--depth', '4', '--scope', 'Counter', '--raw'],
{ strictFlags: true },
);
assert.equal(parsed.command, 'diff');
assert.deepEqual(parsed.positionals, ['snapshot']);
assert.equal(parsed.flags.snapshotInteractiveOnly, true);
assert.equal(parsed.flags.snapshotDepth, 4);
assert.equal(parsed.flags.snapshotScope, 'Counter');
assert.equal(parsed.flags.snapshotRaw, true);
});