-
-
Notifications
You must be signed in to change notification settings - Fork 314
feat: support iOS Simulator launch environment #3000
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,7 +4,7 @@ import { | |
| LAUNCH_CONSOLE_IOS_SIMULATOR_ONLY_MESSAGE, | ||
| } from './launch-console.ts'; | ||
| import type { DeviceInfo } from '@agent-device/kernel/device'; | ||
| import { isIosFamily } from '@agent-device/kernel/device'; | ||
| import { isIosFamily, resolveDeviceAppleOs } from '@agent-device/kernel/device'; | ||
| import { AppError } from '@agent-device/kernel/errors'; | ||
| import type { Interactor, RunnerContext } from './interactor-types.ts'; | ||
| import type { | ||
|
|
@@ -181,6 +181,9 @@ async function invokeDeviceOpen(params: DirectOpenParameters): Promise<void> { | |
| if (params.execution.launchArgs && params.execution.launchArgs.length > 0) { | ||
| throw new AppError('INVALID_ARGS', '--launch-args requires an app target'); | ||
| } | ||
| if (params.execution.launchEnvironment !== undefined) { | ||
| throw new AppError('INVALID_ARGS', '--launch-env requires an app target'); | ||
| } | ||
| await params.interactor.openDevice(); | ||
| } | ||
|
|
||
|
|
@@ -194,6 +197,22 @@ function assertOpenDeviceSupport( | |
| if (device.platform === 'linux' && execution.launchArgs && execution.launchArgs.length > 0) { | ||
| throw new AppError('UNSUPPORTED_OPERATION', '--launch-args is not supported on Linux.'); | ||
| } | ||
| assertLaunchEnvironmentSupport(device, execution.launchEnvironment); | ||
| } | ||
|
|
||
| function assertLaunchEnvironmentSupport( | ||
| device: DeviceInfo, | ||
| launchEnvironment: ApplicationLifecycleExecution['launchEnvironment'], | ||
| ): void { | ||
| if ( | ||
| launchEnvironment !== undefined && | ||
| !(resolveDeviceAppleOs(device) === 'ios' && device.kind === 'simulator') | ||
| ) { | ||
| throw new AppError( | ||
| 'UNSUPPORTED_OPERATION', | ||
| '--launch-env is supported only for iOS Simulator app launches.', | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| async function invokeApplicationUrlOpen( | ||
|
|
@@ -217,6 +236,7 @@ async function invokeApplicationUrlOpen( | |
| activity: params.execution.activity, | ||
| appBundleId: params.appBundleId, | ||
| launchArgs: params.execution.launchArgs ? [...params.execution.launchArgs] : undefined, | ||
| launchEnvironment: params.execution.launchEnvironment, | ||
| terminateRunningApp: params.terminateRunningApp, | ||
| url, | ||
| }); | ||
|
|
@@ -244,6 +264,7 @@ async function invokeApplicationTargetOpen( | |
| appBundleId: params.appBundleId, | ||
| launchConsole: execution.launchConsole, | ||
| launchArgs: execution.launchArgs ? [...execution.launchArgs] : undefined, | ||
| launchEnvironment: execution.launchEnvironment, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: This forwards Prompt for AI agents |
||
| terminateRunningApp: params.terminateRunningApp, | ||
| }); | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export type LaunchEnvironment = Readonly<Record<string, string>>; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| import { expect, test } from 'vitest'; | ||
| import { redactDiagnosticData } from './redaction.ts'; | ||
|
|
||
| test('redacts launch environment maps and CLI entries from structured diagnostics', () => { | ||
| const secret = 'https://example.com/private-clip?nonce=secret-value'; | ||
| const redacted = redactDiagnosticData({ | ||
| launchEnvironment: { _XCAppClipURL: secret }, | ||
| launchEnvironmentEntries: [`_XCAppClipURL=${secret}`], | ||
| safe: 'visible', | ||
| }); | ||
|
|
||
| expect(redacted).toEqual({ | ||
| launchEnvironment: '[REDACTED]', | ||
| launchEnvironmentEntries: '[REDACTED]', | ||
| safe: 'visible', | ||
| }); | ||
| expect(JSON.stringify(redacted)).not.toContain('secret-value'); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,5 @@ | ||
| const SENSITIVE_KEY_RE = | ||
| /(token|secret|password|authorization|cookie|api[_-]?key|access[_-]?key|private[_-]?key|user[_-]?code|device[_-]?code|refresh[_-]?credential)/i; | ||
| /(token|secret|password|authorization|cookie|api[_-]?key|access[_-]?key|private[_-]?key|user[_-]?code|device[_-]?code|refresh[_-]?credential|launch[_-]?environment)/i; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: Only SENSITIVE_KEY_RE was extended; SENSITIVE_ASSIGNMENT_RE (free-text Prompt for AI agentsThere was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P3: The key-level match discards the entire Prompt for AI agents |
||
| const SECRET_TOKEN_RE = | ||
| /\b(?:bearer\s+[a-z0-9._-]+|adc_(?:agent|live|refresh|cli)_[a-z0-9._-]+)\b/gi; | ||
| const SENSITIVE_ASSIGNMENT_RE = | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -340,6 +340,28 @@ test('openIosApp emits a clean simctl launch when launchArgs is an empty array', | |
| ); | ||
| }); | ||
|
|
||
| test('openIosApp translates launch environment keys for the iOS simulator child process', async () => { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P3: The new launchEnvironment tests only cover the plain launch path. Two other code paths in openIosApp also carry the env patch and are untested: Prompt for AI agents |
||
| mockEnsureBootedSimulator.mockResolvedValue(); | ||
| mockRunCmd.mockResolvedValue({ stdout: '', stderr: '', exitCode: 0 }); | ||
|
|
||
| await openIosApp(IOS_TEST_SIMULATOR, 'MyApp', { | ||
| appBundleId: 'com.example.app', | ||
| launchArgs: ['-FeatureFlag', 'YES'], | ||
| launchEnvironment: { | ||
| _XCAppClipURL: 'https://example.com/clip?id=42', | ||
| MODE: 'test', | ||
| }, | ||
| }); | ||
|
|
||
| assert.equal(mockRunCmd.mock.calls.length, 1); | ||
| const [command, args, options] = mockRunCmd.mock.calls[0] ?? []; | ||
| assert.equal(command, 'xcrun'); | ||
| assert.deepEqual(args, ['simctl', 'launch', 'sim-1', 'com.example.app', '-FeatureFlag', 'YES']); | ||
| assert.equal(options?.envPatch?.SIMCTL_CHILD__XCAppClipURL, 'https://example.com/clip?id=42'); | ||
| assert.equal(options?.envPatch?.SIMCTL_CHILD_MODE, 'test'); | ||
| assert.equal(options?.envPatch?._XCAppClipURL, undefined); | ||
| }); | ||
|
|
||
| test('openIosApp appends launchArgs after the bundle id on iOS device', async () => { | ||
| await withFakeAppleTool( | ||
| () => '', | ||
|
|
@@ -472,6 +494,37 @@ test('openIosApp rejects launchArgs combined with bare URL deep link on iOS simu | |
| ); | ||
| }); | ||
|
|
||
| test('openIosApp rejects launchEnvironment combined with bare URL deep link', async () => { | ||
| mockEnsureBootedSimulator.mockResolvedValue(); | ||
| await assertRejectsAppError( | ||
| () => | ||
| openIosApp(IOS_TEST_SIMULATOR, 'myapp://item/42', { | ||
| launchEnvironment: { MODE: 'test' }, | ||
| }), | ||
| { code: 'INVALID_ARGS', message: /simctl openurl/ }, | ||
| ); | ||
| }); | ||
|
|
||
| test('openIosApp rejects launchEnvironment on a physical iOS device', async () => { | ||
| await assertRejectsAppError( | ||
| () => | ||
| openIosApp(IOS_TEST_DEVICE, 'MyApp', { | ||
| launchEnvironment: { MODE: 'test' }, | ||
| }), | ||
| { code: 'UNSUPPORTED_OPERATION', message: /iOS Simulator/ }, | ||
| ); | ||
| }); | ||
|
|
||
| test('openIosApp rejects launchEnvironment on macOS', async () => { | ||
| await assertRejectsAppError( | ||
| () => | ||
| openIosApp(MACOS_TEST_DEVICE, 'TextEdit', { | ||
| launchEnvironment: { MODE: 'test' }, | ||
| }), | ||
| { code: 'UNSUPPORTED_OPERATION', message: /iOS Simulator/ }, | ||
| ); | ||
| }); | ||
|
|
||
| test('openIosApp rejects launchArgs on macOS', async () => { | ||
| await assertRejectsAppError( | ||
| () => | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P2:
assertLaunchEnvironmentSupportgates onresolveDeviceAppleOs(device) === 'ios', which excludes iPadOS simulators (appleOs: 'ipados'). iPad Simulator launches are iOS Simulator app launches — discovery classifies iPad models as'ipados'(inventory-classification.ts:62) andopenIosAppaccepts anykind === 'simulator'— so--launch-envis rejected with "supported only for iOS Simulator" on exactly the devices the platform layer supports. UseisIosFamily(device) && device.kind === 'simulator'to match thelaunchConsolegate (alsoresolveDeviceAppleOsfalls back to'ios'for non-Apple legacy records, making the check both too narrow and accidentally permissive).Prompt for AI agents