wip(k1): checkpoint connection recovery rewrite
Capture the current unreleased K1 connection, recovery, lifecycle, viewer, and test work as a single known-bad baseline for subsequent fixes.
This commit is contained in:
@@ -1,9 +1,11 @@
|
||||
import asyncio
|
||||
from collections.abc import Iterator
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
from bleak.backends.device import BLEDevice
|
||||
from bleak.exc import BleakDeviceNotFoundError, BleakGATTProtocolError
|
||||
|
||||
import k1link.device_plugins.xgrids_k1.ble.ap_activation as ap_module
|
||||
@@ -15,20 +17,25 @@ from k1link.device_plugins.xgrids_k1.ble.ap_activation import (
|
||||
build_ap_activation_frame,
|
||||
is_ap_ready_status,
|
||||
)
|
||||
from k1link.device_plugins.xgrids_k1.ble.runtime_arbiter import (
|
||||
BleRuntimeBusy,
|
||||
bind_ble_runtime_owner_loop,
|
||||
ble_runtime_snapshot,
|
||||
configure_ble_runtime_process_lease,
|
||||
reset_ble_runtime_arbiter_for_tests,
|
||||
wait_for_ble_runtime_idle,
|
||||
)
|
||||
from k1link.device_plugins.xgrids_k1.ble.wifi_provisioning import WifiStatus
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def reset_runtime_handle_lease() -> Iterator[None]:
|
||||
with scanner_module._runtime_handle_lock: # noqa: SLF001
|
||||
scanner_module._runtime_handles.clear() # noqa: SLF001
|
||||
scanner_module._runtime_handle_observed_at_monotonic = None # noqa: SLF001
|
||||
scanner_module._runtime_handle_generation = 0 # noqa: SLF001
|
||||
def reset_runtime_handle_lease(tmp_path: Path) -> Iterator[None]:
|
||||
scanner_module.reset_runtime_handles_for_tests()
|
||||
reset_ble_runtime_arbiter_for_tests()
|
||||
configure_ble_runtime_process_lease(tmp_path)
|
||||
yield
|
||||
with scanner_module._runtime_handle_lock: # noqa: SLF001
|
||||
scanner_module._runtime_handles.clear() # noqa: SLF001
|
||||
scanner_module._runtime_handle_observed_at_monotonic = None # noqa: SLF001
|
||||
scanner_module._runtime_handle_generation = 0 # noqa: SLF001
|
||||
scanner_module.reset_runtime_handles_for_tests()
|
||||
reset_ble_runtime_arbiter_for_tests()
|
||||
|
||||
|
||||
def _seed_scan_lease(handles: dict[str, object], *, observed_at: float) -> None:
|
||||
@@ -70,7 +77,7 @@ def test_ap_activation_uses_retained_handle_without_rediscovery(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
device_id = "synthetic-corebluetooth-uuid"
|
||||
retained_handle = object()
|
||||
retained_handle = BLEDevice(device_id, "XGR-K1", details=object())
|
||||
rediscovery_calls: list[tuple[str, float]] = []
|
||||
client_calls: list[object] = []
|
||||
|
||||
@@ -95,19 +102,39 @@ def test_ap_activation_uses_retained_handle_without_rediscovery(
|
||||
)
|
||||
monkeypatch.setattr(ap_module, "BleakClient", CapturingClient)
|
||||
|
||||
with pytest.raises(SelectedHandleObserved) as caught:
|
||||
asyncio.run(
|
||||
ap_module.activate_device_ap_once(
|
||||
async def scenario() -> SelectedHandleObserved:
|
||||
owner_epoch = bind_ble_runtime_owner_loop()
|
||||
captured = scanner_module.CapturedDiscoveredDevice(
|
||||
device=retained_handle,
|
||||
macos_uuid=device_id,
|
||||
owner_epoch=owner_epoch,
|
||||
)
|
||||
scanner_module.pin_connected_device_handle(
|
||||
captured,
|
||||
device_session_id="device-session-a",
|
||||
)
|
||||
with pytest.raises(SelectedHandleObserved) as caught:
|
||||
await ap_module.activate_device_ap_once(
|
||||
device_id,
|
||||
timeout_seconds=1.0,
|
||||
captured_device=captured,
|
||||
)
|
||||
assert (
|
||||
scanner_module.connected_device_capture(
|
||||
device_id,
|
||||
device_session_id="device-session-a",
|
||||
)
|
||||
is None
|
||||
)
|
||||
return caught.value
|
||||
|
||||
error = asyncio.run(scenario())
|
||||
|
||||
assert rediscovery_calls == []
|
||||
assert client_calls == [retained_handle]
|
||||
assert caught.value.operation_stage == "connect" # type: ignore[attr-defined]
|
||||
assert caught.value.device_write_attempted is False # type: ignore[attr-defined]
|
||||
assert caught.value.device_write_confirmed is False # type: ignore[attr-defined]
|
||||
assert error.operation_stage == "connect" # type: ignore[attr-defined]
|
||||
assert error.device_write_attempted is False # type: ignore[attr-defined]
|
||||
assert error.device_write_confirmed is False # type: ignore[attr-defined]
|
||||
|
||||
|
||||
def test_ap_activation_does_not_fallback_when_fresh_scan_omits_device(
|
||||
@@ -150,7 +177,7 @@ def test_ap_activation_write_error_keeps_type_and_adds_safe_gatt_facts(
|
||||
write_characteristic = SimpleNamespace(
|
||||
uuid=ap_module.WRITE_CHARACTERISTIC_UUID,
|
||||
service_uuid=ap_module.SERVICE_UUID,
|
||||
properties=["write"],
|
||||
properties=["write-without-response", "write"],
|
||||
max_write_without_response_size=512,
|
||||
)
|
||||
status_characteristic = SimpleNamespace(
|
||||
@@ -206,5 +233,136 @@ def test_ap_activation_write_error_keeps_type_and_adds_safe_gatt_facts(
|
||||
assert error.operation_stage == "gatt-write" # type: ignore[attr-defined]
|
||||
assert error.device_write_attempted is True # type: ignore[attr-defined]
|
||||
assert error.device_write_confirmed is False # type: ignore[attr-defined]
|
||||
assert error.resolved_write_mode == "without_response" # type: ignore[attr-defined]
|
||||
assert error.write_characteristic_properties == ( # type: ignore[attr-defined]
|
||||
"write",
|
||||
"write-without-response",
|
||||
)
|
||||
assert error.max_write_without_response_size == 512 # type: ignore[attr-defined]
|
||||
assert error.frame_length == FRAME_LENGTH # type: ignore[attr-defined]
|
||||
assert error.att_error_code == 0x03 # type: ignore[attr-defined]
|
||||
assert error.att_error_name == "WRITE_NOT_PERMITTED" # type: ignore[attr-defined]
|
||||
|
||||
|
||||
def test_ap_activation_keeps_same_client_alive_through_caller_handoff(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
device_id = "synthetic-corebluetooth-uuid"
|
||||
retained_handle = BLEDevice(device_id, "XGR-K1", details=object())
|
||||
service = SimpleNamespace(uuid=ap_module.SERVICE_UUID)
|
||||
write_characteristic = SimpleNamespace(
|
||||
uuid=ap_module.WRITE_CHARACTERISTIC_UUID,
|
||||
service_uuid=ap_module.SERVICE_UUID,
|
||||
properties=["write"],
|
||||
max_write_without_response_size=512,
|
||||
)
|
||||
status_characteristic = SimpleNamespace(
|
||||
uuid=ap_module.STATUS_CHARACTERISTIC_UUID,
|
||||
service_uuid=ap_module.SERVICE_UUID,
|
||||
properties=["read"],
|
||||
)
|
||||
entered_clients = 0
|
||||
exited_clients = 0
|
||||
writes: list[tuple[bytes, bool]] = []
|
||||
|
||||
class FakeServices:
|
||||
def get_service(self, uuid: str) -> object | None:
|
||||
return service if uuid == ap_module.SERVICE_UUID else None
|
||||
|
||||
def get_characteristic(self, uuid: str) -> object | None:
|
||||
if uuid == ap_module.WRITE_CHARACTERISTIC_UUID:
|
||||
return write_characteristic
|
||||
if uuid == ap_module.STATUS_CHARACTERISTIC_UUID:
|
||||
return status_characteristic
|
||||
return None
|
||||
|
||||
class ReadyClient:
|
||||
def __init__(self, device: object, **_kwargs: object) -> None:
|
||||
assert device is retained_handle
|
||||
self.services = FakeServices()
|
||||
self.name = "XGR-K1"
|
||||
self.is_connected = False
|
||||
self._write_completed = False
|
||||
|
||||
async def __aenter__(self) -> Any:
|
||||
nonlocal entered_clients
|
||||
entered_clients += 1
|
||||
self.is_connected = True
|
||||
return self
|
||||
|
||||
async def __aexit__(self, *_args: object) -> None:
|
||||
nonlocal exited_clients
|
||||
exited_clients += 1
|
||||
self.is_connected = False
|
||||
|
||||
async def read_gatt_char(self, _characteristic: object) -> bytes:
|
||||
if not self._write_completed:
|
||||
return bytes(52)
|
||||
ready = bytearray(52)
|
||||
mode = b"WIFI_AP"
|
||||
ready[0] = len(mode)
|
||||
ready[1 : 1 + len(mode)] = mode
|
||||
ready[33] = 4
|
||||
ready[34:38] = bytes((192, 168, 56, 1))
|
||||
ready[50] = 1
|
||||
ready[51] = 1
|
||||
return bytes(ready)
|
||||
|
||||
async def write_gatt_char(
|
||||
self,
|
||||
_characteristic: object,
|
||||
value: bytes,
|
||||
*,
|
||||
response: bool,
|
||||
) -> None:
|
||||
writes.append((bytes(value), response))
|
||||
self._write_completed = True
|
||||
|
||||
monkeypatch.setattr(scanner_module, "monotonic", lambda: 100.0)
|
||||
_seed_scan_lease({device_id: retained_handle}, observed_at=100.0)
|
||||
monkeypatch.setattr(ap_module, "BleakClient", ReadyClient)
|
||||
|
||||
async def scenario() -> None:
|
||||
owner_epoch = bind_ble_runtime_owner_loop()
|
||||
captured = scanner_module.CapturedDiscoveredDevice(
|
||||
device=retained_handle,
|
||||
macos_uuid=device_id,
|
||||
owner_epoch=owner_epoch,
|
||||
)
|
||||
scanner_module.pin_connected_device_handle(
|
||||
captured,
|
||||
device_session_id="device-session-a",
|
||||
)
|
||||
async with ap_module.device_ap_activation_session(
|
||||
device_id,
|
||||
timeout_seconds=0.1,
|
||||
poll_interval_seconds=0.01,
|
||||
captured_device=captured,
|
||||
) as result:
|
||||
assert result["ready_observed"] is True
|
||||
assert entered_clients == 1
|
||||
assert exited_clients == 0
|
||||
assert ble_runtime_snapshot()["active_operation_kind"] == "ap-enable"
|
||||
with pytest.raises(BleRuntimeBusy) as busy:
|
||||
await scanner_module.scan(0.01)
|
||||
assert busy.value.active_operation_kind == "ap-enable"
|
||||
# This represents the host CoreWLAN association window: the setup
|
||||
# deadline is over, but the exact same BLE client must remain alive.
|
||||
await asyncio.sleep(0.02)
|
||||
assert exited_clients == 0
|
||||
assert scanner_module.connected_device_recovery_snapshot(
|
||||
device_id,
|
||||
device_session_id="device-session-a",
|
||||
)["gatt_validated_recently"] is True
|
||||
|
||||
assert exited_clients == 1
|
||||
assert await wait_for_ble_runtime_idle()
|
||||
|
||||
asyncio.run(scenario())
|
||||
|
||||
assert len(writes) == 1
|
||||
payload, response = writes[0]
|
||||
assert len(payload) == FRAME_LENGTH
|
||||
assert payload[:COMMAND_OFFSET] == bytes(COMMAND_OFFSET)
|
||||
assert payload[COMMAND_OFFSET] == ENABLE_AP_COMMAND
|
||||
assert response is True
|
||||
|
||||
Reference in New Issue
Block a user