[codex] Refine BLE ingress fanout (#1280)

* 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

* Allow self-authored RSR ingress replies

* Harden read receipt queue test timing

---------

Co-authored-by: jack <jackjackbits@users.noreply.github.com>
This commit is contained in:
jack
2026-05-31 14:08:30 +02:00
committed by GitHub
co-authored by jack
parent ab0da61533
commit df36b19afe
18 changed files with 1503 additions and 383 deletions
+30
View File
@@ -163,6 +163,36 @@ struct BLEServiceCoreTests {
#expect(ble._test_recordIngressIfNew(packet: packet, linkID: "central-a"))
#expect(!ble._test_recordIngressIfNew(packet: packet, linkID: "central-b"))
}
@Test
func modifiedServices_rediscoverWhenBitChatServiceIsInvalidated() async throws {
let otherService = CBUUID(string: "0000180F-0000-1000-8000-00805F9B34FB")
#expect(BLEService._test_shouldRediscoverBitChatService(
invalidatedServiceUUIDs: [BLEService.serviceUUID],
cachedServiceUUIDs: [otherService]
))
}
@Test
func modifiedServices_rediscoverWhenCachedServicesNoLongerIncludeBitChat() async throws {
let otherService = CBUUID(string: "0000180F-0000-1000-8000-00805F9B34FB")
#expect(BLEService._test_shouldRediscoverBitChatService(
invalidatedServiceUUIDs: [otherService],
cachedServiceUUIDs: [otherService]
))
}
@Test
func modifiedServices_ignoreUnrelatedInvalidationWhenBitChatIsStillCached() async throws {
let otherService = CBUUID(string: "0000180F-0000-1000-8000-00805F9B34FB")
#expect(!BLEService._test_shouldRediscoverBitChatService(
invalidatedServiceUUIDs: [otherService],
cachedServiceUUIDs: [BLEService.serviceUUID, otherService]
))
}
}
private func makeService() -> BLEService {
@@ -0,0 +1,95 @@
import BitFoundation
import Foundation
import Testing
@testable import bitchat
struct BLEFanoutSelectorTests {
@Test
func directedSendUsesAllNonIngressLinks() {
let selection = BLEFanoutSelector.selectLinks(
peripheralIDs: ["p1", "p2"],
centralIDs: ["c1", "c2"],
ingressLink: .central("c1"),
directedPeerHint: PeerID(str: "1122334455667788"),
packetType: MessageType.noiseEncrypted.rawValue,
messageID: "message-1"
)
#expect(selection.peripheralIDs == Set(["p1", "p2"]))
#expect(selection.centralIDs == Set(["c2"]))
}
@Test
func directedSendExcludesAllLinksToIngressPeer() {
let selection = BLEFanoutSelector.selectLinks(
peripheralIDs: ["p1", "p2"],
centralIDs: ["c1", "c2"],
ingressLink: .central("c1"),
excludedLinks: [.peripheral("p2"), .central("c2")],
directedPeerHint: PeerID(str: "1122334455667788"),
packetType: MessageType.noiseEncrypted.rawValue,
messageID: "message-1"
)
#expect(selection.peripheralIDs == Set(["p1"]))
#expect(selection.centralIDs.isEmpty)
}
@Test
func controlPacketsUseAllNonIngressLinks() {
let selection = BLEFanoutSelector.selectLinks(
peripheralIDs: ["p1", "p2", "p3"],
centralIDs: ["c1", "c2", "c3"],
ingressLink: .peripheral("p2"),
directedPeerHint: nil,
packetType: MessageType.requestSync.rawValue,
messageID: "message-1"
)
#expect(selection.peripheralIDs == Set(["p1", "p3"]))
#expect(selection.centralIDs == Set(["c1", "c2", "c3"]))
}
@Test
func broadcastPacketsUseDeterministicSubsetAfterIngressExclusion() {
let peripherals = (1...8).map { "p\($0)" }
let centrals = (1...8).map { "c\($0)" }
let first = BLEFanoutSelector.selectLinks(
peripheralIDs: peripherals,
centralIDs: centrals,
ingressLink: .peripheral("p4"),
directedPeerHint: nil,
packetType: MessageType.message.rawValue,
messageID: "message-1"
)
let second = BLEFanoutSelector.selectLinks(
peripheralIDs: peripherals,
centralIDs: centrals,
ingressLink: .peripheral("p4"),
directedPeerHint: nil,
packetType: MessageType.message.rawValue,
messageID: "message-1"
)
#expect(first == second)
#expect(!first.peripheralIDs.contains("p4"))
#expect(first.peripheralIDs.count == 4)
#expect(first.centralIDs.count == 4)
}
@Test
func broadcastWithTwoLinksKeepsBothAfterIngressExclusion() {
let selection = BLEFanoutSelector.selectLinks(
peripheralIDs: ["p1", "p2"],
centralIDs: ["c1", "c2"],
ingressLink: nil,
directedPeerHint: nil,
packetType: MessageType.message.rawValue,
messageID: "message-1"
)
#expect(selection.peripheralIDs == Set(["p1", "p2"]))
#expect(selection.centralIDs == Set(["c1", "c2"]))
}
}
@@ -0,0 +1,205 @@
import BitFoundation
import Foundation
import Testing
@testable import bitchat
struct BLEFragmentAssemblyBufferTests {
@Test
func appendCompletesOutOfOrderFragments() throws {
var buffer = BLEFragmentAssemblyBuffer()
let original = makePacket(payload: makePayload(count: 512))
let fragmentPackets = try makeFragments(for: original, chunkSize: 128, fragmentID: Data(repeating: 0x01, count: 8))
let headers = try fragmentPackets.reversed().map { try #require(BLEFragmentHeader(packet: $0)) }
var result: BLEFragmentAssemblyBuffer.AppendResult?
for header in headers {
result = buffer.append(header, maxInFlightAssemblies: 8)
}
if case let .complete(header, reassembledData, started) = result {
#expect(header.total == fragmentPackets.count)
#expect(!started)
#expect(BinaryProtocol.decode(reassembledData)?.payload == original.payload)
} else {
Issue.record("Expected final fragment to complete reassembly")
}
}
@Test
func appendDuplicateFragmentDoesNotCompleteEarly() throws {
var buffer = BLEFragmentAssemblyBuffer()
let original = makePacket(payload: makePayload(count: 384))
let fragmentPackets = try makeFragments(for: original, chunkSize: 128, fragmentID: Data(repeating: 0x02, count: 8))
let first = try #require(BLEFragmentHeader(packet: fragmentPackets[0]))
let second = try #require(BLEFragmentHeader(packet: fragmentPackets[1]))
if case let .stored(_, started) = buffer.append(first, maxInFlightAssemblies: 8) {
#expect(started)
} else {
Issue.record("Expected first fragment to be stored")
}
if case .stored = buffer.append(first, maxInFlightAssemblies: 8) {
// Duplicate should replace the same index, not increase completion count.
} else {
Issue.record("Expected duplicate fragment to remain incomplete")
}
if case .stored = buffer.append(second, maxInFlightAssemblies: 8) {
// Still missing at least one fragment.
} else {
Issue.record("Expected assembly to remain incomplete")
}
}
@Test
func appendEvictsOldestAssemblyWhenCapIsReached() throws {
var buffer = BLEFragmentAssemblyBuffer()
let oldPacket = makePacket(payload: makePayload(count: 256, seed: 1), timestamp: 1)
let newPacket = makePacket(payload: makePayload(count: 256, seed: 2), timestamp: 2)
let oldFragments = try makeFragments(for: oldPacket, chunkSize: 128, fragmentID: Data(repeating: 0x03, count: 8))
let newFragments = try makeFragments(for: newPacket, chunkSize: 128, fragmentID: Data(repeating: 0x04, count: 8))
let oldFirst = try #require(BLEFragmentHeader(packet: oldFragments[0]))
let oldSecond = try #require(BLEFragmentHeader(packet: oldFragments[1]))
let newFirst = try #require(BLEFragmentHeader(packet: newFragments[0]))
let newSecond = try #require(BLEFragmentHeader(packet: newFragments[1]))
_ = buffer.append(oldFirst, maxInFlightAssemblies: 1, now: Date(timeIntervalSince1970: 1))
_ = buffer.append(newFirst, maxInFlightAssemblies: 1, now: Date(timeIntervalSince1970: 2))
if case let .stored(_, started) = buffer.append(oldSecond, maxInFlightAssemblies: 1) {
#expect(started)
} else {
Issue.record("Expected evicted assembly to restart when old fragment arrives")
}
if case let .stored(_, started) = buffer.append(newSecond, maxInFlightAssemblies: 1) {
#expect(started)
} else {
Issue.record("Expected new assembly to restart after old one consumed the only slot")
}
}
@Test
func appendOversizedAssemblyDropsPartialState() throws {
var buffer = BLEFragmentAssemblyBuffer()
let fragmentID = Data(repeating: 0x05, count: 8)
let first = try #require(BLEFragmentHeader(packet: makeFragmentPacket(
fragmentID: fragmentID,
index: 0,
total: 2,
originalType: MessageType.message.rawValue,
fragmentData: Data(repeating: 0x01, count: FileTransferLimits.maxPayloadBytes)
)))
let oversized = try #require(BLEFragmentHeader(packet: makeFragmentPacket(
fragmentID: fragmentID,
index: 1,
total: 2,
originalType: MessageType.message.rawValue,
fragmentData: Data([0x02])
)))
_ = buffer.append(first, maxInFlightAssemblies: 8)
let result = buffer.append(oversized, maxInFlightAssemblies: 8)
if case let .oversized(_, projectedSize, limit, started) = result {
#expect(projectedSize == FileTransferLimits.maxPayloadBytes + 1)
#expect(limit == FileTransferLimits.maxPayloadBytes)
#expect(!started)
} else {
Issue.record("Expected oversized fragment assembly to be evicted")
}
if case let .stored(_, started) = buffer.append(oversized, maxInFlightAssemblies: 8) {
#expect(started)
} else {
Issue.record("Expected later fragment to start a clean assembly")
}
}
@Test
func removeExpiredDropsOldAssemblies() throws {
var buffer = BLEFragmentAssemblyBuffer()
let packet = makePacket(payload: makePayload(count: 256))
let fragments = try makeFragments(for: packet, chunkSize: 128, fragmentID: Data(repeating: 0x06, count: 8))
let first = try #require(BLEFragmentHeader(packet: fragments[0]))
let second = try #require(BLEFragmentHeader(packet: fragments[1]))
_ = buffer.append(first, maxInFlightAssemblies: 8, now: Date(timeIntervalSince1970: 1))
#expect(buffer.removeExpired(before: Date(timeIntervalSince1970: 2)) == 1)
if case let .stored(_, started) = buffer.append(second, maxInFlightAssemblies: 8) {
#expect(started)
} else {
Issue.record("Expected expired assembly to be gone")
}
}
private func makePacket(payload: Data, timestamp: UInt64 = 0x0102030405) -> BitchatPacket {
BitchatPacket(
type: MessageType.message.rawValue,
senderID: Data([0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77]),
recipientID: nil,
timestamp: timestamp,
payload: payload,
signature: nil,
ttl: 3
)
}
private func makePayload(count: Int, seed: UInt64 = 0x1234ABCD) -> Data {
var state = seed
return Data((0..<count).map { _ in
state = state &* 6364136223846793005 &+ 1442695040888963407
return UInt8(truncatingIfNeeded: state >> 32)
})
}
private func makeFragments(for packet: BitchatPacket, chunkSize: Int, fragmentID: Data) throws -> [BitchatPacket] {
let fullData = try #require(packet.toBinaryData(padding: false))
let chunks = stride(from: 0, to: fullData.count, by: chunkSize).map { offset in
Data(fullData[offset..<min(offset + chunkSize, fullData.count)])
}
return chunks.enumerated().map { index, chunk in
makeFragmentPacket(
fragmentID: fragmentID,
index: index,
total: chunks.count,
originalType: packet.type,
fragmentData: chunk,
senderID: packet.senderID,
recipientID: packet.recipientID,
timestamp: packet.timestamp
)
}
}
private func makeFragmentPacket(
fragmentID: Data,
index: Int,
total: Int,
originalType: UInt8,
fragmentData: Data,
senderID: Data = Data([0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77]),
recipientID: Data? = nil,
timestamp: UInt64 = 0x0102030405
) -> BitchatPacket {
var payload = Data()
payload.append(fragmentID)
payload.append(contentsOf: withUnsafeBytes(of: UInt16(index).bigEndian) { Data($0) })
payload.append(contentsOf: withUnsafeBytes(of: UInt16(total).bigEndian) { Data($0) })
payload.append(originalType)
payload.append(fragmentData)
return BitchatPacket(
type: MessageType.fragment.rawValue,
senderID: senderID,
recipientID: recipientID,
timestamp: timestamp,
payload: payload,
signature: nil,
ttl: 3
)
}
}
@@ -0,0 +1,140 @@
import BitFoundation
import Foundation
import Testing
@testable import bitchat
struct BLEInboundWriteBufferTests {
@Test
func appendDecodesPacketAcrossChunkedWrites() throws {
var buffer = BLEInboundWriteBuffer()
let packet = makePacket()
let frame = try #require(packet.toBinaryData(padding: false))
let splitIndex = max(1, frame.count / 2)
let firstResult = buffer.append(
chunks: [BLEInboundWriteChunk(offset: 0, data: frame.prefix(splitIndex))],
for: "central-1",
capBytes: 1024
)
if case let .waiting(metadata) = firstResult {
#expect(metadata.accumulatedBytes == splitIndex)
#expect(metadata.appendedBytes == splitIndex)
#expect(metadata.offsets == [0])
#expect(metadata.packetType == MessageType.message.rawValue)
} else {
Issue.record("Expected first chunk to wait for more data")
}
let secondResult = buffer.append(
chunks: [BLEInboundWriteChunk(offset: splitIndex, data: frame.suffix(from: splitIndex))],
for: "central-1",
capBytes: 1024
)
if case let .decoded(decoded, metadata) = secondResult {
#expect(decoded.type == packet.type)
#expect(decoded.senderID == packet.senderID)
#expect(decoded.payload == packet.payload)
#expect(metadata.accumulatedBytes == frame.count)
#expect(metadata.offsets == [splitIndex])
} else {
Issue.record("Expected complete frame to decode")
}
}
@Test
func appendMergesMultipleOffsetChunksInOneCall() throws {
var buffer = BLEInboundWriteBuffer()
let packet = makePacket(timestamp: 0xABC)
let frame = try #require(packet.toBinaryData(padding: false))
let splitIndex = max(1, frame.count / 3)
let result = buffer.append(
chunks: [
BLEInboundWriteChunk(offset: 0, data: frame.prefix(splitIndex)),
BLEInboundWriteChunk(offset: splitIndex, data: frame.suffix(from: splitIndex))
],
for: "central-1",
capBytes: 1024
)
if case let .decoded(decoded, metadata) = result {
#expect(decoded.timestamp == packet.timestamp)
#expect(metadata.appendedBytes == frame.count)
#expect(metadata.offsets == [0, splitIndex])
} else {
Issue.record("Expected merged chunks to decode")
}
}
@Test
func appendDropsOversizedBufferAndAllowsLaterDecode() throws {
var buffer = BLEInboundWriteBuffer()
let oversized = buffer.append(
chunks: [BLEInboundWriteChunk(offset: 0, data: Data(repeating: 0xAA, count: 8))],
for: "central-1",
capBytes: 4
)
if case let .oversized(metadata) = oversized {
#expect(metadata.accumulatedBytes == 8)
#expect(metadata.appendedBytes == 8)
} else {
Issue.record("Expected oversized buffer to be dropped")
}
let packet = makePacket(timestamp: 0xDEF)
let frame = try #require(packet.toBinaryData(padding: false))
let decoded = buffer.append(
chunks: [BLEInboundWriteChunk(offset: 0, data: frame)],
for: "central-1",
capBytes: 1024
)
if case let .decoded(decodedPacket, _) = decoded {
#expect(decodedPacket.timestamp == packet.timestamp)
} else {
Issue.record("Expected later clean frame to decode")
}
}
@Test
func removeAllClearsPartialWrites() throws {
var buffer = BLEInboundWriteBuffer()
let packet = makePacket()
let frame = try #require(packet.toBinaryData(padding: false))
let splitIndex = max(1, frame.count / 2)
_ = buffer.append(
chunks: [BLEInboundWriteChunk(offset: 0, data: frame.prefix(splitIndex))],
for: "central-1",
capBytes: 1024
)
buffer.removeAll()
let result = buffer.append(
chunks: [BLEInboundWriteChunk(offset: splitIndex, data: frame.suffix(from: splitIndex))],
for: "central-1",
capBytes: 1024
)
if case .waiting = result {
// Expected: the first half was cleared, so the second half alone cannot decode.
} else {
Issue.record("Expected buffer reset to discard the earlier partial write")
}
}
private func makePacket(timestamp: UInt64 = 0x0102030405) -> BitchatPacket {
BitchatPacket(
type: MessageType.message.rawValue,
senderID: Data([0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77]),
recipientID: nil,
timestamp: timestamp,
payload: Data([0xDE, 0xAD, 0xBE, 0xEF]),
signature: nil,
ttl: 3
)
}
}
@@ -0,0 +1,178 @@
import Foundation
import Testing
import BitFoundation
@testable import bitchat
struct BLEIngressLinkRegistryTests {
@Test
func recordIfNewSuppressesDuplicateWithinLifetime() {
var registry = BLEIngressLinkRegistry()
let packet = makePacket(sender: PeerID(str: "1122334455667788"), timestamp: 1)
let peer = PeerID(str: "1122334455667788")
let now = Date()
let firstRecord = registry.recordIfNew(packet, link: .central("central-a"), peerID: peer, now: now, lifetime: 3.0)
let duplicateRecord = registry.recordIfNew(packet, link: .peripheral("peripheral-b"), peerID: peer, now: now.addingTimeInterval(1.0), lifetime: 3.0)
#expect(firstRecord)
#expect(!duplicateRecord)
#expect(registry.link(for: packet) == .central("central-a"))
#expect(registry.peerID(for: packet) == peer)
}
@Test
func recordIfNewAllowsExpiredDuplicateAndUpdatesLink() {
var registry = BLEIngressLinkRegistry()
let packet = makePacket(sender: PeerID(str: "1122334455667788"), timestamp: 1)
let peer = PeerID(str: "1122334455667788")
let now = Date()
let firstRecord = registry.recordIfNew(packet, link: .central("central-a"), peerID: peer, now: now, lifetime: 3.0)
let expiredRecord = registry.recordIfNew(packet, link: .peripheral("peripheral-b"), peerID: peer, now: now.addingTimeInterval(4.0), lifetime: 3.0)
#expect(firstRecord)
#expect(expiredRecord)
#expect(registry.link(for: packet) == .peripheral("peripheral-b"))
#expect(registry.peerID(for: packet) == peer)
}
@Test
func pruneRemovesExpiredIngressLinks() {
var registry = BLEIngressLinkRegistry()
let packet = makePacket(sender: PeerID(str: "1122334455667788"), timestamp: 1)
let peer = PeerID(str: "1122334455667788")
let now = Date()
let firstRecord = registry.recordIfNew(packet, link: .central("central-a"), peerID: peer, now: now, lifetime: 3.0)
#expect(firstRecord)
registry.prune(before: now.addingTimeInterval(4.0))
#expect(registry.isEmpty)
#expect(registry.link(for: packet) == nil)
#expect(registry.peerID(for: packet) == nil)
}
@Test
func packetContextRejectsSelfLoopback() {
let localPeer = PeerID(str: "1122334455667788")
let packet = makePacket(sender: localPeer, timestamp: 1)
let result = BLEIngressLinkRegistry.packetContext(
for: packet,
claimedSenderID: localPeer,
boundPeerID: nil,
localPeerID: localPeer,
directAnnounceTTL: 7
)
#expect(result == .failure(.selfLoopback(packetType: MessageType.message.rawValue)))
}
@Test
func packetContextAllowsRelayedSenderOnBoundLink() throws {
let localPeer = PeerID(str: "0011223344556677")
let boundPeer = PeerID(str: "1122334455667788")
let relayedSender = PeerID(str: "8899aabbccddeeff")
let packet = makePacket(sender: relayedSender, timestamp: 1)
let context = try #require(trySuccess(BLEIngressLinkRegistry.packetContext(
for: packet,
claimedSenderID: relayedSender,
boundPeerID: boundPeer,
localPeerID: localPeer,
directAnnounceTTL: 7
)))
#expect(context.receivedFromPeerID == boundPeer)
#expect(context.validationPeerID == relayedSender)
}
@Test
func packetContextRejectsDirectAnnounceMismatchOnBoundLink() {
let localPeer = PeerID(str: "0011223344556677")
let boundPeer = PeerID(str: "1122334455667788")
let claimedPeer = PeerID(str: "8899aabbccddeeff")
let packet = makeAnnouncePacket(sender: claimedPeer, ttl: 7)
let result = BLEIngressLinkRegistry.packetContext(
for: packet,
claimedSenderID: claimedPeer,
boundPeerID: boundPeer,
localPeerID: localPeer,
directAnnounceTTL: 7
)
#expect(result == .failure(.directSenderMismatch(boundPeerID: boundPeer, claimedSenderID: claimedPeer)))
}
@Test
func packetContextUsesBoundPeerForRSRValidation() throws {
let localPeer = PeerID(str: "0011223344556677")
let boundPeer = PeerID(str: "1122334455667788")
let relayedSender = PeerID(str: "8899aabbccddeeff")
var packet = makePacket(sender: relayedSender, timestamp: 1)
packet.isRSR = true
let context = try #require(trySuccess(BLEIngressLinkRegistry.packetContext(
for: packet,
claimedSenderID: relayedSender,
boundPeerID: boundPeer,
localPeerID: localPeer,
directAnnounceTTL: 7
)))
#expect(context.receivedFromPeerID == boundPeer)
#expect(context.validationPeerID == boundPeer)
}
@Test
func packetContextAllowsSelfAuthoredRSRWithTTLZeroFromBoundPeer() throws {
let localPeer = PeerID(str: "0011223344556677")
let boundPeer = PeerID(str: "1122334455667788")
var packet = makePacket(sender: localPeer, timestamp: 1)
packet.isRSR = true
packet.ttl = 0
let context = try #require(trySuccess(BLEIngressLinkRegistry.packetContext(
for: packet,
claimedSenderID: localPeer,
boundPeerID: boundPeer,
localPeerID: localPeer,
directAnnounceTTL: 7
)))
#expect(context.receivedFromPeerID == boundPeer)
#expect(context.validationPeerID == boundPeer)
}
}
private func makePacket(sender: PeerID, timestamp: UInt64) -> BitchatPacket {
BitchatPacket(
type: MessageType.message.rawValue,
senderID: Data(hexString: sender.id) ?? Data(),
recipientID: nil,
timestamp: timestamp,
payload: Data("hello".utf8),
signature: nil,
ttl: 3
)
}
private func makeAnnouncePacket(sender: PeerID, ttl: UInt8) -> BitchatPacket {
BitchatPacket(
type: MessageType.announce.rawValue,
senderID: Data(hexString: sender.id) ?? Data(),
recipientID: nil,
timestamp: 1,
payload: Data(),
signature: nil,
ttl: ttl
)
}
private func trySuccess(_ result: Result<BLEIngressPacketContext, BLEIngressRejection>) -> BLEIngressPacketContext? {
guard case .success(let context) = result else {
return nil
}
return context
}
@@ -0,0 +1,70 @@
import Foundation
import Testing
@testable import bitchat
struct BLEOutboundNotificationBufferTests {
@Test
func enqueueStoresNotificationsUntilTaken() {
var buffer = BLEOutboundNotificationBuffer<String>()
let result = buffer.enqueue(
data: Data([0x01]),
targets: ["central-1"],
capCount: 2
)
if case let .enqueued(count) = result {
#expect(count == 1)
} else {
Issue.record("Expected notification to be enqueued")
}
let pending = buffer.takeAll()
#expect(pending.count == 1)
#expect(pending.first?.data == Data([0x01]))
#expect(pending.first?.targets == ["central-1"])
#expect(buffer.isEmpty)
}
@Test
func enqueueRejectsWhenCapIsFull() {
var buffer = BLEOutboundNotificationBuffer<String>()
_ = buffer.enqueue(data: Data([0x01]), targets: nil, capCount: 1)
let result = buffer.enqueue(data: Data([0x02]), targets: nil, capCount: 1)
if case let .full(count) = result {
#expect(count == 1)
} else {
Issue.record("Expected full notification buffer")
}
#expect(buffer.count == 1)
}
@Test
func prependRestoresUnsentNotificationsAheadOfNewerItems() {
var buffer = BLEOutboundNotificationBuffer<String>()
let unsent = [
BLEPendingNotification(data: Data([0x01]), targets: ["old"])
]
_ = buffer.enqueue(data: Data([0x02]), targets: ["new"], capCount: 4)
buffer.prepend(unsent)
let pending = buffer.takeAll()
#expect(pending.map { Int($0.data.first ?? 0) } == [0x01, 0x02])
}
@Test
func removeAllClearsBufferedNotifications() {
var buffer = BLEOutboundNotificationBuffer<String>()
_ = buffer.enqueue(data: Data([0x01]), targets: nil, capCount: 4)
buffer.removeAll()
#expect(buffer.isEmpty)
}
}
@@ -289,9 +289,10 @@ struct NostrTransportTests {
transport.sendReadReceipt(first, to: fullPeerID)
transport.sendReadReceipt(second, to: fullPeerID)
let sentFirst = await TestHelpers.waitUntil({ probe.sentEvents.count == 1 }, timeout: 1.5)
let readReceiptTimeout: TimeInterval = 5.0
let sentFirst = await TestHelpers.waitUntil({ probe.sentEvents.count >= 1 }, timeout: readReceiptTimeout)
try #require(sentFirst, "Expected first queued read receipt event")
let scheduledThrottle = await TestHelpers.waitUntil({ probe.scheduledActionCount == 1 }, timeout: 1.5)
let scheduledThrottle = await TestHelpers.waitUntil({ probe.scheduledActionCount == 1 }, timeout: readReceiptTimeout)
try #require(scheduledThrottle, "Expected queued throttle action after first read receipt")
let firstEvent = try #require(probe.sentEvents.first, "Expected first queued read receipt event")
let firstPayload = try decodeEmbeddedPayload(from: firstEvent, recipient: recipient).payload
@@ -300,7 +301,7 @@ struct NostrTransportTests {
try #require(probe.runNextScheduledAction(), "Expected queued throttle action after first read receipt")
let sentSecond = await TestHelpers.waitUntil({ probe.sentEvents.count == 2 }, timeout: 1.5)
let sentSecond = await TestHelpers.waitUntil({ probe.sentEvents.count >= 2 }, timeout: readReceiptTimeout)
try #require(sentSecond, "Expected second read receipt after running throttle action")
let secondEvent = try #require(probe.sentEvents.last, "Expected second queued read receipt event")
let secondPayload = try decodeEmbeddedPayload(from: secondEvent, recipient: recipient).payload
@@ -50,6 +50,26 @@ struct RelayControllerTests {
#expect(decision.delayMs >= 10 && decision.delayMs <= 35)
}
@Test
func localRecipientDoesNotRelayDirectedTraffic() async {
let decision = RelayController.decide(
ttl: 7,
senderIsSelf: false,
recipientIsSelf: true,
isEncrypted: true,
isDirectedEncrypted: true,
isFragment: false,
isDirectedFragment: false,
isHandshake: false,
isAnnounce: false,
degree: 3,
highDegreeThreshold: TransportConfig.bleHighDegreeThreshold
)
#expect(!decision.shouldRelay)
#expect(decision.newTTL == 7)
}
@Test
func fragment_relaysWithFragmentCap() async {
let decision = RelayController.decide(
@@ -374,6 +374,7 @@ final class SecureIdentityStateManagerTests: XCTestCase {
XCTAssertTrue(manager.getFavorites().isEmpty)
XCTAssertTrue(manager.getVerifiedFingerprints().isEmpty)
XCTAssertTrue(manager.getBlockedNostrPubkeys().isEmpty)
XCTAssertNil(keychain.getIdentityKey(forKey: "bitchat.identityCache.v2"))
}
func test_clearAllIdentityData_removesCachedState() async {