Gate 2C works on jp, TODO: test on zerodev1

This commit is contained in:
John Poole 2026-05-18 15:32:00 -07:00
commit 387cd1c57d
6 changed files with 681 additions and 1 deletions

View file

@ -0,0 +1,79 @@
Gate 2C task: pybind11 binding for BLEPeerSessionManager plus Python tests
Context:
Gate 2B is complete. BLEPeerSessionManager compiles and native C++ tests pass on jp and zerodev1.
Goal:
Expose BLEPeerSessionManager to Python through the existing protocol_core / pybind11 module so Python tests can instantiate the manager, call its methods, and inspect result/action values.
Restrictions:
- Do not modify BLEInterface.py.
- Do not modify live BLE behavior.
- Do not add environment-flag integration yet.
- Do not run bilateral field tests yet.
- Keep this as binding + Python unit tests only.
Implement:
1. pybind11 bindings for:
- BLEPeerSessionManager
- ConnectionId
- ConnectionSnapshot
- HandshakeResult
- SessionAction
- PeerSessionView if useful
- relevant enums:
- LocalRole
- InputDecision
- SessionActionType
2. Bind static helpers:
- isIdentityHandshakePayload
- identityFromPayload
- computeIdentityKey
- computeFragmenterKey
3. Python tests under:
migration/tests/test_ble_peer_session_manager_pybind.py
Test cases:
- import module succeeds
- non-16-byte payload returns PassToReassembler and consumed=false
- new 16-byte identity returns AcceptedNewIdentity and consumed=true
- identity_key equals first 8 bytes as 16 lowercase hex chars
- fragmenter_key equals full 16-byte identity as 32 lowercase hex chars
- known identity duplicate same returns ConsumedDuplicateSameIdentity
- known identity duplicate mismatch returns ConsumedDuplicateMismatchedIdentity
- MTU provided is preserved
- MTU missing falls back to 23
- duplicate identity active elsewhere requests DisconnectCurrentPeer
- pending identity timeout can be marked and expired
- peer address update preserves identity/fragmenter key
Also run existing tests:
- migration/tests/test_fragmentation_cpp_equivalence.py
- migration/tests/test_fragmentation_backend_shim.py
- migration/tests/test_identity_helpers_cpp_equivalence.py
- migration/tests/test_ble_peer_session_manager_pybind.py
Deliverables:
1. Updated pybind11 binding source.
2. Python tests.
3. Gate 2C report:
migration/phase2/Gate2C_BLEPeerSessionManager_pybind_tests_YYYYMMDD_HHMM.md
4. SQL:
migration/sql/mark_gate2c_protocol_session_pybind_tests_YYYYMMDD_HHMM.sql
SQL requirements:
- Do not modify Phase 1 FIELD_ACCEPTED records.
- Do not mark Gate 2F / FIELD_ACCEPTED.
- Update _handle_identity_handshake tracking row:
phase = '2_ble_protocol_session_manager'
status = 'PYTHON_BOUND'
cpp_candidate = 1
- Insert a reviews row describing:
Gate 2C completed pybind11 exposure and Python unit tests.
- Include a SELECT verification query.
Before final response:
- Run the Python tests.
- Report exact commands and results.

View file

@ -0,0 +1,115 @@
# Gate 2C: BLEPeerSessionManager pybind11 Tests
Date: 2026-05-18 15:07 America/Los_Angeles
Scope: pybind11 exposure and Python unit tests only.
## Summary
Gate 2C exposed the native C++ `BLEPeerSessionManager` skeleton through the existing `ble_protocol_core_cpp` pybind11 module and added Python tests for the binding surface. No live BLE behavior was changed.
Modified:
- `migration/protocol_core/ble_protocol_core.cpp`
- `migration/protocol_core/setup.py`
Added:
- `migration/tests/test_ble_peer_session_manager_pybind.py`
- `migration/phase2/Gate2C_BLEPeerSessionManager_pybind_tests_20260518_1507.md`
- `migration/sql/mark_gate2c_protocol_session_pybind_tests_20260518_1507.sql`
Not changed:
- `src/ble_reticulum/BLEInterface.py`
- live BLE backend selection behavior
- BlueZ/Bleak/DBus integration
- `RNS.Transport` integration
- ESP32 BLE integration
## Binding Surface
Bound classes and structs:
- `BLEPeerSessionManager`
- `ConnectionId`
- `ConnectionSnapshot`
- `HandshakeResult`
- `SessionAction`
- `PeerSessionView`
Bound enums:
- `LocalRole`
- `InputDecision`
- `SessionActionType`
Bound static helpers:
- `is_identity_handshake_payload`
- `identity_from_payload`
- `compute_identity_key`
- `compute_fragmenter_key`
The binding accepts Python `bytes` for identities and payloads, while preserving the C++ manager as the native source of session decisions.
## Tests Added
`migration/tests/test_ble_peer_session_manager_pybind.py` covers:
- import module succeeds;
- non-16-byte payload returns `PassToReassembler` and `consumed=false`;
- new 16-byte identity returns `AcceptedNewIdentity` and `consumed=true`;
- identity key equals first 8 bytes as 16 lowercase hex chars;
- fragmenter key equals full 16-byte identity as 32 lowercase hex chars;
- known identity duplicate same returns `ConsumedDuplicateSameIdentity`;
- known identity duplicate mismatch returns `ConsumedDuplicateMismatchedIdentity`;
- MTU provided is preserved;
- MTU missing falls back to `23`;
- duplicate identity active elsewhere requests `DisconnectCurrentPeer`;
- pending identity timeout can be marked and expired;
- peer address update preserves identity and fragmenter key;
- invalid `identity_from_payload` input raises.
## Verification
Build command:
```sh
cd migration/protocol_core
python3 setup.py build_ext --inplace
```
New test command:
```sh
PYTHONPATH=migration/protocol_core pytest -q migration/tests/test_ble_peer_session_manager_pybind.py
```
Result:
```text
12 passed, 2 warnings in 0.12s
```
Full requested test command:
```sh
pytest -q migration/tests/test_fragmentation_cpp_equivalence.py migration/tests/test_fragmentation_backend_shim.py migration/tests/test_identity_helpers_cpp_equivalence.py migration/tests/test_ble_peer_session_manager_pybind.py
```
Result:
```text
68 passed, 1 skipped, 2 warnings in 1.00s
```
Note: running the full set with an outer `PYTHONPATH=src:migration/protocol_core` makes `test_python_backend_still_works_when_cpp_backend_is_unavailable` fail because that test intentionally controls `PYTHONPATH` inside subprocesses to simulate C++ backend absence. The passing command above leaves that isolation intact.
## SQL
Companion SQL:
`migration/sql/mark_gate2c_protocol_session_pybind_tests_20260518_1507.sql`
The SQL marks `_handle_identity_handshake` as `PYTHON_BOUND` in phase `2_ble_protocol_session_manager`, keeps tag `GLUE`, keeps `cpp_candidate=1`, and does not mark any Gate 2F or field-accepted status.

View file

@ -1,6 +1,8 @@
#include <pybind11/pybind11.h>
#include <pybind11/stl.h>
#include "BLEPeerSessionManager.h"
#include <algorithm>
#include <cstdint>
#include <map>
@ -44,6 +46,21 @@ std::string get_fragmenter_key(py::object peer_identity_obj,
return py::str(peer_identity_obj.attr("hex")()).cast<std::string>();
}
ble_reticulum::PeerIdentity peer_identity_from_py_bytes(const py::bytes &value) {
const std::string data = py_bytes_to_string(value);
return ble_reticulum::BLEPeerSessionManager::identityFromPayload(
reinterpret_cast<const uint8_t *>(data.data()), data.size());
}
py::bytes peer_identity_to_py_bytes(const ble_reticulum::PeerIdentity &identity) {
return py::bytes(reinterpret_cast<const char *>(identity.data()), identity.size());
}
std::vector<uint8_t> py_bytes_to_uint8_vector(const py::bytes &value) {
const std::string data = py_bytes_to_string(value);
return std::vector<uint8_t>(data.begin(), data.end());
}
struct Buffer {
std::map<uint16_t, Bytes> fragments;
uint16_t total = 0;
@ -504,4 +521,198 @@ PYBIND11_MODULE(ble_protocol_core_cpp, m) {
.def_readonly_static("FLAG", &HDLCFramerCpp::FLAG_VALUE)
.def_readonly_static("ESCAPE", &HDLCFramerCpp::ESCAPE_VALUE)
.def_readonly_static("ESCAPE_XOR", &HDLCFramerCpp::ESCAPE_XOR_VALUE);
py::enum_<ble_reticulum::LocalRole>(m, "LocalRole")
.value("Unknown", ble_reticulum::LocalRole::Unknown)
.value("Central", ble_reticulum::LocalRole::Central)
.value("Peripheral", ble_reticulum::LocalRole::Peripheral);
py::enum_<ble_reticulum::InputDecision>(m, "InputDecision")
.value("PassToReassembler", ble_reticulum::InputDecision::PassToReassembler)
.value("ConsumedDuplicateSameIdentity",
ble_reticulum::InputDecision::ConsumedDuplicateSameIdentity)
.value("ConsumedDuplicateMismatchedIdentity",
ble_reticulum::InputDecision::ConsumedDuplicateMismatchedIdentity)
.value("AcceptedNewIdentity", ble_reticulum::InputDecision::AcceptedNewIdentity)
.value("RejectedDuplicateIdentity",
ble_reticulum::InputDecision::RejectedDuplicateIdentity)
.value("ErrorConsumed", ble_reticulum::InputDecision::ErrorConsumed);
py::enum_<ble_reticulum::SessionActionType>(m, "SessionActionType")
.value("ConsumeInput", ble_reticulum::SessionActionType::ConsumeInput)
.value("PassToReassembler", ble_reticulum::SessionActionType::PassToReassembler)
.value("AcceptNewIdentity", ble_reticulum::SessionActionType::AcceptNewIdentity)
.value("RejectDuplicateIdentity",
ble_reticulum::SessionActionType::RejectDuplicateIdentity)
.value("DisconnectCurrentPeer",
ble_reticulum::SessionActionType::DisconnectCurrentPeer)
.value("DisconnectOldPeer", ble_reticulum::SessionActionType::DisconnectOldPeer)
.value("CreateFragmentationState",
ble_reticulum::SessionActionType::CreateFragmentationState)
.value("MarkPeerReady", ble_reticulum::SessionActionType::MarkPeerReady)
.value("UpdatePeerAddress", ble_reticulum::SessionActionType::UpdatePeerAddress)
.value("RemovePendingIdentity",
ble_reticulum::SessionActionType::RemovePendingIdentity)
.value("MarkRealData", ble_reticulum::SessionActionType::MarkRealData)
.value("CleanupOldAddress", ble_reticulum::SessionActionType::CleanupOldAddress)
.value("Warn", ble_reticulum::SessionActionType::Warn);
py::class_<ble_reticulum::ConnectionId>(m, "ConnectionId")
.def(py::init<>())
.def(py::init([](const std::string &address, uint16_t handle) {
ble_reticulum::ConnectionId id;
id.address = address;
id.handle = handle;
return id;
}),
py::arg("address"), py::arg("handle") = 0xffff)
.def_readwrite("address", &ble_reticulum::ConnectionId::address)
.def_readwrite("handle", &ble_reticulum::ConnectionId::handle);
py::class_<ble_reticulum::ConnectionSnapshot>(m, "ConnectionSnapshot")
.def(py::init<>())
.def_readwrite("current", &ble_reticulum::ConnectionSnapshot::current)
.def_readwrite("local_role", &ble_reticulum::ConnectionSnapshot::local_role)
.def_readwrite("negotiated_mtu",
&ble_reticulum::ConnectionSnapshot::negotiated_mtu)
.def_readwrite("existing_address_for_identity",
&ble_reticulum::ConnectionSnapshot::existing_address_for_identity)
.def_readwrite("identity_has_pending_detach",
&ble_reticulum::ConnectionSnapshot::identity_has_pending_detach)
.def_readwrite("existing_address_connected",
&ble_reticulum::ConnectionSnapshot::existing_address_connected)
.def_readwrite("existing_address_in_peer_table",
&ble_reticulum::ConnectionSnapshot::existing_address_in_peer_table)
.def_readwrite("existing_connection_is_zombie",
&ble_reticulum::ConnectionSnapshot::existing_connection_is_zombie)
.def_readwrite("existing_last_real_data",
&ble_reticulum::ConnectionSnapshot::existing_last_real_data)
.def("set_known_identity",
[](ble_reticulum::ConnectionSnapshot &snapshot, const py::bytes &identity) {
snapshot.known_identity_for_address = peer_identity_from_py_bytes(identity);
},
py::arg("identity"))
.def("clear_known_identity",
[](ble_reticulum::ConnectionSnapshot &snapshot) {
snapshot.known_identity_for_address.reset();
})
.def_property_readonly("known_identity",
[](const ble_reticulum::ConnectionSnapshot &snapshot) -> py::object {
if (!snapshot.known_identity_for_address.has_value()) {
return py::none();
}
return peer_identity_to_py_bytes(
snapshot.known_identity_for_address.value());
});
py::class_<ble_reticulum::SessionAction>(m, "SessionAction")
.def(py::init<>())
.def_readwrite("type", &ble_reticulum::SessionAction::type)
.def_readwrite("target", &ble_reticulum::SessionAction::target)
.def_readwrite("old_address", &ble_reticulum::SessionAction::old_address)
.def_readwrite("new_address", &ble_reticulum::SessionAction::new_address)
.def_readwrite("message", &ble_reticulum::SessionAction::message);
py::class_<ble_reticulum::HandshakeResult>(m, "HandshakeResult")
.def(py::init<>())
.def_readwrite("decision", &ble_reticulum::HandshakeResult::decision)
.def_readwrite("actions", &ble_reticulum::HandshakeResult::actions)
.def_readwrite("consumed", &ble_reticulum::HandshakeResult::consumed)
.def_readwrite("accepted", &ble_reticulum::HandshakeResult::accepted)
.def_readwrite("should_disconnect_current",
&ble_reticulum::HandshakeResult::should_disconnect_current)
.def_readwrite("should_disconnect_old",
&ble_reticulum::HandshakeResult::should_disconnect_old)
.def_readwrite("identity_key", &ble_reticulum::HandshakeResult::identity_key)
.def_readwrite("fragmenter_key", &ble_reticulum::HandshakeResult::fragmenter_key)
.def_readwrite("mtu", &ble_reticulum::HandshakeResult::mtu)
.def_property_readonly("peer_identity",
[](const ble_reticulum::HandshakeResult &result) -> py::object {
if (!result.peer_identity.has_value()) {
return py::none();
}
return peer_identity_to_py_bytes(result.peer_identity.value());
});
py::class_<ble_reticulum::PeerSessionView>(m, "PeerSessionView")
.def(py::init<>())
.def_readwrite("identity_key", &ble_reticulum::PeerSessionView::identity_key)
.def_readwrite("current_address", &ble_reticulum::PeerSessionView::current_address)
.def_readwrite("current_handle", &ble_reticulum::PeerSessionView::current_handle)
.def_readwrite("mtu", &ble_reticulum::PeerSessionView::mtu)
.def_readwrite("has_fragmentation_state",
&ble_reticulum::PeerSessionView::has_fragmentation_state)
.def_readwrite("peer_ready", &ble_reticulum::PeerSessionView::peer_ready)
.def_readwrite("pending_identity_since",
&ble_reticulum::PeerSessionView::pending_identity_since)
.def_readwrite("last_real_data", &ble_reticulum::PeerSessionView::last_real_data)
.def_property_readonly("identity",
[](const ble_reticulum::PeerSessionView &view) {
return peer_identity_to_py_bytes(view.identity);
});
py::class_<ble_reticulum::BLEPeerSessionManager>(m, "BLEPeerSessionManager")
.def(py::init<double, double>(), py::arg("pending_identity_timeout") = 30.0,
py::arg("zombie_timeout") = 45.0)
.def("handle_identity_handshake",
[](ble_reticulum::BLEPeerSessionManager &manager,
const ble_reticulum::ConnectionSnapshot &connection,
const py::bytes &data,
double now_seconds) {
const std::vector<uint8_t> bytes = py_bytes_to_uint8_vector(data);
return manager.handleIdentityHandshake(
connection, bytes.data(), bytes.size(), now_seconds);
},
py::arg("connection"), py::arg("data"), py::arg("now_seconds"))
.def("mark_connected", &ble_reticulum::BLEPeerSessionManager::markConnected,
py::arg("connection"), py::arg("now_seconds"))
.def("mark_disconnected", &ble_reticulum::BLEPeerSessionManager::markDisconnected,
py::arg("connection"), py::arg("now_seconds"))
.def("mark_mtu", &ble_reticulum::BLEPeerSessionManager::markMtu,
py::arg("connection"), py::arg("mtu"))
.def("mark_pending_identity",
&ble_reticulum::BLEPeerSessionManager::markPendingIdentity,
py::arg("connection"), py::arg("now_seconds"))
.def("remove_pending_identity",
&ble_reticulum::BLEPeerSessionManager::removePendingIdentity,
py::arg("connection"))
.def("expired_pending_identities",
&ble_reticulum::BLEPeerSessionManager::expiredPendingIdentities,
py::arg("now_seconds"))
.def("session_by_address",
&ble_reticulum::BLEPeerSessionManager::sessionByAddress,
py::arg("address"))
.def("session_by_identity",
[](const ble_reticulum::BLEPeerSessionManager &manager,
const py::bytes &identity) {
return manager.sessionByIdentity(peer_identity_from_py_bytes(identity));
},
py::arg("identity"))
.def_static("is_identity_handshake_payload",
[](const py::bytes &data) {
const std::vector<uint8_t> bytes = py_bytes_to_uint8_vector(data);
return ble_reticulum::BLEPeerSessionManager::
isIdentityHandshakePayload(bytes.data(), bytes.size());
},
py::arg("data"))
.def_static("identity_from_payload",
[](const py::bytes &data) {
const std::vector<uint8_t> bytes = py_bytes_to_uint8_vector(data);
return peer_identity_to_py_bytes(
ble_reticulum::BLEPeerSessionManager::identityFromPayload(
bytes.data(), bytes.size()));
},
py::arg("data"))
.def_static("compute_identity_key",
[](const py::bytes &identity) {
return ble_reticulum::BLEPeerSessionManager::computeIdentityKey(
peer_identity_from_py_bytes(identity));
},
py::arg("identity"))
.def_static("compute_fragmenter_key",
[](const py::bytes &identity) {
return ble_reticulum::BLEPeerSessionManager::computeFragmenterKey(
peer_identity_from_py_bytes(identity));
},
py::arg("identity"));
}

View file

@ -13,7 +13,10 @@ setup(
ext_modules=[
Pybind11Extension(
"ble_protocol_core_cpp",
[str(ROOT / "ble_protocol_core.cpp")],
[
str(ROOT / "ble_protocol_core.cpp"),
str(ROOT / "BLEPeerSessionManager.cpp"),
],
cxx_std=17,
)
],

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',
'PYTHON_BOUND',
1,
'high',
'Gate 2C exposed the native C++ BLEPeerSessionManager through pybind11 and added Python unit tests. This remains reference behavior for C++ session ownership, not a literal BLEInterface 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 2C pybind11 tests passed. No BLEInterface.py changes, live BLE behavior changes, field tests, or Phase 1 FIELD_ACCEPTED row changes were made.',
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 = 'PYTHON_BOUND',
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',
'NATIVE_TESTED',
'PYTHON_BOUND',
'Gate 2C completed pybind11 exposure for BLEPeerSessionManager and Python unit tests. Full requested pytest set passed: 68 passed, 1 skipped. No live 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,180 @@
import os
import sys
import pytest
REPO_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
CPP_BUILD_DIR = os.path.join(REPO_ROOT, "migration", "protocol_core")
sys.path.insert(0, CPP_BUILD_DIR)
cpp = pytest.importorskip(
"ble_protocol_core_cpp",
reason=(
"compiled pybind11 module missing; build with "
"`python3 migration/protocol_core/setup.py build_ext --inplace`"
),
)
def identity(base):
return bytes((base + i) & 0xFF for i in range(16))
def snapshot(address, *, mtu=None):
snap = cpp.ConnectionSnapshot()
snap.current = cpp.ConnectionId(address)
snap.local_role = cpp.LocalRole.Peripheral
snap.negotiated_mtu = mtu
return snap
def action_types(result):
return {action.type for action in result.actions}
def test_import_module_succeeds():
assert cpp.BLEPeerSessionManager is not None
assert cpp.ConnectionSnapshot is not None
def test_non_16_byte_payload_passes_to_reassembler():
manager = cpp.BLEPeerSessionManager()
result = manager.handle_identity_handshake(snapshot("AA:00"), b"abc", 1.0)
assert result.decision == cpp.InputDecision.PassToReassembler
assert result.consumed is False
def test_new_16_byte_identity_sets_keys_and_consumes():
manager = cpp.BLEPeerSessionManager()
peer_identity = identity(0x10)
result = manager.handle_identity_handshake(
snapshot("AA:01"), peer_identity, 2.0
)
assert result.decision == cpp.InputDecision.AcceptedNewIdentity
assert result.consumed is True
assert result.accepted is True
assert result.identity_key == "1011121314151617"
assert result.fragmenter_key == "101112131415161718191a1b1c1d1e1f"
assert result.peer_identity == peer_identity
def test_static_key_helpers():
peer_identity = bytes.fromhex("0102030405060708090a0b0c0d0e0f10")
assert cpp.BLEPeerSessionManager.compute_identity_key(peer_identity) == "0102030405060708"
assert (
cpp.BLEPeerSessionManager.compute_fragmenter_key(peer_identity)
== "0102030405060708090a0b0c0d0e0f10"
)
assert cpp.BLEPeerSessionManager.identity_from_payload(peer_identity) == peer_identity
def test_identity_from_payload_rejects_non_16_byte_input():
with pytest.raises(Exception):
cpp.BLEPeerSessionManager.identity_from_payload(b"abc")
def test_known_identity_duplicate_same():
manager = cpp.BLEPeerSessionManager()
peer_identity = identity(0x20)
snap = snapshot("AA:02")
snap.set_known_identity(peer_identity)
result = manager.handle_identity_handshake(snap, peer_identity, 3.0)
assert result.decision == cpp.InputDecision.ConsumedDuplicateSameIdentity
assert result.consumed is True
def test_known_identity_duplicate_mismatch():
manager = cpp.BLEPeerSessionManager()
snap = snapshot("AA:03")
snap.set_known_identity(identity(0x30))
result = manager.handle_identity_handshake(snap, identity(0x40), 4.0)
assert result.decision == cpp.InputDecision.ConsumedDuplicateMismatchedIdentity
assert result.consumed is True
assert cpp.SessionActionType.Warn in action_types(result)
def test_mtu_provided_is_preserved():
manager = cpp.BLEPeerSessionManager()
peer_identity = identity(0x50)
result = manager.handle_identity_handshake(
snapshot("AA:04", mtu=185), peer_identity, 5.0
)
assert result.mtu == 185
view = manager.session_by_identity(peer_identity)
assert view is not None
assert view.mtu == 185
def test_mtu_missing_falls_back_to_23():
manager = cpp.BLEPeerSessionManager()
result = manager.handle_identity_handshake(
snapshot("AA:05"), identity(0x60), 6.0
)
assert result.mtu == 23
def test_duplicate_identity_active_elsewhere_requests_disconnect_current():
manager = cpp.BLEPeerSessionManager()
peer_identity = identity(0x70)
manager.handle_identity_handshake(snapshot("AA:06"), peer_identity, 7.0)
duplicate = snapshot("AA:07")
duplicate.existing_address_for_identity = "AA:06"
duplicate.existing_address_connected = True
duplicate.existing_address_in_peer_table = True
result = manager.handle_identity_handshake(duplicate, peer_identity, 8.0)
assert result.decision == cpp.InputDecision.RejectedDuplicateIdentity
assert result.should_disconnect_current is True
assert cpp.SessionActionType.DisconnectCurrentPeer in action_types(result)
def test_pending_identity_timeout_can_be_marked_and_expired():
manager = cpp.BLEPeerSessionManager(30.0, 45.0)
first = cpp.ConnectionId("AA:08", 8)
second = cpp.ConnectionId("AA:09", 9)
manager.mark_pending_identity(first, 100.0)
manager.mark_pending_identity(second, 120.0)
expired = manager.expired_pending_identities(131.0)
assert len(expired) == 1
assert expired[0].address == "AA:08"
assert expired[0].handle == 8
def test_peer_address_update_preserves_identity_and_fragmenter_key():
manager = cpp.BLEPeerSessionManager()
peer_identity = identity(0x80)
first_result = manager.handle_identity_handshake(
snapshot("AA:0A"), peer_identity, 9.0
)
rotated = snapshot("AA:0B")
rotated.existing_address_for_identity = "AA:0A"
rotated.identity_has_pending_detach = True
second_result = manager.handle_identity_handshake(rotated, peer_identity, 10.0)
assert second_result.decision == cpp.InputDecision.AcceptedNewIdentity
assert first_result.fragmenter_key == second_result.fragmenter_key
assert manager.session_by_address("AA:0A") is None
view = manager.session_by_address("AA:0B")
assert view is not None
assert view.identity == peer_identity
assert view.identity_key == first_result.identity_key