Skip to content

Commit 7a959fb

Browse files
committed
refactor: move Apple resource access out of daemon
1 parent 369e095 commit 7a959fb

9 files changed

Lines changed: 66 additions & 46 deletions

src/daemon/__tests__/request-execution-scope.test.ts

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -613,13 +613,13 @@ test('prepareLockedRequestScope preserves existing-session selector validation',
613613
leaseRegistry: new LeaseRegistry(),
614614
});
615615

616-
expect(() =>
616+
await expect(
617617
prepareLockedRequestScope({
618618
scope,
619619
sessionStore,
620620
trackDownloadableArtifact: () => 'artifact-id',
621621
}),
622-
).toThrow(/already bound to android device "Pixel" \(emulator-5554\).*--platform=ios/i);
622+
).rejects.toThrow(/already bound to android device "Pixel" \(emulator-5554\).*--platform=ios/i);
623623
});
624624

625625
test('prepareLockedRequestScope blocks commands for invalidated recordings before handlers run', async () => {
@@ -639,12 +639,14 @@ test('prepareLockedRequestScope blocks commands for invalidated recordings befor
639639
leaseRegistry: new LeaseRegistry(),
640640
});
641641

642-
const result = await withDiagnosticsScope({ command: 'snapshot', logPath: LOG_PATH }, async () =>
643-
prepareLockedRequestScope({
644-
scope,
645-
sessionStore,
646-
trackDownloadableArtifact: () => 'artifact-id',
647-
}),
642+
const result = await withDiagnosticsScope(
643+
{ command: 'snapshot', logPath: LOG_PATH },
644+
async () =>
645+
await prepareLockedRequestScope({
646+
scope,
647+
sessionStore,
648+
trackDownloadableArtifact: () => 'artifact-id',
649+
}),
648650
);
649651

650652
expect(result.type).toBe('response');
@@ -665,7 +667,7 @@ test('prepareLockedRequestScope passes the session runner log path into handler
665667
leaseRegistry: new LeaseRegistry(),
666668
});
667669

668-
const result = prepareLockedRequestScope({
670+
const result = await prepareLockedRequestScope({
669671
scope,
670672
sessionStore,
671673
trackDownloadableArtifact: () => 'artifact-id',
@@ -694,7 +696,7 @@ test('prepareLockedRequestScope streams ordinary diagnostics into the active tra
694696
});
695697

696698
await withDiagnosticsScope({ command: 'snapshot', logPath: LOG_PATH }, async () => {
697-
const result = prepareLockedRequestScope({
699+
const result = await prepareLockedRequestScope({
698700
scope,
699701
sessionStore,
700702
trackDownloadableArtifact: () => 'artifact-id',

src/daemon/__tests__/request-recording-health.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ function makeIosSimulatorSession(showTouches: boolean): SessionState {
4040
return session;
4141
}
4242

43-
test('runner-backed iOS recordings still invalidate on runner restarts', () => {
43+
test('runner-backed iOS recordings still invalidate on runner restarts', async () => {
4444
const session = makeIosSimulatorSession(true);
4545
session.device.kind = 'device';
4646
session.screenRecording = makeTestScreenRecordingResource(session, {
@@ -53,7 +53,7 @@ test('runner-backed iOS recordings still invalidate on runner restarts', () => {
5353
sessionId: 'runner-after',
5454
});
5555

56-
refreshRecordingHealth(session);
56+
await refreshRecordingHealth(session);
5757

5858
expect(mockGetRunnerSessionSnapshot).toHaveBeenCalledWith('sim-1');
5959
expect(session.screenRecording?.handle.inspect().invalidatedReason).toBe(

src/daemon/handlers/session-device-utils.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { isIosFamily, type DeviceInfo } from '@agent-device/kernel/device';
22
import { AppError } from '@agent-device/kernel/errors';
33
import { isActiveProviderDevice } from '../../provider-device-runtime.ts';
44
import { ensureDeviceReady } from '../device-ready.ts';
5-
import { getRunnerSessionSnapshot } from '../../platforms/apple/core/runner-client.ts';
5+
import { inspectAppleRunnerSession } from '../../platform-runtime-apple-resources.ts';
66
import { resolveTargetDevice } from '../../core/dispatch-resolve.ts';
77
import type { DaemonRequest, DaemonResponse, SessionState } from '../types.ts';
88
import { hasDeviceSelectionInput, hasExplicitDeviceSelector } from '../device-selector-intent.ts';
@@ -62,7 +62,7 @@ export async function refreshSessionDeviceIfNeeded(device: DeviceInfo): Promise<
6262
// A live XCUITest runner session is attached to this exact UDID, which
6363
// proves the simulator still exists and is booted — the two facts the
6464
// ~0.7s re-resolve inventory listing exists to establish.
65-
if (getRunnerSessionSnapshot(device.id)?.alive) {
65+
if ((await inspectAppleRunnerSession(device.id))?.alive) {
6666
return { ...device, booted: true };
6767
}
6868

src/daemon/handlers/snapshot-session.ts

Lines changed: 2 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,6 @@
11
import { isIosFamily } from '@agent-device/kernel/device';
22
import { resolveTargetDevice } from '../../core/dispatch-resolve.ts';
3-
import {
4-
resolveRunnerAppBundleId,
5-
stopIosRunnerSession,
6-
} from '../../platforms/apple/core/runner-client.ts';
7-
import { closeIosApp } from '../../platforms/apple/core/apps.ts';
8-
import { emitDiagnostic } from '../../utils/diagnostics.ts';
3+
import { cleanupSessionlessAppleRunnerHost } from '../../platform-runtime-apple-resources.ts';
94
import type { DaemonRequest, SessionState } from '../types.ts';
105
import { ensureDeviceReady } from '../device-ready.ts';
116
import { SessionStore } from '../session-store.ts';
@@ -33,27 +28,11 @@ export async function withSessionlessRunnerCleanup<T>(
3328
// Sessionless iOS commands intentionally stop the runner to avoid leaked xcodebuild processes.
3429
// For multi-command flows, keep an active session via `open` so the runner can be reused.
3530
if (shouldCleanupSessionlessIosRunner) {
36-
await stopIosRunnerSession(device.id);
37-
await closeSessionlessIosRunnerHostApp(device);
31+
await cleanupSessionlessAppleRunnerHost(device);
3832
}
3933
}
4034
}
4135

42-
async function closeSessionlessIosRunnerHostApp(device: SessionState['device']): Promise<void> {
43-
const bundleId = resolveRunnerAppBundleId();
44-
await closeIosApp(device, bundleId).catch((error) => {
45-
emitDiagnostic({
46-
level: 'debug',
47-
phase: 'ios_sessionless_runner_host_close_failed',
48-
data: {
49-
deviceId: device.id,
50-
bundleId,
51-
error: error instanceof Error ? error.message : String(error),
52-
},
53-
});
54-
});
55-
}
56-
5736
export function recordIfSession(
5837
sessionStore: SessionStore,
5938
session: SessionState | undefined,

src/daemon/request-execution-scope.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -381,7 +381,7 @@ function applyRequestCommandDefaults(req: DaemonRequest): DaemonRequest {
381381
};
382382
}
383383

384-
export function prepareLockedRequestScope(params: {
384+
export async function prepareLockedRequestScope(params: {
385385
scope: RequestExecutionScope;
386386
sessionStore: SessionStore;
387387
trackDownloadableArtifact: (opts: {
@@ -390,14 +390,14 @@ export function prepareLockedRequestScope(params: {
390390
artifactType: DaemonArtifactType | undefined;
391391
fileName?: string;
392392
}) => string;
393-
}): LockedRequestScopeResult {
393+
}): Promise<LockedRequestScopeResult> {
394394
const { scope, sessionStore, trackDownloadableArtifact } = params;
395395
const logPath = scope.runnerLogPath;
396396
scope.throwIfCanceled();
397397
let existingSession = sessionStore.get(scope.sessionName);
398398
if (existingSession) {
399399
// Called under runLocked: refreshRecordingHealth may mutate session recording state.
400-
refreshRecordingHealth(existingSession);
400+
await refreshRecordingHealth(existingSession);
401401
sessionStore.set(scope.sessionName, existingSession);
402402
}
403403
const binding = prepareLockedRequestBinding({

src/daemon/request-recording-health.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
import { isIosFamily } from '@agent-device/kernel/device';
2-
import { getRunnerSessionSnapshot } from '../platforms/apple/core/runner-client.ts';
2+
import { inspectAppleRunnerSession } from '../platform-runtime-apple-resources.ts';
33
import type { SessionState } from './types.ts';
44

5-
export function refreshRecordingHealth(session: SessionState): void {
5+
export async function refreshRecordingHealth(session: SessionState): Promise<void> {
66
if (!recordingRequiresRunnerHealth(session)) {
77
return;
88
}
99
const recording = session.screenRecording!.handle;
1010
const state = recording.inspect();
1111

12-
const snapshot = getRunnerSessionSnapshot(session.device.id);
12+
const snapshot = await inspectAppleRunnerSession(session.device.id);
1313
if (!state.runnerSessionId) {
1414
if (snapshot?.alive) {
1515
recording.setRunnerSessionId(snapshot.sessionId);

src/daemon/request-router.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ export function createRequestHandler(deps: RequestRouterDeps): DaemonInvokeFn {
197197
inheritedProviderScope?: RequestPlatformProviderScope,
198198
): Promise<DaemonResponse> {
199199
const run = async (): Promise<DaemonResponse> => {
200-
const locked = prepareLockedRequestScope({
200+
const locked = await prepareLockedRequestScope({
201201
scope,
202202
sessionStore,
203203
trackDownloadableArtifact,

src/daemon/selector-runtime.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { parseWaitPositionals } from '../core/wait-positionals.ts';
33
import type { WaitParsed } from '../core/wait-positionals.ts';
44
import { AppError, asAppError } from '@agent-device/kernel/errors';
55
import type { SnapshotNode } from '@agent-device/kernel/snapshot';
6-
import { queryAppleRunnerSelector } from '../platforms/apple/core/runner-selector-query.ts';
6+
import { queryAppleRuntimeSelector } from '../platform-runtime-apple-resources.ts';
77
import type { AppleRunnerRequestOptions } from './apple-runner-options.ts';
88
import type { DaemonRequest, DaemonResponse, SessionState } from './types.ts';
99
import { errorResponse } from './handlers/response.ts';
@@ -381,7 +381,7 @@ export async function queryDirectIosSelector(
381381
selector: Pick<DirectIosSelectorTarget, 'key' | 'value'>,
382382
requestOptions: AppleRunnerRequestOptions,
383383
): Promise<DirectIosSelectorQueryResult> {
384-
const data = await queryAppleRunnerSelector(
384+
const data = await queryAppleRuntimeSelector(
385385
session.device,
386386
selector,
387387
session.appBundleId,
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import type { AppleRunnerRequestOptions } from '@agent-device/contracts/apple-runner-request';
2+
import type { ElementSelectorKey } from '@agent-device/contracts/interactor-types';
3+
import type { DeviceInfo } from '@agent-device/kernel/device';
4+
import { emitDiagnostic } from './utils/diagnostics.ts';
5+
6+
export async function inspectAppleRunnerSession(deviceId: string) {
7+
const { getRunnerSessionSnapshot } = await import('./platforms/apple/core/runner-client.ts');
8+
return getRunnerSessionSnapshot(deviceId);
9+
}
10+
11+
export async function cleanupSessionlessAppleRunnerHost(device: DeviceInfo): Promise<void> {
12+
const { resolveRunnerAppBundleId, stopIosRunnerSession } =
13+
await import('./platforms/apple/core/runner-client.ts');
14+
await stopIosRunnerSession(device.id);
15+
const bundleId = resolveRunnerAppBundleId();
16+
const { closeIosApp } = await import('./platforms/apple/core/apps.ts');
17+
await closeIosApp(device, bundleId).catch((error) => {
18+
emitDiagnostic({
19+
level: 'debug',
20+
phase: 'ios_sessionless_runner_host_close_failed',
21+
data: {
22+
deviceId: device.id,
23+
bundleId,
24+
error: error instanceof Error ? error.message : String(error),
25+
},
26+
});
27+
});
28+
}
29+
30+
export async function queryAppleRuntimeSelector(
31+
device: DeviceInfo,
32+
selector: Readonly<{ key: ElementSelectorKey; value: string }>,
33+
appBundleId: string | undefined,
34+
options: AppleRunnerRequestOptions,
35+
): Promise<Record<string, unknown>> {
36+
const { queryAppleRunnerSelector } =
37+
await import('./platforms/apple/core/runner-selector-query.ts');
38+
return await queryAppleRunnerSelector(device, selector, appBundleId, options);
39+
}

0 commit comments

Comments
 (0)