Files
bitchat/bitchatTests/Services/BLEConnectionSchedulerTests.swift
T
ffa0d7aa4f [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>
2026-05-31 14:24:09 +02:00

157 lines
5.2 KiB
Swift

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
)
}