feat(k1): wire dormant control lease
This commit is contained in:
@@ -88,7 +88,7 @@ def test_repository_catalog_exposes_xgrids_model() -> None:
|
||||
item for item in plugins if item["metadata"]["id"] == "nodedc.device.xgrids-lixelkity-k1"
|
||||
)
|
||||
assert plugin["apiVersion"] == "missioncore.nodedc/v1alpha2"
|
||||
assert plugin["metadata"]["version"] == "0.3.0"
|
||||
assert plugin["metadata"]["version"] == "0.4.0"
|
||||
assert plugin["spec"]["hostApiRange"] == "v1alpha2"
|
||||
assert plugin["spec"]["compatibilityProfiles"] == [
|
||||
{
|
||||
@@ -116,10 +116,13 @@ def test_repository_catalog_exposes_xgrids_model() -> None:
|
||||
"camera.preview.select",
|
||||
"camera.preview.stop",
|
||||
"viewer.settings.update",
|
||||
"application-control.shadow-state",
|
||||
"application-control.shadow-arm",
|
||||
"application-control.shadow-disarm",
|
||||
} <= action_ids
|
||||
assert next(item for item in models if item["id"] == "xgrids.lixelkity-k1") == {
|
||||
"pluginId": "nodedc.device.xgrids-lixelkity-k1",
|
||||
"pluginVersion": "0.3.0",
|
||||
"pluginVersion": "0.4.0",
|
||||
"id": "xgrids.lixelkity-k1",
|
||||
"vendor": "XGRIDS",
|
||||
"displayName": "XGRIDS LixelKity K1",
|
||||
|
||||
@@ -17,10 +17,14 @@ from k1link.device_plugins.xgrids_k1.facade import (
|
||||
CompatibilityAttestationRequest,
|
||||
ConnectRequest,
|
||||
PrepareAcquisitionRequest,
|
||||
ShadowApplicationControlArmRequest,
|
||||
StartAcquisitionRequest,
|
||||
StopAcquisitionRequest,
|
||||
XgridsK1CompatibilityService,
|
||||
)
|
||||
from k1link.device_plugins.xgrids_k1.protocol.application_bootstrap import (
|
||||
ApplicationControlAuthority,
|
||||
)
|
||||
|
||||
ATTESTATION = CompatibilityAttestationRequest(
|
||||
firmware_version="3.0.2",
|
||||
@@ -30,6 +34,16 @@ ATTESTATION = CompatibilityAttestationRequest(
|
||||
PRIMARY_TEST_CREDENTIAL = "x" * 24
|
||||
SECONDARY_TEST_CREDENTIAL = "y" * 24
|
||||
PROJECT_NAME = "K1 lifecycle test"
|
||||
PRIVATE_APPLICATION_AUTHORITY = "11111111-2222-3333-4444-555555555555"
|
||||
|
||||
|
||||
class FakeApplicationAuthorityLoader:
|
||||
def __init__(self) -> None:
|
||||
self.calls = 0
|
||||
|
||||
def load(self) -> ApplicationControlAuthority:
|
||||
self.calls += 1
|
||||
return ApplicationControlAuthority(openapi_key=PRIVATE_APPLICATION_AUTHORITY)
|
||||
|
||||
|
||||
class FakeVisualizationRuntime:
|
||||
@@ -148,6 +162,104 @@ def test_project_name_is_normalized_and_control_characters_are_rejected() -> Non
|
||||
)
|
||||
|
||||
|
||||
def test_facade_arms_bounded_shadow_lease_without_installing_publish_transport(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
loader = FakeApplicationAuthorityLoader()
|
||||
service = XgridsK1CompatibilityService(
|
||||
tmp_path,
|
||||
application_authority_loader=loader,
|
||||
)
|
||||
runtime = FakeVisualizationRuntime()
|
||||
service.runtime = runtime # type: ignore[assignment]
|
||||
service._selected_device_id = "test-ble-transport" # noqa: SLF001
|
||||
service._k1_ip = "192.168.1.20" # noqa: SLF001
|
||||
service._compatibility_attestation = { # noqa: SLF001
|
||||
"firmware_version": "3.0.2",
|
||||
"topology": "direct-lan",
|
||||
"basis": "operator-attested",
|
||||
"observed_at": "2026-07-18T00:00:00Z",
|
||||
}
|
||||
|
||||
state = service.arm_application_control_shadow(
|
||||
ShadowApplicationControlArmRequest(
|
||||
operator_confirmed=True,
|
||||
lease_seconds=60.0,
|
||||
timezone_name="Europe/Moscow",
|
||||
)
|
||||
)
|
||||
|
||||
execution = state["application_control_execution"]
|
||||
assert loader.calls == 1
|
||||
assert execution["state"] == "armed-shadow-only"
|
||||
assert execution["lease"]["authority_cached"] is True
|
||||
assert execution["can_emit_requests"] is False
|
||||
assert execution["live_transport_installed"] is False
|
||||
assert execution["publisher"]["vendor_writes_enabled"] is False
|
||||
assert execution["publisher"]["transport_calls"] == 0
|
||||
assert PRIVATE_APPLICATION_AUTHORITY not in str(state)
|
||||
|
||||
disarmed = service.disarm_application_control_shadow()
|
||||
assert disarmed["application_control_execution"]["state"] == "disarmed"
|
||||
assert disarmed["application_control_execution"]["lease"] is None
|
||||
|
||||
|
||||
def test_shadow_arm_requires_idle_connected_attested_device_before_keychain_read(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
loader = FakeApplicationAuthorityLoader()
|
||||
service = XgridsK1CompatibilityService(
|
||||
tmp_path,
|
||||
application_authority_loader=loader,
|
||||
)
|
||||
service.runtime = FakeVisualizationRuntime() # type: ignore[assignment]
|
||||
|
||||
with pytest.raises(RuntimeError, match="подключите K1"):
|
||||
service.arm_application_control_shadow(
|
||||
ShadowApplicationControlArmRequest(
|
||||
operator_confirmed=True,
|
||||
timezone_name="Europe/Moscow",
|
||||
)
|
||||
)
|
||||
|
||||
assert loader.calls == 0
|
||||
|
||||
|
||||
def test_shadow_arm_contract_requires_explicit_operator_confirmation() -> None:
|
||||
with pytest.raises(ValidationError):
|
||||
ShadowApplicationControlArmRequest.model_validate({"timezone_name": "Europe/Moscow"})
|
||||
|
||||
|
||||
def test_prepare_acquisition_revokes_existing_shadow_authority_lease(tmp_path: Path) -> None:
|
||||
loader = FakeApplicationAuthorityLoader()
|
||||
service = XgridsK1CompatibilityService(
|
||||
tmp_path,
|
||||
application_authority_loader=loader,
|
||||
)
|
||||
service.runtime = FakeVisualizationRuntime() # type: ignore[assignment]
|
||||
service._selected_device_id = "test-ble-transport" # noqa: SLF001
|
||||
service._k1_ip = "192.168.1.20" # noqa: SLF001
|
||||
service._compatibility_attestation = {"profile": "exact"} # noqa: SLF001
|
||||
service.arm_application_control_shadow(
|
||||
ShadowApplicationControlArmRequest(
|
||||
operator_confirmed=True,
|
||||
timezone_name="Europe/Moscow",
|
||||
)
|
||||
)
|
||||
|
||||
prepared = service.prepare_acquisition(
|
||||
PrepareAcquisitionRequest(
|
||||
project_name=PROJECT_NAME,
|
||||
host="192.168.1.20",
|
||||
duration_seconds=60,
|
||||
compatibility_attestation=ATTESTATION,
|
||||
)
|
||||
)
|
||||
|
||||
assert prepared["application_control_execution"]["state"] == "disarmed"
|
||||
assert prepared["application_control_execution"]["lease"] is None
|
||||
|
||||
|
||||
def test_operator_manual_start_is_confirmed_only_by_real_point_data(tmp_path: Path) -> None:
|
||||
service, runtime = service_with_fake_runtime(tmp_path)
|
||||
prepared = service.prepare_acquisition(
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from k1link.device_plugins.xgrids_k1.protocol.application_bootstrap import (
|
||||
ApplicationControlAuthority,
|
||||
)
|
||||
from k1link.device_plugins.xgrids_k1.protocol.application_execution import (
|
||||
ApplicationAuthorityLease,
|
||||
ApplicationControlExecutionError,
|
||||
DormantApplicationControlCoordinator,
|
||||
)
|
||||
from k1link.device_plugins.xgrids_k1.protocol.application_publish import (
|
||||
WriteDisabledOneShotPublisher,
|
||||
)
|
||||
|
||||
PRIVATE_AUTHORITY = "11111111-2222-3333-4444-555555555555"
|
||||
|
||||
|
||||
class FakeClock:
|
||||
def __init__(self) -> None:
|
||||
self.now = 100.0
|
||||
|
||||
def __call__(self) -> float:
|
||||
return self.now
|
||||
|
||||
|
||||
class FakeLoader:
|
||||
def __init__(self) -> None:
|
||||
self.calls = 0
|
||||
|
||||
def load(self) -> ApplicationControlAuthority:
|
||||
self.calls += 1
|
||||
return ApplicationControlAuthority(openapi_key=PRIVATE_AUTHORITY)
|
||||
|
||||
|
||||
class ForbiddenSink:
|
||||
def publish(
|
||||
self,
|
||||
*,
|
||||
topic: str,
|
||||
payload: bytes,
|
||||
qos: int,
|
||||
retain: bool,
|
||||
) -> None:
|
||||
del topic, payload, qos, retain
|
||||
raise AssertionError("write-disabled coordinator touched its sink")
|
||||
|
||||
|
||||
def test_authority_lease_expires_and_drops_its_cached_reference() -> None:
|
||||
clock = FakeClock()
|
||||
loader = FakeLoader()
|
||||
lease = ApplicationAuthorityLease(loader, ttl_seconds=30.0, monotonic=clock)
|
||||
|
||||
assert loader.calls == 1
|
||||
assert lease.snapshot().state == "armed"
|
||||
assert lease.snapshot().authority_cached
|
||||
assert PRIVATE_AUTHORITY not in repr(lease.snapshot())
|
||||
|
||||
clock.now = 130.0
|
||||
snapshot = lease.snapshot()
|
||||
assert snapshot.state == "expired"
|
||||
assert not snapshot.authority_cached
|
||||
assert snapshot.remaining_seconds == 0.0
|
||||
with pytest.raises(ApplicationControlExecutionError, match="not armed"):
|
||||
lease.authority()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("ttl_seconds", [14.999, 300.001, True])
|
||||
def test_authority_lease_rejects_unreviewed_ttl(ttl_seconds: object) -> None:
|
||||
with pytest.raises(ApplicationControlExecutionError):
|
||||
ApplicationAuthorityLease( # type: ignore[arg-type]
|
||||
FakeLoader(),
|
||||
ttl_seconds=ttl_seconds,
|
||||
)
|
||||
|
||||
|
||||
def test_dormant_coordinator_arms_once_and_exposes_no_emission_path() -> None:
|
||||
clock = FakeClock()
|
||||
loader = FakeLoader()
|
||||
coordinator = DormantApplicationControlCoordinator(
|
||||
loader,
|
||||
WriteDisabledOneShotPublisher(ForbiddenSink()),
|
||||
monotonic=clock,
|
||||
)
|
||||
|
||||
armed = coordinator.arm(
|
||||
ttl_seconds=60.0,
|
||||
epoch_seconds=1_752_680_000,
|
||||
timezone_name="Europe/Moscow",
|
||||
)
|
||||
public = armed.as_dict()
|
||||
assert public["state"] == "armed-shadow-only"
|
||||
assert public["can_emit_requests"] is False
|
||||
assert public["live_transport_installed"] is False
|
||||
assert public["publisher"] == {
|
||||
"mode": "write-disabled",
|
||||
"denied_attempts": 0,
|
||||
"transport_calls": 0,
|
||||
"vendor_writes_enabled": False,
|
||||
"publisher_armed": False,
|
||||
"automatic_retry": False,
|
||||
}
|
||||
assert PRIVATE_AUTHORITY not in repr(armed)
|
||||
assert PRIVATE_AUTHORITY not in str(public)
|
||||
with pytest.raises(ApplicationControlExecutionError, match="already armed"):
|
||||
coordinator.arm(
|
||||
ttl_seconds=60.0,
|
||||
epoch_seconds=1_752_680_000,
|
||||
timezone_name="Europe/Moscow",
|
||||
)
|
||||
|
||||
disarmed = coordinator.disarm().as_dict()
|
||||
assert disarmed["state"] == "disarmed"
|
||||
assert disarmed["lease"] is None
|
||||
assert disarmed["orchestrator"] is None
|
||||
assert loader.calls == 1
|
||||
|
||||
|
||||
def test_coordinator_expiry_drops_lease_and_orchestrator_together() -> None:
|
||||
clock = FakeClock()
|
||||
coordinator = DormantApplicationControlCoordinator(
|
||||
FakeLoader(),
|
||||
WriteDisabledOneShotPublisher(ForbiddenSink()),
|
||||
monotonic=clock,
|
||||
)
|
||||
coordinator.arm(
|
||||
ttl_seconds=15.0,
|
||||
epoch_seconds=1_752_680_000,
|
||||
timezone_name="Europe/Moscow",
|
||||
)
|
||||
|
||||
clock.now = 115.0
|
||||
snapshot = coordinator.snapshot().as_dict()
|
||||
assert snapshot["state"] == "expired"
|
||||
assert snapshot["lease"] is None
|
||||
assert snapshot["orchestrator"] is None
|
||||
assert snapshot["can_emit_requests"] is False
|
||||
Reference in New Issue
Block a user