fix(k1): harden live handoff and camera recovery
This commit is contained in:
@@ -19833,6 +19833,8 @@ def test_quick_connect_activates_the_device_ap_then_associates_the_host(
|
||||
assert state["connection_mode"] == "quick-connect"
|
||||
assert state["k1_ip"] == "192.168.56.1"
|
||||
assert state["compatibility"]["attestation"]["topology"] == "device-ap"
|
||||
assert state["last_operation"]["result"]["host_wifi_association_performed"] is True
|
||||
assert state["last_operation"]["result"]["host_wifi_association_outcome"] == "associated"
|
||||
quick_sessions = sorted(service.evidence_root.glob("*viewer_k1_ap_association*"))
|
||||
assert len(quick_sessions) == 1
|
||||
assert not (quick_sessions[0] / "provisioning.sensitive.json").exists()
|
||||
@@ -19843,6 +19845,8 @@ def test_quick_connect_activates_the_device_ap_then_associates_the_host(
|
||||
assert '"credentials_resolved_by_plugin": true' in redacted_manifest
|
||||
assert "credential_provider_id" in redacted_manifest
|
||||
assert "device_ap_activation_profile_id" in redacted_manifest
|
||||
assert '"host_wifi_association_performed": true' in redacted_manifest
|
||||
assert '"host_wifi_association_outcome": "associated"' in redacted_manifest
|
||||
assert (quick_sessions[0] / "ap-activation.redacted.json").exists()
|
||||
assert (
|
||||
service._camera_target_for_session( # noqa: SLF001
|
||||
@@ -29487,6 +29491,114 @@ _RECOVERY_ACQUISITION_ID = "acquisition-persisted-active-k1"
|
||||
_RECOVERY_START_OPERATION_ID = "physical-start-persisted-active-k1"
|
||||
|
||||
|
||||
def _persist_ambiguous_start_with_prepared_checkpoint(
|
||||
service: XgridsK1CompatibilityService,
|
||||
) -> None:
|
||||
"""Persist the exact pre-crash shape: PREPARED token + ambiguous START."""
|
||||
|
||||
identity = PhysicalCommandIdentity(
|
||||
vendor_device_id_sha256=_RECOVERY_VENDOR_HASH,
|
||||
device_serial_sha256=_RECOVERY_SERIAL_HASH,
|
||||
)
|
||||
connection = PhysicalCommandConnectionBinding(
|
||||
intent_id="ambiguous-start-intent",
|
||||
transport_ref="test-ble-transport",
|
||||
connection_mode="bridge",
|
||||
target_ipv4="192.168.68.52",
|
||||
target_port=facade_module.CONTROL_MQTT_PORT,
|
||||
host_path_epoch=1,
|
||||
control_session_id="ambiguous-start-control",
|
||||
producer_generation=1,
|
||||
)
|
||||
observed_at_utc = datetime.now(UTC).isoformat(timespec="milliseconds").replace(
|
||||
"+00:00",
|
||||
"Z",
|
||||
)
|
||||
baseline = PhysicalCommandStatusEvidence(
|
||||
source="live-control-session",
|
||||
vendor_device_id_sha256=_RECOVERY_VENDOR_HASH,
|
||||
device_serial_sha256=_RECOVERY_SERIAL_HASH,
|
||||
control_session_id=connection.control_session_id,
|
||||
host_path_epoch=connection.host_path_epoch,
|
||||
producer_generation=connection.producer_generation,
|
||||
session_state="ready",
|
||||
session_state_code=300,
|
||||
project_bound=False,
|
||||
project_id_sha256=None,
|
||||
init_ready=False,
|
||||
status_message_sha256="1" * 64,
|
||||
mqtt_retained=False,
|
||||
observed_at_utc=observed_at_utc,
|
||||
)
|
||||
operation_id = "physical-start-persisted-ambiguous-k1"
|
||||
acquisition_id = "acquisition-persisted-ambiguous-k1"
|
||||
payload_sha256 = "2" * 64
|
||||
ledger = service._physical_command_ledger # noqa: SLF001
|
||||
ledger.prepare(
|
||||
operation_id=operation_id,
|
||||
parent_operation_id=None,
|
||||
acquisition_id=acquisition_id,
|
||||
action="start",
|
||||
identity=identity,
|
||||
connection=connection,
|
||||
compatibility_profile_id=XGRIDS_K1_COMPATIBILITY_PROFILE_ID,
|
||||
payload_sha256=payload_sha256,
|
||||
baseline_status=baseline,
|
||||
)
|
||||
store = service._active_acquisition_checkpoint # noqa: SLF001
|
||||
assert store is not None
|
||||
store.prepare(
|
||||
transition_id="prepare-persisted-ambiguous-start",
|
||||
predecessor_revision=0,
|
||||
acquisition_id=acquisition_id,
|
||||
original_start_operation_id=operation_id,
|
||||
start_payload_sha256=payload_sha256,
|
||||
identity=ActiveAcquisitionRecoveryIdentity(
|
||||
logical_device_id="known-k1",
|
||||
vendor_device_id_sha256=_RECOVERY_VENDOR_HASH,
|
||||
device_serial_sha256=_RECOVERY_SERIAL_HASH,
|
||||
),
|
||||
connection=ActiveAcquisitionRecoveryConnection(
|
||||
transport_ref=connection.transport_ref,
|
||||
connection_mode=connection.connection_mode,
|
||||
target_ipv4=connection.target_ipv4,
|
||||
target_port=connection.target_port,
|
||||
),
|
||||
compatibility_profile_id=XGRIDS_K1_COMPATIBILITY_PROFILE_ID,
|
||||
project_name="AMBIGUOUS_RECOVERY",
|
||||
project_name_wire_sha256=active_acquisition_project_name_sha256(
|
||||
"AMBIGUOUS_RECOVERY"
|
||||
),
|
||||
original_evidence_session_id="evidence-before-crash",
|
||||
duration_seconds=None,
|
||||
requested_streams=("spatial.point-cloud.live", "camera.rgb.live"),
|
||||
evidence_policy="required",
|
||||
mount_type="handheld",
|
||||
gnss_mode="none",
|
||||
prepared_binding=ActiveAcquisitionRecoveryTransportBinding(
|
||||
runtime_instance_id="runtime-before-crash",
|
||||
intent_id=connection.intent_id,
|
||||
transport_ref=connection.transport_ref,
|
||||
connection_mode=connection.connection_mode,
|
||||
target_ipv4=connection.target_ipv4,
|
||||
target_port=connection.target_port,
|
||||
host_path_epoch=connection.host_path_epoch,
|
||||
control_session_id=connection.control_session_id,
|
||||
producer_generation=connection.producer_generation,
|
||||
logical_device_id="known-k1",
|
||||
compatibility_profile_id=XGRIDS_K1_COMPATIBILITY_PROFILE_ID,
|
||||
vendor_device_id_sha256=_RECOVERY_VENDOR_HASH,
|
||||
device_serial_sha256=_RECOVERY_SERIAL_HASH,
|
||||
),
|
||||
)
|
||||
ledger.mark_dispatching(operation_id)
|
||||
ledger.mark_observing(
|
||||
operation_id,
|
||||
publish_call_returned=True,
|
||||
packet_id=41,
|
||||
)
|
||||
|
||||
|
||||
def _persist_resolved_active_start_for_restart(
|
||||
service: XgridsK1CompatibilityService,
|
||||
) -> None:
|
||||
@@ -30448,6 +30560,54 @@ def test_explicit_verify_reconciles_scanning_as_active_without_unblocking_mode_c
|
||||
}
|
||||
|
||||
|
||||
def test_explicit_verify_scanning_checkpoint_rehydrate_failure_keeps_stop_only(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""A rejected local checkpoint cannot erase proven physical STOP authority."""
|
||||
|
||||
service, runtime = service_with_fake_runtime(tmp_path)
|
||||
coordinator = _VerifyPhysicalRecoveryCoordinator(
|
||||
observed_session_state="scanning",
|
||||
reconciliation_ready=True,
|
||||
)
|
||||
_install_synthetic_verify_recovery(service, coordinator)
|
||||
rehydrate_calls: list[str] = []
|
||||
|
||||
async def reject_receiver_rehydration(**_: object) -> None:
|
||||
rehydrate_calls.append("rejected")
|
||||
raise facade_module.ActiveAcquisitionRecoveryCheckpointError(
|
||||
"synthetic stale prepared checkpoint"
|
||||
)
|
||||
|
||||
monkeypatch.setattr(
|
||||
service,
|
||||
"_rehydrate_active_acquisition_after_restart",
|
||||
reject_receiver_rehydration,
|
||||
)
|
||||
operation_id = "op-00000000-0000-4000-8000-000000001217"
|
||||
|
||||
state = asyncio.run(
|
||||
service.verify_connection(
|
||||
_retained_physical_recovery_verify_request(operation_id=operation_id)
|
||||
)
|
||||
)
|
||||
|
||||
assert rehydrate_calls == ["rejected"]
|
||||
assert state["last_operation"]["status"] == "succeeded"
|
||||
assert state["physical_command"]["record"]["resolution"] == (
|
||||
"physical-active-observed"
|
||||
)
|
||||
assert state["acquisition"]["acquisition_id"] == "acq-persisted-start"
|
||||
assert state["acquisition"]["state"] == "failed"
|
||||
assert state["acquisition"]["result"]["recovery_only"] is True
|
||||
assert state["application_control_session"]["state"] == "scanning"
|
||||
assert state["application_control_session"]["can_stop"] is True
|
||||
assert state["connection_policy"]["actions"]["stop-acquisition"]["allowed"] is True
|
||||
assert runtime.start_calls == []
|
||||
assert runtime.stop_calls == 0
|
||||
|
||||
|
||||
def test_explicit_verify_scanning_cleans_terminal_camera_residual_for_stop_only_shell(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
tmp_path: Path,
|
||||
@@ -30934,6 +31094,113 @@ def test_active_reconciliation_survives_post_commit_adoption_failure_and_next_re
|
||||
]["mode_selection"]
|
||||
|
||||
|
||||
def test_prepared_ambiguous_start_active_then_ready_settles_checkpoint_without_command(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""Fresh SCANNING then READY closes the pre-crash token without replay."""
|
||||
|
||||
service, runtime = service_with_fake_runtime(tmp_path)
|
||||
_persist_ambiguous_start_with_prepared_checkpoint(service)
|
||||
coordinator = service._physical_command_coordinator # noqa: SLF001
|
||||
observed_base = datetime.now(UTC) + timedelta(seconds=1)
|
||||
|
||||
def bind_fresh_status(
|
||||
*,
|
||||
suffix: str,
|
||||
generation: int,
|
||||
session_state: str,
|
||||
) -> None:
|
||||
observed_at_utc = (
|
||||
observed_base + timedelta(seconds=generation)
|
||||
).isoformat(timespec="milliseconds").replace("+00:00", "Z")
|
||||
coordinator.application_response(
|
||||
ApplicationMqttResponseEvidence(
|
||||
operation_key=f"bootstrap:{suffix}:DeviceInfoRequest",
|
||||
response_topic="lixel/application/response/device_info",
|
||||
payload_sha256=hashlib.sha256(
|
||||
f"device-info:{suffix}".encode()
|
||||
).hexdigest(),
|
||||
modeling_action=None,
|
||||
result_code=None,
|
||||
success=None,
|
||||
observed_at_utc=observed_at_utc,
|
||||
)
|
||||
)
|
||||
coordinator.bind_control_session(
|
||||
PhysicalCommandRuntimeBinding(
|
||||
vendor_device_id_sha256=_RECOVERY_VENDOR_HASH,
|
||||
device_serial_sha256=_RECOVERY_SERIAL_HASH,
|
||||
compatibility_profile_id=XGRIDS_K1_COMPATIBILITY_PROFILE_ID,
|
||||
intent_id=f"recovery-{suffix}-intent",
|
||||
transport_ref="test-ble-transport",
|
||||
connection_mode="bridge",
|
||||
target_ipv4="192.168.68.52",
|
||||
target_port=facade_module.CONTROL_MQTT_PORT,
|
||||
host_path_epoch=generation,
|
||||
control_session_id=f"recovery-{suffix}-control",
|
||||
producer_generation=generation,
|
||||
)
|
||||
)
|
||||
scanning = session_state == "scanning"
|
||||
coordinator.device_status(
|
||||
ApplicationMqttDeviceStatusEvidence(
|
||||
vendor_device_id_sha256=_RECOVERY_VENDOR_HASH,
|
||||
device_serial_sha256=_RECOVERY_SERIAL_HASH,
|
||||
session_state=session_state, # type: ignore[arg-type]
|
||||
session_state_code=MODELING_STATE_BASE + (302 if scanning else 300),
|
||||
project_bound=scanning,
|
||||
project_id_sha256=_RECOVERY_PROJECT_HASH if scanning else None,
|
||||
init_ready=scanning,
|
||||
status_message_sha256=hashlib.sha256(
|
||||
f"status:{suffix}:{session_state}".encode()
|
||||
).hexdigest(),
|
||||
mqtt_retained=False,
|
||||
observed_at_utc=observed_at_utc,
|
||||
)
|
||||
)
|
||||
|
||||
bind_fresh_status(suffix="active", generation=2, session_state="scanning")
|
||||
active_record = coordinator.reconcile_unresolved(
|
||||
reconciliation_id="reconcile-ambiguous-active"
|
||||
)
|
||||
assert active_record["resolution"] == "physical-active-observed"
|
||||
token = service._validate_active_acquisition_checkpoint_lineage() # noqa: SLF001
|
||||
assert token is not None and token.checkpoint_state == "prepared"
|
||||
|
||||
bind_fresh_status(suffix="ready", generation=3, session_state="ready")
|
||||
standby_record = coordinator.reconcile_resolved_active(
|
||||
reconciliation_id="reconcile-ambiguous-ready"
|
||||
)
|
||||
assert standby_record["resolution"] == "physical-standby-observed"
|
||||
assert standby_record["reconciliations"][-1]["kind"] == (
|
||||
"resolved-active-cessation"
|
||||
)
|
||||
|
||||
settled = service._settle_restart_checkpoint_after_verified_standby( # noqa: SLF001
|
||||
token=token,
|
||||
reconciliation_id="reconcile-ambiguous-ready",
|
||||
reconciled_record=standby_record,
|
||||
)
|
||||
|
||||
assert settled is True
|
||||
store = service._active_acquisition_checkpoint # noqa: SLF001
|
||||
assert store is not None
|
||||
checkpoint = store.snapshot().checkpoint
|
||||
assert checkpoint is not None and checkpoint.state == "ceased"
|
||||
assert checkpoint.active_project_id_sha256 == _RECOVERY_PROJECT_HASH
|
||||
assert checkpoint.activated_at_utc is None
|
||||
assert checkpoint.first_published_pcl_proof is None
|
||||
assert checkpoint.reconciled_start_origin_proof is not None
|
||||
assert checkpoint.reconciled_start_origin_proof.origin_kind == (
|
||||
"ambiguous-reconciled"
|
||||
)
|
||||
assert checkpoint.reconciled_start_origin_proof.reconciled_active_project_id_sha256 == (
|
||||
_RECOVERY_PROJECT_HASH
|
||||
)
|
||||
assert runtime.start_calls == []
|
||||
assert runtime.stop_calls == 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("proof_kind", "observed_session_state"),
|
||||
[
|
||||
|
||||
Reference in New Issue
Block a user