from __future__ import annotations import asyncio from pathlib import Path from typing import Any import pytest from pydantic import SecretStr import k1link.web.xgrids_k1_facade as facade_module from k1link.web.xgrids_k1_facade import ( DEFAULT_LIVE_STREAMS, XGRIDS_K1_COMPATIBILITY_PROFILE_ID, AbortAcquisitionRequest, CompatibilityAttestationRequest, ConnectRequest, PrepareAcquisitionRequest, StartAcquisitionRequest, StopAcquisitionRequest, XgridsK1CompatibilityService, ) ATTESTATION = CompatibilityAttestationRequest( firmware_version="3.0.2", topology="direct-lan", operator_confirmed=True, ) PRIMARY_TEST_CREDENTIAL = "x" * 24 SECONDARY_TEST_CREDENTIAL = "y" * 24 class FakeVisualizationRuntime: def __init__(self) -> None: self.phase = "idle" self.source_mode = "idle" self.source_ready = False self.pcl_frames = 0 self.start_calls: list[tuple[str, Path, float]] = [] self.stop_calls = 0 self.stop_error: Exception | None = None def snapshot(self) -> dict[str, Any]: return { "phase": self.phase, "message": "test runtime", "source_mode": self.source_mode, "source_ready": self.source_ready, "foxglove_ws_url": None, "foxglove_viewer_url": None, "rerun_grpc_url": None, "viewer_settings": {}, "metrics": { "messages_received": self.pcl_frames, "payload_bytes": 0, "pcl_frames": self.pcl_frames, "pose_frames": 0, "points_published": 0, "last_point_count": 0, "decode_errors": 0, "preview_dropped": 0, "pcl_fps": 0.0, "pose_fps": 0.0, "mqtt_to_publish_ms": None, "mqtt_to_publish_p50_ms": None, "mqtt_to_publish_p95_ms": None, "decode_publish_ms": None, "trajectory_poses": 0, }, } def start_live(self, host: str, out_dir: Path, *, duration_seconds: float) -> None: self.start_calls.append((host, out_dir, duration_seconds)) self.phase = "starting_live" self.source_mode = "live" def mark_ready(self) -> None: self.phase = "live" self.source_ready = True def stop(self) -> None: self.stop_calls += 1 if self.stop_error is not None: raise self.stop_error self.phase = "idle" self.source_mode = "idle" self.source_ready = False def service_with_fake_runtime( tmp_path: Path, ) -> tuple[XgridsK1CompatibilityService, FakeVisualizationRuntime]: service = XgridsK1CompatibilityService(tmp_path) runtime = FakeVisualizationRuntime() service.runtime = runtime # type: ignore[assignment] return service, runtime def test_prepare_creates_provisional_device_session_and_profiled_acquisition( tmp_path: Path, ) -> None: service, runtime = service_with_fake_runtime(tmp_path) state = service.prepare_acquisition( PrepareAcquisitionRequest( host="192.168.1.20", duration_seconds=60, compatibility_attestation=ATTESTATION, ) ) assert runtime.start_calls == [] assert state["device_ref"]["identity_stability"] == "provisional" assert state["device_ref"]["device_id"] != state["device_session"]["device_session_id"] assert state["acquisition"]["state"] == "prepared" assert state["acquisition"]["compatibility_profile_id"] == (XGRIDS_K1_COMPATIBILITY_PROFILE_ID) assert state["compatibility"]["vendor_writes_enabled"] is False def test_operator_manual_start_is_confirmed_only_by_real_point_data(tmp_path: Path) -> None: service, runtime = service_with_fake_runtime(tmp_path) prepared = service.prepare_acquisition( PrepareAcquisitionRequest( host="192.168.1.20", duration_seconds=60, compatibility_attestation=ATTESTATION, ) ) acquisition_id = prepared["acquisition"]["acquisition_id"] starting = service.start_acquisition(StartAcquisitionRequest(acquisition_id=acquisition_id)) assert starting["acquisition"]["state"] == "starting" assert starting["last_operation"]["status"] == "running" runtime.mark_ready() awaiting = service.state() assert awaiting["acquisition"]["state"] == "awaiting_external_start" assert awaiting["last_operation"]["status"] == "operator_action_required" assert len(runtime.start_calls) == 1 runtime.pcl_frames = 1 acquiring = service.state() assert acquiring["acquisition"]["state"] == "acquiring" assert acquiring["last_operation"]["status"] == "succeeded" assert acquiring["last_operation"]["result"]["confirmation"] == "point-frame" def test_receiver_completion_without_point_data_fails_start_operation(tmp_path: Path) -> None: service, runtime = service_with_fake_runtime(tmp_path) prepared = service.prepare_acquisition( PrepareAcquisitionRequest( host="192.168.1.20", duration_seconds=60, compatibility_attestation=ATTESTATION, ) ) acquisition_id = prepared["acquisition"]["acquisition_id"] service.start_acquisition(StartAcquisitionRequest(acquisition_id=acquisition_id)) runtime.phase = "idle" runtime.source_mode = "idle" failed = service.state() assert failed["acquisition"]["state"] == "failed" assert failed["last_operation"]["status"] == "failed" assert failed["acquisition"]["result"]["device_state"] == "unknown" def test_capture_only_stop_never_claims_that_physical_k1_stopped(tmp_path: Path) -> None: service, runtime = service_with_fake_runtime(tmp_path) prepared = service.prepare_acquisition( PrepareAcquisitionRequest( host="192.168.1.20", duration_seconds=60, compatibility_attestation=ATTESTATION, ) ) acquisition_id = prepared["acquisition"]["acquisition_id"] service.start_acquisition(StartAcquisitionRequest(acquisition_id=acquisition_id)) stopped = service.stop_acquisition( StopAcquisitionRequest(acquisition_id=acquisition_id, mode="capture-only") ) assert runtime.stop_calls == 1 assert stopped["acquisition"]["state"] == "completed" assert stopped["acquisition"]["result"] == { "receiver_stopped": True, "device_stop": "unknown", } operations = {item["action"]: item for item in stopped["operations"]} assert operations["acquisition.start"]["status"] == "cancelled" assert operations["acquisition.stop"]["status"] == "succeeded" def test_graceful_stop_waits_for_explicit_operator_confirmation(tmp_path: Path) -> None: service, runtime = service_with_fake_runtime(tmp_path) prepared = service.prepare_acquisition( PrepareAcquisitionRequest( host="192.168.1.20", duration_seconds=60, compatibility_attestation=ATTESTATION, ) ) acquisition_id = prepared["acquisition"]["acquisition_id"] service.start_acquisition(StartAcquisitionRequest(acquisition_id=acquisition_id)) runtime.mark_ready() service.state() runtime.pcl_frames = 1 acquiring = service.state() assert acquiring["acquisition"]["state"] == "acquiring" awaiting = service.stop_acquisition( StopAcquisitionRequest(acquisition_id=acquisition_id, mode="graceful") ) operation_id = awaiting["last_operation"]["operation_id"] assert runtime.stop_calls == 0 assert awaiting["acquisition"]["state"] == "awaiting_external_stop" assert awaiting["last_operation"]["status"] == "operator_action_required" retried = service.stop_acquisition( StopAcquisitionRequest( acquisition_id=acquisition_id, operation_id=operation_id, mode="graceful", ) ) assert retried["acquisition"]["state"] == "awaiting_external_stop" assert retried["last_operation"]["operation_id"] == operation_id assert retried["last_operation"]["status"] == "operator_action_required" with pytest.raises(ValueError, match="исходную stop-operation"): service.stop_acquisition( StopAcquisitionRequest( acquisition_id=acquisition_id, mode="graceful", operator_confirmed=True, ) ) completed = service.stop_acquisition( StopAcquisitionRequest( acquisition_id=acquisition_id, operation_id=operation_id, mode="graceful", operator_confirmed=True, ) ) assert runtime.stop_calls == 1 assert completed["acquisition"]["result"]["device_stop"] == "operator-confirmed" stop_operations = [ item for item in completed["operations"] if item["action"] == "acquisition.stop" ] assert len(stop_operations) == 1 assert stop_operations[0]["operation_id"] == operation_id assert stop_operations[0]["status"] == "succeeded" def test_graceful_stop_retry_by_idempotency_key_reuses_original_operation( tmp_path: Path, ) -> None: service, runtime = service_with_fake_runtime(tmp_path) prepared = service.prepare_acquisition( PrepareAcquisitionRequest( host="192.168.1.20", compatibility_attestation=ATTESTATION, ) ) acquisition_id = prepared["acquisition"]["acquisition_id"] service.start_acquisition(StartAcquisitionRequest(acquisition_id=acquisition_id)) runtime.mark_ready() service.state() runtime.pcl_frames = 1 service.state() first = service.stop_acquisition( StopAcquisitionRequest( acquisition_id=acquisition_id, idempotency_key="graceful-stop-once", mode="graceful", ) ) first_operation_id = first["last_operation"]["operation_id"] retried = service.stop_acquisition( StopAcquisitionRequest( acquisition_id=acquisition_id, idempotency_key="graceful-stop-once", mode="graceful", ) ) assert retried["last_operation"]["operation_id"] == first_operation_id assert retried["last_operation"]["status"] == "operator_action_required" assert ( len([item for item in retried["operations"] if item["action"] == "acquisition.stop"]) == 1 ) def test_unrelated_graceful_stop_is_rejected_while_confirmation_is_pending( tmp_path: Path, ) -> None: service, runtime = service_with_fake_runtime(tmp_path) prepared = service.prepare_acquisition( PrepareAcquisitionRequest( host="192.168.1.20", compatibility_attestation=ATTESTATION, ) ) acquisition_id = prepared["acquisition"]["acquisition_id"] service.start_acquisition(StartAcquisitionRequest(acquisition_id=acquisition_id)) runtime.mark_ready() service.state() runtime.pcl_frames = 1 service.state() first = service.stop_acquisition( StopAcquisitionRequest(acquisition_id=acquisition_id, mode="graceful") ) expected_operation_id = first["last_operation"]["operation_id"] with pytest.raises(ValueError, match="уже ожидает"): service.stop_acquisition( StopAcquisitionRequest( acquisition_id=acquisition_id, idempotency_key="unrelated-stop", mode="graceful", ) ) original = next( item for item in service.state()["operations"] if item["operation_id"] == expected_operation_id ) assert original["status"] == "operator_action_required" def test_graceful_stop_is_rejected_until_point_data_confirms_acquisition( tmp_path: Path, ) -> None: service, runtime = service_with_fake_runtime(tmp_path) prepared = service.prepare_acquisition( PrepareAcquisitionRequest( host="192.168.1.20", compatibility_attestation=ATTESTATION, ) ) acquisition_id = prepared["acquisition"]["acquisition_id"] service.start_acquisition(StartAcquisitionRequest(acquisition_id=acquisition_id)) with pytest.raises(ValueError, match="подтверждённого потока point cloud"): service.stop_acquisition( StopAcquisitionRequest(acquisition_id=acquisition_id, mode="graceful") ) state = service.state() assert runtime.stop_calls == 0 assert state["acquisition"]["state"] == "starting" assert state["last_operation"]["action"] == "acquisition.start" assert state["last_operation"]["status"] == "running" @pytest.mark.parametrize("evidence_policy", ["best-effort", "disabled"]) def test_prepare_rejects_unsupported_evidence_policies( tmp_path: Path, evidence_policy: str, ) -> None: service, _ = service_with_fake_runtime(tmp_path) with pytest.raises(ValueError, match="evidence_policy=required"): service.prepare_acquisition( PrepareAcquisitionRequest( host="192.168.1.20", evidence_policy=evidence_policy, # type: ignore[arg-type] compatibility_attestation=ATTESTATION, ) ) assert service.state()["compatibility"]["profile_id"] is None @pytest.mark.parametrize( "requested_streams", [ DEFAULT_LIVE_STREAMS[:-1], (*DEFAULT_LIVE_STREAMS, DEFAULT_LIVE_STREAMS[0]), ], ) def test_prepare_rejects_stream_subsets_and_duplicates( tmp_path: Path, requested_streams: tuple[str, ...], ) -> None: service, _ = service_with_fake_runtime(tmp_path) with pytest.raises(ValueError, match="полный проверенный набор"): service.prepare_acquisition( PrepareAcquisitionRequest( host="192.168.1.20", requested_streams=requested_streams, # type: ignore[arg-type] compatibility_attestation=ATTESTATION, ) ) def test_exact_profile_is_inactive_until_explicit_operator_attestation( tmp_path: Path, ) -> None: service, _ = service_with_fake_runtime(tmp_path) initial = service.state() assert initial["compatibility"] == { "profile_id": None, "decision": "unknown", "permitted_mode": "evidence-only", "firmware_claim": "exact-3.0.2-profile-not-attested", "attestation": None, "vendor_writes_enabled": False, "camera_preview": "unverified", } assert initial["device_calibration"]["compatibility_profile_id"] is None attested = service.prepare_acquisition( PrepareAcquisitionRequest( host="192.168.1.20", compatibility_attestation=ATTESTATION, ) ) assert attested["compatibility"]["profile_id"] == XGRIDS_K1_COMPATIBILITY_PROFILE_ID assert attested["compatibility"]["decision"] == "limited" assert attested["compatibility"]["attestation"]["basis"] == "operator-attested" assert attested["device_session"]["compatibility_profile_id"] == ( XGRIDS_K1_COMPATIBILITY_PROFILE_ID ) def test_prepare_rejects_device_ap_fallback_as_direct_lan_target(tmp_path: Path) -> None: service, _ = service_with_fake_runtime(tmp_path) with pytest.raises(ValueError, match="точки доступа"): service.prepare_acquisition( PrepareAcquisitionRequest( host="192.168.56.1", compatibility_attestation=ATTESTATION, ) ) state = service.state() assert state["device_ref"] is None assert state["compatibility"]["profile_id"] is None def test_runtime_error_terminalizes_pending_graceful_stop(tmp_path: Path) -> None: service, runtime = service_with_fake_runtime(tmp_path) prepared = service.prepare_acquisition( PrepareAcquisitionRequest( host="192.168.1.20", compatibility_attestation=ATTESTATION, ) ) acquisition_id = prepared["acquisition"]["acquisition_id"] service.start_acquisition(StartAcquisitionRequest(acquisition_id=acquisition_id)) runtime.mark_ready() service.state() runtime.pcl_frames = 1 service.state() awaiting = service.stop_acquisition( StopAcquisitionRequest(acquisition_id=acquisition_id, mode="graceful") ) stop_operation_id = awaiting["last_operation"]["operation_id"] runtime.phase = "error" failed = service.state() stop_operation = next( item for item in failed["operations"] if item["operation_id"] == stop_operation_id ) assert failed["acquisition"]["state"] == "failed" assert stop_operation["status"] == "failed" assert stop_operation["error"]["side_effect_status"] == "unknown" def test_receiver_completion_terminalizes_unconfirmed_graceful_stop( tmp_path: Path, ) -> None: service, runtime = service_with_fake_runtime(tmp_path) prepared = service.prepare_acquisition( PrepareAcquisitionRequest( host="192.168.1.20", compatibility_attestation=ATTESTATION, ) ) acquisition_id = prepared["acquisition"]["acquisition_id"] service.start_acquisition(StartAcquisitionRequest(acquisition_id=acquisition_id)) runtime.mark_ready() service.state() runtime.pcl_frames = 1 service.state() awaiting = service.stop_acquisition( StopAcquisitionRequest(acquisition_id=acquisition_id, mode="graceful") ) stop_operation_id = awaiting["last_operation"]["operation_id"] runtime.phase = "idle" runtime.source_mode = "idle" runtime.source_ready = False failed = service.state() stop_operation = next( item for item in failed["operations"] if item["operation_id"] == stop_operation_id ) assert failed["acquisition"]["state"] == "failed" assert failed["acquisition"]["result"] == { "receiver_stopped": True, "device_state": "unknown", } assert stop_operation["status"] == "failed" assert stop_operation["error"]["code"] == ("receiver-completed-before-device-stop-confirmation") def test_abort_failure_terminalizes_acquisition_and_pending_start(tmp_path: Path) -> None: service, runtime = service_with_fake_runtime(tmp_path) prepared = service.prepare_acquisition( PrepareAcquisitionRequest( host="192.168.1.20", compatibility_attestation=ATTESTATION, ) ) acquisition_id = prepared["acquisition"]["acquisition_id"] service.start_acquisition(StartAcquisitionRequest(acquisition_id=acquisition_id)) runtime.stop_error = RuntimeError("synthetic receiver stop failure") with pytest.raises(RuntimeError, match="synthetic receiver stop failure"): service.abort_acquisition(AbortAcquisitionRequest(acquisition_id=acquisition_id)) state = service.state() operations = {item["action"]: item for item in state["operations"]} assert state["acquisition"]["state"] == "failed" assert state["acquisition"]["result"] == { "receiver_stopped": False, "device_state": "unknown", } assert operations["acquisition.start"]["status"] == "cancelled" assert operations["acquisition.abort"]["status"] == "failed" assert state["source_mode"] == "live" def test_replay_is_rejected_during_nonterminal_acquisition(tmp_path: Path) -> None: service, _ = service_with_fake_runtime(tmp_path) service.prepare_acquisition( PrepareAcquisitionRequest( host="192.168.1.20", compatibility_attestation=ATTESTATION, ) ) with pytest.raises(RuntimeError, match="активной acquisition-сессии"): service.start_replay("sessions/fixture.k1mqtt", speed=1.0, loop=False) assert service.state()["acquisition"]["state"] == "prepared" def test_network_provisioning_is_single_flight_and_secret_is_unwrapped_only_at_boundary( monkeypatch: pytest.MonkeyPatch, tmp_path: Path, ) -> None: service, _ = service_with_fake_runtime(tmp_path) service._devices = [ # noqa: SLF001 - deliberate white-box concurrency fixture {"device_id": "k1-a"}, {"device_id": "k1-b"}, ] boundary_calls: list[tuple[str, str, str]] = [] async def scenario() -> dict[str, Any]: entered = asyncio.Event() release = asyncio.Event() async def fake_provision( device_id: str, ssid: str, password: str, **_: object, ) -> dict[str, Any]: boundary_calls.append((device_id, ssid, password)) entered.set() await release.wait() return { "started_at_utc": "2026-07-16T12:00:00Z", "completed_at_utc": "2026-07-16T12:00:01Z", "profile_id": "xgrids-k1-fw3-wifi-v1", "outcome": "lan_address_observed", "observations": [{"status": {"ipv4": "192.168.1.20"}}], } monkeypatch.setattr(facade_module, "provision_wifi_once", fake_provision) first = asyncio.create_task( service.connect( ConnectRequest( device_id="k1-a", ssid="lab-network", password=SecretStr(PRIMARY_TEST_CREDENTIAL), compatibility_attestation=ATTESTATION, ) ) ) await asyncio.wait_for(entered.wait(), timeout=1.0) with pytest.raises(RuntimeError, match="уже выполняется"): await service.connect( ConnectRequest( device_id="k1-b", ssid="other-network", password=SecretStr(SECONDARY_TEST_CREDENTIAL), compatibility_attestation=ATTESTATION, ) ) release.set() return await asyncio.wait_for(first, timeout=1.0) connected = asyncio.run(scenario()) assert boundary_calls == [("k1-a", "lab-network", PRIMARY_TEST_CREDENTIAL)] assert connected["k1_ip"] == "192.168.1.20" assert PRIMARY_TEST_CREDENTIAL not in str(connected) provision_operations = [ item for item in connected["operations"] if item["action"] == "network.provision" ] assert {item["status"] for item in provision_operations} == {"succeeded", "failed"} def test_provisioning_cannot_switch_device_during_active_acquisition( monkeypatch: pytest.MonkeyPatch, tmp_path: Path, ) -> None: service, _ = service_with_fake_runtime(tmp_path) service._devices = [{"device_id": "k1-a"}] # noqa: SLF001 service.prepare_acquisition( PrepareAcquisitionRequest( host="192.168.1.20", compatibility_attestation=ATTESTATION, ) ) called = False async def should_not_run(*_: object, **__: object) -> dict[str, Any]: nonlocal called called = True raise AssertionError("provisioning boundary must not be reached") monkeypatch.setattr(facade_module, "provision_wifi_once", should_not_run) with pytest.raises(RuntimeError, match="активной acquisition-сессии"): asyncio.run( service.connect( ConnectRequest( device_id="k1-a", ssid="lab-network", password=SecretStr(PRIMARY_TEST_CREDENTIAL), compatibility_attestation=ATTESTATION, ) ) ) assert called is False def test_sensor_catalog_exposes_observed_camera_only_after_profile_attestation( tmp_path: Path, ) -> None: service, _ = service_with_fake_runtime(tmp_path) initial = service.state() initial_camera = next( stream for stream in initial["sensor_catalog"]["streams"] if stream["stream_id"] == "camera.preview.live" ) assert initial_camera["availability"] == "unverified" state = service.prepare_acquisition( PrepareAcquisitionRequest( host="192.168.1.20", compatibility_attestation=ATTESTATION, ) ) camera = next( stream for stream in state["sensor_catalog"]["streams"] if stream["stream_id"] == "camera.preview.live" ) assert camera["availability"] == "observed" assert camera["modality"] == "encoded-video" assert camera["decode_status"] == "transport-observed-runtime-adapter-pending" assert state["connection_verification"]["network_reachability"] == "unknown" assert state["device_calibration"]["status"] == "unavailable" assert state["device_calibration"]["vehicle_extrinsics"] == ("host-domain-not-owned-by-plugin")