diff --git a/src/__tests__/cli-help.test.ts b/src/__tests__/cli-help.test.ts index a49723fe64..3d47049630 100644 --- a/src/__tests__/cli-help.test.ts +++ b/src/__tests__/cli-help.test.ts @@ -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/); }); diff --git a/src/cli/parser/args.ts b/src/cli/parser/args.ts index 161677e3c9..5989fc8dea 100644 --- a/src/cli/parser/args.ts +++ b/src/cli/parser/args.ts @@ -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( @@ -357,14 +353,6 @@ function isCommandKnown(command: string): boolean { return (listCliCommandNames() as readonly string[]).includes(command); } -const COMMAND_ALIAS_SUGGESTIONS: Record = { - 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 @@ -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; } diff --git a/src/cli/parser/cli-help.ts b/src/cli/parser/cli-help.ts index aa2ee64abd..2ac77b01df 100644 --- a/src/cli/parser/cli-help.ts +++ b/src/cli/parser/cli-help.ts @@ -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 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 --button secondary, then snapshot -i. Longpress is for mobile hold gestures, not macOS secondary-click menus.', @@ -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, @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 . 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 . 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 diff --git a/src/daemon/__tests__/session-store.test.ts b/src/daemon/__tests__/session-store.test.ts index 17ece0a893..6228bcb12e 100644 --- a/src/daemon/__tests__/session-store.test.ts +++ b/src/daemon/__tests__/session-store.test.ts @@ -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[1]; @@ -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 }); diff --git a/src/utils/__tests__/args.test.ts b/src/utils/__tests__/args.test.ts index 11ac47fe20..ea2d080c17 100644 --- a/src/utils/__tests__/args.test.ts +++ b/src/utils/__tests__/args.test.ts @@ -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'); @@ -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:/);