From 6ba147ab2b494c0839f8eb4304f4d9a455194613 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Wed, 22 Jul 2026 08:43:02 +0000 Subject: [PATCH 1/4] fix(apple): parse parenthesized xctrace physical device format Accept the new 'Device Name (OS Version) (device-id)' output from xctrace list devices while preserving the legacy bracket format. Relates to #1355 Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../apple/core/__tests__/devices.test.ts | 41 +++++++++++++++++++ src/platforms/apple/core/devices.ts | 8 ++-- 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/src/platforms/apple/core/__tests__/devices.test.ts b/src/platforms/apple/core/__tests__/devices.test.ts index c6dceb4ed..8ca02f2f0 100644 --- a/src/platforms/apple/core/__tests__/devices.test.ts +++ b/src/platforms/apple/core/__tests__/devices.test.ts @@ -231,6 +231,47 @@ test('parseXctracePhysicalAppleDevices tags Apple Vision devices as visionOS', ( ]); }); +test('parseXctracePhysicalAppleDevices parses the parenthesized physical device format', () => { + const parsed = parseXctracePhysicalAppleDevices( + [ + '== Devices ==', + 'iPhone 8 Plus (16.7.16) (00008020-001C2D2234567890)', + 'Studio iPad Pro (17.0) (ipad-udid-1)', + 'Living Room Apple TV (16.0) [tv-udid-1]', + ].join('\n'), + ); + + assert.deepEqual(parsed, [ + { + platform: 'apple', + id: '00008020-001C2D2234567890', + name: 'iPhone 8 Plus', + kind: 'device', + target: 'mobile', + appleOs: 'ios', + booted: true, + }, + { + platform: 'apple', + id: 'ipad-udid-1', + name: 'Studio iPad Pro', + kind: 'device', + target: 'mobile', + appleOs: 'ipados', + booted: true, + }, + { + platform: 'apple', + id: 'tv-udid-1', + name: 'Living Room Apple TV', + kind: 'device', + target: 'tv', + appleOs: 'tvos', + booted: true, + }, + ]); +}); + test('listAppleDevices tags devicectl iPad product types as iPadOS', async () => { mockRunCommand = async (_cmd, args) => { if (args.join(' ') === 'simctl list devices -j') { diff --git a/src/platforms/apple/core/devices.ts b/src/platforms/apple/core/devices.ts index faaa7c281..18f763c89 100644 --- a/src/platforms/apple/core/devices.ts +++ b/src/platforms/apple/core/devices.ts @@ -58,7 +58,8 @@ type IosDeviceDiscoveryOptions = { }; const XCTRACE_SECTION_HEADER_PATTERN = /^==\s*(.+?)\s*==$/; -const XCTRACE_DEVICE_LINE_PATTERN = /^(?.+?)\s+\[(?[^[\]]+)\]\s*$/; +const XCTRACE_DEVICE_LINE_PATTERN = + /^(?.+?)(?:\s+\((?[^)]+)\))?\s+(?:\[(?[^[\]]+)\]|\((?[^)]+)\))\s*$/; function normalizeAppleDescriptor(value: string | undefined): string { return (value ?? '').trim().toLowerCase(); @@ -271,8 +272,9 @@ export function parseXctracePhysicalAppleDevices(output: string): DeviceInfo[] { if (section !== 'Devices') continue; const deviceMatch = XCTRACE_DEVICE_LINE_PATTERN.exec(line); - const id = deviceMatch?.groups?.id?.trim() ?? ''; const name = deviceMatch?.groups?.name?.trim() ?? ''; + const id = deviceMatch?.groups?.idBracket?.trim() ?? deviceMatch?.groups?.idParen?.trim() ?? ''; + const osVersion = deviceMatch?.groups?.osVersion?.trim(); if (!id || !name) continue; const target = resolveAppleTargetFromLabel(name); if (!target) continue; @@ -283,7 +285,7 @@ export function parseXctracePhysicalAppleDevices(output: string): DeviceInfo[] { name, kind: 'device', target, - appleOs: resolveAppleOs(target, [name]), + appleOs: resolveAppleOs(target, osVersion ? [name, osVersion] : [name]), // xctrace lists currently connected devices in the "Devices" section. // The "Devices Offline" section is excluded above, so treating these as // booted preserves the existing physical-device selection semantics. From 740462fa92a3819f4c79b2d7019f45c82d96ffaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Wed, 22 Jul 2026 08:45:50 +0000 Subject: [PATCH 2/4] refactor(apple): split xctrace parser to reduce cyclomatic complexity Extract device line parsing and DeviceInfo construction into focused helpers so the discovery loop stays under fallow thresholds. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- src/platforms/apple/core/devices.ts | 59 +++++++++++++++++++---------- 1 file changed, 40 insertions(+), 19 deletions(-) diff --git a/src/platforms/apple/core/devices.ts b/src/platforms/apple/core/devices.ts index 18f763c89..23a00ab48 100644 --- a/src/platforms/apple/core/devices.ts +++ b/src/platforms/apple/core/devices.ts @@ -255,6 +255,42 @@ function mapDevicectlAppleDevices(payload: DevicectlListDevicesPayload): DeviceI return devices; } +function parseXctraceDeviceLine( + line: string, +): { name: string; id: string; osVersion?: string } | null { + const match = XCTRACE_DEVICE_LINE_PATTERN.exec(line); + if (!match?.groups) return null; + + const name = match.groups.name?.trim() ?? ''; + const id = match.groups.idBracket?.trim() ?? match.groups.idParen?.trim() ?? ''; + const osVersion = match.groups.osVersion?.trim(); + if (!name || !id) return null; + + return { name, id, osVersion }; +} + +function buildXctracePhysicalDevice( + name: string, + id: string, + osVersion: string | undefined, +): DeviceInfo | null { + const target = resolveAppleTargetFromLabel(name); + if (!target) return null; + + return { + platform: 'apple', + id, + name, + kind: 'device', + target, + appleOs: resolveAppleOs(target, osVersion ? [name, osVersion] : [name]), + // xctrace lists currently connected devices in the "Devices" section. + // The "Devices Offline" section is excluded above, so treating these as + // booted preserves the existing physical-device selection semantics. + booted: true, + }; +} + export function parseXctracePhysicalAppleDevices(output: string): DeviceInfo[] { const devices: DeviceInfo[] = []; let section: string | null = null; @@ -271,26 +307,11 @@ export function parseXctracePhysicalAppleDevices(output: string): DeviceInfo[] { if (section !== 'Devices') continue; - const deviceMatch = XCTRACE_DEVICE_LINE_PATTERN.exec(line); - const name = deviceMatch?.groups?.name?.trim() ?? ''; - const id = deviceMatch?.groups?.idBracket?.trim() ?? deviceMatch?.groups?.idParen?.trim() ?? ''; - const osVersion = deviceMatch?.groups?.osVersion?.trim(); - if (!id || !name) continue; - const target = resolveAppleTargetFromLabel(name); - if (!target) continue; + const parsedLine = parseXctraceDeviceLine(line); + if (!parsedLine) continue; - devices.push({ - platform: 'apple', - id, - name, - kind: 'device', - target, - appleOs: resolveAppleOs(target, osVersion ? [name, osVersion] : [name]), - // xctrace lists currently connected devices in the "Devices" section. - // The "Devices Offline" section is excluded above, so treating these as - // booted preserves the existing physical-device selection semantics. - booted: true, - }); + const device = buildXctracePhysicalDevice(parsedLine.name, parsedLine.id, parsedLine.osVersion); + if (device) devices.push(device); } return devices; From fc188ab10d3da02b2b2d7b1361be8ed5d4b04320 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Wed, 22 Jul 2026 09:04:19 +0000 Subject: [PATCH 3/4] fix(apple): avoid interpreting bracket-format device names as having OS version Only treat 'Name (version) (id)' as the new parenthesized xctrace format; preserve the full name for legacy 'Name [id]' lines, including names that contain parentheses. Addresses review feedback in #1360. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../apple/core/__tests__/devices.test.ts | 29 ++++++++++++++++++- src/platforms/apple/core/devices.ts | 2 +- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/src/platforms/apple/core/__tests__/devices.test.ts b/src/platforms/apple/core/__tests__/devices.test.ts index 8ca02f2f0..7c96c59cd 100644 --- a/src/platforms/apple/core/__tests__/devices.test.ts +++ b/src/platforms/apple/core/__tests__/devices.test.ts @@ -237,7 +237,7 @@ test('parseXctracePhysicalAppleDevices parses the parenthesized physical device '== Devices ==', 'iPhone 8 Plus (16.7.16) (00008020-001C2D2234567890)', 'Studio iPad Pro (17.0) (ipad-udid-1)', - 'Living Room Apple TV (16.0) [tv-udid-1]', + 'Living Room Apple TV (16.0) (tv-udid-1)', ].join('\n'), ); @@ -272,6 +272,33 @@ test('parseXctracePhysicalAppleDevices parses the parenthesized physical device ]); }); +test('parseXctracePhysicalAppleDevices preserves parentheses in bracket-format names', () => { + const parsed = parseXctracePhysicalAppleDevices( + ['== Devices ==', "Alex's (iPhone) [alex-udid]", 'Office iPhone (2) [office-udid]'].join('\n'), + ); + + assert.deepEqual(parsed, [ + { + platform: 'apple', + id: 'alex-udid', + name: "Alex's (iPhone)", + kind: 'device', + target: 'mobile', + appleOs: 'ios', + booted: true, + }, + { + platform: 'apple', + id: 'office-udid', + name: 'Office iPhone (2)', + kind: 'device', + target: 'mobile', + appleOs: 'ios', + booted: true, + }, + ]); +}); + test('listAppleDevices tags devicectl iPad product types as iPadOS', async () => { mockRunCommand = async (_cmd, args) => { if (args.join(' ') === 'simctl list devices -j') { diff --git a/src/platforms/apple/core/devices.ts b/src/platforms/apple/core/devices.ts index 23a00ab48..0368d30d2 100644 --- a/src/platforms/apple/core/devices.ts +++ b/src/platforms/apple/core/devices.ts @@ -59,7 +59,7 @@ type IosDeviceDiscoveryOptions = { const XCTRACE_SECTION_HEADER_PATTERN = /^==\s*(.+?)\s*==$/; const XCTRACE_DEVICE_LINE_PATTERN = - /^(?.+?)(?:\s+\((?[^)]+)\))?\s+(?:\[(?[^[\]]+)\]|\((?[^)]+)\))\s*$/; + /^(?.+?)\s+(?:\[(?[^[\]]+)\]|\((?[^)]+)\)\s+\((?[^)]+)\))\s*$/; function normalizeAppleDescriptor(value: string | undefined): string { return (value ?? '').trim().toLowerCase(); From c165b68616305a58aa285c57a226109df9e9612e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Wed, 22 Jul 2026 14:04:20 +0000 Subject: [PATCH 4/4] test(apple): add route-specific xctrace fallback test When devicectl reports no devices, listAppleDevices must source the physical device from the parenthesized xctrace output. Addresses review feedback in #1360. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../apple/core/__tests__/devices.test.ts | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/src/platforms/apple/core/__tests__/devices.test.ts b/src/platforms/apple/core/__tests__/devices.test.ts index 7c96c59cd..37493d411 100644 --- a/src/platforms/apple/core/__tests__/devices.test.ts +++ b/src/platforms/apple/core/__tests__/devices.test.ts @@ -567,6 +567,43 @@ test('listAppleDevices prefers devicectl metadata when xctrace reports the same assert.equal(physicalDevices[0]?.name, 'Primary Name'); }); +test('listAppleDevices falls back to xctrace parenthesized devices when devicectl reports none', async () => { + mockRunCommand = async (_cmd, args) => { + if (args.join(' ') === 'simctl list devices -j') { + return { stdout: createSimctlDevicesPayload(), stderr: '', exitCode: 0 }; + } + + if (args[0] === 'devicectl' && args[1] === 'list' && args[2] === 'devices') { + const jsonPath = String(args[4]); + await fs.writeFile(jsonPath, JSON.stringify({ result: { devices: [] } }), 'utf8'); + return { stdout: '', stderr: '', exitCode: 0 }; + } + + if (args.join(' ') === 'xctrace list devices') { + return { + stdout: ['== Devices ==', 'iPhone 8 Plus (16.7.16) (00008020-001C2D2234567890)'].join('\n'), + stderr: '', + exitCode: 0, + }; + } + + throw new Error(`unexpected xcrun args: ${args.join(' ')}`); + }; + + const devices = await withMockedPlatform( + 'darwin', + async () => await withMockedAppleTools(async () => await listAppleDevices()), + ); + + const physicalDevices = devices.filter( + (device) => device.kind === 'device' && isIosFamily(device), + ); + + assert.equal(physicalDevices.length, 1); + assert.equal(physicalDevices[0]?.id, '00008020-001C2D2234567890'); + assert.equal(physicalDevices[0]?.name, 'iPhone 8 Plus'); +}); + test('listAppleDevices keeps physical discovery disabled for simulator-set scoped runs', async () => { mockRunCommand = async (_cmd, args) => { if (args.includes('simctl') && args.includes('list') && args.includes('devices')) {