wip(k1): checkpoint connection recovery rewrite
Capture the current unreleased K1 connection, recovery, lifecycle, viewer, and test work as a single known-bad baseline for subsequent fixes.
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
from collections.abc import Collection, Sequence
|
||||
from collections.abc import Callable, Collection, Sequence
|
||||
|
||||
import pytest
|
||||
from pydantic import JsonValue, TypeAdapter
|
||||
@@ -20,6 +20,9 @@ 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 (
|
||||
ApplicationMqttTransportError,
|
||||
)
|
||||
from k1link.device_plugins.xgrids_k1.protocol.application_publish import (
|
||||
OneShotPublishEnvelope,
|
||||
)
|
||||
@@ -120,7 +123,19 @@ class SyntheticAcceptanceTransport:
|
||||
envelopes: Sequence[OneShotPublishEnvelope],
|
||||
*,
|
||||
required_response_operation_keys: Collection[str],
|
||||
dispatch_admission_deadline_reached: Callable[[], bool] | None = None,
|
||||
dispatch_admission_commit: Callable[[], None] | None = None,
|
||||
) -> dict[str, bytes]:
|
||||
if (
|
||||
dispatch_admission_deadline_reached is not None
|
||||
and dispatch_admission_deadline_reached()
|
||||
):
|
||||
raise ApplicationMqttTransportError(
|
||||
"control command dispatch deadline expired before publish admission",
|
||||
reason_code="physical-command-dispatch-deadline-expired",
|
||||
)
|
||||
if dispatch_admission_commit is not None:
|
||||
dispatch_admission_commit()
|
||||
self.batches.append(tuple(envelope.operation_key for envelope in envelopes))
|
||||
responses: dict[str, bytes] = {}
|
||||
modeling_operation = next(
|
||||
@@ -357,6 +372,114 @@ def test_canonical_session_owns_start_active_scan_stop_and_save_boundary() -> No
|
||||
TypeAdapter(JsonValue).validate_python(executor.snapshot())
|
||||
|
||||
|
||||
def test_stop_deadline_expiring_during_dispatch_validation_consumes_no_permit_or_publish() -> None:
|
||||
transport = SyntheticAcceptanceTransport()
|
||||
executor = PhysicalAcceptanceDialogueExecutor(transport)
|
||||
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_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,
|
||||
)
|
||||
)
|
||||
executor.execute_canonical_start(
|
||||
start_command,
|
||||
build_canonical_post_start_observation(authority, binding),
|
||||
authority=authority,
|
||||
binding=binding,
|
||||
permit=PhysicalAcceptancePermit(_checklist(ModelingAction.START)),
|
||||
checkpoint=executor.wait_for_operator_checkpoint(
|
||||
"start-confirmed",
|
||||
lambda: True,
|
||||
),
|
||||
)
|
||||
executor.maintain_active_until_stop_requested(lambda: True)
|
||||
stop_command = ShadowModelingCommand.from_command(
|
||||
encode_modeling_stop(
|
||||
CommandHeaderIdentity(
|
||||
device_id=binding.vendor_device_id,
|
||||
openapi_key=APPLICATION_KEY,
|
||||
)
|
||||
)
|
||||
)
|
||||
stop_permit = PhysicalAcceptancePermit(_checklist(ModelingAction.STOP))
|
||||
expired = False
|
||||
|
||||
def slow_dispatch_validation() -> None:
|
||||
nonlocal expired
|
||||
expired = True
|
||||
|
||||
with pytest.raises(ApplicationMqttTransportError) as raised:
|
||||
executor.execute_canonical_stop(
|
||||
stop_command,
|
||||
stop_permit,
|
||||
dispatch_guard=slow_dispatch_validation,
|
||||
dispatch_admission_deadline_reached=lambda: expired,
|
||||
)
|
||||
|
||||
assert raised.value.reason_code == "physical-command-dispatch-deadline-expired"
|
||||
assert stop_permit.snapshot()["consumed"] is False
|
||||
assert transport.stop_emitted is False
|
||||
assert executor.snapshot()["stop_attempted"] is False
|
||||
assert executor.snapshot()["dialogue_stage"] == "stop-requested"
|
||||
|
||||
|
||||
def test_read_only_inspection_publishes_only_ordinal_one_device_info() -> None:
|
||||
transport = SyntheticAcceptanceTransport()
|
||||
executor = PhysicalAcceptanceDialogueExecutor(transport)
|
||||
orchestrator = ShadowApplicationBootstrapOrchestrator(
|
||||
ApplicationControlAuthority(openapi_key=APPLICATION_KEY),
|
||||
epoch_seconds=1_752_680_000,
|
||||
timezone_name="Europe/Moscow",
|
||||
)
|
||||
|
||||
binding = executor.run_read_only_inspection_stage(orchestrator)
|
||||
|
||||
assert binding.vendor_device_id == VENDOR_DEVICE_ID
|
||||
assert transport.batches == [("bootstrap:1:DeviceInfoRequest",)]
|
||||
outbound = {operation for batch in transport.batches for operation in batch}
|
||||
assert outbound == {"bootstrap:1:DeviceInfoRequest"}
|
||||
assert not any("DeviceConfig" in operation for operation in outbound)
|
||||
assert not any("ModelingStatus" in operation for operation in outbound)
|
||||
assert not any(operation.startswith("modeling:") for operation in outbound)
|
||||
assert executor.snapshot()["dialogue_stage"] == "inspection-ready"
|
||||
|
||||
completed = executor.complete_connection_stage(
|
||||
orchestrator,
|
||||
expected_binding=binding,
|
||||
)
|
||||
|
||||
assert completed == binding
|
||||
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",
|
||||
)
|
||||
|
||||
|
||||
def test_start_initialization_wait_has_fail_closed_watchdog() -> None:
|
||||
class NeverInitializedTransport(SyntheticAcceptanceTransport):
|
||||
def scan_initialization_complete(self, _binding: object) -> bool:
|
||||
|
||||
Reference in New Issue
Block a user