From ffa6741b7cf4b99b2ebaa6a897fe87bac327e330 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Mon, 25 May 2026 17:59:54 +0200 Subject: [PATCH 1/5] fix: allow inventory selectors with session locks --- .../__tests__/request-lock-policy.test.ts | 84 +++++++++++++++++++ .../request-platform-providers.test.ts | 48 +++++++++++ src/daemon/request-lock-policy.ts | 25 ++++-- src/daemon/request-platform-providers.ts | 6 +- 4 files changed, 157 insertions(+), 6 deletions(-) diff --git a/src/daemon/__tests__/request-lock-policy.test.ts b/src/daemon/__tests__/request-lock-policy.test.ts index 8b7717aff..8d8e08b31 100644 --- a/src/daemon/__tests__/request-lock-policy.test.ts +++ b/src/daemon/__tests__/request-lock-policy.test.ts @@ -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( { diff --git a/src/daemon/__tests__/request-platform-providers.test.ts b/src/daemon/__tests__/request-platform-providers.test.ts index f872e0eea..3be760e5c 100644 --- a/src/daemon/__tests__/request-platform-providers.test.ts +++ b/src/daemon/__tests__/request-platform-providers.test.ts @@ -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( @@ -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[] = []; diff --git a/src/daemon/request-lock-policy.ts b/src/daemon/request-lock-policy.ts index 772c68957..bd661c0d8 100644 --- a/src/daemon/request-lock-policy.ts +++ b/src/daemon/request-lock-policy.ts @@ -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, @@ -20,6 +21,11 @@ const LOCKABLE_SELECTOR_KEYS: Array = [ 'androidDeviceAllowlist', ]; +const SELECTOR_OVERRIDE_LOCK_POLICY_COMMANDS: ReadonlySet = new Set([ + PUBLIC_COMMANDS.apps, + PUBLIC_COMMANDS.devices, +]); + export function applyRequestLockPolicy( req: DaemonRequest, existingSession?: SessionState, @@ -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)); 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, diff --git a/src/daemon/request-platform-providers.ts b/src/daemon/request-platform-providers.ts index 97b0998ed..72737ec93 100644 --- a/src/daemon/request-platform-providers.ts +++ b/src/daemon/request-platform-providers.ts @@ -270,7 +270,11 @@ async function resolveScopedProviderDevice( req: DaemonRequest, existingSession: SessionState | undefined, ): Promise { - 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) && From a4a4fbf1589bd7428174424df5932c729bf40de0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Mon, 25 May 2026 18:19:25 +0200 Subject: [PATCH 2/5] fix: reduce lock policy complexity --- src/daemon/request-lock-policy.ts | 88 +++++++++++++++++++++++-------- 1 file changed, 65 insertions(+), 23 deletions(-) diff --git a/src/daemon/request-lock-policy.ts b/src/daemon/request-lock-policy.ts index bd661c0d8..cf3b01294 100644 --- a/src/daemon/request-lock-policy.ts +++ b/src/daemon/request-lock-policy.ts @@ -12,6 +12,12 @@ import { isApplePlatform, normalizePlatformSelector } from '../utils/device.ts'; type LockPlatform = NonNullable['lockPlatform']; +type LockPolicyContext = { + allowsSelectorOverride: boolean; + conflicts: SessionSelectorConflict[]; + lockPlatform: LockPlatform; +}; + const LOCKABLE_SELECTOR_KEYS: Array = [ 'target', 'device', @@ -36,22 +42,11 @@ export function applyRequestLockPolicy( } const nextFlags: CommandFlags = { ...(req.flags ?? {}) }; - 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)); - - if (conflicts.length === 0) { - if (lockPlatform && shouldApplyLockPlatformDefault) { - nextFlags.platform = lockPlatform; + const context = resolveLockPolicyContext(req, existingSession, nextFlags); + + if (context.conflicts.length === 0) { + if (shouldApplyLockPlatformDefault(context, existingSession, nextFlags)) { + nextFlags.platform = context.lockPlatform; } return { ...req, @@ -60,12 +55,7 @@ export function applyRequestLockPolicy( } if (lockPolicy === 'strip') { - if (existingSession) { - stripSessionConflicts(nextFlags, conflicts); - nextFlags.platform = existingSession.device.platform; - } else { - stripFreshSessionConflicts(nextFlags, req.meta?.lockPlatform); - } + applyStripLockPolicy(nextFlags, context, existingSession); return { ...req, flags: nextFlags, @@ -74,11 +64,63 @@ export function applyRequestLockPolicy( throw new AppError( 'INVALID_ARGS', - `${req.command} cannot override session lock policy with ${conflicts.map(formatSessionSelectorConflict).join(', ')}. ` + + `${req.command} cannot override session lock policy with ${context.conflicts.map(formatSessionSelectorConflict).join(', ')}. ` + 'Unset those selectors or remove the request lock policy.', ); } +function resolveLockPolicyContext( + req: DaemonRequest, + existingSession: SessionState | undefined, + flags: CommandFlags, +): LockPolicyContext { + const allowsSelectorOverride = SELECTOR_OVERRIDE_LOCK_POLICY_COMMANDS.has(req.command); + return { + allowsSelectorOverride, + conflicts: listLockPolicyConflicts(req, existingSession, flags, allowsSelectorOverride), + lockPlatform: req.meta?.lockPlatform, + }; +} + +function listLockPolicyConflicts( + req: DaemonRequest, + existingSession: SessionState | undefined, + flags: CommandFlags, + allowsSelectorOverride: boolean, +): SessionSelectorConflict[] { + if (allowsSelectorOverride) return []; + return existingSession + ? listSessionSelectorConflicts(existingSession, flags) + : listFreshSessionConflicts(flags, req.meta?.lockPlatform, req.command); +} + +function shouldApplyLockPlatformDefault( + context: LockPolicyContext, + existingSession: SessionState | undefined, + flags: CommandFlags, +): boolean { + if (!context.lockPlatform || existingSession || flags.platform !== undefined) { + return false; + } + if (!context.allowsSelectorOverride) { + return true; + } + return flags.serial === undefined && flags.androidDeviceAllowlist === undefined; +} + +function applyStripLockPolicy( + flags: CommandFlags, + context: LockPolicyContext, + existingSession: SessionState | undefined, +): void { + if (existingSession) { + stripSessionConflicts(flags, context.conflicts); + flags.platform = existingSession.device.platform; + return; + } + stripFreshSessionConflicts(flags, context.lockPlatform); +} + function listFreshSessionConflicts( flags: CommandFlags, lockPlatform: LockPlatform, From d01a987b7413b8a3067a6433228aa187c5b80fb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Mon, 25 May 2026 18:30:33 +0200 Subject: [PATCH 3/5] refactor: simplify lock policy helpers --- src/daemon/request-lock-policy.ts | 61 +++++++++++++++---------------- 1 file changed, 29 insertions(+), 32 deletions(-) diff --git a/src/daemon/request-lock-policy.ts b/src/daemon/request-lock-policy.ts index cf3b01294..71343b5af 100644 --- a/src/daemon/request-lock-policy.ts +++ b/src/daemon/request-lock-policy.ts @@ -12,12 +12,6 @@ import { isApplePlatform, normalizePlatformSelector } from '../utils/device.ts'; type LockPlatform = NonNullable['lockPlatform']; -type LockPolicyContext = { - allowsSelectorOverride: boolean; - conflicts: SessionSelectorConflict[]; - lockPlatform: LockPlatform; -}; - const LOCKABLE_SELECTOR_KEYS: Array = [ 'target', 'device', @@ -42,11 +36,25 @@ export function applyRequestLockPolicy( } const nextFlags: CommandFlags = { ...(req.flags ?? {}) }; - const context = resolveLockPolicyContext(req, existingSession, nextFlags); - - if (context.conflicts.length === 0) { - if (shouldApplyLockPlatformDefault(context, existingSession, nextFlags)) { - nextFlags.platform = context.lockPlatform; + const allowsSelectorOverride = SELECTOR_OVERRIDE_LOCK_POLICY_COMMANDS.has(req.command); + const conflicts = listLockPolicyConflicts( + req, + existingSession, + nextFlags, + allowsSelectorOverride, + ); + const lockPlatform = req.meta?.lockPlatform; + + if (conflicts.length === 0) { + if ( + shouldApplyLockPlatformDefault( + allowsSelectorOverride, + existingSession, + nextFlags, + lockPlatform, + ) + ) { + nextFlags.platform = lockPlatform; } return { ...req, @@ -55,7 +63,7 @@ export function applyRequestLockPolicy( } if (lockPolicy === 'strip') { - applyStripLockPolicy(nextFlags, context, existingSession); + applyStripLockPolicy(nextFlags, conflicts, lockPlatform, existingSession); return { ...req, flags: nextFlags, @@ -64,24 +72,11 @@ export function applyRequestLockPolicy( throw new AppError( 'INVALID_ARGS', - `${req.command} cannot override session lock policy with ${context.conflicts.map(formatSessionSelectorConflict).join(', ')}. ` + + `${req.command} cannot override session lock policy with ${conflicts.map(formatSessionSelectorConflict).join(', ')}. ` + 'Unset those selectors or remove the request lock policy.', ); } -function resolveLockPolicyContext( - req: DaemonRequest, - existingSession: SessionState | undefined, - flags: CommandFlags, -): LockPolicyContext { - const allowsSelectorOverride = SELECTOR_OVERRIDE_LOCK_POLICY_COMMANDS.has(req.command); - return { - allowsSelectorOverride, - conflicts: listLockPolicyConflicts(req, existingSession, flags, allowsSelectorOverride), - lockPlatform: req.meta?.lockPlatform, - }; -} - function listLockPolicyConflicts( req: DaemonRequest, existingSession: SessionState | undefined, @@ -95,14 +90,15 @@ function listLockPolicyConflicts( } function shouldApplyLockPlatformDefault( - context: LockPolicyContext, + allowsSelectorOverride: boolean, existingSession: SessionState | undefined, flags: CommandFlags, + lockPlatform: LockPlatform, ): boolean { - if (!context.lockPlatform || existingSession || flags.platform !== undefined) { + if (!lockPlatform || existingSession || flags.platform !== undefined) { return false; } - if (!context.allowsSelectorOverride) { + if (!allowsSelectorOverride) { return true; } return flags.serial === undefined && flags.androidDeviceAllowlist === undefined; @@ -110,15 +106,16 @@ function shouldApplyLockPlatformDefault( function applyStripLockPolicy( flags: CommandFlags, - context: LockPolicyContext, + conflicts: SessionSelectorConflict[], + lockPlatform: LockPlatform, existingSession: SessionState | undefined, ): void { if (existingSession) { - stripSessionConflicts(flags, context.conflicts); + stripSessionConflicts(flags, conflicts); flags.platform = existingSession.device.platform; return; } - stripFreshSessionConflicts(flags, context.lockPlatform); + stripFreshSessionConflicts(flags, lockPlatform); } function listFreshSessionConflicts( From 203be5b3e6edfd230b6371dbe5105475bede3562 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Mon, 25 May 2026 18:42:26 +0200 Subject: [PATCH 4/5] refactor: trim lock policy cleanup --- .../__tests__/request-lock-policy.test.ts | 38 +++++++------------ src/daemon/request-lock-policy.ts | 36 +++++------------- 2 files changed, 23 insertions(+), 51 deletions(-) diff --git a/src/daemon/__tests__/request-lock-policy.test.ts b/src/daemon/__tests__/request-lock-policy.test.ts index 8d8e08b31..7f24c871b 100644 --- a/src/daemon/__tests__/request-lock-policy.test.ts +++ b/src/daemon/__tests__/request-lock-policy.test.ts @@ -146,18 +146,7 @@ test.each([ 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, - }, - ); + assert.deepEqual(selectedFlags(req), expected); }, ); @@ -187,18 +176,7 @@ test.each([ }, }); - assert.deepEqual( - { - platform: req.flags?.platform, - device: req.flags?.device, - serial: req.flags?.serial, - }, - { - platform: expected.platform, - device: expected.device, - serial: expected.serial, - }, - ); + assert.deepEqual(selectedFlags(req), expected); }, ); @@ -340,3 +318,15 @@ test('strips only conflicting selectors for existing sessions', () => { assert.equal(req.flags?.device, 'iPhone 16'); assert.equal(req.flags?.serial, undefined); }); + +function selectedFlags(req: ReturnType): { + platform: string | undefined; + device: string | undefined; + serial: string | undefined; +} { + return { + platform: req.flags?.platform, + device: req.flags?.device, + serial: req.flags?.serial, + }; +} diff --git a/src/daemon/request-lock-policy.ts b/src/daemon/request-lock-policy.ts index 71343b5af..56dc97a2d 100644 --- a/src/daemon/request-lock-policy.ts +++ b/src/daemon/request-lock-policy.ts @@ -36,23 +36,17 @@ export function applyRequestLockPolicy( } const nextFlags: CommandFlags = { ...(req.flags ?? {}) }; - const allowsSelectorOverride = SELECTOR_OVERRIDE_LOCK_POLICY_COMMANDS.has(req.command); - const conflicts = listLockPolicyConflicts( - req, - existingSession, - nextFlags, - allowsSelectorOverride, - ); + const canOverrideSelector = SELECTOR_OVERRIDE_LOCK_POLICY_COMMANDS.has(req.command); + const conflicts = canOverrideSelector + ? [] + : existingSession + ? listSessionSelectorConflicts(existingSession, nextFlags) + : listFreshSessionConflicts(nextFlags, req.meta?.lockPlatform, req.command); const lockPlatform = req.meta?.lockPlatform; if (conflicts.length === 0) { if ( - shouldApplyLockPlatformDefault( - allowsSelectorOverride, - existingSession, - nextFlags, - lockPlatform, - ) + shouldApplyLockPlatformDefault(canOverrideSelector, existingSession, nextFlags, lockPlatform) ) { nextFlags.platform = lockPlatform; } @@ -77,20 +71,8 @@ export function applyRequestLockPolicy( ); } -function listLockPolicyConflicts( - req: DaemonRequest, - existingSession: SessionState | undefined, - flags: CommandFlags, - allowsSelectorOverride: boolean, -): SessionSelectorConflict[] { - if (allowsSelectorOverride) return []; - return existingSession - ? listSessionSelectorConflicts(existingSession, flags) - : listFreshSessionConflicts(flags, req.meta?.lockPlatform, req.command); -} - function shouldApplyLockPlatformDefault( - allowsSelectorOverride: boolean, + canOverrideSelector: boolean, existingSession: SessionState | undefined, flags: CommandFlags, lockPlatform: LockPlatform, @@ -98,7 +80,7 @@ function shouldApplyLockPlatformDefault( if (!lockPlatform || existingSession || flags.platform !== undefined) { return false; } - if (!allowsSelectorOverride) { + if (!canOverrideSelector) { return true; } return flags.serial === undefined && flags.androidDeviceAllowlist === undefined; From bc5a4c968147b2a22cfb0f758afc05301e3037a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Mon, 25 May 2026 18:49:44 +0200 Subject: [PATCH 5/5] fix: preserve explicit inventory selectors --- .../__tests__/request-lock-policy.test.ts | 37 ++++++++++++++++++- src/daemon/request-lock-policy.ts | 8 +++- 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/src/daemon/__tests__/request-lock-policy.test.ts b/src/daemon/__tests__/request-lock-policy.test.ts index 7f24c871b..9bcbb1f64 100644 --- a/src/daemon/__tests__/request-lock-policy.test.ts +++ b/src/daemon/__tests__/request-lock-policy.test.ts @@ -154,7 +154,7 @@ test.each([ { command: 'apps', flags: { device: 'iPhone 17' }, - expected: { platform: 'ios', device: 'iPhone 17', serial: undefined }, + expected: { platform: undefined, device: 'iPhone 17', serial: undefined }, }, { command: 'devices', @@ -180,6 +180,41 @@ test.each([ }, ); +test('allows inventory commands to use explicit Apple selectors under another lock platform', () => { + const req = applyRequestLockPolicy({ + token: 'token', + session: 'qa-android', + command: 'apps', + positionals: [], + flags: { + udid: 'SIM-001', + }, + meta: { + lockPolicy: 'reject', + lockPlatform: 'android', + }, + }); + + assert.equal(req.flags?.platform, undefined); + assert.equal(req.flags?.udid, 'SIM-001'); +}); + +test('defaults inventory commands without explicit selectors to the lock platform', () => { + const req = applyRequestLockPolicy({ + token: 'token', + session: 'qa-ios', + command: 'apps', + positionals: [], + flags: {}, + meta: { + lockPolicy: 'reject', + lockPlatform: 'ios', + }, + }); + + assert.equal(req.flags?.platform, 'ios'); +}); + test('allows matching redundant selectors for existing sessions', () => { const req = applyRequestLockPolicy( { diff --git a/src/daemon/request-lock-policy.ts b/src/daemon/request-lock-policy.ts index 56dc97a2d..85bdc6e23 100644 --- a/src/daemon/request-lock-policy.ts +++ b/src/daemon/request-lock-policy.ts @@ -83,7 +83,7 @@ function shouldApplyLockPlatformDefault( if (!canOverrideSelector) { return true; } - return flags.serial === undefined && flags.androidDeviceAllowlist === undefined; + return !LOCKABLE_SELECTOR_KEYS.some((key) => hasSelectorValue(flags[key])); } function applyStripLockPolicy( @@ -119,13 +119,17 @@ function listFreshSessionConflicts( } for (const key of LOCKABLE_SELECTOR_KEYS) { const value = flags[key]; - if (typeof value === 'string' && value.trim().length > 0) { + if (hasSelectorValue(value)) { conflicts.push({ key: key as SessionSelectorConflictKey, value }); } } return conflicts; } +function hasSelectorValue(value: unknown): value is string { + return typeof value === 'string' && value.trim().length > 0; +} + function platformSelectorsConflict( requested: ReturnType, locked: ReturnType,