Files
NODEDC_MISSION_CORE/tests/test_ble_scanner.py
T
DCCONSTRUCTIONS 0ca7316a24 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.
2026-08-14 14:57:50 +03:00

1594 lines
56 KiB
Python

import asyncio
from collections.abc import Iterator
from pathlib import Path
from typing import Literal
import pytest
from bleak.backends.device import BLEDevice
from bleak.backends.scanner import AdvertisementData
from pytest import MonkeyPatch
import k1link.device_plugins.xgrids_k1.ble.scanner as scanner_module
from k1link.device_plugins.xgrids_k1.ble.runtime_arbiter import (
BleRuntimeBusy,
bind_ble_runtime_owner_loop,
configure_ble_runtime_process_lease,
reset_ble_runtime_arbiter_for_tests,
run_ble_operation,
)
from k1link.device_plugins.xgrids_k1.ble.scanner import (
advertisement_record,
capture_discovered_device,
connected_device_capture,
connected_device_recovery_name,
connected_device_recovery_snapshot,
demote_connected_device_handle_after_gatt_failure,
discover_known_device_capture_for_status_read,
discovered_device,
discovered_device_selection,
mark_captured_device_gatt_validated,
pin_connected_device_handle,
retrieve_connected_device_capture,
retrieve_known_device_capture_for_status_read,
scan,
)
RecoveryOperationKind = Literal["status-read", "wifi-provision", "ap-enable"]
async def _retrieve_connected_inside_ble_lease(
macos_uuid: str,
*,
device_session_id: str,
operation_kind: RecoveryOperationKind = "status-read",
) -> scanner_module.CapturedDiscoveredDevice | None:
return await run_ble_operation(
operation_kind,
hard_timeout_seconds=2.0,
operation=lambda _progress: retrieve_connected_device_capture(
macos_uuid,
device_session_id=device_session_id,
),
)
@pytest.fixture(autouse=True)
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
scanner_module.reset_runtime_handles_for_tests()
reset_ble_runtime_arbiter_for_tests()
class _FakeCoreBluetoothIdentifier:
def __init__(self, value: str) -> None:
self.value = value
def UUIDString(self) -> str: # noqa: N802 - mirrors NSUUID
return self.value
class _FakeCoreBluetoothPeripheral:
def __init__(self, identifier: _FakeCoreBluetoothIdentifier, name: str) -> None:
self._identifier = identifier
self._name = name
def identifier(self) -> _FakeCoreBluetoothIdentifier:
return self._identifier
def name(self) -> str:
return self._name
class _FakeCentralManager:
def __init__(
self,
responses: list[list[_FakeCoreBluetoothPeripheral]],
) -> None:
self.responses = responses
self.requests: list[list[object]] = []
def retrievePeripheralsWithIdentifiers_( # noqa: N802 - mirrors CoreBluetooth
self,
identifiers: list[object],
) -> list[_FakeCoreBluetoothPeripheral]:
self.requests.append(identifiers)
return self.responses.pop(0)
class _FakeCoreBluetoothManager:
def __init__(
self,
loop: asyncio.AbstractEventLoop,
central_manager: _FakeCentralManager,
) -> None:
self.event_loop = loop
self.central_manager = central_manager
self.ready_calls = 0
async def wait_until_ready(self) -> None:
self.ready_calls += 1
def _active_status_read_runtime(owner_epoch: int = 7) -> dict[str, object]:
return {
"owner_epoch": owner_epoch,
"owner_loop_bound": True,
"active_operation_kind": "status-read",
"cleanup_pending": False,
"poisoned": False,
}
def test_advertisement_record_marks_k1_candidate() -> None:
device = BLEDevice("TEST-UUID", "Unknown", details=None)
advertisement = AdvertisementData(
local_name="Lixel-K1-1234",
manufacturer_data={123: b"\x01\x02"},
service_data={"service": b"\xaa"},
service_uuids=["B", "a"],
tx_power=-4,
rssi=-52,
platform_data=(),
)
record = advertisement_record(device, advertisement)
assert record["k1_name_candidate"] is True
assert record["id_kind"] == "corebluetooth_uuid"
assert record["manufacturer_data_hex"] == {"123": "0102"}
assert record["service_data_hex"] == {"service": "aa"}
assert record["service_uuids"] == ["B", "a"]
def test_advertisement_record_marks_synthetic_xgr_name_as_k1_candidate() -> None:
device = BLEDevice("00000000-0000-0000-0000-000000000001", "XGR-TEST01", None)
advertisement = AdvertisementData(
local_name="XGR-TEST01",
manufacturer_data={},
service_data={},
service_uuids=[],
tx_power=0,
rssi=-60,
platform_data=(),
)
record = advertisement_record(device, advertisement)
assert record["k1_name_candidate"] is True
def test_scan_retains_the_live_corebluetooth_handle(
monkeypatch: MonkeyPatch,
) -> None:
device = BLEDevice("LIVE-UUID", "XGR-LIVE", details=object())
advertisement = AdvertisementData(
local_name="XGR-LIVE",
manufacturer_data={},
service_data={},
service_uuids=[],
tx_power=0,
rssi=-41,
platform_data=(),
)
async def fake_discover(**_kwargs: object) -> dict[str, tuple[BLEDevice, AdvertisementData]]:
return {"LIVE-UUID": (device, advertisement)}
monkeypatch.setattr(
"k1link.device_plugins.xgrids_k1.ble.scanner.BleakScanner.discover",
fake_discover,
)
async def scenario() -> object:
result = await scan(1.0)
assert result["devices"][0]["macos_uuid"] == "LIVE-UUID"
assert discovered_device("LIVE-UUID") is device
assert discovered_device_selection("LIVE-UUID").from_fresh_scan is True
return result
asyncio.run(scenario())
def test_scan_start_invalidates_previous_lease_and_failure_leaves_it_empty(
monkeypatch: MonkeyPatch,
) -> None:
old_device = BLEDevice("OLD-UUID", "XGR-OLD", details=object())
old_advertisement = AdvertisementData(
local_name="XGR-OLD",
manufacturer_data={},
service_data={},
service_uuids=[],
tx_power=0,
rssi=-41,
platform_data=(),
)
async def scenario() -> None:
async def initial_discover(
**_kwargs: object,
) -> dict[str, tuple[BLEDevice, AdvertisementData]]:
return {old_device.address: (old_device, old_advertisement)}
monkeypatch.setattr(scanner_module.BleakScanner, "discover", initial_discover)
await scan(1.0)
assert discovered_device(old_device.address) is old_device
failing_scan_started = asyncio.Event()
release_failing_scan = asyncio.Event()
async def failing_discover(**_kwargs: object) -> object:
failing_scan_started.set()
await release_failing_scan.wait()
raise RuntimeError("synthetic BLE scan failure")
monkeypatch.setattr(scanner_module.BleakScanner, "discover", failing_discover)
scan_task = asyncio.create_task(scan(1.0))
await failing_scan_started.wait()
# Starting a new explicit scan revokes the prior generation before I/O.
assert discovered_device(old_device.address) is None
assert discovered_device_selection(old_device.address).from_fresh_scan is False
release_failing_scan.set()
with pytest.raises(RuntimeError, match="synthetic BLE scan failure"):
await scan_task
assert discovered_device(old_device.address) is None
assert discovered_device_selection(old_device.address).from_fresh_scan is False
asyncio.run(scenario())
def test_process_arbiter_rejects_overlapping_scan(monkeypatch: MonkeyPatch) -> None:
older_device = BLEDevice("OLDER-UUID", "XGR-OLDER", details=object())
older_advertisement = AdvertisementData(
local_name="XGR-OLDER",
manufacturer_data={},
service_data={},
service_uuids=[],
tx_power=0,
rssi=-51,
platform_data=(),
)
async def scenario() -> None:
older_scan_started = asyncio.Event()
release_older_scan = asyncio.Event()
busy_scan_admitted = False
async def blocked_discover(
**_kwargs: object,
) -> dict[str, tuple[BLEDevice, AdvertisementData]]:
older_scan_started.set()
await release_older_scan.wait()
return {older_device.address: (older_device, older_advertisement)}
monkeypatch.setattr(scanner_module.BleakScanner, "discover", blocked_discover)
older_task = asyncio.create_task(scan(1.0))
await older_scan_started.wait()
def record_busy_admission() -> None:
nonlocal busy_scan_admitted
busy_scan_admitted = True
with pytest.raises(BleRuntimeBusy) as raised:
await scan(1.0, on_admitted=record_busy_admission)
assert raised.value.reason_code == "ble-runtime-busy"
assert raised.value.active_operation_kind == "scan"
assert busy_scan_admitted is False
release_older_scan.set()
await older_task
assert discovered_device(older_device.address) is older_device
asyncio.run(scenario())
def test_owner_epoch_rebind_rejects_late_handle_publish() -> None:
old_device = BLEDevice("OLD-UUID", "XGR-OLD", details=object())
late_device = BLEDevice("LATE-UUID", "XGR-LATE", details=object())
first_loop = asyncio.new_event_loop()
try:
first_epoch, generation = first_loop.run_until_complete(_publish_owned_handle(old_device))
finally:
first_loop.close()
second_loop = asyncio.new_event_loop()
try:
second_epoch = second_loop.run_until_complete(
_rebind_and_reject_late_publish(
generation,
first_epoch,
old_device,
late_device,
)
)
assert second_epoch > first_epoch
assert scanner_module._runtime_handles == {} # noqa: SLF001
finally:
second_loop.close()
async def _publish_owned_handle(device: BLEDevice) -> tuple[int, int]:
owner_epoch = bind_ble_runtime_owner_loop()
generation = scanner_module._begin_scan_generation(owner_epoch) # noqa: SLF001
scanner_module._publish_scan_handles( # noqa: SLF001
generation,
owner_epoch,
{device.address: device},
)
assert discovered_device(device.address) is device
return owner_epoch, generation
async def _rebind_and_reject_late_publish(
generation: int,
old_owner_epoch: int,
old_device: BLEDevice,
late_device: BLEDevice,
) -> int:
new_owner_epoch = bind_ble_runtime_owner_loop()
assert discovered_device(old_device.address) is None
scanner_module._publish_scan_handles( # noqa: SLF001
generation,
old_owner_epoch,
{old_device.address: old_device, late_device.address: late_device},
)
assert discovered_device(late_device.address) is None
return new_owner_epoch
def test_runtime_handle_from_latest_scan_does_not_expire_by_age(
monkeypatch: MonkeyPatch,
) -> None:
clock = [100.0]
handle = BLEDevice("LIVE-UUID", "XGR-K1", details=object())
monkeypatch.setattr(scanner_module, "monotonic", lambda: clock[0])
scanner_module._runtime_handles[handle.address] = handle # noqa: SLF001
scanner_module._runtime_handle_observed_at_monotonic = clock[0] # noqa: SLF001
clock[0] += scanner_module.BLE_DISCOVERY_CANDIDATE_LEASE_TTL_SECONDS + 0.001
assert discovered_device(handle.address) is handle
clock[0] = 100.0 + scanner_module.BLE_RUNTIME_HANDLE_LEASE_TTL_SECONDS + 0.001
assert discovered_device(handle.address) is handle
def test_connected_handle_is_session_scoped_and_survives_wall_clock_age(
monkeypatch: MonkeyPatch,
) -> None:
monotonic_clock = [100.0]
suspend_aware_clock = [1_000.0]
device = BLEDevice("LIVE-UUID", "XGR-LIVE", details=object())
advertisement = AdvertisementData(
local_name="XGR-LIVE",
manufacturer_data={},
service_data={},
service_uuids=[],
tx_power=0,
rssi=-41,
platform_data=(),
)
discoveries = [
{device.address: (device, advertisement)},
{},
]
async def fake_discover(**_kwargs: object) -> dict[str, tuple[BLEDevice, AdvertisementData]]:
return discoveries.pop(0)
monkeypatch.setattr(scanner_module.BleakScanner, "discover", fake_discover)
monkeypatch.setattr(scanner_module, "monotonic", lambda: monotonic_clock[0])
monkeypatch.setattr(
scanner_module,
"suspend_aware_time",
lambda: suspend_aware_clock[0],
)
async def scenario() -> None:
await scan(1.0)
captured = capture_discovered_device(device.address)
assert captured is not None
pin_connected_device_handle(captured, device_session_id="device-session-a")
await scan(1.0)
assert discovered_device_selection(
device.address
) == scanner_module.DiscoveredDeviceSelection(
device=None,
from_fresh_scan=True,
)
retained_capture = connected_device_capture(
device.address,
device_session_id="device-session-a",
)
assert retained_capture is not None
assert retained_capture.device is captured.device
assert retained_capture.source == "retained-session"
owner_epoch_for_loop = scanner_module.ble_runtime_owner_epoch_for_current_loop
monkeypatch.setattr(
scanner_module,
"ble_runtime_owner_epoch_for_current_loop",
lambda: None,
)
assert discovered_device_selection(
device.address
) == scanner_module.DiscoveredDeviceSelection(
device=None,
from_fresh_scan=False,
)
monkeypatch.setattr(
scanner_module,
"ble_runtime_owner_epoch_for_current_loop",
owner_epoch_for_loop,
)
retained_capture = connected_device_capture(
device.address,
device_session_id="device-session-a",
)
assert retained_capture is not None
assert retained_capture.device is captured.device
assert (
connected_device_capture(
device.address,
device_session_id="device-session-b",
)
is None
)
assert connected_device_recovery_snapshot(
device.address,
device_session_id="device-session-a",
) == {
"status": "retained",
"scope": "owner-epoch-device-session",
"advertised_now": False,
"gatt_validated_recently": False,
}
monotonic_clock[0] += scanner_module.BLE_RUNTIME_HANDLE_LEASE_TTL_SECONDS + 0.001
suspend_aware_clock[0] += (
scanner_module.BLE_RUNTIME_HANDLE_LEASE_TTL_SECONDS + 0.001
)
assert connected_device_capture(
device.address,
device_session_id="device-session-a",
) is not None
assert connected_device_recovery_snapshot(
device.address,
device_session_id="device-session-a",
) == {
"status": "retained",
"scope": "owner-epoch-device-session",
"advertised_now": False,
"gatt_validated_recently": False,
}
asyncio.run(scenario())
def test_scan_and_retained_handle_survive_macos_sleep_until_explicit_invalidation(
monkeypatch: MonkeyPatch,
) -> None:
monotonic_clock = [100.0]
suspend_aware_clock = [1_000.0]
device = BLEDevice("LIVE-UUID", "XGR-LIVE", details=object())
advertisement = AdvertisementData(
local_name="XGR-LIVE",
manufacturer_data={},
service_data={},
service_uuids=[],
tx_power=0,
rssi=-41,
platform_data=(),
)
async def fake_discover(
**_kwargs: object,
) -> dict[str, tuple[BLEDevice, AdvertisementData]]:
return {device.address: (device, advertisement)}
monkeypatch.setattr(scanner_module.BleakScanner, "discover", fake_discover)
monkeypatch.setattr(scanner_module, "monotonic", lambda: monotonic_clock[0])
monkeypatch.setattr(
scanner_module,
"suspend_aware_time",
lambda: suspend_aware_clock[0],
)
async def scenario() -> None:
await scan(1.0)
captured = capture_discovered_device(device.address)
assert captured is not None
assert mark_captured_device_gatt_validated(captured) is True
pin_connected_device_handle(captured, device_session_id="device-session-a")
assert discovered_device(device.address) is device
retained_capture = connected_device_capture(
device.address,
device_session_id="device-session-a",
)
assert retained_capture is not None
assert retained_capture.device is captured.device
assert retained_capture.source == "retained-session"
assert connected_device_recovery_snapshot(
device.address,
device_session_id="device-session-a",
) == {
"status": "retained",
"scope": "owner-epoch-device-session",
"advertised_now": True,
"gatt_validated_recently": True,
}
# macOS monotonic time may stop while the laptop sleeps. Wall time is
# deliberately the only clock advanced here. GATT validation has the
# shortest lease, so it is revoked before the scan/retained handles.
suspend_aware_clock[0] += (
scanner_module.BLE_GATT_VALIDATION_RECENCY_TTL_SECONDS + 0.001
)
assert connected_device_recovery_snapshot(
device.address,
device_session_id="device-session-a",
) == {
"status": "retained",
"scope": "owner-epoch-device-session",
"advertised_now": True,
"gatt_validated_recently": False,
}
suspend_aware_clock[0] += (
scanner_module.BLE_RUNTIME_HANDLE_LEASE_TTL_SECONDS
- scanner_module.BLE_GATT_VALIDATION_RECENCY_TTL_SECONDS
)
assert discovered_device(device.address) is device
assert scanner_module.captured_device_handle(captured) is device
assert connected_device_capture(
device.address,
device_session_id="device-session-a",
) is not None
assert connected_device_recovery_snapshot(
device.address,
device_session_id="device-session-a",
) == {
"status": "retained",
"scope": "owner-epoch-device-session",
"advertised_now": True,
"gatt_validated_recently": False,
}
asyncio.run(scenario())
def test_slow_gatt_handoff_refreshes_capture_and_retained_lease_together(
monkeypatch: MonkeyPatch,
) -> None:
monotonic_clock = [100.0]
suspend_aware_clock = [1_000.0]
monkeypatch.setattr(scanner_module, "monotonic", lambda: monotonic_clock[0])
monkeypatch.setattr(
scanner_module,
"suspend_aware_time",
lambda: suspend_aware_clock[0],
)
device = BLEDevice("SLOW-UUID", "XGR-SLOW", details=object())
async def scenario() -> None:
owner_epoch = bind_ble_runtime_owner_loop()
captured = scanner_module.CapturedDiscoveredDevice(
device=device,
macos_uuid=device.address,
owner_epoch=owner_epoch,
scan_generation=7,
captured_at_monotonic=monotonic_clock[0],
captured_at_suspend_aware=suspend_aware_clock[0],
)
# A real connect/baseline can outlive the original scan lease. Its
# exact successful GATT result is the new freshness boundary.
elapsed = scanner_module.BLE_RUNTIME_HANDLE_LEASE_TTL_SECONDS + 2.0
monotonic_clock[0] += elapsed
suspend_aware_clock[0] += elapsed
assert scanner_module.captured_device_handle(captured) is device
assert mark_captured_device_gatt_validated(captured) is True
pin_connected_device_handle(captured, device_session_id="device-session-a")
retained = connected_device_capture(
device.address,
device_session_id="device-session-a",
)
assert retained is not None
assert retained.device is device
assert retained.source == "retained-session"
assert retained.captured_at_monotonic == monotonic_clock[0]
assert retained.captured_at_suspend_aware == suspend_aware_clock[0]
assert scanner_module.captured_device_handle(retained) is device
asyncio.run(scenario())
def test_validated_expired_retained_capture_repins_to_new_session(
monkeypatch: MonkeyPatch,
) -> None:
monotonic_clock = [100.0]
suspend_aware_clock = [1_000.0]
monkeypatch.setattr(scanner_module, "monotonic", lambda: monotonic_clock[0])
monkeypatch.setattr(
scanner_module,
"suspend_aware_time",
lambda: suspend_aware_clock[0],
)
device = BLEDevice("ROTATE-UUID", "XGR-ROTATE", details=object())
async def scenario() -> None:
owner_epoch = bind_ble_runtime_owner_loop()
captured = scanner_module.CapturedDiscoveredDevice(
device=device,
macos_uuid=device.address,
owner_epoch=owner_epoch,
scan_generation=11,
captured_at_monotonic=monotonic_clock[0],
captured_at_suspend_aware=suspend_aware_clock[0],
)
pin_connected_device_handle(captured, device_session_id="old-session")
elapsed = scanner_module.BLE_RUNTIME_HANDLE_LEASE_TTL_SECONDS + 2.0
monotonic_clock[0] += elapsed
suspend_aware_clock[0] += elapsed
assert connected_device_capture(
device.address,
device_session_id="old-session",
) is not None
# The exact new GATT baseline, not the old scan timestamp, is the
# admission boundary for rotating this same transport into a new
# facade device session.
assert mark_captured_device_gatt_validated(captured) is True
pin_connected_device_handle(captured, device_session_id="new-session")
assert connected_device_capture(
device.address,
device_session_id="old-session",
) is None
repinned = connected_device_capture(
device.address,
device_session_id="new-session",
)
assert repinned is not None
assert repinned.device is device
assert repinned.captured_at_monotonic == monotonic_clock[0]
assert repinned.captured_at_suspend_aware == suspend_aware_clock[0]
assert connected_device_recovery_snapshot(
device.address,
device_session_id="new-session",
)["gatt_validated_recently"] is True
asyncio.run(scenario())
@pytest.mark.parametrize("source", ["retrieved-session", "retrieved-durable"])
def test_retrieved_capture_cannot_be_pinned_before_exact_gatt_validation(
source: str,
) -> None:
async def scenario() -> None:
owner_epoch = bind_ble_runtime_owner_loop()
device = BLEDevice("UNVALIDATED-UUID", "XGR-UNVALIDATED", details=object())
now = scanner_module._freshness_now() # noqa: SLF001
captured = scanner_module.CapturedDiscoveredDevice(
device=device,
macos_uuid=device.address,
owner_epoch=owner_epoch,
captured_at_monotonic=now.monotonic,
captured_at_suspend_aware=now.suspend_aware,
source=source, # type: ignore[arg-type]
)
with pytest.raises(RuntimeError, match="requires exact GATT validation"):
pin_connected_device_handle(captured, device_session_id="new-session")
assert connected_device_recovery_snapshot(
device.address,
device_session_id="new-session",
)["status"] == "unavailable"
asyncio.run(scenario())
def test_gatt_handoff_token_retains_and_matches_exact_device_object() -> None:
async def scenario() -> None:
owner_epoch = bind_ble_runtime_owner_loop()
validated_device = BLEDevice("HANDOFF-UUID", "XGR-HANDOFF", details=object())
different_device = BLEDevice("HANDOFF-UUID", "XGR-HANDOFF", details=object())
now = scanner_module._freshness_now() # noqa: SLF001
validated = scanner_module.CapturedDiscoveredDevice(
device=validated_device,
macos_uuid=validated_device.address,
owner_epoch=owner_epoch,
scan_generation=13,
captured_at_monotonic=now.monotonic,
captured_at_suspend_aware=now.suspend_aware,
source="retrieved-durable",
)
impostor = scanner_module.CapturedDiscoveredDevice(
device=different_device,
macos_uuid=different_device.address,
owner_epoch=owner_epoch,
scan_generation=13,
captured_at_monotonic=now.monotonic,
captured_at_suspend_aware=now.suspend_aware,
source="retrieved-durable",
)
assert mark_captured_device_gatt_validated(validated) is True
with pytest.raises(RuntimeError, match="requires exact GATT validation"):
pin_connected_device_handle(impostor, device_session_id="device-session-a")
pin_connected_device_handle(validated, device_session_id="device-session-a")
retained = connected_device_capture(
validated_device.address,
device_session_id="device-session-a",
)
assert retained is not None
assert retained.device is validated_device
asyncio.run(scenario())
@pytest.mark.parametrize(
"operation_kind",
["status-read", "wifi-provision", "ap-enable"],
)
def test_retained_handle_survives_age_and_can_be_explicitly_retrieved(
monkeypatch: MonkeyPatch,
operation_kind: RecoveryOperationKind,
) -> None:
monotonic_clock = [100.0]
suspend_aware_clock = [1_000.0]
monkeypatch.setattr(scanner_module, "monotonic", lambda: monotonic_clock[0])
monkeypatch.setattr(
scanner_module,
"suspend_aware_time",
lambda: suspend_aware_clock[0],
)
async def scenario() -> None:
owner_epoch = bind_ble_runtime_owner_loop()
identifier = _FakeCoreBluetoothIdentifier("RETRIEVE-UUID")
original_peripheral = _FakeCoreBluetoothPeripheral(identifier, "XGR-OLD")
retrieved_peripheral = _FakeCoreBluetoothPeripheral(identifier, "XGR-NEW")
central = _FakeCentralManager([[retrieved_peripheral]])
manager = _FakeCoreBluetoothManager(asyncio.get_running_loop(), central)
original_device = BLEDevice(
identifier.value,
"XGR-OLD",
details=(original_peripheral, manager),
)
captured = scanner_module.CapturedDiscoveredDevice(
device=original_device,
macos_uuid=original_device.address,
owner_epoch=owner_epoch,
scan_generation=9,
captured_at_monotonic=monotonic_clock[0],
captured_at_suspend_aware=suspend_aware_clock[0],
)
pin_connected_device_handle(captured, device_session_id="device-session-a")
elapsed = scanner_module.BLE_RUNTIME_HANDLE_LEASE_TTL_SECONDS + 0.001
monotonic_clock[0] += elapsed
suspend_aware_clock[0] += elapsed
assert scanner_module.captured_device_handle(captured) is original_device
assert connected_device_capture(
original_device.address,
device_session_id="device-session-a",
) is not None
assert connected_device_recovery_snapshot(
original_device.address,
device_session_id="device-session-a",
) == {
"status": "retained",
"scope": "owner-epoch-device-session",
"advertised_now": False,
"gatt_validated_recently": False,
}
retrieved = await _retrieve_connected_inside_ble_lease(
original_device.address,
device_session_id="device-session-a",
operation_kind=operation_kind,
)
assert retrieved is not None
assert retrieved.source == "retrieved-session"
assert retrieved.device is not original_device
assert retrieved.device.details == (retrieved_peripheral, manager)
assert scanner_module.captured_device_handle(retrieved) is retrieved.device
assert central.requests == [[identifier]]
assert manager.ready_calls == 1
assert connected_device_recovery_snapshot(
original_device.address,
device_session_id="device-session-a",
) == {
"status": "retained",
"scope": "owner-epoch-device-session",
"advertised_now": False,
"gatt_validated_recently": False,
}
asyncio.run(scenario())
def test_gatt_failure_invalidates_session_token_until_new_explicit_scan() -> None:
async def scenario() -> None:
owner_epoch = bind_ble_runtime_owner_loop()
identifier = _FakeCoreBluetoothIdentifier("RETRY-UUID")
original_peripheral = _FakeCoreBluetoothPeripheral(identifier, "XGR-OLD")
retrieved_peripheral = _FakeCoreBluetoothPeripheral(identifier, "XGR-NEW")
central = _FakeCentralManager([[], [retrieved_peripheral]])
manager = _FakeCoreBluetoothManager(asyncio.get_running_loop(), central)
original_device = BLEDevice(
identifier.value,
"XGR-OLD",
details=(original_peripheral, manager),
)
captured = scanner_module.CapturedDiscoveredDevice(
device=original_device,
macos_uuid=original_device.address,
owner_epoch=owner_epoch,
scan_generation=3,
)
pin_connected_device_handle(captured, device_session_id="device-session-a")
assert demote_connected_device_handle_after_gatt_failure(captured) is True
assert (
await _retrieve_connected_inside_ble_lease(
original_device.address,
device_session_id="device-session-a",
)
is None
)
assert connected_device_recovery_snapshot(
original_device.address,
device_session_id="device-session-a",
)["status"] == "unavailable"
assert central.requests == []
asyncio.run(scenario())
def test_retrieval_rejects_uuid_session_and_owner_mismatch(
monkeypatch: MonkeyPatch,
) -> None:
async def scenario() -> None:
owner_epoch = bind_ble_runtime_owner_loop()
identifier = _FakeCoreBluetoothIdentifier("EXACT-UUID")
peripheral = _FakeCoreBluetoothPeripheral(identifier, "XGR-EXACT")
central = _FakeCentralManager([[peripheral]])
manager = _FakeCoreBluetoothManager(asyncio.get_running_loop(), central)
device = BLEDevice(
identifier.value,
"XGR-EXACT",
details=(peripheral, manager),
)
captured = scanner_module.CapturedDiscoveredDevice(
device=device,
macos_uuid=device.address,
owner_epoch=owner_epoch,
)
pin_connected_device_handle(captured, device_session_id="device-session-a")
assert (
await _retrieve_connected_inside_ble_lease(
"OTHER-UUID",
device_session_id="device-session-a",
)
is None
)
assert (
await _retrieve_connected_inside_ble_lease(
device.address,
device_session_id="device-session-b",
)
is None
)
owner_epoch_lookup = scanner_module.ble_runtime_owner_epoch_for_current_loop
monkeypatch.setattr(
scanner_module,
"ble_runtime_owner_epoch_for_current_loop",
lambda: owner_epoch + 1,
)
assert (
await _retrieve_connected_inside_ble_lease(
device.address,
device_session_id="device-session-a",
)
is None
)
monkeypatch.setattr(
scanner_module,
"ble_runtime_owner_epoch_for_current_loop",
owner_epoch_lookup,
)
assert central.requests == []
assert connected_device_recovery_snapshot(
device.address,
device_session_id="device-session-a",
)["status"] == "retained"
asyncio.run(scenario())
def test_connected_retrieval_requires_active_owned_ble_lease() -> None:
async def scenario() -> None:
owner_epoch = bind_ble_runtime_owner_loop()
identifier = _FakeCoreBluetoothIdentifier("LEASE-UUID")
peripheral = _FakeCoreBluetoothPeripheral(identifier, "XGR-LEASE")
central = _FakeCentralManager([[peripheral]])
manager = _FakeCoreBluetoothManager(asyncio.get_running_loop(), central)
device = BLEDevice(
identifier.value,
"XGR-LEASE",
details=(peripheral, manager),
)
captured = scanner_module.CapturedDiscoveredDevice(
device=device,
macos_uuid=device.address,
owner_epoch=owner_epoch,
)
pin_connected_device_handle(captured, device_session_id="device-session-a")
assert (
await retrieve_connected_device_capture(
device.address,
device_session_id="device-session-a",
)
is None
)
assert central.requests == []
assert connected_device_recovery_snapshot(
device.address,
device_session_id="device-session-a",
)["status"] == "retained"
asyncio.run(scenario())
def test_connected_retrieval_rechecks_same_operation_kind_after_native_await(
monkeypatch: MonkeyPatch,
) -> None:
async def scenario() -> None:
owner_epoch = bind_ble_runtime_owner_loop()
identifier = _FakeCoreBluetoothIdentifier("KIND-UUID")
peripheral = _FakeCoreBluetoothPeripheral(identifier, "XGR-KIND")
central = _FakeCentralManager([[peripheral]])
manager = _FakeCoreBluetoothManager(asyncio.get_running_loop(), central)
device = BLEDevice(identifier.value, "XGR-KIND", details=(peripheral, manager))
captured = scanner_module.CapturedDiscoveredDevice(
device=device,
macos_uuid=device.address,
owner_epoch=owner_epoch,
)
pin_connected_device_handle(captured, device_session_id="device-session-a")
runtime = {
"owner_epoch": owner_epoch,
"owner_loop_bound": True,
"active_operation_kind": "status-read",
"cleanup_pending": False,
"poisoned": False,
}
monkeypatch.setattr(scanner_module, "ble_runtime_snapshot", lambda: dict(runtime))
async def change_active_operation() -> None:
manager.ready_calls += 1
runtime["active_operation_kind"] = "wifi-provision"
manager.wait_until_ready = change_active_operation # type: ignore[method-assign]
assert (
await retrieve_connected_device_capture(
device.address,
device_session_id="device-session-a",
)
is None
)
assert central.requests == [[identifier]]
# A changed lease never consumes or replaces the exact process token.
runtime["active_operation_kind"] = None
assert connected_device_recovery_snapshot(
device.address,
device_session_id="device-session-a",
)["status"] == "retained"
asyncio.run(scenario())
def test_connected_device_recovery_name_is_exact_but_not_presence(
monkeypatch: MonkeyPatch,
) -> None:
monotonic_clock = [100.0]
suspend_aware_clock = [1_000.0]
monkeypatch.setattr(scanner_module, "monotonic", lambda: monotonic_clock[0])
monkeypatch.setattr(
scanner_module,
"suspend_aware_time",
lambda: suspend_aware_clock[0],
)
async def scenario() -> None:
owner_epoch = bind_ble_runtime_owner_loop()
device = BLEDevice("NAME-UUID", " XGR-EXACT ", details=object())
captured = scanner_module.CapturedDiscoveredDevice(
device=device,
macos_uuid=device.address,
owner_epoch=owner_epoch,
captured_at_monotonic=monotonic_clock[0],
captured_at_suspend_aware=suspend_aware_clock[0],
)
pin_connected_device_handle(captured, device_session_id="device-session-a")
elapsed = scanner_module.BLE_RUNTIME_HANDLE_LEASE_TTL_SECONDS + 1.0
monotonic_clock[0] += elapsed
suspend_aware_clock[0] += elapsed
assert connected_device_capture(
device.address,
device_session_id="device-session-a",
) is not None
assert connected_device_recovery_name(
device.address,
device_session_id="device-session-a",
) == "XGR-EXACT"
assert connected_device_recovery_name(
device.address,
device_session_id="other-session",
) is None
snapshot = connected_device_recovery_snapshot(
device.address,
device_session_id="device-session-a",
)
assert snapshot["status"] == "retained"
assert snapshot["advertised_now"] is False
assert snapshot["gatt_validated_recently"] is False
asyncio.run(scenario())
def test_durable_uuid_retrieval_is_available_only_inside_explicit_status_read(
monkeypatch: MonkeyPatch,
) -> None:
macos_uuid = "11111111-2222-4333-8444-555555555555"
async def scenario() -> None:
identifier = _FakeCoreBluetoothIdentifier(macos_uuid)
peripheral = _FakeCoreBluetoothPeripheral(identifier, "XGR-DURABLE")
central = _FakeCentralManager([[peripheral]])
manager = _FakeCoreBluetoothManager(asyncio.get_running_loop(), central)
monkeypatch.setattr(
scanner_module,
"ble_runtime_owner_epoch_for_current_loop",
lambda: 7,
)
runtime = _active_status_read_runtime()
monkeypatch.setattr(scanner_module, "ble_runtime_snapshot", lambda: runtime)
monkeypatch.setattr(
scanner_module,
"_new_corebluetooth_retrieval_context",
lambda requested: (manager, identifier) if requested == macos_uuid else None,
)
captured = await retrieve_known_device_capture_for_status_read(macos_uuid)
assert captured is not None
assert captured.macos_uuid == macos_uuid
assert captured.source == "retrieved-durable"
assert captured.owner_epoch == 7
assert captured.device.details == (peripheral, manager)
assert central.requests == [[identifier]]
assert manager.ready_calls == 1
# Retrieval is only an opaque transport object. It is neither added to
# discovery nor pinned as a current device session by this helper.
assert discovered_device(macos_uuid) is None
assert connected_device_recovery_snapshot(
macos_uuid,
device_session_id="any-session",
)["status"] == "unavailable"
asyncio.run(scenario())
def test_durable_uuid_retrieval_rejects_non_status_operation_before_native_call(
monkeypatch: MonkeyPatch,
) -> None:
calls: list[str] = []
monkeypatch.setattr(
scanner_module,
"ble_runtime_owner_epoch_for_current_loop",
lambda: 7,
)
monkeypatch.setattr(
scanner_module,
"ble_runtime_snapshot",
lambda: {
**_active_status_read_runtime(),
"active_operation_kind": "wifi-provision",
},
)
monkeypatch.setattr(
scanner_module,
"_new_corebluetooth_retrieval_context",
lambda requested: calls.append(requested),
)
assert (
asyncio.run(
retrieve_known_device_capture_for_status_read(
"11111111-2222-4333-8444-555555555555"
)
)
is None
)
assert calls == []
def test_exact_uuid_advertisement_scan_captures_without_publishing_discovery(
monkeypatch: MonkeyPatch,
) -> None:
macos_uuid = "AAAAAAAA-BBBB-4CCC-8DDD-EEEEEEEEEEEE"
native_details = object()
candidate = BLEDevice(
macos_uuid.lower(),
"XGR-ADVERTISEMENT",
details=native_details,
)
calls: list[tuple[str, float]] = []
async def find_exact(address: str, *, timeout: float) -> BLEDevice:
calls.append((address, timeout))
return candidate
monkeypatch.setattr(
scanner_module,
"ble_runtime_owner_epoch_for_current_loop",
lambda: 7,
)
monkeypatch.setattr(
scanner_module,
"ble_runtime_snapshot",
lambda: _active_status_read_runtime(),
)
monkeypatch.setattr(
scanner_module.BleakScanner,
"find_device_by_address",
find_exact,
)
captured = asyncio.run(
discover_known_device_capture_for_status_read(
macos_uuid,
timeout_seconds=30.0,
)
)
assert captured is not None
assert captured.macos_uuid == macos_uuid
assert captured.device.address == macos_uuid
assert captured.device.details is native_details
assert captured.source == "retrieved-durable"
assert calls == [(macos_uuid, 30.0)]
assert discovered_device(macos_uuid) is None
assert connected_device_recovery_snapshot(
macos_uuid,
device_session_id="any-session",
)["status"] == "unavailable"
def test_exact_uuid_advertisement_scan_rejects_wrong_uuid(
monkeypatch: MonkeyPatch,
) -> None:
macos_uuid = "AAAAAAAA-BBBB-4CCC-8DDD-EEEEEEEEEEEE"
wrong = BLEDevice(
"11111111-2222-4333-8444-555555555555",
"XGR-WRONG",
details=object(),
)
async def return_wrong(_address: str, *, timeout: float) -> BLEDevice:
assert timeout == 30.0
return wrong
monkeypatch.setattr(
scanner_module,
"ble_runtime_owner_epoch_for_current_loop",
lambda: 7,
)
monkeypatch.setattr(
scanner_module,
"ble_runtime_snapshot",
lambda: _active_status_read_runtime(),
)
monkeypatch.setattr(
scanner_module.BleakScanner,
"find_device_by_address",
return_wrong,
)
assert (
asyncio.run(
discover_known_device_capture_for_status_read(
macos_uuid,
timeout_seconds=30.0,
)
)
is None
)
assert discovered_device(macos_uuid) is None
def test_exact_uuid_advertisement_scan_rejects_timeout_and_owner_change(
monkeypatch: MonkeyPatch,
) -> None:
macos_uuid = "AAAAAAAA-BBBB-4CCC-8DDD-EEEEEEEEEEEE"
candidate = BLEDevice(macos_uuid, "XGR-LATE", details=object())
runtime_snapshots = [
_active_status_read_runtime(),
_active_status_read_runtime(owner_epoch=8),
]
responses: list[BLEDevice | None] = [None, candidate]
async def find_exact(_address: str, *, timeout: float) -> BLEDevice | None:
assert timeout == 30.0
return responses.pop(0)
monkeypatch.setattr(
scanner_module,
"ble_runtime_owner_epoch_for_current_loop",
lambda: 7,
)
monkeypatch.setattr(
scanner_module.BleakScanner,
"find_device_by_address",
find_exact,
)
monkeypatch.setattr(
scanner_module,
"ble_runtime_snapshot",
lambda: _active_status_read_runtime(),
)
assert (
asyncio.run(
discover_known_device_capture_for_status_read(
macos_uuid,
timeout_seconds=30.0,
)
)
is None
)
monkeypatch.setattr(
scanner_module,
"ble_runtime_snapshot",
lambda: runtime_snapshots.pop(0),
)
assert (
asyncio.run(
discover_known_device_capture_for_status_read(
macos_uuid,
timeout_seconds=30.0,
)
)
is None
)
assert responses == []
assert discovered_device(macos_uuid) is None
def test_pending_gatt_handoff_expires_across_macos_sleep(
monkeypatch: MonkeyPatch,
) -> None:
monotonic_clock = [100.0]
suspend_aware_clock = [1_000.0]
monkeypatch.setattr(scanner_module, "monotonic", lambda: monotonic_clock[0])
monkeypatch.setattr(
scanner_module,
"suspend_aware_time",
lambda: suspend_aware_clock[0],
)
device = BLEDevice("LIVE-UUID", "XGR-LIVE", details=object())
async def scenario() -> None:
owner_epoch = bind_ble_runtime_owner_loop()
captured = scanner_module.CapturedDiscoveredDevice(
device=device,
macos_uuid=device.address,
owner_epoch=owner_epoch,
scan_generation=7,
captured_at_monotonic=monotonic_clock[0],
captured_at_suspend_aware=suspend_aware_clock[0],
)
assert mark_captured_device_gatt_validated(captured) is True
suspend_aware_clock[0] += (
scanner_module.BLE_GATT_VALIDATION_HANDOFF_TTL_SECONDS + 0.001
)
pin_connected_device_handle(captured, device_session_id="device-session-a")
snapshot = connected_device_recovery_snapshot(
device.address,
device_session_id="device-session-a",
)
assert snapshot["status"] == "retained"
assert snapshot["gatt_validated_recently"] is False
asyncio.run(scenario())
def test_later_fresh_scan_does_not_replace_retained_corebluetooth_object(
monkeypatch: MonkeyPatch,
) -> None:
first = BLEDevice("LIVE-UUID", "XGR-LIVE", details=object())
second = BLEDevice("LIVE-UUID", "XGR-LIVE", details=object())
advertisement = AdvertisementData(
local_name="XGR-LIVE",
manufacturer_data={},
service_data={},
service_uuids=[],
tx_power=0,
rssi=-38,
platform_data=(),
)
discoveries = [
{first.address: (first, advertisement)},
{second.address: (second, advertisement)},
]
async def fake_discover(**_kwargs: object) -> dict[str, tuple[BLEDevice, AdvertisementData]]:
return discoveries.pop(0)
monkeypatch.setattr(scanner_module.BleakScanner, "discover", fake_discover)
async def scenario() -> None:
await scan(1.0)
captured = capture_discovered_device(first.address)
assert captured is not None
assert captured.device is first
pin_connected_device_handle(captured, device_session_id="device-session-a")
await scan(1.0)
retained = connected_device_capture(
first.address,
device_session_id="device-session-a",
)
assert retained is not None
assert retained is not captured
assert retained.device is first
assert retained.scan_generation == captured.scan_generation
next_capture = capture_discovered_device(first.address)
assert next_capture is not None
assert next_capture.device is second
assert next_capture.scan_generation > captured.scan_generation
assert connected_device_recovery_snapshot(
first.address,
device_session_id="device-session-a",
) == {
"status": "retained",
"scope": "owner-epoch-device-session",
"advertised_now": True,
"gatt_validated_recently": False,
}
asyncio.run(scenario())
def test_exact_gatt_validation_handoff_marks_newly_pinned_recovery_token() -> None:
device = BLEDevice("LIVE-UUID", "XGR-LIVE", details=object())
async def scenario() -> None:
owner_epoch = bind_ble_runtime_owner_loop()
captured = scanner_module.CapturedDiscoveredDevice(
device=device,
macos_uuid=device.address,
owner_epoch=owner_epoch,
scan_generation=7,
captured_at_monotonic=100.0,
)
# The reviewed baseline read completes immediately before the facade
# creates its new device-session. Validation follows the exact object
# across only that bounded synchronous handoff.
assert mark_captured_device_gatt_validated(captured) is True
pin_connected_device_handle(captured, device_session_id="device-session-a")
assert connected_device_recovery_snapshot(
device.address,
device_session_id="device-session-a",
)["gatt_validated_recently"] is True
asyncio.run(scenario())
def test_failed_gatt_demotes_only_the_exact_retained_corebluetooth_object() -> None:
first = BLEDevice("LIVE-UUID", "XGR-LIVE", details=object())
replacement = BLEDevice("LIVE-UUID", "XGR-LIVE", details=object())
async def scenario() -> None:
owner_epoch = bind_ble_runtime_owner_loop()
stale_capture = scanner_module.CapturedDiscoveredDevice(
device=first,
macos_uuid=first.address,
owner_epoch=owner_epoch,
scan_generation=7,
)
fresh_capture = scanner_module.CapturedDiscoveredDevice(
device=replacement,
macos_uuid=replacement.address,
owner_epoch=owner_epoch,
scan_generation=8,
)
pin_connected_device_handle(
stale_capture,
device_session_id="device-session-a",
)
assert demote_connected_device_handle_after_gatt_failure(stale_capture) is True
assert (
connected_device_capture(
first.address,
device_session_id="device-session-a",
)
is None
)
# A late failure from the old generation must not revoke a replacement
# object already admitted for the same CoreBluetooth UUID/session.
pin_connected_device_handle(
fresh_capture,
device_session_id="device-session-a",
)
assert demote_connected_device_handle_after_gatt_failure(stale_capture) is False
retained_replacement = connected_device_capture(
replacement.address,
device_session_id="device-session-a",
)
assert retained_replacement is not None
assert retained_replacement.device is replacement
assert retained_replacement.scan_generation == fresh_capture.scan_generation
assert retained_replacement.source == "retained-session"
asyncio.run(scenario())
def test_failed_gatt_invalidates_unpinned_selection_until_new_scan_generation() -> None:
first = BLEDevice("LIVE-UUID", "XGR-LIVE", details=object())
replacement = BLEDevice("LIVE-UUID", "XGR-LIVE", details=object())
async def scenario() -> None:
owner_epoch = bind_ble_runtime_owner_loop()
failed = scanner_module.CapturedDiscoveredDevice(
device=first,
macos_uuid=first.address,
owner_epoch=owner_epoch,
scan_generation=7,
)
assert demote_connected_device_handle_after_gatt_failure(failed) is False
assert scanner_module.captured_device_handle(failed) is None
next_scan = scanner_module.CapturedDiscoveredDevice(
device=replacement,
macos_uuid=replacement.address,
owner_epoch=owner_epoch,
scan_generation=8,
)
assert scanner_module.captured_device_handle(next_scan) is replacement
asyncio.run(scenario())
def test_invalidated_visible_row_requires_a_new_exact_scan_object() -> None:
first = BLEDevice("LIVE-UUID", "XGR-LIVE", details=object())
replacement = BLEDevice("LIVE-UUID", "XGR-LIVE", details=object())
async def scenario() -> None:
owner_epoch = bind_ble_runtime_owner_loop()
first_generation = scanner_module._begin_scan_generation(owner_epoch) # noqa: SLF001
scanner_module._publish_scan_handles( # noqa: SLF001
first_generation,
owner_epoch,
{first.address: first},
)
first_capture = capture_discovered_device(first.address)
assert first_capture is not None
assert first_capture.device is first
assert mark_captured_device_gatt_validated(first_capture) is True
pin_connected_device_handle(
first_capture,
device_session_id="verify-provisional-session",
)
# A losing read-only Verify retires its provisional device session but
# may leave the facade's row visible for product continuity. The native
# object behind that same generation must nevertheless stay revoked.
assert scanner_module.invalidate_connected_device_session(
first.address,
device_session_id="verify-provisional-session",
) is True
invalidated_selection = discovered_device_selection(first.address)
assert invalidated_selection.from_fresh_scan is True
assert invalidated_selection.device is None
assert capture_discovered_device(first.address) is None
assert scanner_module.captured_device_handle(first_capture) is None
second_generation = scanner_module._begin_scan_generation(owner_epoch) # noqa: SLF001
scanner_module._publish_scan_handles( # noqa: SLF001
second_generation,
owner_epoch,
{replacement.address: replacement},
)
replacement_capture = capture_discovered_device(replacement.address)
assert replacement_capture is not None
assert replacement_capture.device is replacement
assert replacement_capture.scan_generation == second_generation
assert scanner_module.captured_device_handle(replacement_capture) is replacement
# Neither the old object nor a capture manufactured for another owner
# epoch gains authority from the new scan.
assert scanner_module.captured_device_handle(first_capture) is None
foreign_capture = scanner_module.CapturedDiscoveredDevice(
device=replacement,
macos_uuid=replacement.address,
owner_epoch=owner_epoch + 1,
scan_generation=second_generation,
)
assert scanner_module.captured_device_handle(foreign_capture) is None
asyncio.run(scenario())
def test_exact_session_invalidation_does_not_clear_new_scan_handle() -> None:
retained = BLEDevice("LIVE-UUID", "XGR-LIVE", details=object())
newly_scanned = BLEDevice("LIVE-UUID", "XGR-LIVE", details=object())
async def scenario() -> None:
owner_epoch = bind_ble_runtime_owner_loop()
captured = scanner_module.CapturedDiscoveredDevice(
device=retained,
macos_uuid=retained.address,
owner_epoch=owner_epoch,
scan_generation=7,
)
assert mark_captured_device_gatt_validated(captured) is True
pin_connected_device_handle(captured, device_session_id="device-session-a")
next_generation = scanner_module._begin_scan_generation(owner_epoch) # noqa: SLF001
scanner_module._publish_scan_handles( # noqa: SLF001
next_generation,
owner_epoch,
{newly_scanned.address: newly_scanned},
)
replacement_capture = scanner_module.capture_discovered_device(newly_scanned.address)
assert replacement_capture is not None
assert replacement_capture.device is newly_scanned
assert scanner_module.invalidate_connected_device_session(
retained.address,
device_session_id="device-session-a",
) is True
assert connected_device_capture(
retained.address,
device_session_id="device-session-a",
) is None
assert scanner_module.captured_device_handle(captured) is None
with pytest.raises(RuntimeError, match="invalidated by session teardown"):
pin_connected_device_handle(captured, device_session_id="stale-repin")
with scanner_module._runtime_handle_lock: # noqa: SLF001
assert ( # noqa: SLF001
scanner_module._runtime_handles[newly_scanned.address] is newly_scanned
)
assert scanner_module.captured_device_handle(replacement_capture) is newly_scanned
assert mark_captured_device_gatt_validated(replacement_capture) is True
pin_connected_device_handle(
replacement_capture,
device_session_id="device-session-b",
)
assert connected_device_capture(
newly_scanned.address,
device_session_id="device-session-b",
) is not None
asyncio.run(scenario())