From ed36a8a3b23fa851cce8f80dfc8d7afc43917910 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Mon, 28 Sep 2026 12:06:12 +0200 Subject: [PATCH 1/2] fix(platform-apple): put --payload-url before the bundle ID on devicectl launch devicectl treats everything after the bundle identifier as the app's own argv, so --payload-url after it was silently delivered to the app as a launch argument instead of being honored as a URL to open. Physical-device `open ` never actually opened the link. Fixes #2998 --- .../src/core/__tests__/apps.test.ts | 8 +-- .../physical-device-coredevice.test.ts | 55 +++++++++++++++++++ .../src/core/physical-device-coredevice.ts | 5 +- 3 files changed, 63 insertions(+), 5 deletions(-) diff --git a/packages/platform-apple/src/core/__tests__/apps.test.ts b/packages/platform-apple/src/core/__tests__/apps.test.ts index 4104358338..16102604bb 100644 --- a/packages/platform-apple/src/core/__tests__/apps.test.ts +++ b/packages/platform-apple/src/core/__tests__/apps.test.ts @@ -260,9 +260,9 @@ test('openIosApp web URL on iOS device without app falls back to Safari', async 'launch', '--device', 'ios-device-1', - 'com.apple.mobilesafari', '--payload-url', 'https://example.com/path', + 'com.apple.mobilesafari', ], ]); }, @@ -290,9 +290,9 @@ test('openIosApp custom scheme on iOS device uses active app context', async () 'launch', '--device', 'ios-device-1', - 'com.example.app', '--payload-url', 'myapp://item/42', + 'com.example.app', ], ]); }, @@ -382,9 +382,9 @@ test('openIosApp appends launchArgs alongside --payload-url for iOS device deep 'launch', '--device', 'ios-device-1', - 'com.example.app', '--payload-url', 'myapp://item/42', + 'com.example.app', '--', '-Tracking', 'NO', @@ -562,9 +562,9 @@ test('openIosApp with app and URL on iOS device launches app bundle with payload 'launch', '--device', 'ios-device-1', - 'com.example.app', '--payload-url', 'myapp://screen/to', + 'com.example.app', ], ]); }, diff --git a/packages/platform-apple/src/core/__tests__/physical-device-coredevice.test.ts b/packages/platform-apple/src/core/__tests__/physical-device-coredevice.test.ts index 368c91cc22..6b9f1ab3d2 100644 --- a/packages/platform-apple/src/core/__tests__/physical-device-coredevice.test.ts +++ b/packages/platform-apple/src/core/__tests__/physical-device-coredevice.test.ts @@ -4,6 +4,7 @@ import path from 'node:path'; import { test } from 'vitest'; import type { DeviceInfo } from '@agent-device/kernel/device'; import { + launchCoreDeviceApp, parseIosDeviceDetailsPayload, readIosDeviceReadiness, resolveIosReadyHint, @@ -15,6 +16,7 @@ import { } from '../devicectl.ts'; import { resolveIosPhysicalDeviceControl } from '../physical-device-control.ts'; import { createLocalAppleToolProvider, withAppleToolProvider } from '../tool-provider.ts'; +import { withFakeAppleTool } from '../../__tests__/fake-apple-tool.ts'; /** * `xcrun devicectl device info details` is the one tool that answers what a device thinks of itself, @@ -29,6 +31,59 @@ const DEVICE_INFO_DETAILS_CAPTURE = JSON.parse( ), ) as unknown; +test('launchCoreDeviceApp puts --payload-url before the bundle ID', async () => { + await withFakeAppleTool( + () => '', + async ({ calls }) => { + await launchCoreDeviceApp(IOS_DEVICE, 'com.example.app', { + payloadUrl: 'myapp://item/42', + }); + assert.deepEqual(calls, [ + [ + 'devicectl', + 'device', + 'process', + 'launch', + '--device', + IOS_DEVICE.id, + '--payload-url', + 'myapp://item/42', + 'com.example.app', + ], + ]); + }, + { device: IOS_DEVICE }, + ); +}); + +test('launchCoreDeviceApp keeps launch args after the bundle ID, separated by --', async () => { + await withFakeAppleTool( + () => '', + async ({ calls }) => { + await launchCoreDeviceApp(IOS_DEVICE, 'com.example.app', { + payloadUrl: 'myapp://item/42', + launchArgs: ['--debug'], + }); + assert.deepEqual(calls, [ + [ + 'devicectl', + 'device', + 'process', + 'launch', + '--device', + IOS_DEVICE.id, + '--payload-url', + 'myapp://item/42', + 'com.example.app', + '--', + '--debug', + ], + ]); + }, + { device: IOS_DEVICE }, + ); +}); + test('parseIosDeviceDetailsPayload reads direct and nested tunnel state', () => { assert.equal( parseIosDeviceDetailsPayload({ diff --git a/packages/platform-apple/src/core/physical-device-coredevice.ts b/packages/platform-apple/src/core/physical-device-coredevice.ts index ec983782cb..7a78065003 100644 --- a/packages/platform-apple/src/core/physical-device-coredevice.ts +++ b/packages/platform-apple/src/core/physical-device-coredevice.ts @@ -21,10 +21,13 @@ export async function launchCoreDeviceApp( bundleId: string, options: { payloadUrl?: string; launchArgs?: string[] } = {}, ): Promise { - const args = ['device', 'process', 'launch', '--device', device.id, bundleId]; + const args = ['device', 'process', 'launch', '--device', device.id]; if (options.payloadUrl) { + // `devicectl` treats everything after the bundle ID as app argv, so + // `--payload-url` must precede it to be honored as a launch option. args.push('--payload-url', options.payloadUrl); } + args.push(bundleId); if (options.launchArgs && options.launchArgs.length > 0) { // `devicectl` uses Swift ArgumentParser; preserve app-owned leading dashes. args.push('--', ...options.launchArgs); From f916874ed6c7ffbeb459295b86f197454a8ae5b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Mon, 28 Sep 2026 12:46:34 +0200 Subject: [PATCH 2/2] test(daemon): update app-event-delivery for --payload-url ordering The devicectl launch fix moved --payload-url ahead of the bundle ID; this test still asserted the old (buggy) argv order. --- src/daemon/app-event-delivery.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/daemon/app-event-delivery.test.ts b/src/daemon/app-event-delivery.test.ts index 9b687d11cf..a3ccd68d85 100644 --- a/src/daemon/app-event-delivery.test.ts +++ b/src/daemon/app-event-delivery.test.ts @@ -201,9 +201,9 @@ test('trigger-app-event supports iOS device path and prefers iOS template', asyn 'launch', '--device', 'ios-device-1', - 'com.example.app', '--payload-url', expectedUrl, + 'com.example.app', ]); } finally { process.env.PATH = previousPath;