fix(daemon): skip sessionless local host teardown for provider devices (#2235)

A sessionless snapshot, diff, alert, settings, wait or is command releases
the local iOS execution host when it completes. That teardown ran for every
iOS device, including a provider-owned one. For a Limrun lease the device id
is `limrun:ios:<leaseId>` with `kind: 'simulator'`, so `closeIosApp` drove
`xcrun simctl` on the host against a device the host does not own, with retry
backoff on each failure.

`ensureDeviceReady` already declines local readiness work for a provider-owned
device. The teardown side was not symmetric. Apply the same guard, which covers
all four call sites of the helper.

The cost was about 5.8 seconds per sessionless command against a Limrun iOS
lease. It made `test/integration/provider-scenarios/limrun-ios-snapshot-owner.test.ts`
exceed the 5000 ms default timeout on macOS; that file now runs in 294 ms.
CI did not catch it because the provider-integration lane runs on ubuntu,
where `xcrun` does not exist and the calls fail immediately.
This commit is contained in:
Michał Pierzchała
2026-09-02 16:54:54 +02:00
committed by GitHub
parent 2c7fb93cfc
commit e624ef9d3f
2 changed files with 38 additions and 2 deletions
@@ -1,4 +1,4 @@
import { beforeEach, expect, test, vi } from 'vitest';
import { afterEach, beforeEach, expect, test, vi } from 'vitest';
vi.mock('@agent-device/platform-apple/runner/operations', async (importOriginal) => ({
...(await importOriginal<typeof import('@agent-device/platform-apple/runner/operations')>()),
@@ -14,6 +14,8 @@ import { platformResourceCleanup } from '../../../platform-runtime-resource-clea
import { closeIosApp } from '@agent-device/platform-apple/app-lifecycle';
import { stopIosRunnerSession } from '@agent-device/platform-apple/runner/operations';
import { IOS_SIMULATOR } from '../../../__tests__/test-utils/device-fixtures.ts';
import { setActiveProviderDeviceRuntimes } from '../../../provider-device-runtime.ts';
import type { ProviderDeviceRuntime } from '@agent-device/contracts/device';
const mockStopIosRunnerSession = vi.mocked(stopIosRunnerSession);
const mockCloseIosApp = vi.mocked(closeIosApp);
@@ -24,6 +26,10 @@ beforeEach(() => {
mockCloseIosApp.mockReset().mockResolvedValue();
});
afterEach(() => {
setActiveProviderDeviceRuntimes([]);
});
test('sessionless iOS runner cleanup stops the runner host app', async () => {
const result = await withSessionlessRunnerCleanup(
undefined,
@@ -48,3 +54,27 @@ test('sessionless iOS runner host close is best effort', async () => {
expect(mockStopIosRunnerSession).toHaveBeenCalledWith(IOS_SIMULATOR.id);
expect(mockCloseIosApp).toHaveBeenCalledWith(IOS_SIMULATOR, 'com.callstack.agentdevice.runner');
});
test('sessionless cleanup leaves a provider-owned device to its provider', async () => {
const device = { ...IOS_SIMULATOR, id: 'limrun:ios:lease-a' };
const runtime: ProviderDeviceRuntime = {
provider: 'limrun',
leaseLifecycle: {},
deviceInventoryProvider: async () => [device],
ownsDevice: (candidate) => candidate.id === device.id,
getInteractor: () => undefined,
shutdown: async () => {},
};
setActiveProviderDeviceRuntimes([runtime]);
const result = await withSessionlessRunnerCleanup(
undefined,
device,
returnOk,
platformResourceCleanup,
);
expect(result).toBe('ok');
expect(mockStopIosRunnerSession).not.toHaveBeenCalled();
expect(mockCloseIosApp).not.toHaveBeenCalled();
});
+7 -1
View File
@@ -2,6 +2,7 @@ import { resolveTargetDevice } from '../core/dispatch-resolve.ts';
import type { PlatformResourceCleanup } from '@agent-device/contracts/platform-resource-cleanup';
import type { DaemonRequest, SessionScope, SessionState } from './types.ts';
import { ensureDeviceReady } from './device-ready.ts';
import { isActiveProviderDevice } from '../provider-device-runtime.ts';
import { SessionStore } from './session-store.ts';
export async function resolveSessionDevice(
@@ -27,7 +28,12 @@ export async function withSessionlessRunnerCleanup<T>(
try {
return await task();
} finally {
if (!session) await platformCleanup!.cleanupSessionlessExecutionHost(device);
// Symmetric with `ensureDeviceReady`: only a device this daemon prepared a local execution
// host for can have one to release. A provider-owned device runs on provider infrastructure,
// where local teardown drives host tooling at a device id this host does not own.
if (!session && !isActiveProviderDevice(device)) {
await platformCleanup!.cleanupSessionlessExecutionHost(device);
}
}
}