Files
callstack__agent-device/src/__tests__/cli-client-commands.test.ts
Michał Pierzchała 8e1f8a9f8f perf: lazy load daemon handlers and report bundle size (#608)
* perf: lazy load daemon handlers

* perf: thin command metadata paths

* refactor: isolate platform inventory loading

* refactor: trim lazy loading cleanup

* test: guard daemon routing metadata drift

* ci: report startup timing with size
2026-05-30 16:49:21 +02:00

1133 lines
32 KiB
TypeScript

import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { test } from 'vitest';
import assert from 'node:assert/strict';
import { PNG } from '../utils/png.ts';
import { tryRunClientBackedCommand } from '../cli/commands/router.ts';
import type {
AgentDeviceClient,
AppInstallFromSourceOptions,
AppOpenOptions,
MetroPrepareOptions,
MetroReloadOptions,
} from '../client.ts';
import type { SettingsUpdateOptions } from '../client-types.ts';
import { AppError } from '../utils/errors.ts';
import { resolveCliOptions } from '../utils/cli-options.ts';
test('install-from-source forwards URL and repeated headers to client.apps.installFromSource', async () => {
let observed: AppInstallFromSourceOptions | undefined;
const client = createStubClient({
installFromSource: async (options) => {
observed = options;
return {
launchTarget: 'com.example.demo',
packageName: 'com.example.demo',
identifiers: { appId: 'com.example.demo', package: 'com.example.demo' },
};
},
});
const handled = await tryRunClientBackedCommand({
command: 'install-from-source',
positionals: ['https://example.com/app.apk'],
flags: {
json: false,
help: false,
version: false,
platform: 'android',
header: ['authorization: Bearer token', 'x-build-id: 42'],
retainPaths: true,
retentionMs: 60_000,
},
client,
});
assert.equal(handled, true);
assert.equal(observed?.platform, 'android');
assert.equal(observed?.retainPaths, true);
assert.equal(observed?.retentionMs, 60_000);
assert.deepEqual(observed?.source, {
kind: 'url',
url: 'https://example.com/app.apk',
headers: {
authorization: 'Bearer token',
'x-build-id': '42',
},
});
});
test('install-from-source forwards GitHub Actions artifact flag to client.apps.installFromSource', async () => {
let observed: AppInstallFromSourceOptions | undefined;
const client = createStubClient({
installFromSource: async (options) => {
observed = options;
return {
launchTarget: 'com.example.demo',
packageName: 'com.example.demo',
identifiers: { appId: 'com.example.demo', package: 'com.example.demo' },
};
},
});
const handled = await tryRunClientBackedCommand({
command: 'install-from-source',
positionals: [],
flags: {
json: false,
help: false,
version: false,
platform: 'android',
githubActionsArtifact: 'thymikee/RNCLI83:6635342232',
},
client,
});
assert.equal(handled, true);
assert.equal(observed?.platform, 'android');
assert.deepEqual(observed?.source, {
kind: 'github-actions-artifact',
owner: 'thymikee',
repo: 'RNCLI83',
artifactId: 6635342232,
});
});
test('install-from-source preserves colons in GitHub Actions artifact names', async () => {
let observed: AppInstallFromSourceOptions | undefined;
const client = createStubClient({
installFromSource: async (options) => {
observed = options;
return {
launchTarget: 'com.example.demo',
packageName: 'com.example.demo',
identifiers: { appId: 'com.example.demo', package: 'com.example.demo' },
};
},
});
await tryRunClientBackedCommand({
command: 'install-from-source',
positionals: [],
flags: {
json: false,
help: false,
version: false,
githubActionsArtifact: 'thymikee/RNCLI83:app:debug:pr-19',
},
client,
});
assert.deepEqual(observed?.source, {
kind: 'github-actions-artifact',
owner: 'thymikee',
repo: 'RNCLI83',
artifactName: 'app:debug:pr-19',
});
});
test('install-from-source forwards configured GitHub Actions artifact name to client.apps.installFromSource', async () => {
let observed: AppInstallFromSourceOptions | undefined;
const client = createStubClient({
installFromSource: async (options) => {
observed = options;
return {
launchTarget: 'com.example.demo',
packageName: 'com.example.demo',
identifiers: { appId: 'com.example.demo', package: 'com.example.demo' },
};
},
});
const handled = await tryRunClientBackedCommand({
command: 'install-from-source',
positionals: [],
flags: {
json: false,
help: false,
version: false,
platform: 'android',
installSource: {
kind: 'github-actions-artifact',
owner: 'thymikee',
repo: 'RNCLI83',
artifactName: 'rn-android-emulator-debug-pr-19',
},
},
client,
});
assert.equal(handled, true);
assert.deepEqual(observed?.source, {
kind: 'github-actions-artifact',
owner: 'thymikee',
repo: 'RNCLI83',
artifactName: 'rn-android-emulator-debug-pr-19',
});
});
test('install-from-source rejects malformed header syntax', async () => {
const client = createStubClient({
installFromSource: async () => {
throw new Error('unexpected call');
},
});
await assert.rejects(
() =>
tryRunClientBackedCommand({
command: 'install-from-source',
positionals: ['https://example.com/app.apk'],
flags: {
json: false,
help: false,
version: false,
header: ['authorization'],
},
client,
}),
(error) =>
error instanceof AppError &&
error.code === 'INVALID_ARGS' &&
error.message.includes('Expected "name:value"'),
);
});
test('install-from-source rejects headers with GitHub Actions artifact sources', async () => {
const client = createStubClient({
installFromSource: async () => {
throw new Error('unexpected call');
},
});
await assert.rejects(
() =>
tryRunClientBackedCommand({
command: 'install-from-source',
positionals: [],
flags: {
json: false,
help: false,
version: false,
githubActionsArtifact: 'thymikee/RNCLI83:6635342232',
header: ['authorization: Bearer token'],
},
client,
}),
(error) =>
error instanceof AppError &&
error.code === 'INVALID_ARGS' &&
error.message.includes('--header is only supported for URL sources'),
);
});
test('metro prepare forwards normalized options to client.metro.prepare', async () => {
let observed: MetroPrepareOptions | undefined;
const client = createStubClient({
installFromSource: async () => {
throw new Error('unexpected install call');
},
prepareMetro: async (options) => {
observed = options;
return {
projectRoot: '/tmp/project',
kind: 'react-native',
dependenciesInstalled: false,
packageManager: null,
started: false,
reused: true,
pid: 0,
logPath: '/tmp/project/.agent-device/metro.log',
statusUrl: 'http://127.0.0.1:8081/status',
runtimeFilePath: null,
iosRuntime: {
platform: 'ios',
bundleUrl: 'https://sandbox.example.test/index.bundle?platform=ios',
},
androidRuntime: {
platform: 'android',
bundleUrl: 'https://sandbox.example.test/index.bundle?platform=android',
},
bridge: null,
};
},
});
const stdout = await captureStdout(async () => {
const handled = await tryRunClientBackedCommand({
command: 'metro',
positionals: ['prepare'],
flags: {
json: false,
help: false,
version: false,
metroProjectRoot: './apps/demo',
metroPublicBaseUrl: 'https://sandbox.example.test',
metroProxyBaseUrl: 'https://proxy.example.test',
metroBearerToken: 'secret',
tenant: 'tenant-1',
runId: 'run-1',
leaseId: 'lease-1',
metroPreparePort: 9090,
metroKind: 'expo',
metroRuntimeFile: './.agent-device/metro-runtime.json',
metroNoReuseExisting: true,
metroNoInstallDeps: true,
},
client,
});
assert.equal(handled, true);
});
const payload = JSON.parse(stdout);
assert.deepEqual(observed, {
projectRoot: './apps/demo',
publicBaseUrl: 'https://sandbox.example.test',
proxyBaseUrl: 'https://proxy.example.test',
bearerToken: 'secret',
bridgeScope: {
tenantId: 'tenant-1',
runId: 'run-1',
leaseId: 'lease-1',
},
port: 9090,
kind: 'expo',
runtimeFilePath: './.agent-device/metro-runtime.json',
reuseExisting: false,
installDependenciesIfNeeded: false,
listenHost: undefined,
statusHost: undefined,
startupTimeoutMs: undefined,
probeTimeoutMs: undefined,
});
assert.equal(payload.kind, 'react-native');
assert.equal(payload.runtimeFilePath, null);
});
test('metro prepare rejects when no public or proxy base URL is provided', async () => {
const client = createStubClient({
installFromSource: async () => {
throw new Error('unexpected install call');
},
prepareMetro: async () => {
throw new Error('unexpected metro prepare call');
},
});
await assert.rejects(
() =>
tryRunClientBackedCommand({
command: 'metro',
positionals: ['prepare'],
flags: {
json: false,
help: false,
version: false,
},
client,
}),
(error) =>
error instanceof AppError &&
error.code === 'INVALID_ARGS' &&
error.message.includes('--public-base-url <url> or --proxy-base-url <url>'),
);
});
test('metro reload forwards host, port, bundle URL, and timeout to client.metro.reload', async () => {
let observed: MetroReloadOptions | undefined;
const client = createStubClient({
installFromSource: async () => {
throw new Error('unexpected install call');
},
reloadMetro: async (options) => {
observed = options;
return {
reloaded: true,
reloadUrl: 'http://127.0.0.1:9090/reload',
status: 200,
body: 'OK',
};
},
});
const stdout = await captureStdout(async () => {
const handled = await tryRunClientBackedCommand({
command: 'metro',
positionals: ['reload'],
flags: {
json: false,
help: false,
version: false,
metroHost: '127.0.0.1',
metroPort: 9090,
bundleUrl: 'http://127.0.0.1:9090/index.bundle?platform=ios',
metroProbeTimeoutMs: 1500,
},
client,
});
assert.equal(handled, true);
});
assert.deepEqual(observed, {
metroHost: '127.0.0.1',
metroPort: 9090,
bundleUrl: 'http://127.0.0.1:9090/index.bundle?platform=ios',
timeoutMs: 1500,
});
assert.equal(stdout, 'Reloaded React Native apps via http://127.0.0.1:9090/reload\n');
});
test('screenshot forwards --overlay-refs to the client capture API', async () => {
let observed:
| {
path?: string;
overlayRefs?: boolean;
maxSize?: number;
stabilize?: boolean;
}
| undefined;
const client = createStubClient({
installFromSource: async () => {
throw new Error('unexpected install call');
},
screenshot: async (options) => {
observed = options;
return {
path: '/tmp/screenshot.png',
identifiers: { session: 'default' },
};
},
});
const handled = await tryRunClientBackedCommand({
command: 'screenshot',
positionals: ['/tmp/screenshot.png'],
flags: {
json: false,
help: false,
version: false,
overlayRefs: true,
screenshotMaxSize: 1024,
screenshotNoStabilize: true,
},
client,
});
assert.equal(handled, true);
assert.deepEqual(observed, {
path: '/tmp/screenshot.png',
overlayRefs: true,
maxSize: 1024,
stabilize: false,
});
});
test('diff screenshot forwards --surface to live client screenshot capture', async () => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-device-cli-diff-surface-'));
const baseline = path.join(dir, 'baseline.png');
const out = path.join(dir, 'diff.png');
fs.writeFileSync(baseline, solidPngBuffer(4, 4, { r: 0, g: 0, b: 0 }));
let observed: Parameters<AgentDeviceClient['capture']['screenshot']>[0] | undefined;
try {
const client = createStubClient({
installFromSource: async () => {
throw new Error('unexpected install call');
},
screenshot: async (options) => {
if (!options?.path) {
throw new Error('expected runtime to request a live screenshot path');
}
observed = options;
fs.writeFileSync(options.path, solidPngBuffer(4, 4, { r: 255, g: 255, b: 255 }));
return {
path: options.path,
identifiers: { session: options.session ?? 'default' },
};
},
});
await captureStdout(async () => {
const handled = await tryRunClientBackedCommand({
command: 'diff',
positionals: ['screenshot'],
flags: {
json: true,
help: false,
version: false,
baseline,
out,
platform: 'macos',
session: 'surface-session',
surface: 'menubar',
threshold: '0',
},
client,
});
assert.equal(handled, true);
});
assert.equal(observed?.session, 'surface-session');
assert.equal(observed?.surface, 'menubar');
assert.equal(fs.existsSync(out), true);
} finally {
fs.rmSync(dir, { recursive: true, force: true });
}
});
test('open forwards macOS surface to the client apps API', async () => {
let observed: AppOpenOptions | undefined;
const client = createStubClient({
installFromSource: async () => {
throw new Error('unexpected install call');
},
open: async (options) => {
observed = options;
return {
session: 'default',
appName: 'MenuBarApp',
appBundleId: 'com.example.menubarapp',
identifiers: { session: 'default', appBundleId: 'com.example.menubarapp' },
};
},
});
const handled = await tryRunClientBackedCommand({
command: 'open',
positionals: ['MenuBarApp'],
flags: {
json: false,
help: false,
version: false,
platform: 'macos',
surface: 'menubar',
},
client,
});
assert.equal(handled, true);
assert.equal(observed?.platform, 'macos');
assert.equal(observed?.surface, 'menubar');
});
test('open forwards launch console option to the client apps API', async () => {
let observed: AppOpenOptions | undefined;
const client = createStubClient({
installFromSource: async () => {
throw new Error('unexpected install call');
},
open: async (options) => {
observed = options;
return {
session: 'default',
appName: 'Agent Device Tester',
appBundleId: 'com.example.agentdevicetester',
identifiers: { session: 'default', appBundleId: 'com.example.agentdevicetester' },
};
},
});
const handled = await tryRunClientBackedCommand({
command: 'open',
positionals: ['Agent Device Tester'],
flags: {
json: false,
help: false,
version: false,
platform: 'ios',
launchConsole: '/tmp/launch-console.log',
launchArgs: ['-FeatureFlag', 'YES'],
},
client,
});
assert.equal(handled, true);
assert.equal(observed?.platform, 'ios');
assert.equal(observed?.launchConsole, '/tmp/launch-console.log');
assert.deepEqual(observed?.launchArgs, ['-FeatureFlag', 'YES']);
});
test('apps command defaults to user-installed and prints discovery hint', async () => {
let observedAppsFilter: string | undefined;
const client = createStubClient({
installFromSource: async () => {
throw new Error('unexpected install call');
},
listApps: async (options) => {
observedAppsFilter = options?.appsFilter;
return ['Demo (com.example.demo)'];
},
});
const output = await captureOutput(async () => {
const handled = await tryRunClientBackedCommand({
command: 'apps',
positionals: [],
flags: {
json: false,
help: false,
version: false,
platform: 'android',
appsFilter: 'user-installed',
},
client,
});
assert.equal(handled, true);
});
assert.equal(observedAppsFilter, 'user-installed');
assert.equal(output.stdout, 'Demo (com.example.demo)\n');
assert.match(output.stderr, /user-installed apps/i);
assert.match(output.stderr, /--all/);
});
test('screenshot reports annotated ref count in non-json mode', async () => {
const client = createStubClient({
installFromSource: async () => {
throw new Error('unexpected install call');
},
screenshot: async () => ({
path: '/tmp/screenshot.png',
overlayRefs: [
{
ref: 'e1',
rect: { x: 0, y: 0, width: 10, height: 10 },
overlayRect: { x: 0, y: 0, width: 20, height: 20 },
center: { x: 10, y: 10 },
},
{
ref: 'e2',
rect: { x: 20, y: 0, width: 10, height: 10 },
overlayRect: { x: 40, y: 0, width: 20, height: 20 },
center: { x: 50, y: 10 },
},
],
identifiers: { session: 'default' },
}),
});
const stdout = await captureStdout(async () => {
const handled = await tryRunClientBackedCommand({
command: 'screenshot',
positionals: ['/tmp/screenshot.png'],
flags: {
json: false,
help: false,
version: false,
overlayRefs: true,
},
client,
});
assert.equal(handled, true);
});
assert.equal(stdout, 'Annotated 2 refs onto /tmp/screenshot.png\n');
});
test('wait keeps CLI bare text behavior through the typed client command API', async () => {
let observed: Parameters<AgentDeviceClient['command']['wait']>[0] | undefined;
const client = createStubClient({
installFromSource: async () => {
throw new Error('unexpected install call');
},
});
client.command.wait = async (options) => {
observed = options;
return { text: 'Continue', waitedMs: 12 };
};
const handled = await tryRunClientBackedCommand({
command: 'wait',
positionals: ['Continue', '1500'],
flags: {
json: false,
help: false,
version: false,
},
client,
});
assert.equal(handled, true);
assert.equal(observed?.text, 'Continue');
assert.equal(observed?.timeoutMs, 1500);
});
test('clipboard read keeps human text output through the typed client command API', async () => {
const client = createStubClient({
installFromSource: async () => {
throw new Error('unexpected install call');
},
});
client.command.clipboard = async () => ({ action: 'read', text: 'hello' });
const stdout = await captureStdout(async () => {
const handled = await tryRunClientBackedCommand({
command: 'clipboard',
positionals: ['read'],
flags: {
json: false,
help: false,
version: false,
},
client,
});
assert.equal(handled, true);
});
assert.equal(stdout, 'hello\n');
});
test('keyboard status prints Android input ownership in human output', async () => {
const client = createStubClient({
installFromSource: async () => {
throw new Error('unexpected install call');
},
});
client.command.keyboard = async () => ({
platform: 'android',
action: 'status',
visible: true,
type: 'text',
inputMethodPackage: 'com.google.android.inputmethod.latin',
focusedPackage: 'com.google.android.inputmethod.latin',
focusedResourceId: 'com.google.android.inputmethod.latin:id/handwriting',
inputOwner: 'ime',
});
const stdout = await captureStdout(async () => {
const handled = await tryRunClientBackedCommand({
command: 'keyboard',
positionals: ['status'],
flags: {
json: false,
help: false,
version: false,
},
client,
});
assert.equal(handled, true);
});
assert.match(stdout, /Keyboard visible: yes/);
assert.match(stdout, /Input type: text/);
assert.match(stdout, /Input owner: ime/);
assert.match(stdout, /Input method: com\.google\.android\.inputmethod\.latin/);
assert.match(
stdout,
/Focused resource: com\.google\.android\.inputmethod\.latin:id\/handwriting/,
);
assert.match(stdout, /Next action: Focused input appears to be owned by the keyboard\/IME/);
});
test('metro prepare wraps output in the standard success envelope for --json', async () => {
const client = createStubClient({
installFromSource: async () => {
throw new Error('unexpected install call');
},
});
const stdout = await captureStdout(async () => {
const handled = await tryRunClientBackedCommand({
command: 'metro',
positionals: ['prepare'],
flags: {
json: true,
help: false,
version: false,
metroPublicBaseUrl: 'https://sandbox.example.test',
},
client,
});
assert.equal(handled, true);
});
const payload = JSON.parse(stdout);
assert.equal(payload.success, true);
assert.equal(payload.data.kind, 'react-native');
assert.equal(payload.data.iosRuntime.platform, 'ios');
});
test('metro prepare with --remote-config loads profile defaults', async () => {
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-device-remote-metro-'));
const configDir = path.join(tmpRoot, 'config');
fs.mkdirSync(configDir, { recursive: true });
const remoteConfigPath = path.join(configDir, 'remote.json');
fs.writeFileSync(
remoteConfigPath,
JSON.stringify({
metroProjectRoot: './apps/demo',
metroProxyBaseUrl: 'https://proxy.example.test',
tenant: 'tenant-1',
runId: 'run-1',
leaseId: 'lease-1',
metroPreparePort: 9090,
}),
);
const parsed = resolveCliOptions(['metro', 'prepare', '--remote-config', remoteConfigPath], {
cwd: tmpRoot,
env: process.env,
});
let observedPrepare: MetroPrepareOptions | undefined;
const client = createStubClient({
installFromSource: async () => {
throw new Error('unexpected install call');
},
prepareMetro: async (options) => {
observedPrepare = options;
return {
projectRoot: '/tmp/project',
kind: 'react-native',
dependenciesInstalled: false,
packageManager: null,
started: false,
reused: true,
pid: 0,
logPath: '/tmp/project/.agent-device/metro.log',
statusUrl: 'http://127.0.0.1:8081/status',
runtimeFilePath: null,
iosRuntime: {
platform: 'ios',
bundleUrl: 'https://sandbox.example.test/index.bundle?platform=ios',
},
androidRuntime: {
platform: 'android',
bundleUrl: 'https://sandbox.example.test/index.bundle?platform=android',
},
bridge: null,
};
},
});
const stdout = await captureStdout(async () => {
const handled = await tryRunClientBackedCommand({
command: 'metro',
positionals: ['prepare'],
flags: parsed.flags,
client,
});
assert.equal(handled, true);
});
const payload = JSON.parse(stdout);
assert.deepEqual(observedPrepare, {
projectRoot: path.join(configDir, 'apps/demo'),
kind: undefined,
publicBaseUrl: undefined,
proxyBaseUrl: 'https://proxy.example.test',
bearerToken: undefined,
bridgeScope: {
tenantId: 'tenant-1',
runId: 'run-1',
leaseId: 'lease-1',
},
port: 9090,
listenHost: undefined,
statusHost: undefined,
startupTimeoutMs: undefined,
probeTimeoutMs: undefined,
reuseExisting: undefined,
installDependenciesIfNeeded: undefined,
runtimeFilePath: undefined,
});
assert.equal(payload.kind, 'react-native');
});
test('install prints command-owned success output in human mode', async () => {
const client = createStubClient({
installFromSource: async () => {
throw new Error('unexpected install-from-source call');
},
});
const stdout = await captureStdout(async () => {
const handled = await tryRunClientBackedCommand({
command: 'install',
positionals: ['Demo', '/tmp/Demo.app'],
flags: {
json: false,
help: false,
version: false,
},
client,
});
assert.equal(handled, true);
});
assert.match(stdout, /Installed: Demo/);
});
test('settings location set forwards coordinates to client settings update', async () => {
let observed: SettingsUpdateOptions | undefined;
const client = createStubClient({
installFromSource: async () => ({
launchTarget: 'com.example.demo',
packageName: 'com.example.demo',
identifiers: { appId: 'com.example.demo' },
}),
updateSettings: async (options) => {
observed = options;
return { identifiers: { session: 'default' } };
},
});
const handled = await tryRunClientBackedCommand({
command: 'settings',
positionals: ['location', 'set', '37.3349', '-122.009'],
flags: {
json: false,
help: false,
version: false,
platform: 'ios',
session: 'maps',
},
client,
});
assert.equal(handled, true);
assert.equal(observed?.platform, 'ios');
assert.equal(observed?.setting, 'location');
assert.equal(observed?.state, 'set');
assert.equal(observed?.latitude, 37.3349);
assert.equal(observed?.longitude, -122.009);
});
test('settings location set rejects invalid coordinates before client call', async () => {
const client = createStubClient({
installFromSource: async () => ({
launchTarget: 'com.example.demo',
packageName: 'com.example.demo',
identifiers: { appId: 'com.example.demo' },
}),
updateSettings: async () => {
throw new Error('unexpected settings update');
},
});
const cases: Array<[string[], RegExp]> = [
[['location', 'set', '91', '-122.009'], /latitude must be a number from -90 to 90/],
[['location', 'set', '37.3349', 'not-a-number'], /longitude must be a number from -180 to 180/],
];
for (const [positionals, message] of cases) {
await assert.rejects(
() =>
tryRunClientBackedCommand({
command: 'settings',
positionals,
flags: {
json: false,
help: false,
version: false,
platform: 'ios',
},
client,
}),
(error: unknown) => {
assert.equal(error instanceof AppError, true);
assert.equal((error as AppError).code, 'INVALID_ARGS');
assert.match((error as AppError).message, message);
return true;
},
);
}
});
async function captureStdout(run: () => Promise<void>): Promise<string> {
let stdout = '';
const originalWrite = process.stdout.write.bind(process.stdout);
(process.stdout as any).write = ((chunk: unknown) => {
stdout += String(chunk);
return true;
}) as typeof process.stdout.write;
try {
await run();
} finally {
process.stdout.write = originalWrite;
}
return stdout;
}
async function captureOutput(
run: () => Promise<void>,
): Promise<{ stdout: string; stderr: string }> {
let stdout = '';
let stderr = '';
const originalStdoutWrite = process.stdout.write.bind(process.stdout);
const originalStderrWrite = process.stderr.write.bind(process.stderr);
(process.stdout as any).write = ((chunk: unknown) => {
stdout += String(chunk);
return true;
}) as typeof process.stdout.write;
(process.stderr as any).write = ((chunk: unknown) => {
stderr += String(chunk);
return true;
}) as typeof process.stderr.write;
try {
await run();
} finally {
process.stdout.write = originalStdoutWrite;
process.stderr.write = originalStderrWrite;
}
return { stdout, stderr };
}
function createStubClient(params: {
installFromSource: AgentDeviceClient['apps']['installFromSource'];
listApps?: AgentDeviceClient['apps']['list'];
prepareMetro?: AgentDeviceClient['metro']['prepare'];
reloadMetro?: AgentDeviceClient['metro']['reload'];
open?: AgentDeviceClient['apps']['open'];
screenshot?: AgentDeviceClient['capture']['screenshot'];
updateSettings?: AgentDeviceClient['settings']['update'];
}): AgentDeviceClient {
const unexpectedCommandCall = async (): Promise<never> => {
throw new Error('unexpected command call');
};
const command = createThrowingMethodGroup<AgentDeviceClient['command']>();
return {
command,
devices: {
list: async () => [],
boot: unexpectedCommandCall,
},
sessions: {
list: async () => [],
close: async () => ({ session: 'default', identifiers: { session: 'default' } }),
},
apps: {
install: async () => ({
app: 'Demo',
appPath: '/tmp/Demo.app',
platform: 'ios',
identifiers: { appId: 'com.example.demo' },
}),
reinstall: async () => ({
app: 'Demo',
appPath: '/tmp/Demo.app',
platform: 'ios',
identifiers: { appId: 'com.example.demo' },
}),
installFromSource: params.installFromSource,
list: params.listApps ?? (async () => []),
open:
params.open ??
(async () => ({
session: 'default',
identifiers: { session: 'default' },
})),
close: async () => ({
session: 'default',
identifiers: { session: 'default' },
}),
push: unexpectedCommandCall,
triggerEvent: unexpectedCommandCall,
},
materializations: {
release: async (options) => ({
released: true,
materializationId: options.materializationId,
identifiers: { session: options.session ?? 'default' },
}),
},
leases: {
allocate: async (options) => ({
leaseId: 'lease-1',
tenantId: options.tenant,
runId: options.runId,
backend: options.leaseBackend ?? 'ios-simulator',
}),
heartbeat: async (options) => ({
leaseId: options.leaseId,
tenantId: options.tenant ?? 'tenant',
runId: options.runId ?? 'run',
backend: options.leaseBackend ?? 'ios-simulator',
}),
release: async () => ({ released: true }),
},
metro: {
prepare:
params.prepareMetro ??
(async () => ({
projectRoot: '/tmp/project',
kind: 'react-native',
dependenciesInstalled: false,
packageManager: null,
started: false,
reused: true,
pid: 0,
logPath: '/tmp/project/.agent-device/metro.log',
statusUrl: 'http://127.0.0.1:8081/status',
runtimeFilePath: null,
iosRuntime: {
platform: 'ios',
bundleUrl: 'https://sandbox.example.test/index.bundle?platform=ios',
},
androidRuntime: {
platform: 'android',
bundleUrl: 'https://sandbox.example.test/index.bundle?platform=android',
},
bridge: null,
})),
reload:
params.reloadMetro ??
(async () => ({
reloaded: true,
reloadUrl: 'http://127.0.0.1:8081/reload',
status: 200,
body: 'OK',
})),
},
capture: {
snapshot: async () => ({
nodes: [],
truncated: false,
identifiers: { session: 'default' },
}),
screenshot:
params.screenshot ??
(async () => ({
path: '/tmp/screenshot.png',
identifiers: { session: 'default' },
})),
diff: unexpectedCommandCall,
},
interactions: createThrowingMethodGroup<AgentDeviceClient['interactions']>(),
replay: createThrowingMethodGroup<AgentDeviceClient['replay']>(),
batch: createThrowingMethodGroup<AgentDeviceClient['batch']>(),
observability: createThrowingMethodGroup<AgentDeviceClient['observability']>(),
recording: createThrowingMethodGroup<AgentDeviceClient['recording']>(),
settings: {
update: params.updateSettings ?? unexpectedCommandCall,
},
};
}
function createThrowingMethodGroup<T extends object>(): T {
const unexpectedCommandCall = async (): Promise<never> => {
throw new Error('unexpected command call');
};
return new Proxy({} as Partial<T>, {
get: (target, property) => target[property as keyof T] ?? unexpectedCommandCall,
}) as T;
}
function solidPngBuffer(
width: number,
height: number,
color: { r: number; g: number; b: number },
): Buffer {
const png = new PNG({ width, height });
for (let i = 0; i < png.data.length; i += 4) {
png.data[i] = color.r;
png.data[i + 1] = color.g;
png.data[i + 2] = color.b;
png.data[i + 3] = 255;
}
return PNG.sync.write(png);
}