mirror of
https://github.com/callstack/agent-device.git
synced 2026-09-14 20:06:34 +08:00
a707348b9b
* feat: add cloud webdriver artifacts * fix: clean up local session after provider release failure * fix: tag cloud webdriver provider requests * feat: connect hosted webdriver providers * docs: document hosted provider credentials * refactor: tighten cloud webdriver provider internals * refactor: consolidate cloud webdriver provider definitions * refactor: collapse hosted webdriver runtime wrapper * refactor: reduce cloud webdriver smell surface * docs: clarify hosted provider interfaces * fix: avoid regex slash trimming in webdriver urls * docs: rename hosted providers to device clouds * fix: align provider profile imports with remote modules * fix: skip local android recovery for provider devices * test: classify cloud provider integration flags * fix: close active cloud connection session * test: cover provider disconnect cli flow * fix: make cloud webdriver sessions launchable * fix: align cloud webdriver input gestures * fix: avoid keyboard input during cloud scroll * fix: constrain cloud webdriver scroll gestures * refactor: isolate cloud webdriver scroll frame * refactor: deduplicate cloud webdriver helpers * refactor: tighten cloud webdriver action types * fix: harden cloud webdriver release * fix: polish provider disconnect diagnostics * refactor: group connection profile helpers * fix: repair rebased internal paths * fix: satisfy cloud webdriver CI guards
250 lines
7.4 KiB
TypeScript
250 lines
7.4 KiB
TypeScript
import test, { type TestContext } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import fs from 'node:fs';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import http from 'node:http';
|
|
import {
|
|
closeLoopbackServer,
|
|
listenOnLoopback,
|
|
skipWhenLoopbackUnavailable,
|
|
} from '../../src/__tests__/test-utils/loopback.ts';
|
|
import { formatResultDebug, runBuiltCliJson } from './cli-json.ts';
|
|
|
|
async function readJsonBody(req: http.IncomingMessage): Promise<any> {
|
|
const chunks: Buffer[] = [];
|
|
for await (const chunk of req) {
|
|
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
}
|
|
const body = Buffer.concat(chunks).toString('utf8');
|
|
return body ? JSON.parse(body) : {};
|
|
}
|
|
|
|
test('built CLI provider flow closes active generated session before disconnect cleanup', async (t) => {
|
|
if (await skipWhenLoopbackUnavailable(t, 'provider disconnect smoke coverage')) {
|
|
return;
|
|
}
|
|
|
|
const fixture = await createProviderDaemonFixture(t);
|
|
const env = createProviderEnv();
|
|
const activeSession = await connectBrowserStackProvider(fixture, env);
|
|
await openProviderApp(fixture, env);
|
|
await disconnectProviderSession(fixture, env, activeSession);
|
|
assertProviderDisconnectRpc(fixture.rpcRequests, activeSession);
|
|
await assertNoActiveConnection(fixture, env);
|
|
});
|
|
|
|
type ProviderDaemonFixture = {
|
|
root: string;
|
|
stateDir: string;
|
|
daemonBaseUrl: string;
|
|
rpcRequests: any[];
|
|
};
|
|
|
|
async function createProviderDaemonFixture(t: TestContext): Promise<ProviderDaemonFixture> {
|
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-device-provider-disconnect-smoke-'));
|
|
const stateDir = path.join(root, 'state');
|
|
const rpcRequests: any[] = [];
|
|
let hostPort = 0;
|
|
const hostServer = http.createServer(async (req, res) => {
|
|
if (req.method === 'GET' && req.url === '/agent-device/health') {
|
|
res.writeHead(200, {
|
|
'content-type': 'application/json',
|
|
connection: 'close',
|
|
});
|
|
res.end(
|
|
JSON.stringify({
|
|
ok: true,
|
|
service: 'agent-device-daemon',
|
|
version: '0.0.0-test',
|
|
rpcProtocolVersion: 1,
|
|
}),
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (req.method === 'POST' && req.url === '/agent-device/rpc') {
|
|
const body = await readJsonBody(req);
|
|
rpcRequests.push(body);
|
|
const data = responseDataForRpc(body);
|
|
res.writeHead(200, {
|
|
'content-type': 'application/json',
|
|
connection: 'close',
|
|
});
|
|
res.end(
|
|
JSON.stringify({
|
|
jsonrpc: '2.0',
|
|
id: body?.id ?? 'provider-disconnect-smoke',
|
|
result: {
|
|
ok: true,
|
|
data,
|
|
},
|
|
}),
|
|
);
|
|
return;
|
|
}
|
|
|
|
res.writeHead(404);
|
|
res.end();
|
|
});
|
|
hostPort = await listenOnLoopback(hostServer);
|
|
t.after(async () => {
|
|
await closeLoopbackServer(hostServer);
|
|
fs.rmSync(root, { recursive: true, force: true });
|
|
});
|
|
return {
|
|
root,
|
|
stateDir,
|
|
daemonBaseUrl: `http://127.0.0.1:${hostPort}/agent-device`,
|
|
rpcRequests,
|
|
};
|
|
}
|
|
|
|
function createProviderEnv(): NodeJS.ProcessEnv {
|
|
return {
|
|
...process.env,
|
|
BROWSERSTACK_USERNAME: 'browser-user',
|
|
BROWSERSTACK_ACCESS_KEY: 'browser-key',
|
|
};
|
|
}
|
|
|
|
async function connectBrowserStackProvider(
|
|
fixture: ProviderDaemonFixture,
|
|
env: NodeJS.ProcessEnv,
|
|
): Promise<string> {
|
|
const connectArgs = [
|
|
'connect',
|
|
'browserstack',
|
|
'--platform',
|
|
'android',
|
|
'--device',
|
|
'Google Pixel 8',
|
|
'--provider-os-version',
|
|
'14.0',
|
|
'--provider-app',
|
|
'bs://app-id',
|
|
'--daemon-base-url',
|
|
fixture.daemonBaseUrl,
|
|
'--state-dir',
|
|
fixture.stateDir,
|
|
'--json',
|
|
];
|
|
const connectResult = await runBuiltCliJson(connectArgs, env);
|
|
|
|
assert.equal(connectResult.status, 0, formatResultDebug('connect', connectArgs, connectResult));
|
|
assert.equal(
|
|
connectResult.json?.success,
|
|
true,
|
|
formatResultDebug('connect', connectArgs, connectResult),
|
|
);
|
|
const activeSession = connectResult.json?.data?.session;
|
|
assert.match(activeSession, /^adc-/);
|
|
return activeSession;
|
|
}
|
|
|
|
async function openProviderApp(
|
|
fixture: ProviderDaemonFixture,
|
|
env: NodeJS.ProcessEnv,
|
|
): Promise<void> {
|
|
const openArgs = ['open', 'Demo', '--state-dir', fixture.stateDir, '--json'];
|
|
const openResult = await runBuiltCliJson(openArgs, env);
|
|
assert.equal(openResult.status, 0, formatResultDebug('open', openArgs, openResult));
|
|
assert.equal(openResult.json?.success, true, formatResultDebug('open', openArgs, openResult));
|
|
}
|
|
|
|
async function disconnectProviderSession(
|
|
fixture: ProviderDaemonFixture,
|
|
env: NodeJS.ProcessEnv,
|
|
activeSession: string,
|
|
): Promise<void> {
|
|
const disconnectArgs = ['disconnect', '--state-dir', fixture.stateDir, '--json'];
|
|
const disconnectResult = await runBuiltCliJson(disconnectArgs, env);
|
|
assert.equal(
|
|
disconnectResult.status,
|
|
0,
|
|
formatResultDebug('disconnect', disconnectArgs, disconnectResult),
|
|
);
|
|
assert.equal(
|
|
disconnectResult.json?.success,
|
|
true,
|
|
formatResultDebug('disconnect', disconnectArgs, disconnectResult),
|
|
);
|
|
assert.equal(disconnectResult.json?.data?.session, activeSession);
|
|
assert.equal(disconnectResult.json?.data?.released, true);
|
|
}
|
|
|
|
function assertProviderDisconnectRpc(rpcRequests: any[], activeSession: string): void {
|
|
const closeRpc = rpcRequests.find(
|
|
(request) => request.method === 'agent_device.command' && request.params?.command === 'close',
|
|
);
|
|
assert.equal(closeRpc?.params?.session, activeSession);
|
|
assert.notEqual(closeRpc?.params?.session, 'default');
|
|
|
|
const releaseRpc = rpcRequests.find((request) => request.method === 'agent_device.lease.release');
|
|
assert.equal(releaseRpc?.params?.session, activeSession);
|
|
assert.equal(releaseRpc?.params?.leaseId, 'lease-bs-1');
|
|
assert.equal(releaseRpc?.params?.leaseProvider, 'browserstack');
|
|
}
|
|
|
|
async function assertNoActiveConnection(
|
|
fixture: ProviderDaemonFixture,
|
|
env: NodeJS.ProcessEnv,
|
|
): Promise<void> {
|
|
const statusArgs = ['connection', 'status', '--state-dir', fixture.stateDir, '--json'];
|
|
const statusResult = await runBuiltCliJson(statusArgs, env);
|
|
assert.equal(statusResult.status, 0, formatResultDebug('status', statusArgs, statusResult));
|
|
assert.equal(
|
|
statusResult.json?.success,
|
|
true,
|
|
formatResultDebug('status', statusArgs, statusResult),
|
|
);
|
|
assert.equal(statusResult.json?.data?.connected, false);
|
|
}
|
|
|
|
function responseDataForRpc(body: any): Record<string, unknown> {
|
|
const params = body?.params ?? {};
|
|
if (body?.method === 'agent_device.lease.allocate') {
|
|
return {
|
|
lease: {
|
|
leaseId: 'lease-bs-1',
|
|
tenantId: params.tenantId,
|
|
runId: params.runId,
|
|
backend: params.backend,
|
|
leaseProvider: params.leaseProvider,
|
|
clientId: params.clientId,
|
|
deviceKey: params.deviceKey,
|
|
},
|
|
};
|
|
}
|
|
if (body?.method === 'agent_device.lease.release') {
|
|
return {
|
|
released: true,
|
|
provider: {
|
|
provider: 'browserstack',
|
|
providerSessionId: 'bs-session-1',
|
|
},
|
|
};
|
|
}
|
|
if (params.command === 'open') {
|
|
return {
|
|
session: params.session,
|
|
appName: 'Demo',
|
|
appBundleId: 'com.example.demo',
|
|
platform: 'android',
|
|
target: 'mobile',
|
|
device: 'Pixel',
|
|
id: 'browserstack-pixel',
|
|
serial: 'browserstack-pixel',
|
|
};
|
|
}
|
|
if (params.command === 'close') {
|
|
return {
|
|
provider: {
|
|
provider: 'browserstack',
|
|
providerSessionId: 'bs-session-1',
|
|
},
|
|
};
|
|
}
|
|
return {};
|
|
}
|