feat(k1): add offline control execution gate

This commit is contained in:
DCCONSTRUCTIONS
2026-07-18 14:14:30 +03:00
parent d7749208a7
commit ea811ff370
17 changed files with 916 additions and 44 deletions
@@ -6,6 +6,7 @@ from k1link.device_plugins.xgrids_k1.protocol.application_bootstrap import (
ApplicationBootstrapError,
ApplicationControlAuthority,
LiveDeviceControlBinding,
ShadowApplicationBootstrapOrchestrator,
build_shadow_bootstrap,
decode_and_bind_device_info_response,
)
@@ -75,6 +76,13 @@ def _device_info_response(*, session_id: str = ":DeviceInfoRequest") -> bytes:
return _bytes(1, header) + _bytes(2, device_info) + _bytes(15, error)
def _generic_response(session_id: str) -> bytes:
header = _text(4, VENDOR_DEVICE_ID) + _text(5, session_id) + _text(6, APPLICATION_KEY)
error = _uint(1, OPENAPI_SUCCESS) + _text(2, "success")
repeated_vendor_body = _text(3, "opaque-a") + _text(3, "opaque-b")
return _bytes(1, header) + repeated_vendor_body + _bytes(15, error)
def test_exact_clean_cycle_pre_start_order_and_wire_lengths() -> None:
plan = build_shadow_bootstrap(
_authority(),
@@ -165,3 +173,89 @@ def test_device_info_response_correlation_and_profile_attestation_fail_closed()
epoch_seconds=1_752_680_000,
timezone_name="Europe/Moscow",
)
def test_response_barrier_orchestrator_emits_each_retained_batch_once() -> None:
orchestrator = ShadowApplicationBootstrapOrchestrator(
_authority(),
epoch_seconds=1_752_680_000,
timezone_name="Europe/Moscow",
)
identity = orchestrator.next_batch()
assert [request.ordinal for request in identity] == [1]
with pytest.raises(ApplicationBootstrapError, match="already issued"):
orchestrator.next_batch()
orchestrator.accept_response(identity[0].response_topic, _device_info_response())
initial_reads = orchestrator.next_batch()
assert [request.ordinal for request in initial_reads] == [2, 3]
assert [request.response_required for request in initial_reads] == [False, True]
orchestrator.accept_response(
initial_reads[1].response_topic,
_generic_response(initial_reads[1].session_id),
)
preparation = orchestrator.next_batch()
assert [request.ordinal for request in preparation] == [4, 5, 6]
device_info = preparation[1]
orchestrator.accept_response(
device_info.response_topic,
_device_info_response(session_id=device_info.session_id),
)
with pytest.raises(ApplicationBootstrapError, match="already issued"):
orchestrator.next_batch()
for request in (preparation[2], preparation[0]):
orchestrator.accept_response(
request.response_topic,
_generic_response(request.session_id),
)
ntrip = orchestrator.next_batch()
assert [request.ordinal for request in ntrip] == [7]
orchestrator.accept_response(
ntrip[0].response_topic,
_generic_response(ntrip[0].session_id),
)
final_reads = orchestrator.next_batch()
assert [request.ordinal for request in final_reads] == [8, 9, 10]
for request in final_reads:
payload = (
_device_info_response(session_id=request.session_id)
if request.message_type == "DeviceInfoRequest"
else _generic_response(request.session_id)
)
orchestrator.accept_response(request.response_topic, payload)
snapshot = orchestrator.snapshot()
assert snapshot.bootstrap_complete
assert snapshot.live_binding_observed
assert snapshot.pending_response_topics == ()
assert snapshot.executable is False
assert snapshot.vendor_writes_enabled is False
with pytest.raises(ApplicationBootstrapError, match="already complete"):
orchestrator.next_batch()
def test_orchestrator_rejects_unexpected_response_without_advancing() -> None:
orchestrator = ShadowApplicationBootstrapOrchestrator(
_authority(),
epoch_seconds=1_752_680_000,
timezone_name="Europe/Moscow",
)
orchestrator.next_batch()
with pytest.raises(ApplicationBootstrapError, match="not required"):
orchestrator.accept_response(
"lixel/application/response/get_rtk_advance",
_generic_response(":GetRtkAdvanceRequest"),
)
assert orchestrator.snapshot().current_batch == 1
assert orchestrator.snapshot().batch_issued
def test_application_authority_requires_exact_captured_profile_length() -> None:
with pytest.raises(ApplicationBootstrapError, match="36-byte"):
ApplicationControlAuthority(openapi_key="too-short")