@@ -9,13 +9,49 @@ import type {
99import { isIosFamily , isMacOs , type DeviceInfo } from '@agent-device/kernel/device' ;
1010import { AppError } from '@agent-device/kernel/errors' ;
1111
12+ /**
13+ * One memoized loader per lazily imported module, and the only way the ports below reach one.
14+ *
15+ * A port must never open its own `import(...)`: the open path deliberately leaves the runner
16+ * prewarm unawaited, so two ports routinely resolve the same specifier at the same time. In
17+ * production those duplicates are equivalent — the loader caches — but under Vitest they are
18+ * not: while a `vi.mock` factory is still awaiting `importOriginal()`, a second dynamic import
19+ * of that id resolves to the UNMOCKED module. A unit test's mocked runner then escapes into the
20+ * real local XCTest runner, whose stale-process cleanup `pkill`s xcodebuild on the developer's
21+ * host and fails whichever test happens to be running when it lands (#2314). Resolving each
22+ * specifier exactly once removes the second resolution that the escape needs.
23+ */
24+ let runnerOperationsModule :
25+ | Promise < typeof import ( '@agent-device/platform-apple/runner/operations' ) >
26+ | undefined ;
27+ const loadRunnerOperations = ( ) =>
28+ ( runnerOperationsModule ??= import ( '@agent-device/platform-apple/runner/operations' ) ) ;
29+
30+ let runtimeHintsModule : Promise < typeof import ( './platform-runtime-runtime-hints.ts' ) > | undefined ;
31+ const loadRuntimeHints = ( ) =>
32+ ( runtimeHintsModule ??= import ( './platform-runtime-runtime-hints.ts' ) ) ;
33+
34+ let openTargetModule : Promise < typeof import ( './platform-runtime-open-target.ts' ) > | undefined ;
35+ const loadOpenTarget = ( ) => ( openTargetModule ??= import ( './platform-runtime-open-target.ts' ) ) ;
36+
37+ let appResolutionModule :
38+ | Promise < typeof import ( '@agent-device/platform-apple/app-resolution' ) >
39+ | undefined ;
40+ const loadAppResolution = ( ) =>
41+ ( appResolutionModule ??= import ( '@agent-device/platform-apple/app-resolution' ) ) ;
42+
43+ let macOsModule : Promise < typeof import ( '@agent-device/platform-apple/macos' ) > | undefined ;
44+ const loadMacOs = ( ) => ( macOsModule ??= import ( '@agent-device/platform-apple/macos' ) ) ;
45+
46+ let retryModule : Promise < typeof import ( '@agent-device/host-kit/retry' ) > | undefined ;
47+ const loadRetry = ( ) => ( retryModule ??= import ( '@agent-device/host-kit/retry' ) ) ;
48+
1249/** Lazy Apple tools; the Apple package owns lifecycle sequencing around these primitives. */
1350export function createAppleApplicationTools ( ) : AppleApplicationTools {
1451 return Object . freeze ( {
1552 resolveOpenTarget : async ( device , input ) => await resolveAppleOpenTarget ( device , input ) ,
1653 prewarmRunnerCache : async ( device , execution , signal ) => {
17- const { prewarmAppleRunnerCache } =
18- await import ( '@agent-device/platform-apple/runner/operations' ) ;
54+ const { prewarmAppleRunnerCache } = await loadRunnerOperations ( ) ;
1955 await prewarmAppleRunnerCache ( device , appleRunnerOptions ( execution , signal ) ) ;
2056 } ,
2157 prewarmRunnerSession : async (
@@ -25,32 +61,29 @@ export function createAppleApplicationTools(): AppleApplicationTools {
2561 propagateError ,
2662 options ?: AppleRunnerSessionPrewarmOptions ,
2763 ) => {
28- const { prewarmIosRunnerSession } =
29- await import ( '@agent-device/platform-apple/runner/operations' ) ;
64+ const { prewarmIosRunnerSession } = await loadRunnerOperations ( ) ;
3065 await prewarmIosRunnerSession ( device , {
3166 ...appleRunnerOptions ( execution , signal ) ,
3267 propagateError,
3368 ...options ,
3469 } ) ;
3570 } ,
3671 notifyRunnerAppRelaunched : async ( device , execution , signal ) => {
37- const { notifyIosRunnerAppRelaunched } =
38- await import ( '@agent-device/platform-apple/runner/operations' ) ;
72+ const { notifyIosRunnerAppRelaunched } = await loadRunnerOperations ( ) ;
3973 await notifyIosRunnerAppRelaunched ( device , appleRunnerOptions ( execution , signal ) ) ;
4074 } ,
4175 stopRunnerSession : async ( deviceId ) => {
42- const { stopIosRunnerSession } =
43- await import ( '@agent-device/platform-apple/runner/operations' ) ;
76+ const { stopIosRunnerSession } = await loadRunnerOperations ( ) ;
4477 await stopIosRunnerSession ( deviceId ) ;
4578 } ,
4679 scheduleRunnerIdleStop : ( deviceId ) => {
47- void import ( '@agent-device/platform-apple/runner/operations' ) . then (
48- ( { scheduleIosRunnerIdleStop } ) => scheduleIosRunnerIdleStop ( deviceId ) ,
80+ void loadRunnerOperations ( ) . then ( ( { scheduleIosRunnerIdleStop } ) =>
81+ scheduleIosRunnerIdleStop ( deviceId ) ,
4982 ) ;
5083 } ,
5184 prepareRunner : async ( device , input , signal ) => {
52- const { Deadline } = await import ( '@agent-device/host-kit/retry' ) ;
53- const { prepareIosRunner } = await import ( '@agent-device/platform-apple/runner/operations' ) ;
85+ const { Deadline } = await loadRetry ( ) ;
86+ const { prepareIosRunner } = await loadRunnerOperations ( ) ;
5487 const startedAtMs = Date . now ( ) ;
5588 return await prepareIosRunner ( device , {
5689 ...appleRunnerOptions ( input . execution , signal ) ,
@@ -62,22 +95,20 @@ export function createAppleApplicationTools(): AppleApplicationTools {
6295 } ) ;
6396 } ,
6497 applyRuntimeHints : async ( device , input ) => {
65- const { applyRuntimeHintValues } = await import ( './platform-runtime-runtime-hints.ts' ) ;
98+ const { applyRuntimeHintValues } = await loadRuntimeHints ( ) ;
6699 await applyRuntimeHintValues ( { device, ...input } ) ;
67100 } ,
68101 clearRuntimeHints : async ( device , input ) => {
69- const { clearRuntimeHintValues } = await import ( './platform-runtime-runtime-hints.ts' ) ;
102+ const { clearRuntimeHintValues } = await loadRuntimeHints ( ) ;
70103 await clearRuntimeHintValues ( { device, ...input } ) ;
71104 } ,
72105 dismissCloseAlerts : async ( device , input ) => await dismissMacOsCloseAlerts ( device , input ) ,
73106 detachRunnerSessionsForShutdown : async ( ) => {
74- const { detachIosSimulatorRunnerSessionsForShutdown } =
75- await import ( '@agent-device/platform-apple/runner/operations' ) ;
107+ const { detachIosSimulatorRunnerSessionsForShutdown } = await loadRunnerOperations ( ) ;
76108 await detachIosSimulatorRunnerSessionsForShutdown ( ) ;
77109 } ,
78110 finalizeRunnerSessionsForShutdown : async ( ) => {
79- const { stopAllIosRunnerSessions } =
80- await import ( '@agent-device/platform-apple/runner/operations' ) ;
111+ const { stopAllIosRunnerSessions } = await loadRunnerOperations ( ) ;
81112 await stopAllIosRunnerSessions ( ) ;
82113 } ,
83114 } ) ;
@@ -100,7 +131,7 @@ async function resolveAppleOpenTarget(
100131 ) ;
101132 }
102133 const macOsSurface = await resolveMacOsSurface ( device , input . surface ) ;
103- const { resolveSessionAppBundleIdForTarget } = await import ( './platform-runtime-open-target.ts' ) ;
134+ const { resolveSessionAppBundleIdForTarget } = await loadOpenTarget ( ) ;
104135 return {
105136 appBundleId :
106137 macOsSurface . appBundleId ??
@@ -118,8 +149,7 @@ async function resolveAppleForegroundTarget(
118149 device : DeviceInfo ,
119150) : Promise < OpenTargetResolution | undefined > {
120151 if ( ! isIosFamily ( device ) || device . kind !== 'simulator' ) return undefined ;
121- const { detectSoleRunningIosSimulatorApp } =
122- await import ( '@agent-device/platform-apple/app-resolution' ) ;
152+ const { detectSoleRunningIosSimulatorApp } = await loadAppResolution ( ) ;
123153 const app = await detectSoleRunningIosSimulatorApp ( device ) ;
124154 return app ? { appBundleId : app . bundleId , appName : app . name } : undefined ;
125155}
@@ -131,7 +161,7 @@ async function resolveMacOsSurface(
131161 if ( ! isMacOs ( device ) || surface === 'app' || surface === 'desktop' || surface === 'menubar' ) {
132162 return { } ;
133163 }
134- const { resolveFrontmostMacOsApp } = await import ( '@agent-device/platform-apple/macos' ) ;
164+ const { resolveFrontmostMacOsApp } = await loadMacOs ( ) ;
135165 const frontmost = await resolveFrontmostMacOsApp ( ) ;
136166 return { appBundleId : frontmost . bundleId , appName : frontmost . appName } ;
137167}
@@ -155,7 +185,7 @@ async function dismissMacOsCloseAlerts(
155185 input : CloseApplicationFinalizationInput ,
156186) : Promise < void > {
157187 if ( ! isMacOs ( device ) ) return ;
158- const { runMacOsAlertAction } = await import ( '@agent-device/platform-apple/macos' ) ;
188+ const { runMacOsAlertAction } = await loadMacOs ( ) ;
159189 const dismissOptions =
160190 input . surface === 'frontmost-app'
161191 ? { surface : 'frontmost-app' as const }
0 commit comments