Skip to content
Closed
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
29 changes: 18 additions & 11 deletions src/__tests__/cli-help.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,18 +150,25 @@ test('help rejects multiple positional commands and skips daemon dispatch', asyn
assert.match(result.stderr, /Error \(INVALID_ARGS\): help accepts at most one command/);
});

test('unknown command with flags reports unknown command before flag validation', async () => {
test('tap aliases to press before daemon dispatch and JSON output', async () => {
const result = await runCliCapture(['tap', '@e3', '--json'], async (request) => ({
ok: true,
data: { command: request.command },
}));

assert.equal(result.code, null);
assert.equal(result.calls.length, 1);
assert.equal(result.calls[0]?.command, 'press');
assert.deepEqual(result.calls[0]?.positionals, ['@e3']);
const payload = JSON.parse(result.stdout);
assert.equal(payload.success, true);
assert.equal(payload.data.command, 'press');
});

test('tap alias keeps press target validation', async () => {
const result = await runCliCapture(['tap', 'e3', '--session', 'foo']);
assert.equal(result.code, 1);
assert.equal(result.calls.length, 0);
assert.match(result.stderr, /Error \(INVALID_ARGS\): Unknown command: tap/);
assert.match(result.stderr, /Did you mean press or click/);
});

test('unknown command without flags reports unknown command with alias suggestion', async () => {
const result = await runCliCapture(['tap', 'e3']);
assert.equal(result.code, 1);
assert.equal(result.calls.length, 0);
assert.match(result.stderr, /Error \(INVALID_ARGS\): Unknown command: tap/);
assert.match(result.stderr, /Did you mean press or click/);
assert.match(result.stderr, /Did you mean "@e3"\?/);
assert.doesNotMatch(result.stderr, /Unknown command: tap/);
});
15 changes: 2 additions & 13 deletions src/cli/parser/args.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,11 +154,7 @@ export function finalizeParsedArgs(
// This ensures "Unknown command" errors take precedence over flag validation errors
// However, skip this check if --help is provided, since cli.ts will handle it gracefully
if (parsed.command && !isCommandKnown(parsed.command) && !flags.help) {
const hint = getCommandAliasSuggestion(parsed.command);
const message = hint
? `Unknown command: ${parsed.command}. Did you mean ${hint}?`
: `Unknown command: ${parsed.command}`;
throw new AppError('INVALID_ARGS', message);
throw new AppError('INVALID_ARGS', `Unknown command: ${parsed.command}`);
}

const disallowed = parsed.providedFlags.filter(
Expand Down Expand Up @@ -357,14 +353,6 @@ function isCommandKnown(command: string): boolean {
return (listCliCommandNames() as readonly string[]).includes(command);
}

const COMMAND_ALIAS_SUGGESTIONS: Record<string, string> = {
tap: 'press or click',
};

function getCommandAliasSuggestion(command: string): string | undefined {
return COMMAND_ALIAS_SUGGESTIONS[command];
}

function formatUnsupportedFlagMessage(command: string | null, unsupported: string[]): string {
if (!command) {
return unsupported.length === 1
Expand All @@ -387,5 +375,6 @@ export function usageForCommand(command: string): string | null {
function normalizeCommandAlias(command: string): string {
if (command === 'long-press') return 'longpress';
if (command === 'metrics') return 'perf';
if (command === 'tap') return 'press';
return command;
}
4 changes: 2 additions & 2 deletions src/cli/parser/cli-help.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ const AGENT_QUICKSTART_LINES = [
'Implicit default sessions are scoped to the current worktree; if a prompt names a Session, include --session <name> on every command in that flow.',
'Run mutating commands serially within one session; parallelize only read-only commands or separate sessions/devices.',
'Clipboard limits: iOS Allow Paste cannot be automated through XCUITest; prefill with clipboard write. Android non-ASCII should use fill/type, not raw adb input.',
'After mutation: refs are stale. If the next target is known, use its selector directly; otherwise refresh with snapshot -i, scoped with -s when a stable container is known. Do not use tap; use press or click.',
'After mutation: refs are stale. If the next target is known, use its selector directly; otherwise refresh with snapshot -i, scoped with -s when a stable container is known. Prefer press or click for touch interactions.',
'Raw coordinates are fallback-only: use snapshot -i --json rects when iOS refs no-op or child refs are missing, then verify the action with diff snapshot -i or snapshot --diff.',
'Sparse or AX-unavailable snapshot: use screenshot for visual truth, press the visible coordinate to leave the bad screen, then retry AX with snapshot -i.',
'macOS context menus use click <ref> --button secondary, then snapshot -i. Longpress is for mobile hold gestures, not macOS secondary-click menus.',
Expand Down Expand Up @@ -145,7 +145,7 @@ Command shape:
Snapshot refs look like @e12. After snapshot -i, use the exact @eN ref from that output.
If the exact ref is not known yet, first output snapshot -i, then use a concrete example shape like press @e12 in the next command; do not write @<ref>, @ref, @Label_Name, or @eN placeholders.
Close means agent-device close. App-owned back means back; system back means back --system.
Taps are press or click; tap is not a command. Gestures use swipe, longpress, or gesture <pan|fling|swipe|pinch|rotate|transform>. Use gesture swipe left|right for reliable in-page horizontal swipes, and gesture swipe right-edge for left-edge navigation/back gestures. Android swipe, pinch, rotate, and transform use provider-native touch injection when available, then the bundled touch helper. iOS simulator transform uses private XCTest synthesis for a continuous two-finger pan/scale/rotation path; otherwise it reports UNSUPPORTED_OPERATION.
Taps are press or click; tap is accepted as a CLI alias for press and normalizes back to press in output. Gestures use swipe, longpress, or gesture <pan|fling|swipe|pinch|rotate|transform>. Use gesture swipe left|right for reliable in-page horizontal swipes, and gesture swipe right-edge for left-edge navigation/back gestures. Android swipe, pinch, rotate, and transform use provider-native touch injection when available, then the bundled touch helper. iOS simulator transform uses private XCTest synthesis for a continuous two-finger pan/scale/rotation path; otherwise it reports UNSUPPORTED_OPERATION.

Bootstrap:
agent-device devices --platform ios
Expand Down
18 changes: 18 additions & 0 deletions src/daemon/__tests__/session-store.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import os from 'node:os';
import path from 'node:path';
import { SessionStore } from '../session-store.ts';
import type { SessionState } from '../types.ts';
import { parseArgs } from '../../cli/parser/args.ts';

type RecordActionEntry = Parameters<SessionStore['recordAction']>[1];

Expand Down Expand Up @@ -154,6 +155,23 @@ test('saveScript path writes session log to custom location', () => {
assert.equal(fs.existsSync(path.join(root, 'sessions')), false);
});

test('writeSessionLog records tap alias as canonical press', () => {
const fixture = makeFixture('agent-device-session-log-tap-alias-');
const parsed = parseArgs(['tap', '@e3', '--json'], { strictFlags: true });
assert.equal(parsed.command, 'press');

fixture.store.recordAction(fixture.session, {
command: parsed.command ?? '',
positionals: parsed.positionals,
flags: { ...parsed.flags, platform: 'ios', saveScript: true },
result: {},
});

const script = writeScript(fixture);
assert.match(script, /\npress @e3\n/);
assert.doesNotMatch(script, /\ntap @e3\n/);
});

test('writeSessionLog persists open --relaunch in script output', () => {
const fixture = makeFixture('agent-device-session-log-relaunch-');
recordOpen(fixture.store, fixture.session, { platform: 'ios', saveScript: true, relaunch: true });
Expand Down
8 changes: 8 additions & 0 deletions src/utils/__tests__/args.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1161,6 +1161,13 @@ test('parseArgs supports legacy long-press alias', () => {
assert.deepEqual(parsed.positionals, ['300', '500', '800']);
});

test('parseArgs supports tap alias for 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 metrics alias for perf', () => {
const parsed = parseArgs(['metrics'], { strictFlags: true });
assert.equal(parsed.command, 'perf');
Expand Down Expand Up @@ -1492,6 +1499,7 @@ test('usageForCommand resolves workflow help topic', () => {
const help = usageForCommand('workflow');
if (help === null) throw new Error('Expected workflow help text');
assert.match(help, /agent-device help workflow/);
assert.match(help, /tap is accepted as a CLI alias for press/);
assert.match(help, /Use selectors as positional targets/);
assert.match(help, /Do not use CSS selectors/);
assert.match(help, /Snapshot legend:/);
Expand Down