Skip to content

Commit ef20069

Browse files
committed
fix: project structured batch targets
1 parent 118999c commit ef20069

3 files changed

Lines changed: 61 additions & 10 deletions

File tree

src/__tests__/cli-batch.test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,22 @@ test('batch --steps-file parses file payload', async () => {
6262
assert.equal((req.flags?.batchSteps ?? [])[0]?.command, 'wait');
6363
});
6464

65+
test('batch structured interaction target is projected to positionals, not device flags', async () => {
66+
const result = await runCliCapture([
67+
'batch',
68+
'--steps',
69+
'[{"command":"press","input":{"target":{"kind":"point","x":10,"y":20},"count":2}}]',
70+
'--json',
71+
]);
72+
73+
assert.equal(result.code, null);
74+
assert.equal(result.calls.length, 1);
75+
const step = (result.calls[0]?.flags?.batchSteps ?? [])[0];
76+
assert.deepEqual(step?.positionals, ['10', '20']);
77+
assert.equal(step?.flags?.target, undefined);
78+
assert.equal(step?.flags?.count, 2);
79+
});
80+
6581
test('batch accepts legacy positionals/flags steps with deprecation warning', async () => {
6682
const result = await runCliCapture([
6783
'batch',

src/commands/cli-grammar/common.ts

Lines changed: 44 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,11 @@ export function request(
2626
}
2727

2828
function normalizeCommonRequestOptions(options: CommandInput): CommandInput {
29-
return options.deviceTarget !== undefined && options.target === undefined
30-
? { ...options, target: options.deviceTarget }
31-
: options;
29+
const normalizedTarget =
30+
options.deviceTarget ?? (typeof options.target === 'string' ? options.target : undefined);
31+
if (normalizedTarget === undefined && options.target === undefined) return options;
32+
const { target: _target, ...rest } = options;
33+
return normalizedTarget === undefined ? rest : { ...rest, target: normalizedTarget };
3234
}
3335

3436
export function commonInputFromFlags(flags: CliFlags): Record<string, unknown> {
@@ -99,15 +101,47 @@ export function targetInputFromClientTarget(
99101
return { kind: 'point', x: point.x, y: point.y };
100102
}
101103

102-
export function interactionTargetPositionals(input: InteractionTarget): string[] {
103-
if (input.ref !== undefined) return [input.ref, ...optionalString(input.label)];
104-
if (input.selector !== undefined) return [input.selector];
105-
return [String(input.x), String(input.y)];
104+
export function interactionTargetPositionals(input: InteractionTarget | CommandInput): string[] {
105+
const target = readTargetRecord(input);
106+
if (typeof target.ref === 'string') return [target.ref, ...optionalTargetLabel(target.label)];
107+
if (typeof target.selector === 'string') return [target.selector];
108+
if (target.kind === 'point' || target.x !== undefined || target.y !== undefined) {
109+
return [
110+
String(requiredTargetNumber(target.x, 'x')),
111+
String(requiredTargetNumber(target.y, 'y')),
112+
];
113+
}
114+
throw new AppError('INVALID_ARGS', 'interaction requires @ref, selector, or point target');
115+
}
116+
117+
export function elementTargetPositionals(input: ElementTarget | CommandInput): string[] {
118+
const target = readTargetRecord(input);
119+
if (typeof target.ref === 'string') return [target.ref, ...optionalTargetLabel(target.label)];
120+
if (typeof target.selector === 'string') return [target.selector];
121+
throw new AppError('INVALID_ARGS', 'element command requires @ref or selector target');
122+
}
123+
124+
function readTargetRecord(input: unknown): Record<string, unknown> {
125+
if (!input || typeof input !== 'object' || Array.isArray(input)) {
126+
throw new AppError('INVALID_ARGS', 'Expected target object.');
127+
}
128+
const record = input as Record<string, unknown>;
129+
const nestedTarget = record.target;
130+
if (nestedTarget && typeof nestedTarget === 'object' && !Array.isArray(nestedTarget)) {
131+
return nestedTarget as Record<string, unknown>;
132+
}
133+
return record;
134+
}
135+
136+
function requiredTargetNumber(value: unknown, field: string): number {
137+
if (typeof value !== 'number' || !Number.isFinite(value)) {
138+
throw new AppError('INVALID_ARGS', `point target requires numeric ${field}.`);
139+
}
140+
return value;
106141
}
107142

108-
export function elementTargetPositionals(input: ElementTarget): string[] {
109-
if (input.ref !== undefined) return [input.ref, ...optionalString(input.label)];
110-
return [input.selector];
143+
function optionalTargetLabel(value: unknown): string[] {
144+
return typeof value === 'string' && value.length > 0 ? [value] : [];
111145
}
112146

113147
export function readElementTargetFromPositionals(positionals: string[]): ElementTarget {

src/utils/cli-help.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,7 @@ Validation and evidence:
207207
Inline batch JSON example:
208208
agent-device batch --steps '[{"command":"open","input":{"app":"settings"}},{"command":"wait","input":{"kind":"duration","durationMs":100}}]'
209209
Batch step keys are command, input, and optional runtime. Put command arguments inside input using the same fields as the MCP/Node command. CLI still accepts legacy positionals/flags steps with a deprecation warning until the next major version.
210+
Never use args, step positionals, or flags for new batch JSON; put command inputs under input.
210211
Android animations: settings animations off/on, not animations disable/restore.
211212
Debug logs: logs clear --restart, logs mark, reproduce, then logs path; do not split clear/restart into separate stop/start commands.
212213
Network headers: network dump --include headers; do not write network log headers.

0 commit comments

Comments
 (0)