feat(k1): complete canonical control lifecycle
This commit is contained in:
@@ -1,5 +1,8 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from k1link.device_plugins.xgrids_k1.protocol.application_bootstrap import (
|
||||
@@ -16,6 +19,14 @@ from k1link.device_plugins.xgrids_k1.protocol.protobuf_wire import iter_fields
|
||||
|
||||
APPLICATION_KEY = "11111111-2222-3333-4444-555555555555"
|
||||
VENDOR_DEVICE_ID = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
|
||||
CAPTURE_FACTS = json.loads(
|
||||
(
|
||||
Path(__file__).parent
|
||||
/ "fixtures"
|
||||
/ "xgrids_k1"
|
||||
/ "device_info_fw3_0_2_semantics.json"
|
||||
).read_text(encoding="utf-8")
|
||||
)
|
||||
|
||||
|
||||
def _varint(value: int) -> bytes:
|
||||
@@ -47,11 +58,11 @@ def _binding(**changes: object) -> LiveDeviceControlBinding:
|
||||
values = {
|
||||
"vendor_device_id": VENDOR_DEVICE_ID,
|
||||
"device_serial": "K1SERIAL01",
|
||||
"software_version": "V3.0.2-20260101-release",
|
||||
"system_version": "V3.0.2",
|
||||
"device_model": "LixelKity K1",
|
||||
"device_type": "K1",
|
||||
"is_activated": True,
|
||||
"software_version": CAPTURE_FACTS["software_version"],
|
||||
"system_version": CAPTURE_FACTS["system_version"],
|
||||
"device_model": CAPTURE_FACTS["device_model"],
|
||||
"device_type": CAPTURE_FACTS["platform_type"],
|
||||
"is_activated": CAPTURE_FACTS["is_activated"],
|
||||
}
|
||||
values.update(changes)
|
||||
return LiveDeviceControlBinding(**values) # type: ignore[arg-type]
|
||||
@@ -62,13 +73,13 @@ def _device_info_response(*, session_id: str = ":DeviceInfoRequest") -> bytes:
|
||||
base_info = b"".join(
|
||||
(
|
||||
_text(1, "2026-01-01T00:00:00"),
|
||||
_text(2, "V3.0.2-20260101-release"),
|
||||
_text(3, "V3.0.2"),
|
||||
_text(2, CAPTURE_FACTS["software_version"]),
|
||||
_text(3, CAPTURE_FACTS["system_version"]),
|
||||
_text(4, "V1.2.3"),
|
||||
_text(5, "V1.0"),
|
||||
_text(6, "LixelKity K1"),
|
||||
_text(6, CAPTURE_FACTS["device_model"]),
|
||||
_text(7, "K1SERIAL01"),
|
||||
_text(8, "K1"),
|
||||
_text(8, CAPTURE_FACTS["platform_type"]),
|
||||
)
|
||||
)
|
||||
working_status = _uint(1, 1) + _uint(2, 0)
|
||||
@@ -149,6 +160,27 @@ def test_bootstrap_headers_switch_from_unbound_to_live_device_identity() -> None
|
||||
)
|
||||
|
||||
|
||||
def test_device_config_timestamp_uses_captured_sint64_zigzag_wire_semantics() -> None:
|
||||
captured_epoch_seconds = 1_784_215_030
|
||||
plan = build_shadow_bootstrap(
|
||||
_authority(),
|
||||
_binding(),
|
||||
epoch_seconds=captured_epoch_seconds,
|
||||
timezone_name="Europe/Moscow",
|
||||
)
|
||||
|
||||
config_request = plan.requests[3]
|
||||
top_fields = {field.number: field.value for field in iter_fields(config_request.payload)}
|
||||
time_config = top_fields[4]
|
||||
assert isinstance(time_config, bytes)
|
||||
time_fields = {field.number: field.value for field in iter_fields(time_config)}
|
||||
|
||||
assert time_fields == {
|
||||
1: 3_568_430_060,
|
||||
2: b"Europe/Moscow",
|
||||
}
|
||||
|
||||
|
||||
def test_canonical_post_start_observation_preserves_retained_operations_12_to_14() -> None:
|
||||
plan = build_canonical_post_start_observation(_authority(), _binding())
|
||||
|
||||
@@ -169,6 +201,7 @@ def test_device_info_response_produces_live_binding_without_a_saved_device_profi
|
||||
|
||||
assert response.result_code == OPENAPI_SUCCESS
|
||||
assert response.binding.ready_for_reviewed_profile
|
||||
assert response.binding.matches_reviewed_device
|
||||
assert response.binding.matches_reviewed_firmware
|
||||
assert response.binding.is_activated
|
||||
assert VENDOR_DEVICE_ID not in repr(response)
|
||||
@@ -181,7 +214,6 @@ def test_device_info_response_correlation_and_profile_attestation_fail_closed()
|
||||
_device_info_response(session_id=":AnotherRequest"),
|
||||
_authority(),
|
||||
)
|
||||
|
||||
with pytest.raises(ApplicationBootstrapError, match="does not attest"):
|
||||
build_shadow_bootstrap(
|
||||
_authority(),
|
||||
@@ -191,6 +223,37 @@ def test_device_info_response_correlation_and_profile_attestation_fail_closed()
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("device_model", "device_type"),
|
||||
(
|
||||
("lixelkity K1", "A4"),
|
||||
("LixelKity K1 ", "A4"),
|
||||
("LixelKity K11", "A4"),
|
||||
("LixelKity K1", "a4"),
|
||||
("LixelKity K1", " A4"),
|
||||
("LixelKity K1", "K1"),
|
||||
),
|
||||
)
|
||||
def test_reviewed_profile_rejects_non_exact_device_model_and_type(
|
||||
device_model: str,
|
||||
device_type: str,
|
||||
) -> None:
|
||||
binding = _binding(device_model=device_model, device_type=device_type)
|
||||
|
||||
assert binding.matches_reviewed_firmware
|
||||
assert not binding.matches_reviewed_device
|
||||
assert not binding.ready_for_reviewed_profile
|
||||
with pytest.raises(ApplicationBootstrapError, match="does not attest"):
|
||||
build_shadow_bootstrap(
|
||||
_authority(),
|
||||
binding,
|
||||
epoch_seconds=1_752_680_000,
|
||||
timezone_name="Europe/Moscow",
|
||||
)
|
||||
with pytest.raises(ApplicationBootstrapError, match="does not attest"):
|
||||
build_canonical_post_start_observation(_authority(), binding)
|
||||
|
||||
|
||||
def test_response_barrier_orchestrator_emits_each_retained_batch_once() -> None:
|
||||
orchestrator = ShadowApplicationBootstrapOrchestrator(
|
||||
_authority(),
|
||||
@@ -202,35 +265,37 @@ def test_response_barrier_orchestrator_emits_each_retained_batch_once() -> None:
|
||||
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),
|
||||
"bootstrap:1:DeviceInfoRequest",
|
||||
_device_info_response(),
|
||||
)
|
||||
|
||||
preparation = orchestrator.next_batch()
|
||||
assert [request.ordinal for request in preparation] == [4, 5, 6]
|
||||
device_info = preparation[1]
|
||||
initial_reads = orchestrator.next_batch()
|
||||
assert [request.ordinal for request in initial_reads] == [2, 3, 4, 5, 6]
|
||||
assert [request.response_required for request in initial_reads] == [
|
||||
False,
|
||||
True,
|
||||
True,
|
||||
True,
|
||||
True,
|
||||
]
|
||||
device_info = initial_reads[3]
|
||||
orchestrator.accept_response(
|
||||
device_info.response_topic,
|
||||
"bootstrap:5:DeviceInfoRequest",
|
||||
_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]):
|
||||
for request in (initial_reads[4], initial_reads[1], initial_reads[2]):
|
||||
orchestrator.accept_response(
|
||||
request.response_topic,
|
||||
f"bootstrap:{request.ordinal}:{request.message_type}",
|
||||
_generic_response(request.session_id),
|
||||
)
|
||||
|
||||
ntrip = orchestrator.next_batch()
|
||||
assert [request.ordinal for request in ntrip] == [7]
|
||||
orchestrator.accept_response(
|
||||
ntrip[0].response_topic,
|
||||
"bootstrap:7:GetNtripProfileRequest",
|
||||
_generic_response(ntrip[0].session_id),
|
||||
)
|
||||
|
||||
@@ -242,7 +307,10 @@ def test_response_barrier_orchestrator_emits_each_retained_batch_once() -> None:
|
||||
if request.message_type == "DeviceInfoRequest"
|
||||
else _generic_response(request.session_id)
|
||||
)
|
||||
orchestrator.accept_response(request.response_topic, payload)
|
||||
orchestrator.accept_response(
|
||||
f"bootstrap:{request.ordinal}:{request.message_type}",
|
||||
payload,
|
||||
)
|
||||
|
||||
snapshot = orchestrator.snapshot()
|
||||
assert snapshot.bootstrap_complete
|
||||
@@ -264,7 +332,7 @@ def test_orchestrator_rejects_unexpected_response_without_advancing() -> None:
|
||||
|
||||
with pytest.raises(ApplicationBootstrapError, match="not required"):
|
||||
orchestrator.accept_response(
|
||||
"lixel/application/response/get_rtk_advance",
|
||||
"bootstrap:3:GetRtkAdvanceRequest",
|
||||
_generic_response(":GetRtkAdvanceRequest"),
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user