Skip to content

Commit c7348b9

Browse files
committed
fix: route xctest runner through usbmux
1 parent b48b8c9 commit c7348b9

30 files changed

Lines changed: 1055 additions & 401 deletions

CONTEXT.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,8 @@ task touches:
3535
not the remote client ownership boundary.
3636
- iOS physical-device control: Apple-local module selected from discovery evidence. CoreDevice
3737
devices retain the `devicectl` controller; devices found only by `xctrace` use the XCTest
38-
controller for readiness, app activation/termination, and runner transport without claiming
39-
unsupported app inventory or installation capabilities.
38+
controller for readiness, app activation/termination, and cable-bound usbmux runner transport
39+
without claiming unsupported app inventory or installation capabilities.
4040
- Host process primitive: low-level host PID helpers in `src/utils/host-process.ts` for liveness,
4141
start-time/command reads, process listing, process-tree expansion, PID de-duplication, and
4242
best-effort signaling. It must not own domain cleanup policy such as browser ownership markers,

apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandExecution.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1363,6 +1363,14 @@ extension RunnerTests {
13631363
case .uptime:
13641364
return executeUptime()
13651365
case .activate:
1366+
guard
1367+
let bundleId = command.appBundleId?.trimmingCharacters(in: .whitespacesAndNewlines),
1368+
!bundleId.isEmpty
1369+
else {
1370+
return Response(ok: false, error: ErrorPayload(message: "activate requires appBundleId"))
1371+
}
1372+
// prepareActiveCommandContext already activated this bundle. Keep this case as the
1373+
// explicit acknowledgement after that preflight, not as a second activation.
13661374
return Response(ok: true, data: DataPayload(message: "app activated"))
13671375
case .terminate:
13681376
guard

src/cli/parser/__tests__/cli-help-topics.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -472,6 +472,7 @@ test('usageForCommand resolves physical-device help topic', async () => {
472472
assert.match(help, /AGENT_DEVICE_IOS_BUNDLE_ID=com\.yourname\.agentdevice\.runner/);
473473
assert.match(help, /profile name\/specifier, not a file path/);
474474
assert.match(help, /Older devices visible only to xctrace use the XCTest backend automatically/);
475+
assert.match(help, /runner commands travel through macOS usbmuxd/);
475476
assert.match(help, /app inventory, install\/reinstall, deep links, and launch arguments/);
476477
});
477478

src/cli/parser/cli-help.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -807,6 +807,7 @@ iOS physical-device prerequisites:
807807
The device must be paired/trusted, connected, unlocked when needed, and have Developer Mode enabled.
808808
Modern devices visible to devicectl use CoreDevice. Older devices visible only to xctrace use the XCTest backend automatically.
809809
XCTest-backed devices must already have the target app installed and should be opened by bundle ID; app inventory, install/reinstall, deep links, and launch arguments require CoreDevice.
810+
XCTest-backed runner commands travel through macOS usbmuxd; keep the trusted device connected by cable.
810811
The AgentDeviceRunner XCTest host must be signed before commands can run on a physical device.
811812
Start with Automatic Signing and only these env vars:
812813
AGENT_DEVICE_IOS_TEAM_ID=ABCDE12345

src/core/__tests__/dispatch-resolve.test.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,16 @@ test('resolveTargetDevice uses injected device inventory without local discovery
285285
assert.equal(mockListAppleDevices.mock.calls.length, 0);
286286
});
287287

288+
test('resolveTargetDevice preserves backend evidence from injected inventory', async () => {
289+
const result = await withDeviceInventoryProvider(
290+
async () => [{ ...physical, backend: 'xctest' }],
291+
async () => await resolveTargetDevice({ platform: 'ios', udid: physical.id }),
292+
);
293+
294+
assert.equal(result.backend, 'xctest');
295+
assert.equal(mockListAppleDevices.mock.calls.length, 0);
296+
});
297+
288298
test('resolveTargetDevice preserves Apple simulator preference with injected inventory', async () => {
289299
mockFindBootableIosSimulator.mockResolvedValue(simulator);
290300

src/core/capabilities.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -114,10 +114,6 @@ export function unsupportedHintForDevice(command: string, device: DeviceInfo): s
114114
return tryGetPlugin(device.platform)?.capability.unsupportedHintByDefault?.[command]?.(device);
115115
}
116116

117-
export function deviceBackendForDevice(device: DeviceInfo): string | undefined {
118-
return tryGetPlugin(device.platform)?.capability.deviceBackend?.(device);
119-
}
120-
121117
export function listCapabilityCommands(): string[] {
122118
return Object.keys(COMMAND_CAPABILITY_MATRIX).sort();
123119
}

src/core/platform-plugin/plugin.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,6 @@ export type PlatformPlugin = {
7676
readonly unsupportedHintByDefault?: Readonly<
7777
Record<string, (device: DeviceInfo) => string | undefined>
7878
>;
79-
/** Device-local execution backend selected from discovery/runtime capability evidence. */
80-
readonly deviceBackend?: (device: DeviceInfo) => string | undefined;
8179
};
8280
/**
8381
* The daemon app-log facet (issue #974). `resolveBackend` wraps the platform

src/daemon/__tests__/device-ready.test.ts

Lines changed: 1 addition & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { afterEach, beforeEach, expect, test, vi } from 'vitest';
2-
import assert from 'node:assert/strict';
32
import { promises as fs } from 'node:fs';
43
import type { DeviceInfo } from '../../kernel/device.ts';
54

@@ -19,12 +18,7 @@ import { runCmd } from '../../utils/exec.ts';
1918
import { waitForAndroidBoot } from '../../platforms/android/devices.ts';
2019
import { ensureBootedSimulator } from '../../platforms/apple/core/simulator.ts';
2120
import { ANDROID_EMULATOR, IOS_DEVICE, IOS_SIMULATOR } from '../../__tests__/test-utils/index.ts';
22-
import {
23-
DEVICE_READY_CACHE_TTL_MS,
24-
ensureDeviceReady,
25-
parseIosReadyPayload,
26-
resolveIosReadyHint,
27-
} from '../device-ready.ts';
21+
import { DEVICE_READY_CACHE_TTL_MS, ensureDeviceReady } from '../device-ready.ts';
2822

2923
const mockRunCmd = vi.mocked(runCmd);
3024
const mockEnsureBootedSimulator = vi.mocked(ensureBootedSimulator);
@@ -130,52 +124,3 @@ test('ensureDeviceReady does not cache failed readiness checks', async () => {
130124

131125
expect(mockEnsureBootedSimulator).toHaveBeenCalledTimes(2);
132126
});
133-
134-
test('parseIosReadyPayload reads tunnelState from direct connectionProperties', () => {
135-
const parsed = parseIosReadyPayload({
136-
result: {
137-
connectionProperties: {
138-
tunnelState: 'connected',
139-
},
140-
},
141-
});
142-
assert.equal(parsed.tunnelState, 'connected');
143-
});
144-
145-
test('parseIosReadyPayload reads tunnelState from nested device connectionProperties', () => {
146-
const parsed = parseIosReadyPayload({
147-
result: {
148-
device: {
149-
connectionProperties: {
150-
tunnelState: 'connecting',
151-
},
152-
},
153-
},
154-
});
155-
assert.equal(parsed.tunnelState, 'connecting');
156-
});
157-
158-
test('parseIosReadyPayload returns empty payload for malformed input', () => {
159-
assert.deepEqual(parseIosReadyPayload(null), {});
160-
assert.deepEqual(parseIosReadyPayload({}), {});
161-
assert.deepEqual(
162-
parseIosReadyPayload({
163-
result: { connectionProperties: { tunnelState: 123 } },
164-
}),
165-
{},
166-
);
167-
});
168-
169-
test('resolveIosReadyHint maps known connection errors', () => {
170-
const connecting = resolveIosReadyHint('', 'Device is busy (Connecting to iPhone)');
171-
assert.match(connecting, /still connecting/i);
172-
173-
const coreDeviceTimeout = resolveIosReadyHint('CoreDeviceService timed out', '');
174-
assert.match(coreDeviceTimeout, /coredevice service/i);
175-
});
176-
177-
test('resolveIosReadyHint falls back to generic guidance', () => {
178-
const hint = resolveIosReadyHint('unexpected failure', '');
179-
assert.match(hint, /unlocked/i);
180-
assert.match(hint, /xcode/i);
181-
});

src/daemon/device-ready.ts

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,8 @@
11
import { isIosFamily, type DeviceInfo } from '../kernel/device.ts';
2-
import {
3-
parseIosDeviceDetailsPayload,
4-
resolveIosPhysicalDeviceControl,
5-
resolveIosReadyHint,
6-
} from '../platforms/apple/core/physical-device-control.ts';
2+
import { resolveIosPhysicalDeviceControl } from '../platforms/apple/core/physical-device-control.ts';
73
import { isActiveProviderDevice } from '../provider-device-runtime.ts';
84
import { createTtlMemo } from '../utils/ttl-memo.ts';
95

10-
export { resolveIosReadyHint };
11-
export function parseIosReadyPayload(payload: unknown): { tunnelState?: string } {
12-
const { tunnelState } = parseIosDeviceDetailsPayload(payload);
13-
return tunnelState ? { tunnelState } : {};
14-
}
15-
166
// Exported so unit tests can assert TTL behavior without duplicating the value.
177
export const DEVICE_READY_CACHE_TTL_MS = 5_000;
188

src/daemon/handlers/__tests__/session-inventory-appleos.test.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,12 @@ test('devices omits appleOs for non-Apple devices', async () => {
9393
expect(ios?.appleOs).toBe('ios');
9494
});
9595

96+
test('devices omits internal backend evidence', async () => {
97+
const devices = await listPublicDevices([{ ...IOS_SIMULATOR, backend: 'xctest' }]);
98+
99+
expect(devices[0]).not.toHaveProperty('backend');
100+
});
101+
96102
test('devices drops a stray appleOs on a non-Apple device (gated to Apple platforms)', async () => {
97103
// Regression: appleOs is Apple-only. A malformed/legacy NON-Apple record carrying a
98104
// valid Apple OS value must NOT surface it — the projection gates on the platform,

0 commit comments

Comments
 (0)