Most gated arms in the spec's dispatcher are shaped like this:
case "registerBackgroundTask":
if config.enableBackgroundTasks,
let taskId = body["taskId"] as? String {
registerBackgroundTask(taskId: taskId, callbackId: callbackId)
}
// no elseenableBackgroundTasks defaults to false (CraftApp.swift:219). With the gate off, no arm runs, no callback is ever reached, and the page's promise never settles — these wrappers do not all carry the 30-second timeout, so it is not a slow rejection, it is silence.
An earlier audit put the count at roughly 60 of 65 gated cases. I verified registerBackgroundTask concretely; the shape is visibly the same across the file.
Why it mostly hides
For actions Zig serves, ios_config.gateFor refuses with CAPABILITY_DISABLED before the spec's arm is ever reached, so the page gets a rejection it can catch. The hang is only reachable for actions that fall through to the shim — and only in an app built without the Zig runtime linked, which is the shipping default today.
bridge_mobile_bgtasks.zig anticipated exactly this and wrote down a trigger condition:
if that fix does not land, reconsider this omission
The fix did not land.
The options, none of them free
- Add
else { rejectCallback(…, code: "CAPABILITY_DISABLED") }to the gated arms. Correct and matches what Zig already answers for its own actions. It is ~60 arms of spec, and the spec is what the conformance test reads. - Claim the affected actions in Zig as
.status = .unavailable. They then dispatch and refuse. But.unavailabletakes an action away from a shim that might serve it correctly when the gate is on — right forregisterBackgroundTask, wrong in general. - Leave it. A page calling a disabled capability hangs forever with no diagnostic.
Worth doing 1 as one sweep rather than piecemeal — fixing four arms because they happen to be in the module someone is reading is how a systemic bug becomes a scattered one.
Related: #115 (a different silent-failure shape in the same file).