From 58dbcbba46105fe3533456573a4e452d72564ded Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Fri, 3 Jul 2026 11:14:27 +0200 Subject: [PATCH 1/3] feat: accept tap as an alias of press --- src/__tests__/cli-help.test.ts | 26 ++++++++++++-------------- src/cli/parser/args.ts | 5 ++--- src/cli/parser/cli-help.ts | 4 ++-- src/utils/__tests__/args.test.ts | 20 ++++++++++++++++++++ 4 files changed, 36 insertions(+), 19 deletions(-) diff --git a/src/__tests__/cli-help.test.ts b/src/__tests__/cli-help.test.ts index a49723fe64..461d5ebb82 100644 --- a/src/__tests__/cli-help.test.ts +++ b/src/__tests__/cli-help.test.ts @@ -150,18 +150,16 @@ 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 () => { - 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/); +test('tap is an alias for press', async () => { + const result = await runCliCapture(['tap', '@e3', '--json']); + // The command should normalize to press and execute accordingly + // We check that it doesn't report an unknown command error + assert.doesNotMatch(result.stderr, /Error \(INVALID_ARGS\): Unknown command: tap/); +}); + +test('tap with selector resolves to press command', async () => { + const result = await runCliCapture(['tap', 'label="Submit"', '--json']); + // The command should normalize to press and execute accordingly + // We check that it doesn't report an unknown command error + assert.doesNotMatch(result.stderr, /Error \(INVALID_ARGS\): Unknown command: tap/); }); diff --git a/src/cli/parser/args.ts b/src/cli/parser/args.ts index 161677e3c9..023091beb3 100644 --- a/src/cli/parser/args.ts +++ b/src/cli/parser/args.ts @@ -357,9 +357,7 @@ function isCommandKnown(command: string): boolean { return (listCliCommandNames() as readonly string[]).includes(command); } -const COMMAND_ALIAS_SUGGESTIONS: Record = { - tap: 'press or click', -}; +const COMMAND_ALIAS_SUGGESTIONS: Record = {}; function getCommandAliasSuggestion(command: string): string | undefined { return COMMAND_ALIAS_SUGGESTIONS[command]; @@ -387,5 +385,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..74d7e6521e 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. Use press/click for taps.', '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 an alias for press. 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/utils/__tests__/args.test.ts b/src/utils/__tests__/args.test.ts index 11ac47fe20..c87de985cf 100644 --- a/src/utils/__tests__/args.test.ts +++ b/src/utils/__tests__/args.test.ts @@ -1167,6 +1167,19 @@ test('parseArgs supports metrics alias for perf', () => { assert.deepEqual(parsed.positionals, []); }); +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, @@ -1194,6 +1207,13 @@ test('usageForCommand supports legacy long-press alias', () => { assert.doesNotMatch(help ?? '', /agent-device long-press/); }); +test('usageForCommand supports tap alias for press', () => { + const help = usageForCommand('tap'); + assert.equal(help === null, false); + assert.match(help ?? '', /agent-device press/); + assert.doesNotMatch(help ?? '', /agent-device tap/); +}); + test('usageForCommand documents keyboard dismissal flow', () => { const help = usageForCommand('keyboard'); assert.equal(help === null, false); From 4e88ce43de1eae661fe03b788f2d60b437fee32b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Fri, 3 Jul 2026 11:19:21 +0200 Subject: [PATCH 2/3] test: restore #1036 ordering regression coverage, assert tap dispatches press Making tap a real alias deleted the only tests covering unknown-command-before-flag-validation ordering (they used tap as the unknown command). Restore them with a genuinely unknown command, and strengthen the alias test to assert the dispatched call records press with positionals preserved instead of merely not-erroring. --- src/__tests__/cli-help.test.ts | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/src/__tests__/cli-help.test.ts b/src/__tests__/cli-help.test.ts index 461d5ebb82..b9230899cc 100644 --- a/src/__tests__/cli-help.test.ts +++ b/src/__tests__/cli-help.test.ts @@ -150,16 +150,28 @@ 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('tap is an alias for press', async () => { +test('tap dispatches as press with positionals and flags preserved', async () => { const result = await runCliCapture(['tap', '@e3', '--json']); - // The command should normalize to press and execute accordingly - // We check that it doesn't report an unknown command error - assert.doesNotMatch(result.stderr, /Error \(INVALID_ARGS\): Unknown command: tap/); + assert.doesNotMatch(result.stderr, /Unknown command/); + // Canonicalization: the daemon call must record press, never tap. + assert.equal(result.calls.length, 1); + assert.equal(result.calls[0]?.command, 'press'); + assert.deepEqual(result.calls[0]?.positionals, ['@e3']); }); -test('tap with selector resolves to press command', async () => { - const result = await runCliCapture(['tap', 'label="Submit"', '--json']); - // The command should normalize to press and execute accordingly - // We check that it doesn't report an unknown command error - assert.doesNotMatch(result.stderr, /Error \(INVALID_ARGS\): Unknown command: tap/); +// Regression coverage for #1036 (moved off `tap` when it became an alias): +// unknown commands must be reported before per-command flag validation. +test('unknown command with flags reports unknown command before flag validation', async () => { + const result = await runCliCapture(['bogus-cmd', 'e3', '--session', 'foo']); + assert.equal(result.code, 1); + assert.equal(result.calls.length, 0); + assert.match(result.stderr, /Error \(INVALID_ARGS\): Unknown command: bogus-cmd/); + assert.doesNotMatch(result.stderr, /not supported for command/); +}); + +test('unknown command without flags reports unknown command', async () => { + const result = await runCliCapture(['bogus-cmd', 'e3']); + assert.equal(result.code, 1); + assert.equal(result.calls.length, 0); + assert.match(result.stderr, /Error \(INVALID_ARGS\): Unknown command: bogus-cmd/); }); From 8187e9b3c60530d82ab3571a0de50848a55577ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Fri, 3 Jul 2026 11:50:32 +0200 Subject: [PATCH 3/3] test: cover tap alias composing with the bare-ref hint Adopted from #1052 (credit: @vku2018): tap e3 must normalize to press and then surface the @e3 suggestion, proving the alias and the bare-ref hint compose instead of masking each other. --- src/__tests__/cli-help.test.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/__tests__/cli-help.test.ts b/src/__tests__/cli-help.test.ts index b9230899cc..d3ca82e153 100644 --- a/src/__tests__/cli-help.test.ts +++ b/src/__tests__/cli-help.test.ts @@ -159,6 +159,16 @@ test('tap dispatches as press with positionals and flags preserved', async () => assert.deepEqual(result.calls[0]?.positionals, ['@e3']); }); +// From #1052 (credit: @vku2018): the alias must compose with the bare-ref +// hint — `tap e3` normalizes to press, then gets the @e3 suggestion. +test('tap with a bare ref gets the @ref hint, not an unknown-command error', async () => { + const result = await runCliCapture(['tap', 'e3', '--session', 'foo']); + assert.equal(result.code, 1); + assert.equal(result.calls.length, 0); + assert.match(result.stderr, /Did you mean "@e3"\?/); + assert.doesNotMatch(result.stderr, /Unknown command: tap/); +}); + // Regression coverage for #1036 (moved off `tap` when it became an alias): // unknown commands must be reported before per-command flag validation. test('unknown command with flags reports unknown command before flag validation', async () => {