Skip to content

Commit e624ef9

Browse files
authored
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.
1 parent 2c7fb93 commit e624ef9

2 files changed

Lines changed: 38 additions & 2 deletions

File tree

src/daemon/handlers/__tests__/snapshot-session-cleanup.test.ts

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { beforeEach, expect, test, vi } from 'vitest';
1+
import { afterEach, beforeEach, expect, test, vi } from 'vitest';
22

33
vi.mock('@agent-device/platform-apple/runner/operations', async (importOriginal) => ({
44
...(await importOriginal<typeof import('@agent-device/platform-apple/runner/operations')>()),
@@ -14,6 +14,8 @@ import { platformResourceCleanup } from '../../../platform-runtime-resource-clea
1414
import { closeIosApp } from '@agent-device/platform-apple/app-lifecycle';
1515
import { stopIosRunnerSession } from '@agent-device/platform-apple/runner/operations';
1616
import { IOS_SIMULATOR } from '../../../__tests__/test-utils/device-fixtures.ts';
17+
import { setActiveProviderDeviceRuntimes } from '../../../provider-device-runtime.ts';
18+
import type { ProviderDeviceRuntime } from '@agent-device/contracts/device';
1719

1820
const mockStopIosRunnerSession = vi.mocked(stopIosRunnerSession);
1921
const mockCloseIosApp = vi.mocked(closeIosApp);
@@ -24,6 +26,10 @@ beforeEach(() => {
2426
mockCloseIosApp.mockReset().mockResolvedValue();
2527
});
2628

29+
afterEach(() => {
30+
setActiveProviderDeviceRuntimes([]);
31+
});
32+
2733
test('sessionless iOS runner cleanup stops the runner host app', async () => {
2834
const result = await withSessionlessRunnerCleanup(
2935
undefined,
@@ -48,3 +54,27 @@ test('sessionless iOS runner host close is best effort', async () => {
4854
expect(mockStopIosRunnerSession).toHaveBeenCalledWith(IOS_SIMULATOR.id);
4955
expect(mockCloseIosApp).toHaveBeenCalledWith(IOS_SIMULATOR, 'com.callstack.agentdevice.runner');
5056
});
57+
58+
test('sessionless cleanup leaves a provider-owned device to its provider', async () => {
59+
const device = { ...IOS_SIMULATOR, id: 'limrun:ios:lease-a' };
60+
const runtime: ProviderDeviceRuntime = {
61+
provider: 'limrun',
62+
leaseLifecycle: {},
63+
deviceInventoryProvider: async () => [device],
64+
ownsDevice: (candidate) => candidate.id === device.id,
65+
getInteractor: () => undefined,
66+
shutdown: async () => {},
67+
};
68+
setActiveProviderDeviceRuntimes([runtime]);
69+
70+
const result = await withSessionlessRunnerCleanup(
71+
undefined,
72+
device,
73+
returnOk,
74+
platformResourceCleanup,
75+
);
76+
77+
expect(result).toBe('ok');
78+
expect(mockStopIosRunnerSession).not.toHaveBeenCalled();
79+
expect(mockCloseIosApp).not.toHaveBeenCalled();
80+
});

src/daemon/snapshot-session.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { resolveTargetDevice } from '../core/dispatch-resolve.ts';
22
import type { PlatformResourceCleanup } from '@agent-device/contracts/platform-resource-cleanup';
33
import type { DaemonRequest, SessionScope, SessionState } from './types.ts';
44
import { ensureDeviceReady } from './device-ready.ts';
5+
import { isActiveProviderDevice } from '../provider-device-runtime.ts';
56
import { SessionStore } from './session-store.ts';
67

78
export async function resolveSessionDevice(
@@ -27,7 +28,12 @@ export async function withSessionlessRunnerCleanup<T>(
2728
try {
2829
return await task();
2930
} finally {
30-
if (!session) await platformCleanup!.cleanupSessionlessExecutionHost(device);
31+
// Symmetric with `ensureDeviceReady`: only a device this daemon prepared a local execution
32+
// host for can have one to release. A provider-owned device runs on provider infrastructure,
33+
// where local teardown drives host tooling at a device id this host does not own.
34+
if (!session && !isActiveProviderDevice(device)) {
35+
await platformCleanup!.cleanupSessionlessExecutionHost(device);
36+
}
3137
}
3238
}
3339

0 commit comments

Comments
 (0)