Fix onboard K1 enrollment continuity and share named acquisition preparation
This commit is contained in:
@@ -28,8 +28,10 @@ def isolated_owner(tmp_path):
|
||||
|
||||
@pytest.mark.parametrize("operation", ["read", "provision"])
|
||||
@pytest.mark.parametrize("native", [
|
||||
"macos", "present", "vanished", "absent", "wrong-address", "wrong-adapter",
|
||||
"vanished-again", "owner-changed", "invalidated", "connect-failed", "write-failed",
|
||||
"macos", "present", "cache-cleanup-race", "vanished", "absent",
|
||||
"wrong-address", "wrong-adapter",
|
||||
"vanished-again", "owner-changed", "invalidated", "connect-failed",
|
||||
"connect-cancelled", "write-failed",
|
||||
])
|
||||
def test_selected_transport_survives_native_cache_loss(monkeypatch, operation, native):
|
||||
address = "AA:BB:CC:DD:EE:FF"
|
||||
@@ -42,7 +44,7 @@ def test_selected_transport_survives_native_cache_loss(monkeypatch, operation, n
|
||||
async def retrieve(selected_address, details):
|
||||
assert selected_address == address and details["path"] == path
|
||||
events.append("cache")
|
||||
present = native == "present" or (
|
||||
present = native in {"present", "cache-cleanup-race"} or (
|
||||
events.count("cache") == 2 and native != "vanished-again"
|
||||
)
|
||||
return device if present else None
|
||||
@@ -90,6 +92,12 @@ def test_selected_transport_survives_native_cache_loss(monkeypatch, operation, n
|
||||
|
||||
async def __aenter__(self):
|
||||
events.append("connect")
|
||||
if native == "cache-cleanup-race":
|
||||
await asyncio.sleep(0) # The D-Bus connection yields to BlueZ cleanup.
|
||||
if events.count("hold-discovery") <= events.count("release-discovery"):
|
||||
raise BleakError("selected device not found before watcher registration")
|
||||
if native == "connect-cancelled":
|
||||
raise asyncio.CancelledError()
|
||||
if native == "connect-failed":
|
||||
raise BleakError("synthetic native connection failure")
|
||||
return self
|
||||
@@ -99,6 +107,8 @@ def test_selected_transport_survives_native_cache_loss(monkeypatch, operation, n
|
||||
|
||||
async def read_gatt_char(self, characteristic):
|
||||
assert characteristic is status
|
||||
if native != "macos":
|
||||
assert "release-discovery" in events
|
||||
events.append("read")
|
||||
value = bytearray(52)
|
||||
value[0] = 11
|
||||
@@ -118,7 +128,20 @@ def test_selected_transport_survives_native_cache_loss(monkeypatch, operation, n
|
||||
platform="darwin" if native == "macos" else "linux",
|
||||
))
|
||||
monkeypatch.setattr(scanner, "_retrieve_bluez_device", retrieve)
|
||||
monkeypatch.setattr(scanner.BleakScanner, "find_device_by_address", find)
|
||||
class Scanner:
|
||||
find_device_by_address = staticmethod(find)
|
||||
|
||||
def __init__(self, *, bluez):
|
||||
assert bluez == {"adapter": "hci7"}
|
||||
|
||||
async def __aenter__(self):
|
||||
events.append("hold-discovery")
|
||||
return self
|
||||
|
||||
async def __aexit__(self, *_args):
|
||||
events.append("release-discovery")
|
||||
|
||||
monkeypatch.setattr(scanner, "BleakScanner", Scanner)
|
||||
monkeypatch.setattr(wifi, "BleakClient", Client)
|
||||
|
||||
async def scenario():
|
||||
@@ -142,7 +165,11 @@ def test_selected_transport_survives_native_cache_loss(monkeypatch, operation, n
|
||||
"absent", "wrong-address", "wrong-adapter", "vanished-again",
|
||||
"owner-changed", "invalidated",
|
||||
}
|
||||
if fails_before_connect or native == "connect-failed" or (
|
||||
if native == "connect-cancelled":
|
||||
with pytest.raises(asyncio.CancelledError):
|
||||
await action
|
||||
assert "read" not in events and "write" not in events
|
||||
elif fails_before_connect or native == "connect-failed" or (
|
||||
native == "write-failed" and operation == "provision"
|
||||
):
|
||||
with pytest.raises(BleakError) as raised:
|
||||
@@ -165,6 +192,8 @@ def test_selected_transport_survives_native_cache_loss(monkeypatch, operation, n
|
||||
assert scanner._runtime_handle_generation == generation # noqa: SLF001
|
||||
|
||||
asyncio.run(scenario())
|
||||
assert events.count("scan") == (native not in {"macos", "present"})
|
||||
assert events.count("scan") == (native not in {"macos", "present", "cache-cleanup-race"})
|
||||
assert events.count("hold-discovery") == events.count("release-discovery")
|
||||
assert events.count("hold-discovery") == (native != "macos")
|
||||
assert events.count("connect") <= 1
|
||||
assert events.count("write") <= 1
|
||||
|
||||
@@ -158,7 +158,7 @@ def test_private_release_contains_material_only_in_root_private_member(
|
||||
position += 60 + length + length % 2
|
||||
with tarfile.open(fileobj=io.BytesIO(members["control.tar.gz"]), mode="r:gz") as archive:
|
||||
control = archive.extractfile("control").read().decode()
|
||||
assert "Depends: mission-core-node (>= 0.8.10)" in control
|
||||
assert "Depends: mission-core-node (>= 0.8.11)" in control
|
||||
assert "Replaces: mission-core-node (<< 0.8.0)" in control
|
||||
|
||||
|
||||
|
||||
@@ -10,7 +10,11 @@ from missioncore_plugin_sdk.v0alpha2.session import DeviceSessionSnapshot
|
||||
|
||||
from k1link.device_plugins.xgrids_k1.linux_host import nm_fields, route_fields
|
||||
from k1link.device_plugins.xgrids_k1.node_bridge import NodeBridge, plugin_operation_id
|
||||
from k1link.device_plugins.xgrids_k1.node_sensor import NodeK1Sensor, project_sensor
|
||||
from k1link.device_plugins.xgrids_k1.node_sensor import (
|
||||
NodeK1Sensor,
|
||||
project_sensor,
|
||||
verification_parameters,
|
||||
)
|
||||
from k1link.viewer.node_rerun import NodeRerunHub
|
||||
|
||||
|
||||
@@ -53,6 +57,7 @@ class Facade:
|
||||
elif request.action_id == "acquisition.prepare":
|
||||
self.current["acquisition"] = {
|
||||
"acquisition_id": "acquisition-test",
|
||||
"project_name": request.parameters["project_name"],
|
||||
"state": "prepared",
|
||||
"state_revision": 1,
|
||||
}
|
||||
@@ -418,6 +423,8 @@ def test_one_start_intent_preserves_canonical_enter_prepare_start_sequence():
|
||||
"deadline_at": (datetime.now(UTC) + timedelta(seconds=60)).isoformat(),
|
||||
"parameters": {
|
||||
"operator_confirmed": True,
|
||||
"project_name": " Synthetic survey ",
|
||||
"mount_type": "handheld", "gnss_mode": "none",
|
||||
"control_generation": 1,
|
||||
"acquisition_id": None,
|
||||
},
|
||||
@@ -433,6 +440,9 @@ def test_one_start_intent_preserves_canonical_enter_prepare_start_sequence():
|
||||
|
||||
journal = OperationJournal()
|
||||
for action, payload in device.facade.actions:
|
||||
if action == "acquisition.prepare":
|
||||
assert payload["project_name"] == "Synthetic survey"
|
||||
assert payload["mount_type"] == "handheld" and payload["gnss_mode"] == "none"
|
||||
if action in {"acquisition.prepare", "acquisition.start"}:
|
||||
row, created = journal.begin(
|
||||
action, operation_id=payload["operation_id"],
|
||||
@@ -594,3 +604,47 @@ def test_preview_offer_cannot_cross_acquisition_boundary(replacement):
|
||||
assert all(action == "state.read" for action, _ in device.facade.actions)
|
||||
|
||||
asyncio.run(run())
|
||||
|
||||
|
||||
@pytest.mark.parametrize("draft", [
|
||||
{}, {"project_name": " "}, {"project_name": "x" * 97},
|
||||
{"project_name": "invalid\nname"}, {"project_name": "Valid", "mount_type": "uav"},
|
||||
{"project_name": "Valid", "mount_type": "handheld", "gnss_mode": "rtk"},
|
||||
])
|
||||
def test_invalid_start_draft_dispatches_no_workspace_or_project_command(draft):
|
||||
async def run():
|
||||
device = bridge()
|
||||
item = project_sensor(state(), "node-test")
|
||||
command = {
|
||||
"operation_id": "op_" + "b" * 32, "action_id": "start",
|
||||
"session": {"device_id": item["id"], "session_id": "session-test"},
|
||||
"deadline_at": (datetime.now(UTC) + timedelta(seconds=60)).isoformat(),
|
||||
"parameters": {"operator_confirmed": True, "control_generation": 1,
|
||||
"acquisition_id": None, **draft},
|
||||
}
|
||||
with pytest.raises(ValueError):
|
||||
await NodeK1Sensor(device, None).execute(command, "node-test")
|
||||
assert all(action == "state.read" for action, _ in device.facade.actions)
|
||||
asyncio.run(run())
|
||||
|
||||
|
||||
def test_recheck_uses_only_exact_admitted_durable_bridge_target():
|
||||
current = state()
|
||||
decision = {
|
||||
"allowed": True, "requires_live_gatt_validation": False,
|
||||
"required_transport_ref": current["selected_device_id"],
|
||||
"required_connection_mode": "bridge",
|
||||
}
|
||||
current["connection_policy"] = {"actions": {"observe-configured-device-network": decision}}
|
||||
result = verification_parameters(current, "synthetic-operation")
|
||||
assert result["source"] == "durable-configured-state"
|
||||
assert result["device_id"] == current["selected_device_id"]
|
||||
assert result["expected_mode_revision"] == current["desired_connection_mode_revision"]
|
||||
for patch in [{"allowed": False}, {"requires_live_gatt_validation": True},
|
||||
{"required_connection_mode": "quick-connect"},
|
||||
{"required_transport_ref": "other"}]:
|
||||
decision.update(patch)
|
||||
assert "source" not in verification_parameters(current, "synthetic-operation")
|
||||
decision.update(allowed=True, requires_live_gatt_validation=False,
|
||||
required_transport_ref=current["selected_device_id"],
|
||||
required_connection_mode="bridge")
|
||||
|
||||
Reference in New Issue
Block a user