Skip to content

Commit 7a33fc3

Browse files
committed
fix: admit managed operations at their dispatch boundary
1 parent 2f957ad commit 7a33fc3

7 files changed

Lines changed: 74 additions & 20 deletions

packages/platform-apple/src/runtime-simulator-readiness.test.ts

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,32 +20,31 @@ test('bound simulator readiness captures authority and refuses mismatches or fai
2020
}),
2121
async () => {
2222
const device = { ...IOS_SIMULATOR, simulatorSetPath: '/managed/set' };
23-
const ensureReady = vi.fn(async () => {});
23+
const admit = vi.fn(async (): Promise<never> => {
24+
throw new Error('Deep readiness must not recursively admit');
25+
});
2426
const operations = await withManagedDeviceScope(
2527
{
2628
device,
2729
owner: managedLocalRuntimeOwner('allocator'),
2830
fence: { token: 'fence', generation: 1 },
29-
ensureReady,
31+
admit,
3032
run: async <T>(task: () => Promise<T>) => await task(),
3133
},
3234
async () => bindSimulatorReadiness(Object.freeze({ ensureBootedSimulator })),
3335
);
3436
await operations.ensureBootedSimulator(device);
35-
expect(ensureReady).toHaveBeenCalledOnce();
37+
expect(admit).not.toHaveBeenCalled();
3638
await expect(
3739
operations.ensureBootedSimulator({ ...device, simulatorSetPath: undefined }),
3840
).rejects.toMatchObject({ details: { reason: 'managed-device-transport-mismatch' } });
39-
expect(ensureReady).toHaveBeenCalledOnce();
41+
expect(admit).not.toHaveBeenCalled();
4042
const abort = new AbortController();
4143
abort.abort(new Error('cancelled'));
4244
await expect(
4345
operations.ensureBootedSimulator(device, { signal: abort.signal }),
4446
).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);
47+
expect(admit).not.toHaveBeenCalled();
4948
},
5049
);
5150
});

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

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ test('managed readiness scopes isolate concurrent devices and leave ordinary rea
1818
device,
1919
owner: managedLocalRuntimeOwner('allocator'),
2020
fence: { token: 'fence', generation: 1 },
21-
ensureReady: async () => {
22-
ready.push(device.id);
21+
admit: async <T>(_task: () => Promise<T>): Promise<T> => {
22+
throw new Error('Readiness must not recursively admit');
2323
},
2424
run: async <T>(task: () => Promise<T>) => await task(),
2525
};
@@ -30,13 +30,11 @@ test('managed readiness scopes isolate concurrent devices and leave ordinary rea
3030
{
3131
...managed,
3232
device: selected,
33-
ensureReady: async () => {
34-
ready.push(id);
35-
},
3633
},
3734
async () => {
3835
await Promise.resolve();
3936
expect(await delegateManagedDeviceReadiness(selected)).toBe(true);
37+
ready.push(id);
4038
await expect(
4139
delegateManagedDeviceReadiness({ ...selected, id: 'foreign' }),
4240
).rejects.toMatchObject({ details: { reason: 'managed-device-transport-mismatch' } });

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@ export function resolveManagedDeviceReadiness():
3535
if (!managed) return undefined;
3636
return async (device) => {
3737
assertManagedDeviceIdentity(managed, device);
38-
await managed.ensureReady();
3938
};
4039
}
4140

src/platform-runtime-gateway.fixtures.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ export function managedGatewayScope(
6161
device,
6262
owner,
6363
fence,
64-
ensureReady: async () => {},
64+
admit: async (task) => await task(),
6565
run: async (task) => await task(),
6666
},
6767
};

src/platform-runtime-managed-owner.fixtures.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,10 +59,11 @@ async function managedAutomationScope(platform: ManagedLeasePlatform) {
5959
managedDevice: {
6060
...scope.managedDevice!,
6161
run: reachability.run,
62-
ensureReady: async () => {
63-
const result = await admission.run(horizon, scope.signal, async () => {});
62+
admit: async <T>(task: () => Promise<T>) => {
63+
const result = await admission.run(horizon, scope.signal, task);
6464
if (result.status !== 'admitted')
6565
throw new AppError('COMMAND_FAILED', 'Managed lease refused.', { reason: result.status });
66+
return result.value;
6667
},
6768
},
6869
};

src/platform-runtime-managed-owner.test.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,62 @@ test.skipIf(process.platform === 'win32')(
240240
);
241241

242242
describe('managed local runtime owner', () => {
243+
test('dispatches the real operation inside admission before authority can be fenced', async () => {
244+
const events: string[] = [];
245+
const cancellation = new AbortController();
246+
let cancelOnAdmission = false;
247+
const family = localFamilyRuntimeFixture({ family: 'apple', device });
248+
const local = await family.module.loadRuntime({} as PlatformRuntimeHost);
249+
const owner = createManagedLocalRuntimeOwner({
250+
owner: managed,
251+
loadLocal: async () => ({
252+
...local,
253+
bind: async (request) => {
254+
const binding = await local.bind(request);
255+
return {
256+
...binding,
257+
operations: {
258+
...binding.operations,
259+
readClipboard: async () => {
260+
events.push('native');
261+
expect(events.at(-2)).toBe('admitted');
262+
return 'clipboard';
263+
},
264+
},
265+
};
266+
},
267+
}),
268+
});
269+
const binding = await owner.bind({
270+
device,
271+
intent: exactly(),
272+
scope: {
273+
...scope,
274+
signal: cancellation.signal,
275+
managedDevice: {
276+
...scope.managedDevice!,
277+
admit: async (task) => {
278+
events.push('admitted');
279+
if (cancelOnAdmission) cancellation.abort(new Error('cancelled during admission'));
280+
try {
281+
return await task();
282+
} finally {
283+
events.push('fenced');
284+
}
285+
},
286+
},
287+
},
288+
});
289+
expect(await binding.operations.readClipboard!({})).toBe('clipboard');
290+
cancelOnAdmission = true;
291+
await expect(binding.operations.readClipboard!({})).rejects.toThrow(
292+
'cancelled during admission',
293+
);
294+
await binding[Symbol.asyncDispose]();
295+
expect(events).toEqual(['admitted', 'native', 'fenced', 'admitted', 'fenced']);
296+
expect(family.calls.disposals).toBe(1);
297+
});
298+
243299
test('exposes only reviewed operations even when the local family offers every cell', async () => {
244300
const { owner } = managedOwnerFixture();
245301

src/platform-runtime-managed-owner.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -105,9 +105,10 @@ export function createManagedLocalRuntimeOwner(params: {
105105
withMethodScope({ ...binding.operations }, (task) =>
106106
run(async () => {
107107
request.scope.signal.throwIfAborted();
108-
await managed.ensureReady();
109-
request.scope.signal.throwIfAborted();
110-
return await task();
108+
return await managed.admit(async () => {
109+
request.scope.signal.throwIfAborted();
110+
return await task();
111+
});
111112
}),
112113
),
113114
facts,

0 commit comments

Comments
 (0)