Skip to content

Commit 13cda2a

Browse files
committed
refactor(root): bind the runner host into the daemon's entered store (#3794)
Composition runs inside the runtime's handle, so binding this runtime's host for the Apple runner writes that state directly and the deps no longer carry platform handles back.
1 parent 0f45aae commit 13cda2a

2 files changed

Lines changed: 38 additions & 24 deletions

File tree

‎src/daemon-composition.test.ts‎

Lines changed: 32 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ import {
1010
createTestHostEnvironment,
1111
} from '@agent-device/test-kit';
1212
import { createDaemonRuntimeDeps } from './daemon-composition.ts';
13-
import { helperRuntimeSlot } from '@agent-device/platform-android/adb-host';
13+
import { createRuntimeStateHandle, runtimeSlot } from '@agent-device/host-kit/runtime-slots';
14+
import { resolveRunnerAppBundleId } from '@agent-device/platform-apple/runner/operations';
1415
import { setActiveProviderDeviceRuntimes } from './provider-device-runtime.ts';
1516

1617
const ports = vi.hoisted(() => ({
@@ -126,13 +127,20 @@ test('a composition that succeeds leaves the plugins open for the daemon to clos
126127
expect(ports.shutdownProviders).toHaveBeenCalledOnce();
127128
});
128129

129-
test('a composed daemon registers the Apple runner and Android helper state, one set per composition', async () => {
130+
test('each composition binds its own runtime state into the entered handle, not a returned one', async () => {
130131
const compose = async (name: string) => {
131132
const baseDir = mkdtempForTestSync(`agent-device-daemon-composition-states-${name}-`);
132-
return await createDaemonRuntimeDeps({
133-
host: createTestHostEnvironment({
134-
env: { ...process.env, AGENT_DEVICE_STATE_DIR: baseDir },
135-
}),
133+
const host = createTestHostEnvironment({
134+
env: {
135+
...process.env,
136+
AGENT_DEVICE_STATE_DIR: baseDir,
137+
// The runner's first-choice configuration key: the per-runtime read below must see this
138+
// runtime's value even when the process env (spread above) carries a different one.
139+
AGENT_DEVICE_IOS_BUNDLE_ID: `com.test.runtime.${name}`,
140+
},
141+
});
142+
const deps = await createDaemonRuntimeDeps({
143+
host,
136144
baseDir,
137145
sessionsDir: `${baseDir}/sessions`,
138146
assertShutdownAllowed: undefined,
@@ -142,21 +150,28 @@ test('a composed daemon registers the Apple runner and Android helper state, one
142150
},
143151
openWebSessionNames: () => [],
144152
});
153+
return { deps, host };
145154
};
146-
const first = await compose('first');
147-
const second = await compose('second');
148155
const slotKey = Symbol('composition-test');
149156

150-
// Apple's runner state and Android's helper state.
151-
expect(first.runtimeStates).toHaveLength(2);
152-
expect(second.runtimeStates).toHaveLength(2);
153-
// The Android handle scopes the slots the helper modules read.
154-
const [, firstAndroid] = first.runtimeStates;
155-
const [, secondAndroid] = second.runtimeStates;
156-
const inFirst = firstAndroid!.run(() => helperRuntimeSlot(slotKey, () => ({})));
157-
const inSecond = secondAndroid!.run(() => helperRuntimeSlot(slotKey, () => ({})));
157+
// One entered store per runtime (#3794): the daemon enters the handle, then composes into it,
158+
// so each composition's writes — the Apple runner host, and any slot a platform module declares
159+
// — land in that runtime's state and nowhere else. The host binding is read back through the
160+
// runner's own configuration read (`resolveRunnerAppBundleId` takes its env from the bound
161+
// host), the same path production resource cleanup uses.
162+
const first = createRuntimeStateHandle();
163+
const second = createRuntimeStateHandle();
164+
const firstRun = await first.run(() => compose('first'));
165+
const secondRun = await second.run(() => compose('second'));
166+
expect(firstRun.deps).toBeTypeOf('object');
167+
expect(secondRun.deps).toBeTypeOf('object');
168+
169+
const inFirst = first.run(() => runtimeSlot(slotKey, () => ({})));
170+
const inSecond = second.run(() => runtimeSlot(slotKey, () => ({})));
158171
expect(inFirst).not.toBe(inSecond);
159-
expect(firstAndroid!.run(() => helperRuntimeSlot(slotKey, () => ({})))).toBe(inFirst);
172+
expect(first.run(() => runtimeSlot(slotKey, () => ({})))).toBe(inFirst);
173+
expect(first.run(() => resolveRunnerAppBundleId())).toBe('com.test.runtime.first');
174+
expect(second.run(() => resolveRunnerAppBundleId())).toBe('com.test.runtime.second');
160175
});
161176

162177
test('the composed daemon route configures interactors from its host, not from the process (#3794)', async () => {

‎src/daemon-composition.ts‎

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -133,15 +133,14 @@ export const createDaemonRuntimeDeps: ComposeDaemonRuntimeDeps = async (
133133
const { createDaemonPlatformServices } = await import('./platform-runtime-daemon-services.ts');
134134
const { createClaimRecoveryGatewayFor } = await import('./platform-runtime-claim-recovery.ts');
135135

136-
const { createAppleRunnerRuntimeState } =
136+
// The Apple runner reads its configuration from this runtime's host (#3794). Composition runs
137+
// inside the runtime's entered state, so binding it here writes that state's slot directly;
138+
// the Android helper needs no binding — its modules declare slot keys on the same store and
139+
// the daemon's entered state owns them (#3794).
140+
const { setAppleRunnerHostEnvironment } =
137141
await import('@agent-device/platform-apple/runner-owner');
138-
const { createAndroidHelperRuntimeState } =
139-
await import('@agent-device/platform-android/adb-host');
142+
setAppleRunnerHostEnvironment(host);
140143
return Object.freeze({
141-
runtimeStates: Object.freeze([
142-
createAppleRunnerRuntimeState(host),
143-
createAndroidHelperRuntimeState(),
144-
]),
145144
ownedProcessRecords,
146145
hostDiagnostics,
147146
ownerLifecycle: createPlatformDaemonLifecycleOwners(host),

0 commit comments

Comments
 (0)