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:
DCCONSTRUCTIONS
2026-08-14 14:57:50 +03:00
parent aff331082f
commit 0ca7316a24
157 changed files with 152962 additions and 4036 deletions
+730 -22
View File
@@ -20,19 +20,49 @@ from pydantic import ValidationError
import k1link.web.device_plugin_composition as plugin_composition
from k1link.device_plugins.xgrids_k1.facade import (
ACTION_ACQUISITION_ABORT,
ACTION_ACQUISITION_PREPARE,
ACTION_ACQUISITION_START,
ACTION_ACQUISITION_STOP,
ACTION_APPLICATION_CONTROL_SESSION_CLOSE,
ACTION_APPLICATION_CONTROL_SESSION_OPEN,
ACTION_APPLICATION_CONTROL_WORKSPACE_ENTER,
ACTION_CONFIGURED_ENDPOINT_PROBE,
ACTION_CONNECTION_MODE_SELECT,
ACTION_CONNECTION_RECONFIGURE_PREPARE,
ACTION_CONNECTION_VERIFY,
ACTION_DEVICE_CALIBRATION_SNAPSHOT_READ,
ACTION_DISCOVERY_SCAN,
ACTION_NETWORK_PROVISION,
ACTION_PHYSICAL_COMMAND_RECONCILE,
ACTION_PHYSICAL_COMMAND_REOPEN_RETIRED_RECONCILIATION,
ACTION_PHYSICAL_COMMAND_RETIRE_UNAVAILABLE,
ACTION_STREAM_START_LIVE,
ACTION_STREAM_START_REPLAY,
ACTION_STREAM_STOP,
ACTION_VIEWER_SETTINGS_UPDATE,
XGRIDS_K1_PLUGIN_ID,
XGRIDS_K1_PLUGIN_VERSION,
AbortAcquisitionRequest,
BleScanRequest,
CloseApplicationControlSessionRequest,
CompatibilityAttestationRequest,
ConfiguredEndpointProbeRequest,
ConnectionVerificationError,
ConnectionVerifyRequest,
ConnectRequest,
DesiredConnectionModeRequest,
EnterApplicationWorkspaceRequest,
NetworkProvisioningConflict,
OpenApplicationControlSessionRequest,
PrepareAcquisitionRequest,
PrepareConnectionReconfigurationRequest,
ReconcilePhysicalCommandRequest,
ReopenRetiredPhysicalCommandReconciliationRequest,
RetireUnavailablePhysicalCommandRequest,
SnapshotRuntimeConflict,
StartAcquisitionRequest,
StopAcquisitionRequest,
ViewerSettingsRequest,
XgridsK1PluginFacade,
)
@@ -57,6 +87,21 @@ from k1link.web.plugin_runtime import (
class FakeXgridsService:
def __init__(self) -> None:
self.calls: list[tuple[str, object]] = []
self.scan_loop: asyncio.AbstractEventLoop | None = None
self.verify_loop: asyncio.AbstractEventLoop | None = None
self.snapshot_runtime_id = "snapshot-runtime-test"
self.bind_calls = 0
def require_snapshot_runtime_id(self, expected_snapshot_runtime_id: str) -> None:
if expected_snapshot_runtime_id != self.snapshot_runtime_id:
raise SnapshotRuntimeConflict()
def bind_runtime_event_loop(
self,
loop: asyncio.AbstractEventLoop | None = None,
) -> None:
del loop
self.bind_calls += 1
def state(self) -> dict[str, Any]:
self.calls.append(("state", None))
@@ -66,21 +111,60 @@ class FakeXgridsService:
self.calls.append(("calibration", None))
return {"status": "available", "snapshot_id": "fixture-snapshot"}
async def scan_ble(self, duration_seconds: float) -> dict[str, Any]:
self.calls.append(("scan", duration_seconds))
async def scan_ble(self, request: BleScanRequest) -> dict[str, Any]:
self.scan_loop = asyncio.get_running_loop()
self.calls.append(("scan", request))
return {"phase": "idle", "devices": []}
async def connect(self, request: ConnectRequest) -> dict[str, Any]:
self.calls.append(("connect", request))
return {"phase": "connected", "k1_ip": "192.168.1.20"}
def verify_connection(
def select_connection_mode(
self,
request: DesiredConnectionModeRequest,
) -> dict[str, Any]:
self.calls.append(("mode-select", request))
return {
"phase": "idle",
"desired_connection_mode": request.connection_mode,
"desired_connection_mode_revision": request.expected_revision + 1,
}
async def prepare_connection_reconfiguration(
self,
request: PrepareConnectionReconfigurationRequest,
) -> dict[str, Any]:
self.calls.append(("reconfigure", request))
return {
"phase": "idle",
"connection_reconfiguration": {
"revision": request.expected_reconfiguration_revision + 1,
"intent": request.intent,
},
}
async def verify_connection(
self,
request: ConnectionVerifyRequest | None = None,
) -> dict[str, Any]:
self.verify_loop = asyncio.get_running_loop()
self.calls.append(("verify", request))
return {"phase": "connected", "k1_ip": "192.168.1.20"}
async def probe_configured_endpoint(
self,
request: ConfiguredEndpointProbeRequest | None = None,
) -> dict[str, Any]:
self.calls.append(("endpoint-probe", request))
return {
"phase": "idle",
"configured_endpoint_probe": {
"status": "reachable",
"ble_operation_performed": False,
},
}
def start_live(
self,
project_name: str,
@@ -101,10 +185,87 @@ class FakeXgridsService:
self.calls.append(("stop", None))
return {"phase": "idle"}
def prepare_acquisition(self, request: PrepareAcquisitionRequest) -> dict[str, Any]:
self.calls.append(("prepare", request))
return {"phase": "connected", "acquisition": {"state": "prepared"}}
def start_acquisition(self, request: StartAcquisitionRequest) -> dict[str, Any]:
self.calls.append(("start", request))
return {"phase": "starting_live"}
def stop_acquisition(self, request: StopAcquisitionRequest) -> dict[str, Any]:
self.calls.append(("acquisition-stop", request))
return {"phase": "stopping"}
def abort_acquisition(self, request: AbortAcquisitionRequest) -> dict[str, Any]:
self.calls.append(("abort", request))
return {"phase": "idle"}
def open_application_control_session(
self,
request: OpenApplicationControlSessionRequest,
) -> dict[str, Any]:
self.calls.append(("control-open", request))
return {"phase": "connected"}
def enter_application_workspace(
self,
request: EnterApplicationWorkspaceRequest,
) -> dict[str, Any]:
self.calls.append(("control-enter", request))
return {"phase": "connected"}
def close_application_control_session(
self,
request: CloseApplicationControlSessionRequest,
) -> dict[str, Any]:
self.calls.append(("control-close", request))
return {"phase": "connected"}
def reconcile_physical_command(
self,
request: ReconcilePhysicalCommandRequest,
) -> dict[str, Any]:
self.calls.append(("physical-reconcile", request))
return {"phase": "connected"}
def update_viewer_settings(self, request: ViewerSettingsRequest) -> dict[str, Any]:
self.calls.append(("viewer", request))
return {"phase": "idle", "viewer_settings": request.model_dump()}
def retire_unavailable_physical_command(
self,
request: RetireUnavailablePhysicalCommandRequest,
) -> dict[str, Any]:
self.calls.append(("physical-retire", request))
return {
"phase": "idle",
"physical_command": {
"status": "resolved",
"physical_outcome": "unknown",
},
}
def reopen_retired_physical_command_reconciliation(
self,
request: ReopenRetiredPhysicalCommandReconciliationRequest,
) -> dict[str, Any]:
self.calls.append(("physical-reopen", request))
return {
"phase": "idle",
"physical_command": {
"status": "unresolved",
"requires_reconciliation": True,
},
}
def _snapshot_fenced(payload: dict[str, Any] | None = None) -> dict[str, Any]:
return {
**(payload or {}),
"expected_snapshot_runtime_id": "snapshot-runtime-test",
}
def _in_process_runtime(
adapter: Any,
@@ -151,9 +312,7 @@ def test_manifest_and_runtime_facade_declare_identical_actions() -> None:
def test_calibration_snapshot_action_calls_the_read_only_service_method() -> None:
service = FakeXgridsService()
dispatcher = DevicePluginDispatcher(
[_in_process_runtime(XgridsK1PluginFacade(service))]
)
dispatcher = DevicePluginDispatcher([_in_process_runtime(XgridsK1PluginFacade(service))])
result = asyncio.run(
dispatcher.invoke(
@@ -169,22 +328,21 @@ def test_calibration_snapshot_action_calls_the_read_only_service_method() -> Non
def test_connection_verify_action_accepts_read_only_adoption_request() -> None:
service = FakeXgridsService()
dispatcher = DevicePluginDispatcher(
[_in_process_runtime(XgridsK1PluginFacade(service))]
)
dispatcher = DevicePluginDispatcher([_in_process_runtime(XgridsK1PluginFacade(service))])
result = asyncio.run(
dispatcher.invoke(
XGRIDS_K1_PLUGIN_ID,
ACTION_CONNECTION_VERIFY,
{
_snapshot_fenced({
"device_id": "test-ble-transport",
"compatibility_attestation": {
"firmware_version": "3.0.2",
"topology": "direct-lan",
"verification": "live-device-info",
},
},
"expected_discovery_generation": 0,
}),
)
)
@@ -200,15 +358,13 @@ def test_connection_verify_action_accepts_read_only_adoption_request() -> None:
def test_connection_verify_action_keeps_empty_refresh_request_compatible() -> None:
service = FakeXgridsService()
dispatcher = DevicePluginDispatcher(
[_in_process_runtime(XgridsK1PluginFacade(service))]
)
dispatcher = DevicePluginDispatcher([_in_process_runtime(XgridsK1PluginFacade(service))])
asyncio.run(
dispatcher.invoke(
XGRIDS_K1_PLUGIN_ID,
ACTION_CONNECTION_VERIFY,
{},
_snapshot_fenced(),
)
)
@@ -219,6 +375,548 @@ def test_connection_verify_action_keeps_empty_refresh_request_compatible() -> No
assert request.compatibility_attestation is None
def test_connection_mode_select_delegates_exact_cas_payload() -> None:
service = FakeXgridsService()
dispatcher = DevicePluginDispatcher([_in_process_runtime(XgridsK1PluginFacade(service))])
result = asyncio.run(
dispatcher.invoke(
XGRIDS_K1_PLUGIN_ID,
ACTION_CONNECTION_MODE_SELECT,
_snapshot_fenced({
"connection_mode": "quick-connect",
"expected_revision": 7,
}),
)
)
assert result["desired_connection_mode"] == "quick-connect"
assert result["desired_connection_mode_revision"] == 8
assert len(service.calls) == 1
action, request = service.calls[0]
assert action == "mode-select"
assert isinstance(request, DesiredConnectionModeRequest)
assert request.connection_mode == "quick-connect"
assert request.expected_revision == 7
def test_connection_scenario_reset_dispatches_before_runtime_loop_binding() -> None:
service = FakeXgridsService()
dispatcher = DevicePluginDispatcher([_in_process_runtime(XgridsK1PluginFacade(service))])
result = asyncio.run(
dispatcher.invoke(
XGRIDS_K1_PLUGIN_ID,
ACTION_CONNECTION_MODE_SELECT,
_snapshot_fenced(
{
"connection_mode": "bridge",
"expected_revision": 3,
"reset_scenario": True,
"reset_id": "op-reset-dispatch-local-only-01",
}
),
)
)
assert result["desired_connection_mode_revision"] == 4
assert service.bind_calls == 0
assert len(service.calls) == 1
action, request = service.calls[0]
assert action == "mode-select"
assert isinstance(request, DesiredConnectionModeRequest)
assert request.reset_scenario is True
assert request.reset_id == "op-reset-dispatch-local-only-01"
@pytest.mark.parametrize(
("intent", "intent_id"),
[("select-device", None), ("change-network", None), ("cancel", "intent-9")],
)
def test_connection_reconfigure_action_delegates_exact_stable_cas_payload(
intent: str,
intent_id: str | None,
) -> None:
service = FakeXgridsService()
dispatcher = DevicePluginDispatcher([_in_process_runtime(XgridsK1PluginFacade(service))])
payload = {
"intent": intent,
"expected_reconfiguration_revision": 9,
"expected_reconfiguration_intent_id": intent_id,
"expected_desired_mode_revision": 4,
"expected_active_binding_key": "a" * 64 if intent_id is None else None,
}
result = asyncio.run(
dispatcher.invoke(
XGRIDS_K1_PLUGIN_ID,
ACTION_CONNECTION_RECONFIGURE_PREPARE,
_snapshot_fenced(payload),
)
)
assert result["connection_reconfiguration"] == {
"revision": 10,
"intent": intent,
}
assert len(service.calls) == 1
action, request = service.calls[0]
assert action == "reconfigure"
assert isinstance(request, PrepareConnectionReconfigurationRequest)
assert request.model_dump(mode="json") == payload
def test_configured_endpoint_probe_action_is_separate_from_ble_verify() -> None:
service = FakeXgridsService()
dispatcher = DevicePluginDispatcher([_in_process_runtime(XgridsK1PluginFacade(service))])
result = asyncio.run(
dispatcher.invoke(
XGRIDS_K1_PLUGIN_ID,
ACTION_CONFIGURED_ENDPOINT_PROBE,
_snapshot_fenced(
{"operation_id": "op-00000000-0000-4000-8000-000000000652"}
),
)
)
assert result["configured_endpoint_probe"] == {
"status": "reachable",
"ble_operation_performed": False,
}
assert len(service.calls) == 1
action, request = service.calls[0]
assert action == "endpoint-probe"
assert isinstance(request, ConfiguredEndpointProbeRequest)
assert request.operation_id == "op-00000000-0000-4000-8000-000000000652"
def test_physical_retirement_action_delegates_exact_confirmed_cas_payload() -> None:
service = FakeXgridsService()
dispatcher = DevicePluginDispatcher([_in_process_runtime(XgridsK1PluginFacade(service))])
payload = {
"retirement_id": "retirement-browser-stable-id",
"expected_operation_id": "physical-stop-persisted",
"expected_revision": 17,
"expected_transport_ref": "F89438FA-55ED-85AD-EED7-734AC84746D8",
"operator_confirmed": True,
"reason": "device-permanently-unavailable-or-replaced",
}
result = asyncio.run(
dispatcher.invoke(
XGRIDS_K1_PLUGIN_ID,
ACTION_PHYSICAL_COMMAND_RETIRE_UNAVAILABLE,
_snapshot_fenced(payload),
)
)
assert result["physical_command"] == {
"status": "resolved",
"physical_outcome": "unknown",
}
assert service.bind_calls == 0
assert len(service.calls) == 1
action, request = service.calls[0]
assert action == "physical-retire"
assert isinstance(request, RetireUnavailablePhysicalCommandRequest)
assert request.model_dump(mode="json") == payload
def test_physical_reopen_action_is_runtime_fenced_exact_and_never_binds_loop() -> None:
service = FakeXgridsService()
adapter = XgridsK1PluginFacade(service)
dispatcher = DevicePluginDispatcher([_in_process_runtime(adapter)])
payload = {
"reopening_id": "reopening-browser-stable-id",
"expected_revision": 18,
"expected_retirement_id": "retirement-browser-stable-id",
"expected_transport_ref": "f89438fa-55ed-85ad-eed7-734ac84746d8",
"expected_discovery_generation": 7,
"expected_desired_mode": "bridge",
"expected_desired_mode_revision": 4,
"operator_confirmed": True,
"reason": "device-returned-for-explicit-reconciliation",
}
result = asyncio.run(
dispatcher.invoke(
XGRIDS_K1_PLUGIN_ID,
ACTION_PHYSICAL_COMMAND_REOPEN_RETIRED_RECONCILIATION,
_snapshot_fenced(payload),
)
)
assert result["physical_command"] == {
"status": "unresolved",
"requires_reconciliation": True,
}
assert service.bind_calls == 0
assert len(service.calls) == 1
action, request = service.calls[0]
assert action == "physical-reopen"
assert isinstance(request, ReopenRetiredPhysicalCommandReconciliationRequest)
assert request.model_dump(mode="json") == payload
assert ACTION_PHYSICAL_COMMAND_REOPEN_RETIRED_RECONCILIATION in adapter.action_ids
@pytest.mark.parametrize(
"reason_code",
[
"physical-command-reconciliation-reopen-stale-checkpoint",
"network-provisioning-idempotency-operation-mismatch",
"reconciliation-target-mode-mismatch",
"reconciliation-target-physical-recovery-mismatch",
"fresh-ble-candidate-required",
"physical-command-recovery-target-not-observed",
"network-provision-operation-active",
"control-local-retirement-pending",
"device-calibration-read-active",
"acquisition-active",
"acquisition-cleanup-pending",
"acquisition-start-operation-active",
"acquisition-stop-operation-active",
"local-runtime-active",
"control-session-not-admissible-for-network-change",
"k1-lifecycle-process-lease-control-owned",
"k1-lifecycle-process-lease-active",
],
)
def test_physical_reopen_expected_conflicts_are_http_409(reason_code: str) -> None:
class ReopenConflictService(FakeXgridsService):
def reopen_retired_physical_command_reconciliation(
self,
request: ReopenRetiredPhysicalCommandReconciliationRequest,
) -> dict[str, Any]:
del request
raise NetworkProvisioningConflict(
"reopen checkpoint is no longer executable",
reason_code=reason_code,
)
dispatcher = DevicePluginDispatcher(
[_in_process_runtime(XgridsK1PluginFacade(ReopenConflictService()))]
)
payload = {
"reopening_id": "reopening-http-conflict",
"expected_revision": 18,
"expected_retirement_id": "retirement-http-conflict",
"expected_transport_ref": "f89438fa-55ed-85ad-eed7-734ac84746d8",
"expected_discovery_generation": 7,
"expected_desired_mode": "bridge",
"expected_desired_mode_revision": 4,
"operator_confirmed": True,
"reason": "device-returned-for-explicit-reconciliation",
}
with pytest.raises(PluginExecutionError) as raised:
asyncio.run(
dispatcher.invoke(
XGRIDS_K1_PLUGIN_ID,
ACTION_PHYSICAL_COMMAND_REOPEN_RETIRED_RECONCILIATION,
_snapshot_fenced(payload),
)
)
assert raised.value.http_status_code == 409
assert raised.value.reason_code == reason_code
@pytest.mark.parametrize(
"action_id",
[
ACTION_DISCOVERY_SCAN,
ACTION_NETWORK_PROVISION,
ACTION_CONNECTION_MODE_SELECT,
ACTION_CONNECTION_RECONFIGURE_PREPARE,
ACTION_CONNECTION_VERIFY,
ACTION_CONFIGURED_ENDPOINT_PROBE,
ACTION_ACQUISITION_PREPARE,
ACTION_ACQUISITION_START,
ACTION_ACQUISITION_STOP,
ACTION_ACQUISITION_ABORT,
ACTION_APPLICATION_CONTROL_SESSION_OPEN,
ACTION_APPLICATION_CONTROL_WORKSPACE_ENTER,
ACTION_APPLICATION_CONTROL_SESSION_CLOSE,
ACTION_PHYSICAL_COMMAND_RECONCILE,
ACTION_STREAM_STOP,
ACTION_PHYSICAL_COMMAND_RETIRE_UNAVAILABLE,
ACTION_PHYSICAL_COMMAND_REOPEN_RETIRED_RECONCILIATION,
],
)
@pytest.mark.parametrize(
"runtime_fence",
[None, "snapshot-runtime-stale-browser"],
)
def test_snapshot_fenced_actions_reject_stale_browser_before_service_or_io(
action_id: str,
runtime_fence: str | None,
) -> None:
service = FakeXgridsService()
dispatcher = DevicePluginDispatcher([_in_process_runtime(XgridsK1PluginFacade(service))])
payload = (
{}
if runtime_fence is None
else {"expected_snapshot_runtime_id": runtime_fence}
)
with pytest.raises(PluginExecutionError) as raised:
asyncio.run(dispatcher.invoke(XGRIDS_K1_PLUGIN_ID, action_id, payload))
assert raised.value.http_status_code == 409
assert raised.value.reason_code == "snapshot-runtime-conflict"
assert service.bind_calls == 0
assert service.calls == []
def test_lifecycle_ui_payload_contract_passes_backend_validation_exactly() -> None:
service = FakeXgridsService()
dispatcher = DevicePluginDispatcher([_in_process_runtime(XgridsK1PluginFacade(service))])
compatibility = {
"firmware_version": "3.0.2",
"topology": "direct-lan",
"verification": "live-device-info",
}
payloads = (
(
ACTION_ACQUISITION_PREPARE,
{
"operation_id": "op-00000000-0000-4000-8000-000000000201",
"idempotency_key": (
"acquisition.prepare:op-00000000-0000-4000-8000-000000000201"
),
"project_name": "CONTRACT01",
"mount_type": "handheld",
"gnss_mode": "none",
"compatibility_attestation": compatibility,
"expected_control_session_generation": 7,
"expected_control_state_revision": 11,
},
),
(
ACTION_ACQUISITION_START,
{
"operation_id": "op-00000000-0000-4000-8000-000000000202",
"idempotency_key": (
"acquisition.start:op-00000000-0000-4000-8000-000000000202"
),
"acquisition_id": "acquisition-contract",
"expected_control_session_generation": 7,
"expected_control_state_revision": 12,
},
),
(
ACTION_ACQUISITION_STOP,
{
"operation_id": "op-00000000-0000-4000-8000-000000000203",
"idempotency_key": (
"acquisition.stop:op-00000000-0000-4000-8000-000000000203"
),
"acquisition_id": "acquisition-contract",
"mode": "graceful",
"expected_control_session_generation": 7,
"expected_control_state_revision": 13,
},
),
(
ACTION_ACQUISITION_ABORT,
{
"operation_id": "op-00000000-0000-4000-8000-000000000204",
"idempotency_key": (
"acquisition.abort:op-00000000-0000-4000-8000-000000000204"
),
"acquisition_id": "acquisition-contract",
"expected_control_session_generation": 7,
"expected_control_state_revision": 14,
},
),
(
ACTION_APPLICATION_CONTROL_SESSION_OPEN,
{
"operator_present": True,
"owner_controlled_device": True,
"lixelgo_closed": True,
"battery_storage_confirmed": True,
"expected_physical_state_confirmed": True,
"timezone_name": "Europe/Moscow",
},
),
(
ACTION_APPLICATION_CONTROL_WORKSPACE_ENTER,
{
"operator_confirmed": True,
"expected_session_generation": 7,
"expected_state_revision": 15,
},
),
(
ACTION_APPLICATION_CONTROL_SESSION_CLOSE,
{
"expected_session_generation": 7,
"expected_state_revision": 16,
},
),
(
ACTION_PHYSICAL_COMMAND_RECONCILE,
{
"reconciliation_id": "reconciliation-contract",
"expected_session_generation": 7,
"expected_state_revision": 17,
},
),
)
for action_id, payload in payloads:
asyncio.run(
dispatcher.invoke(
XGRIDS_K1_PLUGIN_ID,
action_id,
_snapshot_fenced(payload),
)
)
assert [call[0] for call in service.calls] == [
"prepare",
"start",
"acquisition-stop",
"abort",
"control-open",
"control-enter",
"control-close",
"physical-reconcile",
]
for _, request in service.calls[:4]:
assert request.operation_id is not None
assert request.idempotency_key.endswith(request.operation_id)
def test_connection_verify_expected_state_is_not_reported_as_bad_gateway() -> None:
class AddressUnavailableService(FakeXgridsService):
async def verify_connection(
self,
request: ConnectionVerifyRequest | None = None,
) -> dict[str, Any]:
del request
raise ConnectionVerificationError(
"K1 не сообщил адрес общей локальной сети",
reason_code="connection-verify-address-unavailable",
)
dispatcher = DevicePluginDispatcher(
[_in_process_runtime(XgridsK1PluginFacade(AddressUnavailableService()))]
)
with pytest.raises(PluginExecutionError) as raised:
asyncio.run(
dispatcher.invoke(
XGRIDS_K1_PLUGIN_ID,
ACTION_CONNECTION_VERIFY,
_snapshot_fenced(),
)
)
assert raised.value.http_status_code == 409
assert raised.value.reason_code == "connection-verify-address-unavailable"
@pytest.mark.parametrize(
("reason_code", "expected_status"),
[
("ble-runtime-busy", 409),
("ble-runtime-cleanup-pending", 409),
("provisioning-already-running", 409),
("connection-verify-mqtt-unreachable", 409),
("connection-verify-lease-changed", 409),
("connection-verify-resolved-apply-target-mismatch", 409),
("configured-endpoint-topology-corrupt", 409),
("connection-mode-selection-lifecycle-busy", 409),
("connection-mode-selection-physical-state-unsafe", 409),
("connection-mode-selection-control-state-unsafe", 409),
("connection-mode-switch-acquisition-changed", 409),
("connection-scenario-reset-pending", 409),
("connection-scenario-reset-lifecycle-timeout", 409),
("acquisition-start-lifecycle-busy", 409),
("application-control-process-lease-unavailable", 409),
("ble-runtime-owner-loop-conflict", 503),
("ble-runtime-restart-required", 503),
("connection-verify-exact-uuid-scan-timeout", 504),
("ble-discovery-timeout", 504),
("ble-status-read-timeout", 504),
("ble-provisioning-timeout", 504),
("ble-ap-enable-timeout", 504),
("network-not-found", 504),
("host-wifi-operation-timeout", 504),
("physical-command-reconciliation-proof-timeout", 504),
("physical-command-reconciliation-control-adoption-timeout", 504),
("keychain-authorization-required", 409),
("profile-unavailable", 409),
("profile-credential-source-mismatch", 409),
("wifi-interface-unavailable", 503),
],
)
def test_ble_runtime_failure_preserves_actionable_http_class(
reason_code: str,
expected_status: int,
) -> None:
class ClassifiedRuntimeError(RuntimeError):
def __init__(self) -> None:
super().__init__("classified BLE failure")
self.reason_code = reason_code
class FailingScanService(FakeXgridsService):
async def scan_ble(self, request: BleScanRequest) -> dict[str, Any]:
del request
raise ClassifiedRuntimeError()
dispatcher = DevicePluginDispatcher(
[_in_process_runtime(XgridsK1PluginFacade(FailingScanService()))]
)
with pytest.raises(PluginExecutionError) as raised:
asyncio.run(
dispatcher.invoke(
XGRIDS_K1_PLUGIN_ID,
ACTION_DISCOVERY_SCAN,
_snapshot_fenced({"duration_seconds": 6}),
)
)
assert raised.value.http_status_code == expected_status
assert raised.value.reason_code == reason_code
def test_connection_verify_reuses_the_discovery_event_loop() -> None:
service = FakeXgridsService()
dispatcher = DevicePluginDispatcher([_in_process_runtime(XgridsK1PluginFacade(service))])
async def scenario() -> asyncio.AbstractEventLoop:
loop = asyncio.get_running_loop()
await dispatcher.invoke(
XGRIDS_K1_PLUGIN_ID,
ACTION_DISCOVERY_SCAN,
_snapshot_fenced({"duration_seconds": 6}),
)
await dispatcher.invoke(
XGRIDS_K1_PLUGIN_ID,
ACTION_CONNECTION_VERIFY,
_snapshot_fenced({
"device_id": "test-ble-transport",
"compatibility_attestation": {
"firmware_version": "3.0.2",
"topology": "direct-lan",
"verification": "live-device-info",
},
"expected_discovery_generation": 0,
}),
)
return loop
dispatcher_loop = asyncio.run(scenario())
assert service.scan_loop is dispatcher_loop
assert service.verify_loop is dispatcher_loop
def test_repository_runtime_composition_exactly_matches_catalog() -> None:
repository_root = Path(__file__).resolve().parents[1]
environment = load_installed_device_plugins(repository_root)
@@ -437,12 +1135,16 @@ def test_dispatcher_routes_allowlisted_action_to_xgrids_facade() -> None:
dispatcher.invoke(
XGRIDS_K1_PLUGIN_ID,
ACTION_DISCOVERY_SCAN,
{"duration_seconds": 6},
_snapshot_fenced({"duration_seconds": 6}),
)
)
assert state == {"phase": "idle", "devices": []}
assert service.calls == [("scan", 6.0)]
assert len(service.calls) == 1
action, request = service.calls[0]
assert action == "scan"
assert isinstance(request, BleScanRequest)
assert request.duration_seconds == 6.0
def test_runtime_classifies_non_json_plugin_output_as_execution_failure(
@@ -496,12 +1198,12 @@ def test_facade_validates_payload_before_calling_service() -> None:
dispatcher.invoke(
XGRIDS_K1_PLUGIN_ID,
ACTION_NETWORK_PROVISION,
{
_snapshot_fenced({
"device_id": "id",
"ssid": "network",
"password": "x" * 24,
"extra": True,
},
}),
)
)
@@ -529,7 +1231,7 @@ def test_facade_validates_payload_before_calling_service() -> None:
{"path": "sessions/capture.k1mqtt", "speed": 1, "loop": False},
"replay",
),
(ACTION_STREAM_STOP, {}, "stop"),
(ACTION_STREAM_STOP, _snapshot_fenced(), "stop"),
(
ACTION_VIEWER_SETTINGS_UPDATE,
{
@@ -571,7 +1273,13 @@ def test_sync_runtime_actions_run_outside_the_api_event_loop() -> None:
dispatcher = DevicePluginDispatcher([_in_process_runtime(XgridsK1PluginFacade(service))])
event_loop_thread = threading.get_ident()
asyncio.run(dispatcher.invoke(XGRIDS_K1_PLUGIN_ID, ACTION_STREAM_STOP, {}))
asyncio.run(
dispatcher.invoke(
XGRIDS_K1_PLUGIN_ID,
ACTION_STREAM_STOP,
_snapshot_fenced(),
)
)
assert service.thread_id is not None
assert service.thread_id != event_loop_thread
@@ -649,6 +1357,6 @@ def test_dispatcher_rejects_uncorrelated_transport_result() -> None:
DevicePluginDispatcher([runtime]).invoke(
XGRIDS_K1_PLUGIN_ID,
ACTION_STREAM_STOP,
{},
_snapshot_fenced(),
)
)