From 45827c1220958f43b52aef7bd447007ad1659c16 Mon Sep 17 00:00:00 2001 From: John Poole Date: Mon, 18 May 2026 14:58:20 -0700 Subject: [PATCH] Add Gate 2B BLE peer session manager native tests --- ...ssionManager_native_tests_20260518_1445.md | 107 ++++++ .../protocol_core/BLEPeerSessionManager.cpp | 349 ++++++++++++++++++ .../protocol_core/BLEPeerSessionManager.h | 163 ++++++++ ...col_session_native_tests_20260518_1445.sql | 92 +++++ .../native/test_ble_peer_session_manager.cpp | 279 ++++++++++++++ 5 files changed, 990 insertions(+) create mode 100644 migration/phase2/Gate2B_BLEPeerSessionManager_native_tests_20260518_1445.md create mode 100644 migration/protocol_core/BLEPeerSessionManager.cpp create mode 100644 migration/protocol_core/BLEPeerSessionManager.h create mode 100644 migration/sql/mark_gate2b_protocol_session_native_tests_20260518_1445.sql create mode 100644 migration/tests/native/test_ble_peer_session_manager.cpp diff --git a/migration/phase2/Gate2B_BLEPeerSessionManager_native_tests_20260518_1445.md b/migration/phase2/Gate2B_BLEPeerSessionManager_native_tests_20260518_1445.md new file mode 100644 index 0000000..c1bd37b --- /dev/null +++ b/migration/phase2/Gate2B_BLEPeerSessionManager_native_tests_20260518_1445.md @@ -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. diff --git a/migration/protocol_core/BLEPeerSessionManager.cpp b/migration/protocol_core/BLEPeerSessionManager.cpp new file mode 100644 index 0000000..47c4894 --- /dev/null +++ b/migration/protocol_core/BLEPeerSessionManager.cpp @@ -0,0 +1,349 @@ +#include "BLEPeerSessionManager.h" + +#include +#include + +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 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 BLEPeerSessionManager::expiredPendingIdentities( + double now_seconds) const { + std::vector 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 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 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 diff --git a/migration/protocol_core/BLEPeerSessionManager.h b/migration/protocol_core/BLEPeerSessionManager.h new file mode 100644 index 0000000..be5bbe7 --- /dev/null +++ b/migration/protocol_core/BLEPeerSessionManager.h @@ -0,0 +1,163 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace ble_reticulum { + +using PeerIdentity = std::array; + +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 known_identity_for_address; + std::optional negotiated_mtu; + + std::optional 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 actions; + + bool consumed = false; + bool accepted = false; + bool should_disconnect_current = false; + bool should_disconnect_old = false; + + std::optional 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 expiredPendingIdentities(double now_seconds) const; + + std::optional sessionByAddress(const std::string& address) const; + std::optional 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 sessions_; + std::vector 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 diff --git a/migration/sql/mark_gate2b_protocol_session_native_tests_20260518_1445.sql b/migration/sql/mark_gate2b_protocol_session_native_tests_20260518_1445.sql new file mode 100644 index 0000000..1c08f40 --- /dev/null +++ b/migration/sql/mark_gate2b_protocol_session_native_tests_20260518_1445.sql @@ -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; diff --git a/migration/tests/native/test_ble_peer_session_manager.cpp b/migration/tests/native/test_ble_peer_session_manager.cpp new file mode 100644 index 0000000..374b45a --- /dev/null +++ b/migration/tests/native/test_ble_peer_session_manager.cpp @@ -0,0 +1,279 @@ +#include "../../protocol_core/BLEPeerSessionManager.h" + +#include +#include +#include +#include +#include +#include + +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(base + i); + } + return identity; +} + +std::vector to_payload(const PeerIdentity& identity) { + return std::vector(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 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 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 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 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 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 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 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 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 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 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 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 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; +}