[codex] Refactor BLE transport event handling (#1266)

* Refactor BLE transport event handling

* Make image output paths unique

* Keep queued Nostr read receipts alive

* Allow self-authored RSR ingress replies

---------

Co-authored-by: jack <jackjackbits@users.noreply.github.com>
This commit is contained in:
jack
2026-05-31 13:58:27 +02:00
committed by GitHub
co-authored by jack
parent 3be8fbf1c4
commit ab0da61533
19 changed files with 575 additions and 152 deletions
+71
View File
@@ -92,6 +92,77 @@ struct BLEServiceCoreTests {
_ = await TestHelpers.waitUntil({ !ble.currentPeerSnapshots().isEmpty }, timeout: 0.3)
#expect(ble.currentPeerSnapshots().isEmpty)
}
@Test
func ingressAllowsRelayedSenderOnBoundLink() async throws {
let ble = makeService()
let boundPeer = PeerID(str: "1122334455667788")
let relayedSender = PeerID(str: "8899aabbccddeeff")
let packet = makePublicPacket(
content: "Relayed",
sender: relayedSender,
timestamp: UInt64(Date().timeIntervalSince1970 * 1000)
)
#expect(ble._test_acceptsIngress(packet: packet, boundPeerID: boundPeer))
}
@Test
func ingressRejectsDirectAnnounceThatConflictsWithBoundLink() async throws {
let ble = makeService()
let boundPeer = PeerID(str: "1122334455667788")
let claimedPeer = PeerID(str: "8899aabbccddeeff")
let packet = BitchatPacket(
type: MessageType.announce.rawValue,
senderID: Data(hexString: claimedPeer.id) ?? Data(),
recipientID: nil,
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
payload: Data(),
signature: nil,
ttl: 7
)
#expect(!ble._test_acceptsIngress(packet: packet, boundPeerID: boundPeer))
}
@Test
func ingressRejectsSelfLoopbackBeforeSpoofChecks() async throws {
let ble = makeService()
let packet = makePublicPacket(
content: "Loopback",
sender: ble.myPeerID,
timestamp: UInt64(Date().timeIntervalSince1970 * 1000)
)
#expect(!ble._test_acceptsIngress(packet: packet, boundPeerID: PeerID(str: "1122334455667788")))
}
@Test
func ingressAllowsSelfAuthoredRSRWithTTLZeroFromBoundPeer() async throws {
let ble = makeService()
var packet = makePublicPacket(
content: "Recovered by sync",
sender: ble.myPeerID,
timestamp: UInt64(Date().timeIntervalSince1970 * 1000)
)
packet.isRSR = true
packet.ttl = 0
#expect(ble._test_acceptsIngress(packet: packet, boundPeerID: PeerID(str: "1122334455667788")))
}
@Test
func ingressRecordSuppressesSecondLinkDuplicate() async throws {
let ble = makeService()
let packet = makePublicPacket(
content: "Duplicate link copy",
sender: PeerID(str: "1122334455667788"),
timestamp: UInt64(Date().timeIntervalSince1970 * 1000)
)
#expect(ble._test_recordIngressIfNew(packet: packet, linkID: "central-a"))
#expect(!ble._test_recordIngressIfNew(packet: packet, linkID: "central-b"))
}
}
private func makeService() -> BLEService {
+2
View File
@@ -42,6 +42,7 @@ struct ChatViewModelInitializationTests {
// The viewModel should set itself as the transport delegate
#expect(transport.delegate === viewModel)
#expect(transport.eventDelegate === viewModel)
}
@Test @MainActor
@@ -421,6 +422,7 @@ struct ChatViewModelReceivingTests {
// Message may or may not appear due to rate limiting/pipeline batching
// The important thing is no crash and delegate was called
#expect(transport.delegate === viewModel)
#expect(transport.eventDelegate === viewModel)
}
@Test @MainActor
@@ -64,4 +64,19 @@ struct ImageUtilsTests {
#expect(data.starts(with: Data([0xFF, 0xD8])))
#expect(data.count > 0)
}
@Test
func processImage_usesUniqueOutputURLs() throws {
let image = makePlatformImage(size: CGSize(width: 64, height: 64))
let firstURL = try ImageUtils.processImage(image, maxDimension: 64)
let secondURL = try ImageUtils.processImage(image, maxDimension: 64)
defer {
try? FileManager.default.removeItem(at: firstURL)
try? FileManager.default.removeItem(at: secondURL)
}
#expect(firstURL != secondURL)
#expect(FileManager.default.fileExists(atPath: firstURL.path))
#expect(FileManager.default.fileExists(atPath: secondURL.path))
}
}
+32 -5
View File
@@ -195,12 +195,39 @@ struct GossipSyncManagerTests {
manager._performMaintenanceSynchronously(now: Date())
let sentPackets = delegate.packets
#expect(sentPackets.count == 3)
#expect(sentPackets.count == 1)
let decoded = sentPackets.compactMap { RequestSyncPacket.decode(from: $0.payload) }
#expect(decoded.count == 3)
#expect(decoded[0].types == .publicMessages)
#expect(decoded[1].types == .fragment)
#expect(decoded[2].types == .fileTransfer)
#expect(decoded.count == 1)
let types = try #require(decoded.first?.types)
#expect(types.contains(.announce))
#expect(types.contains(.message))
#expect(types.contains(.fragment))
#expect(types.contains(.fileTransfer))
}
@Test func initialSyncCoalescesEnabledTypes() async throws {
var config = GossipSyncManager.Config()
config.seenCapacity = 10
config.fragmentCapacity = 5
config.fileTransferCapacity = 4
config.fragmentSyncIntervalSeconds = 1
config.fileTransferSyncIntervalSeconds = 1
let requestSyncManager = RequestSyncManager()
let manager = GossipSyncManager(myPeerID: myPeerID, config: config, requestSyncManager: requestSyncManager)
let delegate = RecordingDelegate()
manager.delegate = delegate
manager.scheduleInitialSyncToPeer(PeerID(str: "FFFFFFFFFFFFFFFF"), delaySeconds: 0.0)
try await TestHelpers.waitFor({ delegate.packets.count == 1 }, timeout: TestConstants.shortTimeout)
let packet = try #require(delegate.packets.first)
let request = try #require(RequestSyncPacket.decode(from: packet.payload))
let types = try #require(request.types)
#expect(types.contains(.announce))
#expect(types.contains(.message))
#expect(types.contains(.fragment))
#expect(types.contains(.fileTransfer))
}
@Test func handleRequestSyncHonorsTypeFilter() async throws {
+1
View File
@@ -19,6 +19,7 @@ final class MockTransport: Transport {
// MARK: - Protocol Properties
weak var delegate: BitchatDelegate?
weak var eventDelegate: TransportEventDelegate?
weak var peerEventsDelegate: TransportPeerEventsDelegate?
var myPeerID: PeerID = PeerID(str: "TESTPEER")
@@ -97,6 +97,27 @@ struct NotificationStreamAssemblerTests {
#expect(decoded.timestamp == packet.timestamp)
}
@Test func discardsPKCSPaddingBetweenNotificationFrames() throws {
var assembler = NotificationStreamAssembler()
let packet1 = makePacket(timestamp: 0x111)
let packet2 = makePacket(timestamp: 0x222)
let paddedFrame1 = try #require(packet1.toBinaryData(padding: true), "Failed to encode first padded packet")
let paddedFrame2 = try #require(packet2.toBinaryData(padding: true), "Failed to encode second padded packet")
var result = assembler.append(paddedFrame1)
#expect(result.frames.count == 1)
#expect(result.droppedPrefixes.isEmpty)
#expect(result.reset == false)
result = assembler.append(paddedFrame2)
#expect(result.frames.count == 1)
#expect(result.droppedPrefixes.isEmpty)
#expect(result.reset == false)
let decoded = try #require(BinaryProtocol.decode(result.frames[0]), "Failed to decode second frame")
#expect(decoded.timestamp == packet2.timestamp)
}
func testAssemblesCompressedLargeFrame() throws {
var assembler = NotificationStreamAssembler()
+1
View File
@@ -15,6 +15,7 @@ private final class DefaultDelegateProbe: BitchatDelegate {
private final class DefaultTransportProbe: Transport {
weak var delegate: BitchatDelegate?
weak var eventDelegate: TransportEventDelegate?
weak var peerEventsDelegate: TransportPeerEventsDelegate?
let subject = CurrentValueSubject<[TransportPeerSnapshot], Never>([])
@@ -0,0 +1,75 @@
import Foundation
import Testing
@testable import bitchat
struct BLEOutboundWriteBufferTests {
@Test
func enqueueOrdersWritesByPriority() {
var buffer = BLEOutboundWriteBuffer()
let peerID = "peer-1"
_ = buffer.enqueue(
data: Data(repeating: 0x01, count: 4),
for: peerID,
priority: .fileTransfer,
capBytes: 64
)
_ = buffer.enqueue(
data: Data(repeating: 0x02, count: 4),
for: peerID,
priority: .high,
capBytes: 64
)
_ = buffer.enqueue(
data: Data(repeating: 0x03, count: 4),
for: peerID,
priority: .fragment(totalFragments: 2),
capBytes: 64
)
let writes = buffer.takeAll(for: peerID)
#expect(writes.map { Int($0.data.first ?? 0) } == [0x02, 0x03, 0x01])
}
@Test
func enqueueTrimsLowestPriorityItemsToCap() {
var buffer = BLEOutboundWriteBuffer()
let peerID = "peer-1"
_ = buffer.enqueue(data: Data(repeating: 0x01, count: 8), for: peerID, priority: .low, capBytes: 16)
_ = buffer.enqueue(data: Data(repeating: 0x02, count: 8), for: peerID, priority: .fileTransfer, capBytes: 16)
let result = buffer.enqueue(data: Data(repeating: 0x03, count: 8), for: peerID, priority: .high, capBytes: 16)
if case let .enqueued(trimmedBytes, remainingBytes) = result {
#expect(trimmedBytes == 8)
#expect(remainingBytes == 16)
} else {
Issue.record("Expected buffered write to trim, not drop as oversized")
}
let writes = buffer.takeAll(for: peerID)
#expect(writes.map { Int($0.data.first ?? 0) } == [0x03, 0x02])
}
@Test
func enqueueRejectsOversizedSingleChunk() {
var buffer = BLEOutboundWriteBuffer()
let result = buffer.enqueue(
data: Data(repeating: 0x01, count: 32),
for: "peer-1",
priority: .high,
capBytes: 16
)
if case let .oversized(bytes) = result {
#expect(bytes == 32)
} else {
Issue.record("Expected oversized write to be rejected")
}
#expect(buffer.peripheralIDs.isEmpty)
}
}
@@ -306,6 +306,7 @@ struct NostrTransportTests {
let secondPayload = try decodeEmbeddedPayload(from: secondEvent, recipient: recipient).payload
#expect(secondPayload.type == .readReceipt)
#expect(String(data: secondPayload.data, encoding: .utf8) == "read-2")
withExtendedLifetime(transport) {}
}
@Test("Concurrent read receipt enqueue does not crash")