Commit Graph
8 Commits
Author SHA1 Message Date
Matthew Wolter b4b40718e9 Remove internal references from transport symmetry tests
Rename test_g4_transport_symmetry.py to test_transport_symmetry.py.
Strip G4 from module docstring, _g4_ from function names, and finding
IDs (F04, NEW-A, F18, M06, N04) from docstrings and section comments.
2026-04-12 07:56:48 -07:00
Matthew Wolter f6bc0908b0 G4: add verification tests for F04, NEW-A, F18, M06, F16, F17, N04
10 new tests in tests/unit/test_g4_transport_symmetry.py covering all
G4 findings:

- test_g4_tcp_send_write_error_fires_disconnect (F04): TCP write
  OSError fires _disconnect_callback.
- test_g4_serial_send_no_transport_fires_disconnect (NEW-A): serial
  send on None transport fires _disconnect_callback.
- test_g4_serial_send_write_error_fires_disconnect (F04): serial write
  OSError fires _disconnect_callback.
- test_g4_ble_send_no_client_fires_disconnect (F04): BLE send with no
  client fires _disconnect_callback.
- test_g4_serial_connect_timeout (F18): connect raises TimeoutError
  when connection_made never fires.
- test_g4_tcp_oversize_frame_empty_data_returns (M06): oversize header
  with empty trailing data returns without dispatch.
- test_g4_serial_oversize_frame_empty_data_returns (M06): same for
  serial transport.
- test_g4_tcp_receive_count_per_frame_not_per_segment (N04): 3 TCP
  segments carrying 1 frame yield _receive_count == 1.
- test_g4_tcp_multiple_frames_count_correctly (N04): 2 complete frames
  yield _receive_count == 2.

F16 and F17 are covered by the updated pre-existing test in
tests/test_ble_pin_pairing.py (committed with F17).

Refs: Forensics report findings F04, NEW-A, F18, M06, N04
2026-04-11 20:25:52 -07:00
Matthew Wolter 7db73b5817 G4: N04 — move TCP receive counter from data_received to handle_rx
Why: _receive_count incremented inside data_received(), which fires
once per TCP read — not once per completed MeshCore frame. Under TCP
fragmentation a single frame can arrive in multiple segments, inflating
_receive_count relative to _send_count. The disconnect-detection
heuristic (send_count - receive_count >= 5) then never fires because
the receive side is over-counted. Moving the increment into handle_rx
after a complete frame is assembled makes the counter semantically
correct: one increment per MeshCore frame dispatched to the reader.
Refs: Forensics report finding N04
2026-04-11 20:25:26 -07:00
Matthew Wolter 76e2e54157 G4: F17 — re-raise BLE pairing failure instead of swallowing
Why: When BLE pairing failed during connect(), the exception was caught
as a warning and connect() continued normally. This left the transport
in a half-usable state — the BLE link was up but the pairing handshake
never completed, so encrypted characteristics could silently fail.
Now the pairing exception disconnects the client and re-raises, giving
the caller a clean failure. Updated the pre-existing
test_ble_connection_with_pin_failed_pairing test to assert the re-raise
behavior instead of the old swallow-and-continue behavior.
Refs: Forensics report finding F17
2026-04-11 20:25:04 -07:00
Matthew Wolter fe0dcac90f G4: F16 — re-register disconnect callback after BLE client reset
Why: handle_disconnect resets self.client to self._user_provided_client.
That client's disconnected_callback was not re-registered, so subsequent
BLE disconnects after a successful reconnect cycle were missed —
ConnectionManager never learned the link dropped again. Now re-registers
via set_disconnected_callback with a hasattr guard and try/except for
bleak version compatibility. The next connect() call would also re-create
the client with the callback, but this closes the gap between disconnect
and reconnect.
Refs: Forensics report finding F16
2026-04-11 20:24:24 -07:00
Matthew Wolter 9150a49c6f G4: M06 — add return after oversize-frame state reset
Why: When handle_rx detects an oversize frame header (>300 bytes), it
resets state (header, inframe, frame_expected_size) and re-calls itself
on any remaining data. But when data is empty after the header, the
method fell through to the frame-assembly code with inframe=b"",
eventually dispatching an empty frame to reader.handle_rx. Currently
absorbed by F06's umbrella try/except, but a guaranteed crash if that
guard moves. Adding a bare return after the reset block prevents the
fallthrough in both TCP and serial transports.
Refs: Forensics report finding M06
2026-04-11 20:24:06 -07:00
Matthew Wolter d6197dc71e G4: F18 — add timeout to serial_cx.connect() event wait
Why: After create_serial_connection, connect() awaited
_connected_event.wait() with no timeout. If the serial device opened
but connection_made was never called (driver bug, USB adapter glitch),
connect() hung indefinitely. Now wrapped in asyncio.wait_for with a
configurable timeout (default 10s). asyncio.TimeoutError propagates
to the caller for clean failure handling.
Refs: Forensics report finding F18
2026-04-11 20:23:44 -07:00
Matthew Wolter e475a567f0 G4: F04 + NEW-A — symmetric send() failure signaling across all transports
Why: TCP was the only transport that fired _disconnect_callback on a dead
transport or send failure. Serial and BLE returned silently, leaving
ConnectionManager unaware the link was down. Additionally, transport.write()
(TCP/serial) and write_gatt_char (BLE) had no try/except — any write-time
exception escaped the send() coroutine unhandled. This commit makes all three
transports symmetric: (a) fire _disconnect_callback when transport is None/dead,
(b) wrap the write call in try/except and fire _disconnect_callback on failure.
Refs: Forensics report findings F04, NEW-A
2026-04-11 20:23:27 -07:00