Skip to content

iOS device selector picks wrong booted simulator when --udid is omitted #1335

Description

@thymikee

Summary

With multiple iOS simulators booted, agent-device open <bundle-id> / replay without an
explicit --udid
can select a booted simulator that does not have the target app installed,
causing a spurious failure. This isn't randomness across runs of the same fixed device set — it's
that the selector's tie-break has no awareness of app-install state at all, so whichever booted
simulator sorts first (by device-type rank, then name) wins regardless of whether the requested
app is actually there. From the caller's point of view — across differently-named/ordered
simulators, CI runners, or dev machines — this reads as "it non-deterministically grabs the wrong
device."

Explicit --udid <UDID> is a full, deterministic workaround today.

Mechanism (read from source, verified against origin/main @ ef118b9d1)

resolveDevice in src/kernel/device.ts:237-296 is the shared device-selection path (called from
src/core/dispatch-resolve.ts:163 and :180, which back open/replay/every command that
resolves a target device without an explicit selector).

When no --udid/--serial is given and more than one candidate matches, the multi-candidate
branch is:

// src/kernel/device.ts:284-293
const virtual = candidates.filter((device) => device.kind !== 'device');
const selectable = virtual.length > 0 ? virtual : candidates;
const booted = selectable.filter((device) => device.booted);
const onlyBooted = booted[0];
if (onlyBooted && booted.length === 1 && !isAppleDeviceCandidateSet(selectable)) {
  return onlyBooted;
}
const selected = isAppleDeviceCandidateSet(selectable)
  ? selectable[0]
  : (booted[0] ?? selectable[0]);

For an all-Apple candidate set (isAppleDeviceCandidateSet, src/kernel/device.ts:385-387
— true whenever every candidate is platform === 'apple', i.e. the normal --platform ios case),
the "more than one booted simulator" case always falls through to selectable[0] — the first
entry after sorting — with no check of which candidate has the target app installed.

The sort itself (sortAppleDevicesForSelectioncompareAppleDevicesForSelection,
src/kernel/device.ts:351-361) ranks by device kind/target (simulator vs. physical, phone vs.
iPad vs. tv — appleDeviceSelectionRank, :363-368), then by booted-first, then
alphabetically by simulator name (left.device.name.localeCompare(right.device.name),
:358), then by original list index. So with two booted iPhone simulators, e.g. "iPhone 15" and
"iPhone 16", open always picks "iPhone 15" (alphabetically first) — whether or not that's the
simulator the target app happens to be installed on. Nothing upstream of this filters candidates
by app-install state before the pick is made.

Repro

  1. Boot ≥2 iOS simulators (e.g. two different iPhone models), install the target app on only one
    of them.
  2. agent-device open com.some.app --relaunch --platform ios (no --udid).
  3. Observe: the resolver may pick the simulator without the app installed, surfacing a spurious
    "app not installed" / launch failure instead of a clear "which device?" prompt.

Deterministic workaround: pass --udid <UDID> explicitly to pin the exact simulator.

Cross-reference

This is a live instance of exactly the ownership/selection ambiguity that #1328 ("feat: add
advisory device claims and daemonless ownership status (Stage 1)") is designed to address.
Advisory device claims would let the daemon (a) know which simulators are already associated with
a given app/session and (b) surface an explicit disambiguation instead of silently guessing when
multiple booted candidates exist. Until then, --udid is the only deterministic mitigation.

Suggested direction

  • At minimum, when multiple booted simulators match a selector with no --udid, prefer a booted
    candidate that actually has the target bundle installed (when the command has a bundle id to
    check against) before falling back to the name-sort tie-break.
  • Longer term, tie this into feat: add advisory device claims and daemonless ownership status (Stage 1) #1328's advisory-claim model so the resolver can prefer/require an
    explicitly claimed or otherwise disambiguated device instead of an arbitrary first match.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions