Skip to content
Open
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
31 changes: 31 additions & 0 deletions packages/platform-android/src/__tests__/adb-executor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,37 @@ test('createLocalAndroidAdbProvider exposes local pull and install capabilities'
]);
});

test('createLocalAndroidAdbProvider carries a private server port through every adb capability', async () => {
mockRunCmd.mockClear();
mockRunCmdBackground.mockClear();
const provider = createLocalAndroidAdbProvider(
{
platform: 'android',
id: 'emulator-5554',
name: 'Pixel Emulator',
kind: 'emulator',
booted: true,
},
{ serverPort: 15_037 },
);

await provider.exec(['shell', 'echo', 'ok']);
provider.spawn?.(['logcat']);
await provider.reverse?.ensure({ local: 'tcp:8081', remote: 'tcp:8081' });
await provider.pull?.('/sdcard/video.mp4', '/tmp/video.mp4');
await provider.install?.('/tmp/app.apk');

assert.equal(readServerPort(mockRunCmdBackground.mock.calls[0]?.[2]), 15_037);
assert.equal(mockRunCmd.mock.calls.length, 4);
for (const call of mockRunCmd.mock.calls) assert.equal(readServerPort(call[2]), 15_037);
});

function readServerPort(options: unknown): number | undefined {
if (options === null || typeof options !== 'object') return undefined;
const value = (options as { serverPort?: unknown }).serverPort;
return typeof value === 'number' ? value : undefined;
}

test('createAndroidPortReverseManager makes duplicate setup idempotent and cleans owner mappings', async () => {
const calls: string[][] = [];
const manager = createAndroidPortReverseManager(async (args) => {
Expand Down
191 changes: 191 additions & 0 deletions packages/platform-android/src/adb-provider-scope.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ import { expect, test } from 'vitest';
import type { DeviceInfo } from '@agent-device/kernel/device';
import { bindAndroidAdbHostStub } from './adb-host.fixtures.ts';
import {
createLocalAndroidAdbProvider,
resolveAndroidAdbExecutor,
resolveAndroidAdbProvider,
resolveAndroidTextInjector,
resolveScopedAndroidAdbBackgroundTransport,
withAndroidAdbProvider,
} from './adb-provider-scope.ts';
import { runAndroidHostAdb } from './adb-host.ts';
import type { AndroidAdbExecutorResult, AndroidAdbProvider } from './adb-transport.ts';

const DEVICE: DeviceInfo = {
Expand Down Expand Up @@ -92,3 +94,192 @@ test('the installed override routes only normalized device-scoped adb calls to t
});
expect(providerCalls).toEqual([['shell', 'ls']]);
});

test('a managed port scope routes host adb and matching serial calls to its private server', async () => {
const hostCalls: Array<{ args: string[]; serverPort?: number }> = [];
bindAndroidAdbHostStub({
execHostAdb: async (args, options) => {
hostCalls.push({ args, serverPort: options?.serverPort });
return ok();
},
});

await withAndroidAdbProvider(
{ exec: async () => ok() },
{ serial: DEVICE.id, serverPort: 15_037 },
async () => {
await runAndroidHostAdb(['devices']);
await runAndroidHostAdb(['-s', DEVICE.id, 'shell', 'getprop']);
await runAndroidHostAdb(['-s', OTHER.id, 'shell', 'getprop']);
},
);
await runAndroidHostAdb(['devices']);

expect(hostCalls).toEqual([
{ args: ['devices'], serverPort: 15_037 },
{ args: ['-s', DEVICE.id, 'shell', 'getprop'], serverPort: 15_037 },
{ args: ['-s', OTHER.id, 'shell', 'getprop'] },
{ args: ['devices'] },
]);
});

test('a managed port scope classifies absolute adb commands and preserves the default boundary', async () => {
const providerCalls: string[][] = [];
const hostCalls: string[][] = [];
let captured:
| ((cmd: string, args: string[], options: object) => Promise<unknown> | undefined)
| undefined;
bindAndroidAdbHostStub({
execHostAdb: async (args) => {
hostCalls.push(args);
return ok();
},
withAdbCommandExecutorOverride: async (override, fn) => {
captured = override;
return await fn();
},
});

await withAndroidAdbProvider(
{
exec: async (args) => {
providerCalls.push(args);
return ok();
},
},
{ serial: DEVICE.id, serverPort: 15_037 },
async () => {
const global = captured?.('/opt/android-sdk/platform-tools/adb', ['devices', '-l'], {});
const matching = captured?.(
'/opt/android-sdk/platform-tools/adb',
['-s', DEVICE.id, 'shell', 'ls'],
{},
);
expect(captured?.('adb', ['-s', OTHER.id, 'shell', 'ls'], {})).toBeUndefined();
expect(captured?.('emulator', ['-list-avds'], {})).toBeUndefined();
expect(global).toBeDefined();
expect(matching).toBeDefined();
await global;
await matching;
},
);

expect(hostCalls).toEqual([['devices', '-l']]);
expect(providerCalls).toEqual([['shell', 'ls']]);
});

test('a managed port scope keeps shell -s arguments on the private transport', async () => {
const hostCalls: Array<{ args: string[]; serverPort?: number }> = [];
let captured:
| ((cmd: string, args: string[], options: object) => Promise<unknown> | undefined)
| undefined;
bindAndroidAdbHostStub({
execHostAdb: async (args, options) => {
hostCalls.push({ args, serverPort: options?.serverPort });
return ok();
},
withAdbCommandExecutorOverride: async (override, fn) => {
captured = override;
return await fn();
},
});

await withAndroidAdbProvider(
{ exec: async () => ok() },
{ serial: DEVICE.id, serverPort: 15_037 },
async () => {
await runAndroidHostAdb(['shell', 'echo', '-s', OTHER.id]);
const shellCommand = captured?.('adb', ['shell', 'echo', '-s', OTHER.id], {});
expect(shellCommand).toBeDefined();
await shellCommand;
},
);

expect(hostCalls).toEqual([
{ args: ['shell', 'echo', '-s', OTHER.id], serverPort: 15_037 },
{ args: ['shell', 'echo', '-s', OTHER.id], serverPort: 15_037 },
]);
});

test('a managed port scope restores the default transport after task failure', async () => {
const ports: Array<number | undefined> = [];
bindAndroidAdbHostStub({
execHostAdb: async (_args, options) => {
ports.push(options?.serverPort);
return ok();
},
});

await expect(
withAndroidAdbProvider(
{ exec: async () => ok() },
{ serial: DEVICE.id, serverPort: 15_037 },
async () => {
await runAndroidHostAdb(['devices']);
throw new Error('stop managed request');
},
),
).rejects.toThrow('stop managed request');
await runAndroidHostAdb(['devices']);

expect(ports).toEqual([15_037, undefined]);
});

test('a managed port scope carries its server through the local background transport', async () => {
const spawnCalls: Array<{ serial: string; args: string[]; serverPort?: number }> = [];
bindAndroidAdbHostStub({
spawnSerialAdb: (serial, args, options) => {
spawnCalls.push({ serial, args, serverPort: options?.serverPort });
return undefined as never;
},
});
const deviceProvider = createLocalAndroidAdbProvider(DEVICE, { serverPort: 15_037 });

await withAndroidAdbProvider(
deviceProvider,
{ serial: DEVICE.id, serverPort: 15_037 },
async () => {
const transport = resolveScopedAndroidAdbBackgroundTransport(DEVICE);
expect(transport.mode).toBe('transport-composed');
if (transport.mode === 'transport-composed') {
transport.spawn?.(['logcat', '-v', 'threadtime']);
}
},
);

expect(spawnCalls).toEqual([
{ serial: DEVICE.id, args: ['logcat', '-v', 'threadtime'], serverPort: 15_037 },
]);
});

test('managed port scopes remain isolated across concurrent requests', async () => {
const hostCalls: Array<{ serial: string; serverPort?: number }> = [];
bindAndroidAdbHostStub({
execHostAdb: async (args, options) => {
hostCalls.push({
serial: args[args.indexOf('-s') + 1] ?? 'global',
serverPort: options?.serverPort,
});
await Promise.resolve();
return ok();
},
});

await Promise.all([
withAndroidAdbProvider(
{ exec: async () => ok() },
{ serial: DEVICE.id, serverPort: 15_037 },
async () => await runAndroidHostAdb(['-s', DEVICE.id, 'shell', 'id']),
),
withAndroidAdbProvider(
{ exec: async () => ok() },
{ serial: OTHER.id, serverPort: 15_038 },
async () => await runAndroidHostAdb(['-s', OTHER.id, 'shell', 'id']),
),
]);

expect(hostCalls).toEqual([
{ serial: DEVICE.id, serverPort: 15_037 },
{ serial: OTHER.id, serverPort: 15_038 },
]);
});
Loading
Loading