fix(k1): harden live handoff and camera recovery
This commit is contained in:
@@ -20,6 +20,7 @@ from k1link.device_plugins.xgrids_k1.protocol.normalizer import normalize_k1_mes
|
||||
from k1link.device_plugins.xgrids_k1.viewer.messages import StreamMessage
|
||||
from k1link.device_plugins.xgrids_k1.viewer.runtime import VisualizationRuntime
|
||||
from k1link.viewer.rerun_bridge import (
|
||||
LIVE_GRPC_BUFFER_LIMIT,
|
||||
RerunBridge,
|
||||
RerunSceneSettings,
|
||||
_live_time_panel,
|
||||
@@ -37,8 +38,10 @@ class FakeRecording:
|
||||
self.blueprints: list[object] = []
|
||||
self.disconnected = False
|
||||
self.flush_count = 0
|
||||
self.serve_grpc_options: dict[str, object] | None = None
|
||||
|
||||
def serve_grpc(self, **_: object) -> str:
|
||||
def serve_grpc(self, **options: object) -> str:
|
||||
self.serve_grpc_options = options
|
||||
return "rerun+http://127.0.0.1:9876/proxy"
|
||||
|
||||
def log(self, path: str, entity: object, *, static: bool = False) -> None:
|
||||
@@ -299,6 +302,10 @@ def test_legacy_points_and_pose_are_logged_to_rerun(
|
||||
"stream_time",
|
||||
}
|
||||
assert recording.flush_count == 1
|
||||
assert recording.serve_grpc_options is not None
|
||||
assert recording.serve_grpc_options["server_memory_limit"] == "32MiB"
|
||||
assert recording.serve_grpc_options["newest_first"] is False
|
||||
assert LIVE_GRPC_BUFFER_LIMIT == "32MiB"
|
||||
|
||||
bridge.close()
|
||||
assert recording.disconnected is True
|
||||
|
||||
@@ -11,6 +11,7 @@ import pytest
|
||||
from fastapi import APIRouter
|
||||
from fastapi.routing import APIRoute
|
||||
from pydantic import ValidationError
|
||||
from starlette.requests import Request
|
||||
|
||||
from k1link.web.runtime_diagnostics import (
|
||||
SCANNER_LOGGER_NAME,
|
||||
@@ -34,6 +35,13 @@ def _endpoint(router: APIRouter, path: str, method: str) -> Callable[..., Any]:
|
||||
raise AssertionError(f"{method} {path} route is missing")
|
||||
|
||||
|
||||
def _request(*, ui_build_id: str | None = None) -> Request:
|
||||
headers = []
|
||||
if ui_build_id is not None:
|
||||
headers.append((b"x-missioncore-ui-build", ui_build_id.encode("ascii")))
|
||||
return Request({"type": "http", "method": "GET", "path": "/", "headers": headers})
|
||||
|
||||
|
||||
def test_private_scanner_diagnostics_are_durable_structured_and_bounded(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
@@ -76,6 +84,11 @@ def test_private_scanner_diagnostics_are_durable_structured_and_bounded(
|
||||
"device_write_performed": False,
|
||||
"preferred_port": 9876,
|
||||
"selected_port": 9877,
|
||||
"camera_queue_bytes": 12_000_000,
|
||||
"camera_queue_segments": 96,
|
||||
"camera_retry_count": 3,
|
||||
"websocket_close_code": 4_008,
|
||||
"transport_epoch": 7,
|
||||
"unapproved_secret_field": "must-not-be-written",
|
||||
},
|
||||
)
|
||||
@@ -118,6 +131,11 @@ def test_private_scanner_diagnostics_are_durable_structured_and_bounded(
|
||||
assert document["device_write_performed"] is False
|
||||
assert document["preferred_port"] == 9876
|
||||
assert document["selected_port"] == 9877
|
||||
assert document["camera_queue_bytes"] == 12_000_000
|
||||
assert document["camera_queue_segments"] == 96
|
||||
assert document["camera_retry_count"] == 3
|
||||
assert document["websocket_close_code"] == 4_008
|
||||
assert document["transport_epoch"] == 7
|
||||
assert "unapproved_secret_field" not in document
|
||||
|
||||
parent = logging.getLogger(SCANNER_LOGGER_NAME)
|
||||
@@ -180,6 +198,40 @@ def test_live_viewer_diagnostic_endpoint_accepts_only_bounded_events(
|
||||
)
|
||||
assert fallback.failure_stage is None
|
||||
|
||||
camera_restart = LiveViewerDiagnosticEvent(
|
||||
schema_version="missioncore.live-viewer-diagnostic/v2",
|
||||
event_code="live_camera_transport_restart_requested",
|
||||
ui_build_id=expected_build,
|
||||
document_instance_id="00000000-0000-4000-8000-000000000001",
|
||||
viewer_instance_id="00000000-0000-4000-8000-000000000003",
|
||||
lifecycle_generation=2,
|
||||
failure_stage="camera-queue-capacity",
|
||||
stream_id="camera-preview-2",
|
||||
camera_queue_bytes=12_000_000,
|
||||
camera_queue_segments=96,
|
||||
camera_retry_count=3,
|
||||
websocket_close_code=4_008,
|
||||
transport_epoch=7,
|
||||
camera_append_error_name="InvalidStateError",
|
||||
camera_media_source_state="open",
|
||||
camera_video_error_code=3,
|
||||
)
|
||||
with caplog.at_level(
|
||||
logging.INFO,
|
||||
logger="k1link.device_plugins.xgrids_k1.viewer_receiver",
|
||||
):
|
||||
camera_response = endpoint(camera_restart)
|
||||
assert camera_response.status_code == 204
|
||||
assert caplog.records[-1].failure_stage == "camera-queue-capacity"
|
||||
assert caplog.records[-1].camera_queue_bytes == 12_000_000
|
||||
assert caplog.records[-1].camera_queue_segments == 96
|
||||
assert caplog.records[-1].camera_retry_count == 3
|
||||
assert caplog.records[-1].websocket_close_code == 4_008
|
||||
assert caplog.records[-1].transport_epoch == 7
|
||||
assert caplog.records[-1].camera_append_error_name == "InvalidStateError"
|
||||
assert caplog.records[-1].camera_media_source_state == "open"
|
||||
assert caplog.records[-1].camera_video_error_code == 3
|
||||
|
||||
|
||||
def test_live_viewer_diagnostic_rejects_stale_build_before_logging(
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
@@ -212,7 +264,7 @@ def test_live_viewer_client_contract_is_no_store_and_exact_build() -> None:
|
||||
router = build_viewer_diagnostics_router(expected_ui_build_id=lambda: expected_build)
|
||||
endpoint = _endpoint(router, "/api/v1/viewer/client-contract", "GET")
|
||||
|
||||
response = endpoint()
|
||||
response = endpoint(_request(ui_build_id=expected_build))
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.headers["cache-control"] == "no-store"
|
||||
@@ -225,11 +277,33 @@ def test_live_viewer_client_contract_is_no_store_and_exact_build() -> None:
|
||||
}
|
||||
|
||||
|
||||
def test_live_viewer_client_contract_logs_suppressed_stale_build_reload(
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
loaded_build = "/assets/index-abcdefgh.js"
|
||||
expected_build = "/assets/index-ijklmnop.js"
|
||||
router = build_viewer_diagnostics_router(expected_ui_build_id=lambda: expected_build)
|
||||
endpoint = _endpoint(router, "/api/v1/viewer/client-contract", "GET")
|
||||
|
||||
with caplog.at_level(
|
||||
logging.INFO,
|
||||
logger="k1link.device_plugins.xgrids_k1.viewer_receiver",
|
||||
):
|
||||
response = endpoint(_request(ui_build_id=loaded_build))
|
||||
|
||||
assert response.status_code == 200
|
||||
assert caplog.records[-1].event_code == "ui_build_drift_reload_suppressed"
|
||||
assert caplog.records[-1].ui_build_id == loaded_build
|
||||
assert caplog.records[-1].expected_ui_build_id == expected_build
|
||||
assert caplog.records[-1].device_write_performed is False
|
||||
assert caplog.records[-1].automatic_retry is False
|
||||
|
||||
|
||||
def test_live_viewer_client_contract_no_dist_is_retryable_without_reload_header() -> None:
|
||||
router = build_viewer_diagnostics_router(expected_ui_build_id=lambda: None)
|
||||
endpoint = _endpoint(router, "/api/v1/viewer/client-contract", "GET")
|
||||
|
||||
response = endpoint()
|
||||
response = endpoint(_request())
|
||||
|
||||
assert response.status_code == 503
|
||||
assert response.headers["cache-control"] == "no-store"
|
||||
|
||||
@@ -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"),
|
||||
[
|
||||
|
||||
@@ -126,12 +126,14 @@ def _connection(
|
||||
control_session_id: str,
|
||||
host_path_epoch: int,
|
||||
producer_generation: int,
|
||||
connection_mode: str = "bridge",
|
||||
target_ipv4: str = "192.168.68.52",
|
||||
) -> PhysicalCommandConnectionBinding:
|
||||
return PhysicalCommandConnectionBinding(
|
||||
intent_id="intent-checkpoint-integration",
|
||||
transport_ref="transport-checkpoint-integration",
|
||||
connection_mode="bridge",
|
||||
target_ipv4="192.168.68.52",
|
||||
connection_mode=connection_mode, # type: ignore[arg-type]
|
||||
target_ipv4=target_ipv4,
|
||||
target_port=1883,
|
||||
host_path_epoch=host_path_epoch,
|
||||
control_session_id=control_session_id,
|
||||
@@ -460,10 +462,22 @@ def test_repeated_reset_reopen_fresh_ready_ceases_old_active_checkpoint(
|
||||
(False, True),
|
||||
ids=("process-restart", "same-process-new-control-epoch"),
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"acknowledged_stop",
|
||||
(True, False),
|
||||
ids=("acknowledged-stop", "ambiguous-stop"),
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"cross_mode",
|
||||
(False, True),
|
||||
ids=("same-mode", "quick-to-bridge"),
|
||||
)
|
||||
def test_ready_after_dispatched_stop_ceases_active_checkpoint(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
same_process: bool,
|
||||
acknowledged_stop: bool,
|
||||
cross_mode: bool,
|
||||
) -> None:
|
||||
"""Fresh READY settles a dispatched STOP across either recovery boundary."""
|
||||
|
||||
@@ -472,6 +486,8 @@ def test_ready_after_dispatched_stop_ceases_active_checkpoint(
|
||||
control_session_id="checkpoint-dispatched-stop-original-control",
|
||||
host_path_epoch=1,
|
||||
producer_generation=1,
|
||||
connection_mode="quick-connect" if cross_mode else "bridge",
|
||||
target_ipv4="192.168.56.1" if cross_mode else "192.168.68.52",
|
||||
)
|
||||
_prepare_and_activate_checkpoint(service, original)
|
||||
ledger = service._physical_command_ledger # noqa: SLF001
|
||||
@@ -500,21 +516,22 @@ def test_ready_after_dispatched_stop_ceases_active_checkpoint(
|
||||
publish_call_returned=True,
|
||||
packet_id=42,
|
||||
)
|
||||
ledger.mark_qos2_completed(FIRST_STOP_OPERATION_ID, packet_id=42)
|
||||
ledger.record_application_response(
|
||||
FIRST_STOP_OPERATION_ID,
|
||||
PhysicalCommandApplicationResponse(
|
||||
operation_id=FIRST_STOP_OPERATION_ID,
|
||||
action="stop",
|
||||
control_session_id=original.control_session_id,
|
||||
host_path_epoch=original.host_path_epoch,
|
||||
producer_generation=original.producer_generation,
|
||||
result_code=PHYSICAL_COMMAND_APPLICATION_SUCCESS_CODE,
|
||||
success=True,
|
||||
payload_sha256="3" * 64,
|
||||
observed_at_utc="2026-08-13T12:01:01.000Z",
|
||||
),
|
||||
)
|
||||
if acknowledged_stop:
|
||||
ledger.mark_qos2_completed(FIRST_STOP_OPERATION_ID, packet_id=42)
|
||||
ledger.record_application_response(
|
||||
FIRST_STOP_OPERATION_ID,
|
||||
PhysicalCommandApplicationResponse(
|
||||
operation_id=FIRST_STOP_OPERATION_ID,
|
||||
action="stop",
|
||||
control_session_id=original.control_session_id,
|
||||
host_path_epoch=original.host_path_epoch,
|
||||
producer_generation=original.producer_generation,
|
||||
result_code=PHYSICAL_COMMAND_APPLICATION_SUCCESS_CODE,
|
||||
success=True,
|
||||
payload_sha256="3" * 64,
|
||||
observed_at_utc="2026-08-13T12:01:01.000Z",
|
||||
),
|
||||
)
|
||||
if not same_process:
|
||||
service._snapshot_runtime_id = ( # noqa: SLF001
|
||||
"snapshot-runtime-checkpoint-dispatched-stop-successor"
|
||||
@@ -527,6 +544,8 @@ def test_ready_after_dispatched_stop_ceases_active_checkpoint(
|
||||
# behind an ACTIVE checkpoint, as observed in the live Bridge flow.
|
||||
host_path_epoch=(original.host_path_epoch if same_process else 2),
|
||||
producer_generation=2,
|
||||
connection_mode="bridge",
|
||||
target_ipv4="192.168.68.52",
|
||||
)
|
||||
coordinator = service._physical_command_coordinator # noqa: SLF001
|
||||
coordinator.application_response(
|
||||
|
||||
@@ -537,6 +537,7 @@ def _cease_prepared_resolved_start_standby_kwargs(
|
||||
prepared_binding: ActiveAcquisitionRecoveryTransportBinding,
|
||||
*,
|
||||
terminal_state: str,
|
||||
origin_kind: str = "composite-resolved",
|
||||
) -> dict[str, Any]:
|
||||
binding = _reconciled_binding()
|
||||
status = _status(
|
||||
@@ -545,16 +546,21 @@ def _cease_prepared_resolved_start_standby_kwargs(
|
||||
evidence_session_id="evidence-restarted",
|
||||
observed_at="2026-08-13T12:00:03.000Z",
|
||||
)
|
||||
composite = origin_kind == "composite-resolved"
|
||||
physical = ActiveAcquisitionRecoveryPhysicalLineageProof(
|
||||
ledger_schema_version=ACTIVE_ACQUISITION_RECOVERY_PHYSICAL_LEDGER_SCHEMA,
|
||||
ledger_revision=21,
|
||||
proof_id="physical-composite-resolved-standby",
|
||||
proof_id=f"physical-{origin_kind}-standby",
|
||||
operation_id=START_OPERATION_ID,
|
||||
original_start_operation_id=START_OPERATION_ID,
|
||||
parent_operation_id=None,
|
||||
acquisition_id=ACQUISITION_ID,
|
||||
action="start",
|
||||
resolution="start-active-observed",
|
||||
resolution=(
|
||||
"start-active-observed"
|
||||
if composite
|
||||
else "physical-standby-observed"
|
||||
),
|
||||
payload_sha256=START_PAYLOAD_SHA256,
|
||||
original_start_payload_sha256=START_PAYLOAD_SHA256,
|
||||
reconciliation_kind="resolved-active-cessation",
|
||||
@@ -562,18 +568,22 @@ def _cease_prepared_resolved_start_standby_kwargs(
|
||||
status_message_sha256=status.status_message_sha256,
|
||||
observed_session_state=status.session_state,
|
||||
binding=binding,
|
||||
composite_complete=True,
|
||||
composite_complete=composite,
|
||||
edge_terminal=True,
|
||||
late_start_excluded=True,
|
||||
stop_fence="none",
|
||||
observed_at_utc=status.observed_at_utc,
|
||||
)
|
||||
origin = _origin(
|
||||
origin_kind="composite-resolved",
|
||||
origin_kind=origin_kind,
|
||||
prepared_binding=prepared_binding,
|
||||
physical_proof=physical,
|
||||
)
|
||||
assert origin.original_project_id_sha256 is not None
|
||||
settlement_project_id_sha256 = (
|
||||
origin.original_project_id_sha256
|
||||
or origin.reconciled_active_project_id_sha256
|
||||
)
|
||||
assert settlement_project_id_sha256 is not None
|
||||
return {
|
||||
"transition_id": (
|
||||
f"transition-cease-prepared-resolved-start-{terminal_state}"
|
||||
@@ -590,7 +600,7 @@ def _cease_prepared_resolved_start_standby_kwargs(
|
||||
origin.original_attempt_sha256
|
||||
),
|
||||
"reconciliation_original_project_id_sha256": (
|
||||
origin.original_project_id_sha256
|
||||
settlement_project_id_sha256
|
||||
),
|
||||
}
|
||||
|
||||
@@ -981,6 +991,52 @@ def test_cease_prepared_resolved_start_standby_is_terminal_without_activation(
|
||||
assert restarted.snapshot().checkpoint == ceased
|
||||
|
||||
|
||||
@pytest.mark.parametrize("terminal_state", ("ready", "scan_over"))
|
||||
def test_cease_prepared_ambiguous_active_start_standby_is_terminal_without_activation(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
terminal_state: str,
|
||||
) -> None:
|
||||
"""Two read-only proofs close an ambiguous START without inventing STOP/PCL."""
|
||||
|
||||
prepared_binding = _binding()
|
||||
store = _store(tmp_path, monkeypatch)
|
||||
prepared = _prepare(store, prepared_binding)
|
||||
kwargs = _cease_prepared_resolved_start_standby_kwargs(
|
||||
prepared_binding,
|
||||
terminal_state=terminal_state,
|
||||
origin_kind="ambiguous-reconciled",
|
||||
)
|
||||
|
||||
ceased = store.cease_prepared_resolved_start_standby(**kwargs)
|
||||
|
||||
assert ceased.state == "ceased"
|
||||
assert ceased.revision == prepared.revision + 1
|
||||
assert ceased.active_project_id_sha256 == ACTIVE_PROJECT_ID_SHA256
|
||||
assert ceased.activated_at_utc is None
|
||||
assert ceased.activation_status_proof is None
|
||||
assert ceased.activation_physical_proof is None
|
||||
assert ceased.current_active_status_proof is None
|
||||
assert ceased.current_active_physical_proof is None
|
||||
assert ceased.first_published_pcl_proof is None
|
||||
assert ceased.reconciled_start_origin_proof is not None
|
||||
assert ceased.reconciled_start_origin_proof.origin_kind == (
|
||||
"ambiguous-reconciled"
|
||||
)
|
||||
assert ceased.reconciled_start_origin_proof.original_project_id_sha256 is None
|
||||
assert ceased.reconciled_start_origin_proof.reconciled_active_project_id_sha256 == (
|
||||
ACTIVE_PROJECT_ID_SHA256
|
||||
)
|
||||
assert ceased.cessation_status_proof == kwargs["cessation_status_proof"]
|
||||
assert ceased.cessation_physical_proof == kwargs["cessation_physical_proof"]
|
||||
assert (
|
||||
ActiveAcquisitionRecoveryCheckpointStore(tmp_path / "repository")
|
||||
.snapshot()
|
||||
.checkpoint
|
||||
== ceased
|
||||
)
|
||||
|
||||
|
||||
def test_cease_prepared_resolved_start_standby_rejects_inexact_restart_proofs(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
|
||||
@@ -801,6 +801,16 @@ def test_preview_queue_enforces_exact_fragment_byte_and_age_bounds() -> None:
|
||||
assert age_bounded.offer(("media", b"too-old")) is False
|
||||
|
||||
|
||||
def test_preview_queue_survives_measured_ui_pause() -> None:
|
||||
now = [100.0]
|
||||
preview_queue = _CameraPreviewSegmentQueue(clock=lambda: now[0])
|
||||
|
||||
assert preview_queue.offer(("media", b"first")) is True
|
||||
now[0] += 3.2
|
||||
|
||||
assert preview_queue.offer(("media", b"next")) is True
|
||||
|
||||
|
||||
def test_camera_router_closes_lagging_preview_with_explicit_private_code(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
|
||||
@@ -470,6 +470,63 @@ def test_reset_attempt_cutoff_uses_operation_identity_not_local_stage_sequence(
|
||||
assert projected["status"] == "failed"
|
||||
|
||||
|
||||
def test_quick_connect_unknown_write_can_reset_to_bridge_without_device_restart(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
service = _service(monkeypatch, tmp_path)
|
||||
operation_id = "op-00000000-0000-4000-8000-000000000103"
|
||||
journal = service._require_network_provisioning_idempotency_journal() # noqa: SLF001
|
||||
admitted = journal.begin(
|
||||
idempotency_key="network-provision:quick-to-bridge-reset",
|
||||
action=facade_module.ACTION_NETWORK_PROVISION,
|
||||
operation_id=operation_id,
|
||||
request_binding_sha256=hashlib.sha256(b"quick-to-bridge-reset").hexdigest(),
|
||||
)
|
||||
journal.mark_unresolved(
|
||||
operation_id,
|
||||
expected_revision=admitted.record.revision,
|
||||
)
|
||||
prepared = service._network_mutation_ledger.prepare( # noqa: SLF001
|
||||
operation_id=operation_id,
|
||||
transport_ref="RESET-QUICK-K1-UUID",
|
||||
intended_mode="quick-connect",
|
||||
write_mode="with_response",
|
||||
baseline_status=NetworkStatusEvidence(
|
||||
mode="WIFI_AP",
|
||||
ipv4="192.168.56.1",
|
||||
status_code=1,
|
||||
reserved=0,
|
||||
),
|
||||
)
|
||||
service._network_mutation_ledger.mark_dispatching( # noqa: SLF001
|
||||
operation_id,
|
||||
expected_revision=prepared.revision,
|
||||
)
|
||||
|
||||
reset = service.select_connection_mode(
|
||||
_reset(
|
||||
mode="bridge",
|
||||
revision=0,
|
||||
reset_id="op-reset-quick-to-bridge-without-device-restart-01",
|
||||
)
|
||||
)
|
||||
|
||||
assert reset["desired_connection_mode"] == "bridge"
|
||||
assert reset["desired_connection_mode_revision"] == 1
|
||||
assert reset["connection_scenario_reset"]["network_write_performed"] is False
|
||||
network_record = service._network_mutation_ledger.snapshot().record # noqa: SLF001
|
||||
assert network_record is not None
|
||||
assert network_record.resolution == "superseded"
|
||||
terminal_record = journal.snapshot().records[-1]
|
||||
assert terminal_record.operation_id == operation_id
|
||||
assert terminal_record.stage == "terminal"
|
||||
assert terminal_record.terminal is not None
|
||||
assert terminal_record.terminal.outcome == "cancelled"
|
||||
assert terminal_record.terminal.side_effect_status == "unknown"
|
||||
assert terminal_record.terminal.safe_to_retry is False
|
||||
|
||||
|
||||
def test_scenario_reset_preserves_physical_audit_and_unrelated_plugin_state(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
tmp_path: Path,
|
||||
|
||||
Reference in New Issue
Block a user