Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
84 changes: 84 additions & 0 deletions src/daemon/__tests__/request-lock-policy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,90 @@ test('rejects existing-session selector conflicts under request lock policy', ()
);
});

test.each([
{
command: 'apps',
flags: { platform: 'ios', device: 'iPhone 17' },
expected: { platform: 'ios', device: 'iPhone 17', serial: undefined },
},
{
command: 'devices',
flags: { platform: 'android', serial: 'emulator-5554' },
expected: { platform: 'android', device: undefined, serial: 'emulator-5554' },
},
] as const)(
'allows $command to inspect a different selector under existing-session lock policy',
({ command, flags, expected }) => {
const req = applyRequestLockPolicy(
{
token: 'token',
session: 'qa-ios',
command,
positionals: [],
flags,
meta: {
lockPolicy: 'reject',
},
},
IOS_SESSION,
);

assert.deepEqual(
{
platform: req.flags?.platform,
device: req.flags?.device,
serial: req.flags?.serial,
},
{
platform: expected.platform,
device: expected.device,
serial: expected.serial,
},
);
},
);

test.each([
{
command: 'apps',
flags: { device: 'iPhone 17' },
expected: { platform: 'ios', device: 'iPhone 17', serial: undefined },
},
{
command: 'devices',
flags: { serial: 'emulator-5554' },
expected: { platform: undefined, device: undefined, serial: 'emulator-5554' },
},
] as const)(
'allows $command to inspect a fresh selector under session lock policy',
({ command, flags, expected }) => {
const req = applyRequestLockPolicy({
token: 'token',
session: 'qa-ios',
command,
positionals: [],
flags,
meta: {
lockPolicy: 'reject',
lockPlatform: 'ios',
},
});

assert.deepEqual(
{
platform: req.flags?.platform,
device: req.flags?.device,
serial: req.flags?.serial,
},
{
platform: expected.platform,
device: expected.device,
serial: expected.serial,
},
);
},
);

test('allows matching redundant selectors for existing sessions', () => {
const req = applyRequestLockPolicy(
{
Expand Down
48 changes: 48 additions & 0 deletions src/daemon/__tests__/request-platform-providers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,22 @@ import {
makeAndroidSession,
makeIosSession,
} from '../../__tests__/test-utils/index.ts';
import { withTargetDeviceResolutionScope } from '../../core/dispatch-resolve.ts';
import { createLocalAppleToolProvider, runXcrun } from '../../platforms/ios/tool-provider.ts';
import type { DeviceInfo } from '../../utils/device.ts';
import { startAppLog } from '../app-log.ts';
import { resolveRecordingProvider } from '../recording-provider.ts';
import { withRequestPlatformProviderScope } from '../request-platform-providers.ts';
import type { DaemonRequest } from '../types.ts';

const OTHER_IOS_SIMULATOR: DeviceInfo = {
platform: 'ios',
id: 'sim-2',
name: 'iPhone 17',
kind: 'simulator',
booted: true,
};

test('request platform provider scope exposes Android executor for Android sessions', async () => {
const calls: string[][] = [];
const response = await withRequestPlatformProviderScope(
Expand Down Expand Up @@ -179,6 +189,44 @@ test('request platform provider scope applies Apple tool provider only for Apple
assert.deepEqual(calls, [['list', 'devices', '-j']]);
});

test('request platform provider scope follows explicit apps selector for existing sessions', async () => {
const seenDevices: string[] = [];

const result = await withTargetDeviceResolutionScope(
async () => [OTHER_IOS_SIMULATOR],
async () =>
await withRequestPlatformProviderScope(
{
req: {
...request('apps'),
flags: {
platform: 'ios',
device: 'iPhone 17',
},
},
existingSession: makeIosSession('default'),
providers: {
appleToolProvider: ({ device, session }) => {
seenDevices.push(`${session?.name}:${device.id}`);
return createLocalAppleToolProvider({
runCommand: async (cmd, args) => {
throw new Error(`unexpected generic command: ${cmd} ${args.join(' ')}`);
},
simctl: {
run: async () => ({ exitCode: 0, stdout: 'apps-ok', stderr: '' }),
},
});
},
},
},
async () => await runXcrun(['simctl', 'listapps', OTHER_IOS_SIMULATOR.id]),
),
);

assert.equal(result.stdout, 'apps-ok');
assert.deepEqual(seenDevices, [`default:${OTHER_IOS_SIMULATOR.id}`]);
});

test('request platform provider scopes stay isolated across concurrent requests', async () => {
const androidCalls: string[] = [];
const appleCalls: string[] = [];
Expand Down
25 changes: 20 additions & 5 deletions src/daemon/request-lock-policy.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { AppError } from '../utils/errors.ts';
import type { CommandFlags } from '../core/dispatch.ts';
import type { SessionState, DaemonRequest } from './types.ts';
import { PUBLIC_COMMANDS } from '../command-catalog.ts';
import {
formatSessionSelectorConflict,
listSessionSelectorConflicts,
Expand All @@ -20,6 +21,11 @@ const LOCKABLE_SELECTOR_KEYS: Array<keyof CommandFlags> = [
'androidDeviceAllowlist',
];

const SELECTOR_OVERRIDE_LOCK_POLICY_COMMANDS: ReadonlySet<string> = new Set([
PUBLIC_COMMANDS.apps,
PUBLIC_COMMANDS.devices,
]);

export function applyRequestLockPolicy(
req: DaemonRequest,
existingSession?: SessionState,
Expand All @@ -30,13 +36,22 @@ export function applyRequestLockPolicy(
}

const nextFlags: CommandFlags = { ...(req.flags ?? {}) };
const conflicts = existingSession
? listSessionSelectorConflicts(existingSession, nextFlags)
: listFreshSessionConflicts(nextFlags, req.meta?.lockPlatform, req.command);
const allowsSelectorOverride = SELECTOR_OVERRIDE_LOCK_POLICY_COMMANDS.has(req.command);
const conflicts = allowsSelectorOverride
? []
: existingSession
? listSessionSelectorConflicts(existingSession, nextFlags)
: listFreshSessionConflicts(nextFlags, req.meta?.lockPlatform, req.command);
const lockPlatform = req.meta?.lockPlatform;
const shouldApplyLockPlatformDefault =
!existingSession &&
nextFlags.platform === undefined &&
(!allowsSelectorOverride ||
(nextFlags.serial === undefined && nextFlags.androidDeviceAllowlist === undefined));
Comment thread
thymikee marked this conversation as resolved.
Outdated

if (conflicts.length === 0) {
if (!existingSession && req.meta?.lockPlatform && nextFlags.platform === undefined) {
nextFlags.platform = req.meta.lockPlatform;
if (lockPlatform && shouldApplyLockPlatformDefault) {
nextFlags.platform = lockPlatform;
}
return {
...req,
Expand Down
6 changes: 5 additions & 1 deletion src/daemon/request-platform-providers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,11 @@ async function resolveScopedProviderDevice(
req: DaemonRequest,
existingSession: SessionState | undefined,
): Promise<DeviceInfo | undefined> {
if (existingSession) return existingSession.device;
if (existingSession) {
return req.command === PUBLIC_COMMANDS.apps && hasExplicitDeviceSelector(req.flags)
? await resolveTargetDevice(req.flags ?? {})
: existingSession.device;
}
if (
req.command !== PUBLIC_COMMANDS.open &&
!hasExplicitDeviceSelector(req.flags) &&
Expand Down
Loading