The application-tools ports each opened their own `import(...)` of the same
specifier — seven of them for `@agent-device/platform-apple/runner/operations`
alone. In production those duplicates are equivalent, because the loader caches.
Under Vitest they are not: while a `vi.mock` factory is still awaiting
`importOriginal()`, a second dynamic import of that id resolves to the UNMOCKED
module.
The open path makes that overlap routine — it deliberately leaves the iOS runner
prewarm unawaited — so a unit test that mocks the runner could still reach the
real one. `session-open-runtime.test.ts` did: its two iOS-simulator cases
scheduled two prewarms, the second one bypassed the mock, and the real local
XCTest runner started for device `sim-1`. Its stale-process cleanup then spawned
`pkill -f xcodebuild...session-sim-1-[0-9]` against the developer's own process
table, which the hermetic-signal guard refused and reported against whichever
test happened to be running when it landed — the unrelated
`open --metro-port alone stays host-ambiguous on a physical Android device`.
Each specifier now has one memoized loader, and the ports reach their module
only through it, so the second resolution the escape needs no longer exists.
Fixes #2314
Fixes #2314.
What was happening
session-open-runtime.test.ts > open --metro-port alone stays host-ambiguous on a physical Android devicefailed the hermetic-signal guard, but only whenvitest relatedscheduled it in a worker under load:The Android test never touches an Apple runner. The
pkillcame from two tests earlier in the same file, and reached the host through a Vitest mocking race.Each port on
createAppleApplicationTools()opened its ownimport(...)of the same specifier — seven of them for@agent-device/platform-apple/runner/operationsalone. In production those duplicates are equivalent, because the module loader caches. Under Vitest they are not: while avi.mockfactory is still awaitingimportOriginal(), a second dynamic import of that id resolves to the UNMOCKED module.The open path makes that overlap routine —
packages/platform-apple/src/lifecycle.tsdeliberately leaves the iOS runner prewarm unawaited (recordUnawaited). So:open applies launch-only flags…(iOS simulatorsim-1) schedules prewarm fix: skill should work, even if the npm package is not installed #1. Its dynamic import triggers the file'svi.mockfactory, which awaitsimportOriginal()— the whole real Apple runner graph, slow under a loaded worker.open --metro-port alone defaults the host to 127.0.0.1 on an iOS simulatorschedules prewarm Update README with correct GitHub link #2 while that factory is still in flight. It gets the real module.prewarmIosRunnerSession→prepareLocalIosRunner→ensureRunnerSession→prepareRunnerLeaseForStartup, which finds an empty lease and spawnspkill -f xcodebuild...session-sim-1-[0-9]against the developer's own process table.That is the guard doing exactly its job: a unit test really was reaching for foreign processes.
The fix
Each specifier now has one memoized loader, and the ports reach their module only through it — so the second resolution the escape needs no longer exists. Same idiom already used in
src/platform-runtime.tsandpackages/platform-android/src/index.ts. Applied to the Apple tools, the Android tools, andloadAndroidMechanics, which had the same duplicated-lazy-import shape.No production behavior changes: Node's loader already returned the same cached module for the duplicate imports.
Regression test
src/__tests__/platform-runtime-apple-application-tools.test.tsdrives two runner ports concurrently and asserts both reach the mock. It fails deterministically without the fix:It uses the two shutdown ports, whose real implementations are no-ops on an empty session map, so a regression run fails on the assertion without doing host process work.
Validation
waitForFileContenttimeout inandroid-lifecycle.test.tsunder contention; it passes on its own). Before the fix it reproduced in 1 of the first runs each time I tried.pnpm test:unit— 1203 files, 9011 passed.pnpm lint,pnpm format:check,pnpm typecheck,pnpm check:layering,pnpm check:di-seams,pnpm depgraph:test,pnpm check:gate-manifest,pnpm check:fallow— all green.