fix(k1): translate onboard operation identities at plugin boundary
This commit is contained in:
@@ -9,7 +9,7 @@ import pytest
|
||||
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
|
||||
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.viewer.node_rerun import NodeRerunHub
|
||||
|
||||
@@ -107,16 +107,99 @@ def test_node_scan_reaches_service_through_real_facade_with_runtime_fence():
|
||||
}
|
||||
output = await device.deliver(command)
|
||||
assert service.fences == ["runtime-one"]
|
||||
assert service.scans == [command["operation_id"]]
|
||||
assert service.scans == [plugin_operation_id(command["operation_id"])]
|
||||
assert len(output["candidates"]) == 1
|
||||
command["runtime_id"] = "retired-runtime"
|
||||
rejected = await device.deliver(command)
|
||||
assert rejected["command_result"]["status"] == "rejected"
|
||||
assert service.scans == [command["operation_id"]]
|
||||
assert service.scans == [plugin_operation_id(command["operation_id"])]
|
||||
|
||||
asyncio.run(run())
|
||||
|
||||
|
||||
@pytest.mark.parametrize("radio_failure", [False, True])
|
||||
def test_node_scan_real_service_preserves_operation_identity_and_replay(
|
||||
tmp_path, monkeypatch, radio_failure,
|
||||
):
|
||||
from bleak.backends.device import BLEDevice
|
||||
from bleak.backends.scanner import AdvertisementData
|
||||
|
||||
from k1link.device_plugins.xgrids_k1 import facade
|
||||
from k1link.device_plugins.xgrids_k1.ble import scanner
|
||||
|
||||
calls = []
|
||||
|
||||
class Radio:
|
||||
def __init__(self, detection_callback):
|
||||
self.callback = detection_callback
|
||||
|
||||
async def __aenter__(self):
|
||||
calls.append("scan")
|
||||
if radio_failure:
|
||||
raise RuntimeError("synthetic radio unavailable")
|
||||
self.callback(
|
||||
BLEDevice("AA:BB:CC:DD:EE:FF", "XGR-TEST", None),
|
||||
AdvertisementData(
|
||||
local_name="XGR-TEST", manufacturer_data={}, service_data={},
|
||||
service_uuids=[], tx_power=None, rssi=-50, platform_data=(),
|
||||
),
|
||||
)
|
||||
return self
|
||||
|
||||
async def __aexit__(self, *_args):
|
||||
pass
|
||||
|
||||
# Keep the actual Node adapter, facade, service, journal and BLE arbiter.
|
||||
# Replace only the OS radio endpoint and shorten its observation window.
|
||||
monkeypatch.setattr(scanner, "BleakScanner", Radio)
|
||||
monkeypatch.setattr(scanner, "BLE_SCAN_INITIAL_WINDOW_SECONDS", 0.01)
|
||||
service = facade.XgridsK1CompatibilityService(tmp_path)
|
||||
|
||||
async def run():
|
||||
device = NodeBridge(tmp_path, service=service)
|
||||
current = await device.state()
|
||||
command = {
|
||||
"operation_id": "op_" + "b" * 32,
|
||||
"runtime_id": current["runtime_id"],
|
||||
"deadline_at": (datetime.now(UTC) + timedelta(seconds=60)).isoformat(),
|
||||
"action": "scan", "parameters": {},
|
||||
}
|
||||
result = await device.deliver(command)
|
||||
repeated = await device.deliver(command)
|
||||
assert len(calls) == 1
|
||||
assert result["discovery_generation"] == 1
|
||||
if radio_failure:
|
||||
assert result["candidates"] == []
|
||||
else:
|
||||
assert result["candidates"][0]["id"] == "AA:BB:CC:DD:EE:FF"
|
||||
assert result["command_result"] == repeated["command_result"] == {
|
||||
"operation_id": command["operation_id"],
|
||||
"action": "scan",
|
||||
"status": "failed" if radio_failure else "succeeded",
|
||||
"error_code": "RuntimeError" if radio_failure else None,
|
||||
}
|
||||
|
||||
try:
|
||||
asyncio.run(run())
|
||||
finally:
|
||||
service.close()
|
||||
|
||||
|
||||
def test_node_attempt_projection_keeps_host_correlation_without_mutating_plugin_state():
|
||||
current = state()
|
||||
identifier = "op_" + "a" * 32
|
||||
current["connection_attempt"] = {
|
||||
"schema_version": "missioncore.xgrids-k1-connection-attempt/v1",
|
||||
"attempt_id": plugin_operation_id(identifier),
|
||||
"recovery_operation_id": plugin_operation_id("op_" + "b" * 32),
|
||||
"status": "failed", "public_error_code": "wifi-ssid-not-found",
|
||||
}
|
||||
result = NodeBridge.project(current)
|
||||
assert result["connection_attempt"]["attempt_id"] == identifier
|
||||
assert result["connection_attempt"]["recovery_operation_id"] == "op_" + "b" * 32
|
||||
assert current["connection_attempt"]["attempt_id"] == plugin_operation_id(identifier)
|
||||
|
||||
|
||||
def test_node_bridge_forces_reviewed_bridge_and_clears_input_secret():
|
||||
async def run():
|
||||
device = bridge()
|
||||
@@ -200,6 +283,17 @@ def test_one_start_intent_preserves_canonical_enter_prepare_start_sequence():
|
||||
"acquisition.prepare",
|
||||
"acquisition.start",
|
||||
]
|
||||
from k1link.web.device_lifecycle import OperationJournal
|
||||
|
||||
journal = OperationJournal()
|
||||
for action, payload in device.facade.actions:
|
||||
if action in {"acquisition.prepare", "acquisition.start"}:
|
||||
row, created = journal.begin(
|
||||
action, operation_id=payload["operation_id"],
|
||||
idempotency_key=payload["idempotency_key"],
|
||||
)
|
||||
assert created
|
||||
assert journal.begin(action, operation_id=row.operation_id)[1] is False
|
||||
assert result["snapshot"]["acquisition"] == "streaming"
|
||||
command["parameters"]["control_generation"] = 2
|
||||
with pytest.raises(ValueError):
|
||||
|
||||
Reference in New Issue
Block a user