Skip to content

Commit 54df6f3

Browse files
authored
feat(runtime): route managed leases through contained transports (#2285)
1 parent fd221b1 commit 54df6f3

8 files changed

Lines changed: 807 additions & 33 deletions

packages/platform-android/src/__tests__/adb-executor.test.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,37 @@ test('createLocalAndroidAdbProvider exposes local pull and install capabilities'
231231
]);
232232
});
233233

234+
test('createLocalAndroidAdbProvider carries a private server port through every adb capability', async () => {
235+
mockRunCmd.mockClear();
236+
mockRunCmdBackground.mockClear();
237+
const provider = createLocalAndroidAdbProvider(
238+
{
239+
platform: 'android',
240+
id: 'emulator-5554',
241+
name: 'Pixel Emulator',
242+
kind: 'emulator',
243+
booted: true,
244+
},
245+
{ serverPort: 15_037 },
246+
);
247+
248+
await provider.exec(['shell', 'echo', 'ok']);
249+
provider.spawn?.(['logcat']);
250+
await provider.reverse?.ensure({ local: 'tcp:8081', remote: 'tcp:8081' });
251+
await provider.pull?.('/sdcard/video.mp4', '/tmp/video.mp4');
252+
await provider.install?.('/tmp/app.apk');
253+
254+
assert.equal(readServerPort(mockRunCmdBackground.mock.calls[0]?.[2]), 15_037);
255+
assert.equal(mockRunCmd.mock.calls.length, 4);
256+
for (const call of mockRunCmd.mock.calls) assert.equal(readServerPort(call[2]), 15_037);
257+
});
258+
259+
function readServerPort(options: unknown): number | undefined {
260+
if (options === null || typeof options !== 'object') return undefined;
261+
const value = (options as { serverPort?: unknown }).serverPort;
262+
return typeof value === 'number' ? value : undefined;
263+
}
264+
234265
test('createAndroidPortReverseManager makes duplicate setup idempotent and cleans owner mappings', async () => {
235266
const calls: string[][] = [];
236267
const manager = createAndroidPortReverseManager(async (args) => {

packages/platform-android/src/adb-provider-scope.test.ts

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@ import { expect, test } from 'vitest';
22
import type { DeviceInfo } from '@agent-device/kernel/device';
33
import { bindAndroidAdbHostStub } from './adb-host.fixtures.ts';
44
import {
5+
createLocalAndroidAdbProvider,
56
resolveAndroidAdbExecutor,
67
resolveAndroidAdbProvider,
78
resolveAndroidTextInjector,
89
resolveScopedAndroidAdbBackgroundTransport,
910
withAndroidAdbProvider,
1011
} from './adb-provider-scope.ts';
12+
import { runAndroidHostAdb } from './adb-host.ts';
1113
import type { AndroidAdbExecutorResult, AndroidAdbProvider } from './adb-transport.ts';
1214

1315
const DEVICE: DeviceInfo = {
@@ -92,3 +94,192 @@ test('the installed override routes only normalized device-scoped adb calls to t
9294
});
9395
expect(providerCalls).toEqual([['shell', 'ls']]);
9496
});
97+
98+
test('a managed port scope routes host adb and matching serial calls to its private server', async () => {
99+
const hostCalls: Array<{ args: string[]; serverPort?: number }> = [];
100+
bindAndroidAdbHostStub({
101+
execHostAdb: async (args, options) => {
102+
hostCalls.push({ args, serverPort: options?.serverPort });
103+
return ok();
104+
},
105+
});
106+
107+
await withAndroidAdbProvider(
108+
{ exec: async () => ok() },
109+
{ serial: DEVICE.id, serverPort: 15_037 },
110+
async () => {
111+
await runAndroidHostAdb(['devices']);
112+
await runAndroidHostAdb(['-s', DEVICE.id, 'shell', 'getprop']);
113+
await runAndroidHostAdb(['-s', OTHER.id, 'shell', 'getprop']);
114+
},
115+
);
116+
await runAndroidHostAdb(['devices']);
117+
118+
expect(hostCalls).toEqual([
119+
{ args: ['devices'], serverPort: 15_037 },
120+
{ args: ['-s', DEVICE.id, 'shell', 'getprop'], serverPort: 15_037 },
121+
{ args: ['-s', OTHER.id, 'shell', 'getprop'] },
122+
{ args: ['devices'] },
123+
]);
124+
});
125+
126+
test('a managed port scope classifies absolute adb commands and preserves the default boundary', async () => {
127+
const providerCalls: string[][] = [];
128+
const hostCalls: string[][] = [];
129+
let captured:
130+
| ((cmd: string, args: string[], options: object) => Promise<unknown> | undefined)
131+
| undefined;
132+
bindAndroidAdbHostStub({
133+
execHostAdb: async (args) => {
134+
hostCalls.push(args);
135+
return ok();
136+
},
137+
withAdbCommandExecutorOverride: async (override, fn) => {
138+
captured = override;
139+
return await fn();
140+
},
141+
});
142+
143+
await withAndroidAdbProvider(
144+
{
145+
exec: async (args) => {
146+
providerCalls.push(args);
147+
return ok();
148+
},
149+
},
150+
{ serial: DEVICE.id, serverPort: 15_037 },
151+
async () => {
152+
const global = captured?.('/opt/android-sdk/platform-tools/adb', ['devices', '-l'], {});
153+
const matching = captured?.(
154+
'/opt/android-sdk/platform-tools/adb',
155+
['-s', DEVICE.id, 'shell', 'ls'],
156+
{},
157+
);
158+
expect(captured?.('adb', ['-s', OTHER.id, 'shell', 'ls'], {})).toBeUndefined();
159+
expect(captured?.('emulator', ['-list-avds'], {})).toBeUndefined();
160+
expect(global).toBeDefined();
161+
expect(matching).toBeDefined();
162+
await global;
163+
await matching;
164+
},
165+
);
166+
167+
expect(hostCalls).toEqual([['devices', '-l']]);
168+
expect(providerCalls).toEqual([['shell', 'ls']]);
169+
});
170+
171+
test('a managed port scope keeps shell -s arguments on the private transport', async () => {
172+
const hostCalls: Array<{ args: string[]; serverPort?: number }> = [];
173+
let captured:
174+
| ((cmd: string, args: string[], options: object) => Promise<unknown> | undefined)
175+
| undefined;
176+
bindAndroidAdbHostStub({
177+
execHostAdb: async (args, options) => {
178+
hostCalls.push({ args, serverPort: options?.serverPort });
179+
return ok();
180+
},
181+
withAdbCommandExecutorOverride: async (override, fn) => {
182+
captured = override;
183+
return await fn();
184+
},
185+
});
186+
187+
await withAndroidAdbProvider(
188+
{ exec: async () => ok() },
189+
{ serial: DEVICE.id, serverPort: 15_037 },
190+
async () => {
191+
await runAndroidHostAdb(['shell', 'echo', '-s', OTHER.id]);
192+
const shellCommand = captured?.('adb', ['shell', 'echo', '-s', OTHER.id], {});
193+
expect(shellCommand).toBeDefined();
194+
await shellCommand;
195+
},
196+
);
197+
198+
expect(hostCalls).toEqual([
199+
{ args: ['shell', 'echo', '-s', OTHER.id], serverPort: 15_037 },
200+
{ args: ['shell', 'echo', '-s', OTHER.id], serverPort: 15_037 },
201+
]);
202+
});
203+
204+
test('a managed port scope restores the default transport after task failure', async () => {
205+
const ports: Array<number | undefined> = [];
206+
bindAndroidAdbHostStub({
207+
execHostAdb: async (_args, options) => {
208+
ports.push(options?.serverPort);
209+
return ok();
210+
},
211+
});
212+
213+
await expect(
214+
withAndroidAdbProvider(
215+
{ exec: async () => ok() },
216+
{ serial: DEVICE.id, serverPort: 15_037 },
217+
async () => {
218+
await runAndroidHostAdb(['devices']);
219+
throw new Error('stop managed request');
220+
},
221+
),
222+
).rejects.toThrow('stop managed request');
223+
await runAndroidHostAdb(['devices']);
224+
225+
expect(ports).toEqual([15_037, undefined]);
226+
});
227+
228+
test('a managed port scope carries its server through the local background transport', async () => {
229+
const spawnCalls: Array<{ serial: string; args: string[]; serverPort?: number }> = [];
230+
bindAndroidAdbHostStub({
231+
spawnSerialAdb: (serial, args, options) => {
232+
spawnCalls.push({ serial, args, serverPort: options?.serverPort });
233+
return undefined as never;
234+
},
235+
});
236+
const deviceProvider = createLocalAndroidAdbProvider(DEVICE, { serverPort: 15_037 });
237+
238+
await withAndroidAdbProvider(
239+
deviceProvider,
240+
{ serial: DEVICE.id, serverPort: 15_037 },
241+
async () => {
242+
const transport = resolveScopedAndroidAdbBackgroundTransport(DEVICE);
243+
expect(transport.mode).toBe('transport-composed');
244+
if (transport.mode === 'transport-composed') {
245+
transport.spawn?.(['logcat', '-v', 'threadtime']);
246+
}
247+
},
248+
);
249+
250+
expect(spawnCalls).toEqual([
251+
{ serial: DEVICE.id, args: ['logcat', '-v', 'threadtime'], serverPort: 15_037 },
252+
]);
253+
});
254+
255+
test('managed port scopes remain isolated across concurrent requests', async () => {
256+
const hostCalls: Array<{ serial: string; serverPort?: number }> = [];
257+
bindAndroidAdbHostStub({
258+
execHostAdb: async (args, options) => {
259+
hostCalls.push({
260+
serial: args[args.indexOf('-s') + 1] ?? 'global',
261+
serverPort: options?.serverPort,
262+
});
263+
await Promise.resolve();
264+
return ok();
265+
},
266+
});
267+
268+
await Promise.all([
269+
withAndroidAdbProvider(
270+
{ exec: async () => ok() },
271+
{ serial: DEVICE.id, serverPort: 15_037 },
272+
async () => await runAndroidHostAdb(['-s', DEVICE.id, 'shell', 'id']),
273+
),
274+
withAndroidAdbProvider(
275+
{ exec: async () => ok() },
276+
{ serial: OTHER.id, serverPort: 15_038 },
277+
async () => await runAndroidHostAdb(['-s', OTHER.id, 'shell', 'id']),
278+
),
279+
]);
280+
281+
expect(hostCalls).toEqual([
282+
{ serial: DEVICE.id, serverPort: 15_037 },
283+
{ serial: OTHER.id, serverPort: 15_038 },
284+
]);
285+
});

0 commit comments

Comments
 (0)