Безопасное завершение калибровки при отключении K1
This commit is contained in:
@@ -28455,6 +28455,91 @@ class XgridsK1CompatibilityService:
|
||||
)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _matching_accepted_start_scan_over_before_active(
|
||||
physical_command_proof: Mapping[str, Any] | None,
|
||||
*,
|
||||
acquisition_id: str | None,
|
||||
start_operation_id: str | None,
|
||||
verified_control: Mapping[str, Any] | None,
|
||||
) -> bool:
|
||||
"""Recognize an accepted START followed by bound pre-active SCAN_OVER.
|
||||
|
||||
K1 can accept the canonical START and then leave calibration before it
|
||||
ever reports the target SCANNING state (for example, after losing
|
||||
power). The physical ledger correctly remains unresolved because no
|
||||
active target was observed. Local capture may nevertheless terminate
|
||||
once the same DeviceInfo-bound generation reports fresh, non-retained
|
||||
SCAN_OVER. This proof never resolves the physical ledger or authorizes
|
||||
another START/STOP; a later explicit read-only reconciliation remains
|
||||
mandatory.
|
||||
"""
|
||||
|
||||
if (
|
||||
not isinstance(physical_command_proof, Mapping)
|
||||
or not isinstance(acquisition_id, str)
|
||||
or not acquisition_id
|
||||
or not isinstance(start_operation_id, str)
|
||||
or not start_operation_id
|
||||
or not isinstance(verified_control, Mapping)
|
||||
or physical_command_proof.get("status") != "unresolved"
|
||||
or physical_command_proof.get("requires_reconciliation") is not True
|
||||
or physical_command_proof.get("runtime_bound") is not True
|
||||
or physical_command_proof.get("reconciliation_ready") is not True
|
||||
or physical_command_proof.get("observed_session_state") != "scan_over"
|
||||
or physical_command_proof.get("active_operation_id")
|
||||
!= start_operation_id
|
||||
):
|
||||
return False
|
||||
record = physical_command_proof.get("record")
|
||||
if not isinstance(record, Mapping):
|
||||
return False
|
||||
expected_binding = (
|
||||
XgridsK1CompatibilityService._exact_control_binding_document(
|
||||
verified_control
|
||||
)
|
||||
)
|
||||
baseline = record.get("baseline_status")
|
||||
response = record.get("application_response")
|
||||
last_status = record.get("last_status")
|
||||
last_status_valid = bool(
|
||||
last_status is None
|
||||
or (
|
||||
isinstance(last_status, Mapping)
|
||||
and last_status.get("session_state") == "scanning"
|
||||
and last_status.get("project_bound") is True
|
||||
and last_status.get("init_ready") is True
|
||||
and last_status.get("mqtt_retained") is False
|
||||
and last_status.get("system_error_code") is None
|
||||
)
|
||||
)
|
||||
return bool(
|
||||
expected_binding is not None
|
||||
and isinstance(record.get("connection"), Mapping)
|
||||
and dict(record["connection"]) == expected_binding
|
||||
and record.get("operation_id") == start_operation_id
|
||||
and record.get("acquisition_id") == acquisition_id
|
||||
and record.get("action") == "start"
|
||||
and record.get("stage") == "observing"
|
||||
and record.get("resolution") is None
|
||||
and record.get("publish_call_returned") is True
|
||||
and record.get("qos2_completed") is True
|
||||
and isinstance(record.get("packet_id"), int)
|
||||
and not isinstance(record.get("packet_id"), bool)
|
||||
and int(record["packet_id"]) > 0
|
||||
and isinstance(baseline, Mapping)
|
||||
and baseline.get("session_state") == "ready"
|
||||
and baseline.get("project_bound") is False
|
||||
and baseline.get("init_ready") is False
|
||||
and baseline.get("mqtt_retained") is False
|
||||
and baseline.get("system_error_code") is None
|
||||
and isinstance(response, Mapping)
|
||||
and response.get("operation_id") == start_operation_id
|
||||
and response.get("action") == "start"
|
||||
and response.get("success") is True
|
||||
and last_status_valid
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _matching_classified_prepared_stop_active(
|
||||
physical_command_proof: Mapping[str, Any] | None,
|
||||
@@ -30254,6 +30339,18 @@ class XgridsK1CompatibilityService:
|
||||
if isinstance(terminal_control_proof, Mapping)
|
||||
else None
|
||||
)
|
||||
accepted_start_scan_over_before_active = (
|
||||
self._matching_accepted_start_scan_over_before_active(
|
||||
physical_command_proof,
|
||||
acquisition_id=current_acquisition_id,
|
||||
start_operation_id=canonical_start_operation_id,
|
||||
verified_control=(
|
||||
terminal_verified_control
|
||||
if isinstance(terminal_verified_control, Mapping)
|
||||
else None
|
||||
),
|
||||
)
|
||||
)
|
||||
device_reported_scan_over_without_stop = bool(
|
||||
current is not None
|
||||
and current.control_mode == "plugin-commanded"
|
||||
@@ -30262,7 +30359,8 @@ class XgridsK1CompatibilityService:
|
||||
and isinstance(terminal_control_proof, Mapping)
|
||||
and terminal_control_proof.get("state") == "failed"
|
||||
and isinstance(terminal_control_failure, Mapping)
|
||||
and terminal_control_failure.get("failed_phase") == "scanning"
|
||||
and terminal_control_failure.get("failed_phase")
|
||||
in {"initializing", "scanning"}
|
||||
and terminal_control_failure.get("modeling_command_attempted") is True
|
||||
and terminal_control_failure.get("stop_command_attempted") is False
|
||||
and terminal_control_failure.get("diagnostic_snapshot_unavailable") == []
|
||||
@@ -30281,15 +30379,18 @@ class XgridsK1CompatibilityService:
|
||||
and physical_command_proof.get("runtime_bound") is True
|
||||
and physical_command_proof.get("reconciliation_ready") is True
|
||||
and physical_command_proof.get("observed_session_state") == "scan_over"
|
||||
and self._matching_start_active_confirmed(
|
||||
physical_command_proof,
|
||||
acquisition_id=current_acquisition_id,
|
||||
start_operation_id=canonical_start_operation_id,
|
||||
verified_control=(
|
||||
terminal_verified_control
|
||||
if isinstance(terminal_verified_control, Mapping)
|
||||
else None
|
||||
),
|
||||
and (
|
||||
self._matching_start_active_confirmed(
|
||||
physical_command_proof,
|
||||
acquisition_id=current_acquisition_id,
|
||||
start_operation_id=canonical_start_operation_id,
|
||||
verified_control=(
|
||||
terminal_verified_control
|
||||
if isinstance(terminal_verified_control, Mapping)
|
||||
else None
|
||||
),
|
||||
)
|
||||
or accepted_start_scan_over_before_active
|
||||
)
|
||||
)
|
||||
# Camera-only transport loss is supervised by the backend producer
|
||||
|
||||
@@ -17194,6 +17194,209 @@ def test_bound_scan_over_without_stop_dominates_late_empty_camera_epoch_failure(
|
||||
assert control.stop_calls == stop_calls_before == 0
|
||||
|
||||
|
||||
def test_accepted_start_then_scan_over_during_calibration_stops_local_wait_only(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
service, runtime = service_with_fake_runtime(tmp_path)
|
||||
control = FakeInteractiveControlSession()
|
||||
service._application_control_session = control # type: ignore[assignment] # noqa: SLF001
|
||||
binding = _seed_supervised_connection(service)
|
||||
prepared = service.prepare_acquisition(
|
||||
_prepare_request(
|
||||
project_name="POWERLOSS001",
|
||||
host=binding.target_ipv4,
|
||||
compatibility_attestation=ATTESTATION,
|
||||
)
|
||||
)
|
||||
acquisition_id = str(prepared["acquisition"]["acquisition_id"])
|
||||
service.start_acquisition(
|
||||
_start_request(
|
||||
acquisition_id=acquisition_id,
|
||||
physical_acceptance=PHYSICAL_ACCEPTANCE,
|
||||
)
|
||||
)
|
||||
start_operation_id = service._acquisition_start_operation_id # noqa: SLF001
|
||||
assert isinstance(start_operation_id, str)
|
||||
physical = _exact_start_physical_proof(
|
||||
operation_id=start_operation_id,
|
||||
acquisition_id=acquisition_id,
|
||||
binding=binding,
|
||||
resolved=False,
|
||||
)
|
||||
physical.update(
|
||||
{
|
||||
"runtime_bound": True,
|
||||
"reconciliation_ready": True,
|
||||
"observed_session_state": "scan_over",
|
||||
"active_operation_id": start_operation_id,
|
||||
}
|
||||
)
|
||||
record = physical["record"]
|
||||
assert isinstance(record, dict)
|
||||
record["last_status"] = None
|
||||
monkeypatch.setattr(
|
||||
service._physical_command_coordinator, # noqa: SLF001
|
||||
"snapshot",
|
||||
lambda: physical,
|
||||
)
|
||||
runtime.mark_ready()
|
||||
awaiting = service.state()
|
||||
assert awaiting["acquisition"]["state"] == "awaiting_external_start"
|
||||
assert runtime.pcl_frames == 0
|
||||
|
||||
original_control_snapshot = control.snapshot
|
||||
|
||||
def failed_during_calibration_snapshot() -> dict[str, object]:
|
||||
snapshot = original_control_snapshot()
|
||||
snapshot["outcome_unknown"] = True
|
||||
snapshot["failure"] = {
|
||||
"reason_code": "mqtt_network_loop_failed",
|
||||
"failed_phase": "initializing",
|
||||
"modeling_command_attempted": True,
|
||||
"stop_command_attempted": False,
|
||||
"diagnostic_snapshot_unavailable": [],
|
||||
"diagnostic_evidence_unavailable": [],
|
||||
"safe_to_retry": False,
|
||||
}
|
||||
snapshot["transport"] = {
|
||||
"state": "failed",
|
||||
"publish_attempts": 12,
|
||||
"device_status_reports": 4,
|
||||
"latest_device_session_state": "scan_over",
|
||||
"latest_device_project_bound": True,
|
||||
"latest_device_init_ready": False,
|
||||
"latest_system_error_code": None,
|
||||
"automatic_retry": False,
|
||||
"automatic_reconnect": False,
|
||||
}
|
||||
return snapshot
|
||||
|
||||
monkeypatch.setattr(control, "snapshot", failed_during_calibration_snapshot)
|
||||
control.state = "failed"
|
||||
control.state_revision += 1
|
||||
monkeypatch.setattr(service, "_seal_acquisition_capture_clock", lambda: None)
|
||||
camera_stop_calls: list[str] = []
|
||||
monkeypatch.setattr(
|
||||
service.camera_preview,
|
||||
"stop_current",
|
||||
lambda: camera_stop_calls.append("stop") or {},
|
||||
)
|
||||
start_projects_before = list(control.start_projects)
|
||||
physical_record_before = json.dumps(record, sort_keys=True)
|
||||
|
||||
terminal = service.state()
|
||||
|
||||
start_operation = next(
|
||||
operation
|
||||
for operation in terminal["operations"]
|
||||
if operation["operation_id"] == start_operation_id
|
||||
)
|
||||
assert terminal["acquisition"]["state"] == "interrupted"
|
||||
assert terminal["acquisition"]["message_code"] == (
|
||||
"acquisition.recovery.device_standby_observed"
|
||||
)
|
||||
assert terminal["acquisition"]["result"] == {
|
||||
"receiver_stopped": True,
|
||||
"device_state": "scan_over",
|
||||
"device_stop": "not-sent",
|
||||
"automatic_command_retry": False,
|
||||
"read_only_recovery": True,
|
||||
"physical_reconciliation_required": True,
|
||||
}
|
||||
assert terminal["acquisition"]["cleanup_pending"] is False
|
||||
assert start_operation["status"] == "interrupted"
|
||||
assert start_operation["stage_code"] == (
|
||||
"physical-start-active-but-no-point-before-standby"
|
||||
)
|
||||
assert start_operation["error"]["side_effect_status"] == "succeeded"
|
||||
assert start_operation["error"]["automatic_replay_allowed"] is False
|
||||
assert camera_stop_calls == ["stop"]
|
||||
assert runtime.stop_calls == 1
|
||||
assert control.start_projects == start_projects_before == ["POWERLOSS001"]
|
||||
assert control.stop_calls == 0
|
||||
assert physical["status"] == "unresolved"
|
||||
assert physical["requires_reconciliation"] is True
|
||||
assert json.dumps(record, sort_keys=True) == physical_record_before
|
||||
|
||||
repeated = service.state()
|
||||
assert repeated["acquisition"] == terminal["acquisition"]
|
||||
assert repeated["physical_command"]["status"] == "unresolved"
|
||||
assert repeated["physical_command"]["requires_reconciliation"] is True
|
||||
assert camera_stop_calls == ["stop"]
|
||||
assert runtime.stop_calls == 1
|
||||
assert control.start_projects == ["POWERLOSS001"]
|
||||
assert control.stop_calls == 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"invalid_proof",
|
||||
[
|
||||
"active-owner",
|
||||
"application-response",
|
||||
"binding-generation",
|
||||
"qos2",
|
||||
"retained-target",
|
||||
],
|
||||
)
|
||||
def test_pre_active_scan_over_classifier_rejects_inexact_start_proof(
|
||||
tmp_path: Path,
|
||||
invalid_proof: str,
|
||||
) -> None:
|
||||
service, _runtime = service_with_fake_runtime(tmp_path)
|
||||
control = FakeInteractiveControlSession()
|
||||
service._application_control_session = control # type: ignore[assignment] # noqa: SLF001
|
||||
binding = _seed_supervised_connection(service)
|
||||
operation_id = "op-00000000-0000-4000-8000-000000009901"
|
||||
acquisition_id = "acq-00000000-0000-4000-8000-000000009901"
|
||||
physical = _exact_start_physical_proof(
|
||||
operation_id=operation_id,
|
||||
acquisition_id=acquisition_id,
|
||||
binding=binding,
|
||||
resolved=False,
|
||||
)
|
||||
physical.update(
|
||||
{
|
||||
"runtime_bound": True,
|
||||
"reconciliation_ready": True,
|
||||
"observed_session_state": "scan_over",
|
||||
"active_operation_id": operation_id,
|
||||
}
|
||||
)
|
||||
record = physical["record"]
|
||||
assert isinstance(record, dict)
|
||||
record["last_status"] = None
|
||||
verified_control = _verified_control_for_binding(binding)
|
||||
if invalid_proof == "active-owner":
|
||||
physical["active_operation_id"] = "op-other-start"
|
||||
elif invalid_proof == "application-response":
|
||||
response = record["application_response"]
|
||||
assert isinstance(response, dict)
|
||||
response["operation_id"] = "op-other-start"
|
||||
elif invalid_proof == "binding-generation":
|
||||
verified_control["producer_generation"] = 2
|
||||
elif invalid_proof == "qos2":
|
||||
record["qos2_completed"] = False
|
||||
else:
|
||||
record["last_status"] = {
|
||||
"session_state": "scanning",
|
||||
"project_bound": True,
|
||||
"init_ready": True,
|
||||
"mqtt_retained": True,
|
||||
"system_error_code": None,
|
||||
}
|
||||
|
||||
assert (
|
||||
service._matching_accepted_start_scan_over_before_active( # noqa: SLF001
|
||||
physical,
|
||||
acquisition_id=acquisition_id,
|
||||
start_operation_id=operation_id,
|
||||
verified_control=verified_control,
|
||||
)
|
||||
is False
|
||||
)
|
||||
|
||||
|
||||
def test_active_stream_recovery_reopens_exact_camera_epoch_once_and_fences_stale_lineage(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
|
||||
Reference in New Issue
Block a user