Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

## Unreleased

- Changed (android): the snapshot helper release manifest no longer carries `installArgs`, and the
helper installs with a fixed `adb install -r` like the IME helper. The array only ever spelled
`install -r` plus the `-t` that #2603 retired with the `testOnly` flag, so the manifest → flag →
option → flag round trip and its allowlist carried nothing. Older manifests that still contain
the field parse unchanged; the field is ignored. The adb provider `install` capability now takes
only `replace` (#2364).
- Fixed: BrowserStack sessions honour `--provider-project`, `--provider-build`, and
`--provider-session-name`. The capability builder emitted the legacy JSON Wire keys `device`,
`os_version`, and `app` at the top level next to the W3C `bstack:options` block; the hub treats a
Expand Down
6 changes: 3 additions & 3 deletions android/snapshot-helper/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,6 @@ The final instrumentation result for the default `snapshot` mode includes:
Failures return `ok=false`, `errorType`, and `message` in the final result.

The release manifest is a stable provider contract for the current helper protocol. Providers should
resolve the APK from `apkUrl`, verify `sha256`, install using `installArgs`, and run
`instrumentationRunner`. `installArgs` must start with `install`; extra arguments are limited to the
allowlisted adb install flags `-r`, `-t`, `-d`, and `-g`, and the consumer appends the APK path.
resolve the APK from `apkUrl`, verify `sha256`, install it with `adb install -r <apk>`, and run
`instrumentationRunner`. Manifests up to 0.21.3 also carried an `installArgs` array; it only ever
spelled `install -r` (plus `-t` while the helper was `testOnly`), so consumers can ignore it.
10 changes: 2 additions & 8 deletions packages/platform-android/src/__tests__/adb-executor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,13 +209,7 @@ test('createLocalAndroidAdbProvider exposes local pull and install capabilities'
});

await provider.pull?.('/sdcard/video.mp4', '/tmp/video.mp4', { allowFailure: true });
await provider.install?.('/tmp/app.apk', {
allowDowngrade: true,
allowTestPackages: true,
grantPermissions: true,
replace: true,
timeoutMs: 2000,
});
await provider.install?.('/tmp/app.apk', { replace: true, timeoutMs: 2000 });

assert.deepEqual(mockRunCmd.mock.calls, [
[
Expand All @@ -225,7 +219,7 @@ test('createLocalAndroidAdbProvider exposes local pull and install capabilities'
],
[
'adb',
['-s', 'emulator-5554', 'install', '-r', '-t', '-d', '-g', '/tmp/app.apk'],
['-s', 'emulator-5554', 'install', '-r', '/tmp/app.apk'],
{ timeoutMs: 2000, ...localAdbExecOptions },
],
]);
Expand Down
49 changes: 10 additions & 39 deletions packages/platform-android/src/__tests__/snapshot-helper.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ const manifest: AndroidSnapshotHelperManifest = {
targetSdk: 36,
outputFormat: 'uiautomator-xml',
statusProtocol: 'android-snapshot-helper-v1',
installArgs: ['install', '-r'],
};

beforeEach(() => {
Expand Down Expand Up @@ -592,17 +591,11 @@ test('ensureAndroidSnapshotHelper uninstalls and retries when signatures differ'
assert.deepEqual(calls[3], ['install', '-r', apkPath]);
});

test('ensureAndroidSnapshotHelper uses provider install capability and semantic install options', async () => {
test('ensureAndroidSnapshotHelper uses provider install capability with a replace install', async () => {
const tmpDir = await mkdtempForTest('snapshot-helper-provider-install-');
const apkPath = path.join(tmpDir, 'helper.apk');
await fs.writeFile(apkPath, 'helper-apk');
const installCalls: Array<{
apkPath: string;
replace?: boolean;
allowTestPackages?: boolean;
allowDowngrade?: boolean;
grantPermissions?: boolean;
}> = [];
const installCalls: Array<{ apkPath: string; replace?: boolean }> = [];
const adb: AndroidAdbExecutor = async (args) => {
if (args.includes('--show-versioncode')) {
return { exitCode: 1, stdout: '', stderr: 'not found' };
Expand All @@ -612,13 +605,7 @@ test('ensureAndroidSnapshotHelper uses provider install capability and semantic
const adbProvider: AndroidAdbProvider = {
exec: adb,
install: async (path, options) => {
installCalls.push({
apkPath: path,
replace: options?.replace,
allowTestPackages: options?.allowTestPackages,
allowDowngrade: options?.allowDowngrade,
grantPermissions: options?.grantPermissions,
});
installCalls.push({ apkPath: path, replace: options?.replace });
return { exitCode: 0, stdout: '', stderr: '' };
},
};
Expand All @@ -628,24 +615,12 @@ test('ensureAndroidSnapshotHelper uses provider install capability and semantic
adbProvider,
artifact: {
apkPath,
manifest: {
...manifest,
installArgs: ['install', '-r', '-t', '-d', '-g'],
sha256: sha256Text('helper-apk'),
},
manifest: { ...manifest, sha256: sha256Text('helper-apk') },
},
});

assert.equal(result.installed, true);
assert.deepEqual(installCalls, [
{
apkPath,
replace: true,
allowTestPackages: true,
allowDowngrade: true,
grantPermissions: true,
},
]);
assert.deepEqual(installCalls, [{ apkPath, replace: true }]);
});

test('ensureAndroidSnapshotHelper retry install also uses provider install capability', async () => {
Expand Down Expand Up @@ -935,15 +910,11 @@ test('parseAndroidSnapshotHelperManifest validates manifest shape', () => {
assert.throws(() => parseAndroidSnapshotHelperManifest({ ...manifest, outputFormat: 'json' }), {
message: 'Android snapshot helper manifest outputFormat must be "uiautomator-xml".',
});
assert.throws(() => parseAndroidSnapshotHelperManifest({ ...manifest, installArgs: ['shell'] }), {
message: 'Android snapshot helper manifest installArgs must start with "install".',
});
assert.throws(
() => parseAndroidSnapshotHelperManifest({ ...manifest, installArgs: ['install', '--user'] }),
{
message:
'Android snapshot helper manifest installArgs contains unsupported install flag "--user".',
},
// Release manifests up to 0.21.3 carried installArgs; it is ignored, never validated or kept.
assert.equal(
'installArgs' in
parseAndroidSnapshotHelperManifest({ ...manifest, installArgs: ['install', '-r', '-t'] }),
false,
);
assert.throws(() => parseAndroidSnapshotHelperManifest({ ...manifest, sha256: 'not-a-sha' }), {
message: 'Android snapshot helper manifest sha256 must be a 64-character hex string.',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ export const ANDROID_SNAPSHOT_HELPER_FIXTURE_ARTIFACT: AndroidSnapshotHelperArti
targetSdk: 36,
outputFormat: 'uiautomator-xml',
statusProtocol: 'android-snapshot-helper-v1',
installArgs: ['install', '-r'],
},
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ export const ANDROID_TOUCH_HELPER_MANIFEST = {
versionCode: 17000,
instrumentationRunner: 'com.callstack.agentdevice.snapshothelper/.SnapshotInstrumentation',
statusProtocol: 'android-snapshot-helper-v1' as const,
installArgs: ['install', '-r'],
};

export function androidTouchHelperResultRecord(values: Record<string, string>): string {
Expand Down
12 changes: 3 additions & 9 deletions packages/platform-android/src/adb-transfer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ test('semantic provider methods win over the exec fallback and escape the overri
expect(escaped).toBe(2);
});

test('the exec fallback lowers semantic install options into adb flags', async () => {
test('the exec fallback lowers the semantic replace option into adb flags', async () => {
bindAndroidAdbHostStub();
const calls: string[][] = [];
const provider: AndroidAdbProvider = {
Expand All @@ -47,17 +47,11 @@ test('the exec fallback lowers semantic install options into adb flags', async (
},
};

await installAndroidAdbPackage('/tmp/a.apk', {
provider,
replace: true,
allowTestPackages: true,
allowDowngrade: true,
grantPermissions: true,
});
await installAndroidAdbPackage('/tmp/a.apk', { provider, replace: true });
await pullAndroidAdbFile('/device/a.apk', '/tmp/a.apk', { provider });

expect(calls).toEqual([
['install', '-r', '-t', '-d', '-g', '/tmp/a.apk'],
['install', '-r', '/tmp/a.apk'],
['pull', '/device/a.apk', '/tmp/a.apk'],
]);
});
Expand Down
17 changes: 4 additions & 13 deletions packages/platform-android/src/adb-transport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,9 +102,6 @@ export type AndroidPortReverseProvider = {
export type AndroidAdbTransferOptions = AndroidAdbExecutorOptions;
export type AndroidAdbInstallOptions = AndroidAdbTransferOptions & {
replace?: boolean;
allowTestPackages?: boolean;
allowDowngrade?: boolean;
grantPermissions?: boolean;
};

export type AndroidAdbPuller = (
Expand All @@ -114,8 +111,8 @@ export type AndroidAdbPuller = (
) => Promise<AndroidAdbExecutorResult>;

/**
* Installs an APK path. Implementations are responsible for honoring semantic
* install options such as replace/test/downgrade/grant-permissions.
* Installs an APK path. Implementations are responsible for honoring the
* semantic `replace` option (`adb install -r`).
*/
export type AndroidAdbInstaller = (
apkPath: string,
Expand Down Expand Up @@ -196,12 +193,6 @@ export function normalizeAndroidAdbInstallOptions(options?: AndroidAdbInstallOpt
installArgs: string[];
execOptions: AndroidAdbTransferOptions;
} {
const { replace, allowTestPackages, allowDowngrade, grantPermissions, ...execOptions } =
options ?? {};
const installArgs: string[] = [];
if (replace) installArgs.push('-r');
if (allowTestPackages) installArgs.push('-t');
if (allowDowngrade) installArgs.push('-d');
if (grantPermissions) installArgs.push('-g');
return { installArgs, execOptions };
const { replace, ...execOptions } = options ?? {};
return { installArgs: replace ? ['-r'] : [], execOptions };
}
1 change: 0 additions & 1 deletion packages/platform-android/src/helper-artifacts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ export type AndroidSnapshotHelperManifest = {
targetSdk?: number;
outputFormat: 'uiautomator-xml';
statusProtocol: 'android-snapshot-helper-v1';
installArgs: string[];
};

export type AndroidSnapshotHelperArtifact = {
Expand Down
6 changes: 1 addition & 5 deletions packages/platform-android/src/mechanics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -255,11 +255,7 @@ export {
classifyAndroidHelperContent,
type AndroidHelperContentRecoveryDecision,
} from './snapshot-content-recovery.ts';
export {
parseAndroidSnapshotHelperManifest,
readAndroidSnapshotHelperInstallOptions,
type AndroidSnapshotHelperInstallOptions,
} from './snapshot-helper-artifact.ts';
export { parseAndroidSnapshotHelperManifest } from './snapshot-helper-artifact.ts';
export {
buildAndroidSnapshotHelperArgs,
buildAndroidSnapshotHelperCaptureOptions,
Expand Down
93 changes: 3 additions & 90 deletions packages/platform-android/src/snapshot-helper-artifact.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,24 +10,9 @@ import {
type AndroidSnapshotHelperManifest,
} from './snapshot-helper-types.ts';

export type AndroidSnapshotHelperInstallOptions = {
replace?: boolean;
allowTestPackages?: boolean;
allowDowngrade?: boolean;
grantPermissions?: boolean;
};

type AndroidSnapshotHelperInstallOptionName = keyof AndroidSnapshotHelperInstallOptions;

const ANDROID_SNAPSHOT_HELPER_INSTALL_FLAG_OPTIONS = {
'-r': 'replace',
'-t': 'allowTestPackages',
'-d': 'allowDowngrade',
'-g': 'grantPermissions',
} as const satisfies Record<string, AndroidSnapshotHelperInstallOptionName>;

type AndroidSnapshotHelperInstallFlag = keyof typeof ANDROID_SNAPSHOT_HELPER_INSTALL_FLAG_OPTIONS;

// Release manifests up to 0.21.3 also carried `installArgs`; it only ever encoded the `-t` flag the
// former `testOnly` helper needed, so the parser ignores it and the helper installs with a fixed
// `adb install -r` like every other helper APK.
export function parseAndroidSnapshotHelperManifest(value: unknown): AndroidSnapshotHelperManifest {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
throw new AppError('INVALID_ARGS', 'Android snapshot helper manifest must be an object.');
Expand Down Expand Up @@ -57,7 +42,6 @@ export function parseAndroidSnapshotHelperManifest(value: unknown): AndroidSnaps
'statusProtocol',
ANDROID_SNAPSHOT_HELPER_PROTOCOL,
),
installArgs: readAndroidSnapshotHelperManifestInstallArgs(record.installArgs),
};
}

Expand All @@ -73,54 +57,6 @@ function readLiteral<const Value extends string>(
return readAndroidHelperManifestLiteral(value, field, expected, 'snapshot helper');
}

export function readAndroidSnapshotHelperInstallOptions(
manifest: AndroidSnapshotHelperManifest,
): AndroidSnapshotHelperInstallOptions {
const installArgs = readAndroidSnapshotHelperManifestInstallArgs(manifest.installArgs);
return installOptionsFromSnapshotHelperInstallArgs(installArgs);
}

function readAndroidSnapshotHelperManifestInstallArgs(value: unknown): string[] {
const installArgs = readStringArray(value, 'installArgs');
if (installArgs[0] !== 'install') {
throw new AppError(
'INVALID_ARGS',
'Android snapshot helper manifest installArgs must start with "install".',
);
}
if (installArgs.some((arg) => arg.includes('\u0000'))) {
throw new AppError(
'INVALID_ARGS',
'Android snapshot helper manifest installArgs must not contain null bytes.',
);
}
const unsupportedArg = installArgs.slice(1).find((arg) => !isAllowedInstallFlag(arg));
if (unsupportedArg) {
throw new AppError(
'INVALID_ARGS',
`Android snapshot helper manifest installArgs contains unsupported install flag "${unsupportedArg}".`,
);
}
return installArgs;
}

function installOptionsFromSnapshotHelperInstallArgs(
installArgs: string[],
): AndroidSnapshotHelperInstallOptions {
const options: AndroidSnapshotHelperInstallOptions = {};
for (const arg of installArgs.slice(1)) {
const optionName = installOptionForSnapshotHelperInstallFlag(arg);
if (!optionName) {
throw new AppError(
'INVALID_ARGS',
`Android snapshot helper manifest installArgs contains unsupported install flag "${arg}".`,
);
}
options[optionName] = true;
}
return options;
}

function readSha256(value: unknown): string {
const sha256 = readString(value, 'sha256').trim().toLowerCase();
if (sha256.length !== 64 || !isLowerHex(sha256)) {
Expand Down Expand Up @@ -148,29 +84,6 @@ function readOptionalNullableString(value: unknown, field: string): string | nul
return readString(value, field);
}

function readStringArray(value: unknown, field: string): string[] {
if (!Array.isArray(value) || !value.every((entry) => typeof entry === 'string')) {
throw new AppError(
'INVALID_ARGS',
`Android snapshot helper manifest ${field} must be a string array.`,
);
}
return value;
}

function isAllowedInstallFlag(arg: string): boolean {
return installOptionForSnapshotHelperInstallFlag(arg) !== undefined;
}

function installOptionForSnapshotHelperInstallFlag(
arg: string,
): AndroidSnapshotHelperInstallOptionName | undefined {
if (!Object.hasOwn(ANDROID_SNAPSHOT_HELPER_INSTALL_FLAG_OPTIONS, arg)) {
return undefined;
}
return ANDROID_SNAPSHOT_HELPER_INSTALL_FLAG_OPTIONS[arg as AndroidSnapshotHelperInstallFlag];
}

function isLowerHex(value: string): boolean {
for (const char of value) {
const code = char.charCodeAt(0);
Expand Down
Loading
Loading