fix(k1): translate onboard operation identities at plugin boundary

This commit is contained in:
DCCONSTRUCTIONS
2026-09-07 12:34:48 +03:00
parent 3dffe39c89
commit d2dbfc9c85
9 changed files with 264 additions and 34 deletions
+97 -3
View File
@@ -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):