Skip to content

Commit 2f957ad

Browse files
committed
fix: preserve lazy simulator readiness through scoped authority
1 parent 71413ed commit 2f957ad

5 files changed

Lines changed: 96 additions & 7 deletions

File tree

packages/platform-apple/src/core/simulator.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { Deadline, retryWithPolicy } from '@agent-device/host-kit/retry';
55

66
import { createTtlMemo } from '@agent-device/kernel/ttl-memo';
77
import { bootFailureHint, classifyBootFailure } from '@agent-device/provision-kit/boot-diagnostics';
8-
import { delegateManagedDeviceReadiness } from '@agent-device/provision-kit/managed-device-scope';
8+
import { createScopedProvider } from '@agent-device/kernel/scoped-provider';
99

1010
import {
1111
IOS_BOOT_TIMEOUT_MS,
@@ -18,6 +18,17 @@ import { runAppleToolCommand, runXcrun } from './tool-provider.ts';
1818
const IOS_SIMULATOR_HOST_APPS = ['Simulator'] as const;
1919
const IOS_DEVICE_HUB_HOST_APPS = ['Device Hub', 'Simulator'] as const;
2020

21+
const simulatorReadiness = createScopedProvider<
22+
((device: DeviceInfo) => Promise<void>) | undefined
23+
>(undefined);
24+
25+
export async function withSimulatorReadiness<T>(
26+
ensureReady: (device: DeviceInfo) => Promise<void>,
27+
task: () => Promise<T>,
28+
): Promise<T> {
29+
return await simulatorReadiness.run(ensureReady, task);
30+
}
31+
2132
type OpenIosSimulatorAppOptions = {
2233
background?: boolean;
2334
deviceHub?: boolean;
@@ -86,7 +97,12 @@ export async function ensureBootedSimulator(
8697
): Promise<void> {
8798
if (device.kind !== 'simulator') return;
8899
options.signal?.throwIfAborted();
89-
if (await delegateManagedDeviceReadiness(device)) return;
100+
const ensureReady = simulatorReadiness.resolve();
101+
if (ensureReady) {
102+
await ensureReady(device);
103+
options.signal?.throwIfAborted();
104+
return;
105+
}
90106

91107
const state = wasSimulatorRecentlyObservedBooted(device)
92108
? 'Booted'
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { expect, test, vi } from 'vitest';
2+
import { managedLocalRuntimeOwner } from '@agent-device/contracts/platform-runtime';
3+
import { withManagedDeviceScope } from '@agent-device/provision-kit/managed-device-scope';
4+
import { IOS_SIMULATOR } from './__tests__/device-fixtures.ts';
5+
import { ensureBootedSimulator } from './core/simulator.ts';
6+
import { createLocalAppleToolProvider, withAppleToolProvider } from './core/tool-provider.ts';
7+
import { bindSimulatorReadiness } from './runtime-simulator-readiness.ts';
8+
9+
test('ordinary simulator operations preserve their binding without a readiness override', () => {
10+
const operations = { ensureBootedSimulator };
11+
expect(bindSimulatorReadiness(operations)).toBe(operations);
12+
});
13+
14+
test('bound simulator readiness captures authority and refuses mismatches or failures before local boot', async () => {
15+
await withAppleToolProvider(
16+
createLocalAppleToolProvider({
17+
runCommand: async () => {
18+
throw new Error('Unexpected local readiness');
19+
},
20+
}),
21+
async () => {
22+
const device = { ...IOS_SIMULATOR, simulatorSetPath: '/managed/set' };
23+
const ensureReady = vi.fn(async () => {});
24+
const operations = await withManagedDeviceScope(
25+
{
26+
device,
27+
owner: managedLocalRuntimeOwner('allocator'),
28+
fence: { token: 'fence', generation: 1 },
29+
ensureReady,
30+
run: async <T>(task: () => Promise<T>) => await task(),
31+
},
32+
async () => bindSimulatorReadiness(Object.freeze({ ensureBootedSimulator })),
33+
);
34+
await operations.ensureBootedSimulator(device);
35+
expect(ensureReady).toHaveBeenCalledOnce();
36+
await expect(
37+
operations.ensureBootedSimulator({ ...device, simulatorSetPath: undefined }),
38+
).rejects.toMatchObject({ details: { reason: 'managed-device-transport-mismatch' } });
39+
expect(ensureReady).toHaveBeenCalledOnce();
40+
const abort = new AbortController();
41+
abort.abort(new Error('cancelled'));
42+
await expect(
43+
operations.ensureBootedSimulator(device, { signal: abort.signal }),
44+
).rejects.toThrow('cancelled');
45+
expect(ensureReady).toHaveBeenCalledOnce();
46+
ensureReady.mockRejectedValueOnce(new Error('lease fenced'));
47+
await expect(operations.ensureBootedSimulator(device)).rejects.toThrow('lease fenced');
48+
expect(ensureReady).toHaveBeenCalledTimes(2);
49+
},
50+
);
51+
});
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { withMethodScope } from '@agent-device/kernel/scoped-provider';
2+
import { resolveManagedDeviceReadiness } from '@agent-device/provision-kit/managed-device-scope';
3+
import { withSimulatorReadiness } from './core/simulator.ts';
4+
5+
export function bindSimulatorReadiness<T extends object>(operations: T): Readonly<T> {
6+
const ensureReady = resolveManagedDeviceReadiness();
7+
if (!ensureReady) return Object.freeze(operations);
8+
return Object.freeze({
9+
...withMethodScope({ ...operations }, (task) => withSimulatorReadiness(ensureReady, task)),
10+
});
11+
}

packages/platform-apple/src/runtime.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
localRuntimeOwner,
55
whenAdmitted,
66
} from '@agent-device/contracts/platform-runtime';
7+
import { bindSimulatorReadiness } from './runtime-simulator-readiness.ts';
78
import type { NetworkDumpInput } from '@agent-device/contracts/network-runtime';
89
import type {
910
PlatformRuntimeHost,
@@ -494,7 +495,7 @@ export function createApplePlatformRuntime(host: PlatformRuntimeHost): PlatformR
494495
device: logs.device,
495496
owner,
496497
facts,
497-
operations: Object.freeze(operations),
498+
operations: bindSimulatorReadiness(operations),
498499
[Symbol.asyncDispose]: async () => await logs[Symbol.asyncDispose](),
499500
}) satisfies DeviceBinding<PlatformRuntimeOperations>;
500501
},

packages/provision-kit/src/managed-device-scope.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,20 @@ export function assertManagedDeviceIdentity(managed: ManagedDeviceScope, device:
2828
}
2929
}
3030

31-
export async function delegateManagedDeviceReadiness(device: DeviceInfo): Promise<boolean> {
31+
export function resolveManagedDeviceReadiness():
32+
| ((device: DeviceInfo) => Promise<void>)
33+
| undefined {
3234
const managed = currentManagedDeviceScope();
33-
if (!managed) return false;
34-
assertManagedDeviceIdentity(managed, device);
35-
await managed.ensureReady();
35+
if (!managed) return undefined;
36+
return async (device) => {
37+
assertManagedDeviceIdentity(managed, device);
38+
await managed.ensureReady();
39+
};
40+
}
41+
42+
export async function delegateManagedDeviceReadiness(device: DeviceInfo): Promise<boolean> {
43+
const ensureReady = resolveManagedDeviceReadiness();
44+
if (!ensureReady) return false;
45+
await ensureReady(device);
3646
return true;
3747
}

0 commit comments

Comments
 (0)