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
60 changes: 1 addition & 59 deletions src/__tests__/runtime-public.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,12 @@ import { test } from 'vitest';
import {
createAgentDevice,
createMemorySessionStore,
assertBackendCapabilityAllowed,
localCommandPolicy,
restrictedCommandPolicy,
type AgentDevice,
type CommandSessionStore,
} from '../runtime.ts';
import {
BACKEND_CAPABILITY_NAMES,
hasBackendCapability,
type AgentDeviceBackend,
} from '../backend.ts';
import { BACKEND_CAPABILITY_NAMES, type AgentDeviceBackend } from '../backend.ts';
import { commands, type ScreenshotCommandOptions } from '../commands/index.ts';
import {
createLocalArtifactAdapter,
Expand Down Expand Up @@ -167,54 +162,6 @@ test('local artifact adapter can constrain explicit local paths to a root', asyn
}
});

test('named backend capabilities require backend support and policy allowance', () => {
const supportedRuntime = createAgentDevice({
backend: {
platform: 'android',
capabilities: ['android.shell'],
escapeHatches: {
androidShell: async () => ({ exitCode: 0, stdout: '', stderr: '' }),
},
},
artifacts,
policy: restrictedCommandPolicy({ allowNamedBackendCapabilities: ['android.shell'] }),
});

assert.doesNotThrow(() => assertBackendCapabilityAllowed(supportedRuntime, 'android.shell'));

const policyBlockedRuntime = createAgentDevice({
backend: {
platform: 'android',
capabilities: ['android.shell'],
escapeHatches: {
androidShell: async () => ({ exitCode: 0, stdout: '', stderr: '' }),
},
},
artifacts,
});

assert.throws(
() => assertBackendCapabilityAllowed(policyBlockedRuntime, 'android.shell'),
/not allowed by command policy/,
);

assert.throws(
() => assertBackendCapabilityAllowed(supportedRuntime, 'ios.runnerCommand'),
/not supported by this backend/,
);

const missingMethodRuntime = createAgentDevice({
backend: { platform: 'android', capabilities: ['android.shell'] },
artifacts,
policy: restrictedCommandPolicy({ allowNamedBackendCapabilities: ['android.shell'] }),
});

assert.throws(
() => assertBackendCapabilityAllowed(missingMethodRuntime, 'android.shell'),
/does not implement its escape hatch method/,
);
});

test('memory session store does not expose mutable record references', async () => {
const store = createMemorySessionStore([
{
Expand Down Expand Up @@ -288,11 +235,6 @@ test('internal backend, commands, and io modules are usable', () => {
out: { kind: 'path', path: '/tmp/screen.png' },
} satisfies ScreenshotCommandOptions;
assert.equal(BACKEND_CAPABILITY_NAMES.includes('android.shell'), true);
assert.equal(hasBackendCapability(backend, 'android.shell'), false);
assert.equal(
hasBackendCapability({ platform: 'android', capabilities: ['android.shell'] }, 'android.shell'),
true,
);
assert.equal(options.out.kind, 'path');
assert.equal(typeof commands.capture.screenshot, 'function');
assert.equal(typeof commands.capture.diffScreenshot, 'function');
Expand Down
21 changes: 0 additions & 21 deletions src/backend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -414,12 +414,6 @@ export type BackendEscapeHatches = {
): Promise<BackendScreenshotResult | void>;
};

const BACKEND_CAPABILITY_ESCAPE_HATCH_METHODS = {
'android.shell': 'androidShell',
'ios.runnerCommand': 'iosRunnerCommand',
'macos.desktopScreenshot': 'macosDesktopScreenshot',
} as const satisfies Record<BackendCapabilityName, keyof BackendEscapeHatches>;

export type AgentDeviceBackend = {
platform: AgentDeviceBackendPlatform;
capabilities?: BackendCapabilitySet;
Expand Down Expand Up @@ -572,18 +566,3 @@ export type AgentDeviceBackend = {
options?: BackendMeasurePerfOptions,
): Promise<BackendMeasurePerfResult>;
};

export function hasBackendCapability(
backend: Pick<AgentDeviceBackend, 'platform' | 'capabilities'>,
capability: BackendCapabilityName,
): boolean {
return backend.capabilities?.includes(capability) ?? false;
}

export function hasBackendEscapeHatch(
backend: Pick<AgentDeviceBackend, 'escapeHatches'>,
capability: BackendCapabilityName,
): boolean {
const method = BACKEND_CAPABILITY_ESCAPE_HATCH_METHODS[capability];
return typeof backend.escapeHatches?.[method] === 'function';
}
6 changes: 0 additions & 6 deletions src/commands/cli-grammar/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,12 +196,6 @@ export function setOf<T extends string>(...values: T[]): ReadonlySet<T> {
return new Set(values);
}

export function commandNameSet<const TName extends string>(
names: readonly TName[],
): ReadonlySet<string> {
return new Set(names);
}

export function isOneOf<T extends string>(
value: string | undefined,
values: ReadonlySet<T>,
Expand Down
4 changes: 0 additions & 4 deletions src/compat/maestro/__tests__/support-matrix.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { getFlagDefinitions } from '../../../utils/cli-flags.ts';
import {
MAESTRO_COMPAT_SUPPORTED_CAPABILITIES,
MAESTRO_COMPAT_TRACKER_URL,
MAESTRO_COMPAT_UNSUPPORTED_CAPABILITIES,
formatMaestroCapabilityList,
} from '../support-matrix.ts';

Expand All @@ -22,7 +21,4 @@ test('Maestro replay docs stay in sync with the compatibility support matrix', (
for (const capability of MAESTRO_COMPAT_SUPPORTED_CAPABILITIES) {
expect(plainDocs).toContain(capability);
}
for (const capability of MAESTRO_COMPAT_UNSUPPORTED_CAPABILITIES) {
expect(plainDocs).toContain(capability);
}
});
9 changes: 0 additions & 9 deletions src/compat/maestro/support-matrix.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,6 @@ export const MAESTRO_COMPAT_SUPPORTED_CAPABILITIES = [
'ordered trusted runScript file/env scripts with http.post, json, and output variables',
] as const;

export const MAESTRO_COMPAT_UNSUPPORTED_CAPABILITIES = [
'repeat.while',
'full expression predicates beyond boolean literals and maestro.platform comparisons',
'evalScript',
'device utility commands',
'Android app launch arguments',
'Android app state reset',
] as const;

export const MAESTRO_COMPAT_TRACKER_URL = 'https://github.com/callstack/agent-device/issues/558';

export const MAESTRO_NEW_ISSUE_URL = 'https://github.com/callstack/agent-device/issues/new';
Expand Down
69 changes: 0 additions & 69 deletions src/core/__tests__/dispatch-resolve.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ vi.mock('../../platforms/ios/devices.ts', async (importOriginal) => {
});

import {
resolveIosDevice,
resolveTargetDevice,
withDeviceInventoryProvider,
withResolveTargetDeviceCacheScope,
Expand Down Expand Up @@ -66,74 +65,6 @@ beforeEach(() => {
mockListAppleDevices.mockReset();
});

// --- Physical device rejected in favour of simulator fallback ---

test('resolveIosDevice prefers fallback simulator over auto-selected physical device', async () => {
mockFindBootableIosSimulator.mockResolvedValue(simulator);
const result = await resolveIosDevice([physical], { platform: 'ios' }, {});
assert.equal(result.id, 'sim-1');
assert.equal(result.kind, 'simulator');
});

test('resolveIosDevice falls back to physical device when no simulator is found', async () => {
const result = await resolveIosDevice([physical], { platform: 'ios' }, {});
assert.equal(result.id, 'phys-1');
assert.equal(result.kind, 'device');
});

// --- Explicit selectors bypass the fallback ---

test('resolveIosDevice keeps physical device when udid is explicit', async () => {
mockFindBootableIosSimulator.mockResolvedValue(simulator);
const result = await resolveIosDevice([physical], { platform: 'ios', udid: 'phys-1' }, {});
assert.equal(result.id, 'phys-1');
assert.equal(mockFindBootableIosSimulator.mock.calls.length, 0);
});

test('resolveIosDevice keeps physical device when deviceName is explicit', async () => {
mockFindBootableIosSimulator.mockResolvedValue(simulator);
const result = await resolveIosDevice(
[physical],
{ platform: 'ios', deviceName: 'My iPhone' },
{},
);
assert.equal(result.id, 'phys-1');
assert.equal(mockFindBootableIosSimulator.mock.calls.length, 0);
});

// --- Empty device list triggers fallback (P1-A: DEVICE_NOT_FOUND recovery) ---

test('resolveIosDevice recovers from empty device list via simulator fallback', async () => {
mockFindBootableIosSimulator.mockResolvedValue(simulator);
const result = await resolveIosDevice([], { platform: 'ios' }, {});
assert.equal(result.id, 'sim-1');
assert.equal(result.kind, 'simulator');
});

test('resolveIosDevice throws DEVICE_NOT_FOUND when empty list and no fallback simulator', async () => {
const err = await resolveIosDevice([], { platform: 'ios' }, {}).catch((e) => e);
assert.ok(err instanceof AppError);
assert.equal(err.code, 'DEVICE_NOT_FOUND');
});

test('resolveIosDevice rethrows DEVICE_NOT_FOUND from resolveDevice when explicit selector used', async () => {
mockFindBootableIosSimulator.mockResolvedValue(simulator);
const err = await resolveIosDevice([], { platform: 'ios', udid: 'nonexistent' }, {}).catch(
(e) => e,
);
assert.ok(err instanceof AppError);
assert.equal(err.code, 'DEVICE_NOT_FOUND');
});

// --- Simulator already in the device list (normal path) ---

test('resolveIosDevice returns simulator directly when present in device list', async () => {
const result = await resolveIosDevice([physical, bootedSimulator], { platform: 'ios' }, {});
assert.equal(result.id, 'sim-2');
assert.equal(result.kind, 'simulator');
assert.equal(mockFindBootableIosSimulator.mock.calls.length, 0);
});

test('resolveTargetDevice reuses request-scoped device resolution cache for identical selectors', async () => {
mockListAppleDevices.mockResolvedValue([bootedSimulator]);

Expand Down
8 changes: 0 additions & 8 deletions src/core/dispatch-resolve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,14 +111,6 @@ function hasExplicitAppleDeviceSelector(selector: AppleDeviceSelector): boolean
return Boolean(selector.udid || selector.serial || selector.deviceName);
}

export async function resolveIosDevice(
devices: DeviceInfo[],
selector: AppleDeviceSelector,
context: { simulatorSetPath?: string },
): Promise<DeviceInfo> {
return await resolveAppleDevice(devices, selector, context);
}

export async function resolveTargetDevice(flags: ResolveDeviceFlags): Promise<DeviceInfo> {
const normalizedPlatform = normalizePlatformSelector(flags.platform);
const iosSimulatorSetPath = resolveAppleSimulatorSetPathForSelector({
Expand Down
74 changes: 0 additions & 74 deletions src/daemon-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,28 +23,6 @@ export { shouldResetDaemonAfterRequestTimeout } from './daemon-client-timeout.ts
export type DaemonRequest = SharedDaemonRequest;
export type DaemonResponse = SharedDaemonResponse;

export type OpenAppOptions = {
session?: string;
app?: string;
url?: string;
lockPolicy?: NonNullable<DaemonRequest['meta']>['lockPolicy'];
lockPlatform?: NonNullable<DaemonRequest['meta']>['lockPlatform'];
platform?: NonNullable<DaemonRequest['flags']>['platform'];
target?: NonNullable<DaemonRequest['flags']>['target'];
device?: NonNullable<DaemonRequest['flags']>['device'];
udid?: NonNullable<DaemonRequest['flags']>['udid'];
serial?: NonNullable<DaemonRequest['flags']>['serial'];
activity?: NonNullable<DaemonRequest['flags']>['activity'];
launchConsole?: NonNullable<DaemonRequest['flags']>['launchConsole'];
launchArgs?: NonNullable<DaemonRequest['flags']>['launchArgs'];
out?: NonNullable<DaemonRequest['flags']>['out'];
saveScript?: NonNullable<DaemonRequest['flags']>['saveScript'];
deviceHub?: NonNullable<DaemonRequest['flags']>['deviceHub'];
relaunch?: boolean;
runtime?: DaemonRequest['runtime'];
meta?: Omit<NonNullable<DaemonRequest['meta']>, 'uploadedArtifactId' | 'clientArtifactPaths'>;
};

const REQUEST_TIMEOUT_MS = 90_000;
const PREPARE_REQUEST_TIMEOUT_MS = 240_000;

Expand Down Expand Up @@ -134,55 +112,3 @@ function isExplicitTimeoutCommand(command: string | undefined): boolean {
command === PUBLIC_COMMANDS.snapshot
);
}

export async function openApp(options: OpenAppOptions = {}): Promise<DaemonResponse> {
const {
session = 'default',
app,
url,
lockPolicy,
lockPlatform,
platform,
target,
device,
udid,
serial,
activity,
launchConsole,
launchArgs,
out,
saveScript,
deviceHub,
relaunch,
runtime,
meta,
} = options;

const positionals = app ? (url ? [app, url] : [app]) : url ? [url] : [];

return await sendToDaemon({
session,
command: 'open',
positionals,
flags: {
...(platform !== undefined ? { platform } : {}),
...(target !== undefined ? { target } : {}),
...(device !== undefined ? { device } : {}),
...(udid !== undefined ? { udid } : {}),
...(serial !== undefined ? { serial } : {}),
...(activity !== undefined ? { activity } : {}),
...(launchConsole !== undefined ? { launchConsole } : {}),
...(launchArgs !== undefined ? { launchArgs } : {}),
...(out !== undefined ? { out } : {}),
...(saveScript !== undefined ? { saveScript } : {}),
...(deviceHub !== undefined ? { deviceHub } : {}),
...(relaunch ? { relaunch: true } : {}),
},
...(runtime !== undefined ? { runtime } : {}),
meta: {
...(meta ?? {}),
...(lockPolicy !== undefined ? { lockPolicy } : {}),
...(lockPlatform !== undefined ? { lockPlatform } : {}),
},
});
}
13 changes: 8 additions & 5 deletions src/daemon/__tests__/daemon-command-registry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import {
getDaemonCommandRoute,
getSessionCommandKind,
isLeaseAdmissionExempt,
listDaemonHandlerCommands,
shouldBlockForInvalidRecording,
shouldGuardAndroidBlockingDialog,
shouldLockSessionExecution,
Expand All @@ -19,18 +18,22 @@ import {
import type { DaemonRequest } from '../types.ts';

test('daemon command registry owns specialized handler routes', () => {
assert.deepEqual(listDaemonHandlerCommands('lease').sort(), [
for (const command of [
INTERNAL_COMMANDS.leaseAllocate,
INTERNAL_COMMANDS.leaseHeartbeat,
INTERNAL_COMMANDS.leaseRelease,
]);
assert.deepEqual(listDaemonHandlerCommands('snapshot').sort(), [
]) {
assert.equal(getDaemonCommandRoute(command), 'lease', `${command} lease route`);
}
for (const command of [
PUBLIC_COMMANDS.alert,
PUBLIC_COMMANDS.diff,
PUBLIC_COMMANDS.settings,
PUBLIC_COMMANDS.snapshot,
PUBLIC_COMMANDS.wait,
]);
]) {
assert.equal(getDaemonCommandRoute(command), 'snapshot', `${command} snapshot route`);
}
assert.equal(getDaemonCommandRoute(PUBLIC_COMMANDS.back), 'generic');
});

Expand Down
Loading
Loading