You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Follow-up from #37 (two-tier auto-reconnect). Split out per code-review Q2.
Background
The two-tier reconnect design leans on iOS's CBConnectPeripheralOptionEnableAutoReconnect (Tier 0) and supplements it with the library backoff ladder (Tier 1). The Tier-0 → Tier-1 handoff happens when the OS gives up: after an unexpected drop the 5-param delegate first fires didDisconnectPeripheral:...:isReconnecting:true (OS still trying), and if the OS eventually gives up it fires a seconddidDisconnect with isReconnecting: false, at which point the library arms its own ladder.
Problem: the mock can't drive the give-up transition
CoreBluetoothMock (Nordic) does simulate isReconnecting: true when CBConnectPeripheralOptionEnableAutoReconnect is passed, but it stores that flag per-connect and never emits the true → false give-up transition. So the "OS gave up → hand off to Tier 1" path and the .system-suppresses-library path cannot be exercised through the normal mock disconnect primitive.
Current workaround (to remove)
An internal actor test hook, BluetoothActor.testInjectDisconnect(for:isReconnecting:error:), injects a 5-param didDisconnect event directly (routed through the production handleDidDisconnect) so tests can drive isReconnecting: false. It works and exercises real handler logic, but it is a test-only seam standing in for genuine mock fidelity.
Proposed work
Upstream / mock fidelity. Either contribute an upstream CoreBluetoothMock enhancement (or a local mock shim) that simulates the full OS auto-reconnect state machine: connect(auto) → drop → isReconnecting: true → (recover)didConnect, or(give up)isReconnecting: false, with a configurable give-up budget. Then migrate the Tier-0 give-up tests off testInjectDisconnect and retire the hook.
On-device verification of the real OS behavior, since Apple does not document the give-up budget/timing.
Specific device test (iOS 18+, real peripheral)
Environment: physical iOS 18+ device, a real BLE peripheral you can power-cycle / move out of range, ReliaBLE Demo with logging enabled.
Successful OS reconnect (no Tier 1). Connect with autoReconnect: true. Briefly drop the link (move peripheral out of range, then back). Expect: didDisconnect(isReconnecting: true) → library emits .reconnecting(source: .system, nil, nil); when the peripheral returns, didConnect → .connected. Assert the library ladder never armed (no .reconnecting(source: .library, ...)).
OS give-up → Tier-1 handoff. Connect with autoReconnect: true. Drop the link and keep the peripheral off/out of range long enough for iOS to give up. Expect a first didDisconnect(isReconnecting: true) then, after the OS budget elapses, a second didDisconnect(isReconnecting: false) → library hands off to the Tier-1 ladder (.reconnecting(source: .library, attempt: 1, ...)), then its normal give-up after maxAttempts.
Measure the undocumented budget. From the logs, record the wall-clock time and any retry pattern between the isReconnecting: true and the isReconnecting: false give-up. Capture across a few runs / conditions (screen locked, app backgrounded) to characterize it. Document findings in GettingStarted.md / the plan's deferred section.
Explicit disconnect during OS reconnect. While in the .system reconnecting window, call disconnect. Expect no reconnect and a clean settle to .disconnected(reason: nil) (verifies the intentional-disconnect-vs-OS-reconnect ordering).
Apple: CBConnectPeripheralOptionEnableAutoReconnect (iOS 17+), didDisconnectPeripheral:timestamp:isReconnecting:error:, WWDC23 "What's new in Core Bluetooth". OS give-up budget/timing is undocumented.
Test hook to retire: BluetoothActor.testInjectDisconnect(for:isReconnecting:error:).
Follow-up from #37 (two-tier auto-reconnect). Split out per code-review Q2.
Background
The two-tier reconnect design leans on iOS's
CBConnectPeripheralOptionEnableAutoReconnect(Tier 0) and supplements it with the library backoff ladder (Tier 1). The Tier-0 → Tier-1 handoff happens when the OS gives up: after an unexpected drop the 5-param delegate first firesdidDisconnectPeripheral:...:isReconnecting:true(OS still trying), and if the OS eventually gives up it fires a seconddidDisconnectwithisReconnecting: false, at which point the library arms its own ladder.Problem: the mock can't drive the give-up transition
CoreBluetoothMock(Nordic) does simulateisReconnecting: truewhenCBConnectPeripheralOptionEnableAutoReconnectis passed, but it stores that flag per-connect and never emits thetrue → falsegive-up transition. So the "OS gave up → hand off to Tier 1" path and the.system-suppresses-library path cannot be exercised through the normal mock disconnect primitive.Current workaround (to remove)
An internal actor test hook,
BluetoothActor.testInjectDisconnect(for:isReconnecting:error:), injects a 5-paramdidDisconnectevent directly (routed through the productionhandleDidDisconnect) so tests can driveisReconnecting: false. It works and exercises real handler logic, but it is a test-only seam standing in for genuine mock fidelity.Proposed work
CoreBluetoothMockenhancement (or a local mock shim) that simulates the full OS auto-reconnect state machine:connect(auto)→ drop →isReconnecting: true→ (recover)didConnect, or (give up)isReconnecting: false, with a configurable give-up budget. Then migrate the Tier-0 give-up tests offtestInjectDisconnectand retire the hook.Specific device test (iOS 18+, real peripheral)
Environment: physical iOS 18+ device, a real BLE peripheral you can power-cycle / move out of range, ReliaBLE Demo with logging enabled.
autoReconnect: true. Briefly drop the link (move peripheral out of range, then back). Expect:didDisconnect(isReconnecting: true)→ library emits.reconnecting(source: .system, nil, nil); when the peripheral returns,didConnect→.connected. Assert the library ladder never armed (no.reconnecting(source: .library, ...)).autoReconnect: true. Drop the link and keep the peripheral off/out of range long enough for iOS to give up. Expect a firstdidDisconnect(isReconnecting: true)then, after the OS budget elapses, a seconddidDisconnect(isReconnecting: false)→ library hands off to the Tier-1 ladder (.reconnecting(source: .library, attempt: 1, ...)), then its normal give-up aftermaxAttempts.isReconnecting: trueand theisReconnecting: falsegive-up. Capture across a few runs / conditions (screen locked, app backgrounded) to characterize it. Document findings inGettingStarted.md/ the plan's deferred section..systemreconnecting window, calldisconnect. Expect no reconnect and a clean settle to.disconnected(reason: nil)(verifies the intentional-disconnect-vs-OS-reconnect ordering).References
docs/plans/auto-reconnect-backoff-2026-07-05.md(Deferred / follow-up).CBConnectPeripheralOptionEnableAutoReconnect(iOS 17+),didDisconnectPeripheral:timestamp:isReconnecting:error:, WWDC23 "What's new in Core Bluetooth". OS give-up budget/timing is undocumented.BluetoothActor.testInjectDisconnect(for:isReconnecting:error:).