mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 01:05:19 +00:00
[codex] Extract BLE link state store (#1310)
* Refactor BLE transport event handling * Make image output paths unique * Keep queued Nostr read receipts alive * Refine BLE ingress fanout * Rediscover BLE service after invalidation * Extract BLE notification retry buffer * Extract BLE inbound write buffer * Extract BLE fragment assembly buffer * Tidy secure log handling from device run * Extract BLE outbound fragment scheduler * Harden app CI media tests * Redact BLE message content from logs * Extract BLE Noise session queues * Fix BLE read receipt UI updates * Allow self-authored RSR ingress replies * Harden read receipt queue test timing * Extract BLE outbound policy and incoming file storage * Avoid duplicate BLE link snapshots during send * Canonicalize Nostr relay URLs * Extract BLE link state store * Extract BLE connection scheduler --------- Co-authored-by: jack <jackjackbits@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
import Foundation
|
||||
import Testing
|
||||
@testable import bitchat
|
||||
|
||||
struct BLEConnectionSchedulerTests {
|
||||
@Test
|
||||
func discoveryQueuesWeakSignalCandidates() {
|
||||
let scheduler = BLEConnectionScheduler<String>(dynamicRSSIThreshold: -80)
|
||||
let now = Date()
|
||||
let candidate = makeCandidate(id: "p1", rssi: -85, now: now)
|
||||
|
||||
let decision = scheduler.handleDiscovery(
|
||||
candidate,
|
||||
connectedOrConnectingCount: 0,
|
||||
existingState: nil,
|
||||
peripheralState: .disconnected,
|
||||
now: now
|
||||
)
|
||||
|
||||
#expect(decision == .queued)
|
||||
#expect(scheduler.candidateCount == 1)
|
||||
}
|
||||
|
||||
@Test
|
||||
func discoveryQueuesAndSchedulesRetryWhenRateLimited() {
|
||||
let scheduler = BLEConnectionScheduler<String>(connectRateLimitInterval: 1.0)
|
||||
let now = Date()
|
||||
scheduler.recordConnectionAttempt(at: now)
|
||||
|
||||
let decision = scheduler.handleDiscovery(
|
||||
makeCandidate(id: "p1", rssi: -50, now: now),
|
||||
connectedOrConnectingCount: 0,
|
||||
existingState: nil,
|
||||
peripheralState: .disconnected,
|
||||
now: now.addingTimeInterval(0.25)
|
||||
)
|
||||
|
||||
guard case .scheduleRetry(let delay) = decision else {
|
||||
Issue.record("Expected scheduleRetry, got \(decision)")
|
||||
return
|
||||
}
|
||||
#expect(delay > 0)
|
||||
#expect(scheduler.candidateCount == 1)
|
||||
}
|
||||
|
||||
@Test
|
||||
func nextCandidateSelectsBestScoredCandidate() {
|
||||
let scheduler = BLEConnectionScheduler<String>()
|
||||
let now = Date()
|
||||
scheduler.enqueue(makeCandidate(id: "weak", rssi: -88, now: now))
|
||||
scheduler.enqueue(makeCandidate(id: "strong", rssi: -60, now: now.addingTimeInterval(1)))
|
||||
|
||||
let decision = scheduler.nextCandidate(
|
||||
connectedOrConnectingCount: 0,
|
||||
isAlreadyConnectingOrConnected: { _ in false },
|
||||
now: now.addingTimeInterval(2)
|
||||
)
|
||||
|
||||
guard case .connect(let candidate) = decision else {
|
||||
Issue.record("Expected connect decision")
|
||||
return
|
||||
}
|
||||
#expect(candidate.peripheralID == "strong")
|
||||
}
|
||||
|
||||
@Test
|
||||
func enqueueReplacesExistingPeripheralCandidate() {
|
||||
let scheduler = BLEConnectionScheduler<String>()
|
||||
let now = Date()
|
||||
scheduler.enqueue(makeCandidate(id: "same", rssi: -90, now: now))
|
||||
scheduler.enqueue(makeCandidate(id: "same", rssi: -55, now: now.addingTimeInterval(1)))
|
||||
|
||||
let decision = scheduler.nextCandidate(
|
||||
connectedOrConnectingCount: 0,
|
||||
isAlreadyConnectingOrConnected: { _ in false },
|
||||
now: now.addingTimeInterval(2)
|
||||
)
|
||||
|
||||
guard case .connect(let candidate) = decision else {
|
||||
Issue.record("Expected connect decision")
|
||||
return
|
||||
}
|
||||
#expect(scheduler.candidateCount == 0)
|
||||
#expect(candidate.peripheralID == "same")
|
||||
#expect(candidate.rssi == -55)
|
||||
}
|
||||
|
||||
@Test
|
||||
func weakTimedOutCandidateIsRequeuedWithRetryDelay() {
|
||||
let scheduler = BLEConnectionScheduler<String>(
|
||||
weakLinkCooldownSeconds: 30,
|
||||
weakLinkRSSICutoff: -90
|
||||
)
|
||||
let now = Date()
|
||||
scheduler.recordConnectionTimeout(peripheralID: "weak", at: now)
|
||||
scheduler.enqueue(makeCandidate(id: "weak", rssi: -95, now: now))
|
||||
|
||||
let decision = scheduler.nextCandidate(
|
||||
connectedOrConnectingCount: 0,
|
||||
isAlreadyConnectingOrConnected: { _ in false },
|
||||
now: now.addingTimeInterval(5)
|
||||
)
|
||||
|
||||
guard case .retryAfter(let delay) = decision else {
|
||||
Issue.record("Expected retryAfter decision")
|
||||
return
|
||||
}
|
||||
#expect(delay == 15)
|
||||
#expect(scheduler.candidateCount == 1)
|
||||
}
|
||||
|
||||
@Test
|
||||
func rssiThresholdTightensAfterRepeatedRecentTimeouts() {
|
||||
let scheduler = BLEConnectionScheduler<String>()
|
||||
let now = Date()
|
||||
scheduler.recordConnectionTimeout(peripheralID: "p1", at: now)
|
||||
scheduler.recordConnectionTimeout(peripheralID: "p2", at: now)
|
||||
scheduler.recordConnectionTimeout(peripheralID: "p3", at: now)
|
||||
|
||||
let threshold = scheduler.updateRSSIThreshold(
|
||||
connectedCount: 1,
|
||||
connectedOrConnectingLinkCount: 1,
|
||||
now: now.addingTimeInterval(1)
|
||||
)
|
||||
|
||||
#expect(threshold == TransportConfig.bleRSSIHighTimeoutThreshold)
|
||||
#expect(scheduler.dynamicRSSIThreshold == TransportConfig.bleRSSIHighTimeoutThreshold)
|
||||
}
|
||||
|
||||
@Test
|
||||
func rssiThresholdTightensWhenCandidateQueueIsFull() {
|
||||
let scheduler = BLEConnectionScheduler<String>(candidateCap: 2)
|
||||
let now = Date()
|
||||
scheduler.enqueue(makeCandidate(id: "p1", rssi: -65, now: now))
|
||||
scheduler.enqueue(makeCandidate(id: "p2", rssi: -66, now: now))
|
||||
|
||||
let threshold = scheduler.updateRSSIThreshold(
|
||||
connectedCount: 1,
|
||||
connectedOrConnectingLinkCount: 1,
|
||||
now: now
|
||||
)
|
||||
|
||||
#expect(threshold == TransportConfig.bleRSSIConnectedThreshold)
|
||||
}
|
||||
}
|
||||
|
||||
private func makeCandidate(id: String, rssi: Int, now: Date) -> BLEConnectionCandidate<String> {
|
||||
BLEConnectionCandidate(
|
||||
peripheral: id,
|
||||
peripheralID: id,
|
||||
rssi: rssi,
|
||||
name: id,
|
||||
isConnectable: true,
|
||||
discoveredAt: now
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import BitFoundation
|
||||
import Testing
|
||||
@testable import bitchat
|
||||
|
||||
struct BLELinkStateStoreTests {
|
||||
@Test
|
||||
func centralBindingExposesDirectLinkStateAndLinks() {
|
||||
let store = BLELinkStateStore()
|
||||
let peerID = PeerID(str: "1122334455667788")
|
||||
|
||||
store.bindCentral("central-a", to: peerID)
|
||||
|
||||
#expect(store.peerID(forCentralUUID: "central-a") == peerID)
|
||||
#expect(store.directLinkState(for: peerID) == BLEDirectLinkState(hasPeripheral: false, hasCentral: true))
|
||||
#expect(store.links(to: peerID) == [.central("central-a")])
|
||||
}
|
||||
|
||||
@Test
|
||||
func linksReturnsAllCentralBindingsForPeer() {
|
||||
let store = BLELinkStateStore()
|
||||
let peerID = PeerID(str: "1122334455667788")
|
||||
let otherPeerID = PeerID(str: "8899aabbccddeeff")
|
||||
|
||||
store.bindCentral("central-a", to: peerID)
|
||||
store.bindCentral("central-b", to: peerID)
|
||||
store.bindCentral("central-c", to: otherPeerID)
|
||||
|
||||
#expect(store.links(to: peerID) == [.central("central-a"), .central("central-b")])
|
||||
}
|
||||
|
||||
@Test
|
||||
func clearCentralsReturnsPreviouslyBoundPeerIDsAndClearsLookups() {
|
||||
let store = BLELinkStateStore()
|
||||
let firstPeerID = PeerID(str: "1122334455667788")
|
||||
let secondPeerID = PeerID(str: "8899aabbccddeeff")
|
||||
|
||||
store.bindCentral("central-a", to: firstPeerID)
|
||||
store.bindCentral("central-b", to: secondPeerID)
|
||||
|
||||
let removedPeerIDs = Set(store.clearCentrals())
|
||||
|
||||
#expect(removedPeerIDs == Set([firstPeerID, secondPeerID]))
|
||||
#expect(store.peerID(forCentralUUID: "central-a") == nil)
|
||||
#expect(store.links(to: firstPeerID).isEmpty)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user