Skip to content

Commit 5b758f2

Browse files
committed
fix: reject unsupported launch args openings
1 parent 3b8959b commit 5b758f2

7 files changed

Lines changed: 79 additions & 132 deletions

File tree

src/commands/client-command-contracts.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,9 @@ export const clientCommandDefinitions = [
8181
surface: enumField(SURFACE_VALUES),
8282
activity: stringField('Android activity name.'),
8383
launchConsole: stringField('Launch console mode.'),
84-
launchArgs: stringArrayField('iOS launch arguments forwarded verbatim to the app process.'),
84+
launchArgs: stringArrayField(
85+
'Launch arguments forwarded verbatim to the platform launch command.',
86+
),
8587
relaunch: booleanField('Force relaunch.'),
8688
saveScript: jsonSchemaField<boolean | string>({ oneOf: [booleanSchema(), stringSchema()] }),
8789
noRecord: booleanField('Do not record this action.'),

src/core/__tests__/dispatch-open.test.ts

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,7 @@ vi.mock('../../platforms/ios/apps.ts', async (importOriginal) => {
2020
});
2121

2222
vi.mock('../../platforms/android/app-lifecycle.ts', async (importOriginal) => {
23-
const actual =
24-
await importOriginal<typeof import('../../platforms/android/app-lifecycle.ts')>();
23+
const actual = await importOriginal<typeof import('../../platforms/android/app-lifecycle.ts')>();
2524
return {
2625
...actual,
2726
openAndroidApp: vi.fn(async () => {}),
@@ -58,6 +57,18 @@ test('dispatch open rejects URL as first argument when second URL is provided',
5857
);
5958
});
6059

60+
test('dispatch open rejects launch arguments without an app target', async () => {
61+
await assert.rejects(
62+
() => dispatchCommand(IOS_SIMULATOR, 'open', [], undefined, { launchArgs: ['-Flag'] }),
63+
(error: unknown) => {
64+
assert.equal(error instanceof AppError, true);
65+
assert.equal((error as AppError).code, 'INVALID_ARGS');
66+
assert.match((error as AppError).message, /requires an app target/i);
67+
return true;
68+
},
69+
);
70+
});
71+
6172
test('dispatch open forwards Android launch arguments to openAndroidApp', async () => {
6273
const device: DeviceInfo = {
6374
platform: 'android',
@@ -79,6 +90,29 @@ test('dispatch open forwards Android launch arguments to openAndroidApp', async
7990
assert.deepEqual(optionsArg.launchArgs, ['--es', 'KEY', 'value']);
8091
});
8192

93+
test('dispatch open rejects launch arguments on Linux', async () => {
94+
const device: DeviceInfo = {
95+
platform: 'linux',
96+
id: 'linux-local',
97+
name: 'Linux',
98+
kind: 'device',
99+
booted: true,
100+
};
101+
102+
await assert.rejects(
103+
() =>
104+
dispatchCommand(device, 'open', ['org.example.App'], undefined, {
105+
launchArgs: ['--fixture', 'demo'],
106+
}),
107+
(error: unknown) => {
108+
assert.equal(error instanceof AppError, true);
109+
assert.equal((error as AppError).code, 'UNSUPPORTED_OPERATION');
110+
assert.match((error as AppError).message, /Linux/i);
111+
return true;
112+
},
113+
);
114+
});
115+
82116
test('dispatch open clears Maestro iOS simulator state and launches once', async () => {
83117
const result = await dispatchCommand(IOS_SIMULATOR, 'open', ['com.example.app'], undefined, {
84118
clearAppState: true,

src/core/dispatch.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -181,19 +181,26 @@ async function handleOpenCommand(
181181
const app = positionals[0];
182182
const url = positionals[1];
183183
const launchConsole = context?.launchConsole;
184+
const launchArgs = context?.launchArgs;
184185
if (positionals.length > 2) {
185186
throw new AppError('INVALID_ARGS', 'open accepts at most two arguments: <app|url> [url]');
186187
}
187188
if (!app) {
188189
if (launchConsole) {
189190
throw new AppError('INVALID_ARGS', '--launch-console requires an app target');
190191
}
192+
if (launchArgs && launchArgs.length > 0) {
193+
throw new AppError('INVALID_ARGS', '--launch-args requires an app target');
194+
}
191195
await interactor.openDevice();
192196
return { app: null, ...successText('Opened device') };
193197
}
194198
if (launchConsole && (device.platform !== 'ios' || device.kind !== 'simulator')) {
195199
throw new AppError('UNSUPPORTED_OPERATION', LAUNCH_CONSOLE_IOS_SIMULATOR_ONLY_MESSAGE);
196200
}
201+
if (device.platform === 'linux' && launchArgs && launchArgs.length > 0) {
202+
throw new AppError('UNSUPPORTED_OPERATION', '--launch-args is not supported on Linux.');
203+
}
197204
if (url !== undefined) {
198205
if (isDeepLinkTarget(app)) {
199206
throw new AppError(
@@ -210,7 +217,7 @@ async function handleOpenCommand(
210217
await interactor.open(app, {
211218
activity: context?.activity,
212219
appBundleId: context?.appBundleId,
213-
launchArgs: context?.launchArgs,
220+
launchArgs,
214221
url,
215222
});
216223
return { app, url, ...successText(`Opened: ${app}`) };
@@ -237,7 +244,7 @@ async function handleOpenCommand(
237244
activity: context?.activity,
238245
appBundleId: context?.appBundleId,
239246
launchConsole,
240-
launchArgs: context?.launchArgs,
247+
launchArgs,
241248
});
242249
return { app, ...(launchConsole ? { launchConsole } : {}), ...successText(`Opened: ${app}`) };
243250
}

src/platforms/android/__tests__/index.test.ts

Lines changed: 27 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,24 @@ async function withMockedAdb(
6666
}
6767
}
6868

69+
function androidOpenAdbScript(): string {
70+
return [
71+
'#!/bin/sh',
72+
'printf "__CMD__\\n" >> "$AGENT_DEVICE_TEST_ARGS_FILE"',
73+
'printf "%s\\n" "$@" >> "$AGENT_DEVICE_TEST_ARGS_FILE"',
74+
'if [ "$1" = "-s" ]; then',
75+
' shift',
76+
' shift',
77+
'fi',
78+
'if [ "$1" = "shell" ] && [ "$2" = "am" ] && [ "$3" = "start" ]; then',
79+
' echo "Status: ok"',
80+
' exit 0',
81+
'fi',
82+
'exit 0',
83+
'',
84+
].join('\n');
85+
}
86+
6987
test('parseUiHierarchy reads double-quoted Android node attributes', () => {
7088
const xml =
7189
'<hierarchy><node class="android.widget.TextView" text="Hello" content-desc="Greeting" resource-id="com.demo:id/title" bounds="[10,20][110,60]" clickable="true" enabled="true"/></hierarchy>';
@@ -1121,25 +1139,7 @@ test('installAndroidInstallablePath invalidates cached display-name package matc
11211139
test('openAndroidApp default launch uses -p package flag', async () => {
11221140
await withMockedAdb(
11231141
'agent-device-android-open-default-',
1124-
[
1125-
'#!/bin/sh',
1126-
'printf "__CMD__\\n" >> "$AGENT_DEVICE_TEST_ARGS_FILE"',
1127-
'printf "%s\\n" "$@" >> "$AGENT_DEVICE_TEST_ARGS_FILE"',
1128-
'if [ "$1" = "-s" ]; then',
1129-
' shift',
1130-
' shift',
1131-
'fi',
1132-
'if [ "$1" = "shell" ] && [ "$2" = "pm" ] && [ "$3" = "list" ]; then',
1133-
' echo "package:com.example.app"',
1134-
' exit 0',
1135-
'fi',
1136-
'if [ "$1" = "shell" ] && [ "$2" = "am" ] && [ "$3" = "start" ]; then',
1137-
' echo "Status: ok"',
1138-
' exit 0',
1139-
'fi',
1140-
'exit 0',
1141-
'',
1142-
].join('\n'),
1142+
androidOpenAdbScript(),
11431143
async ({ argsLogPath, device }) => {
11441144
await openAndroidApp(device, 'com.example.app');
11451145
const logged = await fs.readFile(argsLogPath, 'utf8');
@@ -1152,25 +1152,7 @@ test('openAndroidApp default launch uses -p package flag', async () => {
11521152
test('openAndroidApp appends launchArgs to am start when launching by package', async () => {
11531153
await withMockedAdb(
11541154
'agent-device-android-open-launch-args-',
1155-
[
1156-
'#!/bin/sh',
1157-
'printf "__CMD__\\n" >> "$AGENT_DEVICE_TEST_ARGS_FILE"',
1158-
'printf "%s\\n" "$@" >> "$AGENT_DEVICE_TEST_ARGS_FILE"',
1159-
'if [ "$1" = "-s" ]; then',
1160-
' shift',
1161-
' shift',
1162-
'fi',
1163-
'if [ "$1" = "shell" ] && [ "$2" = "pm" ] && [ "$3" = "list" ]; then',
1164-
' echo "package:com.example.app"',
1165-
' exit 0',
1166-
'fi',
1167-
'if [ "$1" = "shell" ] && [ "$2" = "am" ] && [ "$3" = "start" ]; then',
1168-
' echo "Status: ok"',
1169-
' exit 0',
1170-
'fi',
1171-
'exit 0',
1172-
'',
1173-
].join('\n'),
1155+
androidOpenAdbScript(),
11741156
async ({ argsLogPath, device }) => {
11751157
await openAndroidApp(device, 'com.example.app', {
11761158
launchArgs: ['--es', 'screen', 'home', '--ez', 'fresh', 'true'],
@@ -1184,88 +1166,36 @@ test('openAndroidApp appends launchArgs to am start when launching by package',
11841166
test('openAndroidApp appends launchArgs to am start when activity override is set', async () => {
11851167
await withMockedAdb(
11861168
'agent-device-android-open-launch-args-activity-',
1187-
[
1188-
'#!/bin/sh',
1189-
'printf "__CMD__\\n" >> "$AGENT_DEVICE_TEST_ARGS_FILE"',
1190-
'printf "%s\\n" "$@" >> "$AGENT_DEVICE_TEST_ARGS_FILE"',
1191-
'if [ "$1" = "-s" ]; then',
1192-
' shift',
1193-
' shift',
1194-
'fi',
1195-
'if [ "$1" = "shell" ] && [ "$2" = "am" ] && [ "$3" = "start" ]; then',
1196-
' echo "Status: ok"',
1197-
' exit 0',
1198-
'fi',
1199-
'exit 0',
1200-
'',
1201-
].join('\n'),
1169+
androidOpenAdbScript(),
12021170
async ({ argsLogPath, device }) => {
12031171
await openAndroidApp(device, 'com.example.app', {
12041172
activity: '.MainActivity',
12051173
launchArgs: ['--es', 'mode', 'debug'],
12061174
});
12071175
const logged = await fs.readFile(argsLogPath, 'utf8');
1208-
assert.match(
1209-
logged,
1210-
/-n\ncom\.example\.app\/\.MainActivity\n--es\nmode\ndebug/,
1211-
);
1176+
assert.match(logged, /-n\ncom\.example\.app\/\.MainActivity\n--es\nmode\ndebug/);
12121177
},
12131178
);
12141179
});
12151180

12161181
test('openAndroidApp appends launchArgs to am start for deep link URL opens', async () => {
12171182
await withMockedAdb(
12181183
'agent-device-android-open-launch-args-url-',
1219-
[
1220-
'#!/bin/sh',
1221-
'printf "__CMD__\\n" >> "$AGENT_DEVICE_TEST_ARGS_FILE"',
1222-
'printf "%s\\n" "$@" >> "$AGENT_DEVICE_TEST_ARGS_FILE"',
1223-
'if [ "$1" = "-s" ]; then',
1224-
' shift',
1225-
' shift',
1226-
'fi',
1227-
'if [ "$1" = "shell" ] && [ "$2" = "am" ] && [ "$3" = "start" ]; then',
1228-
' echo "Status: ok"',
1229-
' exit 0',
1230-
'fi',
1231-
'exit 0',
1232-
'',
1233-
].join('\n'),
1184+
androidOpenAdbScript(),
12341185
async ({ argsLogPath, device }) => {
12351186
await openAndroidApp(device, 'myapp://item/42', {
12361187
launchArgs: ['--es', 'ref', 'campaign'],
12371188
});
12381189
const logged = await fs.readFile(argsLogPath, 'utf8');
1239-
assert.match(
1240-
logged,
1241-
/-d\nmyapp:\/\/item\/42\n--es\nref\ncampaign/,
1242-
);
1190+
assert.match(logged, /-d\nmyapp:\/\/item\/42\n--es\nref\ncampaign/);
12431191
},
12441192
);
12451193
});
12461194

12471195
test('openAndroidApp appends launchArgs to am start for app-bound URL opens', async () => {
12481196
await withMockedAdb(
12491197
'agent-device-android-open-launch-args-app-bound-url-',
1250-
[
1251-
'#!/bin/sh',
1252-
'printf "__CMD__\\n" >> "$AGENT_DEVICE_TEST_ARGS_FILE"',
1253-
'printf "%s\\n" "$@" >> "$AGENT_DEVICE_TEST_ARGS_FILE"',
1254-
'if [ "$1" = "-s" ]; then',
1255-
' shift',
1256-
' shift',
1257-
'fi',
1258-
'if [ "$1" = "shell" ] && [ "$2" = "pm" ] && [ "$3" = "list" ]; then',
1259-
' echo "package:com.example.app"',
1260-
' exit 0',
1261-
'fi',
1262-
'if [ "$1" = "shell" ] && [ "$2" = "am" ] && [ "$3" = "start" ]; then',
1263-
' echo "Status: ok"',
1264-
' exit 0',
1265-
'fi',
1266-
'exit 0',
1267-
'',
1268-
].join('\n'),
1198+
androidOpenAdbScript(),
12691199
async ({ argsLogPath, device }) => {
12701200
await openAndroidApp(device, 'com.example.app', {
12711201
url: 'https://example.com/promo',
@@ -1283,25 +1213,7 @@ test('openAndroidApp appends launchArgs to am start for app-bound URL opens', as
12831213
test('openAndroidApp shell-quotes launchArgs containing JSON or shell metacharacters', async () => {
12841214
await withMockedAdb(
12851215
'agent-device-android-open-launch-args-quoting-',
1286-
[
1287-
'#!/bin/sh',
1288-
'printf "__CMD__\\n" >> "$AGENT_DEVICE_TEST_ARGS_FILE"',
1289-
'printf "%s\\n" "$@" >> "$AGENT_DEVICE_TEST_ARGS_FILE"',
1290-
'if [ "$1" = "-s" ]; then',
1291-
' shift',
1292-
' shift',
1293-
'fi',
1294-
'if [ "$1" = "shell" ] && [ "$2" = "pm" ] && [ "$3" = "list" ]; then',
1295-
' echo "package:com.example.app"',
1296-
' exit 0',
1297-
'fi',
1298-
'if [ "$1" = "shell" ] && [ "$2" = "am" ] && [ "$3" = "start" ]; then',
1299-
' echo "Status: ok"',
1300-
' exit 0',
1301-
'fi',
1302-
'exit 0',
1303-
'',
1304-
].join('\n'),
1216+
androidOpenAdbScript(),
13051217
async ({ argsLogPath, device }) => {
13061218
// Value contains characters the device shell would otherwise re-interpret:
13071219
// `#` (comment), `;` (statement separator), `&` (background), `*` (glob),
@@ -1313,10 +1225,7 @@ test('openAndroidApp shell-quotes launchArgs containing JSON or shell metacharac
13131225
const logged = await fs.readFile(argsLogPath, 'utf8');
13141226
// `--es` and the safe extra key pass through unquoted; the JSON value
13151227
// is single-quoted so `adb shell` re-tokenisation preserves it.
1316-
assert.match(
1317-
logged,
1318-
/--es\nEXTRA_CONFIG\n'\{"a":"x #y;z&w","b":"path\/\*"\}'/,
1319-
);
1228+
assert.match(logged, /--es\nEXTRA_CONFIG\n'\{"a":"x #y;z&w","b":"path\/\*"\}'/);
13201229
},
13211230
);
13221231
});

src/platforms/android/app-lifecycle.ts

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -433,10 +433,7 @@ async function openAndroidPackageActivity(
433433
? activity
434434
: `${packageName}/${activity.startsWith('.') ? activity : `.${activity}`}`;
435435
try {
436-
await runAndroidAdb(
437-
device,
438-
buildAndroidActivityLaunchArgs(component, launchCategory, options),
439-
);
436+
await runAndroidAdb(device, buildAndroidActivityLaunchArgs(component, launchCategory, options));
440437
} catch (error) {
441438
await maybeRethrowAndroidMissingPackageError(device, packageName, error);
442439
throw error;
@@ -481,10 +478,7 @@ async function openAndroidPackage(
481478
stderr: primaryResult.stderr,
482479
});
483480
}
484-
await runAndroidAdb(
485-
device,
486-
buildAndroidActivityLaunchArgs(component, launchCategory, options),
487-
);
481+
await runAndroidAdb(device, buildAndroidActivityLaunchArgs(component, launchCategory, options));
488482
}
489483

490484
function buildAndroidActivityLaunchArgs(

src/utils/__tests__/args.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,7 @@ test('usageForCommand documents open --launch-args', () => {
331331
if (help === null) throw new Error('Expected open help text');
332332
assert.match(help, /--launch-args <arg>/);
333333
assert.match(help, /forwarded verbatim/);
334+
assert.match(help, /Linux and macOS reject the flag/);
334335
});
335336

336337
test('parseArgs accepts install-from-source GitHub Actions artifact flag', () => {

src/utils/cli-flags.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -506,7 +506,7 @@ const FLAG_DEFINITIONS: readonly FlagDefinition[] = [
506506
multiple: true,
507507
usageLabel: '--launch-args <arg>',
508508
usageDescription:
509-
'open: repeatable launch argument forwarded verbatim to the iOS launch command (simctl launch positional args for simulators; devicectl process launch positional args for devices, after `--`). Currently supported only on iOS; Android and macOS reject the flag.',
509+
'open: repeatable launch argument forwarded verbatim to the platform launch command (iOS app process args; Android adb shell am start args). Linux and macOS reject the flag.',
510510
},
511511
{
512512
key: 'header',

0 commit comments

Comments
 (0)