From d056820e6b1a7b256e24276edcc0934148272990 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Fri, 13 Feb 2026 13:26:02 +0100 Subject: [PATCH 01/14] ci: add manual iOS runner prebuild cache for iOS workflow --- .github/workflows/ios-runner-prebuild.yml | 127 ++++++++++++++++++++++ .github/workflows/ios.yml | 48 ++++---- 2 files changed, 155 insertions(+), 20 deletions(-) create mode 100644 .github/workflows/ios-runner-prebuild.yml diff --git a/.github/workflows/ios-runner-prebuild.yml b/.github/workflows/ios-runner-prebuild.yml new file mode 100644 index 000000000..d95c07e53 --- /dev/null +++ b/.github/workflows/ios-runner-prebuild.yml @@ -0,0 +1,127 @@ +name: iOS Runner Prebuild (CI-only) + +on: + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ci-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + prebuild-ios-runner: + name: Build iOS 26.2 Prebuilt + runs-on: macos-26 + timeout-minutes: 60 + env: + IOS_RUNTIME_VERSION: '26.2' + IOS_DEVICE_NAME: 'iPhone 17 Pro' + PREBUILT_DIR: ${{ runner.temp }}/ios-runner-prebuilt + DERIVED_DATA_PATH: ${{ runner.temp }}/ios-runner-derived + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Setup toolchain + uses: ./.github/actions/setup-node-pnpm + + - name: Resolve Xcode cache key + id: xcode + run: | + set -euo pipefail + XCODE_VERSION="$(xcodebuild -version | tr '\n' ' ' | sed -E 's/[[:space:]]+/ /g; s/[[:space:]]$//')" + XCODE_KEY="$(echo "$XCODE_VERSION" | tr ' ' '-' | tr -cd '[:alnum:]._-')" + echo "version=$XCODE_VERSION" >> "$GITHUB_OUTPUT" + echo "key=$XCODE_KEY" >> "$GITHUB_OUTPUT" + + - name: Restore prebuilt cache + id: restore-prebuilt + uses: actions/cache/restore@v4 + with: + path: ${{ env.PREBUILT_DIR }} + key: ios-runner-prebuilt-${{ steps.xcode.outputs.key }}-ios-${{ env.IOS_RUNTIME_VERSION }} + + - name: Resolve iOS runtime 26.2 + if: steps.restore-prebuilt.outputs.cache-hit != 'true' + id: runtime + run: | + set -euo pipefail + RUNTIME_ID="$( + xcrun simctl list runtimes -j | node -e " + const fs = require('node:fs'); + const payload = JSON.parse(fs.readFileSync(0, 'utf8')); + const targetVersion = process.env.IOS_RUNTIME_VERSION; + const runtime = (payload.runtimes ?? []) + .filter((item) => item.isAvailable) + .find((item) => item.platform === 'iOS' && item.version === targetVersion); + if (!runtime?.identifier) process.exit(1); + process.stdout.write(runtime.identifier); + " + )" + + if [ -z "$RUNTIME_ID" ]; then + echo "Could not find available iOS runtime ${IOS_RUNTIME_VERSION}" >&2 + exit 1 + fi + + echo "runtime_id=$RUNTIME_ID" >> "$GITHUB_OUTPUT" + + - name: Create simulator for runtime + if: steps.restore-prebuilt.outputs.cache-hit != 'true' + id: simulator + run: | + set -euo pipefail + UDID="$(xcrun simctl create "agent-device-prebuild-${GITHUB_RUN_ID}" "$IOS_DEVICE_NAME" "${{ steps.runtime.outputs.runtime_id }}")" + echo "udid=$UDID" >> "$GITHUB_OUTPUT" + xcrun simctl boot "$UDID" || true + + - name: Build ios-runner for testing + if: steps.restore-prebuilt.outputs.cache-hit != 'true' + run: | + set -euo pipefail + rm -rf "$DERIVED_DATA_PATH" "$PREBUILT_DIR" + xcodebuild build-for-testing \ + -project ios-runner/AgentDeviceRunner/AgentDeviceRunner.xcodeproj \ + -scheme AgentDeviceRunner \ + -destination "id=${{ steps.simulator.outputs.udid }}" \ + -derivedDataPath "$DERIVED_DATA_PATH" + + - name: Package prebuilt artifact + if: steps.restore-prebuilt.outputs.cache-hit != 'true' + run: | + set -euo pipefail + mkdir -p "$PREBUILT_DIR" + cp -R "$DERIVED_DATA_PATH" "$PREBUILT_DIR/derived-data" + cat > "$PREBUILT_DIR/metadata.txt" <> "$GITHUB_OUTPUT" + + - name: Restore iOS runner prebuilt cache + id: restore-prebuilt + uses: actions/cache/restore@v4 + with: + path: ${{ env.PREBUILT_DIR }} + key: ios-runner-prebuilt-${{ steps.xcode.outputs.key }}-ios-${{ env.IOS_RUNTIME_VERSION }} + + - name: Hydrate derived data from prebuilt cache + if: steps.restore-prebuilt.outputs.cache-hit == 'true' + run: | + set -euo pipefail + mkdir -p "$HOME/.agent-device/ios-runner" + rm -rf "$HOME/.agent-device/ios-runner/derived" + cp -R "$PREBUILT_DIR/derived-data" "$HOME/.agent-device/ios-runner/derived" + - name: Resolve agent-device home id: ios-agent-home run: echo "dir=$HOME/.agent-device" >> "$GITHUB_OUTPUT" - - name: Select and start iOS simulator - run: | - UDID="$( - xcrun simctl list devices -j | node -e " - const fs = require('node:fs'); - const payload = JSON.parse(fs.readFileSync(0, 'utf8')); - const all = Object.values(payload.devices ?? {}).flat(); - const available = all.filter((d) => d.isAvailable); - const preferred = - available.find((d) => d.state === 'Booted') ?? - available.find((d) => d.name === 'iPhone 17 Pro') ?? - available[0]; - if (!preferred?.udid) process.exit(1); - process.stdout.write(preferred.udid); - " - )" - xcrun simctl boot "$UDID" || true - echo "IOS_UDID=$UDID" >> "$GITHUB_ENV" - - name: Build iOS integration artifacts + if: steps.restore-prebuilt.outputs.cache-hit != 'true' run: pnpm build:xcuitest - name: Boot preflight via agent-device run: | set -euo pipefail - node --experimental-strip-types src/bin.ts boot --platform ios --udid "$IOS_UDID" --json + node --experimental-strip-types src/bin.ts boot --platform ios --device "iPhone 17 Pro" --json env: AGENT_DEVICE_IOS_BOOT_TIMEOUT_MS: "180000" AGENT_DEVICE_RETRY_LOGS: "1" From db0110ae730d567a57192ac4e65cc6918a48bf8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Fri, 13 Feb 2026 13:28:28 +0100 Subject: [PATCH 02/14] ci: simplify manual iOS prebuild workflow --- .github/workflows/ios-runner-prebuild.yml | 46 +---------------------- 1 file changed, 2 insertions(+), 44 deletions(-) diff --git a/.github/workflows/ios-runner-prebuild.yml b/.github/workflows/ios-runner-prebuild.yml index d95c07e53..b6b45761a 100644 --- a/.github/workflows/ios-runner-prebuild.yml +++ b/.github/workflows/ios-runner-prebuild.yml @@ -43,40 +43,6 @@ jobs: path: ${{ env.PREBUILT_DIR }} key: ios-runner-prebuilt-${{ steps.xcode.outputs.key }}-ios-${{ env.IOS_RUNTIME_VERSION }} - - name: Resolve iOS runtime 26.2 - if: steps.restore-prebuilt.outputs.cache-hit != 'true' - id: runtime - run: | - set -euo pipefail - RUNTIME_ID="$( - xcrun simctl list runtimes -j | node -e " - const fs = require('node:fs'); - const payload = JSON.parse(fs.readFileSync(0, 'utf8')); - const targetVersion = process.env.IOS_RUNTIME_VERSION; - const runtime = (payload.runtimes ?? []) - .filter((item) => item.isAvailable) - .find((item) => item.platform === 'iOS' && item.version === targetVersion); - if (!runtime?.identifier) process.exit(1); - process.stdout.write(runtime.identifier); - " - )" - - if [ -z "$RUNTIME_ID" ]; then - echo "Could not find available iOS runtime ${IOS_RUNTIME_VERSION}" >&2 - exit 1 - fi - - echo "runtime_id=$RUNTIME_ID" >> "$GITHUB_OUTPUT" - - - name: Create simulator for runtime - if: steps.restore-prebuilt.outputs.cache-hit != 'true' - id: simulator - run: | - set -euo pipefail - UDID="$(xcrun simctl create "agent-device-prebuild-${GITHUB_RUN_ID}" "$IOS_DEVICE_NAME" "${{ steps.runtime.outputs.runtime_id }}")" - echo "udid=$UDID" >> "$GITHUB_OUTPUT" - xcrun simctl boot "$UDID" || true - - name: Build ios-runner for testing if: steps.restore-prebuilt.outputs.cache-hit != 'true' run: | @@ -85,10 +51,10 @@ jobs: xcodebuild build-for-testing \ -project ios-runner/AgentDeviceRunner/AgentDeviceRunner.xcodeproj \ -scheme AgentDeviceRunner \ - -destination "id=${{ steps.simulator.outputs.udid }}" \ + -destination "platform=iOS Simulator,name=${IOS_DEVICE_NAME},OS=${IOS_RUNTIME_VERSION}" \ -derivedDataPath "$DERIVED_DATA_PATH" - - name: Package prebuilt artifact + - name: Package prebuilt cache payload if: steps.restore-prebuilt.outputs.cache-hit != 'true' run: | set -euo pipefail @@ -96,9 +62,7 @@ jobs: cp -R "$DERIVED_DATA_PATH" "$PREBUILT_DIR/derived-data" cat > "$PREBUILT_DIR/metadata.txt" < Date: Fri, 13 Feb 2026 13:39:01 +0100 Subject: [PATCH 03/14] ci: pin cache actions in iOS workflows --- .github/workflows/ios-runner-prebuild.yml | 4 ++-- .github/workflows/ios.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ios-runner-prebuild.yml b/.github/workflows/ios-runner-prebuild.yml index b6b45761a..0cc99c360 100644 --- a/.github/workflows/ios-runner-prebuild.yml +++ b/.github/workflows/ios-runner-prebuild.yml @@ -38,7 +38,7 @@ jobs: - name: Restore prebuilt cache id: restore-prebuilt - uses: actions/cache/restore@v4 + uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.2.3 with: path: ${{ env.PREBUILT_DIR }} key: ios-runner-prebuilt-${{ steps.xcode.outputs.key }}-ios-${{ env.IOS_RUNTIME_VERSION }} @@ -70,7 +70,7 @@ jobs: - name: Save prebuilt cache if: steps.restore-prebuilt.outputs.cache-hit != 'true' - uses: actions/cache/save@v4 + uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.2.3 with: path: ${{ env.PREBUILT_DIR }} key: ios-runner-prebuilt-${{ steps.xcode.outputs.key }}-ios-${{ env.IOS_RUNTIME_VERSION }} diff --git a/.github/workflows/ios.yml b/.github/workflows/ios.yml index 62c62b93b..a9bdd6452 100644 --- a/.github/workflows/ios.yml +++ b/.github/workflows/ios.yml @@ -39,7 +39,7 @@ jobs: - name: Restore iOS runner prebuilt cache id: restore-prebuilt - uses: actions/cache/restore@v4 + uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.2.3 with: path: ${{ env.PREBUILT_DIR }} key: ios-runner-prebuilt-${{ steps.xcode.outputs.key }}-ios-${{ env.IOS_RUNTIME_VERSION }} From fc8aa24feb31c2c734ae2e8c60da07d4f9b36af7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Fri, 13 Feb 2026 13:45:58 +0100 Subject: [PATCH 04/14] ci: fix iOS workflow env paths for parser compatibility --- .github/workflows/ios-runner-prebuild.yml | 4 ++-- .github/workflows/ios.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ios-runner-prebuild.yml b/.github/workflows/ios-runner-prebuild.yml index 0cc99c360..db31244cc 100644 --- a/.github/workflows/ios-runner-prebuild.yml +++ b/.github/workflows/ios-runner-prebuild.yml @@ -18,8 +18,8 @@ jobs: env: IOS_RUNTIME_VERSION: '26.2' IOS_DEVICE_NAME: 'iPhone 17 Pro' - PREBUILT_DIR: ${{ runner.temp }}/ios-runner-prebuilt - DERIVED_DATA_PATH: ${{ runner.temp }}/ios-runner-derived + PREBUILT_DIR: ${{ github.workspace }}/.tmp/ios-runner-prebuilt + DERIVED_DATA_PATH: ${{ github.workspace }}/.tmp/ios-runner-derived steps: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 diff --git a/.github/workflows/ios.yml b/.github/workflows/ios.yml index a9bdd6452..0f628948d 100644 --- a/.github/workflows/ios.yml +++ b/.github/workflows/ios.yml @@ -21,7 +21,7 @@ jobs: continue-on-error: true env: IOS_RUNTIME_VERSION: '26.2' - PREBUILT_DIR: ${{ runner.temp }}/ios-runner-prebuilt + PREBUILT_DIR: ${{ github.workspace }}/.tmp/ios-runner-prebuilt steps: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 From 0a21c6c6c7c0056a8691cec1b4840a48360e802a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Fri, 13 Feb 2026 14:06:03 +0100 Subject: [PATCH 05/14] ci: increase daemon timeout for iOS boot preflight --- .github/workflows/ios.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ios.yml b/.github/workflows/ios.yml index 0f628948d..647ddd959 100644 --- a/.github/workflows/ios.yml +++ b/.github/workflows/ios.yml @@ -65,6 +65,7 @@ jobs: set -euo pipefail node --experimental-strip-types src/bin.ts boot --platform ios --device "iPhone 17 Pro" --json env: + AGENT_DEVICE_DAEMON_TIMEOUT_MS: "300000" AGENT_DEVICE_IOS_BOOT_TIMEOUT_MS: "180000" AGENT_DEVICE_RETRY_LOGS: "1" From 3c5c802fcba877185557f636c7bb8d44214a44e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Fri, 13 Feb 2026 14:42:15 +0100 Subject: [PATCH 06/14] ios: harden simulator state timeout and reuse preflight session --- .github/workflows/ios.yml | 3 ++- src/platforms/ios/index.ts | 7 ++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ios.yml b/.github/workflows/ios.yml index 647ddd959..62e6370ba 100644 --- a/.github/workflows/ios.yml +++ b/.github/workflows/ios.yml @@ -22,6 +22,7 @@ jobs: env: IOS_RUNTIME_VERSION: '26.2' PREBUILT_DIR: ${{ github.workspace }}/.tmp/ios-runner-prebuilt + AGENT_DEVICE_IOS_SIMCTL_LIST_TIMEOUT_MS: "60000" steps: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -63,7 +64,7 @@ jobs: - name: Boot preflight via agent-device run: | set -euo pipefail - node --experimental-strip-types src/bin.ts boot --platform ios --device "iPhone 17 Pro" --json + node --experimental-strip-types src/bin.ts boot --platform ios --device "iPhone 17 Pro" --json --session ios-test env: AGENT_DEVICE_DAEMON_TIMEOUT_MS: "300000" AGENT_DEVICE_IOS_BOOT_TIMEOUT_MS: "180000" diff --git a/src/platforms/ios/index.ts b/src/platforms/ios/index.ts index 9dbf8fd13..ad41b37fc 100644 --- a/src/platforms/ios/index.ts +++ b/src/platforms/ios/index.ts @@ -14,6 +14,11 @@ const IOS_BOOT_TIMEOUT_MS = resolveTimeoutMs( TIMEOUT_PROFILES.ios_boot.totalMs, 5_000, ); +const IOS_SIMCTL_LIST_TIMEOUT_MS = resolveTimeoutMs( + process.env.AGENT_DEVICE_IOS_SIMCTL_LIST_TIMEOUT_MS, + TIMEOUT_PROFILES.ios_boot.operationMs, + 1_000, +); const RETRY_LOGS_ENABLED = isEnvTruthy(process.env.AGENT_DEVICE_RETRY_LOGS); export async function resolveIosApp(device: DeviceInfo, app: string): Promise { @@ -365,7 +370,7 @@ export async function ensureBootedSimulator(device: DeviceInfo): Promise { async function getSimulatorState(udid: string): Promise { const result = await runCmd('xcrun', ['simctl', 'list', 'devices', '-j'], { allowFailure: true, - timeoutMs: TIMEOUT_PROFILES.ios_boot.operationMs, + timeoutMs: IOS_SIMCTL_LIST_TIMEOUT_MS, }); if (result.exitCode !== 0) return null; try { From 62aeed0c89fc056eb8d30265a6a401c08da8e38d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Fri, 13 Feb 2026 14:47:10 +0100 Subject: [PATCH 07/14] ci: remove redundant iOS boot preflight step --- .github/workflows/ios.yml | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ios.yml b/.github/workflows/ios.yml index 62e6370ba..971548384 100644 --- a/.github/workflows/ios.yml +++ b/.github/workflows/ios.yml @@ -23,6 +23,8 @@ jobs: IOS_RUNTIME_VERSION: '26.2' PREBUILT_DIR: ${{ github.workspace }}/.tmp/ios-runner-prebuilt AGENT_DEVICE_IOS_SIMCTL_LIST_TIMEOUT_MS: "60000" + AGENT_DEVICE_DAEMON_TIMEOUT_MS: "300000" + AGENT_DEVICE_IOS_BOOT_TIMEOUT_MS: "180000" steps: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -61,18 +63,7 @@ jobs: if: steps.restore-prebuilt.outputs.cache-hit != 'true' run: pnpm build:xcuitest - - name: Boot preflight via agent-device - run: | - set -euo pipefail - node --experimental-strip-types src/bin.ts boot --platform ios --device "iPhone 17 Pro" --json --session ios-test - env: - AGENT_DEVICE_DAEMON_TIMEOUT_MS: "300000" - AGENT_DEVICE_IOS_BOOT_TIMEOUT_MS: "180000" - AGENT_DEVICE_RETRY_LOGS: "1" - - name: Run iOS integration test - env: - AGENT_DEVICE_DAEMON_TIMEOUT_MS: "300000" run: node --test test/integration/ios.test.ts - name: Upload iOS artifacts From 5659377c5195e6bf7208e571ac7cf2589d5d9ece Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Fri, 13 Feb 2026 15:07:20 +0100 Subject: [PATCH 08/14] ios: retry transient simctl launch failures on simulator --- src/platforms/ios/index.ts | 35 ++++++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/src/platforms/ios/index.ts b/src/platforms/ios/index.ts index ad41b37fc..dec39671d 100644 --- a/src/platforms/ios/index.ts +++ b/src/platforms/ios/index.ts @@ -45,7 +45,28 @@ export async function openIosApp(device: DeviceInfo, app: string): Promise if (device.kind === 'simulator') { await ensureBootedSimulator(device); await runCmd('open', ['-a', 'Simulator'], { allowFailure: true }); - await runCmd('xcrun', ['simctl', 'launch', device.id, bundleId]); + await retryWithPolicy( + async () => { + const result = await runCmd('xcrun', ['simctl', 'launch', device.id, bundleId], { + allowFailure: true, + }); + if (result.exitCode === 0) return; + throw new AppError('COMMAND_FAILED', `xcrun exited with code ${result.exitCode}`, { + cmd: 'xcrun', + args: ['simctl', 'launch', device.id, bundleId], + stdout: result.stdout, + stderr: result.stderr, + exitCode: result.exitCode, + }); + }, + { + maxAttempts: 3, + baseDelayMs: 500, + maxDelayMs: 2000, + jitter: 0.2, + shouldRetry: (error) => isTransientSimulatorLaunchFailure(error), + }, + ); return; } await runCmd('xcrun', [ @@ -213,6 +234,18 @@ function parseSettingState(state: string): boolean { throw new AppError('INVALID_ARGS', `Invalid setting state: ${state}`); } +function isTransientSimulatorLaunchFailure(error: unknown): boolean { + if (!(error instanceof AppError)) return false; + if (error.code !== 'COMMAND_FAILED') return false; + const details = (error.details ?? {}) as { exitCode?: number; stderr?: unknown }; + if (details.exitCode !== 4) return false; + const stderr = String(details.stderr ?? '').toLowerCase(); + return ( + stderr.includes('fbsopenapplicationserviceerrordomain') && + stderr.includes('the request to open') + ); +} + export async function listSimulatorApps( device: DeviceInfo, ): Promise<{ bundleId: string; name: string }[]> { From 09ac46510c65a1704e8cab6da65cec4546af1ea4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Fri, 13 Feb 2026 15:29:56 +0100 Subject: [PATCH 09/14] ci: pin iOS integration test target to resolved 26.2 simulator --- .github/workflows/ios.yml | 21 +++++++++++++++++++++ test/integration/ios.test.ts | 9 ++++++--- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ios.yml b/.github/workflows/ios.yml index 971548384..c45c4774c 100644 --- a/.github/workflows/ios.yml +++ b/.github/workflows/ios.yml @@ -59,6 +59,27 @@ jobs: id: ios-agent-home run: echo "dir=$HOME/.agent-device" >> "$GITHUB_OUTPUT" + - name: Resolve iOS 26.2 simulator UDID + run: | + set -euo pipefail + UDID="$( + xcrun simctl list devices -j | node -e " + const fs = require('node:fs'); + const payload = JSON.parse(fs.readFileSync(0, 'utf8')); + const runtimeKey = 'com.apple.CoreSimulator.SimRuntime.iOS-26-2'; + const devices = payload.devices?.[runtimeKey] ?? []; + const available = devices.filter((d) => d.isAvailable && d.name === 'iPhone 17 Pro'); + const preferred = available.find((d) => d.state === 'Booted') ?? available[0]; + if (!preferred?.udid) process.exit(1); + process.stdout.write(preferred.udid); + " + )" + if [ -z "$UDID" ]; then + echo "Unable to resolve iOS 26.2 iPhone 17 Pro simulator UDID" >&2 + exit 1 + fi + echo "AGENT_DEVICE_IOS_TEST_UDID=$UDID" >> "$GITHUB_ENV" + - name: Build iOS integration artifacts if: steps.restore-prebuilt.outputs.cache-hit != 'true' run: pnpm build:xcuitest diff --git a/test/integration/ios.test.ts b/test/integration/ios.test.ts index 7bc917ee3..08dd08fe5 100644 --- a/test/integration/ios.test.ts +++ b/test/integration/ios.test.ts @@ -4,9 +4,12 @@ import path from 'node:path'; import { createIntegrationTestContext, runCliJson } from './test-helpers.ts'; const session = ['--session', 'ios-test']; +const iosTarget = process.env.AGENT_DEVICE_IOS_TEST_UDID + ? ['--udid', process.env.AGENT_DEVICE_IOS_TEST_UDID] + : ['--platform', 'ios']; test.after(() => { - runCliJson(['close', '--platform', 'ios', '--json', ...session]); + runCliJson(['close', ...iosTarget, '--json', ...session]); }); test('ios settings commands', { skip: shouldSkipIos() }, async () => { @@ -14,11 +17,11 @@ test('ios settings commands', { skip: shouldSkipIos() }, async () => { platform: 'ios', testName: 'ios settings commands', }); - const openArgs = ['open', 'com.apple.Preferences', '--platform', 'ios', '--json', ...session]; + const openArgs = ['open', 'com.apple.Preferences', ...iosTarget, '--json', ...session]; integration.runStep('open settings', openArgs); const outPath = path.resolve('test/screenshots/ios-settings.png'); - const shotArgs = ['screenshot', outPath, '--platform', 'ios', '--json', ...session]; + const shotArgs = ['screenshot', outPath, ...iosTarget, '--json', ...session]; const shot = integration.runStep('screenshot settings', shotArgs); integration.assertResult(existsSync(outPath), 'screenshot file missing', shotArgs, shot, { detail: `expected screenshot file at ${outPath}`, From fef2e6d4afb86ade29ab11e8e6bc5f5ff55533a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Fri, 13 Feb 2026 15:40:47 +0100 Subject: [PATCH 10/14] ci: harden iOS prebuild cache keys and remove derived-data copy path --- .github/workflows/ios-runner-prebuild.yml | 35 ++++++++++------------- .github/workflows/ios.yml | 33 ++++++++++++--------- src/platforms/ios/runner-client.ts | 12 ++++++-- 3 files changed, 45 insertions(+), 35 deletions(-) diff --git a/.github/workflows/ios-runner-prebuild.yml b/.github/workflows/ios-runner-prebuild.yml index db31244cc..cc8d0987a 100644 --- a/.github/workflows/ios-runner-prebuild.yml +++ b/.github/workflows/ios-runner-prebuild.yml @@ -2,6 +2,11 @@ name: iOS Runner Prebuild (CI-only) on: workflow_dispatch: + inputs: + cache_buster: + description: Optional suffix to force a fresh prebuild cache entry + required: false + default: stable permissions: contents: read @@ -18,7 +23,6 @@ jobs: env: IOS_RUNTIME_VERSION: '26.2' IOS_DEVICE_NAME: 'iPhone 17 Pro' - PREBUILT_DIR: ${{ github.workspace }}/.tmp/ios-runner-prebuilt DERIVED_DATA_PATH: ${{ github.workspace }}/.tmp/ios-runner-derived steps: - name: Checkout @@ -36,48 +40,39 @@ jobs: echo "version=$XCODE_VERSION" >> "$GITHUB_OUTPUT" echo "key=$XCODE_KEY" >> "$GITHUB_OUTPUT" + - name: Resolve prebuild source hash + id: source-hash + run: echo "value=${{ hashFiles('ios-runner/**', 'package.json', 'pnpm-lock.yaml') }}" >> "$GITHUB_OUTPUT" + - name: Restore prebuilt cache id: restore-prebuilt uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.2.3 with: - path: ${{ env.PREBUILT_DIR }} - key: ios-runner-prebuilt-${{ steps.xcode.outputs.key }}-ios-${{ env.IOS_RUNTIME_VERSION }} + path: ${{ env.DERIVED_DATA_PATH }} + key: ios-runner-prebuilt-${{ steps.xcode.outputs.key }}-ios-${{ env.IOS_RUNTIME_VERSION }}-${{ steps.source-hash.outputs.value }}-${{ github.event.inputs.cache_buster }} - name: Build ios-runner for testing if: steps.restore-prebuilt.outputs.cache-hit != 'true' run: | set -euo pipefail - rm -rf "$DERIVED_DATA_PATH" "$PREBUILT_DIR" + rm -rf "$DERIVED_DATA_PATH" xcodebuild build-for-testing \ -project ios-runner/AgentDeviceRunner/AgentDeviceRunner.xcodeproj \ -scheme AgentDeviceRunner \ -destination "platform=iOS Simulator,name=${IOS_DEVICE_NAME},OS=${IOS_RUNTIME_VERSION}" \ -derivedDataPath "$DERIVED_DATA_PATH" - - name: Package prebuilt cache payload - if: steps.restore-prebuilt.outputs.cache-hit != 'true' - run: | - set -euo pipefail - mkdir -p "$PREBUILT_DIR" - cp -R "$DERIVED_DATA_PATH" "$PREBUILT_DIR/derived-data" - cat > "$PREBUILT_DIR/metadata.txt" <> "$GITHUB_OUTPUT" + - name: Resolve prebuild source hash + id: source-hash + run: echo "value=${{ hashFiles('ios-runner/**', 'package.json', 'pnpm-lock.yaml') }}" >> "$GITHUB_OUTPUT" + - name: Restore iOS runner prebuilt cache id: restore-prebuilt uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.2.3 with: - path: ${{ env.PREBUILT_DIR }} - key: ios-runner-prebuilt-${{ steps.xcode.outputs.key }}-ios-${{ env.IOS_RUNTIME_VERSION }} - - - name: Hydrate derived data from prebuilt cache - if: steps.restore-prebuilt.outputs.cache-hit == 'true' - run: | - set -euo pipefail - mkdir -p "$HOME/.agent-device/ios-runner" - rm -rf "$HOME/.agent-device/ios-runner/derived" - cp -R "$PREBUILT_DIR/derived-data" "$HOME/.agent-device/ios-runner/derived" + path: ${{ env.DERIVED_DATA_PATH }} + key: ios-runner-prebuilt-${{ steps.xcode.outputs.key }}-ios-${{ env.IOS_RUNTIME_VERSION }}-${{ steps.source-hash.outputs.value }}-stable + restore-keys: | + ios-runner-prebuilt-${{ steps.xcode.outputs.key }}-ios-${{ env.IOS_RUNTIME_VERSION }}-${{ steps.source-hash.outputs.value }}- - name: Resolve agent-device home id: ios-agent-home @@ -66,7 +65,8 @@ jobs: xcrun simctl list devices -j | node -e " const fs = require('node:fs'); const payload = JSON.parse(fs.readFileSync(0, 'utf8')); - const runtimeKey = 'com.apple.CoreSimulator.SimRuntime.iOS-26-2'; + const runtimeVersion = process.env.IOS_RUNTIME_VERSION; + const runtimeKey = 'com.apple.CoreSimulator.SimRuntime.iOS-' + runtimeVersion.replace(/\./g, '-'); const devices = payload.devices?.[runtimeKey] ?? []; const available = devices.filter((d) => d.isAvailable && d.name === 'iPhone 17 Pro'); const preferred = available.find((d) => d.state === 'Booted') ?? available[0]; @@ -82,7 +82,14 @@ jobs: - name: Build iOS integration artifacts if: steps.restore-prebuilt.outputs.cache-hit != 'true' - run: pnpm build:xcuitest + run: | + set -euo pipefail + rm -rf "$DERIVED_DATA_PATH" + xcodebuild build-for-testing \ + -project ios-runner/AgentDeviceRunner/AgentDeviceRunner.xcodeproj \ + -scheme AgentDeviceRunner \ + -destination "platform=iOS Simulator,name=iPhone 17 Pro,OS=${IOS_RUNTIME_VERSION}" \ + -derivedDataPath "$DERIVED_DATA_PATH" - name: Run iOS integration test run: node --test test/integration/ios.test.ts diff --git a/src/platforms/ios/runner-client.ts b/src/platforms/ios/runner-client.ts index 74597d522..6f4e71f64 100644 --- a/src/platforms/ios/runner-client.ts +++ b/src/platforms/ios/runner-client.ts @@ -292,8 +292,7 @@ async function ensureXctestrun( udid: string, options: { verbose?: boolean; logPath?: string; traceLogPath?: string }, ): Promise { - const base = path.join(os.homedir(), '.agent-device', 'ios-runner'); - const derived = path.join(base, 'derived'); + const derived = resolveRunnerDerivedPath(); if (shouldCleanDerived()) { try { fs.rmSync(derived, { recursive: true, force: true }); @@ -354,6 +353,15 @@ async function ensureXctestrun( return built; } +function resolveRunnerDerivedPath(): string { + const override = process.env.AGENT_DEVICE_IOS_RUNNER_DERIVED_PATH?.trim(); + if (override) { + return path.resolve(override); + } + const base = path.join(os.homedir(), '.agent-device', 'ios-runner'); + return path.join(base, 'derived'); +} + function findXctestrun(root: string): string | null { if (!fs.existsSync(root)) return null; const candidates: { path: string; mtimeMs: number }[] = []; From a476c9cf1eaa834f92745401a972227dfb6c3c74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Fri, 13 Feb 2026 16:59:54 +0100 Subject: [PATCH 11/14] ci: simplify iOS workflow caching and drop redundant prebuild workflow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove standalone ios-runner-prebuild.yml (redundant — ios.yml builds on cache miss) - Switch from actions/cache/restore to actions/cache (auto-saves on miss) - Drop restore-keys and -stable suffix (no longer needed without prebuild workflow) - Remove UDID resolution step (test falls back to --platform ios) - Use point-free shouldRetry for isTransientSimulatorLaunchFailure Co-authored-by: Cursor --- .github/workflows/ios-runner-prebuild.yml | 80 ----------------------- .github/workflows/ios.yml | 30 +-------- src/platforms/ios/index.ts | 2 +- 3 files changed, 4 insertions(+), 108 deletions(-) delete mode 100644 .github/workflows/ios-runner-prebuild.yml diff --git a/.github/workflows/ios-runner-prebuild.yml b/.github/workflows/ios-runner-prebuild.yml deleted file mode 100644 index cc8d0987a..000000000 --- a/.github/workflows/ios-runner-prebuild.yml +++ /dev/null @@ -1,80 +0,0 @@ -name: iOS Runner Prebuild (CI-only) - -on: - workflow_dispatch: - inputs: - cache_buster: - description: Optional suffix to force a fresh prebuild cache entry - required: false - default: stable - -permissions: - contents: read - -concurrency: - group: ci-${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - prebuild-ios-runner: - name: Build iOS 26.2 Prebuilt - runs-on: macos-26 - timeout-minutes: 60 - env: - IOS_RUNTIME_VERSION: '26.2' - IOS_DEVICE_NAME: 'iPhone 17 Pro' - DERIVED_DATA_PATH: ${{ github.workspace }}/.tmp/ios-runner-derived - steps: - - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - - name: Setup toolchain - uses: ./.github/actions/setup-node-pnpm - - - name: Resolve Xcode cache key - id: xcode - run: | - set -euo pipefail - XCODE_VERSION="$(xcodebuild -version | tr '\n' ' ' | sed -E 's/[[:space:]]+/ /g; s/[[:space:]]$//')" - XCODE_KEY="$(echo "$XCODE_VERSION" | tr ' ' '-' | tr -cd '[:alnum:]._-')" - echo "version=$XCODE_VERSION" >> "$GITHUB_OUTPUT" - echo "key=$XCODE_KEY" >> "$GITHUB_OUTPUT" - - - name: Resolve prebuild source hash - id: source-hash - run: echo "value=${{ hashFiles('ios-runner/**', 'package.json', 'pnpm-lock.yaml') }}" >> "$GITHUB_OUTPUT" - - - name: Restore prebuilt cache - id: restore-prebuilt - uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.2.3 - with: - path: ${{ env.DERIVED_DATA_PATH }} - key: ios-runner-prebuilt-${{ steps.xcode.outputs.key }}-ios-${{ env.IOS_RUNTIME_VERSION }}-${{ steps.source-hash.outputs.value }}-${{ github.event.inputs.cache_buster }} - - - name: Build ios-runner for testing - if: steps.restore-prebuilt.outputs.cache-hit != 'true' - run: | - set -euo pipefail - rm -rf "$DERIVED_DATA_PATH" - xcodebuild build-for-testing \ - -project ios-runner/AgentDeviceRunner/AgentDeviceRunner.xcodeproj \ - -scheme AgentDeviceRunner \ - -destination "platform=iOS Simulator,name=${IOS_DEVICE_NAME},OS=${IOS_RUNTIME_VERSION}" \ - -derivedDataPath "$DERIVED_DATA_PATH" - - - name: Save prebuilt cache - if: steps.restore-prebuilt.outputs.cache-hit != 'true' - uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.2.3 - with: - path: ${{ env.DERIVED_DATA_PATH }} - key: ios-runner-prebuilt-${{ steps.xcode.outputs.key }}-ios-${{ env.IOS_RUNTIME_VERSION }}-${{ steps.source-hash.outputs.value }}-${{ github.event.inputs.cache_buster }} - - - name: Report prebuild cache status - run: | - set -euo pipefail - echo "cache_key=ios-runner-prebuilt-${{ steps.xcode.outputs.key }}-ios-${{ env.IOS_RUNTIME_VERSION }}-${{ steps.source-hash.outputs.value }}-${{ github.event.inputs.cache_buster }}" - if [ "${{ steps.restore-prebuilt.outputs.cache-hit }}" = "true" ]; then - echo "Reused existing prebuild cache." - else - echo "Created/updated prebuild cache." - fi diff --git a/.github/workflows/ios.yml b/.github/workflows/ios.yml index b30f6b207..623a30733 100644 --- a/.github/workflows/ios.yml +++ b/.github/workflows/ios.yml @@ -45,41 +45,17 @@ jobs: id: source-hash run: echo "value=${{ hashFiles('ios-runner/**', 'package.json', 'pnpm-lock.yaml') }}" >> "$GITHUB_OUTPUT" - - name: Restore iOS runner prebuilt cache + - name: Cache iOS runner prebuilt id: restore-prebuilt - uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.2.3 + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.2.3 with: path: ${{ env.DERIVED_DATA_PATH }} - key: ios-runner-prebuilt-${{ steps.xcode.outputs.key }}-ios-${{ env.IOS_RUNTIME_VERSION }}-${{ steps.source-hash.outputs.value }}-stable - restore-keys: | - ios-runner-prebuilt-${{ steps.xcode.outputs.key }}-ios-${{ env.IOS_RUNTIME_VERSION }}-${{ steps.source-hash.outputs.value }}- + key: ios-runner-prebuilt-${{ steps.xcode.outputs.key }}-ios-${{ env.IOS_RUNTIME_VERSION }}-${{ steps.source-hash.outputs.value }} - name: Resolve agent-device home id: ios-agent-home run: echo "dir=$HOME/.agent-device" >> "$GITHUB_OUTPUT" - - name: Resolve iOS 26.2 simulator UDID - run: | - set -euo pipefail - UDID="$( - xcrun simctl list devices -j | node -e " - const fs = require('node:fs'); - const payload = JSON.parse(fs.readFileSync(0, 'utf8')); - const runtimeVersion = process.env.IOS_RUNTIME_VERSION; - const runtimeKey = 'com.apple.CoreSimulator.SimRuntime.iOS-' + runtimeVersion.replace(/\./g, '-'); - const devices = payload.devices?.[runtimeKey] ?? []; - const available = devices.filter((d) => d.isAvailable && d.name === 'iPhone 17 Pro'); - const preferred = available.find((d) => d.state === 'Booted') ?? available[0]; - if (!preferred?.udid) process.exit(1); - process.stdout.write(preferred.udid); - " - )" - if [ -z "$UDID" ]; then - echo "Unable to resolve iOS 26.2 iPhone 17 Pro simulator UDID" >&2 - exit 1 - fi - echo "AGENT_DEVICE_IOS_TEST_UDID=$UDID" >> "$GITHUB_ENV" - - name: Build iOS integration artifacts if: steps.restore-prebuilt.outputs.cache-hit != 'true' run: | diff --git a/src/platforms/ios/index.ts b/src/platforms/ios/index.ts index dec39671d..0860b199e 100644 --- a/src/platforms/ios/index.ts +++ b/src/platforms/ios/index.ts @@ -64,7 +64,7 @@ export async function openIosApp(device: DeviceInfo, app: string): Promise baseDelayMs: 500, maxDelayMs: 2000, jitter: 0.2, - shouldRetry: (error) => isTransientSimulatorLaunchFailure(error), + shouldRetry: isTransientSimulatorLaunchFailure, }, ); return; From 894942a7babb173b5b90ba0e91c3382a9943785c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Fri, 13 Feb 2026 17:43:18 +0100 Subject: [PATCH 12/14] ios: increase simctl launch retry budget for CI simulators MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SpringBoard can take several seconds after boot to accept app launches. Increase from 3 attempts / 500ms–2s to 5 attempts / 1s–5s to give CI simulators enough time. Co-authored-by: Cursor --- src/platforms/ios/index.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/platforms/ios/index.ts b/src/platforms/ios/index.ts index 0860b199e..97d2d95cf 100644 --- a/src/platforms/ios/index.ts +++ b/src/platforms/ios/index.ts @@ -60,9 +60,9 @@ export async function openIosApp(device: DeviceInfo, app: string): Promise }); }, { - maxAttempts: 3, - baseDelayMs: 500, - maxDelayMs: 2000, + maxAttempts: 5, + baseDelayMs: 1_000, + maxDelayMs: 5_000, jitter: 0.2, shouldRetry: isTransientSimulatorLaunchFailure, }, From 3525a3ffe1caca9c3c03d98a724c41af458b11ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Fri, 13 Feb 2026 17:58:11 +0100 Subject: [PATCH 13/14] ios: use deadline-based retry for simulator app launch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The fixed-attempt retry (5 attempts, ~12s window) is too short for CI simulators where SpringBoard needs 30-60s after boot to accept app launches — especially when the build step is cached and provides zero warm-up time. Switch to a deadline-based approach matching ensureBootedSimulator: - Default 30s timeout (configurable via AGENT_DEVICE_IOS_APP_LAUNCH_TIMEOUT_MS) - CI sets 60s to handle cold-boot scenarios - maxAttempts set high (30) so the deadline is the real limit Co-authored-by: Cursor --- .github/workflows/ios.yml | 1 + src/platforms/ios/index.ts | 16 ++++++++++++++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ios.yml b/.github/workflows/ios.yml index 623a30733..c8e49e125 100644 --- a/.github/workflows/ios.yml +++ b/.github/workflows/ios.yml @@ -26,6 +26,7 @@ jobs: AGENT_DEVICE_IOS_SIMCTL_LIST_TIMEOUT_MS: "60000" AGENT_DEVICE_DAEMON_TIMEOUT_MS: "300000" AGENT_DEVICE_IOS_BOOT_TIMEOUT_MS: "180000" + AGENT_DEVICE_IOS_APP_LAUNCH_TIMEOUT_MS: "60000" steps: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 diff --git a/src/platforms/ios/index.ts b/src/platforms/ios/index.ts index 97d2d95cf..5064a17c0 100644 --- a/src/platforms/ios/index.ts +++ b/src/platforms/ios/index.ts @@ -19,6 +19,11 @@ const IOS_SIMCTL_LIST_TIMEOUT_MS = resolveTimeoutMs( TIMEOUT_PROFILES.ios_boot.operationMs, 1_000, ); +const IOS_APP_LAUNCH_TIMEOUT_MS = resolveTimeoutMs( + process.env.AGENT_DEVICE_IOS_APP_LAUNCH_TIMEOUT_MS, + 30_000, + 5_000, +); const RETRY_LOGS_ENABLED = isEnvTruthy(process.env.AGENT_DEVICE_RETRY_LOGS); export async function resolveIosApp(device: DeviceInfo, app: string): Promise { @@ -45,8 +50,14 @@ export async function openIosApp(device: DeviceInfo, app: string): Promise if (device.kind === 'simulator') { await ensureBootedSimulator(device); await runCmd('open', ['-a', 'Simulator'], { allowFailure: true }); + const launchDeadline = Deadline.fromTimeoutMs(IOS_APP_LAUNCH_TIMEOUT_MS); await retryWithPolicy( - async () => { + async ({ deadline: attemptDeadline }) => { + if (attemptDeadline?.isExpired()) { + throw new AppError('COMMAND_FAILED', 'App launch deadline exceeded', { + timeoutMs: IOS_APP_LAUNCH_TIMEOUT_MS, + }); + } const result = await runCmd('xcrun', ['simctl', 'launch', device.id, bundleId], { allowFailure: true, }); @@ -60,12 +71,13 @@ export async function openIosApp(device: DeviceInfo, app: string): Promise }); }, { - maxAttempts: 5, + maxAttempts: 30, baseDelayMs: 1_000, maxDelayMs: 5_000, jitter: 0.2, shouldRetry: isTransientSimulatorLaunchFailure, }, + { deadline: launchDeadline }, ); return; } From 4da8dd006c571d76e0b933625dc9747974aa4cd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Fri, 13 Feb 2026 18:33:36 +0100 Subject: [PATCH 14/14] ci(ios): keep tests platform-based and stabilize simulator selection --- .github/workflows/ios.yml | 32 ++++++++++++++++++++++++++++++++ test/integration/ios.test.ts | 4 +--- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ios.yml b/.github/workflows/ios.yml index c8e49e125..3893243b0 100644 --- a/.github/workflows/ios.yml +++ b/.github/workflows/ios.yml @@ -68,6 +68,38 @@ jobs: -destination "platform=iOS Simulator,name=iPhone 17 Pro,OS=${IOS_RUNTIME_VERSION}" \ -derivedDataPath "$DERIVED_DATA_PATH" + - name: Resolve and boot iOS test simulator + run: | + set -euo pipefail + RUNTIME_TOKEN="SimRuntime.iOS-${IOS_RUNTIME_VERSION//./-}" + export RUNTIME_TOKEN + UDID="$( + xcrun simctl list devices -j | node -e ' + const fs = require("node:fs"); + const runtimeToken = process.env.RUNTIME_TOKEN; + const payload = JSON.parse(fs.readFileSync(0, "utf8")); + const entries = Object.entries(payload.devices ?? {}); + const iosRuntimes = entries.filter(([runtime]) => runtime.includes("SimRuntime.iOS-")); + const runtimeMatches = iosRuntimes.filter(([runtime]) => runtime.includes(runtimeToken)); + const pool = runtimeMatches.length > 0 ? runtimeMatches : iosRuntimes; + const available = pool.flatMap(([runtime, devices]) => + (devices ?? []) + .filter((device) => device.isAvailable) + .map((device) => ({ ...device, runtime })), + ); + const preferred = + available.find((device) => device.state === "Booted" && device.name === "iPhone 17 Pro") ?? + available.find((device) => device.name === "iPhone 17 Pro") ?? + available.find((device) => device.state === "Booted") ?? + available[0]; + if (!preferred?.udid) process.exit(1); + process.stdout.write(preferred.udid); + ' + )" + xcrun simctl shutdown all || true + xcrun simctl boot "$UDID" || true + xcrun simctl bootstatus "$UDID" -b + - name: Run iOS integration test run: node --test test/integration/ios.test.ts diff --git a/test/integration/ios.test.ts b/test/integration/ios.test.ts index 08dd08fe5..67feb716a 100644 --- a/test/integration/ios.test.ts +++ b/test/integration/ios.test.ts @@ -4,9 +4,7 @@ import path from 'node:path'; import { createIntegrationTestContext, runCliJson } from './test-helpers.ts'; const session = ['--session', 'ios-test']; -const iosTarget = process.env.AGENT_DEVICE_IOS_TEST_UDID - ? ['--udid', process.env.AGENT_DEVICE_IOS_TEST_UDID] - : ['--platform', 'ios']; +const iosTarget = ['--platform', 'ios']; test.after(() => { runCliJson(['close', ...iosTarget, '--json', ...session]);