Skip to content

Commit ffa6741

Browse files
committed
fix: allow inventory selectors with session locks
1 parent 47b981c commit ffa6741

4 files changed

Lines changed: 157 additions & 6 deletions

File tree

src/daemon/__tests__/request-lock-policy.test.ts

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,90 @@ test('rejects existing-session selector conflicts under request lock policy', ()
118118
);
119119
});
120120

121+
test.each([
122+
{
123+
command: 'apps',
124+
flags: { platform: 'ios', device: 'iPhone 17' },
125+
expected: { platform: 'ios', device: 'iPhone 17', serial: undefined },
126+
},
127+
{
128+
command: 'devices',
129+
flags: { platform: 'android', serial: 'emulator-5554' },
130+
expected: { platform: 'android', device: undefined, serial: 'emulator-5554' },
131+
},
132+
] as const)(
133+
'allows $command to inspect a different selector under existing-session lock policy',
134+
({ command, flags, expected }) => {
135+
const req = applyRequestLockPolicy(
136+
{
137+
token: 'token',
138+
session: 'qa-ios',
139+
command,
140+
positionals: [],
141+
flags,
142+
meta: {
143+
lockPolicy: 'reject',
144+
},
145+
},
146+
IOS_SESSION,
147+
);
148+
149+
assert.deepEqual(
150+
{
151+
platform: req.flags?.platform,
152+
device: req.flags?.device,
153+
serial: req.flags?.serial,
154+
},
155+
{
156+
platform: expected.platform,
157+
device: expected.device,
158+
serial: expected.serial,
159+
},
160+
);
161+
},
162+
);
163+
164+
test.each([
165+
{
166+
command: 'apps',
167+
flags: { device: 'iPhone 17' },
168+
expected: { platform: 'ios', device: 'iPhone 17', serial: undefined },
169+
},
170+
{
171+
command: 'devices',
172+
flags: { serial: 'emulator-5554' },
173+
expected: { platform: undefined, device: undefined, serial: 'emulator-5554' },
174+
},
175+
] as const)(
176+
'allows $command to inspect a fresh selector under session lock policy',
177+
({ command, flags, expected }) => {
178+
const req = applyRequestLockPolicy({
179+
token: 'token',
180+
session: 'qa-ios',
181+
command,
182+
positionals: [],
183+
flags,
184+
meta: {
185+
lockPolicy: 'reject',
186+
lockPlatform: 'ios',
187+
},
188+
});
189+
190+
assert.deepEqual(
191+
{
192+
platform: req.flags?.platform,
193+
device: req.flags?.device,
194+
serial: req.flags?.serial,
195+
},
196+
{
197+
platform: expected.platform,
198+
device: expected.device,
199+
serial: expected.serial,
200+
},
201+
);
202+
},
203+
);
204+
121205
test('allows matching redundant selectors for existing sessions', () => {
122206
const req = applyRequestLockPolicy(
123207
{

src/daemon/__tests__/request-platform-providers.test.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,22 @@ import {
66
makeAndroidSession,
77
makeIosSession,
88
} from '../../__tests__/test-utils/index.ts';
9+
import { withTargetDeviceResolutionScope } from '../../core/dispatch-resolve.ts';
910
import { createLocalAppleToolProvider, runXcrun } from '../../platforms/ios/tool-provider.ts';
11+
import type { DeviceInfo } from '../../utils/device.ts';
1012
import { startAppLog } from '../app-log.ts';
1113
import { resolveRecordingProvider } from '../recording-provider.ts';
1214
import { withRequestPlatformProviderScope } from '../request-platform-providers.ts';
1315
import type { DaemonRequest } from '../types.ts';
1416

17+
const OTHER_IOS_SIMULATOR: DeviceInfo = {
18+
platform: 'ios',
19+
id: 'sim-2',
20+
name: 'iPhone 17',
21+
kind: 'simulator',
22+
booted: true,
23+
};
24+
1525
test('request platform provider scope exposes Android executor for Android sessions', async () => {
1626
const calls: string[][] = [];
1727
const response = await withRequestPlatformProviderScope(
@@ -179,6 +189,44 @@ test('request platform provider scope applies Apple tool provider only for Apple
179189
assert.deepEqual(calls, [['list', 'devices', '-j']]);
180190
});
181191

192+
test('request platform provider scope follows explicit apps selector for existing sessions', async () => {
193+
const seenDevices: string[] = [];
194+
195+
const result = await withTargetDeviceResolutionScope(
196+
async () => [OTHER_IOS_SIMULATOR],
197+
async () =>
198+
await withRequestPlatformProviderScope(
199+
{
200+
req: {
201+
...request('apps'),
202+
flags: {
203+
platform: 'ios',
204+
device: 'iPhone 17',
205+
},
206+
},
207+
existingSession: makeIosSession('default'),
208+
providers: {
209+
appleToolProvider: ({ device, session }) => {
210+
seenDevices.push(`${session?.name}:${device.id}`);
211+
return createLocalAppleToolProvider({
212+
runCommand: async (cmd, args) => {
213+
throw new Error(`unexpected generic command: ${cmd} ${args.join(' ')}`);
214+
},
215+
simctl: {
216+
run: async () => ({ exitCode: 0, stdout: 'apps-ok', stderr: '' }),
217+
},
218+
});
219+
},
220+
},
221+
},
222+
async () => await runXcrun(['simctl', 'listapps', OTHER_IOS_SIMULATOR.id]),
223+
),
224+
);
225+
226+
assert.equal(result.stdout, 'apps-ok');
227+
assert.deepEqual(seenDevices, [`default:${OTHER_IOS_SIMULATOR.id}`]);
228+
});
229+
182230
test('request platform provider scopes stay isolated across concurrent requests', async () => {
183231
const androidCalls: string[] = [];
184232
const appleCalls: string[] = [];

src/daemon/request-lock-policy.ts

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { AppError } from '../utils/errors.ts';
22
import type { CommandFlags } from '../core/dispatch.ts';
33
import type { SessionState, DaemonRequest } from './types.ts';
4+
import { PUBLIC_COMMANDS } from '../command-catalog.ts';
45
import {
56
formatSessionSelectorConflict,
67
listSessionSelectorConflicts,
@@ -20,6 +21,11 @@ const LOCKABLE_SELECTOR_KEYS: Array<keyof CommandFlags> = [
2021
'androidDeviceAllowlist',
2122
];
2223

24+
const SELECTOR_OVERRIDE_LOCK_POLICY_COMMANDS: ReadonlySet<string> = new Set([
25+
PUBLIC_COMMANDS.apps,
26+
PUBLIC_COMMANDS.devices,
27+
]);
28+
2329
export function applyRequestLockPolicy(
2430
req: DaemonRequest,
2531
existingSession?: SessionState,
@@ -30,13 +36,22 @@ export function applyRequestLockPolicy(
3036
}
3137

3238
const nextFlags: CommandFlags = { ...(req.flags ?? {}) };
33-
const conflicts = existingSession
34-
? listSessionSelectorConflicts(existingSession, nextFlags)
35-
: listFreshSessionConflicts(nextFlags, req.meta?.lockPlatform, req.command);
39+
const allowsSelectorOverride = SELECTOR_OVERRIDE_LOCK_POLICY_COMMANDS.has(req.command);
40+
const conflicts = allowsSelectorOverride
41+
? []
42+
: existingSession
43+
? listSessionSelectorConflicts(existingSession, nextFlags)
44+
: listFreshSessionConflicts(nextFlags, req.meta?.lockPlatform, req.command);
45+
const lockPlatform = req.meta?.lockPlatform;
46+
const shouldApplyLockPlatformDefault =
47+
!existingSession &&
48+
nextFlags.platform === undefined &&
49+
(!allowsSelectorOverride ||
50+
(nextFlags.serial === undefined && nextFlags.androidDeviceAllowlist === undefined));
3651

3752
if (conflicts.length === 0) {
38-
if (!existingSession && req.meta?.lockPlatform && nextFlags.platform === undefined) {
39-
nextFlags.platform = req.meta.lockPlatform;
53+
if (lockPlatform && shouldApplyLockPlatformDefault) {
54+
nextFlags.platform = lockPlatform;
4055
}
4156
return {
4257
...req,

src/daemon/request-platform-providers.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,11 @@ async function resolveScopedProviderDevice(
270270
req: DaemonRequest,
271271
existingSession: SessionState | undefined,
272272
): Promise<DeviceInfo | undefined> {
273-
if (existingSession) return existingSession.device;
273+
if (existingSession) {
274+
return req.command === PUBLIC_COMMANDS.apps && hasExplicitDeviceSelector(req.flags)
275+
? await resolveTargetDevice(req.flags ?? {})
276+
: existingSession.device;
277+
}
274278
if (
275279
req.command !== PUBLIC_COMMANDS.open &&
276280
!hasExplicitDeviceSelector(req.flags) &&

0 commit comments

Comments
 (0)