mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-09-19 05:00:48 +00:00
* Cohere per-link Noise auth and rebind containment into BLELinkAuthState The authenticated-link owners, the reconnect revalidation policy, and the two rebind-containment cooldowns were four loose bleQueue-owned maps whose invariants lived in call-site discipline: every teardown path had to remember to retire the proof AND close the revalidation epoch (the pair appeared seven times), and both cooldowns hand-rolled the same prune-check-record dance. BLELinkAuthState owns them as whole transitions — retireLink, retireLinks(ownedBy:), permitRebind, permitRedundantRetirement — with the ownership question (bleQueue today, engine after the option-B flip) answered in one place. No behavior change; the one call-site reordering (redundant retirement computes the survivor before the cooldown check instead of after) is outcome-equivalent since the cooldown only ever recorded when a survivor existed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Split identity-link bindings out of the physical link store BLELinkStateStore owned two different kinds of truth: what physical links exist (CB handles, connect lifecycles, characteristics, stream assemblers) and who each link belongs to (peer bindings in both roles plus the preferred-peripheral reverse map for directed sends and fanout collapse). The bindings now live on BLELinkBindings — same bleQueue ownership, whole-transition methods, direct tests for the rotation reverse-map cleanup and the preferred-link survivor repair that were previously only exercised end to end. Composed operations that need both truths (remove-with-repair, direct link state, the subscribed- central snapshot, bind-only-live-links) live on the transport as explicitly bleQueue-confined helpers. This is the structural half of the option-B boundary flip (docs/BLE-ARCHITECTURE-V3.md): ownership of the bindings can now move to the engine without touching what-links-exist. An audit of every physical clear/remove found three sites (emergency clear, both unauthorized branches) that needed explicit binding-clear pairing under the split — each now clears both. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Fix iOS-gated constructors and preserve containment cooldowns on reset CI caught what the macOS SwiftPM build cannot see: two #if os(iOS) sites still passed the peerID field that slice B1 removed from BLEPeripheralLinkState (willRestoreState in BLEService and armPendingBackgroundConnects in BLERadioController). Both fixed and verified with a local iOS simulator xcodebuild. Codex also caught a real regression: BLELinkAuthState.removeAll() cleared the rebind/retirement cooldown maps, which the original panic and emergency reset paths deliberately left alive. A stable CoreBluetooth UUID must not earn a fresh rebind allowance just because the session state around it was wiped. removeAll() now clears only the proofs and revalidation epochs, and BLELinkAuthStateTests pins the survival invariant along with the other auth-state transitions. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: jack <jackjackbits@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
112 lines
4.1 KiB
Swift
112 lines
4.1 KiB
Swift
import BitFoundation
|
|
import Testing
|
|
@testable import bitchat
|
|
|
|
struct BLELinkBindingsTests {
|
|
private let peerID = PeerID(str: "1122334455667788")
|
|
private let otherPeerID = PeerID(str: "8899aabbccddeeff")
|
|
|
|
@Test
|
|
func centralBindingExposesBoundPeerAndLinks() {
|
|
var bindings = BLELinkBindings()
|
|
|
|
bindings.bindCentral("central-a", to: peerID)
|
|
|
|
#expect(bindings.peer(forCentralUUID: "central-a") == peerID)
|
|
#expect(bindings.hasCentral(boundTo: peerID))
|
|
#expect(bindings.boundPeer(for: .central("central-a")) == peerID)
|
|
#expect(bindings.links(to: peerID) == [.central("central-a")])
|
|
}
|
|
|
|
@Test
|
|
func linksReturnsAllBindingsForPeerAcrossRoles() {
|
|
var bindings = BLELinkBindings()
|
|
|
|
bindings.bindCentral("central-a", to: peerID)
|
|
bindings.bindCentral("central-b", to: peerID)
|
|
bindings.bindCentral("central-c", to: otherPeerID)
|
|
bindings.bindPeripheral("periph-a", to: peerID)
|
|
|
|
#expect(bindings.links(to: peerID) == [.central("central-a"), .central("central-b"), .peripheral("periph-a")])
|
|
}
|
|
|
|
@Test
|
|
func clearCentralsReturnsPreviouslyBoundPeerIDsAndClearsLookups() {
|
|
var bindings = BLELinkBindings()
|
|
|
|
bindings.bindCentral("central-a", to: peerID)
|
|
bindings.bindCentral("central-b", to: otherPeerID)
|
|
|
|
let removedPeerIDs = Set(bindings.clearCentrals())
|
|
|
|
#expect(removedPeerIDs == Set([peerID, otherPeerID]))
|
|
#expect(bindings.peer(forCentralUUID: "central-a") == nil)
|
|
#expect(bindings.links(to: peerID).isEmpty)
|
|
}
|
|
|
|
@Test
|
|
func rotationRebindDropsTheRetiredIdentitysReverseMapping() {
|
|
var bindings = BLELinkBindings()
|
|
bindings.bindPeripheral("periph-a", to: peerID)
|
|
#expect(bindings.preferredPeripheralUUID(for: peerID) == "periph-a")
|
|
|
|
// The link's owner rotates: the old identity must no longer claim
|
|
// this link as its preferred peripheral.
|
|
bindings.bindPeripheral("periph-a", to: otherPeerID)
|
|
|
|
#expect(bindings.preferredPeripheralUUID(for: peerID) == nil)
|
|
#expect(bindings.preferredPeripheralUUID(for: otherPeerID) == "periph-a")
|
|
#expect(bindings.peer(forPeripheralID: "periph-a") == otherPeerID)
|
|
}
|
|
|
|
@Test
|
|
func removingThePreferredLinkRepairsOntoTheChosenSurvivor() {
|
|
var bindings = BLELinkBindings()
|
|
bindings.bindPeripheral("periph-a", to: peerID)
|
|
bindings.bindPeripheral("periph-b", to: peerID)
|
|
// periph-b bound last: it is the preferred link.
|
|
#expect(bindings.preferredPeripheralUUID(for: peerID) == "periph-b")
|
|
|
|
let removed = bindings.peripheralRemoved("periph-b") { remaining in
|
|
#expect(remaining == ["periph-a"])
|
|
return remaining.first
|
|
}
|
|
|
|
#expect(removed == peerID)
|
|
#expect(bindings.preferredPeripheralUUID(for: peerID) == "periph-a")
|
|
#expect(bindings.links(to: peerID) == [.peripheral("periph-a")])
|
|
}
|
|
|
|
@Test
|
|
func removingADuplicateLinkDoesNotStrandThePreferredOne() {
|
|
var bindings = BLELinkBindings()
|
|
bindings.bindPeripheral("periph-a", to: peerID)
|
|
bindings.bindPeripheral("periph-b", to: peerID)
|
|
|
|
// Removing the non-preferred duplicate must leave the reverse map
|
|
// untouched (no repair callback consulted for a non-preferred link).
|
|
let removed = bindings.peripheralRemoved("periph-a") { _ in
|
|
Issue.record("survivor choice must not run for a non-preferred link")
|
|
return nil
|
|
}
|
|
|
|
#expect(removed == peerID)
|
|
#expect(bindings.preferredPeripheralUUID(for: peerID) == "periph-b")
|
|
}
|
|
|
|
@Test
|
|
func removingTheLastLinkClearsThePreferredMapping() {
|
|
var bindings = BLELinkBindings()
|
|
bindings.bindPeripheral("periph-a", to: peerID)
|
|
|
|
let removed = bindings.peripheralRemoved("periph-a") { remaining in
|
|
#expect(remaining.isEmpty)
|
|
return nil
|
|
}
|
|
|
|
#expect(removed == peerID)
|
|
#expect(bindings.preferredPeripheralUUID(for: peerID) == nil)
|
|
#expect(bindings.links(to: peerID).isEmpty)
|
|
}
|
|
}
|