The mobile E2E workflow has been failing intermittently on main. This fixes two of the three causes; the third is filed as #211.
The shim leg that never started its app
iOS simulator failed on main (run 35091847509), on #208 and on #210 in the same way:
FAIL ios-shim — 0/0 cases, no zig
the app produced no terminating event within 180000ms
the test page never announced a plan; it did not reach the native bridge
ok ios-runtime — 5/5 cases, ...The console was 0 bytes and the screenshot showed the home screen with the app installed but not running. Across the five earlier CI runs whose evidence is still available, the simulator was always Shutdown when the shim leg picked it. The shim leg is therefore always the first launch after a cold boot, and the runtime leg, second on the same device, never failed.
Reproduced on a simulator created for the test (simctl create, used, then deleted):
| step on a fresh device | seconds after boot started |
|---|---|
bootstatus -b returns | 41–42 |
first system app launch (com.apple.Preferences) returns | 64 |
plain simctl launch of the probe returns | 83 (3s after install) |
simctl launch --console-pty of the same app, first page event | 287 |
So the slowness comes from --console-pty itself, not from the device or the app. The harness started its 180-second clock at that spawn.
The fix
Both iOS harnesses, scripts/mobile-e2e/ios.ts and packages/ios/fixtures/zig-slice/build-and-run.sh, now launch with:
SIMCTL_CHILD_NSUnbufferedIO=YES xcrun simctl launch --terminate-running-process \
--stdout=<app-stdout.log> --stderr=<app-stderr.log> <udid> <bundle>and read the two files back as the app runs.
NSUnbufferedIO=YES is not optional. Measured on the same fresh device:
- without it: Swift's
printis fully buffered, so the file received nothing, not even aftersimctl terminate; - with it: the first event arrived 2 seconds after launch.
A pty is line-buffered, which is the only reason --console-pty ever worked. Xcode sets the same variable when it runs an app.
A plain simctl launch also returns once the app has a pid, so a launch that cannot happen now surfaces as simctl's own error. Before, it looked like a timeout blamed on the page.
The slice assertion that disagreed with its own wait loop
iOS Zig runtime slice failed on main (run 35094828558) with:
FAIL: getCurrentPosition did not return the simulated coordinateThat was after the wait loop had seen i=76 arrive, and the job log shows the line. The wait loop matched i=76($|[^0-9]) against the raw log. count matched i=76$ against a copy with colours and carriage returns stripped, which is the stricter boundary. count now uses the wait loop's boundary, and without a pty there is no CRLF left for the two to disagree about.
Not fixed here: #211
The other slice failure on #208 and #210 was timed out after 900s waiting for: i=38. removeSiriShortcut answers from NSUserActivity's deletion completion, which on a cold simulator sometimes never fires, and nothing else settles the call. That is a product bug, a promise with no way to settle, so it is filed rather than papered over in the fixture.
Verification
bun run test:mobile-e2e:iosfrom a shut-down simulator: both legs pass (5/5), 76 seconds in total.- The slice fixture locally: all assertions pass, 28 seconds.
bun run test,bun run typecheck,bun run verify:packages, Pickier and the 37 protocol tests.