Gate 2C works on jp, TODO: test on zerodev1
This commit is contained in:
parent
ff6caae67a
commit
387cd1c57d
6 changed files with 681 additions and 1 deletions
79
migration/Codex_prompt_20260518_1504_Phase2_Gate_2C.md
Normal file
79
migration/Codex_prompt_20260518_1504_Phase2_Gate_2C.md
Normal 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.
|
||||
|
|
@ -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.
|
||||
|
|
@ -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"));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
],
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
180
migration/tests/test_ble_peer_session_manager_pybind.py
Normal file
180
migration/tests/test_ble_peer_session_manager_pybind.py
Normal 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue