From 9eec6cc7a87cb47f91f2a8e4eed302b0f6187794 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Mon, 5 Oct 2026 12:24:05 +0200 Subject: [PATCH] fix(apple): start no host runner cache build while a provider serves Apple tooling The readiness keep-hot prewarm called prewarmAppleRunnerCache with no request id, so it reached the host xcodebuild even when a scoped Apple tool provider served the request. On macOS every provider-scenario iOS open started a real build-for-testing in the background; the lease-expiry parity test then hit the hermetic signal guard when teardown pkill'd that build's children. The cache prewarm is speculative host work, so it now declines (debug diagnostic ios_runner_cache_prewarm_unavailable) while a provider serves Apple tooling, the same rule the macOS helper build already follows. --- .../platform-apple/src/core/runner-host.ts | 8 ++- .../__tests__/runner-client-prewarm.test.ts | 63 ++++++++++++++++--- packages/platform-apple/src/runner/host.ts | 6 +- .../src/runner/runner-client.ts | 11 +++- 4 files changed, 78 insertions(+), 10 deletions(-) diff --git a/packages/platform-apple/src/core/runner-host.ts b/packages/platform-apple/src/core/runner-host.ts index 33a43bd392..6790d89a7c 100644 --- a/packages/platform-apple/src/core/runner-host.ts +++ b/packages/platform-apple/src/core/runner-host.ts @@ -44,7 +44,12 @@ import { getRunnerLeaseOwnerStateDir, } from './runner-owner-state.ts'; import { buildSimctlArgsForDevice, simulatorAddressFor } from './simctl.ts'; -import { readApplePlistJson, runAppleToolCommand, runXcrun } from './tool-provider.ts'; +import { + hasScopedAppleToolProvider, + readApplePlistJson, + runAppleToolCommand, + runXcrun, +} from './tool-provider.ts'; /** * The real host capabilities for `@agent-device/platform-apple/runner`: the one place @@ -97,6 +102,7 @@ export const appleRunnerHost: AppleRunnerHost = { runAppleToolCommand, runXcrun, readApplePlistJson, + hasScopedAppleToolProvider, buildSimctlArgsForDevice, simulatorAddressFor, resolveIosPhysicalDeviceControl, diff --git a/packages/platform-apple/src/runner/__tests__/runner-client-prewarm.test.ts b/packages/platform-apple/src/runner/__tests__/runner-client-prewarm.test.ts index 4ef9adaa37..4fbeb4d078 100644 --- a/packages/platform-apple/src/runner/__tests__/runner-client-prewarm.test.ts +++ b/packages/platform-apple/src/runner/__tests__/runner-client-prewarm.test.ts @@ -4,13 +4,19 @@ import { AppError } from '@agent-device/kernel/errors'; import { IOS_SIMULATOR } from './device-fixtures.ts'; import { makeRunnerSession } from './runner-session-fixtures.ts'; import { appleRunnerTestHost } from '../test-host.ts'; +import { createLocalAppleToolProvider, withAppleToolProvider } from '../../core/tool-provider.ts'; -const { mockEnsureRunnerSession, mockExecuteRunnerCommandWithSession, mockEmitDiagnostic } = - vi.hoisted(() => ({ - mockEnsureRunnerSession: vi.fn(), - mockExecuteRunnerCommandWithSession: vi.fn(), - mockEmitDiagnostic: vi.fn(), - })); +const { + mockEnsureRunnerSession, + mockExecuteRunnerCommandWithSession, + mockEnsureXctestrunArtifact, + mockEmitDiagnostic, +} = vi.hoisted(() => ({ + mockEnsureRunnerSession: vi.fn(), + mockExecuteRunnerCommandWithSession: vi.fn(), + mockEnsureXctestrunArtifact: vi.fn(), + mockEmitDiagnostic: vi.fn(), +})); vi.mock('../runner-session.ts', async () => { const actual = @@ -22,7 +28,13 @@ vi.mock('../runner-session.ts', async () => { }; }); -import { prewarmIosRunnerSession } from '../runner-client.ts'; +vi.mock('../runner-xctestrun.ts', async () => { + const actual = + await vi.importActual('../runner-xctestrun.ts'); + return { ...actual, ensureXctestrunArtifact: mockEnsureXctestrunArtifact }; +}); + +import { prewarmAppleRunnerCache, prewarmIosRunnerSession } from '../runner-client.ts'; beforeEach(() => { vi.resetAllMocks(); @@ -82,3 +94,40 @@ test('prewarmIosRunnerSession can propagate setup failures for blocking callers' }); assert.equal(mockEnsureRunnerSession.mock.calls[0]?.[1]?.propagateError, undefined); }); + +test('prewarmAppleRunnerCache builds the runner artifact when the host serves Apple tooling', async () => { + mockEnsureXctestrunArtifact.mockResolvedValueOnce({}); + + await prewarmAppleRunnerCache(IOS_SIMULATOR, { requestId: 'cache-request' }); + + assert.equal(mockEnsureXctestrunArtifact.mock.calls.length, 1); + assert.equal(mockEnsureXctestrunArtifact.mock.calls[0]?.[0], IOS_SIMULATOR); + assert.equal(mockEnsureXctestrunArtifact.mock.calls[0]?.[1]?.requestId, 'cache-request'); +}); + +test('prewarmAppleRunnerCache starts no host build while a provider serves Apple tooling', async () => { + const providerCalls: string[] = []; + const scriptedTools = createLocalAppleToolProvider({ + runCommand: async (cmd, args) => { + providerCalls.push([cmd, ...args].join(' ')); + throw new Error('a cache prewarm must not reach provider-served Apple tooling'); + }, + }); + + const scoped = await withAppleToolProvider(scriptedTools, async () => ({ + prewarm: prewarmAppleRunnerCache(IOS_SIMULATOR, { requestId: 'provider-request' }), + })); + + assert.equal(scoped.prewarm, undefined); + assert.equal(mockEnsureXctestrunArtifact.mock.calls.length, 0); + assert.deepEqual(providerCalls, []); + assert.deepEqual(mockEmitDiagnostic.mock.calls, [ + [ + { + level: 'debug', + phase: 'ios_runner_cache_prewarm_unavailable', + data: { deviceId: IOS_SIMULATOR.id }, + }, + ], + ]); +}); diff --git a/packages/platform-apple/src/runner/host.ts b/packages/platform-apple/src/runner/host.ts index 3c4c58171b..37be783617 100644 --- a/packages/platform-apple/src/runner/host.ts +++ b/packages/platform-apple/src/runner/host.ts @@ -81,7 +81,10 @@ export type AppleRunnerHost = Pick< Pick & Pick & Pick & - Pick & + Pick< + typeof AppleToolProvider, + 'runAppleToolCommand' | 'runXcrun' | 'readApplePlistJson' | 'hasScopedAppleToolProvider' + > & Pick & Pick & { /** @@ -185,6 +188,7 @@ export const bootFailureHint = delegate('bootFailureHint'); export const runAppleToolCommand = delegate('runAppleToolCommand'); export const runXcrun = delegate('runXcrun'); export const readApplePlistJson = delegate('readApplePlistJson'); +export const hasScopedAppleToolProvider = delegate('hasScopedAppleToolProvider'); export const buildSimctlArgsForDevice = delegate('buildSimctlArgsForDevice'); export const simulatorAddressFor = delegate('simulatorAddressFor'); export const visitXmlPlistEntries = delegate('visitXmlPlistEntries'); diff --git a/packages/platform-apple/src/runner/runner-client.ts b/packages/platform-apple/src/runner/runner-client.ts index eb93d8f959..c5a1a425cc 100644 --- a/packages/platform-apple/src/runner/runner-client.ts +++ b/packages/platform-apple/src/runner/runner-client.ts @@ -1,4 +1,4 @@ -import { retryWithPolicy, emitDiagnostic } from './host.ts'; +import { retryWithPolicy, emitDiagnostic, hasScopedAppleToolProvider } from './host.ts'; import { isIosFamily, type DeviceInfo } from '@agent-device/kernel/device'; import { ensureRunnerSession, @@ -119,6 +119,7 @@ type PrewarmIosRunnerOptions = AppleRunnerPrewarmOptions & { propagateError?: boolean; }; +/** Speculative host build: a request whose Apple tooling a provider serves has none to warm. */ export function prewarmAppleRunnerCache( device: DeviceInfo, options: PrewarmIosRunnerOptions = {}, @@ -126,6 +127,14 @@ export function prewarmAppleRunnerCache( if (!isIosFamily(device)) { return undefined; } + if (hasScopedAppleToolProvider()) { + emitDiagnostic({ + level: 'debug', + phase: 'ios_runner_cache_prewarm_unavailable', + data: { deviceId: device.id }, + }); + return undefined; + } return runBestEffortIosRunnerPrewarm({ device, options,