From 785f61bd761fe9708f35682b7a73b6f5f59bcdbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Fri, 25 Sep 2026 14:57:07 +0200 Subject: [PATCH] refactor(ios-runner): restore a legacy XCTestDevices redirect before the daemon serves requests The restore now runs inside the configureForDaemonLock startup phase, before the servers open and before daemon.json is published. Its diagnostics are buffered with the other startup diagnostics and written to daemon.log after publication. The migration-specific lifecycle method and its daemon wrapper are deleted. A restore failure is reported as a diagnostic, not thrown. A simulator_set_destination_not_found error now names the Xcode version that this process's runner cache decision already read, and never probes the toolchain; without that value it says "(version unreadable)". Docs say that a symlinked XCTestDevices is not supported and is removed at daemon startup, and that the Xcode version can be unreadable. Tests pin exact failure messages per fixture and cover the real daemon startup path. Captured fixtures use placeholder home, checkout and temp paths. --- .../__tests__/runner-device-set.test.ts | 68 ++++++--- .../runner-early-exit-diagnosis.test.ts | 37 ++--- .../runner-startup-failure-fixtures.ts | 18 +-- .../runner-startup-failure-reasons.test.ts | 17 ++- .../src/runner/runner-cache-metadata.ts | 21 +-- .../src/runner/runner-device-set.ts | 66 ++++++--- src/daemon/platform-owner-lifecycle.ts | 19 ++- .../daemon-runtime-xctest-device-set.test.ts | 136 ++++++++++++------ src/daemon/server/daemon-runtime.ts | 42 ++---- src/platform-runtime-daemon-lifecycle.ts | 4 +- src/platform-runtime-resource-cleanup.ts | 7 +- website/docs/docs/commands.md | 4 +- 12 files changed, 259 insertions(+), 180 deletions(-) diff --git a/packages/platform-apple/src/runner/__tests__/runner-device-set.test.ts b/packages/platform-apple/src/runner/__tests__/runner-device-set.test.ts index 8c8e8efabe..83295749c3 100644 --- a/packages/platform-apple/src/runner/__tests__/runner-device-set.test.ts +++ b/packages/platform-apple/src/runner/__tests__/runner-device-set.test.ts @@ -5,7 +5,6 @@ import { afterEach, beforeEach, test, vi } from 'vitest'; import { resetAllProcessMemosForTests } from '@agent-device/kernel/ttl-memo'; import { IOS_DEVICE, IOS_SIMULATOR, TVOS_SIMULATOR } from './device-fixtures.ts'; import { mkdtempForTestSync } from './tmp-dir.ts'; -import { appleRunnerTestHost } from '../test-host.ts'; import { STUBBED_APPLE_TOOLCHAIN, stubAppleToolchainProbes } from './apple-toolchain-fixtures.ts'; import { isSameRunnerSimulator, @@ -13,6 +12,7 @@ import { runnerSimulatorSetFailureDetails, xcodebuildDestinationArgs, } from '../runner-device-set.ts'; +import { resolveExpectedRunnerCacheMetadata } from '../runner-cache-metadata.ts'; const toolchainProbe = stubAppleToolchainProbes(); beforeEach(resetAllProcessMemosForTests); @@ -54,6 +54,7 @@ test('the default set, a blank set path and a physical device leave the destinat test('a failure reports the scoped set and the selected Xcode only for a scoped-set simulator', () => { const scoped = { ...IOS_SIMULATOR, simulatorSetPath: '/tmp/tenant-a/simulators' }; + resolveExpectedRunnerCacheMetadata(scoped); assert.deepEqual(runnerSimulatorSetFailureDetails(scoped), { simulatorSetPath: '/tmp/tenant-a/simulators', @@ -62,13 +63,13 @@ test('a failure reports the scoped set and the selected Xcode only for a scoped- assert.deepEqual(runnerSimulatorSetFailureDetails(IOS_SIMULATOR), {}); }); -test('a failure whose Xcode cannot be read still names the scoped set, and no Xcode key', () => { - toolchainProbe.mockReturnValue({ exitCode: 1, stdout: '', stderr: 'xcode-select: error' }); - +test('a failure before any cache decision read the Xcode names the scoped set, never probing', () => { + toolchainProbe.mockClear(); assert.deepEqual( runnerSimulatorSetFailureDetails({ ...IOS_SIMULATOR, simulatorSetPath: '/tmp/tenant-a/sims' }), { simulatorSetPath: '/tmp/tenant-a/sims' }, ); + assert.equal(toolchainProbe.mock.calls.length, 0); }); test('one udid in two simulator sets names two simulators', () => { @@ -109,9 +110,8 @@ test('an older redirect is undone: the symlink goes and the host set comes back' const paths = makeLegacyPaths(); fs.mkdirSync(path.join(paths.backupPath, 'HOST-UDID'), { recursive: true }); fs.symlinkSync(paths.scopedSetPath, paths.xctestDeviceSetPath, 'dir'); - const phases = recordDiagnosticPhases(); - restoreLegacyXctestDeviceSetRedirect(paths.xctestDeviceSetPath); + const phases = restoreRecordingPhases(paths); assert.equal(fs.lstatSync(paths.xctestDeviceSetPath).isSymbolicLink(), false); assert.ok(fs.existsSync(path.join(paths.xctestDeviceSetPath, 'HOST-UDID'))); @@ -133,11 +133,22 @@ test('a symlink an older redirect left without a backup is removed, whatever it : path.join(path.dirname(paths.scopedSetPath), 'external-volume'); fs.mkdirSync(linkTarget, { recursive: true }); fs.symlinkSync(linkTarget, paths.xctestDeviceSetPath, 'dir'); + const diagnostics: unknown[] = []; - restoreLegacyXctestDeviceSetRedirect(paths.xctestDeviceSetPath); + restoreLegacyXctestDeviceSetRedirect( + (diagnostic) => diagnostics.push(diagnostic), + paths.xctestDeviceSetPath, + ); assert.equal(fs.lstatSync(paths.xctestDeviceSetPath, { throwIfNoEntry: false }), undefined); assert.ok(fs.existsSync(linkTarget), target); + assert.deepEqual(diagnostics, [ + { + phase: 'ios_runner_legacy_xctest_device_set_link_removed', + resourcePath: paths.xctestDeviceSetPath, + data: { linkTarget }, + }, + ]); } }); @@ -146,7 +157,7 @@ test('a backup never replaces a host set that is already in place', () => { fs.mkdirSync(path.join(paths.xctestDeviceSetPath, 'CURRENT-UDID'), { recursive: true }); fs.mkdirSync(path.join(paths.backupPath, 'OLD-UDID'), { recursive: true }); - restoreLegacyXctestDeviceSetRedirect(paths.xctestDeviceSetPath); + restoreRecordingPhases(paths); assert.ok(fs.existsSync(path.join(paths.xctestDeviceSetPath, 'CURRENT-UDID'))); assert.ok(fs.existsSync(path.join(paths.backupPath, 'OLD-UDID'))); @@ -154,9 +165,8 @@ test('a backup never replaces a host set that is already in place', () => { test('a host with no leftovers is left untouched and reports nothing', () => { const paths = makeLegacyPaths(); - const phases = recordDiagnosticPhases(); - restoreLegacyXctestDeviceSetRedirect(paths.xctestDeviceSetPath); + const phases = restoreRecordingPhases(paths); assert.equal(fs.existsSync(paths.xctestDeviceSetPath), false); assert.equal(fs.existsSync(paths.backupPath), false); @@ -167,13 +177,13 @@ test('a host with no leftovers is left untouched and reports nothing', () => { function withOtherDaemonFinishingBefore( call: 'unlinkSync' | 'renameSync', paths: LegacyRedirect, -): void { +): string[] { const original = fs[call] as (...args: unknown[]) => unknown; vi.spyOn(fs, call).mockImplementationOnce(((...args: unknown[]) => { - restoreLegacyXctestDeviceSetRedirect(paths.xctestDeviceSetPath); + restoreRecordingPhases(paths); return original.apply(fs, args); }) as never); - restoreLegacyXctestDeviceSetRedirect(paths.xctestDeviceSetPath); + return restoreRecordingPhases(paths); } test('a daemon whose unlink or rename the other daemon already made finishes the restore', () => { @@ -182,8 +192,13 @@ test('a daemon whose unlink or rename the other daemon already made finishes the fs.mkdirSync(path.join(paths.backupPath, 'HOST-UDID'), { recursive: true }); fs.symlinkSync(paths.scopedSetPath, paths.xctestDeviceSetPath, 'dir'); - withOtherDaemonFinishingBefore(call, paths); + const phases = withOtherDaemonFinishingBefore(call, paths); + assert.equal( + phases.includes('ios_runner_legacy_xctest_device_set_restore_failed'), + false, + call, + ); assert.equal(fs.lstatSync(paths.xctestDeviceSetPath).isSymbolicLink(), false, call); assert.ok(fs.existsSync(path.join(paths.xctestDeviceSetPath, 'HOST-UDID')), call); assert.equal(fs.existsSync(paths.backupPath), false, call); @@ -192,19 +207,36 @@ test('a daemon whose unlink or rename the other daemon already made finishes the } }); -test('an unlink that fails while the link is still there is reported', () => { +test('an unlink that fails while the link is still there is reported, not thrown', () => { const paths = makeLegacyPaths(); fs.mkdirSync(paths.backupPath); fs.symlinkSync(paths.scopedSetPath, paths.xctestDeviceSetPath, 'dir'); vi.spyOn(fs, 'unlinkSync').mockImplementationOnce(() => { throw Object.assign(new Error('EACCES: permission denied'), { code: 'EACCES' }); }); + const diagnostics: unknown[] = []; + + restoreLegacyXctestDeviceSetRedirect( + (diagnostic) => diagnostics.push(diagnostic), + paths.xctestDeviceSetPath, + ); - assert.throws(() => restoreLegacyXctestDeviceSetRedirect(paths.xctestDeviceSetPath), /EACCES/); + assert.deepEqual(diagnostics, [ + { + phase: 'ios_runner_legacy_xctest_device_set_restore_failed', + resourcePath: paths.xctestDeviceSetPath, + data: { error: 'EACCES: permission denied' }, + }, + ]); + assert.equal(fs.lstatSync(paths.xctestDeviceSetPath).isSymbolicLink(), true); + assert.ok(fs.existsSync(paths.backupPath)); }); -function recordDiagnosticPhases(): string[] { +function restoreRecordingPhases(paths: LegacyRedirect): string[] { const phases: string[] = []; - appleRunnerTestHost.update({ emitDiagnostic: (event) => phases.push(event.phase) }); + restoreLegacyXctestDeviceSetRedirect( + (diagnostic) => phases.push(diagnostic.phase), + paths.xctestDeviceSetPath, + ); return phases; } diff --git a/packages/platform-apple/src/runner/__tests__/runner-early-exit-diagnosis.test.ts b/packages/platform-apple/src/runner/__tests__/runner-early-exit-diagnosis.test.ts index 298473855b..fec5e389c3 100644 --- a/packages/platform-apple/src/runner/__tests__/runner-early-exit-diagnosis.test.ts +++ b/packages/platform-apple/src/runner/__tests__/runner-early-exit-diagnosis.test.ts @@ -7,6 +7,7 @@ import { resetAllProcessMemosForTests } from '@agent-device/kernel/ttl-memo'; import type { ExecBackgroundResult } from '@agent-device/host-kit/command'; import { buildRunnerEarlyExitError } from '../runner-startup-transport.ts'; import { readRunnerLogTail } from '../runner-io.ts'; +import { resolveExpectedRunnerCacheMetadata } from '../runner-cache-metadata.ts'; import type { RunnerSession } from '../runner-session-types.ts'; import { mkdtempForTestSync } from './tmp-dir.ts'; import { STUBBED_APPLE_TOOLCHAIN, stubAppleToolchainProbes } from './apple-toolchain-fixtures.ts'; @@ -177,8 +178,8 @@ test('a session that never probed the device publishes no disk-image claim (#268 const SET_WITHOUT_UDID = CAPTURED_SCOPED_SIMULATOR.setWithoutUdid; -// An external xctestrun: the session carries no build of its own, so the Xcode it names comes from -// the toolchain the host selects. +// An external xctestrun: the session carries no build of its own, so the Xcode it names is the one a +// runner cache decision in this process already read, if any. function simulatorSessionFailingWith(log: string, simulatorSetPath?: string): RunnerSession { return { ...sessionFailingWith(log), @@ -204,28 +205,26 @@ function simulatorSessionFailingWith(log: string, simulatorSetPath?: string): Ru } test('a scoped-set simulator whose destination is missing names its set and the Xcode', async () => { - const error = (await buildRunnerEarlyExitError({ - session: simulatorSessionFailingWith( - CAPTURED_LAUNCH_DESTINATION_NOT_FOUND_OUTPUT, - SET_WITHOUT_UDID, - ), - port: 8100, - })) as AppError; + const session = simulatorSessionFailingWith( + CAPTURED_LAUNCH_DESTINATION_NOT_FOUND_OUTPUT, + SET_WITHOUT_UDID, + ); + resolveExpectedRunnerCacheMetadata(session.device); + + const error = (await buildRunnerEarlyExitError({ session, port: 8100 })) as AppError; assert.equal(error.details?.reason, 'simulator_set_destination_not_found'); assert.match(String(error.details?.hint), /-DVTSimulatorSetLocation/); assert.equal(error.details?.simulatorSetPath, SET_WITHOUT_UDID); assert.equal(error.details?.xcodeVersion, STUBBED_APPLE_TOOLCHAIN.xcodeVersion); - assert.ok( - error.message.endsWith( - `simulator set ${SET_WITHOUT_UDID} with Xcode ${STUBBED_APPLE_TOOLCHAIN.xcodeVersion}`, - ), + assert.equal( + error.message, + `Runner did not accept connection (xcodebuild exited early): xcodebuild found no simulator ${CAPTURED_SCOPED_SIMULATOR.udid} in simulator set ${SET_WITHOUT_UDID} with Xcode ${STUBBED_APPLE_TOOLCHAIN.xcodeVersion}`, ); }); -test('a scoped-set destination error whose Xcode cannot be read still names the set', async () => { - toolchainProbe.mockReturnValue({ exitCode: 1, stdout: '', stderr: 'xcode-select: error' }); - +test('a scoped-set destination error before any cache decision read the Xcode still names the set', async () => { + toolchainProbe.mockClear(); const error = (await buildRunnerEarlyExitError({ session: simulatorSessionFailingWith( CAPTURED_LAUNCH_DESTINATION_NOT_FOUND_OUTPUT, @@ -237,9 +236,11 @@ test('a scoped-set destination error whose Xcode cannot be read still names the assert.equal(error.details?.reason, 'simulator_set_destination_not_found'); assert.equal(error.details?.simulatorSetPath, SET_WITHOUT_UDID); assert.equal('xcodeVersion' in (error.details ?? {}), false); - assert.ok( - error.message.endsWith(`simulator set ${SET_WITHOUT_UDID} with Xcode (version unreadable)`), + assert.equal( + error.message, + `Runner did not accept connection (xcodebuild exited early): xcodebuild found no simulator ${CAPTURED_SCOPED_SIMULATOR.udid} in simulator set ${SET_WITHOUT_UDID} with Xcode (version unreadable)`, ); + assert.equal(toolchainProbe.mock.calls.length, 0); }); test('a default-set simulator early exit keeps its boot-failure reason', async () => { diff --git a/packages/platform-apple/src/runner/__tests__/runner-startup-failure-fixtures.ts b/packages/platform-apple/src/runner/__tests__/runner-startup-failure-fixtures.ts index 484648a495..8c520eceba 100644 --- a/packages/platform-apple/src/runner/__tests__/runner-startup-failure-fixtures.ts +++ b/packages/platform-apple/src/runner/__tests__/runner-startup-failure-fixtures.ts @@ -83,17 +83,17 @@ const CAPTURED_DESTINATION_XCODE_VERSION = 'Xcode 26.2 / Build version 17C52'; /** The scoped-set simulator the destination captures targeted, and a set that does not hold it. */ export const CAPTURED_SCOPED_SIMULATOR = { udid: '9DD70D58-C485-4CE5-8743-5FED48468446', - setWithoutUdid: - '/Users/thymikee/Developer/agent-device/.claude/worktrees/_scratch/2963-simset-empty', + setWithoutUdid: '/Users//simulator-sets/empty', } as const; /** * The runner launch (`test-without-building`) output for {@link CAPTURED_SCOPED_SIMULATOR} with * `-DVTSimulatorSetLocation` naming the set that does not hold it, captured with the launch argv from - * `runner-process-launch.ts` against a cached xctestrun. Host destination lines are left out. + * `runner-process-launch.ts` against a cached xctestrun. Host destination lines are left out, and + * home, checkout and temp paths are placeholders. */ export const CAPTURED_LAUNCH_DESTINATION_NOT_FOUND_OUTPUT = - 'Command line invocation:\n /Applications/Xcode-26.2.0.app/Contents/Developer/usr/bin/xcodebuild test-without-building -only-testing AgentDeviceRunnerUITests/RunnerTests/testCommand -parallel-testing-enabled NO -test-timeouts-enabled NO -collect-test-diagnostics never -maximum-concurrent-test-simulator-destinations 1 -destination-timeout 20 -xctestrun /Users/thymikee/.agent-device/apple-runner/derived/ios-simulator/cache-327953f5ae7efca8/Build/Products/AgentDeviceRunner_AgentDeviceRunnerUITests_iphonesimulator26.2-arm64.xctestrun -derivedDataPath /Users/thymikee/Developer/agent-device/.claude/worktrees/_scratch/2963-capture-derived -destination "platform=iOS Simulator,id=9DD70D58-C485-4CE5-8743-5FED48468446" -DVTSimulatorSetLocation=/Users/thymikee/Developer/agent-device/.claude/worktrees/_scratch/2963-simset-empty\n\n2026-09-25 14:06:21.656 xcodebuild[56376:166317816] [MT] IDERunDestination: Supported platforms for the buildables in the current scheme is empty.\n2026-09-25 14:06:21.753 xcodebuild[56376:166317816] [MT] IDERunDestination: Supported platforms for the buildables in the current scheme is empty.\n2026-09-25 14:06:41.674 xcodebuild[56376:166317816] [MT] IDERunDestination: Supported platforms for the buildables in the current scheme is empty.\n2026-09-25 14:06:41.685 xcodebuild[56376:166317816] Writing error result bundle to /var/folders/65/fz9_2bsj6fzgct46vx226s8c0000gn/T/ResultBundle_2026-25-09_14-06-0041.xcresult\nxcodebuild: error: Unable to find a device matching the provided destination specifier:\n\t\t{ platform:iOS Simulator, id:9DD70D58-C485-4CE5-8743-5FED48468446 }\n\n\tThe requested device could not be found because no available devices matched the request.\n\n\tAvailable destinations for the "AgentDeviceRunner" scheme:\n\t\t{ platform:iOS, id:dvtdevice-DVTiPhonePlaceholder-iphoneos:placeholder, name:Any iOS Device }\n\t\t{ platform:iOS Simulator, id:dvtdevice-DVTiOSDeviceSimulatorPlaceholder-iphonesimulator:placeholder, name:Any iOS Simulator Device }\n\t\t{ platform:macOS, name:Any Mac }\n\t\t{ platform:macOS, variant:Mac Catalyst, name:Any Mac }\n\t\t{ platform:tvOS, id:dvtdevice-DVTiOSDevicePlaceholder-appletvos:placeholder, name:Any tvOS Device }\n\t\t{ platform:tvOS Simulator, id:dvtdevice-DVTiOSDeviceSimulatorPlaceholder-appletvsimulator:placeholder, name:Any tvOS Simulator Device }\n\t\t{ platform:visionOS, id:dvtdevice-DVTiOSDevicePlaceholder-xros:placeholder, name:Any visionOS Device }\n\t\t{ platform:visionOS Simulator, id:dvtdevice-DVTiOSDeviceSimulatorPlaceholder-xrsimulator:placeholder, name:Any visionOS Simulator Device }\n\t\t{ platform:watchOS, id:dvtdevice-DVTiOSDevicePlaceholder-watchos:placeholder, name:Any watchOS Device }\n\t\t{ platform:watchOS Simulator, id:dvtdevice-DVTiOSDeviceSimulatorPlaceholder-watchsimulator:placeholder, name:Any watchOS Simulator Device }\n'; + 'Command line invocation:\n /Applications/Xcode-26.2.0.app/Contents/Developer/usr/bin/xcodebuild test-without-building -only-testing AgentDeviceRunnerUITests/RunnerTests/testCommand -parallel-testing-enabled NO -test-timeouts-enabled NO -collect-test-diagnostics never -maximum-concurrent-test-simulator-destinations 1 -destination-timeout 20 -xctestrun /Users//.agent-device/apple-runner/derived/ios-simulator/cache-327953f5ae7efca8/Build/Products/AgentDeviceRunner_AgentDeviceRunnerUITests_iphonesimulator26.2-arm64.xctestrun -derivedDataPath /Users//runner-derived -destination "platform=iOS Simulator,id=9DD70D58-C485-4CE5-8743-5FED48468446" -DVTSimulatorSetLocation=/Users//simulator-sets/empty\n\n2026-09-25 14:06:21.656 xcodebuild[56376:166317816] [MT] IDERunDestination: Supported platforms for the buildables in the current scheme is empty.\n2026-09-25 14:06:21.753 xcodebuild[56376:166317816] [MT] IDERunDestination: Supported platforms for the buildables in the current scheme is empty.\n2026-09-25 14:06:41.674 xcodebuild[56376:166317816] [MT] IDERunDestination: Supported platforms for the buildables in the current scheme is empty.\n2026-09-25 14:06:41.685 xcodebuild[56376:166317816] Writing error result bundle to /var/folders//T/ResultBundle_2026-25-09_14-06-0041.xcresult\nxcodebuild: error: Unable to find a device matching the provided destination specifier:\n\t\t{ platform:iOS Simulator, id:9DD70D58-C485-4CE5-8743-5FED48468446 }\n\n\tThe requested device could not be found because no available devices matched the request.\n\n\tAvailable destinations for the "AgentDeviceRunner" scheme:\n\t\t{ platform:iOS, id:dvtdevice-DVTiPhonePlaceholder-iphoneos:placeholder, name:Any iOS Device }\n\t\t{ platform:iOS Simulator, id:dvtdevice-DVTiOSDeviceSimulatorPlaceholder-iphonesimulator:placeholder, name:Any iOS Simulator Device }\n\t\t{ platform:macOS, name:Any Mac }\n\t\t{ platform:macOS, variant:Mac Catalyst, name:Any Mac }\n\t\t{ platform:tvOS, id:dvtdevice-DVTiOSDevicePlaceholder-appletvos:placeholder, name:Any tvOS Device }\n\t\t{ platform:tvOS Simulator, id:dvtdevice-DVTiOSDeviceSimulatorPlaceholder-appletvsimulator:placeholder, name:Any tvOS Simulator Device }\n\t\t{ platform:visionOS, id:dvtdevice-DVTiOSDevicePlaceholder-xros:placeholder, name:Any visionOS Device }\n\t\t{ platform:visionOS Simulator, id:dvtdevice-DVTiOSDeviceSimulatorPlaceholder-xrsimulator:placeholder, name:Any visionOS Simulator Device }\n\t\t{ platform:watchOS, id:dvtdevice-DVTiOSDevicePlaceholder-watchos:placeholder, name:Any watchOS Device }\n\t\t{ platform:watchOS Simulator, id:dvtdevice-DVTiOSDeviceSimulatorPlaceholder-watchsimulator:placeholder, name:Any watchOS Simulator Device }\n'; export type RunnerStartupFailureFixture = Readonly<{ /** Stable name for a focused test or a review comment. */ @@ -244,28 +244,28 @@ export const RUNNER_STARTUP_FAILURE_FIXTURES: readonly RunnerStartupFailureFixtu reason: 'simulator_set_destination_not_found', site: 'build-for-testing', command: - '/Applications/Xcode-26.2.0.app/Contents/Developer/usr/bin/xcodebuild build-for-testing -project /Users/thymikee/Developer/agent-device/.claude/worktrees/impl-2935b/apple/runner/AgentDeviceRunner/AgentDeviceRunner.xcodeproj -scheme AgentDeviceRunner -parallel-testing-enabled NO -maximum-concurrent-test-simulator-destinations 1 -destination "platform=iOS Simulator,id=9DD70D58-C485-4CE5-8743-5FED48468446" -DVTSimulatorSetLocation=/Users/thymikee/Developer/agent-device/.claude/worktrees/_scratch/2963-simset-empty -derivedDataPath /Users/thymikee/Developer/agent-device/.claude/worktrees/_scratch/2963-capture-derived COMPILER_INDEX_STORE_ENABLE=NO ENABLE_CODE_COVERAGE=NO ONLY_ACTIVE_ARCH=YES ENABLE_PREVIEWS=NO ENABLE_DEBUG_DYLIB=NO -IDEPackageSupportDisableManifestSandbox=1 -IDEPackageSupportDisablePluginExecutionSandbox=1 ENABLE_USER_SCRIPT_SANDBOXING=NO "OTHER_SWIFT_FLAGS=$(inherited) -disable-sandbox" AGENT_DEVICE_IOS_RUNNER_APP_BUNDLE_ID=com.callstack.agentdevice.runner AGENT_DEVICE_IOS_RUNNER_TEST_BUNDLE_ID=com.callstack.agentdevice.runner.uitests', + '/Applications/Xcode-26.2.0.app/Contents/Developer/usr/bin/xcodebuild build-for-testing -project /Users//agent-device/apple/runner/AgentDeviceRunner/AgentDeviceRunner.xcodeproj -scheme AgentDeviceRunner -parallel-testing-enabled NO -maximum-concurrent-test-simulator-destinations 1 -destination "platform=iOS Simulator,id=9DD70D58-C485-4CE5-8743-5FED48468446" -DVTSimulatorSetLocation=/Users//simulator-sets/empty -derivedDataPath /Users//runner-derived COMPILER_INDEX_STORE_ENABLE=NO ENABLE_CODE_COVERAGE=NO ONLY_ACTIVE_ARCH=YES ENABLE_PREVIEWS=NO ENABLE_DEBUG_DYLIB=NO -IDEPackageSupportDisableManifestSandbox=1 -IDEPackageSupportDisablePluginExecutionSandbox=1 ENABLE_USER_SCRIPT_SANDBOXING=NO "OTHER_SWIFT_FLAGS=$(inherited) -disable-sandbox" AGENT_DEVICE_IOS_RUNNER_APP_BUNDLE_ID=com.callstack.agentdevice.runner AGENT_DEVICE_IOS_RUNNER_TEST_BUNDLE_ID=com.callstack.agentdevice.runner.uitests', xcodeVersion: CAPTURED_DESTINATION_XCODE_VERSION, provenance: 'captured', output: - 'Command line invocation:\n /Applications/Xcode-26.2.0.app/Contents/Developer/usr/bin/xcodebuild build-for-testing -project /Users/thymikee/Developer/agent-device/.claude/worktrees/impl-2935b/apple/runner/AgentDeviceRunner/AgentDeviceRunner.xcodeproj -scheme AgentDeviceRunner -parallel-testing-enabled NO -maximum-concurrent-test-simulator-destinations 1 -destination "platform=iOS Simulator,id=9DD70D58-C485-4CE5-8743-5FED48468446" -DVTSimulatorSetLocation=/Users/thymikee/Developer/agent-device/.claude/worktrees/_scratch/2963-simset-empty -derivedDataPath /Users/thymikee/Developer/agent-device/.claude/worktrees/_scratch/2963-capture-derived COMPILER_INDEX_STORE_ENABLE=NO ENABLE_CODE_COVERAGE=NO ONLY_ACTIVE_ARCH=YES ENABLE_PREVIEWS=NO ENABLE_DEBUG_DYLIB=NO -IDEPackageSupportDisableManifestSandbox=1 -IDEPackageSupportDisablePluginExecutionSandbox=1 ENABLE_USER_SCRIPT_SANDBOXING=NO "OTHER_SWIFT_FLAGS=$(inherited) -disable-sandbox" AGENT_DEVICE_IOS_RUNNER_APP_BUNDLE_ID=com.callstack.agentdevice.runner AGENT_DEVICE_IOS_RUNNER_TEST_BUNDLE_ID=com.callstack.agentdevice.runner.uitests\n\nBuild settings from command line:\n AGENT_DEVICE_IOS_RUNNER_APP_BUNDLE_ID = com.callstack.agentdevice.runner\n AGENT_DEVICE_IOS_RUNNER_TEST_BUNDLE_ID = com.callstack.agentdevice.runner.uitests\n COMPILER_INDEX_STORE_ENABLE = NO\n ENABLE_CODE_COVERAGE = NO\n ENABLE_DEBUG_DYLIB = NO\n ENABLE_PREVIEWS = NO\n ENABLE_USER_SCRIPT_SANDBOXING = NO\n ONLY_ACTIVE_ARCH = YES\n OTHER_SWIFT_FLAGS = $(inherited) -disable-sandbox\n\nResolve Package Graph\n\n\nResolved source packages:\n agent-device-snapshot-presentation: /Users/thymikee/Developer/agent-device/.claude/worktrees/impl-2935b/apple/snapshot-presentation @ local\n\n2026-09-25 14:05:36.652 xcodebuild[34716:166287301] Writing error result bundle to /var/folders/65/fz9_2bsj6fzgct46vx226s8c0000gn/T/ResultBundle_2026-25-09_14-05-0036.xcresult\nxcodebuild: error: Unable to find a device matching the provided destination specifier:\n\t\t{ platform:iOS Simulator, id:9DD70D58-C485-4CE5-8743-5FED48468446 }\n\n\tThe requested device could not be found because no available devices matched the request.\n\n\tAvailable destinations for the "AgentDeviceRunner" scheme:\n\t\t{ platform:iOS, id:dvtdevice-DVTiPhonePlaceholder-iphoneos:placeholder, name:Any iOS Device }\n\t\t{ platform:iOS Simulator, id:dvtdevice-DVTiOSDeviceSimulatorPlaceholder-iphonesimulator:placeholder, name:Any iOS Simulator Device }\n\t\t{ platform:macOS, name:Any Mac }\n\t\t{ platform:tvOS, id:dvtdevice-DVTiOSDevicePlaceholder-appletvos:placeholder, name:Any tvOS Device }\n\t\t{ platform:tvOS Simulator, id:dvtdevice-DVTiOSDeviceSimulatorPlaceholder-appletvsimulator:placeholder, name:Any tvOS Simulator Device }\n\t\t{ platform:visionOS, id:dvtdevice-DVTiOSDevicePlaceholder-xros:placeholder, name:Any visionOS Device }\n\t\t{ platform:visionOS Simulator, id:dvtdevice-DVTiOSDeviceSimulatorPlaceholder-xrsimulator:placeholder, name:Any visionOS Simulator Device }\n', + 'Command line invocation:\n /Applications/Xcode-26.2.0.app/Contents/Developer/usr/bin/xcodebuild build-for-testing -project /Users//agent-device/apple/runner/AgentDeviceRunner/AgentDeviceRunner.xcodeproj -scheme AgentDeviceRunner -parallel-testing-enabled NO -maximum-concurrent-test-simulator-destinations 1 -destination "platform=iOS Simulator,id=9DD70D58-C485-4CE5-8743-5FED48468446" -DVTSimulatorSetLocation=/Users//simulator-sets/empty -derivedDataPath /Users//runner-derived COMPILER_INDEX_STORE_ENABLE=NO ENABLE_CODE_COVERAGE=NO ONLY_ACTIVE_ARCH=YES ENABLE_PREVIEWS=NO ENABLE_DEBUG_DYLIB=NO -IDEPackageSupportDisableManifestSandbox=1 -IDEPackageSupportDisablePluginExecutionSandbox=1 ENABLE_USER_SCRIPT_SANDBOXING=NO "OTHER_SWIFT_FLAGS=$(inherited) -disable-sandbox" AGENT_DEVICE_IOS_RUNNER_APP_BUNDLE_ID=com.callstack.agentdevice.runner AGENT_DEVICE_IOS_RUNNER_TEST_BUNDLE_ID=com.callstack.agentdevice.runner.uitests\n\nBuild settings from command line:\n AGENT_DEVICE_IOS_RUNNER_APP_BUNDLE_ID = com.callstack.agentdevice.runner\n AGENT_DEVICE_IOS_RUNNER_TEST_BUNDLE_ID = com.callstack.agentdevice.runner.uitests\n COMPILER_INDEX_STORE_ENABLE = NO\n ENABLE_CODE_COVERAGE = NO\n ENABLE_DEBUG_DYLIB = NO\n ENABLE_PREVIEWS = NO\n ENABLE_USER_SCRIPT_SANDBOXING = NO\n ONLY_ACTIVE_ARCH = YES\n OTHER_SWIFT_FLAGS = $(inherited) -disable-sandbox\n\nResolve Package Graph\n\n\nResolved source packages:\n agent-device-snapshot-presentation: /Users//agent-device/apple/snapshot-presentation @ local\n\n2026-09-25 14:05:36.652 xcodebuild[34716:166287301] Writing error result bundle to /var/folders//T/ResultBundle_2026-25-09_14-05-0036.xcresult\nxcodebuild: error: Unable to find a device matching the provided destination specifier:\n\t\t{ platform:iOS Simulator, id:9DD70D58-C485-4CE5-8743-5FED48468446 }\n\n\tThe requested device could not be found because no available devices matched the request.\n\n\tAvailable destinations for the "AgentDeviceRunner" scheme:\n\t\t{ platform:iOS, id:dvtdevice-DVTiPhonePlaceholder-iphoneos:placeholder, name:Any iOS Device }\n\t\t{ platform:iOS Simulator, id:dvtdevice-DVTiOSDeviceSimulatorPlaceholder-iphonesimulator:placeholder, name:Any iOS Simulator Device }\n\t\t{ platform:macOS, name:Any Mac }\n\t\t{ platform:tvOS, id:dvtdevice-DVTiOSDevicePlaceholder-appletvos:placeholder, name:Any tvOS Device }\n\t\t{ platform:tvOS Simulator, id:dvtdevice-DVTiOSDeviceSimulatorPlaceholder-appletvsimulator:placeholder, name:Any tvOS Simulator Device }\n\t\t{ platform:visionOS, id:dvtdevice-DVTiOSDevicePlaceholder-xros:placeholder, name:Any visionOS Device }\n\t\t{ platform:visionOS Simulator, id:dvtdevice-DVTiOSDeviceSimulatorPlaceholder-xrsimulator:placeholder, name:Any visionOS Simulator Device }\n', device: { ...IOS_SIMULATOR, id: CAPTURED_SCOPED_SIMULATOR.udid, simulatorSetPath: CAPTURED_SCOPED_SIMULATOR.setWithoutUdid, }, - note: "Captured by running the runner build argv agent-device records in runner.log, with `-DVTSimulatorSetLocation` naming a set that does not hold the udid: what a simulator deleted from its scoped set, or an Xcode that stops honoring the key, leaves xcodebuild to report. Destination lines naming this host's Mac, paired iPhone and default-set simulators are left out; the rest is verbatim.", + note: "Captured by running the runner build argv agent-device records in runner.log, with `-DVTSimulatorSetLocation` naming a set that does not hold the udid: what a simulator deleted from its scoped set, or an Xcode that stops honoring the key, leaves xcodebuild to report. Destination lines naming this host's Mac, paired iPhone and default-set simulators are left out, and home, checkout and temp paths are placeholders; the rest is verbatim.", }, { id: 'default-set-destination-not-found', reason: 'build_failed_unclassified', site: 'build-for-testing', command: - '/Applications/Xcode-26.2.0.app/Contents/Developer/usr/bin/xcodebuild build-for-testing -project /Users/thymikee/Developer/agent-device/.claude/worktrees/impl-2935b/apple/runner/AgentDeviceRunner/AgentDeviceRunner.xcodeproj -scheme AgentDeviceRunner -parallel-testing-enabled NO -maximum-concurrent-test-simulator-destinations 1 -destination "platform=iOS Simulator,id=9DD70D58-C485-4CE5-8743-5FED48468446" -derivedDataPath /Users/thymikee/Developer/agent-device/.claude/worktrees/_scratch/2963-capture-derived COMPILER_INDEX_STORE_ENABLE=NO ENABLE_CODE_COVERAGE=NO ONLY_ACTIVE_ARCH=YES ENABLE_PREVIEWS=NO ENABLE_DEBUG_DYLIB=NO -IDEPackageSupportDisableManifestSandbox=1 -IDEPackageSupportDisablePluginExecutionSandbox=1 ENABLE_USER_SCRIPT_SANDBOXING=NO "OTHER_SWIFT_FLAGS=$(inherited) -disable-sandbox" AGENT_DEVICE_IOS_RUNNER_APP_BUNDLE_ID=com.callstack.agentdevice.runner AGENT_DEVICE_IOS_RUNNER_TEST_BUNDLE_ID=com.callstack.agentdevice.runner.uitests', + '/Applications/Xcode-26.2.0.app/Contents/Developer/usr/bin/xcodebuild build-for-testing -project /Users//agent-device/apple/runner/AgentDeviceRunner/AgentDeviceRunner.xcodeproj -scheme AgentDeviceRunner -parallel-testing-enabled NO -maximum-concurrent-test-simulator-destinations 1 -destination "platform=iOS Simulator,id=9DD70D58-C485-4CE5-8743-5FED48468446" -derivedDataPath /Users//runner-derived COMPILER_INDEX_STORE_ENABLE=NO ENABLE_CODE_COVERAGE=NO ONLY_ACTIVE_ARCH=YES ENABLE_PREVIEWS=NO ENABLE_DEBUG_DYLIB=NO -IDEPackageSupportDisableManifestSandbox=1 -IDEPackageSupportDisablePluginExecutionSandbox=1 ENABLE_USER_SCRIPT_SANDBOXING=NO "OTHER_SWIFT_FLAGS=$(inherited) -disable-sandbox" AGENT_DEVICE_IOS_RUNNER_APP_BUNDLE_ID=com.callstack.agentdevice.runner AGENT_DEVICE_IOS_RUNNER_TEST_BUNDLE_ID=com.callstack.agentdevice.runner.uitests', xcodeVersion: CAPTURED_DESTINATION_XCODE_VERSION, provenance: 'captured', output: - 'Command line invocation:\n /Applications/Xcode-26.2.0.app/Contents/Developer/usr/bin/xcodebuild build-for-testing -project /Users/thymikee/Developer/agent-device/.claude/worktrees/impl-2935b/apple/runner/AgentDeviceRunner/AgentDeviceRunner.xcodeproj -scheme AgentDeviceRunner -parallel-testing-enabled NO -maximum-concurrent-test-simulator-destinations 1 -destination "platform=iOS Simulator,id=9DD70D58-C485-4CE5-8743-5FED48468446" -derivedDataPath /Users/thymikee/Developer/agent-device/.claude/worktrees/_scratch/2963-capture-derived COMPILER_INDEX_STORE_ENABLE=NO ENABLE_CODE_COVERAGE=NO ONLY_ACTIVE_ARCH=YES ENABLE_PREVIEWS=NO ENABLE_DEBUG_DYLIB=NO -IDEPackageSupportDisableManifestSandbox=1 -IDEPackageSupportDisablePluginExecutionSandbox=1 ENABLE_USER_SCRIPT_SANDBOXING=NO "OTHER_SWIFT_FLAGS=$(inherited) -disable-sandbox" AGENT_DEVICE_IOS_RUNNER_APP_BUNDLE_ID=com.callstack.agentdevice.runner AGENT_DEVICE_IOS_RUNNER_TEST_BUNDLE_ID=com.callstack.agentdevice.runner.uitests\n\nBuild settings from command line:\n AGENT_DEVICE_IOS_RUNNER_APP_BUNDLE_ID = com.callstack.agentdevice.runner\n AGENT_DEVICE_IOS_RUNNER_TEST_BUNDLE_ID = com.callstack.agentdevice.runner.uitests\n COMPILER_INDEX_STORE_ENABLE = NO\n ENABLE_CODE_COVERAGE = NO\n ENABLE_DEBUG_DYLIB = NO\n ENABLE_PREVIEWS = NO\n ENABLE_USER_SCRIPT_SANDBOXING = NO\n ONLY_ACTIVE_ARCH = YES\n OTHER_SWIFT_FLAGS = $(inherited) -disable-sandbox\n\nResolve Package Graph\n\n\nResolved source packages:\n agent-device-snapshot-presentation: /Users/thymikee/Developer/agent-device/.claude/worktrees/impl-2935b/apple/snapshot-presentation @ local\n\n2026-09-25 14:04:35.750 xcodebuild[28777:166276731] Writing error result bundle to /var/folders/65/fz9_2bsj6fzgct46vx226s8c0000gn/T/ResultBundle_2026-25-09_14-04-0035.xcresult\nxcodebuild: error: Unable to find a device matching the provided destination specifier:\n\t\t{ platform:iOS Simulator, id:9DD70D58-C485-4CE5-8743-5FED48468446 }\n\n\tThe requested device could not be found because no available devices matched the request.\n\n\tAvailable destinations for the "AgentDeviceRunner" scheme:\n\t\t{ platform:iOS, id:dvtdevice-DVTiPhonePlaceholder-iphoneos:placeholder, name:Any iOS Device }\n\t\t{ platform:iOS Simulator, id:dvtdevice-DVTiOSDeviceSimulatorPlaceholder-iphonesimulator:placeholder, name:Any iOS Simulator Device }\n\t\t{ platform:macOS, name:Any Mac }\n\t\t{ platform:tvOS, id:dvtdevice-DVTiOSDevicePlaceholder-appletvos:placeholder, name:Any tvOS Device }\n\t\t{ platform:tvOS Simulator, id:dvtdevice-DVTiOSDeviceSimulatorPlaceholder-appletvsimulator:placeholder, name:Any tvOS Simulator Device }\n\t\t{ platform:visionOS, id:dvtdevice-DVTiOSDevicePlaceholder-xros:placeholder, name:Any visionOS Device }\n\t\t{ platform:visionOS Simulator, id:dvtdevice-DVTiOSDeviceSimulatorPlaceholder-xrsimulator:placeholder, name:Any visionOS Simulator Device }\n', + 'Command line invocation:\n /Applications/Xcode-26.2.0.app/Contents/Developer/usr/bin/xcodebuild build-for-testing -project /Users//agent-device/apple/runner/AgentDeviceRunner/AgentDeviceRunner.xcodeproj -scheme AgentDeviceRunner -parallel-testing-enabled NO -maximum-concurrent-test-simulator-destinations 1 -destination "platform=iOS Simulator,id=9DD70D58-C485-4CE5-8743-5FED48468446" -derivedDataPath /Users//runner-derived COMPILER_INDEX_STORE_ENABLE=NO ENABLE_CODE_COVERAGE=NO ONLY_ACTIVE_ARCH=YES ENABLE_PREVIEWS=NO ENABLE_DEBUG_DYLIB=NO -IDEPackageSupportDisableManifestSandbox=1 -IDEPackageSupportDisablePluginExecutionSandbox=1 ENABLE_USER_SCRIPT_SANDBOXING=NO "OTHER_SWIFT_FLAGS=$(inherited) -disable-sandbox" AGENT_DEVICE_IOS_RUNNER_APP_BUNDLE_ID=com.callstack.agentdevice.runner AGENT_DEVICE_IOS_RUNNER_TEST_BUNDLE_ID=com.callstack.agentdevice.runner.uitests\n\nBuild settings from command line:\n AGENT_DEVICE_IOS_RUNNER_APP_BUNDLE_ID = com.callstack.agentdevice.runner\n AGENT_DEVICE_IOS_RUNNER_TEST_BUNDLE_ID = com.callstack.agentdevice.runner.uitests\n COMPILER_INDEX_STORE_ENABLE = NO\n ENABLE_CODE_COVERAGE = NO\n ENABLE_DEBUG_DYLIB = NO\n ENABLE_PREVIEWS = NO\n ENABLE_USER_SCRIPT_SANDBOXING = NO\n ONLY_ACTIVE_ARCH = YES\n OTHER_SWIFT_FLAGS = $(inherited) -disable-sandbox\n\nResolve Package Graph\n\n\nResolved source packages:\n agent-device-snapshot-presentation: /Users//agent-device/apple/snapshot-presentation @ local\n\n2026-09-25 14:04:35.750 xcodebuild[28777:166276731] Writing error result bundle to /var/folders//T/ResultBundle_2026-25-09_14-04-0035.xcresult\nxcodebuild: error: Unable to find a device matching the provided destination specifier:\n\t\t{ platform:iOS Simulator, id:9DD70D58-C485-4CE5-8743-5FED48468446 }\n\n\tThe requested device could not be found because no available devices matched the request.\n\n\tAvailable destinations for the "AgentDeviceRunner" scheme:\n\t\t{ platform:iOS, id:dvtdevice-DVTiPhonePlaceholder-iphoneos:placeholder, name:Any iOS Device }\n\t\t{ platform:iOS Simulator, id:dvtdevice-DVTiOSDeviceSimulatorPlaceholder-iphonesimulator:placeholder, name:Any iOS Simulator Device }\n\t\t{ platform:macOS, name:Any Mac }\n\t\t{ platform:tvOS, id:dvtdevice-DVTiOSDevicePlaceholder-appletvos:placeholder, name:Any tvOS Device }\n\t\t{ platform:tvOS Simulator, id:dvtdevice-DVTiOSDeviceSimulatorPlaceholder-appletvsimulator:placeholder, name:Any tvOS Simulator Device }\n\t\t{ platform:visionOS, id:dvtdevice-DVTiOSDevicePlaceholder-xros:placeholder, name:Any visionOS Device }\n\t\t{ platform:visionOS Simulator, id:dvtdevice-DVTiOSDeviceSimulatorPlaceholder-xrsimulator:placeholder, name:Any visionOS Simulator Device }\n', device: { ...IOS_SIMULATOR, id: CAPTURED_SCOPED_SIMULATOR.udid }, note: "Captured with the same argv and no `-DVTSimulatorSetLocation`, so xcodebuild looked in the default set, which does not hold the udid. The text is the scoped row's, word for word: only the scoped set path on the device tells the two apart. Host destination lines are left out as above.", }, diff --git a/packages/platform-apple/src/runner/__tests__/runner-startup-failure-reasons.test.ts b/packages/platform-apple/src/runner/__tests__/runner-startup-failure-reasons.test.ts index 10c8b29c6c..d2aa865755 100644 --- a/packages/platform-apple/src/runner/__tests__/runner-startup-failure-reasons.test.ts +++ b/packages/platform-apple/src/runner/__tests__/runner-startup-failure-reasons.test.ts @@ -50,6 +50,11 @@ import { mkdtempForTestSync } from './tmp-dir.ts'; const CACHE_RECOVERY_HINT = /clean:xcuitest|apple-runner\/derived/; +/** The fixtures whose failure message names more than the failed build, by fixture id. */ +const MESSAGE_FOR_FIXTURE: Readonly> = { + 'scoped-set-destination-not-found': `xcodebuild build-for-testing failed: xcodebuild found no simulator ${CAPTURED_SCOPED_SIMULATOR.udid} in simulator set ${CAPTURED_SCOPED_SIMULATOR.setWithoutUdid} with Xcode ${STUBBED_APPLE_TOOLCHAIN.xcodeVersion}`, +}; + /** * The phrase each reason's advice has to contain. Kept as text rather than as syntax because two of * them are quotations from `core/devicectl.ts`, and a fifth escaping helper for a prose remedy with @@ -174,7 +179,10 @@ function assertFailureEnvelope( fixture: RunnerStartupFailureFixture, ): void { assert.equal(envelope.code, 'COMMAND_FAILED'); - assert.ok(envelope.message.startsWith('xcodebuild build-for-testing failed')); + assert.equal( + envelope.message, + MESSAGE_FOR_FIXTURE[fixture.id] ?? 'xcodebuild build-for-testing failed', + ); assert.equal(envelope.details?.reason, fixture.reason); assert.ok( String(envelope.hint).includes(HINT_FOR_REASON[fixture.reason]), @@ -239,16 +247,13 @@ test('every reason the classifier can name is produced by a rule row', () => { }); test('a scoped-set simulator xcodebuild cannot find names its set and the Xcode', async () => { - const { udid, setWithoutUdid } = CAPTURED_SCOPED_SIMULATOR; + const { setWithoutUdid } = CAPTURED_SCOPED_SIMULATOR; const envelope = await driveBuildFailure(buildFixtureById('scoped-set-destination-not-found')); assert.equal(envelope.details?.reason, 'simulator_set_destination_not_found'); assert.equal(envelope.details?.simulatorSetPath, setWithoutUdid); assert.equal(envelope.details?.xcodeVersion, STUBBED_APPLE_TOOLCHAIN.xcodeVersion); - assert.equal( - envelope.message, - `xcodebuild build-for-testing failed: xcodebuild found no simulator ${udid} in simulator set ${setWithoutUdid} with Xcode ${STUBBED_APPLE_TOOLCHAIN.xcodeVersion}`, - ); + assert.equal(envelope.message, MESSAGE_FOR_FIXTURE['scoped-set-destination-not-found']); const buildArgs = runCmdStreaming.mock.calls[0]?.[1] as string[]; assert.ok(buildArgs.includes(`-DVTSimulatorSetLocation=${setWithoutUdid}`)); }); diff --git a/packages/platform-apple/src/runner/runner-cache-metadata.ts b/packages/platform-apple/src/runner/runner-cache-metadata.ts index af48309f62..4cda65e64d 100644 --- a/packages/platform-apple/src/runner/runner-cache-metadata.ts +++ b/packages/platform-apple/src/runner/runner-cache-metadata.ts @@ -38,8 +38,6 @@ const RUNNER_CACHE_METADATA_VALUE_MAX_LENGTH = 300; * probe, its warm retry, and the two probes still to run (#2422). */ const TOOLCHAIN_FINGERPRINT_BUDGET_MS = 45_000; -/** What naming the Xcode in a failure report may spend when the fingerprint is not memoized yet. */ -const XCODE_VERSION_REPORT_BUDGET_MS = 5_000; const TOOLCHAIN_PROBE_MAX_BUFFER = 128 * 1024; const TOOLCHAIN_PROBE_DETAIL_MAX_LENGTH = 200; const TOOLCHAIN_PROBE_HINT = @@ -279,19 +277,14 @@ function requireRunnerToolchainFingerprint( } /** - * The selected Xcode's version, for a failure report that names it. Reads the fingerprint the cache - * decision already memoized; a cold or unreadable toolchain answers undefined within - * {@link XCODE_VERSION_REPORT_BUDGET_MS} instead of delaying the failure it describes. + * The selected Xcode's version as this process's runner cache decision memoized it, for a failure + * report that names it; undefined when no decision has read the toolchain. Never probes: a report + * must not wait on the toolchain it describes. */ -export function readRunnerXcodeVersion(device: DeviceInfo): string | undefined { - try { - return requireRunnerToolchainFingerprint( - resolveRunnerSdkName(resolveRunnerPlatformName(device), device.kind), - createRunnerPhaseBudget(XCODE_VERSION_REPORT_BUDGET_MS, undefined), - ).xcodeVersion; - } catch { - return undefined; - } +export function memoizedRunnerXcodeVersion(device: DeviceInfo): string | undefined { + return toolchainFingerprintCache().get( + resolveRunnerSdkName(resolveRunnerPlatformName(device), device.kind), + )?.xcodeVersion; } function readRunnerToolchainFingerprint( diff --git a/packages/platform-apple/src/runner/runner-device-set.ts b/packages/platform-apple/src/runner/runner-device-set.ts index 48c30b6e17..071c9f1391 100644 --- a/packages/platform-apple/src/runner/runner-device-set.ts +++ b/packages/platform-apple/src/runner/runner-device-set.ts @@ -2,8 +2,8 @@ import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import type { DeviceInfo } from '@agent-device/kernel/device'; -import { emitDiagnostic, simulatorAddressFor } from './host.ts'; -import { readRunnerXcodeVersion } from './runner-cache-metadata.ts'; +import { simulatorAddressFor } from './host.ts'; +import { memoizedRunnerXcodeVersion } from './runner-cache-metadata.ts'; /** The scoped simulator set that holds this runner's simulator, or undefined for the default set. */ export function runnerSimulatorSetPath(device: DeviceInfo): string | undefined { @@ -29,34 +29,54 @@ export function xcodebuildDestinationArgs(device: DeviceInfo, destination: strin : ['-destination', destination, `-DVTSimulatorSetLocation=${simulatorSetPath}`]; } +/** One step of the legacy `XCTestDevices` restore, for the daemon to record once its log is published. */ +type LegacyXctestDeviceSetRestoreDiagnostic = Readonly<{ + phase: + | 'ios_runner_legacy_xctest_device_set_link_removed' + | 'ios_runner_legacy_xctest_device_set_backup_restored' + | 'ios_runner_legacy_xctest_device_set_restore_failed'; + resourcePath: string; + data: Readonly>; +}>; + /** * Puts the host's own `~/Library/Developer/XCTestDevices` back where an older agent-device left it * redirected into a scoped simulator set: a symlink in its place, with the real directory renamed to * `XCTestDevices.agent-device-backup` when one existed. Xcode's first-launch cleanup deletes every - * device in `XCTestDevices`, so a symlink left there deletes the set it points at. Any symlink there - * is removed, as released versions did on every scoped runner start; unlinking deletes no data. - * Daemons starting together may both undo it: a step the other daemon already took is done, not a - * failure. + * device in `XCTestDevices`, so a symlink left there deletes the set it points at. A symlinked + * `XCTestDevices` is unsupported: any symlink there is removed, as released versions did on every + * scoped runner start; unlinking deletes no data. Daemons starting together may both undo it: a step + * the other daemon already took is done, not a failure. Best effort: a failure is reported, not + * thrown. */ export function restoreLegacyXctestDeviceSetRedirect( + onDiagnostic: (diagnostic: LegacyXctestDeviceSetRestoreDiagnostic) => void, xctestDeviceSetPath: string = path.join(os.homedir(), 'Library', 'Developer', 'XCTestDevices'), ): void { const backupPath = `${xctestDeviceSetPath}.agent-device-backup`; - if (isSymlinkAt(xctestDeviceSetPath)) { - const linkTarget = readLinkTarget(xctestDeviceSetPath); - removeSymlinkUnlessGone(xctestDeviceSetPath); - emitDiagnostic({ - level: 'warn', - phase: 'ios_runner_legacy_xctest_device_set_link_removed', - data: { xctestDeviceSetPath, linkTarget }, - }); - } - if (fs.existsSync(backupPath) && !fs.existsSync(xctestDeviceSetPath)) { - restoreBackupUnlessRestored(backupPath, xctestDeviceSetPath); - emitDiagnostic({ - level: 'warn', - phase: 'ios_runner_legacy_xctest_device_set_backup_restored', - data: { xctestDeviceSetPath, backupPath }, + try { + if (isSymlinkAt(xctestDeviceSetPath)) { + const linkTarget = readLinkTarget(xctestDeviceSetPath); + removeSymlinkUnlessGone(xctestDeviceSetPath); + onDiagnostic({ + phase: 'ios_runner_legacy_xctest_device_set_link_removed', + resourcePath: xctestDeviceSetPath, + data: { linkTarget }, + }); + } + if (fs.existsSync(backupPath) && !fs.existsSync(xctestDeviceSetPath)) { + restoreBackupUnlessRestored(backupPath, xctestDeviceSetPath); + onDiagnostic({ + phase: 'ios_runner_legacy_xctest_device_set_backup_restored', + resourcePath: xctestDeviceSetPath, + data: { backupPath }, + }); + } + } catch (error) { + onDiagnostic({ + phase: 'ios_runner_legacy_xctest_device_set_restore_failed', + resourcePath: xctestDeviceSetPath, + data: { error: error instanceof Error ? error.message : String(error) }, }); } } @@ -94,14 +114,14 @@ type RunnerSimulatorSetFailureDetails = { simulatorSetPath?: string; xcodeVersio /** * The scoped set a runner xcodebuild phase resolved its destination in, with the selected Xcode when - * the toolchain answers; empty for the default set. + * the runner cache decision already read it; empty for the default set. */ export function runnerSimulatorSetFailureDetails( device: DeviceInfo, ): RunnerSimulatorSetFailureDetails { const simulatorSetPath = runnerSimulatorSetPath(device); if (simulatorSetPath === undefined) return {}; - const xcodeVersion = readRunnerXcodeVersion(device); + const xcodeVersion = memoizedRunnerXcodeVersion(device); return xcodeVersion === undefined ? { simulatorSetPath } : { simulatorSetPath, xcodeVersion }; } diff --git a/src/daemon/platform-owner-lifecycle.ts b/src/daemon/platform-owner-lifecycle.ts index faf4356d9c..b59186ff11 100644 --- a/src/daemon/platform-owner-lifecycle.ts +++ b/src/daemon/platform-owner-lifecycle.ts @@ -14,6 +14,13 @@ export type LegacyAppLogMarkerRecoveryOutcome = Readonly<{ }>[]; }>; +/** A startup finding the daemon holds until daemon.log is published, then records as a warning. */ +export type DaemonStartupDiagnostic = Readonly<{ + phase: string; + resourcePath: string; + data: Readonly>; +}>; + /** * Typed startup/shutdown participation for the platform resource owners the daemon coordinates * but does not itself implement (#2333): the daemon supplies its own inputs, ordering, and @@ -21,11 +28,16 @@ export type LegacyAppLogMarkerRecoveryOutcome = Readonly<{ * this surface. No generic hook bag — every phase this daemon relies on is named here. */ export type PlatformOwnerLifecycle = Readonly<{ - /** Publishes the daemon-owned lease-owner state dir and claim-authority probe. */ + /** + * Startup, before servers open: publishes the daemon-owned lease-owner state dir and + * claim-authority probe, and puts back the host's own `XCTestDevices` where an older agent-device + * left it redirected into a scoped simulator set. + */ configureForDaemonLock( input: Readonly<{ stateDir: string; hasDeviceClaimAuthority: DeviceClaimAuthorityProbe; + onDiagnostic: (diagnostic: DaemonStartupDiagnostic) => void; }>, ): Promise; /** Clears the configuration above: on a failed lock acquisition, and on shutdown. */ @@ -40,11 +52,6 @@ export type PlatformOwnerLifecycle = Readonly<{ ownedProcessRecords?: OwnedProcessRecordStore; }>, ): Promise; - /** - * Startup, after daemon.log publication: puts back the host's own `XCTestDevices` directory where - * an older agent-device left it redirected into a scoped simulator set. - */ - restoreLegacyXctestDeviceSetRedirect(): Promise; /** Shutdown: resets Android snapshot-helper runtime sessions. */ resetAndroidSnapshotHelper(): Promise; }>; diff --git a/src/daemon/server/daemon-runtime-xctest-device-set.test.ts b/src/daemon/server/daemon-runtime-xctest-device-set.test.ts index f3f99dc146..bb8b23568c 100644 --- a/src/daemon/server/daemon-runtime-xctest-device-set.test.ts +++ b/src/daemon/server/daemon-runtime-xctest-device-set.test.ts @@ -1,75 +1,119 @@ import fs from 'node:fs'; import path from 'node:path'; import { afterEach, expect, test, vi } from 'vitest'; -import { emitDiagnostic } from '@agent-device/host-kit/diagnostics'; import { mkdtempForTestSync } from '../../__tests__/test-utils/tmp-dir.ts'; +import { withMockedPlatform } from '../../__tests__/test-utils/host-execution.ts'; -const { mockRestoreLegacyXctestDeviceSetRedirect } = vi.hoisted(() => ({ - mockRestoreLegacyXctestDeviceSetRedirect: vi.fn(), +const legacyRedirect = vi.hoisted(() => ({ + xctestDeviceSetPath: '', + infoPublishedAtRestore: [] as boolean[], + infoPath: '', })); -vi.mock('../../platform-runtime-daemon-lifecycle.ts', async (importOriginal) => { +vi.mock('@agent-device/platform-apple/runner/operations', async (importOriginal) => { const actual = - await importOriginal(); + await importOriginal(); return { - platformDaemonLifecycleOwners: { - ...actual.platformDaemonLifecycleOwners, - restoreLegacyXctestDeviceSetRedirect: mockRestoreLegacyXctestDeviceSetRedirect, + ...actual, + restoreLegacyXctestDeviceSetRedirect: ( + onDiagnostic: Parameters[0], + ) => { + legacyRedirect.infoPublishedAtRestore.push(fs.existsSync(legacyRedirect.infoPath)); + actual.restoreLegacyXctestDeviceSetRedirect(onDiagnostic, legacyRedirect.xctestDeviceSetPath); }, }; }); -import { restoreLegacyXctestDeviceSetForDaemonStartup } from './daemon-runtime.ts'; +vi.mock('../../platform-runtime.ts', () => ({ + androidObservation: {}, + createRequestPlatformProviders: () => ({ + run: async (_context: unknown, task: () => Promise) => await task(), + }), + createPlatformRuntimeGateway: () => ({ + applicationLifecycle: { + recoverStartupResources: async () => {}, + detachForDaemonShutdown: async () => {}, + finalizeDaemonShutdown: async () => {}, + }, + inspectFacts: async () => { + throw new Error('unused'); + }, + bind: async () => { + throw new Error('unused'); + }, + shutdown: async () => {}, + }), + createPlatformDeviceInventoryGateways: () => ({}), +})); + +vi.mock('../../provider-device-runtimes.ts', () => ({ + DEFAULT_PROVIDER_RUNTIME_REQUIRED_IDS: [], + createDefaultProviderRuntimeComposition: async () => ({ runtimes: [], platformModules: [] }), +})); + +import { startDaemonRuntime } from './daemon-runtime.ts'; const roots: string[] = []; afterEach(() => { - mockRestoreLegacyXctestDeviceSetRedirect.mockReset(); + legacyRedirect.infoPublishedAtRestore.length = 0; for (const root of roots.splice(0)) fs.rmSync(root, { recursive: true, force: true }); }); -function daemonLogPath(): string { - const root = mkdtempForTestSync('agent-device-daemon-xctest-device-set-'); - roots.push(root); - return path.join(root, 'daemon.log'); -} - -function loggedPhases(logPath: string): string[] { - if (!fs.existsSync(logPath)) return []; +function loggedEvents(logPath: string): Array<{ phase: string; data?: Record }> { return fs .readFileSync(logPath, 'utf8') .trim() .split('\n') - .map((line) => (JSON.parse(line) as { phase: string }).phase); + .map((line) => JSON.parse(line) as { phase: string; data?: Record }) + .filter((event) => event.phase.startsWith('ios_runner_legacy_xctest_device_set_')); } -test('what the restore puts back is recorded in daemon.log', async () => { - const logPath = daemonLogPath(); - mockRestoreLegacyXctestDeviceSetRedirect.mockImplementationOnce(async () => { - emitDiagnostic({ level: 'warn', phase: 'ios_runner_legacy_xctest_device_set_link_removed' }); +test('macOS daemon startup puts back a redirected XCTestDevices before it publishes readiness', async () => { + const root = mkdtempForTestSync('agent-device-daemon-xctest-device-set-'); + roots.push(root); + const stateDir = path.join(root, 'state'); + const developer = path.join(root, 'Library', 'Developer'); + const scopedSetPath = path.join(root, 'tenant-set'); + fs.mkdirSync(path.join(scopedSetPath, 'SCOPED-UDID'), { recursive: true }); + fs.mkdirSync(path.join(developer, 'XCTestDevices.agent-device-backup', 'HOST-UDID'), { + recursive: true, }); - - await restoreLegacyXctestDeviceSetForDaemonStartup(logPath); - - expect(loggedPhases(logPath)).toEqual(['ios_runner_legacy_xctest_device_set_link_removed']); -}); - -test('a restore that fails is recorded in daemon.log and does not fail the daemon startup', async () => { - const logPath = daemonLogPath(); - mockRestoreLegacyXctestDeviceSetRedirect.mockRejectedValueOnce( - Object.assign(new Error('EACCES: permission denied'), { code: 'EACCES' }), + legacyRedirect.xctestDeviceSetPath = path.join(developer, 'XCTestDevices'); + legacyRedirect.infoPath = path.join(stateDir, 'daemon.json'); + fs.symlinkSync(scopedSetPath, legacyRedirect.xctestDeviceSetPath, 'dir'); + + const runtime = await withMockedPlatform('darwin', () => + startDaemonRuntime({ + env: { + ...process.env, + AGENT_DEVICE_STATE_DIR: stateDir, + AGENT_DEVICE_DAEMON_IDLE_TIMEOUT_MS: '0', + AGENT_DEVICE_DAEMON_SERVER_MODE: 'http', + }, + exit: () => {}, + registerProcessHandlers: false, + stderr: { write: () => {} }, + stdout: { write: () => {} }, + }), ); - - await expect(restoreLegacyXctestDeviceSetForDaemonStartup(logPath)).resolves.toBeUndefined(); - - expect(loggedPhases(logPath)).toEqual(['ios_runner_legacy_xctest_device_set_restore_failed']); -}); - -test('a host with nothing to put back leaves daemon.log untouched', async () => { - const logPath = daemonLogPath(); - mockRestoreLegacyXctestDeviceSetRedirect.mockResolvedValueOnce(undefined); - - await restoreLegacyXctestDeviceSetForDaemonStartup(logPath); - - expect(fs.existsSync(logPath)).toBe(false); + try { + expect(runtime).not.toBeNull(); + expect(legacyRedirect.infoPublishedAtRestore).toEqual([false]); + expect(fs.lstatSync(legacyRedirect.xctestDeviceSetPath).isSymbolicLink()).toBe(false); + expect(fs.existsSync(path.join(legacyRedirect.xctestDeviceSetPath, 'HOST-UDID'))).toBe(true); + expect(fs.existsSync(path.join(scopedSetPath, 'SCOPED-UDID'))).toBe(true); + expect(loggedEvents(path.join(stateDir, 'daemon.log'))).toEqual([ + expect.objectContaining({ + phase: 'ios_runner_legacy_xctest_device_set_link_removed', + data: expect.objectContaining({ + resourcePath: legacyRedirect.xctestDeviceSetPath, + linkTarget: scopedSetPath, + }), + }), + expect.objectContaining({ phase: 'ios_runner_legacy_xctest_device_set_backup_restored' }), + ]); + } finally { + await runtime?.shutdown(); + } }); diff --git a/src/daemon/server/daemon-runtime.ts b/src/daemon/server/daemon-runtime.ts index aca7766de1..52bc756e9d 100644 --- a/src/daemon/server/daemon-runtime.ts +++ b/src/daemon/server/daemon-runtime.ts @@ -80,10 +80,8 @@ import { prewarmPngWorker, terminatePngWorker } from '@agent-device/capture-kit/ import { platformResourceCleanup } from '../../platform-runtime-resource-cleanup.ts'; import { platformDaemonLifecycleOwners } from '../../platform-runtime-daemon-lifecycle.ts'; import { openWebSessionNames } from '../web-session-names.ts'; -import { - recoverAppLogResourcesAfterDaemonLock, - type AppLogRecoveryDiagnostic, -} from '../app-log-resource-recovery.ts'; +import { recoverAppLogResourcesAfterDaemonLock } from '../app-log-resource-recovery.ts'; +import type { DaemonStartupDiagnostic } from '../platform-owner-lifecycle.ts'; import { createDaemonRecoveryPlatformScope } from '../platform-request-scope.ts'; import { createAppLogAdmissionLedger } from '../app-log-admission-ledger.ts'; @@ -217,7 +215,7 @@ export type DaemonRuntimeController = { export async function flushDaemonStartupDiagnostics( logPath: string, - diagnostics: readonly AppLogRecoveryDiagnostic[], + diagnostics: readonly DaemonStartupDiagnostic[], ): Promise { if (diagnostics.length === 0) return; await withDiagnosticsScope( @@ -495,24 +493,25 @@ export async function startDaemonRuntime( let servers: DaemonServer[] = []; let socketPort: number | undefined; let httpPort: number | undefined; - const startupAppLogDiagnostics: AppLogRecoveryDiagnostic[] = []; + const startupDiagnostics: DaemonStartupDiagnostic[] = []; try { await platformDaemonLifecycleOwners.configureForDaemonLock({ stateDir: baseDir, hasDeviceClaimAuthority: processOwnsActiveDeviceClaim, + onDiagnostic: (diagnostic) => startupDiagnostics.push(diagnostic), }); const legacyMarkerRecovery = await platformDaemonLifecycleOwners.recoverLegacyAppLogMarkers(sessionsDir); appLogAdmissionLedger.retainLegacyMarkers(legacyMarkerRecovery.retained); for (const markerPath of legacyMarkerRecovery.recovered) { - startupAppLogDiagnostics.push({ + startupDiagnostics.push({ phase: 'app_log_legacy_marker_recovered', resourcePath: markerPath, data: {}, }); } for (const retained of legacyMarkerRecovery.retained) { - startupAppLogDiagnostics.push({ + startupDiagnostics.push({ phase: 'app_log_legacy_marker_retained', resourcePath: retained.markerPath, data: { @@ -525,7 +524,7 @@ export async function startDaemonRuntime( sessionsDir, gateway: deviceRuntimeGateway, scope: createDaemonRecoveryPlatformScope(), - onDiagnostic: (diagnostic) => startupAppLogDiagnostics.push(diagnostic), + onDiagnostic: (diagnostic) => startupDiagnostics.push(diagnostic), }); await reapOwnedProcessRecordsAtStartup(ownedProcessRecords, { openWebSessionNames: openWebSessionNames(sessionStore), @@ -551,7 +550,7 @@ export async function startDaemonRuntime( socketPort = opened.socketPort; httpPort = opened.httpPort; publishDaemonInfo(socketPort, httpPort); - await flushDaemonStartupDiagnostics(logPath, startupAppLogDiagnostics); + await flushDaemonStartupDiagnostics(logPath, startupDiagnostics); // After publication: publishDaemonInfo truncates daemon.log, so anything // written before it is lost — including reconciliation diagnostics. await reconcileDeviceClaimsForDaemonStartup( @@ -559,7 +558,6 @@ export async function startDaemonRuntime( createOwnerScopedDeviceClaimReconciler(createDaemonRecoveryPlatformScope()), baseDir, ); - await restoreLegacyXctestDeviceSetForDaemonStartup(logPath); // Arms the initial idle-reap timer: a daemon that starts and never // receives a request must still be able to reap itself. idleReap.noteActivity(); @@ -708,28 +706,6 @@ async function reconcileDeviceClaimsForDaemonStartup( ); } -/** - * Best effort: the runner never reads `XCTestDevices`, so a restore that fails here is recorded in - * daemon.log and fails neither this daemon nor a runner start. - */ -export async function restoreLegacyXctestDeviceSetForDaemonStartup(logPath: string): Promise { - await withDiagnosticsScope( - { command: 'daemon', session: 'daemon', logPath, debug: false }, - async () => { - try { - await platformDaemonLifecycleOwners.restoreLegacyXctestDeviceSetRedirect(); - } catch (error) { - emitDiagnostic({ - level: 'warn', - phase: 'ios_runner_legacy_xctest_device_set_restore_failed', - data: { error: error instanceof Error ? error.message : String(error) }, - }); - } - flushDiagnosticsToSessionFile({ force: true }); - }, - ); -} - export async function cleanupWebBrowserOrphansForDaemonStartup(params: { stateDir: string; sessionStore: SessionStore; diff --git a/src/platform-runtime-daemon-lifecycle.ts b/src/platform-runtime-daemon-lifecycle.ts index be1b22bec0..8526e96620 100644 --- a/src/platform-runtime-daemon-lifecycle.ts +++ b/src/platform-runtime-daemon-lifecycle.ts @@ -19,6 +19,7 @@ export const platformDaemonLifecycleOwners: PlatformOwnerLifecycle = Object.free configureForDaemonLock: async (input) => { await configureAppleRunnerLeaseOwnerStateDir(input.stateDir); await configureAppleRunnerDeviceClaimAuthorityProbe(input.hasDeviceClaimAuthority); + await restoreLegacyXctestDeviceSetRedirectRuntime(input.onDiagnostic); }, clearDaemonLockConfiguration: async () => { await configureAppleRunnerLeaseOwnerStateDir(undefined); @@ -32,9 +33,6 @@ export const platformDaemonLifecycleOwners: PlatformOwnerLifecycle = Object.free cleanupManagedWebOrphans: async (params) => { await cleanupManagedWebRuntimeOrphans(params); }, - restoreLegacyXctestDeviceSetRedirect: async () => { - await restoreLegacyXctestDeviceSetRedirectRuntime(); - }, resetAndroidSnapshotHelper: async () => { await resetAndroidSnapshotHelperRuntime(); }, diff --git a/src/platform-runtime-resource-cleanup.ts b/src/platform-runtime-resource-cleanup.ts index e39c21b20d..4edc082e17 100644 --- a/src/platform-runtime-resource-cleanup.ts +++ b/src/platform-runtime-resource-cleanup.ts @@ -1,6 +1,7 @@ import type { DeviceInfo } from '@agent-device/kernel/device'; import { isIosFamily } from '@agent-device/kernel/device'; import type { PlatformResourceCleanup } from './daemon/platform-resource-cleanup.ts'; +import type { DaemonStartupDiagnostic } from './daemon/platform-owner-lifecycle.ts'; import { emitDiagnostic } from '@agent-device/host-kit/diagnostics'; import { type OwnedProcessRecordStore } from '@agent-device/host-kit/process'; import { loadAndroidMechanics } from './platform-runtime-android-mechanics.ts'; @@ -17,11 +18,13 @@ async function stopAndroidSnapshotHelperRuntimeForDevice(device: DeviceInfo): Pr } /** Only a macOS host has an `XCTestDevices` an older agent-device could have redirected. */ -export async function restoreLegacyXctestDeviceSetRedirectRuntime(): Promise { +export async function restoreLegacyXctestDeviceSetRedirectRuntime( + onDiagnostic: (diagnostic: DaemonStartupDiagnostic) => void, +): Promise { if (process.platform !== 'darwin') return; const { restoreLegacyXctestDeviceSetRedirect } = await import('@agent-device/platform-apple/runner/operations'); - restoreLegacyXctestDeviceSetRedirect(); + restoreLegacyXctestDeviceSetRedirect(onDiagnostic); } export async function cleanupManagedWebRuntimeOrphans(params: { diff --git a/website/docs/docs/commands.md b/website/docs/docs/commands.md index f17c2983e6..f0cd853b24 100644 --- a/website/docs/docs/commands.md +++ b/website/docs/docs/commands.md @@ -204,8 +204,8 @@ agent-device devices --platform android --android-device-allowlist emulator-5554 ``` - `--ios-simulator-device-set ` constrains simulator discovery and simulator command execution via `xcrun simctl --set ...`. -- The XCTest runner's `xcodebuild` phases resolve a scoped simulator in the same set through `-DVTSimulatorSetLocation=`; `~/Library/Developer/XCTestDevices` is never redirected. If the selected Xcode no longer resolves the simulator that way, the runner start fails with `details.reason: "simulator_set_destination_not_found"`, and the error names the set and the selected Xcode version. -- On macOS, daemon startup puts back a `~/Library/Developer/XCTestDevices` that an older agent-device left redirected into a scoped set: it removes any symlink at that path, restores `XCTestDevices.agent-device-backup` when it exists, and records each step in `daemon.log`. A failed restore is recorded there too and does not stop the daemon or a runner start. +- The XCTest runner's `xcodebuild` phases resolve a scoped simulator in the same set through `-DVTSimulatorSetLocation=`; `~/Library/Developer/XCTestDevices` is never redirected. If the selected Xcode no longer resolves the simulator that way, the runner start fails with `details.reason: "simulator_set_destination_not_found"`, and the error names the set and the selected Xcode version, or `(version unreadable)` when this daemon has not read the Xcode version yet. +- On macOS, daemon startup puts back a `~/Library/Developer/XCTestDevices` that an older agent-device left redirected into a scoped set, before the daemon accepts requests: it removes any symlink at that path, restores `XCTestDevices.agent-device-backup` when it exists, and records each step in `daemon.log`. A symlinked `XCTestDevices` is not supported: daemon startup removes a symlink you made yourself too, for example one that moves `XCTestDevices` to another volume. Only the link is removed, never the directory it points to, and the `daemon.log` entry names that directory. A failed restore is recorded there too and does not stop the daemon or a runner start. - `--android-device-allowlist ` constrains Android discovery/selection to comma or space separated serials. - Scope is applied before selectors (`--device`, `--udid`, `--serial`), so out-of-scope selectors fail with `DEVICE_NOT_FOUND`. - With iOS simulator-set scope enabled, iOS physical devices are not enumerated.