Add Gate 2B BLE peer session manager native tests

This commit is contained in:
John Poole 2026-05-18 14:58:20 -07:00
commit 45827c1220
5 changed files with 990 additions and 0 deletions

View file

@ -0,0 +1,107 @@
# Gate 2B: BLEPeerSessionManager Native Tests
Date: 2026-05-18 14:45 America/Los_Angeles
Scope: C++ native skeleton and pure native unit tests only.
## Summary
Gate 2B implemented a standalone C++ `BLEPeerSessionManager` skeleton under `migration/protocol_core` with no pybind11 bindings and no integration into live Python BLE behavior.
Files added:
- `migration/protocol_core/BLEPeerSessionManager.h`
- `migration/protocol_core/BLEPeerSessionManager.cpp`
- `migration/tests/native/test_ble_peer_session_manager.cpp`
- `migration/sql/mark_gate2b_protocol_session_native_tests_20260518_1445.sql`
`BLEInterface.py` was not modified. No BlueZ, Bleak, DBus, `RNS.Transport`, `BLEPeerInterface`, ESP32 BLE APIs, or live BLE tests were used.
## Implemented C++ Surface
Implemented types and enums:
- `PeerIdentity`
- `LocalRole`
- `InputDecision`
- `SessionActionType`
- `ConnectionId`
- `ConnectionSnapshot`
- `SessionAction`
- `HandshakeResult`
- `PeerSessionView`
- `BLEPeerSessionManager`
Implemented helpers:
- `isIdentityHandshakePayload`
- `identityFromPayload`
- `computeIdentityKey`
- `computeFragmenterKey`
Implemented session methods:
- `handleIdentityHandshake`
- `markConnected`
- `markDisconnected`
- `markMtu`
- `markPendingIdentity`
- `removePendingIdentity`
- `expiredPendingIdentities`
- `sessionByAddress`
- `sessionByIdentity`
## Behavior Covered
The C++ skeleton covers the Gate 2B reference cases:
| Case | Result |
|---|---|
| non-16-byte payload | `PassToReassembler`, `consumed=false` |
| new 16-byte identity | `AcceptedNewIdentity`, `consumed=true`, `accepted=true`, keys set |
| known identity duplicate same | `ConsumedDuplicateSameIdentity`, `consumed=true` |
| known identity duplicate mismatch | `ConsumedDuplicateMismatchedIdentity`, `Warn`, `consumed=true` |
| duplicate identity active elsewhere | `RejectedDuplicateIdentity`, `DisconnectCurrentPeer` |
| duplicate identity with stale/pending detach | accept new identity, `CleanupOldAddress`, `UpdatePeerAddress` |
| duplicate identity with zombie old connection | accept new identity, `DisconnectOldPeer`, `CleanupOldAddress` |
| MTU provided | result/session MTU equals provided value |
| MTU missing | MTU falls back to `23` |
| pending identity timeout | expired connection IDs returned without platform calls |
| peer address update | session address changes, fragmenter key is unchanged |
| identity key helper | first 8 bytes as 16 lowercase hex chars |
| fragmenter key helper | full 16 bytes as 32 lowercase hex chars |
| invalid identity payload | `identityFromPayload` throws `std::invalid_argument` |
## Build And Test Instructions
Build:
```sh
g++ -std=c++17 -Wall -Wextra -Werror -Imigration/protocol_core migration/protocol_core/BLEPeerSessionManager.cpp migration/tests/native/test_ble_peer_session_manager.cpp -o /tmp/test_ble_peer_session_manager
```
Run:
```sh
/tmp/test_ble_peer_session_manager
```
Observed result:
```text
BLEPeerSessionManager native tests passed
```
## Notes
The manager currently uses standard containers for native/Linux development. Gate 2G should decide whether the ESP32/microReticulum build uses the same public API with fixed-size pools internally.
The core returns adapter actions only. It does not log, disconnect peers, create Python interfaces, touch Reticulum transport state, or call BLE platform APIs.
## SQL
Companion SQL:
`migration/sql/mark_gate2b_protocol_session_native_tests_20260518_1445.sql`
The SQL marks `_handle_identity_handshake` as `NATIVE_TESTED` for phase `2_ble_protocol_session_manager` because the native tests passed. It does not mark anything `FIELD_ACCEPTED` and does not alter Phase 1 field-accepted rows.

View file

@ -0,0 +1,349 @@
#include "BLEPeerSessionManager.h"
#include <algorithm>
#include <stdexcept>
namespace ble_reticulum {
namespace {
constexpr uint16_t BLE_MINIMUM_MTU = 23;
SessionAction make_action(SessionActionType type, const ConnectionId& target) {
SessionAction action;
action.type = type;
action.target = target;
return action;
}
SessionAction make_message_action(SessionActionType type,
const ConnectionId& target,
const std::string& message) {
SessionAction action = make_action(type, target);
action.message = message;
return action;
}
} // namespace
BLEPeerSessionManager::BLEPeerSessionManager(double pending_identity_timeout,
double zombie_timeout)
: pending_identity_timeout_(pending_identity_timeout),
zombie_timeout_(zombie_timeout) {}
bool BLEPeerSessionManager::isIdentityHandshakePayload(const uint8_t* data,
size_t data_size) {
return data != nullptr && data_size == 16;
}
PeerIdentity BLEPeerSessionManager::identityFromPayload(const uint8_t* data,
size_t data_size) {
if (!isIdentityHandshakePayload(data, data_size)) {
throw std::invalid_argument("identity payload must be exactly 16 bytes");
}
PeerIdentity identity{};
std::copy(data, data + identity.size(), identity.begin());
return identity;
}
std::string BLEPeerSessionManager::computeIdentityKey(const PeerIdentity& identity) {
static constexpr char hex[] = "0123456789abcdef";
std::string out;
out.reserve(16);
for (size_t i = 0; i < 8; ++i) {
const uint8_t byte = identity[i];
out.push_back(hex[(byte >> 4) & 0x0f]);
out.push_back(hex[byte & 0x0f]);
}
return out;
}
std::string BLEPeerSessionManager::computeFragmenterKey(const PeerIdentity& identity) {
static constexpr char hex[] = "0123456789abcdef";
std::string out;
out.reserve(32);
for (uint8_t byte : identity) {
out.push_back(hex[(byte >> 4) & 0x0f]);
out.push_back(hex[byte & 0x0f]);
}
return out;
}
HandshakeResult BLEPeerSessionManager::handleIdentityHandshake(
const ConnectionSnapshot& connection,
const uint8_t* data,
size_t data_size,
double now_seconds) {
HandshakeResult result;
result.mtu = connection.negotiated_mtu.value_or(BLE_MINIMUM_MTU);
if (!isIdentityHandshakePayload(data, data_size)) {
result.decision = InputDecision::PassToReassembler;
result.consumed = false;
result.actions.push_back(make_action(SessionActionType::PassToReassembler,
connection.current));
return result;
}
PeerIdentity payload_identity = identityFromPayload(data, data_size);
result.peer_identity = payload_identity;
result.identity_key = computeIdentityKey(payload_identity);
result.fragmenter_key = computeFragmenterKey(payload_identity);
result.consumed = true;
if (connection.known_identity_for_address.has_value()) {
if (connection.known_identity_for_address.value() == payload_identity) {
result.decision = InputDecision::ConsumedDuplicateSameIdentity;
result.actions.push_back(make_action(SessionActionType::ConsumeInput,
connection.current));
return result;
}
result.decision = InputDecision::ConsumedDuplicateMismatchedIdentity;
result.actions.push_back(make_message_action(
SessionActionType::Warn,
connection.current,
"16-byte data differs from known identity; consumed as identity-like data"));
result.actions.push_back(make_action(SessionActionType::ConsumeInput,
connection.current));
return result;
}
const PeerSession* existing_session = findSessionByIdentityKey(result.identity_key);
std::optional<std::string> existing_address = connection.existing_address_for_identity;
if (!existing_address.has_value() && existing_session &&
!existing_session->current_address.empty()) {
existing_address = existing_session->current_address;
}
const bool duplicate_elsewhere =
existing_address.has_value() && existing_address.value() != connection.current.address;
if (duplicate_elsewhere) {
const bool stale_or_pending =
connection.identity_has_pending_detach ||
(!connection.existing_address_connected &&
!connection.existing_address_in_peer_table);
if (connection.existing_connection_is_zombie || stale_or_pending) {
SessionAction cleanup = make_action(SessionActionType::CleanupOldAddress,
connection.current);
cleanup.old_address = existing_address.value();
cleanup.new_address = connection.current.address;
result.actions.push_back(cleanup);
if (connection.existing_connection_is_zombie) {
result.should_disconnect_old = true;
SessionAction disconnect_old =
make_action(SessionActionType::DisconnectOldPeer, connection.current);
disconnect_old.old_address = existing_address.value();
result.actions.push_back(disconnect_old);
}
SessionAction update = make_action(SessionActionType::UpdatePeerAddress,
connection.current);
update.old_address = existing_address.value();
update.new_address = connection.current.address;
result.actions.push_back(update);
} else {
result.decision = InputDecision::RejectedDuplicateIdentity;
result.should_disconnect_current = true;
result.actions.push_back(make_action(SessionActionType::RejectDuplicateIdentity,
connection.current));
result.actions.push_back(make_action(SessionActionType::DisconnectCurrentPeer,
connection.current));
return result;
}
}
upsertAcceptedSession(connection, payload_identity, result.mtu, now_seconds);
removePendingIdentity(connection.current);
result.decision = InputDecision::AcceptedNewIdentity;
result.accepted = true;
result.actions.push_back(make_action(SessionActionType::AcceptNewIdentity,
connection.current));
result.actions.push_back(make_action(SessionActionType::CreateFragmentationState,
connection.current));
result.actions.push_back(make_action(SessionActionType::MarkPeerReady,
connection.current));
result.actions.push_back(make_action(SessionActionType::RemovePendingIdentity,
connection.current));
result.actions.push_back(make_action(SessionActionType::MarkRealData,
connection.current));
return result;
}
void BLEPeerSessionManager::markConnected(const ConnectionSnapshot& connection,
double now_seconds) {
if (connection.known_identity_for_address.has_value()) {
upsertAcceptedSession(connection,
connection.known_identity_for_address.value(),
connection.negotiated_mtu.value_or(BLE_MINIMUM_MTU),
now_seconds);
} else {
markPendingIdentity(connection.current, now_seconds);
}
}
void BLEPeerSessionManager::markDisconnected(const ConnectionId& connection,
double /*now_seconds*/) {
sessions_.erase(std::remove_if(sessions_.begin(), sessions_.end(),
[&](const PeerSession& session) {
return session.current_address == connection.address ||
session.current_handle == connection.handle;
}),
sessions_.end());
removePendingIdentity(connection);
}
void BLEPeerSessionManager::markMtu(const ConnectionId& connection, uint16_t mtu) {
PeerSession* session = findSessionByAddress(connection.address);
if (session) {
session->mtu = mtu;
}
}
void BLEPeerSessionManager::markPendingIdentity(const ConnectionId& connection,
double now_seconds) {
for (PendingIdentity& pending : pending_identities_) {
if (pending.connection.address == connection.address) {
pending.connection = connection;
pending.started_at = now_seconds;
return;
}
}
PendingIdentity pending;
pending.connection = connection;
pending.started_at = now_seconds;
pending_identities_.push_back(pending);
}
void BLEPeerSessionManager::removePendingIdentity(const ConnectionId& connection) {
pending_identities_.erase(
std::remove_if(pending_identities_.begin(), pending_identities_.end(),
[&](const PendingIdentity& pending) {
return pending.connection.address == connection.address;
}),
pending_identities_.end());
}
std::vector<ConnectionId> BLEPeerSessionManager::expiredPendingIdentities(
double now_seconds) const {
std::vector<ConnectionId> expired;
for (const PendingIdentity& pending : pending_identities_) {
if (now_seconds - pending.started_at > pending_identity_timeout_) {
expired.push_back(pending.connection);
}
}
return expired;
}
std::optional<PeerSessionView> BLEPeerSessionManager::sessionByAddress(
const std::string& address) const {
const PeerSession* session = findSessionByAddress(address);
if (!session) {
return std::nullopt;
}
PeerSessionView view;
view.identity = session->identity;
view.identity_key = session->identity_key;
view.current_address = session->current_address;
view.current_handle = session->current_handle;
view.mtu = session->mtu;
view.has_fragmentation_state = session->has_fragmentation_state;
view.peer_ready = session->peer_ready;
view.pending_identity_since = session->pending_identity_since;
view.last_real_data = session->last_real_data;
return view;
}
std::optional<PeerSessionView> BLEPeerSessionManager::sessionByIdentity(
const PeerIdentity& identity) const {
const PeerSession* session = findSessionByIdentityKey(computeIdentityKey(identity));
if (!session) {
return std::nullopt;
}
PeerSessionView view;
view.identity = session->identity;
view.identity_key = session->identity_key;
view.current_address = session->current_address;
view.current_handle = session->current_handle;
view.mtu = session->mtu;
view.has_fragmentation_state = session->has_fragmentation_state;
view.peer_ready = session->peer_ready;
view.pending_identity_since = session->pending_identity_since;
view.last_real_data = session->last_real_data;
return view;
}
BLEPeerSessionManager::PeerSession* BLEPeerSessionManager::findSessionByIdentityKey(
const std::string& identity_key) {
for (PeerSession& session : sessions_) {
if (session.identity_key == identity_key) {
return &session;
}
}
return nullptr;
}
const BLEPeerSessionManager::PeerSession* BLEPeerSessionManager::findSessionByIdentityKey(
const std::string& identity_key) const {
for (const PeerSession& session : sessions_) {
if (session.identity_key == identity_key) {
return &session;
}
}
return nullptr;
}
BLEPeerSessionManager::PeerSession* BLEPeerSessionManager::findSessionByAddress(
const std::string& address) {
for (PeerSession& session : sessions_) {
if (session.current_address == address) {
return &session;
}
}
return nullptr;
}
const BLEPeerSessionManager::PeerSession* BLEPeerSessionManager::findSessionByAddress(
const std::string& address) const {
for (const PeerSession& session : sessions_) {
if (session.current_address == address) {
return &session;
}
}
return nullptr;
}
void BLEPeerSessionManager::upsertAcceptedSession(
const ConnectionSnapshot& connection,
const PeerIdentity& identity,
uint16_t mtu,
double now_seconds) {
const std::string identity_key = computeIdentityKey(identity);
PeerSession* session = findSessionByIdentityKey(identity_key);
if (!session) {
PeerSession new_session;
new_session.identity = identity;
new_session.identity_key = identity_key;
sessions_.push_back(new_session);
session = &sessions_.back();
}
session->identity = identity;
session->identity_key = identity_key;
session->current_address = connection.current.address;
session->current_handle = connection.current.handle;
session->mtu = mtu;
session->has_fragmentation_state = true;
session->peer_ready = true;
session->pending_identity_since = 0.0;
session->last_real_data = now_seconds;
}
} // namespace ble_reticulum

View file

@ -0,0 +1,163 @@
#pragma once
#include <array>
#include <cstddef>
#include <cstdint>
#include <optional>
#include <string>
#include <vector>
namespace ble_reticulum {
using PeerIdentity = std::array<uint8_t, 16>;
enum class LocalRole : uint8_t {
Unknown,
Central,
Peripheral,
};
enum class InputDecision : uint8_t {
PassToReassembler,
ConsumedDuplicateSameIdentity,
ConsumedDuplicateMismatchedIdentity,
AcceptedNewIdentity,
RejectedDuplicateIdentity,
ErrorConsumed,
};
enum class SessionActionType : uint8_t {
ConsumeInput,
PassToReassembler,
AcceptNewIdentity,
RejectDuplicateIdentity,
DisconnectCurrentPeer,
DisconnectOldPeer,
CreateFragmentationState,
MarkPeerReady,
UpdatePeerAddress,
RemovePendingIdentity,
MarkRealData,
CleanupOldAddress,
Warn,
};
struct ConnectionId {
std::string address;
uint16_t handle = 0xffff;
bool operator==(const ConnectionId& other) const {
return address == other.address && handle == other.handle;
}
};
struct ConnectionSnapshot {
ConnectionId current;
LocalRole local_role = LocalRole::Unknown;
std::optional<PeerIdentity> known_identity_for_address;
std::optional<uint16_t> negotiated_mtu;
std::optional<std::string> existing_address_for_identity;
bool identity_has_pending_detach = false;
bool existing_address_connected = false;
bool existing_address_in_peer_table = false;
bool existing_connection_is_zombie = false;
double existing_last_real_data = 0.0;
};
struct SessionAction {
SessionActionType type = SessionActionType::ConsumeInput;
ConnectionId target;
std::string old_address;
std::string new_address;
std::string message;
};
struct HandshakeResult {
InputDecision decision = InputDecision::PassToReassembler;
std::vector<SessionAction> actions;
bool consumed = false;
bool accepted = false;
bool should_disconnect_current = false;
bool should_disconnect_old = false;
std::optional<PeerIdentity> peer_identity;
std::string identity_key;
std::string fragmenter_key;
uint16_t mtu = 23;
};
struct PeerSessionView {
PeerIdentity identity{};
std::string identity_key;
std::string current_address;
uint16_t current_handle = 0xffff;
uint16_t mtu = 23;
bool has_fragmentation_state = false;
bool peer_ready = false;
double pending_identity_since = 0.0;
double last_real_data = 0.0;
};
class BLEPeerSessionManager {
public:
explicit BLEPeerSessionManager(double pending_identity_timeout = 30.0,
double zombie_timeout = 45.0);
HandshakeResult handleIdentityHandshake(const ConnectionSnapshot& connection,
const uint8_t* data,
size_t data_size,
double now_seconds);
void markConnected(const ConnectionSnapshot& connection, double now_seconds);
void markDisconnected(const ConnectionId& connection, double now_seconds);
void markMtu(const ConnectionId& connection, uint16_t mtu);
void markPendingIdentity(const ConnectionId& connection, double now_seconds);
void removePendingIdentity(const ConnectionId& connection);
std::vector<ConnectionId> expiredPendingIdentities(double now_seconds) const;
std::optional<PeerSessionView> sessionByAddress(const std::string& address) const;
std::optional<PeerSessionView> sessionByIdentity(const PeerIdentity& identity) const;
static bool isIdentityHandshakePayload(const uint8_t* data, size_t data_size);
static PeerIdentity identityFromPayload(const uint8_t* data, size_t data_size);
static std::string computeIdentityKey(const PeerIdentity& identity);
static std::string computeFragmenterKey(const PeerIdentity& identity);
private:
double pending_identity_timeout_;
double zombie_timeout_;
struct PendingIdentity {
ConnectionId connection;
double started_at = 0.0;
};
struct PeerSession {
PeerIdentity identity{};
std::string identity_key;
std::string current_address;
uint16_t current_handle = 0xffff;
uint16_t mtu = 23;
bool has_fragmentation_state = false;
bool peer_ready = false;
double pending_identity_since = 0.0;
double last_real_data = 0.0;
};
std::vector<PeerSession> sessions_;
std::vector<PendingIdentity> pending_identities_;
PeerSession* findSessionByIdentityKey(const std::string& identity_key);
const PeerSession* findSessionByIdentityKey(const std::string& identity_key) const;
PeerSession* findSessionByAddress(const std::string& address);
const PeerSession* findSessionByAddress(const std::string& address) const;
void upsertAcceptedSession(const ConnectionSnapshot& connection,
const PeerIdentity& identity,
uint16_t mtu,
double now_seconds);
};
} // namespace ble_reticulum

View file

@ -0,0 +1,92 @@
BEGIN TRANSACTION;
INSERT INTO symbols (
source_file,
symbol_name,
symbol_type,
class_name,
line_number,
tag,
phase,
status,
cpp_candidate,
confidence,
rationale,
callers,
callees,
notes,
first_seen_at,
updated_at
)
VALUES (
'src/ble_reticulum/BLEInterface.py',
'_handle_identity_handshake',
'method',
'BLEInterface',
1202,
'GLUE',
'2_ble_protocol_session_manager',
'NATIVE_TESTED',
1,
'high',
'Gate 2B implemented a native C++ BLEPeerSessionManager skeleton and pure native tests covering current Python reference decisions. This remains reference behavior for C++ session ownership, not a literal method port.',
'_data_received_callback',
'_compute_identity_hash; _check_duplicate_identity; driver.disconnect; driver.get_peer_mtu; _get_fragmenter_key; BLEFragmenter; BLEReassembler; _spawn_peer_interface',
'Gate 2B native tests passed. No pybind11 bindings, Python integration, live BLE behavior, or Phase 1 FIELD_ACCEPTED rows were changed.',
CURRENT_TIMESTAMP,
CURRENT_TIMESTAMP
)
ON CONFLICT(source_file, class_name, symbol_name, line_number) DO UPDATE SET
tag = 'GLUE',
phase = '2_ble_protocol_session_manager',
status = 'NATIVE_TESTED',
cpp_candidate = 1,
confidence = 'high',
rationale = excluded.rationale,
callers = excluded.callers,
callees = excluded.callees,
notes = excluded.notes,
updated_at = CURRENT_TIMESTAMP;
INSERT INTO reviews (
symbol_id,
reviewed_at,
reviewer,
old_tag,
new_tag,
old_status,
new_status,
note
)
SELECT
symbol_id,
CURRENT_TIMESTAMP,
'Codex',
'GLUE',
'GLUE',
'DESIGN',
'NATIVE_TESTED',
'Gate 2B native C++ BLEPeerSessionManager skeleton implemented with pure native tests. Tests passed via g++ build and /tmp/test_ble_peer_session_manager. No live Python BLE behavior or Phase 1 FIELD_ACCEPTED records changed.'
FROM symbols
WHERE source_file = 'src/ble_reticulum/BLEInterface.py'
AND class_name = 'BLEInterface'
AND symbol_name = '_handle_identity_handshake'
AND line_number = 1202;
SELECT
symbol_id,
source_file,
class_name,
symbol_name,
tag,
phase,
status,
cpp_candidate,
updated_at
FROM symbols
WHERE source_file = 'src/ble_reticulum/BLEInterface.py'
AND class_name = 'BLEInterface'
AND symbol_name = '_handle_identity_handshake'
AND line_number = 1202;
COMMIT;

View file

@ -0,0 +1,279 @@
#include "../../protocol_core/BLEPeerSessionManager.h"
#include <cassert>
#include <cstdint>
#include <iostream>
#include <stdexcept>
#include <string>
#include <vector>
using namespace ble_reticulum;
namespace {
PeerIdentity identity_with_base(uint8_t base) {
PeerIdentity identity{};
for (size_t i = 0; i < identity.size(); ++i) {
identity[i] = static_cast<uint8_t>(base + i);
}
return identity;
}
std::vector<uint8_t> to_payload(const PeerIdentity& identity) {
return std::vector<uint8_t>(identity.begin(), identity.end());
}
ConnectionSnapshot snapshot(const std::string& address) {
ConnectionSnapshot snap;
snap.current.address = address;
snap.local_role = LocalRole::Peripheral;
return snap;
}
bool has_action(const HandshakeResult& result, SessionActionType type) {
for (const SessionAction& action : result.actions) {
if (action.type == type) {
return true;
}
}
return false;
}
void test_non_16_byte_payload() {
BLEPeerSessionManager manager;
auto snap = snapshot("AA:BB:CC:00:00:01");
std::vector<uint8_t> data{1, 2, 3};
HandshakeResult result =
manager.handleIdentityHandshake(snap, data.data(), data.size(), 10.0);
assert(result.decision == InputDecision::PassToReassembler);
assert(!result.consumed);
assert(!result.accepted);
assert(has_action(result, SessionActionType::PassToReassembler));
}
void test_new_16_byte_identity() {
BLEPeerSessionManager manager;
PeerIdentity identity = identity_with_base(0x10);
std::vector<uint8_t> data = to_payload(identity);
auto snap = snapshot("AA:BB:CC:00:00:02");
HandshakeResult result =
manager.handleIdentityHandshake(snap, data.data(), data.size(), 11.0);
assert(result.decision == InputDecision::AcceptedNewIdentity);
assert(result.consumed);
assert(result.accepted);
assert(result.identity_key == "1011121314151617");
assert(result.fragmenter_key == "101112131415161718191a1b1c1d1e1f");
assert(has_action(result, SessionActionType::AcceptNewIdentity));
assert(has_action(result, SessionActionType::CreateFragmentationState));
assert(has_action(result, SessionActionType::MarkPeerReady));
}
void test_known_identity_duplicate_same() {
BLEPeerSessionManager manager;
PeerIdentity identity = identity_with_base(0x20);
std::vector<uint8_t> data = to_payload(identity);
auto snap = snapshot("AA:BB:CC:00:00:03");
snap.known_identity_for_address = identity;
HandshakeResult result =
manager.handleIdentityHandshake(snap, data.data(), data.size(), 12.0);
assert(result.decision == InputDecision::ConsumedDuplicateSameIdentity);
assert(result.consumed);
assert(!result.accepted);
}
void test_known_identity_duplicate_mismatch() {
BLEPeerSessionManager manager;
PeerIdentity known = identity_with_base(0x30);
PeerIdentity incoming = identity_with_base(0x40);
std::vector<uint8_t> data = to_payload(incoming);
auto snap = snapshot("AA:BB:CC:00:00:04");
snap.known_identity_for_address = known;
HandshakeResult result =
manager.handleIdentityHandshake(snap, data.data(), data.size(), 13.0);
assert(result.decision == InputDecision::ConsumedDuplicateMismatchedIdentity);
assert(result.consumed);
assert(has_action(result, SessionActionType::Warn));
}
void test_duplicate_identity_active_elsewhere() {
BLEPeerSessionManager manager;
PeerIdentity identity = identity_with_base(0x50);
std::vector<uint8_t> data = to_payload(identity);
auto first = snapshot("AA:BB:CC:00:00:05");
manager.handleIdentityHandshake(first, data.data(), data.size(), 14.0);
auto duplicate = snapshot("AA:BB:CC:00:00:06");
duplicate.existing_address_for_identity = first.current.address;
duplicate.existing_address_connected = true;
duplicate.existing_address_in_peer_table = true;
HandshakeResult result =
manager.handleIdentityHandshake(duplicate, data.data(), data.size(), 15.0);
assert(result.decision == InputDecision::RejectedDuplicateIdentity);
assert(result.consumed);
assert(!result.accepted);
assert(result.should_disconnect_current);
assert(has_action(result, SessionActionType::DisconnectCurrentPeer));
}
void test_duplicate_identity_with_stale_pending_detach() {
BLEPeerSessionManager manager;
PeerIdentity identity = identity_with_base(0x60);
std::vector<uint8_t> data = to_payload(identity);
auto first = snapshot("AA:BB:CC:00:00:07");
manager.handleIdentityHandshake(first, data.data(), data.size(), 16.0);
auto rotated = snapshot("AA:BB:CC:00:00:08");
rotated.existing_address_for_identity = first.current.address;
rotated.identity_has_pending_detach = true;
HandshakeResult result =
manager.handleIdentityHandshake(rotated, data.data(), data.size(), 17.0);
assert(result.decision == InputDecision::AcceptedNewIdentity);
assert(result.accepted);
assert(has_action(result, SessionActionType::CleanupOldAddress));
assert(has_action(result, SessionActionType::UpdatePeerAddress));
auto view = manager.sessionByIdentity(identity);
assert(view.has_value());
assert(view->current_address == rotated.current.address);
}
void test_duplicate_identity_with_zombie_old_connection() {
BLEPeerSessionManager manager;
PeerIdentity identity = identity_with_base(0x70);
std::vector<uint8_t> data = to_payload(identity);
auto first = snapshot("AA:BB:CC:00:00:09");
manager.handleIdentityHandshake(first, data.data(), data.size(), 18.0);
auto replacement = snapshot("AA:BB:CC:00:00:0A");
replacement.existing_address_for_identity = first.current.address;
replacement.existing_address_connected = true;
replacement.existing_address_in_peer_table = true;
replacement.existing_connection_is_zombie = true;
HandshakeResult result =
manager.handleIdentityHandshake(replacement, data.data(), data.size(), 19.0);
assert(result.decision == InputDecision::AcceptedNewIdentity);
assert(result.accepted);
assert(result.should_disconnect_old);
assert(has_action(result, SessionActionType::DisconnectOldPeer));
assert(has_action(result, SessionActionType::CleanupOldAddress));
}
void test_mtu_provided() {
BLEPeerSessionManager manager;
PeerIdentity identity = identity_with_base(0x80);
std::vector<uint8_t> data = to_payload(identity);
auto snap = snapshot("AA:BB:CC:00:00:0B");
snap.negotiated_mtu = 185;
HandshakeResult result =
manager.handleIdentityHandshake(snap, data.data(), data.size(), 20.0);
assert(result.mtu == 185);
auto view = manager.sessionByIdentity(identity);
assert(view.has_value());
assert(view->mtu == 185);
}
void test_mtu_missing_fallback() {
BLEPeerSessionManager manager;
PeerIdentity identity = identity_with_base(0x90);
std::vector<uint8_t> data = to_payload(identity);
auto snap = snapshot("AA:BB:CC:00:00:0C");
HandshakeResult result =
manager.handleIdentityHandshake(snap, data.data(), data.size(), 21.0);
assert(result.mtu == 23);
}
void test_pending_identity_timeout() {
BLEPeerSessionManager manager(30.0, 45.0);
ConnectionId one{"AA:BB:CC:00:00:0D", 1};
ConnectionId two{"AA:BB:CC:00:00:0E", 2};
manager.markPendingIdentity(one, 100.0);
manager.markPendingIdentity(two, 120.0);
std::vector<ConnectionId> expired = manager.expiredPendingIdentities(131.0);
assert(expired.size() == 1);
assert(expired[0] == one);
manager.removePendingIdentity(one);
expired = manager.expiredPendingIdentities(200.0);
assert(expired.size() == 1);
assert(expired[0] == two);
}
void test_peer_address_update_fragmenter_key_unchanged() {
BLEPeerSessionManager manager;
PeerIdentity identity = identity_with_base(0xa0);
std::vector<uint8_t> data = to_payload(identity);
auto first = snapshot("AA:BB:CC:00:00:0F");
HandshakeResult first_result =
manager.handleIdentityHandshake(first, data.data(), data.size(), 22.0);
auto second = snapshot("AA:BB:CC:00:00:10");
second.existing_address_for_identity = first.current.address;
second.identity_has_pending_detach = true;
HandshakeResult second_result =
manager.handleIdentityHandshake(second, data.data(), data.size(), 23.0);
assert(first_result.fragmenter_key == second_result.fragmenter_key);
auto old_view = manager.sessionByAddress(first.current.address);
auto new_view = manager.sessionByAddress(second.current.address);
assert(!old_view.has_value());
assert(new_view.has_value());
assert(new_view->current_address == second.current.address);
}
void test_key_helpers() {
PeerIdentity identity = identity_with_base(0x01);
assert(BLEPeerSessionManager::computeIdentityKey(identity) == "0102030405060708");
assert(BLEPeerSessionManager::computeFragmenterKey(identity) ==
"0102030405060708090a0b0c0d0e0f10");
}
void test_identity_from_payload_rejects_non_16() {
std::vector<uint8_t> data{1, 2, 3};
bool threw = false;
try {
(void)BLEPeerSessionManager::identityFromPayload(data.data(), data.size());
} catch (const std::invalid_argument&) {
threw = true;
}
assert(threw);
}
} // namespace
int main() {
test_non_16_byte_payload();
test_new_16_byte_identity();
test_known_identity_duplicate_same();
test_known_identity_duplicate_mismatch();
test_duplicate_identity_active_elsewhere();
test_duplicate_identity_with_stale_pending_detach();
test_duplicate_identity_with_zombie_old_connection();
test_mtu_provided();
test_mtu_missing_fallback();
test_pending_identity_timeout();
test_peer_address_update_fragmenter_key_unchanged();
test_key_helpers();
test_identity_from_payload_rejects_non_16();
std::cout << "BLEPeerSessionManager native tests passed\n";
return 0;
}