feat(k1): complete canonical control lifecycle

This commit is contained in:
DCCONSTRUCTIONS
2026-07-19 01:07:02 +03:00
parent d7a2c22faf
commit ffffee1879
42 changed files with 3577 additions and 556 deletions
@@ -0,0 +1,10 @@
{
"schema_version": 1,
"source": "sanitized-owner-captured-clean-cycle-device-info",
"device_model": "LixelKity K1",
"platform_type": "A4",
"software_version": "V3.0.2_20250624.122658",
"system_version": "V3.0.2",
"is_activated": true,
"redaction": "No device identifier, serial, authority, network address, credential, or raw payload is retained."
}
+21
View File
@@ -4,6 +4,7 @@ from typing import Any
from typer.testing import CliRunner
from k1link.device_plugins.xgrids_k1 import cli
from k1link.device_plugins.xgrids_k1.cli import app
runner = CliRunner()
@@ -25,6 +26,26 @@ def test_doctor_json() -> None:
assert any(item["name"] == "tcpdump" for item in payload["tools"])
def test_serve_resolves_frontend_from_repository_root(monkeypatch: Any) -> None:
captured: dict[str, object] = {}
def fake_run(application: str, **kwargs: object) -> None:
captured.update({"application": application, **kwargs})
monkeypatch.setattr(cli.uvicorn, "run", fake_run)
result = runner.invoke(app, ["serve"])
assert result.exit_code == 0
assert captured == {
"application": "k1link.web.app:app",
"host": "127.0.0.1",
"port": 8000,
"log_level": "info",
"access_log": True,
}
def test_authority_provision_requires_explicit_reviewed_value_confirmation() -> None:
result = runner.invoke(app, ["authority", "provision"])
+32 -1
View File
@@ -361,6 +361,37 @@ def test_dispatcher_routes_allowlisted_action_to_xgrids_facade() -> None:
assert service.calls == [("scan", 6.0)]
def test_runtime_classifies_non_json_plugin_output_as_execution_failure(
caplog: pytest.LogCaptureFixture,
) -> None:
retained_marker = "must-not-appear-in-the-server-log"
class NonJsonAdapter:
plugin_id = "example.non-json"
action_ids = frozenset({"state.read"})
async def invoke(
self,
_invocation: RuntimeActionInvocation,
) -> dict[str, Any]:
return {"dialogue": {"response_evidence": (retained_marker,)}}
dispatcher = DevicePluginDispatcher(
[
_in_process_runtime(
NonJsonAdapter(),
plugin_version="0.1.0",
host_api_version="missioncore.nodedc/v1alpha2",
)
]
)
with pytest.raises(PluginExecutionError, match="non-JSON"):
asyncio.run(dispatcher.invoke("example.non-json", "state.read", {}))
assert retained_marker not in caplog.text
def test_dispatcher_rejects_unknown_plugin_and_action() -> None:
dispatcher = DevicePluginDispatcher(
[_in_process_runtime(XgridsK1PluginFacade(FakeXgridsService()))]
@@ -404,7 +435,7 @@ def test_facade_validates_payload_before_calling_service() -> None:
"compatibility_attestation": {
"firmware_version": "3.0.2",
"topology": "direct-lan",
"operator_confirmed": True,
"verification": "live-device-info",
},
},
"live",
+8
View File
@@ -17,6 +17,7 @@ from k1link.device_plugins.xgrids_k1.protocol.modeling import (
from k1link.device_plugins.xgrids_k1.protocol.protobuf_wire import (
ProtobufWireError,
decode_zigzag64,
encode_zigzag64,
iter_fields,
)
from k1link.device_plugins.xgrids_k1.protocol.streams import (
@@ -94,6 +95,13 @@ def test_protobuf_wire_zigzag_and_bounds() -> None:
assert decode_zigzag64(0) == 0
assert decode_zigzag64(1) == -1
assert decode_zigzag64(2) == 1
assert encode_zigzag64(0) == 0
assert encode_zigzag64(-1) == 1
assert encode_zigzag64(1) == 2
assert decode_zigzag64(encode_zigzag64(-(1 << 63))) == -(1 << 63)
assert decode_zigzag64(encode_zigzag64((1 << 63) - 1)) == (1 << 63) - 1
with pytest.raises(ProtobufWireError, match="outside int64"):
encode_zigzag64(1 << 63)
with pytest.raises(ProtobufWireError, match="truncated"):
list(iter_fields(b"\x0a\x02\x01"))
with pytest.raises(ProtobufWireError, match="unsupported"):
+1 -1
View File
@@ -77,7 +77,7 @@ def test_validation_errors_do_not_echo_sensitive_request_values(
"compatibility_attestation": {
"firmware_version": "3.0.2",
"topology": "direct-lan",
"operator_confirmed": True,
"verification": "live-device-info",
},
}
payload = {"input": action_input} if wrap_input else action_input
+145 -20
View File
@@ -31,7 +31,7 @@ from k1link.device_plugins.xgrids_k1.protocol.application_bootstrap import (
ATTESTATION = CompatibilityAttestationRequest(
firmware_version="3.0.2",
topology="direct-lan",
operator_confirmed=True,
verification="live-device-info",
)
PRIMARY_TEST_CREDENTIAL = "x" * 24
SECONDARY_TEST_CREDENTIAL = "y" * 24
@@ -120,12 +120,11 @@ class FakeInteractiveControlSession:
self.state = "workspace-ready"
self.start_projects: list[str] = []
self.stop_calls = 0
self.confirm_calls = 0
def snapshot(self) -> dict[str, object]:
return {
"state": self.state,
"can_confirm_standby": self.state == "awaiting-standby-confirmation",
"can_confirm_standby": False,
}
def open_project_prompt(self) -> dict[str, object]:
@@ -147,12 +146,6 @@ class FakeInteractiveControlSession:
self.state = "awaiting-standby-confirmation"
return self.snapshot()
def confirm_standby(self) -> dict[str, object]:
assert self.state == "awaiting-standby-confirmation"
self.confirm_calls += 1
self.state = "completed"
return self.snapshot()
def close_prestart(self) -> dict[str, object]:
self.state = "closed"
return self.snapshot()
@@ -211,6 +204,8 @@ def test_prepare_creates_provisional_device_session_and_profiled_acquisition(
assert state["device_ref"]["device_id"] != state["device_session"]["device_session_id"]
assert state["acquisition"]["state"] == "prepared"
assert state["acquisition"]["project_name"] == PROJECT_NAME
assert state["acquisition"]["mount_type"] == "handheld"
assert state["acquisition"]["gnss_mode"] == "none"
assert state["acquisition"]["compatibility_profile_id"] == (XGRIDS_K1_COMPATIBILITY_PROFILE_ID)
assert state["compatibility"]["vendor_writes_enabled"] is False
@@ -230,6 +225,29 @@ def test_project_name_is_normalized_and_control_characters_are_rejected() -> Non
host="192.168.1.20",
compatibility_attestation=ATTESTATION,
)
def test_only_physically_accepted_configuration_values_are_admitted() -> None:
with pytest.raises(ValidationError):
ConnectRequest(
device_id="synthetic-device",
ssid="synthetic-network",
password=SecretStr(PRIMARY_TEST_CREDENTIAL),
connection_mode="quick-connect", # type: ignore[arg-type]
compatibility_attestation=ATTESTATION,
)
with pytest.raises(ValidationError):
PrepareAcquisitionRequest(
project_name=PROJECT_NAME,
host="192.168.1.20",
mount_type="uav", # type: ignore[arg-type]
compatibility_attestation=ATTESTATION,
)
with pytest.raises(ValidationError):
PrepareAcquisitionRequest(
project_name=PROJECT_NAME,
host="192.168.1.20",
gnss_mode="rtk", # type: ignore[arg-type]
compatibility_attestation=ATTESTATION,
)
def test_facade_arms_bounded_shadow_lease_without_installing_publish_transport(
@@ -247,7 +265,8 @@ def test_facade_arms_bounded_shadow_lease_without_installing_publish_transport(
service._compatibility_attestation = { # noqa: SLF001
"firmware_version": "3.0.2",
"topology": "direct-lan",
"basis": "operator-attested",
"verification": "live-device-info",
"basis": "selected-profile-live-device-info-required",
"observed_at": "2026-07-18T00:00:00Z",
}
@@ -274,7 +293,7 @@ def test_facade_arms_bounded_shadow_lease_without_installing_publish_transport(
assert disarmed["application_control_execution"]["lease"] is None
def test_shadow_arm_requires_idle_connected_attested_device_before_keychain_read(
def test_shadow_arm_requires_idle_connected_profile_selected_device_before_keychain_read(
tmp_path: Path,
) -> None:
loader = FakeApplicationAuthorityLoader()
@@ -405,21 +424,82 @@ def test_plugin_commanded_acquisition_keeps_start_and_stop_as_explicit_actions(
physical_acceptance=PHYSICAL_ACCEPTANCE,
)
)
stop_operation = stopping["last_operation"]
assert control.stop_calls == 1
assert stopping["acquisition"]["state"] == "awaiting_external_stop"
assert stopping["last_operation"]["status"] == "running"
completed = service.stop_acquisition(
control.state = "completed"
completed = service.state()
assert control.stop_calls == 1
assert completed["acquisition"]["state"] == "completed"
assert completed["acquisition"]["result"]["device_state"] == "ready"
assert completed["last_operation"]["status"] == "succeeded"
assert runtime.stop_calls == 1
def test_device_standby_retires_sources_after_terminal_local_stop_failure(
tmp_path: Path,
) -> None:
service, runtime = service_with_fake_runtime(tmp_path)
control = FakeInteractiveControlSession()
service._application_control_session = control # type: ignore[assignment] # noqa: SLF001
service._k1_ip = "192.168.1.20" # noqa: SLF001
service._compatibility_attestation = {"profile": "exact"} # noqa: SLF001
prepared = service.prepare_acquisition(
PrepareAcquisitionRequest(
project_name="TEST001",
host="192.168.1.20",
compatibility_attestation=ATTESTATION,
)
)
acquisition_id = prepared["acquisition"]["acquisition_id"]
service.start_acquisition(
StartAcquisitionRequest(
acquisition_id=acquisition_id,
physical_acceptance=PHYSICAL_ACCEPTANCE,
)
)
runtime.mark_ready()
runtime.pcl_frames = 1
service.state()
stopping = service.stop_acquisition(
StopAcquisitionRequest(
acquisition_id=acquisition_id,
mode="graceful",
operator_confirmed=True,
operation_id=stop_operation["operation_id"],
physical_acceptance=PHYSICAL_ACCEPTANCE,
)
)
stop_operation = stopping["last_operation"]
with service._lock: # noqa: SLF001
assert service._acquisition is not None # noqa: SLF001
service._acquisition.transition( # noqa: SLF001
"failed",
message_code="acquisition.camera_failed",
result={"camera_failure_code": "camera-source-ended"},
)
service._operations.transition( # noqa: SLF001
stop_operation["operation_id"],
"failed",
stage_code="runtime-failed",
message_code="acquisition.stop.runtime_failed",
error={
"category": "stream",
"code": "runtime-failed",
"retryable": False,
"safe_to_retry": False,
"side_effect_status": "unknown",
},
)
control.state = "completed"
recovered = service.state()
assert control.stop_calls == 1
assert control.confirm_calls == 1
assert completed["acquisition"]["state"] == "completed"
assert control.state == "completed"
assert recovered["acquisition"]["state"] == "failed"
assert recovered["application_control_session"]["state"] == "completed"
assert runtime.stop_calls == 1
@@ -1257,7 +1337,7 @@ def test_prepare_rejects_stream_subsets_and_duplicates(
)
def test_exact_profile_is_inactive_until_explicit_operator_attestation(
def test_exact_profile_is_inactive_until_selected_for_live_device_info_verification(
tmp_path: Path,
) -> None:
service, _ = service_with_fake_runtime(tmp_path)
@@ -1267,7 +1347,7 @@ def test_exact_profile_is_inactive_until_explicit_operator_attestation(
"profile_id": None,
"decision": "unknown",
"permitted_mode": "evidence-only",
"firmware_claim": "exact-3.0.2-profile-not-attested",
"firmware_claim": "exact-3.0.2-profile-not-selected",
"attestation": None,
"vendor_writes_enabled": False,
"camera_preview": "unverified",
@@ -1284,7 +1364,9 @@ def test_exact_profile_is_inactive_until_explicit_operator_attestation(
assert attested["compatibility"]["profile_id"] == XGRIDS_K1_COMPATIBILITY_PROFILE_ID
assert attested["compatibility"]["decision"] == "limited"
assert attested["compatibility"]["attestation"]["basis"] == "operator-attested"
assert attested["compatibility"]["attestation"]["basis"] == (
"selected-profile-live-device-info-required"
)
assert attested["device_session"]["compatibility_profile_id"] == (
XGRIDS_K1_COMPATIBILITY_PROFILE_ID
)
@@ -1722,6 +1804,7 @@ def test_network_provisioning_is_single_flight_and_secret_is_unwrapped_only_at_b
assert boundary_calls == [("k1-a", "lab-network", PRIMARY_TEST_CREDENTIAL)]
assert connected["k1_ip"] == "192.168.1.20"
assert connected["connection_mode"] == "bridge"
assert PRIMARY_TEST_CREDENTIAL not in str(connected)
provision_operations = [
item for item in connected["operations"] if item["action"] == "network.provision"
@@ -1729,6 +1812,48 @@ def test_network_provisioning_is_single_flight_and_secret_is_unwrapped_only_at_b
assert {item["status"] for item in provision_operations} == {"succeeded", "failed"}
def test_network_provisioning_rejects_an_ipv4_owned_by_the_local_host(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
service, _ = service_with_fake_runtime(tmp_path)
service._devices = [{"device_id": "k1-a"}] # noqa: SLF001
async def fake_provision(*_: object, **__: object) -> dict[str, Any]:
return {
"started_at_utc": "2026-07-18T15:19:25Z",
"completed_at_utc": "2026-07-18T15:19:26Z",
"profile_id": "xgrids-k1-fw3-wifi-v1",
"outcome": "lan_address_observed",
"observations": [{"status": {"ipv4": "192.168.68.51"}}],
}
monkeypatch.setattr(facade_module, "provision_wifi_once", fake_provision)
monkeypatch.setattr(facade_module, "_target_is_local_ipv4", lambda _target: True)
with pytest.raises(RuntimeError, match="уже принадлежит этому компьютеру"):
asyncio.run(
service.connect(
ConnectRequest(
device_id="k1-a",
ssid="lab-network",
password=SecretStr(PRIMARY_TEST_CREDENTIAL),
compatibility_attestation=ATTESTATION,
)
)
)
state = service.state()
assert state["selected_device_id"] is None
assert state["k1_ip"] is None
operation = next(
item for item in state["operations"] if item["action"] == "network.provision"
)
assert operation["status"] == "failed"
assert operation["error"]["safe_to_retry"] is False
assert operation["error"]["side_effect_status"] == "unknown"
def test_provisioning_cannot_switch_device_during_active_acquisition(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
+166 -41
View File
@@ -4,6 +4,7 @@ import hashlib
from collections.abc import Collection, Sequence
import pytest
from pydantic import JsonValue, TypeAdapter
from k1link.device_plugins.xgrids_k1.protocol.application_acceptance import (
ApplicationAcceptanceError,
@@ -11,6 +12,7 @@ from k1link.device_plugins.xgrids_k1.protocol.application_acceptance import (
PhysicalAcceptanceChecklist,
PhysicalAcceptanceDialogueExecutor,
PhysicalAcceptancePermit,
ScanInitializationTimeout,
)
from k1link.device_plugins.xgrids_k1.protocol.application_bootstrap import (
DEVICE_CONFIG_RESPONSE_TOPIC,
@@ -18,9 +20,6 @@ from k1link.device_plugins.xgrids_k1.protocol.application_bootstrap import (
ShadowApplicationBootstrapOrchestrator,
build_canonical_post_start_observation,
)
from k1link.device_plugins.xgrids_k1.protocol.application_mqtt import (
MODELING_RESPONSE_TOPIC,
)
from k1link.device_plugins.xgrids_k1.protocol.application_publish import (
OneShotPublishEnvelope,
)
@@ -67,14 +66,19 @@ def _header(session_id: str) -> bytes:
return _text(4, VENDOR_DEVICE_ID) + _text(5, session_id) + _text(6, APPLICATION_KEY)
def _device_info_response(session_id: str) -> bytes:
def _device_info_response(
session_id: str,
*,
device_model: str = "LixelKity K1",
device_type: str = "A4",
) -> bytes:
base_info = b"".join(
(
_text(2, "V3.0.2-20260101-release"),
_text(2, "V3.0.2_20250624.122658"),
_text(3, "V3.0.2"),
_text(6, "LixelKity K1"),
_text(6, device_model),
_text(7, "K1SERIAL01"),
_text(8, "K1"),
_text(8, device_type),
)
)
working_status = _uint(1, 1)
@@ -93,7 +97,13 @@ def _modeling_response(action: ModelingAction) -> bytes:
class SyntheticAcceptanceTransport:
def __init__(self, clock: FakeClock | None = None) -> None:
def __init__(
self,
clock: FakeClock | None = None,
*,
device_model: str = "LixelKity K1",
device_type: str = "A4",
) -> None:
self.batches: list[tuple[str, ...]] = []
self.maintain_calls: list[tuple[float, tuple[str, ...]]] = []
self.clock = clock
@@ -102,22 +112,32 @@ class SyntheticAcceptanceTransport:
self.post_stop_maintain_calls = 0
self.start_emitted = False
self.stop_emitted = False
self.device_model = device_model
self.device_type = device_type
def exchange_batch_once(
self,
envelopes: Sequence[OneShotPublishEnvelope],
*,
required_response_topics: Collection[str],
required_response_operation_keys: Collection[str],
) -> dict[str, bytes]:
self.batches.append(tuple(envelope.operation_key for envelope in envelopes))
responses: dict[str, bytes] = {}
if MODELING_RESPONSE_TOPIC in required_response_topics:
modeling_operation = next(
(
operation_key
for operation_key in required_response_operation_keys
if operation_key in {"modeling:start", "modeling:stop"}
),
None,
)
if modeling_operation is not None:
action = (
ModelingAction.START
if envelopes[0].operation_key == "modeling:start"
if modeling_operation == "modeling:start"
else ModelingAction.STOP
)
responses[MODELING_RESPONSE_TOPIC] = _modeling_response(action)
responses[modeling_operation] = _modeling_response(action)
if action is ModelingAction.START:
self.start_emitted = True
else:
@@ -134,7 +154,11 @@ class SyntheticAcceptanceTransport:
if ordinal == 1
else f"{VENDOR_DEVICE_ID}:DeviceInfoRequest"
)
response = _device_info_response(session)
response = _device_info_response(
session,
device_model=self.device_model,
device_type=self.device_type,
)
else:
session = (
f":{message_type}" if ordinal <= 3 else f"{VENDOR_DEVICE_ID}:{message_type}"
@@ -142,9 +166,8 @@ class SyntheticAcceptanceTransport:
if message_type == "DeviceConfigRequest":
session += ":Publish_Proto_DeviceConfig_SetTime"
response = _generic_response(session)
for topic in required_response_topics:
if topic.endswith(_response_suffix(message_type)):
responses[topic] = response
if envelope.operation_key in required_response_operation_keys:
responses[envelope.operation_key] = response
return responses
def maintain_open_for(
@@ -178,18 +201,6 @@ class SyntheticAcceptanceTransport:
return None
def _response_suffix(message_type: str) -> str:
values = {
"DeviceInfoRequest": "device_info",
"ModelingStatusRequest": "modeling_status",
"GetRtkAdvanceRequest": "get_rtk_advance",
"DeviceConfigRequest": "device_config",
"GetNtripProfileRequest": "get_ntrip_profile",
"GetCloudServerConfigRequest": "get_cloud_server_config",
}
return values[message_type]
def _checklist(action: ModelingAction) -> PhysicalAcceptanceChecklist:
return PhysicalAcceptanceChecklist(
action=action,
@@ -232,7 +243,14 @@ def test_canonical_session_owns_start_active_scan_stop_and_save_boundary() -> No
owner_token=object(),
),
)
assert [len(batch) for batch in transport.batches] == [1, 2, 3]
assert [len(batch) for batch in transport.batches] == [1, 5]
assert transport.batches[1] == (
"bootstrap:2:ModelingStatusRequest",
"bootstrap:3:GetRtkAdvanceRequest",
"bootstrap:4:DeviceConfigRequest",
"bootstrap:5:DeviceInfoRequest",
"bootstrap:6:GetRtkAdvanceRequest",
)
workspace_checks = iter((False, False, True))
workspace_checkpoint = executor.wait_for_operator_checkpoint(
"workspace-entered",
@@ -313,14 +331,11 @@ def test_canonical_session_owns_start_active_scan_stop_and_save_boundary() -> No
)
)
stop_response = executor.execute_canonical_stop(stop_command, stop_permit)
standby_checks = iter((False, False, True))
executor.maintain_post_stop_until_standby_confirmed(
lambda: next(standby_checks)
)
executor.maintain_post_stop_until_standby()
assert stop_response.action is ModelingAction.STOP
assert [len(batch) for batch in transport.batches] == [1, 2, 3, 1, 3, 1, 1, 2, 1]
assert len(transport.maintain_calls) == 214
assert [len(batch) for batch in transport.batches] == [1, 5, 1, 3, 1, 1, 2, 1]
assert len(transport.maintain_calls) == 212
assert set(transport.maintain_calls) == {
(1.0, ("lixel/application/response/modeling_status",))
}
@@ -332,13 +347,120 @@ def test_canonical_session_owns_start_active_scan_stop_and_save_boundary() -> No
assert executor.snapshot()["stop_complete"] is True
assert executor.snapshot()["dialogue_stage"] == "standby-confirmed"
response_evidence = executor.snapshot()["response_evidence"]
assert isinstance(response_evidence, tuple)
assert isinstance(response_evidence, list)
assert len(response_evidence) == 13
assert response_evidence[0]["operation_key"] == "bootstrap:1:DeviceInfoRequest"
assert response_evidence[-1]["operation_key"] == "modeling:stop"
assert all("payload" not in item for item in response_evidence)
assert APPLICATION_KEY not in str(executor.snapshot())
assert VENDOR_DEVICE_ID not in str(executor.snapshot())
TypeAdapter(JsonValue).validate_python(executor.snapshot())
def test_start_initialization_wait_has_fail_closed_watchdog() -> None:
class NeverInitializedTransport(SyntheticAcceptanceTransport):
def scan_initialization_complete(self, _binding: object) -> bool:
return False
clock = FakeClock()
transport = NeverInitializedTransport(clock)
executor = PhysicalAcceptanceDialogueExecutor(
transport,
monotonic=clock,
scan_initialization_timeout_seconds=2.0,
)
authority = ApplicationControlAuthority(openapi_key=APPLICATION_KEY)
orchestrator = ShadowApplicationBootstrapOrchestrator(
authority,
epoch_seconds=1_752_680_000,
timezone_name="Europe/Moscow",
)
binding = executor.run_connection_stage(orchestrator)
executor.run_workspace_entry_stage(
orchestrator,
executor.wait_for_operator_checkpoint("workspace-entered", lambda: True),
)
executor.run_project_prompt_stage(
orchestrator,
executor.wait_for_operator_checkpoint("project-prompt-opened", lambda: True),
)
start_checkpoint = executor.wait_for_operator_checkpoint(
"start-confirmed",
lambda: True,
)
command = ShadowModelingCommand.from_command(
encode_modeling_start(
CommandHeaderIdentity(
device_id=binding.vendor_device_id,
openapi_key=APPLICATION_KEY,
),
project_name="SAFE_PROJECT",
record_mode=RecordMode.RECORD_AND_CALCULATE,
scan_mode=ScanMode.LCC,
mount_type=MountType.HANDHELD,
)
)
permit = PhysicalAcceptancePermit(
_checklist(ModelingAction.START),
monotonic=clock,
)
with pytest.raises(ScanInitializationTimeout):
executor.execute_canonical_start(
command,
build_canonical_post_start_observation(authority, binding),
authority=authority,
binding=binding,
permit=permit,
checkpoint=start_checkpoint,
)
assert transport.start_emitted
assert not transport.stop_emitted
assert executor.snapshot()["dialogue_stage"] == "initializing"
assert executor.snapshot()["start_attempted"] is True
assert executor.snapshot()["start_complete"] is False
@pytest.mark.parametrize(
("device_model", "device_type"),
(
("LixelKity K2", "A4"),
("LixelKity K1", "K1"),
),
)
def test_connection_stage_rejects_other_device_model_or_type_before_preparation(
device_model: str,
device_type: str,
) -> None:
transport = SyntheticAcceptanceTransport(
device_model=device_model,
device_type=device_type,
)
executor = PhysicalAcceptanceDialogueExecutor(transport)
orchestrator = ShadowApplicationBootstrapOrchestrator(
ApplicationControlAuthority(openapi_key=APPLICATION_KEY),
epoch_seconds=1_752_680_000,
timezone_name="Europe/Moscow",
)
with pytest.raises(ApplicationAcceptanceError, match="incompatible with the selected"):
executor.run_connection_stage(orchestrator)
assert transport.batches == [("bootstrap:1:DeviceInfoRequest",)]
assert not transport.start_emitted
assert orchestrator.binding is None
snapshot = executor.snapshot()
assert snapshot["correlation_failure"] is None
compatibility_failure = snapshot["compatibility_failure"]
assert isinstance(compatibility_failure, dict)
assert compatibility_failure["reason_code"] == "compatibility_profile_mismatch"
assert compatibility_failure["expected"] == {
"device_model": "LixelKity K1",
"platform_type": "A4",
"firmware": "3.0.2",
"is_activated": True,
}
def test_bootstrap_correlation_failure_records_only_redacted_response_evidence() -> None:
@@ -347,14 +469,15 @@ def test_bootstrap_correlation_failure_records_only_redacted_response_evidence()
self,
envelopes: Sequence[OneShotPublishEnvelope],
*,
required_response_topics: Collection[str],
required_response_operation_keys: Collection[str],
) -> dict[str, bytes]:
responses = super().exchange_batch_once(
envelopes,
required_response_topics=required_response_topics,
required_response_operation_keys=required_response_operation_keys,
)
if DEVICE_CONFIG_RESPONSE_TOPIC in responses:
responses[DEVICE_CONFIG_RESPONSE_TOPIC] = _generic_response(
operation_key = "bootstrap:4:DeviceConfigRequest"
if operation_key in responses:
responses[operation_key] = _generic_response(
f"{VENDOR_DEVICE_ID}:wrong-session"
)
return responses
@@ -380,10 +503,11 @@ def test_bootstrap_correlation_failure_records_only_redacted_response_evidence()
"phase": "bootstrap",
"operation_key": "bootstrap:4:DeviceConfigRequest",
"response_topic": DEVICE_CONFIG_RESPONSE_TOPIC,
"reason_code": "response_session_mismatch",
"reason": "application response session correlation failed",
}
evidence = snapshot["response_evidence"]
assert isinstance(evidence, tuple)
assert isinstance(evidence, list)
assert evidence[-1]["payload_sha256"] == hashlib.sha256(
_generic_response(f"{VENDOR_DEVICE_ID}:wrong-session")
).hexdigest()
@@ -391,6 +515,7 @@ def test_bootstrap_correlation_failure_records_only_redacted_response_evidence()
assert all("payload" not in item for item in evidence)
assert APPLICATION_KEY not in str(snapshot)
assert VENDOR_DEVICE_ID not in str(snapshot)
TypeAdapter(JsonValue).validate_python(snapshot)
def test_standalone_start_and_stop_are_both_disabled() -> None:
@@ -16,6 +16,23 @@ from k1link.device_plugins.xgrids_k1.protocol.application_authority import (
PRIVATE_AUTHORITY = b"11111111-2222-3333-4444-555555555555\n"
@patch("platform.system", return_value="Darwin")
def test_runtime_authority_loads_through_security_framework_without_subprocess(
_system: object,
) -> None:
calls: list[tuple[str, str]] = []
def framework_reader(*, service: str, account: str) -> bytes:
calls.append((service, account))
return PRIVATE_AUTHORITY
loader = MacOSKeychainApplicationAuthorityLoader(framework_reader=framework_reader)
authority = loader.load()
assert calls == [(KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT)]
assert PRIVATE_AUTHORITY.decode().strip() not in repr(authority)
@patch("platform.system", return_value="Darwin")
@patch("shutil.which", return_value="/usr/bin/security")
def test_authority_loads_only_from_fixed_macos_keychain_item(
@@ -111,6 +128,21 @@ def test_authority_loader_rejects_wrong_profile_length_without_reflection(
assert invalid_secret.decode() not in str(error.value)
@patch("platform.system", return_value="Darwin")
def test_framework_authority_failure_is_redacted(_system: object) -> None:
private_error = "private-framework-diagnostic"
def framework_reader(*, service: str, account: str) -> bytes:
assert service == KEYCHAIN_SERVICE
assert account == KEYCHAIN_ACCOUNT
raise RuntimeError(private_error)
with pytest.raises(ApplicationAuthorityLoadError) as error:
MacOSKeychainApplicationAuthorityLoader(framework_reader=framework_reader).load()
assert private_error not in str(error.value)
@patch("sys.stdout.isatty", return_value=True)
@patch("sys.stdin.isatty", return_value=True)
@patch("platform.system", return_value="Darwin")
+94 -26
View File
@@ -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"),
)
+389 -17
View File
@@ -10,11 +10,14 @@ import pytest
from k1link.device_plugins.xgrids_k1.protocol.application_bootstrap import (
DEVICE_INFO_RESPONSE_TOPIC,
GET_RTK_ADVANCE_REQUEST_TOPIC,
GET_RTK_ADVANCE_RESPONSE_TOPIC,
MODELING_STATUS_RESPONSE_TOPIC,
LiveDeviceControlBinding,
)
from k1link.device_plugins.xgrids_k1.protocol.application_mqtt import (
CONTROL_SUBSCRIPTION_GROUPS,
MODELING_RESPONSE_TOPIC,
SYSTEM_ERROR_TOPIC,
ApplicationCommandOutcomeUnknown,
ApplicationControlDeviceFault,
@@ -26,12 +29,17 @@ from k1link.device_plugins.xgrids_k1.protocol.application_publish import (
from k1link.device_plugins.xgrids_k1.protocol.modeling_control import (
MODELING_STATE_BASE,
SYSTEM_ERROR_STATE_BASE,
ModelingAction,
SessionState,
)
from k1link.device_plugins.xgrids_k1.protocol.modeling_safety import DEVICE_STATUS_TOPIC
from k1link.device_plugins.xgrids_k1.protocol.modeling_safety import (
DEVICE_STATUS_TOPIC,
MODELING_REQUEST_TOPIC,
)
VENDOR_DEVICE_ID = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
DEVICE_SERIAL = "K1SERIAL01"
APPLICATION_KEY = "11111111-2222-3333-4444-555555555555"
class FakeReasonCode:
@@ -70,6 +78,7 @@ class FakeControlClient:
self.unsubscribe_calls: list[list[str]] = []
self.disconnect_calls = 0
self.next_mid = 20
self.connect_timeout = 5.0
def connect(self, host: str, port: int, keepalive: int) -> mqtt.MQTTErrorCode:
self.connect_calls.append((host, port, keepalive))
@@ -94,7 +103,7 @@ class FakeControlClient:
self.next_mid += 1
if self.emit_exchange:
self.events.append(("publish", mid))
self.events.append(("message", (self.response_topic, b"response")))
self.events.append(("message", (self.response_topic, payload)))
return SimpleNamespace(rc=mqtt.MQTT_ERR_SUCCESS, mid=mid)
def loop(self, timeout: float) -> mqtt.MQTTErrorCode:
@@ -131,11 +140,32 @@ class FakeControlClient:
return mqtt.MQTT_ERR_SUCCESS
def _envelope(operation_key: str = "bootstrap:1:DeviceInfoRequest") -> OneShotPublishEnvelope:
payload = b"synthetic-reviewed-request"
def _application_message(
session_id: str,
*,
device_id: str | None = None,
) -> bytes:
header = b"".join(
(
_text(4, device_id) if device_id is not None else b"",
_text(5, session_id),
_text(6, APPLICATION_KEY),
)
)
return _bytes(1, header)
def _envelope(
operation_key: str = "bootstrap:1:DeviceInfoRequest",
*,
topic: str = "lixel/application/request/device_info",
session_id: str = ":DeviceInfoRequest",
device_id: str | None = None,
) -> OneShotPublishEnvelope:
payload = _application_message(session_id, device_id=device_id)
return OneShotPublishEnvelope(
operation_key=operation_key,
topic="lixel/application/request/device_info",
topic=topic,
payload=payload,
payload_sha256=hashlib.sha256(payload).hexdigest(),
payload_bytes=len(payload),
@@ -144,10 +174,15 @@ def _envelope(operation_key: str = "bootstrap:1:DeviceInfoRequest") -> OneShotPu
)
def _modeling_status_envelope() -> OneShotPublishEnvelope:
payload = b"synthetic-reviewed-modeling-status"
def _modeling_status_envelope(
operation_key: str = "dialogue:12:ModelingStatusRequest",
) -> OneShotPublishEnvelope:
payload = _application_message(
f"{VENDOR_DEVICE_ID}:ModelingStatusRequest",
device_id=VENDOR_DEVICE_ID,
)
return OneShotPublishEnvelope(
operation_key="dialogue:12:ModelingStatusRequest",
operation_key=operation_key,
topic="lixel/application/request/modeling_status",
payload=payload,
payload_sha256=hashlib.sha256(payload).hexdigest(),
@@ -157,6 +192,33 @@ def _modeling_status_envelope() -> OneShotPublishEnvelope:
)
def _modeling_envelope(action: ModelingAction) -> OneShotPublishEnvelope:
payload = _application_message(
f"{VENDOR_DEVICE_ID}:ModelingRequest",
device_id=VENDOR_DEVICE_ID,
) + _uint(2, action)
return OneShotPublishEnvelope(
operation_key=f"modeling:{action.name.casefold()}",
topic=MODELING_REQUEST_TOPIC,
payload=payload,
payload_sha256=hashlib.sha256(payload).hexdigest(),
payload_bytes=len(payload),
qos=2,
retain=False,
)
def _modeling_response(action: ModelingAction) -> bytes:
return (
_application_message(
f"{VENDOR_DEVICE_ID}:ModelingRequest",
device_id=VENDOR_DEVICE_ID,
)
+ _uint(2, action)
+ _bytes(15, _uint(1, 0))
)
def _varint(value: int) -> bytes:
encoded = bytearray()
while value > 0x7F:
@@ -203,7 +265,7 @@ def _binding() -> LiveDeviceControlBinding:
software_version="V3.0.2-20260101-release",
system_version="V3.0.2",
device_model="LixelKity K1",
device_type="K1",
device_type="A4",
is_activated=True,
)
@@ -234,21 +296,24 @@ def test_acceptance_transport_connects_once_and_completes_one_qos2_exchange() ->
opened = transport.open().as_dict()
responses = transport.exchange_batch_once(
[_envelope()],
required_response_topics={DEVICE_INFO_RESPONSE_TOPIC},
required_response_operation_keys={"bootstrap:1:DeviceInfoRequest"},
)
transport.close()
assert fake.connect_calls == [("192.168.1.20", 1883, 60)]
assert fake.connect_timeout == 10.0
assert fake.subscribe_calls == [list(group) for group in CONTROL_SUBSCRIPTION_GROUPS]
assert fake.publish_calls == [
(
"lixel/application/request/device_info",
b"synthetic-reviewed-request",
_application_message(":DeviceInfoRequest"),
2,
False,
)
]
assert responses == {DEVICE_INFO_RESPONSE_TOPIC: b"response"}
assert responses == {
"bootstrap:1:DeviceInfoRequest": _application_message(":DeviceInfoRequest")
}
assert opened["clean_session"] is False
assert opened["automatic_reconnect"] is False
assert opened["automatic_retry"] is False
@@ -270,18 +335,325 @@ def test_consumed_operation_key_can_never_be_published_again() -> None:
transport.open()
transport.exchange_batch_once(
[_envelope()],
required_response_topics={DEVICE_INFO_RESPONSE_TOPIC},
required_response_operation_keys={"bootstrap:1:DeviceInfoRequest"},
)
with pytest.raises(ApplicationCommandOutcomeUnknown, match="already consumed"):
transport.exchange_batch_once(
[_envelope()],
required_response_topics={DEVICE_INFO_RESPONSE_TOPIC},
required_response_operation_keys={"bootstrap:1:DeviceInfoRequest"},
)
assert len(fake.publish_calls) == 1
def test_same_topic_inflight_responses_are_routed_by_exact_session_without_retry() -> None:
fake = FakeControlClient(response_topic=GET_RTK_ADVANCE_RESPONSE_TOPIC)
transport = ReviewedApplicationMqttTransport(
"192.168.1.20",
client_factory=lambda: cast(mqtt.Client, fake),
)
transport.open()
unbound = _envelope(
"bootstrap:3:GetRtkAdvanceRequest",
topic=GET_RTK_ADVANCE_REQUEST_TOPIC,
session_id=":GetRtkAdvanceRequest",
)
bound_session = f"{VENDOR_DEVICE_ID}:GetRtkAdvanceRequest"
bound = _envelope(
"bootstrap:6:GetRtkAdvanceRequest",
topic=GET_RTK_ADVANCE_REQUEST_TOPIC,
session_id=bound_session,
device_id=VENDOR_DEVICE_ID,
)
responses = transport.exchange_batch_once(
[unbound, bound],
required_response_operation_keys={unbound.operation_key, bound.operation_key},
)
assert responses[unbound.operation_key] == _application_message(
":GetRtkAdvanceRequest"
)
assert responses[bound.operation_key] == _application_message(
bound_session,
device_id=VENDOR_DEVICE_ID,
)
snapshot = transport.snapshot()
assert snapshot.publish_attempts == 2
assert snapshot.qos2_completions == 2
assert snapshot.correlated_responses == 2
assert snapshot.late_known_responses == 0
assert snapshot.automatic_retry is False
assert len(fake.publish_calls) == 2
def test_duplicate_start_response_blocks_stop_before_stop_is_published() -> None:
class ModelingControlClient(FakeControlClient):
def __init__(self) -> None:
super().__init__(
emit_exchange=False,
response_topic=MODELING_RESPONSE_TOPIC,
)
def publish(
self,
topic: str,
payload: bytes,
qos: int,
retain: bool,
) -> SimpleNamespace:
info = super().publish(topic, payload, qos, retain)
action = (
ModelingAction.START
if len(self.publish_calls) == 1
else ModelingAction.STOP
)
self.events.append(("publish", info.mid))
self.events.append(
(
"message",
(MODELING_RESPONSE_TOPIC, _modeling_response(action)),
)
)
return info
fake = ModelingControlClient()
transport = ReviewedApplicationMqttTransport(
"192.168.1.20",
client_factory=lambda: cast(mqtt.Client, fake),
)
transport.open()
transport.exchange_batch_once(
[_modeling_envelope(ModelingAction.START)],
required_response_operation_keys={"modeling:start"},
)
fake.events.append(
(
"message",
(
MODELING_RESPONSE_TOPIC,
_modeling_response(ModelingAction.START),
),
)
)
with pytest.raises(ApplicationCommandOutcomeUnknown, match="duplicate application response"):
transport.exchange_batch_once(
[_modeling_envelope(ModelingAction.STOP)],
required_response_operation_keys={"modeling:stop"},
)
snapshot = transport.snapshot()
assert snapshot.publish_attempts == 1
assert snapshot.correlated_responses == 1
assert snapshot.late_known_responses == 0
assert snapshot.state == "poisoned"
assert len(fake.publish_calls) == 1
def test_unanswered_optional_status_does_not_steal_same_identity_required_refresh() -> None:
class ReusedStatusClient(FakeControlClient):
def __init__(self) -> None:
super().__init__(
emit_exchange=False,
response_topic=MODELING_STATUS_RESPONSE_TOPIC,
)
def publish(
self,
topic: str,
payload: bytes,
qos: int,
retain: bool,
) -> SimpleNamespace:
info = super().publish(topic, payload, qos, retain)
self.events.append(("publish", info.mid))
if len(self.publish_calls) == 2:
# Ordinals 12 and 14 intentionally reuse the same protocol
# identity. The capture has only one response after ordinal
# 14; unanswered optional ordinal 12 must not swallow it.
self.events.append(
("message", (MODELING_STATUS_RESPONSE_TOPIC, payload))
)
return info
fake = ReusedStatusClient()
transport = ReviewedApplicationMqttTransport(
"192.168.1.20",
client_factory=lambda: cast(mqtt.Client, fake),
)
transport.open()
immediate = _modeling_status_envelope()
refresh = _modeling_status_envelope("dialogue:14:ModelingStatusRequest")
transport.exchange_batch_once(
[immediate],
required_response_operation_keys=(),
)
responses = transport.exchange_batch_once(
[refresh],
required_response_operation_keys={refresh.operation_key},
)
assert responses[refresh.operation_key] == refresh.payload
snapshot = transport.snapshot()
assert snapshot.publish_attempts == 2
assert snapshot.correlated_responses == 1
assert snapshot.ignored_known_responses == 0
assert snapshot.late_known_responses == 0
def test_already_arrived_optional_status_is_consumed_before_required_refresh() -> None:
class ReusedStatusClient(FakeControlClient):
def __init__(self) -> None:
super().__init__(
emit_exchange=False,
response_topic=MODELING_STATUS_RESPONSE_TOPIC,
)
def publish(
self,
topic: str,
payload: bytes,
qos: int,
retain: bool,
) -> SimpleNamespace:
info = super().publish(topic, payload, qos, retain)
self.events.append(("publish", info.mid))
if len(self.publish_calls) == 2:
self.events.append(
("message", (MODELING_STATUS_RESPONSE_TOPIC, payload))
)
return info
fake = ReusedStatusClient()
transport = ReviewedApplicationMqttTransport(
"192.168.1.20",
client_factory=lambda: cast(mqtt.Client, fake),
)
transport.open()
immediate = _modeling_status_envelope()
refresh = _modeling_status_envelope("dialogue:14:ModelingStatusRequest")
transport.exchange_batch_once(
[immediate],
required_response_operation_keys=(),
)
# This callback is already available before ordinal 14 is admitted. The
# retained socket's zero-wait service turn must consume it as ordinal 12.
fake.events.append(
("message", (MODELING_STATUS_RESPONSE_TOPIC, immediate.payload))
)
responses = transport.exchange_batch_once(
[refresh],
required_response_operation_keys={refresh.operation_key},
)
assert responses[refresh.operation_key] == refresh.payload
snapshot = transport.snapshot()
assert snapshot.publish_attempts == 2
assert snapshot.correlated_responses == 1
assert snapshot.ignored_known_responses == 1
assert snapshot.late_known_responses == 1
def test_unbound_optional_status_remains_distinct_from_bound_required_refresh() -> None:
class DistinctStatusClient(FakeControlClient):
def __init__(self) -> None:
super().__init__(
emit_exchange=False,
response_topic=MODELING_STATUS_RESPONSE_TOPIC,
)
def publish(
self,
topic: str,
payload: bytes,
qos: int,
retain: bool,
) -> SimpleNamespace:
info = super().publish(topic, payload, qos, retain)
self.events.append(("publish", info.mid))
if len(self.publish_calls) == 2:
self.events.append(
(
"message",
(
MODELING_STATUS_RESPONSE_TOPIC,
_application_message(":ModelingStatusRequest"),
),
)
)
self.events.append(
("message", (MODELING_STATUS_RESPONSE_TOPIC, payload))
)
return info
fake = DistinctStatusClient()
transport = ReviewedApplicationMqttTransport(
"192.168.1.20",
client_factory=lambda: cast(mqtt.Client, fake),
)
transport.open()
initial = _envelope(
"bootstrap:2:ModelingStatusRequest",
topic="lixel/application/request/modeling_status",
session_id=":ModelingStatusRequest",
)
refresh = _modeling_status_envelope("dialogue:14:ModelingStatusRequest")
transport.exchange_batch_once(
[initial],
required_response_operation_keys=(),
)
responses = transport.exchange_batch_once(
[refresh],
required_response_operation_keys={refresh.operation_key},
)
assert responses[refresh.operation_key] == refresh.payload
snapshot = transport.snapshot()
assert snapshot.publish_attempts == 2
assert snapshot.correlated_responses == 1
assert snapshot.ignored_known_responses == 1
assert snapshot.late_known_responses == 1
def test_duplicate_current_application_response_fails_closed() -> None:
class DuplicateResponseClient(FakeControlClient):
def publish(
self,
topic: str,
payload: bytes,
qos: int,
retain: bool,
) -> SimpleNamespace:
info = super().publish(topic, payload, qos, retain)
self.events.append(("message", (self.response_topic, payload)))
return info
fake = DuplicateResponseClient()
transport = ReviewedApplicationMqttTransport(
"192.168.1.20",
client_factory=lambda: cast(mqtt.Client, fake),
)
transport.open()
with pytest.raises(
ApplicationCommandOutcomeUnknown,
match="duplicate application response",
) as raised:
transport.exchange_batch_once(
[_envelope()],
required_response_operation_keys={"bootstrap:1:DeviceInfoRequest"},
)
assert raised.value.reason_code == "duplicate_application_response"
assert transport.snapshot().state == "poisoned"
assert len(fake.publish_calls) == 1
def test_post_start_status_can_be_response_free_and_socket_is_continuously_serviced() -> None:
clock = FakeClock()
fake = FakeControlClient(
@@ -297,7 +669,7 @@ def test_post_start_status_can_be_response_free_and_socket_is_continuously_servi
responses = transport.exchange_batch_once(
[_modeling_status_envelope()],
required_response_topics=(),
required_response_operation_keys=(),
)
transport.maintain_open_for(
2.0,
@@ -386,7 +758,7 @@ def test_post_publish_timeout_poisoned_transport_never_retries() -> None:
with pytest.raises(ApplicationCommandOutcomeUnknown, match="automatic retry is forbidden"):
transport.exchange_batch_once(
[_envelope()],
required_response_topics={DEVICE_INFO_RESPONSE_TOPIC},
required_response_operation_keys={"bootstrap:1:DeviceInfoRequest"},
)
assert transport.snapshot().state == "poisoned"
@@ -394,6 +766,6 @@ def test_post_publish_timeout_poisoned_transport_never_retries() -> None:
with pytest.raises(ApplicationCommandOutcomeUnknown, match="already consumed"):
transport.exchange_batch_once(
[_envelope()],
required_response_topics={DEVICE_INFO_RESPONSE_TOPIC},
required_response_operation_keys={"bootstrap:1:DeviceInfoRequest"},
)
assert len(fake.publish_calls) == 1
+1 -1
View File
@@ -40,7 +40,7 @@ def _envelope() -> OneShotPublishEnvelope:
software_version="V3.0.2-build.1",
system_version="V3.0.2",
device_model="LixelKity K1",
device_type="K1",
device_type="A4",
is_activated=True,
)
request = build_shadow_bootstrap(
+524 -14
View File
@@ -1,5 +1,6 @@
from __future__ import annotations
import logging
import threading
import time
from dataclasses import dataclass
@@ -37,6 +38,11 @@ class FakeTransportSnapshot:
def as_dict(self) -> dict[str, object]:
return {
"state": self.state,
"publish_attempts": 0,
"qos2_completions": 0,
"correlated_responses": 0,
"ignored_known_responses": 0,
"late_known_responses": 0,
"latest_device_session_state": "ready" if self.ready else "scanning",
"latest_device_project_bound": not self.ready,
"automatic_retry": False,
@@ -71,8 +77,8 @@ class FakeExecutor:
device_serial="serial-id",
software_version="3.0.2",
system_version="3.0.2",
device_model="K1",
device_type="scanner",
device_model="LixelKity K1",
device_type="A4",
is_activated=True,
)
@@ -124,13 +130,17 @@ class FakeExecutor:
self.transport.ready = True
return object()
def maintain_post_stop_until_standby_confirmed(self, observed: Any) -> None:
self.records.append("wait:steady-green")
while not observed():
threading.Event().wait(0.005)
def maintain_post_stop_until_standby(self) -> None:
self.records.append("wait:device-standby")
def snapshot(self) -> dict[str, object]:
return {"records": tuple(self.records), "automatic_retry": False}
return {
"records": list(self.records),
"dialogue_stage": "test-stage",
"start_attempted": False,
"stop_attempted": False,
"automatic_retry": False,
}
def _confirmation() -> OperatorPresenceConfirmation:
@@ -156,7 +166,7 @@ def _wait_phase(
raise AssertionError(f"session did not reach {expected}: {session.snapshot()}")
def test_every_canonical_stage_requires_a_separate_operator_event(
def test_canonical_stages_require_operator_events_but_device_standby_does_not(
monkeypatch: pytest.MonkeyPatch,
) -> None:
FakeExecutor.records = []
@@ -195,12 +205,9 @@ def test_every_canonical_stage_requires_a_separate_operator_event(
assert FakeExecutor.records[-1] == "wait:stop"
session.request_stop(confirmation=_confirmation())
standby = _wait_phase(session, "awaiting-standby-confirmation")
assert standby["can_confirm_standby"] is True
assert FakeExecutor.records[-1] == "wait:steady-green"
session.confirm_standby()
completed = _wait_phase(session, "completed")
assert completed["can_confirm_standby"] is False
assert completed["pending_operator_action"] is None
assert loader.calls == 1
assert completed["automatic_retry"] is False
assert completed["scripted_transitions"] is False
@@ -214,5 +221,508 @@ def test_every_canonical_stage_requires_a_separate_operator_event(
"start:11-14",
"wait:stop",
"stop",
"wait:steady-green",
"wait:device-standby",
]
def test_prestart_failure_is_reported_and_requires_a_new_operator_click(
monkeypatch: pytest.MonkeyPatch,
caplog: pytest.LogCaptureFixture,
) -> None:
caplog.set_level(logging.ERROR)
class BootstrapFailureExecutor(FakeExecutor):
def run_connection_stage(
self,
_orchestrator: object,
) -> LiveDeviceControlBinding:
raise RuntimeError("bootstrap correlation failed")
def snapshot(self) -> dict[str, object]:
return {
"dialogue_stage": "connection",
"start_attempted": False,
"stop_attempted": False,
"response_evidence": [],
"automatic_retry": False,
}
monkeypatch.setattr(
session_module,
"PhysicalAcceptanceDialogueExecutor",
BootstrapFailureExecutor,
)
session = InteractiveApplicationControlSession(
FakeAuthorityLoader(),
transport_factory=lambda host: FakeTransport(host), # type: ignore[arg-type]
)
session.open(
host="192.168.1.20",
timezone_name="Europe/Moscow",
confirmation=_confirmation(),
)
_wait_phase(session, "failed")
failed_thread = session._thread # noqa: SLF001
assert failed_thread is not None
failed_thread.join(timeout=2.0)
assert not failed_thread.is_alive()
failed = session.snapshot()
assert failed["outcome_unknown"] is False
assert failed["can_open"] is True
assert failed["automatic_retry"] is False
assert failed["failure"] == {
"code": "RuntimeError",
"reason_code": "unexpected_runtime_error",
"message": "bootstrap correlation failed",
"failed_phase": "connecting",
"dialogue_stage": "connection",
"transport_state": "ready",
"publish_attempts": 0,
"qos2_completions": 0,
"correlated_responses": 0,
"ignored_known_responses": 0,
"late_known_responses": 0,
"modeling_command_attempted": False,
"diagnostic_snapshot_unavailable": [],
"diagnostic_evidence_unavailable": [],
"correlation_failure": None,
"compatibility_failure": None,
"safe_to_retry": True,
}
assert "reason_code=unexpected_runtime_error" in caplog.text
def test_correlated_read_only_profile_mismatch_allows_only_a_fresh_explicit_attempt(
monkeypatch: pytest.MonkeyPatch,
) -> None:
class ProfileMismatchError(RuntimeError):
reason_code = "compatibility_profile_mismatch"
@dataclass
class CorrelatedReadSnapshot:
state: str
def as_dict(self) -> dict[str, object]:
return {
"state": self.state,
"publish_attempts": 1,
"qos2_completions": 1,
"correlated_responses": 1,
"ignored_known_responses": 0,
"late_known_responses": 0,
"automatic_retry": False,
"automatic_reconnect": False,
}
class CorrelatedReadTransport(FakeTransport):
def snapshot(self) -> CorrelatedReadSnapshot:
return CorrelatedReadSnapshot(self.state)
class ProfileMismatchExecutor(FakeExecutor):
def run_connection_stage(
self,
_orchestrator: object,
) -> LiveDeviceControlBinding:
raise ProfileMismatchError("live DeviceInfo profile mismatch")
def snapshot(self) -> dict[str, object]:
return {
"dialogue_stage": "new",
"start_attempted": False,
"stop_attempted": False,
"response_evidence": [],
"correlation_failure": None,
"compatibility_failure": {
"operation_key": "bootstrap:1:DeviceInfoRequest",
"reason_code": "compatibility_profile_mismatch",
},
"automatic_retry": False,
}
monkeypatch.setattr(
session_module,
"PhysicalAcceptanceDialogueExecutor",
ProfileMismatchExecutor,
)
session = InteractiveApplicationControlSession(
FakeAuthorityLoader(),
transport_factory=lambda host: CorrelatedReadTransport(host), # type: ignore[arg-type]
)
session.open(
host="192.168.1.20",
timezone_name="Europe/Moscow",
confirmation=_confirmation(),
)
failed_thread = session._thread # noqa: SLF001
assert failed_thread is not None
failed_thread.join(timeout=2.0)
assert not failed_thread.is_alive()
failed = session.snapshot()
assert failed["state"] == "failed"
assert failed["outcome_unknown"] is False
assert failed["automatic_retry"] is False
assert failed["can_open"] is True
failure = failed["failure"]
assert isinstance(failure, dict)
assert failure["reason_code"] == "compatibility_profile_mismatch"
assert failure["modeling_command_attempted"] is False
assert failure["safe_to_retry"] is True
def test_new_explicit_session_waits_for_old_worker_transport_retirement(
monkeypatch: pytest.MonkeyPatch,
) -> None:
old_close_entered = threading.Event()
release_old_close = threading.Event()
transports: list[TaggedTransport] = []
@dataclass
class TaggedTransportSnapshot:
state: str
ready: bool
tag: str
def as_dict(self) -> dict[str, object]:
return {
"state": self.state,
"tag": self.tag,
"publish_attempts": 0,
"qos2_completions": 0,
"correlated_responses": 0,
"ignored_known_responses": 0,
"late_known_responses": 0,
"latest_device_session_state": "ready" if self.ready else "scanning",
"latest_device_project_bound": not self.ready,
"automatic_retry": False,
"automatic_reconnect": False,
}
class TaggedTransport(FakeTransport):
def __init__(self, host: str, tag: str) -> None:
super().__init__(host)
self.tag = tag
self.close_calls = 0
def close(self) -> None:
if self.tag == "generation-1":
old_close_entered.set()
assert release_old_close.wait(timeout=2.0)
self.close_calls += 1
super().close()
def snapshot(self) -> TaggedTransportSnapshot:
return TaggedTransportSnapshot(self.state, self.ready, self.tag)
class RacingExecutor(FakeExecutor):
instances = 0
def __init__(self, transport: TaggedTransport) -> None:
super().__init__(transport)
type(self).instances += 1
self.instance = type(self).instances
def run_connection_stage(
self,
orchestrator: object,
) -> LiveDeviceControlBinding:
if self.instance == 1:
raise RuntimeError("first generation failed before publish")
return super().run_connection_stage(orchestrator)
def transport_factory(host: str) -> TaggedTransport:
transport = TaggedTransport(host, f"generation-{len(transports) + 1}")
transports.append(transport)
return transport
FakeExecutor.records = []
monkeypatch.setattr(
session_module,
"PhysicalAcceptanceDialogueExecutor",
RacingExecutor,
)
session = InteractiveApplicationControlSession(
FakeAuthorityLoader(),
transport_factory=transport_factory, # type: ignore[arg-type]
)
session.open(
host="192.168.1.20",
timezone_name="Europe/Moscow",
confirmation=_confirmation(),
)
_wait_phase(session, "failed")
assert old_close_entered.wait(timeout=2.0)
old_thread = session._thread # noqa: SLF001
assert old_thread is not None
# The failure is safe to retry at the protocol level, but the old worker
# still owns its socket. A new explicit click must not construct or open a
# second transport until close() completes and the worker exits.
retiring = session.snapshot()
assert retiring["failure"]["safe_to_retry"] is True # type: ignore[index]
assert retiring["can_open"] is False
with pytest.raises(
session_module.ApplicationAcceptanceError,
match="already open or requires manual recovery",
):
session.open(
host="192.168.1.20",
timezone_name="Europe/Moscow",
confirmation=_confirmation(),
)
assert len(transports) == 1
assert transports[0].close_calls == 0
release_old_close.set()
old_thread.join(timeout=2.0)
assert not old_thread.is_alive()
retired = session.snapshot()
assert retired["state"] == "failed"
assert retired["can_open"] is True
assert transports[0].close_calls == 1
# Only a fresh explicit action after retirement creates generation 2.
session.open(
host="192.168.1.20",
timezone_name="Europe/Moscow",
confirmation=_confirmation(),
)
new_thread = session._thread # noqa: SLF001
assert new_thread is not None
current = _wait_phase(session, "connection-ready")
assert current["transport"]["tag"] == "generation-2" # type: ignore[index]
assert session._transport is transports[1] # noqa: SLF001
assert transports[1].close_calls == 0
# Complete the second synthetic session so no background waiter remains.
session.enter_workspace()
_wait_phase(session, "workspace-ready")
session.open_project_prompt()
_wait_phase(session, "project-ready")
session.request_start(project_name="TEST002", confirmation=_confirmation())
_wait_phase(session, "scanning")
session.request_stop(confirmation=_confirmation())
_wait_phase(session, "completed")
new_thread.join(timeout=2.0)
assert not new_thread.is_alive()
assert transports[1].close_calls == 1
def test_start_outcome_unknown_blocks_reopen_even_when_transport_is_closed(
monkeypatch: pytest.MonkeyPatch,
) -> None:
class StartFailureExecutor(FakeExecutor):
start_attempted = False
def execute_canonical_start(self, *_args: object, **_kwargs: object) -> object:
type(self).start_attempted = True
raise session_module.ApplicationCommandOutcomeUnknown(
"START response was not correlated"
)
def snapshot(self) -> dict[str, object]:
return {
"dialogue_stage": "start-attempted",
"start_attempted": type(self).start_attempted,
"stop_attempted": False,
"response_evidence": [],
"automatic_retry": False,
}
FakeExecutor.records = []
StartFailureExecutor.start_attempted = False
monkeypatch.setattr(
session_module,
"PhysicalAcceptanceDialogueExecutor",
StartFailureExecutor,
)
session = InteractiveApplicationControlSession(
FakeAuthorityLoader(),
transport_factory=lambda host: FakeTransport(host), # type: ignore[arg-type]
)
session.open(
host="192.168.1.20",
timezone_name="Europe/Moscow",
confirmation=_confirmation(),
)
_wait_phase(session, "connection-ready")
session.enter_workspace()
_wait_phase(session, "workspace-ready")
session.open_project_prompt()
_wait_phase(session, "project-ready")
session.request_start(project_name="TEST001", confirmation=_confirmation())
failed = _wait_phase(session, "failed")
assert failed["outcome_unknown"] is True
assert failed["can_open"] is False
assert failed["failure"]["modeling_command_attempted"] is True # type: ignore[index]
assert failed["failure"]["safe_to_retry"] is False # type: ignore[index]
def test_unavailable_transport_snapshot_after_publish_blocks_reopen(
monkeypatch: pytest.MonkeyPatch,
) -> None:
executor_entered = threading.Event()
release_failure = threading.Event()
class SnapshotFailureTransport(FakeTransport):
def __init__(self, host: str) -> None:
super().__init__(host)
self.simulated_publish_attempts = 0
self.snapshot_unavailable = False
def snapshot(self) -> FakeTransportSnapshot:
if self.snapshot_unavailable:
raise RuntimeError("simulated transport snapshot failure")
return super().snapshot()
class PublishedThenFailedExecutor(FakeExecutor):
def run_connection_stage(
self,
_orchestrator: object,
) -> LiveDeviceControlBinding:
executor_entered.set()
assert release_failure.wait(timeout=2.0)
assert isinstance(self.transport, SnapshotFailureTransport)
self.transport.simulated_publish_attempts = 1
self.transport.snapshot_unavailable = True
raise RuntimeError("bootstrap failed after simulated publish")
monkeypatch.setattr(
session_module,
"PhysicalAcceptanceDialogueExecutor",
PublishedThenFailedExecutor,
)
transport = SnapshotFailureTransport("192.168.1.20")
session = InteractiveApplicationControlSession(
FakeAuthorityLoader(),
transport_factory=lambda _host: transport, # type: ignore[arg-type]
)
session.open(
host="192.168.1.20",
timezone_name="Europe/Moscow",
confirmation=_confirmation(),
)
assert executor_entered.wait(timeout=2.0)
failed_thread = session._thread # noqa: SLF001
assert failed_thread is not None
release_failure.set()
failed_thread.join(timeout=2.0)
assert not failed_thread.is_alive()
failed = session.snapshot()
assert transport.simulated_publish_attempts == 1
assert failed["state"] == "failed"
assert failed["outcome_unknown"] is True
assert failed["can_open"] is False
failure = failed["failure"]
assert isinstance(failure, dict)
assert failure["transport_state"] is None
assert failure["publish_attempts"] is None
assert failure["diagnostic_snapshot_unavailable"] == ["transport"]
assert failure["diagnostic_evidence_unavailable"] == ["transport.publish_attempts"]
assert failure["modeling_command_attempted"] is False
assert failure["safe_to_retry"] is False
with pytest.raises(
session_module.ApplicationAcceptanceError,
match="already open or requires manual recovery",
):
session.open(
host="192.168.1.20",
timezone_name="Europe/Moscow",
confirmation=_confirmation(),
)
def test_unavailable_dialogue_snapshot_is_outcome_unknown(
monkeypatch: pytest.MonkeyPatch,
) -> None:
class DialogueSnapshotFailureExecutor(FakeExecutor):
def run_connection_stage(
self,
_orchestrator: object,
) -> LiveDeviceControlBinding:
raise RuntimeError("bootstrap failed with unavailable dialogue evidence")
def snapshot(self) -> dict[str, object]:
raise RuntimeError("simulated dialogue snapshot failure")
monkeypatch.setattr(
session_module,
"PhysicalAcceptanceDialogueExecutor",
DialogueSnapshotFailureExecutor,
)
session = InteractiveApplicationControlSession(
FakeAuthorityLoader(),
transport_factory=lambda host: FakeTransport(host), # type: ignore[arg-type]
)
session.open(
host="192.168.1.20",
timezone_name="Europe/Moscow",
confirmation=_confirmation(),
)
failed_thread = session._thread # noqa: SLF001
assert failed_thread is not None
failed_thread.join(timeout=2.0)
assert not failed_thread.is_alive()
failed = session.snapshot()
assert failed["state"] == "failed"
assert failed["outcome_unknown"] is True
assert failed["can_open"] is False
failure = failed["failure"]
assert isinstance(failure, dict)
assert failure["publish_attempts"] == 0
assert failure["modeling_command_attempted"] is None
assert failure["diagnostic_snapshot_unavailable"] == ["dialogue"]
assert failure["diagnostic_evidence_unavailable"] == ["dialogue.modeling_command_attempted"]
assert failure["safe_to_retry"] is False
def test_pretransport_authority_failure_remains_safe_after_worker_retirement() -> None:
class FailingAuthorityLoader(FakeAuthorityLoader):
def load(self) -> ApplicationControlAuthority:
self.calls += 1
raise RuntimeError("authority unavailable before transport creation")
transport_factory_calls = 0
def transport_factory(host: str) -> FakeTransport:
nonlocal transport_factory_calls
transport_factory_calls += 1
return FakeTransport(host)
session = InteractiveApplicationControlSession(
FailingAuthorityLoader(),
transport_factory=transport_factory, # type: ignore[arg-type]
)
session.open(
host="192.168.1.20",
timezone_name="Europe/Moscow",
confirmation=_confirmation(),
)
failed_thread = session._thread # noqa: SLF001
assert failed_thread is not None
failed_thread.join(timeout=2.0)
assert not failed_thread.is_alive()
failed = session.snapshot()
assert transport_factory_calls == 0
assert failed["state"] == "failed"
assert failed["outcome_unknown"] is False
assert failed["can_open"] is True
failure = failed["failure"]
assert isinstance(failure, dict)
assert failure["publish_attempts"] is None
assert failure["modeling_command_attempted"] is False
assert failure["diagnostic_snapshot_unavailable"] == []
assert failure["diagnostic_evidence_unavailable"] == []
assert failure["safe_to_retry"] is True
+89 -1
View File
@@ -64,6 +64,23 @@ def _burst_ffmpeg(tmp_path: Path) -> Path:
return executable
def _clean_source_end_ffmpeg(tmp_path: Path) -> Path:
executable = tmp_path / "clean-source-end-ffmpeg"
executable.write_text(
f"#!{sys.executable}\n"
"import sys, time\n"
"def box(kind, payload=b''):\n"
" return (8 + len(payload)).to_bytes(4, 'big') + kind + payload\n"
"sys.stdout.buffer.write(box(b'ftyp', b'isom') + box(b'moov'))\n"
"sys.stdout.buffer.write(box(b'moof') + box(b'mdat', b'final-frame'))\n"
"sys.stdout.buffer.flush()\n"
"time.sleep(0.15)\n",
encoding="utf-8",
)
executable.chmod(0o700)
return executable
def _buffered_tail_ffmpeg(tmp_path: Path, sentinel: Path, *, fragments: int) -> Path:
executable = tmp_path / "buffered-tail-ffmpeg"
executable.write_text(
@@ -289,6 +306,76 @@ def test_acquisition_records_without_browser_and_source_switch_seals_epochs(
gateway.close()
def test_expected_camera_source_end_during_device_stop_seals_complete_epoch(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setenv(
"MISSIONCORE_FFMPEG_BINARY",
str(_clean_source_end_ffmpeg(tmp_path)),
)
gateway = XgridsK1CameraGateway(tmp_path, XGRIDS_K1_PLUGIN_ID)
session = tmp_path / "session"
session.mkdir()
try:
selected = gateway.select("sensor.camera.left", "192.168.1.20")
gateway.start_recording(session)
gateway.expect_source_end_for_device_stop()
_wait_until(
lambda: gateway.snapshot()["recording"]["completed_epochs"] == 1,
)
state = gateway.snapshot()
assert state["phase"] == "idle"
assert state["error"] is None
assert state["delivery"] is None
assert state["recording"]["active"] is True
assert state["recording"]["source_end_expected"] is False
gateway.stop_recording(status="complete")
epoch = (
session
/ "media"
/ "sensor.camera.left"
/ f"epoch-{selected['generation']}"
)
summary = json.loads((epoch / "summary.json").read_text(encoding="utf-8"))
assert summary["status"] == "complete"
assert summary["failure_code"] is None
assert summary["media_segment_count"] == 1
finally:
gateway.close()
def test_unexpected_camera_source_end_remains_a_recording_failure(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setenv(
"MISSIONCORE_FFMPEG_BINARY",
str(_clean_source_end_ffmpeg(tmp_path)),
)
gateway = XgridsK1CameraGateway(tmp_path, XGRIDS_K1_PLUGIN_ID)
session = tmp_path / "session"
session.mkdir()
try:
selected = gateway.select("sensor.camera.left", "192.168.1.20")
gateway.start_recording(session)
_wait_until(lambda: gateway.snapshot()["phase"] == "error")
epoch = (
session
/ "media"
/ "sensor.camera.left"
/ f"epoch-{selected['generation']}"
)
summary = json.loads((epoch / "summary.json").read_text(encoding="utf-8"))
assert summary["status"] == "interrupted"
assert summary["failure_code"] == "camera-source-ended"
finally:
gateway.close()
def test_camera_storage_open_failure_is_loud_and_never_starts_ffmpeg(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
@@ -501,7 +588,8 @@ def test_service_publishes_two_dynamic_camera_rows_and_stale_stop_is_safe(
service._compatibility_attestation = {
"firmware_version": "3.0.2",
"topology": "direct-lan",
"basis": "operator-attested",
"verification": "live-device-info",
"basis": "selected-profile-live-device-info-required",
"observed_at": "2026-07-16T20:00:00Z",
}
@@ -48,6 +48,7 @@ def test_xgrids_compatibility_profile_loads_exact_firmware_and_sources() -> None
assert profile["profile_id"] == "xgrids.lixelkity-k1.fw-3.0.2.direct-lan.v1"
assert profile["scope"]["vendor"] == "XGRIDS"
assert profile["scope"]["model"] == "LixelKity K1"
assert profile["scope"]["platform_type"] == "A4"
assert profile["scope"]["firmware"] == {"match": "exact", "version": "3.0.2"}
assert profile["scope"]["topology"] == "direct-lan"
assert LOADER.matches_target(profile, firmware="3.0.2", topology="direct-lan")
@@ -146,6 +147,15 @@ def test_xgrids_compatibility_profile_maps_actions_without_enabling_writes() ->
assert profile["safety"]["default_mode"] == "read-only"
assert profile["safety"]["vendor_writes_enabled"] is False
assert control["mode"] == "operator-manual"
assert control["software_acceptance_transport"] == {
"status": "installed-operator-present",
"default_authority": "disabled",
"profile_gate": "live-device-info-exact-match",
"dialogue": "single-socket-canonical-start-to-stop",
"automatic_retry": False,
"supported_mount_type": "handheld",
"supported_gnss_mode": "none",
}
assert control["verified_device_control"]["gesture"] == "physical-double-click"
for action_id, action_code in (("acquisition.start", 1), ("acquisition.stop", 2)):
+1 -1
View File
@@ -75,7 +75,7 @@ def _binding(**changes: object) -> LiveDeviceControlBinding:
"software_version": "V3.0.2-build.1",
"system_version": "V3.0.2",
"device_model": "LixelKity K1",
"device_type": "K1",
"device_type": "A4",
"is_activated": True,
}
values.update(changes)