Skip to content

Commit a51f819

Browse files
thymikeeclaude
andauthored
feat: include device_udid and ios_simulator_device_set in iOS JSON payloads (#180)
Add device identity fields to open, appstate, and session_list responses for iOS targets, enabling isolation verification in concurrent multi-session runs without extra simctl calls. Closes #173 Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 5f941c1 commit a51f819

4 files changed

Lines changed: 85 additions & 2 deletions

File tree

skills/agent-device/SKILL.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,8 @@ agent-device batch --steps-file /tmp/batch-steps.json --json
176176
- Android `.aab` requires `bundletool` in `PATH`, or `AGENT_DEVICE_BUNDLETOOL_JAR=<path-to-bundletool-all.jar>` with `java` in `PATH`.
177177
- Android `.aab` optional: set `AGENT_DEVICE_ANDROID_BUNDLETOOL_MODE=<mode>` to control bundletool `build-apks --mode` (default: `universal`).
178178
- iOS `.ipa`: extract/install from `Payload/*.app`; when multiple app bundles are present, `<app>` is used as a bundle id/name hint.
179-
- iOS `appstate` is session-scoped; Android `appstate` is live foreground state.
179+
- iOS `appstate` is session-scoped; Android `appstate` is live foreground state. iOS responses include `device_udid` and `ios_simulator_device_set` for isolation verification.
180+
- iOS `open` responses include `device_udid` and `ios_simulator_device_set` to confirm which simulator handled the session.
180181
- Clipboard helpers: `clipboard read` / `clipboard write <text>` are supported on Android and iOS simulators; iOS physical devices are not supported yet.
181182
- Android keyboard helpers: `keyboard status|get|dismiss` report keyboard visibility/type and dismiss via keyevent when visible.
182183
- `network dump` is best-effort and parses HTTP(s) entries from the session app log file.

skills/agent-device/references/session-management.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ agent-device devices --platform android --android-device-allowlist emulator-5554
4747
agent-device session list
4848
```
4949

50+
iOS session entries include `device_udid` and `ios_simulator_device_set` (null when using the default set). Use these fields to confirm device routing in concurrent multi-session runs without additional `simctl` calls.
51+
5052
## Replay within sessions
5153

5254
```bash

src/daemon/handlers/__tests__/session.test.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -668,6 +668,8 @@ test('appstate with explicit selector matching session returns session state', a
668668
assert.equal(response.data?.appName, 'Maps');
669669
assert.equal(response.data?.appBundleId, 'com.apple.Maps');
670670
assert.equal(response.data?.source, 'session');
671+
assert.equal(response.data?.device_udid, 'sim-1');
672+
assert.equal(response.data?.ios_simulator_device_set, null);
671673
}
672674
});
673675

@@ -716,6 +718,8 @@ test('appstate returns session appName when bundle id is unavailable', async ()
716718
assert.equal(response.data?.appName, 'Maps');
717719
assert.equal(response.data?.appBundleId, undefined);
718720
assert.equal(response.data?.source, 'session');
721+
assert.equal(response.data?.device_udid, 'sim-1');
722+
assert.equal(response.data?.ios_simulator_device_set, null);
719723
}
720724
});
721725

@@ -1411,6 +1415,10 @@ test('open app on existing iOS session resolves and stores bundle id', async ()
14111415
assert.equal(updated?.appBundleId, 'com.apple.Preferences');
14121416
assert.equal(updated?.appName, 'settings');
14131417
assert.equal(dispatchedContext?.appBundleId, 'com.apple.Preferences');
1418+
if (response && response.ok) {
1419+
assert.equal(response.data?.device_udid, 'sim-1');
1420+
assert.equal(response.data?.ios_simulator_device_set, null);
1421+
}
14141422
});
14151423

14161424
test('open app on existing Android session resolves and stores package id', async () => {
@@ -2770,3 +2778,62 @@ test('network dump validates include mode and limit', async () => {
27702778
assert.match(invalidMode.error.message, /summary, headers, body, all/);
27712779
}
27722780
});
2781+
2782+
test('session_list includes device_udid and ios_simulator_device_set for iOS sessions', async () => {
2783+
const sessionStore = makeSessionStore();
2784+
sessionStore.set(
2785+
'ios-default',
2786+
makeSession('ios-default', {
2787+
platform: 'ios',
2788+
id: 'ABC-123',
2789+
name: 'iPhone 16',
2790+
kind: 'simulator',
2791+
booted: true,
2792+
}),
2793+
);
2794+
sessionStore.set(
2795+
'ios-scoped',
2796+
makeSession('ios-scoped', {
2797+
platform: 'ios',
2798+
id: 'DEF-456',
2799+
name: 'iPhone 16',
2800+
kind: 'simulator',
2801+
booted: true,
2802+
simulatorSetPath: '/tmp/tenant-a/simulators',
2803+
}),
2804+
);
2805+
sessionStore.set(
2806+
'android-1',
2807+
makeSession('android-1', {
2808+
platform: 'android',
2809+
id: 'emulator-5554',
2810+
name: 'Pixel Emulator',
2811+
kind: 'emulator',
2812+
booted: true,
2813+
}),
2814+
);
2815+
2816+
const response = await handleSessionCommands({
2817+
req: { token: 't', session: 'default', command: 'session_list', positionals: [] },
2818+
sessionName: 'default',
2819+
logPath: path.join(os.tmpdir(), 'daemon.log'),
2820+
sessionStore,
2821+
invoke: noopInvoke,
2822+
});
2823+
2824+
assert.ok(response);
2825+
assert.equal(response?.ok, true);
2826+
if (response && response.ok) {
2827+
const sessions = response.data?.sessions as Array<Record<string, unknown>>;
2828+
assert.ok(Array.isArray(sessions));
2829+
const iosDefault = sessions.find((s) => s.name === 'ios-default');
2830+
assert.equal(iosDefault?.device_udid, 'ABC-123');
2831+
assert.equal(iosDefault?.ios_simulator_device_set, null);
2832+
const iosScoped = sessions.find((s) => s.name === 'ios-scoped');
2833+
assert.equal(iosScoped?.device_udid, 'DEF-456');
2834+
assert.equal(iosScoped?.ios_simulator_device_set, '/tmp/tenant-a/simulators');
2835+
const android = sessions.find((s) => s.name === 'android-1');
2836+
assert.equal(android?.device_udid, undefined);
2837+
assert.equal(android?.ios_simulator_device_set, undefined);
2838+
}
2839+
});

src/daemon/handlers/session.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,12 +99,17 @@ function buildOpenResult(params: {
9999
appName?: string;
100100
appBundleId?: string;
101101
startup?: StartupPerfSample;
102+
device?: DeviceInfo;
102103
}): Record<string, unknown> {
103-
const { sessionName, appName, appBundleId, startup } = params;
104+
const { sessionName, appName, appBundleId, startup, device } = params;
104105
const result: Record<string, unknown> = { session: sessionName };
105106
if (appName) result.appName = appName;
106107
if (appBundleId) result.appBundleId = appBundleId;
107108
if (startup) result.startup = startup;
109+
if (device?.platform === 'ios') {
110+
result.device_udid = device.id;
111+
result.ios_simulator_device_set = device.simulatorSetPath ?? null;
112+
}
108113
return result;
109114
}
110115

@@ -593,6 +598,8 @@ async function handleAppStateCommand(params: {
593598
appName: appName ?? 'unknown',
594599
appBundleId: session.appBundleId,
595600
source: 'session',
601+
device_udid: session.device.id,
602+
ios_simulator_device_set: session.device.simulatorSetPath ?? null,
596603
},
597604
};
598605
}
@@ -741,6 +748,10 @@ export async function handleSessionCommands(params: {
741748
device: s.device.name,
742749
id: s.device.id,
743750
createdAt: s.createdAt,
751+
...(s.device.platform === 'ios' && {
752+
device_udid: s.device.id,
753+
ios_simulator_device_set: s.device.simulatorSetPath ?? null,
754+
}),
744755
})),
745756
};
746757
return { ok: true, data };
@@ -1127,6 +1138,7 @@ export async function handleSessionCommands(params: {
11271138
appName: openTarget,
11281139
appBundleId,
11291140
startup: startupSample,
1141+
device: session.device,
11301142
});
11311143
sessionStore.recordAction(nextSession, {
11321144
command,
@@ -1205,6 +1217,7 @@ export async function handleSessionCommands(params: {
12051217
appName: openTarget,
12061218
appBundleId,
12071219
startup: startupSample,
1220+
device,
12081221
});
12091222
sessionStore.recordAction(session, {
12101223
command,

0 commit comments

Comments
 (0)