diff --git a/src/__tests__/cli-help.test.ts b/src/__tests__/cli-help.test.ts index a49723fe64..d3ca82e153 100644 --- a/src/__tests__/cli-help.test.ts +++ b/src/__tests__/cli-help.test.ts @@ -150,18 +150,38 @@ 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 dispatches as press with positionals and flags preserved', async () => { + const result = await runCliCapture(['tap', '@e3', '--json']); + 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']); +}); + +// 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, /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/); +}); + +// 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 with alias suggestion', async () => { - const result = await runCliCapture(['tap', 'e3']); +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: tap/); - assert.match(result.stderr, /Did you mean press or click/); + assert.match(result.stderr, /Error \(INVALID_ARGS\): Unknown command: bogus-cmd/); }); 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);