Closes #197. Stacked on #212, whose launch fix keeps the iOS shim leg from failing on a cold simulator. Once #212 merges, this is one commit.
Where #197 stood
Both halves of the report were already fixed in Swift by 4fe86f0, which landed an hour and a half after the issue was filed:
craft.geolocation.getCurrentPositionnow honourstimeout,maximumAgeandenableHighAccuracy, with a matching timeout on the native side.requestPermission('location')sets the manager's delegate and answers at once when authorization has already been decided.
bridge_mobile_location.zig has carried the same timeout on the Zig side since it landed.
What was missing was anything showing it works where the bug was seen. The report came from a compiled app on a simulator. The only tests added with the fix were string checks against the generated Swift, and the E2E probe app ran with geolocation switched off. A disabled capability is refused at the gate and never reaches the code that hung.
What runs now
The iOS probe app has geolocation on. Before launch the harness does two things from the host:
simctl privacy <device> grant location <bundle>, so no prompt waits on a person;simctl location <device> set 51.5007,-0.1246, which is Westminster, well away from the simulator's default at Apple Park, so a cached or default fix cannot pass.
Two cases replace geolocation.disabled.rejects:
| case | what it asserts |
|---|---|
permissions.location.granted | permissions.check('location') and then permissions.request('location') both answer granted, the second one immediately. This is the report's second half: a request for a permission already held waited for an authorization change that never came. |
geolocation.currentPosition | location.getCurrentPosition({enableHighAccuracy: true, maximumAge: 0, timeout: 15000}), the report's exact options, settles. The coordinate the page reports must match the one the harness set, to within 0.001°. |
The rejection path is not lost. share.disabled.rejects, merged in #208, now carries it. requiredCaseProblems would fail this PR if iOS were left with no rejection case.
Failing without the fix
Reverting the whole of CraftApp.swift to 4fe86f0's parent does not isolate this fix, because that file also predates the later craft://app origin fix and every bridge call is blocked. So the mutation removes only 4fe86f0's already-granted shortcut from requestPermission, which is the half this suite can reach deterministically:
FAIL ios-shim — 5/6 cases, no zig
case permissions.location.granted failed: permissions.request never settled for a permission already granted
FAIL ios-runtime — 5/6 cases, zig served checkPermission, clipboardRead, clipboardWrite, getCurrentPosition, getDeviceInfo, log, requestPermission, share
case permissions.location.granted failed: permissions.request never settled for a permission already grantedThe timeout half of 4fe86f0 is not exercised here. Its failure needs Core Location to stay silent with permission granted, and the simulator answers once it has a coordinate. bridge_mobile_location.zig's unit tests cover the Zig timeout, and the slice fixture's i=82 case covers a zero timeout rejecting with TIMEOUT on a simulator.
Proving Zig served it, not just saw it
With geolocation on, the runtime leg can no longer lean on Zig's refusal line to show that Zig answered getCurrentPosition. That line only appears when the capability is off. The dispatch line is no help either, because it is logged on entry whether or not Zig then serves the call.
So craft_ios_handle_action now logs one more line when no Zig module takes an action and it goes back to Swift:
ios: requestPermission is not served here; handing it back to the hostThe runtime leg fails if any action it expects Zig to serve shows up in that line, or in the existing "declared unavailable" line. requestPermission is the one deliberate exception, because bridge_mobile_permissions.zig leaves location authorization to Swift, and the protocol tests pin exactly that exception. The shim leg now also fails if a hand-back line appears at all.
ok ios-shim — 6/6 cases, no zig
ok ios-runtime — 6/6 cases, zig served checkPermission, clipboardRead, clipboardWrite, getCurrentPosition, getDeviceInfo, log, requestPermission, shareIn the passing runtime log, requestPermission is the only action handed back, and both legs reported 51.5007, -0.1246.
Verification
bun run test, bun run typecheck, bun run verify:packages, Pickier, the android/ios/typescript package tests, 36 protocol tests, and both iOS legs locally on an iOS 26.5 simulator, with and without the fix.