Безопасное завершение калибровки при отключении K1
This commit is contained in:
@@ -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