Blolabel
← Notes
event 7 min read

xcuitest driver 12.14.1 stops trusting an empty tunnel list; phoneshell still reads a failed usbmux call as an unplugged cable

Three xcuitest driver releases landed on 3 October 2026. The one that matters for real iPhones is 12.14.1: a tunnel registry that answered with an empty list used to win over a usbmux lookup that could see the phone. phoneshell does not run Appium, but its own bring-up makes the same mistake with usbmux, and tells the user to plug in a phone that is plugged in.

appium-xcuitest-driver 12.14.1, published to npm on 3 October 2026 at 18:15 UTC, stops a reachable but empty RemoteXPC tunnel registry from overruling the usbmux and devicectl device list. Up to 12.13.3, once every tunnel had dropped, each new real-device session failed with "Unknown device or simulator UDID" while the phone sat on the cable, visible to usbmux (issue #2986, opened 22 September). The fix, PR #2991, returns the union of both lists.

Abstract technical illustration of two parallel signal lines feeding a junction, one line carrying no pulses and the other carrying a steady pulse train that the junction ignores

Three driver releases on 3 October, one of them about real devices

Versionnpm publish (UTC)ChangePull request, merged (UTC)Real devices affectedSource
12.13.328 Sep 14:49bump WebDriverAgent for package creation with Xcode 26+#2996build onlyCHANGELOG.md
12.14.03 Oct 05:47support simulatorPasteboardAutomaticSync and the connect-hardware preference for Device Hub, by updating dependencies#2999, 05:42no (simulator)CHANGELOG.md, PR #2999
12.14.13 Oct 18:15merge tunnel and legacy device discovery#2991, 18:08 (opened 23 Sep)yes: discovery on iOS 17+ tunnelsCHANGELOG.md, PR #2991
12.15.03 Oct 21:44support launching WebDriverAgent on the iPhone Duo simulator (issue #2997, Xcode 27.1 beta, preinstalled WDA)#3000, 21:36no (simulator)CHANGELOG.md, PR #3000

The bug report is precise. getConnectedDevices() in lib/device/connected-devices-client.ts runs the tunnel registry lookup and the legacy lookup in parallel with Promise.allSettled, then returned the tunnel result whenever that promise was fulfilled. A registry whose HTTP endpoint answers but lists zero tunnels is fulfilled with an empty array. That empty array went to device-discovery.ts, which concluded the UDID was not a real device and tried the simulator path, which threw. The reporter, from a CI device farm, put the consequence plainly: running the documented long-lived tunnel-creation process without reconnect retries was "strictly worse than not running it at all once a single tunnel drops", because before a registry existed the same devices resolved through the legacy path.

The diff between the 12.13.3 and 12.15.0 npm tarballs shows the replacement. Tunnel UDIDs come first, then legacy-only UDIDs, de-duplicated case-insensitively with the registry's spelling kept. If one lookup fails, the driver warns and uses the other, including an empty result; it throws only when both fail, with both causes attached. The release also requires appium-ios-device ^3.1.24 to exclude a bplist-parser@0.5.0 import failure. The PR reports focused tests passing 15 of 15 and 15 of 18 functional CI jobs passing. The three failures (server startup on 26.4, a nested-iframe web test, Settings navigation on 16.4) passed in the nearest upstream run, and the author does not claim a macOS or iPhone hardware comparison.

getConnectedDevices(), before and after 12.14.1 tunnel registry lookupRemoteXPC, iOS 17 and later legacy lookupusbmux / devicectl both run in parallel (Promise.allSettled) 12.13.3 and earlier tunnel lookup resolved? return its list (even [ ]) only if it rejected: use the legacy list registry up, tunnel dropped: [ ] -> simulator path -> "Unknown device or simulator UDID", device still on usbmux 12.14.1 (PR #2991, fixes issue #2986) union: tunnel UDIDs first, then legacy-only UDIDs de-duplicated case-insensitively, registry spelling kept one lookup fails: warn, use the other (even [ ]) both fail: throw with both causes Side effect the PR states detectUdid() is unchanged and takes the final UDID in the list, so with no udid capability and devices in both sources, a legacy-only device can now be picked. Pin the udid capability. Sources: lib/device/connected-devices-client.ts in appium-xcuitest-driver 12.13.3 and 12.15.0 (npm tarballs); PR #2991 description; issue #2986 (opened 22 September 2026).

What changes for anyone running real iPhones through Appium

If you run appium driver run xcuitest tunnel-creation as a daemon, upgrade to 12.14.1 or set --disconnect-retry-*; on 12.13.3 and earlier a single dropped tunnel turns every subsequent session into a hard failure with a misleading error. After upgrading, set the udid capability on every real-device session. The PR says so itself: detectUdid is unchanged and selects the final UDID in the returned list, so with devices in both sources and no UDID given, a legacy-only device can now be picked where a tunnel device used to be. On a desk with two phones that is the wrong phone. The error message you used to see is also worth unlearning: on these versions "Unknown device or simulator UDID" did not mean the UDID was wrong.

phoneshell makes the same mistake with usbmux

phoneshell does not use Appium; it builds, launches and forwards WebDriverAgent itself. Its device discovery in phoneshell/bringup.py merges two lists the way 12.14.1 now does: scan() takes pymobiledevice3 usbmux list first, then adds devices from xcrun devicectl list devices. The flaw is one layer down. usbmux_devices() in phoneshell/device.py returns an empty list when the pymobiledevice3 binary is missing or its output is not JSON, so a failed lookup and an empty desk look identical. Then diagnose() counts a phone as on the cable only if it came from usbmux. Its docstring says "no phone in usbmux -> the cable, always".

We reproduced the consequence on 3 October 2026 by stubbing both lookups with the same UDID. With usbmux listing the phone, diagnose() returns state no_tunnel, "The phone is here but nothing is listening", which is right. With usbmux returning an empty list and devicectl reporting the phone as wired and connected, it returns no_device, "No iPhone on the cable", with the fix "Plug the phone in with a cable that carries data". The phone is plugged in. pymobiledevice3==11.8.0 is in requirements.txt, so this needs an install that skipped it or a version whose output changed; it is a latent fault, not one we have seen on the rig.

phoneshell bringup.py has the pre-12.14.1 shape, one layer down dev.usbmux_devices()pymobiledevice3 usbmux listbinary missing or bad JSON: [ ] dev.list_devices()xcrun devicectl list devicestransport wired / localNetwork scan(): merge,usbmux firstdedupe on exact UDID diagnose(): "on the cable"= source is usbmux, onlydocstring: no phone in usbmux-> "the cable, always" Reproduced 3 October 2026, both lookups stubbed, same UDID usbmux lists the phone -> scan [usbmux, USB]state no_tunnel "The phone is here but nothing is listening": correct, the next layer is checked usbmux lookup returns [ ] -> scan [devicectl, wired, connected]state no_device "No iPhone on the cable ... Plug the phone in": wrong, the phone is plugged in and devicectl says so Same class as issue #2986: a lookup that failed quietly is read as an authoritative "nothing here". Sources: phoneshell/device.py usbmux_devices(), list_devices(); phoneshell/bringup.py scan(), diagnose() (read 3 October 2026).

The fix has the same two parts as PR #2991: make usbmux_devices() return None on failure so the caller can tell "could not ask" from "asked, nothing there", and treat a devicectl entry with a wired transport and connected=True as on the cable when usbmux could not be asked. The rule underneath both bugs: an empty list from a lookup is a measurement only if the lookup is known to have worked.

Check it yourself

What would prove this wrong

If a session on appium-xcuitest-driver 12.14.1 or later, with a reachable registry that lists no tunnels and the device visible to usbmux, still fails with "Unknown device or simulator UDID", the fix did not cover the case it was written for. We expect no such report in the driver's issue tracker by 31 December 2026. For phoneshell, by the same date the stubbed reproduction above should return a state other than no_device; if it still says the cable is out, the fix described here was not made.

Sources

  1. maksymalferov-b2s, Real-device discovery treats a reachable-but-empty tunnel registry the same as a fully working one, appium-xcuitest-driver issue #2986, 22 September 2026
  2. dlowzzxx, fix: merge tunnel and legacy device discovery, PR #2991, opened 23 September, merged 3 October 2026
  3. npm registry metadata for appium-xcuitest-driver: publish times for 12.13.3, 12.14.0, 12.14.1, 12.15.0 (read 3 October 2026)
  4. KazuCocoa, Device Hub preferences, PR #2999, merged 3 October 2026
  5. KazuCocoa, support launching WDA on Duo, PR #3000, merged 3 October 2026; issue #2997 (29 September 2026)
  6. appium-ios-device PR #246, plist import compatibility fix required via ^3.1.24
  7. Blolabel, earlier note on UDID case handling across iOS tools
  8. phoneshell source: phoneshell/device.py, phoneshell/bringup.py, requirements.txt (read 3 October 2026)