NODEDC_MISSION_CORE/tests/test_xgrids_acquisition_lif...

1676 lines
59 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from __future__ import annotations
import asyncio
import threading
from pathlib import Path
from typing import Any
import pytest
from pydantic import SecretStr, ValidationError
import k1link.device_plugins.xgrids_k1.facade as facade_module
from k1link.device_plugins.xgrids_k1.facade import (
DEFAULT_LIVE_STREAMS,
XGRIDS_K1_COMPATIBILITY_PROFILE_ID,
AbortAcquisitionRequest,
CameraPreviewSelectRequest,
CompatibilityAttestationRequest,
ConnectRequest,
PrepareAcquisitionRequest,
ShadowApplicationControlArmRequest,
StartAcquisitionRequest,
StopAcquisitionRequest,
XgridsK1CompatibilityService,
)
from k1link.device_plugins.xgrids_k1.protocol.application_bootstrap import (
ApplicationControlAuthority,
)
ATTESTATION = CompatibilityAttestationRequest(
firmware_version="3.0.2",
topology="direct-lan",
operator_confirmed=True,
)
PRIMARY_TEST_CREDENTIAL = "x" * 24
SECONDARY_TEST_CREDENTIAL = "y" * 24
PROJECT_NAME = "K1 lifecycle test"
PRIVATE_APPLICATION_AUTHORITY = "11111111-2222-3333-4444-555555555555"
class FakeApplicationAuthorityLoader:
def __init__(self) -> None:
self.calls = 0
def load(self) -> ApplicationControlAuthority:
self.calls += 1
return ApplicationControlAuthority(openapi_key=PRIVATE_APPLICATION_AUTHORITY)
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, str]] = []
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,
project_name: str,
) -> None:
self.start_calls.append((host, out_dir, duration_seconds, project_name))
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 close(self) -> None:
self.stop()
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(
project_name=PROJECT_NAME,
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"]["project_name"] == PROJECT_NAME
assert state["acquisition"]["compatibility_profile_id"] == (XGRIDS_K1_COMPATIBILITY_PROFILE_ID)
assert state["compatibility"]["vendor_writes_enabled"] is False
def test_project_name_is_normalized_and_control_characters_are_rejected() -> None:
request = PrepareAcquisitionRequest(
project_name=" Lab ",
host="192.168.1.20",
compatibility_attestation=ATTESTATION,
)
assert request.project_name == "K1 Lab"
for invalid in (" ", "line\nbreak", "\ud800", "x" * 97):
with pytest.raises(ValidationError):
PrepareAcquisitionRequest(
project_name=invalid,
host="192.168.1.20",
compatibility_attestation=ATTESTATION,
)
def test_facade_arms_bounded_shadow_lease_without_installing_publish_transport(
tmp_path: Path,
) -> None:
loader = FakeApplicationAuthorityLoader()
service = XgridsK1CompatibilityService(
tmp_path,
application_authority_loader=loader,
)
runtime = FakeVisualizationRuntime()
service.runtime = runtime # type: ignore[assignment]
service._selected_device_id = "test-ble-transport" # noqa: SLF001
service._k1_ip = "192.168.1.20" # noqa: SLF001
service._compatibility_attestation = { # noqa: SLF001
"firmware_version": "3.0.2",
"topology": "direct-lan",
"basis": "operator-attested",
"observed_at": "2026-07-18T00:00:00Z",
}
state = service.arm_application_control_shadow(
ShadowApplicationControlArmRequest(
operator_confirmed=True,
lease_seconds=60.0,
timezone_name="Europe/Moscow",
)
)
execution = state["application_control_execution"]
assert loader.calls == 1
assert execution["state"] == "armed-shadow-only"
assert execution["lease"]["authority_cached"] is True
assert execution["can_emit_requests"] is False
assert execution["live_transport_installed"] is False
assert execution["publisher"]["vendor_writes_enabled"] is False
assert execution["publisher"]["transport_calls"] == 0
assert PRIVATE_APPLICATION_AUTHORITY not in str(state)
disarmed = service.disarm_application_control_shadow()
assert disarmed["application_control_execution"]["state"] == "disarmed"
assert disarmed["application_control_execution"]["lease"] is None
def test_shadow_arm_requires_idle_connected_attested_device_before_keychain_read(
tmp_path: Path,
) -> None:
loader = FakeApplicationAuthorityLoader()
service = XgridsK1CompatibilityService(
tmp_path,
application_authority_loader=loader,
)
service.runtime = FakeVisualizationRuntime() # type: ignore[assignment]
with pytest.raises(RuntimeError, match="подключите K1"):
service.arm_application_control_shadow(
ShadowApplicationControlArmRequest(
operator_confirmed=True,
timezone_name="Europe/Moscow",
)
)
assert loader.calls == 0
def test_shadow_arm_contract_requires_explicit_operator_confirmation() -> None:
with pytest.raises(ValidationError):
ShadowApplicationControlArmRequest.model_validate({"timezone_name": "Europe/Moscow"})
def test_prepare_acquisition_revokes_existing_shadow_authority_lease(tmp_path: Path) -> None:
loader = FakeApplicationAuthorityLoader()
service = XgridsK1CompatibilityService(
tmp_path,
application_authority_loader=loader,
)
service.runtime = FakeVisualizationRuntime() # type: ignore[assignment]
service._selected_device_id = "test-ble-transport" # noqa: SLF001
service._k1_ip = "192.168.1.20" # noqa: SLF001
service._compatibility_attestation = {"profile": "exact"} # noqa: SLF001
service.arm_application_control_shadow(
ShadowApplicationControlArmRequest(
operator_confirmed=True,
timezone_name="Europe/Moscow",
)
)
prepared = service.prepare_acquisition(
PrepareAcquisitionRequest(
project_name=PROJECT_NAME,
host="192.168.1.20",
duration_seconds=60,
compatibility_attestation=ATTESTATION,
)
)
assert prepared["application_control_execution"]["state"] == "disarmed"
assert prepared["application_control_execution"]["lease"] is None
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(
project_name=PROJECT_NAME,
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
assert runtime.start_calls[0][3] == PROJECT_NAME
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_second_start_conflict_does_not_tear_down_start_owner(tmp_path: Path) -> None:
service, runtime = service_with_fake_runtime(tmp_path)
prepared = service.prepare_acquisition(
PrepareAcquisitionRequest(
project_name=PROJECT_NAME,
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(RuntimeError, match="состояния starting"):
service.start_acquisition(StartAcquisitionRequest(acquisition_id=acquisition_id))
state = service.state()
start_operations = [
item for item in state["operations"] if item["action"] == "acquisition.start"
]
assert state["acquisition"]["state"] == "starting"
assert runtime.stop_calls == 0
assert [item["status"] for item in start_operations] == ["running", "failed"]
assert start_operations[-1]["error"]["category"] == "conflict"
def test_state_waits_for_atomic_start_handoff(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
service, runtime = service_with_fake_runtime(tmp_path)
prepared = service.prepare_acquisition(
PrepareAcquisitionRequest(
project_name=PROJECT_NAME,
host="192.168.1.20",
compatibility_attestation=ATTESTATION,
)
)
acquisition_id = prepared["acquisition"]["acquisition_id"]
entered_start = threading.Event()
release_start = threading.Event()
state_started = threading.Event()
state_finished = threading.Event()
worker_errors: list[BaseException] = []
snapshots: list[dict[str, Any]] = []
original_start_live = runtime.start_live
def blocked_start_live(*args: object, **kwargs: object) -> None:
entered_start.set()
if not release_start.wait(timeout=2):
raise TimeoutError("test did not release start handoff")
original_start_live(*args, **kwargs) # type: ignore[arg-type]
def start_worker() -> None:
try:
service.start_acquisition(StartAcquisitionRequest(acquisition_id=acquisition_id))
except BaseException as exc: # pragma: no cover - asserted below
worker_errors.append(exc)
def state_worker() -> None:
state_started.set()
try:
snapshots.append(service.state())
except BaseException as exc: # pragma: no cover - asserted below
worker_errors.append(exc)
finally:
state_finished.set()
monkeypatch.setattr(runtime, "start_live", blocked_start_live)
start_thread = threading.Thread(target=start_worker)
state_thread = threading.Thread(target=state_worker)
start_thread.start()
assert entered_start.wait(timeout=2)
state_thread.start()
assert state_started.wait(timeout=2)
assert not state_finished.wait(timeout=0.05)
release_start.set()
start_thread.join(timeout=2)
state_thread.join(timeout=2)
assert not start_thread.is_alive()
assert not state_thread.is_alive()
assert worker_errors == []
assert snapshots[0]["acquisition"]["state"] == "starting"
assert runtime.source_mode == "live"
assert service._acquisition_session_lease is not None # noqa: SLF001
def test_abort_waits_for_start_handoff_then_stops_owned_producers(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
service, runtime = service_with_fake_runtime(tmp_path)
prepared = service.prepare_acquisition(
PrepareAcquisitionRequest(
project_name=PROJECT_NAME,
host="192.168.1.20",
compatibility_attestation=ATTESTATION,
)
)
acquisition_id = prepared["acquisition"]["acquisition_id"]
entered_start = threading.Event()
release_start = threading.Event()
abort_finished = threading.Event()
worker_errors: list[BaseException] = []
original_start_live = runtime.start_live
def blocked_start_live(*args: object, **kwargs: object) -> None:
entered_start.set()
if not release_start.wait(timeout=2):
raise TimeoutError("test did not release start handoff")
original_start_live(*args, **kwargs) # type: ignore[arg-type]
def start_worker() -> None:
try:
service.start_acquisition(StartAcquisitionRequest(acquisition_id=acquisition_id))
except BaseException as exc: # pragma: no cover - asserted below
worker_errors.append(exc)
def abort_worker() -> None:
try:
service.abort_acquisition(AbortAcquisitionRequest(acquisition_id=acquisition_id))
except BaseException as exc: # pragma: no cover - asserted below
worker_errors.append(exc)
finally:
abort_finished.set()
monkeypatch.setattr(runtime, "start_live", blocked_start_live)
start_thread = threading.Thread(target=start_worker)
abort_thread = threading.Thread(target=abort_worker)
start_thread.start()
assert entered_start.wait(timeout=2)
abort_thread.start()
assert not abort_finished.wait(timeout=0.05)
release_start.set()
start_thread.join(timeout=2)
abort_thread.join(timeout=2)
assert worker_errors == []
state = service.state()
assert state["acquisition"]["state"] == "aborted"
assert runtime.source_mode == "idle"
assert service._acquisition_session_lease is None # noqa: SLF001
def test_camera_selection_finishes_before_serialized_acquisition_stop(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
service, runtime = service_with_fake_runtime(tmp_path)
prepared = service.prepare_acquisition(
PrepareAcquisitionRequest(
project_name=PROJECT_NAME,
host="192.168.1.20",
compatibility_attestation=ATTESTATION,
)
)
acquisition_id = prepared["acquisition"]["acquisition_id"]
device_session_id = prepared["device_session"]["device_session_id"]
service.start_acquisition(StartAcquisitionRequest(acquisition_id=acquisition_id))
service._k1_ip = "192.168.1.20" # noqa: SLF001
entered_camera_arm = threading.Event()
release_camera_arm = threading.Event()
stop_finished = threading.Event()
worker_errors: list[BaseException] = []
events: list[str] = []
def blocked_camera_arm(_out_dir: Path, *, require_session: bool = False) -> None:
assert require_session is True
events.append("arm")
entered_camera_arm.set()
if not release_camera_arm.wait(timeout=2):
raise TimeoutError("test did not release camera arm")
def select_worker() -> None:
try:
service.select_camera_preview(
CameraPreviewSelectRequest(
source_id="sensor.camera.left",
device_session_id=device_session_id,
)
)
except BaseException as exc: # pragma: no cover - asserted below
worker_errors.append(exc)
def stop_worker() -> None:
try:
service.stop_acquisition(
StopAcquisitionRequest(
acquisition_id=acquisition_id,
mode="capture-only",
)
)
except BaseException as exc: # pragma: no cover - asserted below
worker_errors.append(exc)
finally:
stop_finished.set()
monkeypatch.setattr(service, "_arm_camera_recording", blocked_camera_arm)
monkeypatch.setattr(
service.camera_preview,
"select",
lambda _source_id, _target: events.append("select"),
)
monkeypatch.setattr(
service.camera_preview,
"stop_recording",
lambda **_kwargs: events.append("camera-stop"),
)
monkeypatch.setattr(runtime, "stop", lambda: events.append("runtime-stop"))
select_thread = threading.Thread(target=select_worker)
stop_thread = threading.Thread(target=stop_worker)
select_thread.start()
assert entered_camera_arm.wait(timeout=2)
stop_thread.start()
assert not stop_finished.wait(timeout=0.05)
release_camera_arm.set()
select_thread.join(timeout=2)
stop_thread.join(timeout=2)
assert worker_errors == []
assert events == ["arm", "select", "camera-stop", "runtime-stop"]
assert service.state()["acquisition"]["state"] == "completed"
assert service._acquisition_session_lease is None # noqa: SLF001
def test_camera_arm_failure_seals_stopped_session_before_releasing_lease(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
service, runtime = service_with_fake_runtime(tmp_path)
prepared = service.prepare_acquisition(
PrepareAcquisitionRequest(
project_name=PROJECT_NAME,
host="192.168.1.20",
compatibility_attestation=ATTESTATION,
)
)
acquisition_id = prepared["acquisition"]["acquisition_id"]
events: list[str] = []
def start_live(
_host: str,
out_dir: Path,
*,
duration_seconds: float,
project_name: str,
) -> None:
assert duration_seconds > 0
assert project_name == PROJECT_NAME
events.append("start")
out_dir.mkdir(parents=True)
runtime.phase = "starting_live"
runtime.source_mode = "live"
def stop_runtime() -> None:
events.append("runtime")
runtime.phase = "idle"
runtime.source_mode = "idle"
monkeypatch.setattr(runtime, "start_live", start_live)
monkeypatch.setattr(runtime, "stop", stop_runtime)
monkeypatch.setattr(
service,
"_arm_camera_recording",
lambda _out_dir: (_ for _ in ()).throw(RuntimeError("synthetic camera arm failure")),
)
monkeypatch.setattr(
service.camera_preview,
"stop_recording",
lambda **_kwargs: events.append("camera"),
)
monkeypatch.setattr(
facade_module,
"seal_capture_clock",
lambda _capture_root: events.append("seal"),
)
with pytest.raises(RuntimeError, match="synthetic camera arm failure"):
service.start_acquisition(StartAcquisitionRequest(acquisition_id=acquisition_id))
state = service.state()
assert state["acquisition"]["state"] == "failed"
assert state["last_operation"]["status"] == "failed"
assert events == ["start", "camera", "runtime", "seal"]
assert service._acquisition_session_lease is None # noqa: SLF001
def test_start_cleanup_failure_retains_lease_and_marks_side_effect_unknown(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
service, runtime = service_with_fake_runtime(tmp_path)
prepared = service.prepare_acquisition(
PrepareAcquisitionRequest(
project_name=PROJECT_NAME,
host="192.168.1.20",
compatibility_attestation=ATTESTATION,
)
)
acquisition_id = prepared["acquisition"]["acquisition_id"]
def fail_camera_arm(out_dir: Path) -> None:
out_dir.mkdir()
raise RuntimeError("synthetic camera arm failure")
monkeypatch.setattr(service, "_arm_camera_recording", fail_camera_arm)
monkeypatch.setattr(facade_module, "seal_capture_clock", lambda _capture_root: None)
runtime.stop_error = RuntimeError("synthetic cleanup timeout")
with pytest.raises(RuntimeError, match="synthetic camera arm failure"):
service.start_acquisition(StartAcquisitionRequest(acquisition_id=acquisition_id))
state = service.state()
operation = next(item for item in state["operations"] if item["action"] == "acquisition.start")
assert state["acquisition"]["state"] == "failed"
assert operation["status"] == "failed"
assert operation["error"]["side_effect_status"] == "unknown"
assert service._acquisition_session_lease is not None # noqa: SLF001
runtime.stop_error = None
service.stop()
assert service._acquisition_session_lease is None # noqa: SLF001
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(
project_name=PROJECT_NAME,
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(
project_name=PROJECT_NAME,
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_stop_seals_session_clock_after_camera_and_runtime_before_lease_release(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
service, runtime = service_with_fake_runtime(tmp_path)
out_dir = tmp_path / "evidence-session"
out_dir.mkdir()
service._acquisition_out_dir = out_dir # noqa: SLF001
events: list[object] = []
monkeypatch.setattr(
service.camera_preview,
"stop_recording",
lambda **_kwargs: events.append("camera"),
)
monkeypatch.setattr(runtime, "stop", lambda: events.append("runtime"))
monkeypatch.setattr(
facade_module,
"seal_capture_clock",
lambda capture_root: events.append(("seal", capture_root)),
)
class Lease:
def release(self) -> None:
events.append("lease")
service._acquisition_session_lease = Lease() # type: ignore[assignment] # noqa: SLF001
service._stop_acquisition_sources( # noqa: SLF001
camera_status="complete",
camera_failure_code=None,
)
assert events == [
"camera",
"runtime",
("seal", out_dir / "captures" / "mqtt_live"),
"lease",
]
def test_stop_timeout_retains_lease_and_blocks_replacement_acquisition(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
service, runtime = service_with_fake_runtime(tmp_path)
prepared = service.prepare_acquisition(
PrepareAcquisitionRequest(
project_name=PROJECT_NAME,
host="192.168.1.20",
compatibility_attestation=ATTESTATION,
)
)
acquisition_id = prepared["acquisition"]["acquisition_id"]
service.start_acquisition(StartAcquisitionRequest(acquisition_id=acquisition_id))
out_dir = service._acquisition_out_dir # noqa: SLF001
assert out_dir is not None
out_dir.mkdir()
seal_calls: list[Path] = []
monkeypatch.setattr(
facade_module,
"seal_capture_clock",
lambda capture_root: seal_calls.append(capture_root),
)
runtime.stop_error = RuntimeError("synthetic stop timeout")
with pytest.raises(RuntimeError, match="synthetic stop timeout"):
service.stop_acquisition(
StopAcquisitionRequest(acquisition_id=acquisition_id, mode="capture-only")
)
assert seal_calls == []
assert service._acquisition_session_lease is not None # noqa: SLF001
assert service.state()["acquisition"]["cleanup_pending"] is True
with pytest.raises(RuntimeError, match="evidence-сессия"):
service.prepare_acquisition(
PrepareAcquisitionRequest(
project_name="replacement",
host="192.168.1.20",
compatibility_attestation=ATTESTATION,
)
)
runtime.stop_error = None
retried = service.stop()
assert retried["acquisition"]["state"] == "failed"
assert len(seal_calls) == 1
assert service._acquisition_session_lease is None # noqa: SLF001
assert retried["acquisition"]["cleanup_pending"] is False
replacement = service.prepare_acquisition(
PrepareAcquisitionRequest(
project_name="replacement",
host="192.168.1.20",
compatibility_attestation=ATTESTATION,
)
)
assert replacement["acquisition"]["state"] == "prepared"
def test_replay_rejects_retained_failed_acquisition_lease(
tmp_path: Path,
) -> None:
service, runtime = service_with_fake_runtime(tmp_path)
prepared = service.prepare_acquisition(
PrepareAcquisitionRequest(
project_name=PROJECT_NAME,
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 stop timeout")
with pytest.raises(RuntimeError, match="synthetic stop timeout"):
service.stop_acquisition(
StopAcquisitionRequest(acquisition_id=acquisition_id, mode="capture-only")
)
with pytest.raises(RuntimeError, match="не запечатана"):
service.start_replay("sessions/fixture.k1mqtt", speed=1.0, loop=False)
runtime.stop_error = None
service.stop()
def test_new_explicit_stop_retries_retained_terminal_cleanup(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
service, runtime = service_with_fake_runtime(tmp_path)
prepared = service.prepare_acquisition(
PrepareAcquisitionRequest(
project_name=PROJECT_NAME,
host="192.168.1.20",
compatibility_attestation=ATTESTATION,
)
)
acquisition_id = prepared["acquisition"]["acquisition_id"]
service.start_acquisition(StartAcquisitionRequest(acquisition_id=acquisition_id))
out_dir = service._acquisition_out_dir # noqa: SLF001
assert out_dir is not None
out_dir.mkdir()
seal_calls: list[Path] = []
monkeypatch.setattr(
facade_module,
"seal_capture_clock",
lambda capture_root: seal_calls.append(capture_root),
)
runtime.stop_error = RuntimeError("synthetic stop timeout")
with pytest.raises(RuntimeError, match="synthetic stop timeout"):
service.stop_acquisition(
StopAcquisitionRequest(acquisition_id=acquisition_id, mode="capture-only")
)
runtime.stop_error = None
recovered = service.stop_acquisition(
StopAcquisitionRequest(
acquisition_id=acquisition_id,
mode="capture-only",
idempotency_key="retry-retained-cleanup",
)
)
retry_operation = recovered["last_operation"]
assert retry_operation["status"] == "succeeded"
assert retry_operation["stage_code"] == "retained-cleanup-completed"
assert retry_operation["result"]["device_stop"] == "unknown"
assert len(seal_calls) == 1
assert service._acquisition_session_lease is None # noqa: SLF001
def test_natural_receiver_completion_fails_acquisition_when_session_clock_cannot_seal(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
service, runtime = service_with_fake_runtime(tmp_path)
prepared = service.prepare_acquisition(
PrepareAcquisitionRequest(
project_name=PROJECT_NAME,
host="192.168.1.20",
compatibility_attestation=ATTESTATION,
)
)
acquisition_id = prepared["acquisition"]["acquisition_id"]
service.start_acquisition(StartAcquisitionRequest(acquisition_id=acquisition_id))
runtime.pcl_frames = 1
service.state()
out_dir = service._acquisition_out_dir # noqa: SLF001
assert out_dir is not None
out_dir.mkdir()
monkeypatch.setattr(
facade_module,
"seal_capture_clock",
lambda _capture_root: (_ for _ in ()).throw(RuntimeError("synthetic seal failure")),
)
runtime.phase = "idle"
runtime.source_mode = "idle"
with pytest.raises(RuntimeError, match="synthetic seal failure"):
service.state()
assert service._acquisition is not None # noqa: SLF001
assert service._acquisition.state == "failed" # noqa: SLF001
def test_natural_receiver_completion_reserves_finalization_before_reentrant_callback(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
service, runtime = service_with_fake_runtime(tmp_path)
prepared = service.prepare_acquisition(
PrepareAcquisitionRequest(
project_name=PROJECT_NAME,
host="192.168.1.20",
compatibility_attestation=ATTESTATION,
)
)
acquisition_id = prepared["acquisition"]["acquisition_id"]
service.start_acquisition(StartAcquisitionRequest(acquisition_id=acquisition_id))
runtime.pcl_frames = 1
service.state()
out_dir = service._acquisition_out_dir # noqa: SLF001
assert out_dir is not None
out_dir.mkdir()
events: list[str] = []
def reentrant_camera_stop(**_kwargs: object) -> None:
events.append("camera")
nested = service.state()
assert nested["acquisition"]["state"] == "finalizing"
monkeypatch.setattr(service.camera_preview, "stop_recording", reentrant_camera_stop)
monkeypatch.setattr(
facade_module,
"seal_capture_clock",
lambda _capture_root: events.append("seal"),
)
runtime.phase = "idle"
runtime.source_mode = "idle"
completed = service.state()
assert completed["acquisition"]["state"] == "completed"
assert events == ["camera", "seal"]
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(
project_name=PROJECT_NAME,
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(
project_name=PROJECT_NAME,
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(
project_name=PROJECT_NAME,
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(
project_name=PROJECT_NAME,
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(
project_name=PROJECT_NAME,
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(
project_name=PROJECT_NAME,
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(
project_name=PROJECT_NAME,
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(
project_name=PROJECT_NAME,
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(
project_name=PROJECT_NAME,
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(
project_name=PROJECT_NAME,
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_unconfirmed_stop_operation_terminalizes_before_clock_seal_error(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
service, runtime = service_with_fake_runtime(tmp_path)
prepared = service.prepare_acquisition(
PrepareAcquisitionRequest(
project_name=PROJECT_NAME,
host="192.168.1.20",
compatibility_attestation=ATTESTATION,
)
)
acquisition_id = prepared["acquisition"]["acquisition_id"]
service.start_acquisition(StartAcquisitionRequest(acquisition_id=acquisition_id))
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"]
out_dir = service._acquisition_out_dir # noqa: SLF001
assert out_dir is not None
out_dir.mkdir()
monkeypatch.setattr(
facade_module,
"seal_capture_clock",
lambda _capture_root: (_ for _ in ()).throw(RuntimeError("synthetic seal failure")),
)
runtime.phase = "idle"
runtime.source_mode = "idle"
with pytest.raises(RuntimeError, match="synthetic seal failure"):
service.state()
state = service.state()
stop_operation = next(
item for item in state["operations"] if item["operation_id"] == stop_operation_id
)
assert state["acquisition"]["state"] == "failed"
assert stop_operation["status"] == "failed"
assert service._acquisition_session_lease is not None # noqa: SLF001
monkeypatch.setattr(facade_module, "seal_capture_clock", lambda _capture_root: None)
service.stop()
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(
project_name=PROJECT_NAME,
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_abort_reserves_stopping_before_reentrant_runtime_callback(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
service, runtime = service_with_fake_runtime(tmp_path)
prepared = service.prepare_acquisition(
PrepareAcquisitionRequest(
project_name=PROJECT_NAME,
host="192.168.1.20",
compatibility_attestation=ATTESTATION,
)
)
acquisition_id = prepared["acquisition"]["acquisition_id"]
service.start_acquisition(StartAcquisitionRequest(acquisition_id=acquisition_id))
runtime.pcl_frames = 1
service.state()
out_dir = service._acquisition_out_dir # noqa: SLF001
assert out_dir is not None
out_dir.mkdir()
events: list[str] = []
monkeypatch.setattr(
service.camera_preview,
"stop_recording",
lambda **_kwargs: events.append("camera"),
)
def reentrant_runtime_stop() -> None:
events.append("runtime")
runtime.phase = "idle"
runtime.source_mode = "idle"
nested = service.state()
assert nested["acquisition"]["state"] == "stopping"
monkeypatch.setattr(runtime, "stop", reentrant_runtime_stop)
monkeypatch.setattr(
facade_module,
"seal_capture_clock",
lambda _capture_root: events.append("seal"),
)
aborted = service.abort_acquisition(AbortAcquisitionRequest(acquisition_id=acquisition_id))
assert aborted["acquisition"]["state"] == "aborted"
assert events == ["camera", "runtime", "seal"]
assert service._acquisition_session_lease is None # noqa: SLF001
def test_clean_close_seals_and_cancels_pending_start_operation(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
service, runtime = service_with_fake_runtime(tmp_path)
prepared = service.prepare_acquisition(
PrepareAcquisitionRequest(
project_name=PROJECT_NAME,
host="192.168.1.20",
compatibility_attestation=ATTESTATION,
)
)
acquisition_id = prepared["acquisition"]["acquisition_id"]
service.start_acquisition(StartAcquisitionRequest(acquisition_id=acquisition_id))
out_dir = service._acquisition_out_dir # noqa: SLF001
assert out_dir is not None
out_dir.mkdir()
events: list[str] = []
monkeypatch.setattr(
service.camera_preview,
"close",
lambda: events.append("camera"),
)
def close_runtime() -> None:
events.append("runtime")
runtime.phase = "idle"
runtime.source_mode = "idle"
monkeypatch.setattr(runtime, "close", close_runtime)
monkeypatch.setattr(
facade_module,
"seal_capture_clock",
lambda _capture_root: events.append("seal"),
)
service.close()
state = service.state()
start_operation = next(
item for item in state["operations"] if item["action"] == "acquisition.start"
)
assert state["acquisition"]["state"] == "interrupted"
assert start_operation["status"] == "cancelled"
assert events == ["camera", "runtime", "seal"]
assert service._acquisition_session_lease is None # noqa: SLF001
def test_clean_close_cancels_pending_external_stop_operation(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
service, runtime = service_with_fake_runtime(tmp_path)
prepared = service.prepare_acquisition(
PrepareAcquisitionRequest(
project_name=PROJECT_NAME,
host="192.168.1.20",
compatibility_attestation=ATTESTATION,
)
)
acquisition_id = prepared["acquisition"]["acquisition_id"]
service.start_acquisition(StartAcquisitionRequest(acquisition_id=acquisition_id))
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"]
monkeypatch.setattr(service.camera_preview, "close", lambda: None)
monkeypatch.setattr(runtime, "close", lambda: runtime.stop())
service.close()
state = service.state()
stop_operation = next(
item for item in state["operations"] if item["operation_id"] == stop_operation_id
)
assert state["acquisition"]["state"] == "interrupted"
assert stop_operation["status"] == "cancelled"
assert all(
item["status"] not in {"accepted", "running", "operator_action_required"}
for item in state["operations"]
if item["action"].startswith("acquisition.")
)
def test_close_failure_fails_pending_operation_and_retains_lease(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
service, runtime = service_with_fake_runtime(tmp_path)
prepared = service.prepare_acquisition(
PrepareAcquisitionRequest(
project_name=PROJECT_NAME,
host="192.168.1.20",
compatibility_attestation=ATTESTATION,
)
)
acquisition_id = prepared["acquisition"]["acquisition_id"]
service.start_acquisition(StartAcquisitionRequest(acquisition_id=acquisition_id))
monkeypatch.setattr(service.camera_preview, "close", lambda: None)
monkeypatch.setattr(
runtime,
"close",
lambda: (_ for _ in ()).throw(RuntimeError("synthetic close timeout")),
)
with pytest.raises(RuntimeError, match="synthetic close timeout"):
service.close()
state = service.state()
start_operation = next(
item for item in state["operations"] if item["action"] == "acquisition.start"
)
assert state["acquisition"]["state"] == "failed"
assert start_operation["status"] == "failed"
assert start_operation["error"]["side_effect_status"] == "unknown"
assert service._acquisition_session_lease is not None # noqa: SLF001
runtime.stop_error = None
service.stop()
assert service._acquisition_session_lease is None # noqa: SLF001
def test_replay_is_rejected_during_nonterminal_acquisition(tmp_path: Path) -> None:
service, _ = service_with_fake_runtime(tmp_path)
service.prepare_acquisition(
PrepareAcquisitionRequest(
project_name=PROJECT_NAME,
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_prepare_rejects_active_replay_without_stopping_or_replacing_it(
tmp_path: Path,
) -> None:
service, runtime = service_with_fake_runtime(tmp_path)
runtime.phase = "replay"
runtime.source_mode = "replay"
runtime.source_ready = True
with pytest.raises(RuntimeError, match="активного live/replay"):
service.prepare_acquisition(
PrepareAcquisitionRequest(
project_name=PROJECT_NAME,
host="192.168.1.20",
compatibility_attestation=ATTESTATION,
)
)
state = service.state()
assert state["acquisition"] is None
assert state["source_mode"] == "replay"
assert runtime.stop_calls == 0
assert service._acquisition_session_lease is None # noqa: SLF001
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(
project_name=PROJECT_NAME,
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_two_browser_adapter_cameras_after_profile_attestation(
tmp_path: Path,
) -> None:
service, _ = service_with_fake_runtime(tmp_path)
initial = service.state()
initial_cameras = [
stream
for stream in initial["sensor_catalog"]["streams"]
if stream.get("semantic_channel_id") == "camera.preview.live"
]
assert {stream["source_id"] for stream in initial_cameras} == {
"sensor.camera.left",
"sensor.camera.right",
}
assert all(stream["availability"] == "unverified" for stream in initial_cameras)
state = service.prepare_acquisition(
PrepareAcquisitionRequest(
project_name=PROJECT_NAME,
host="192.168.1.20",
compatibility_attestation=ATTESTATION,
)
)
cameras = [
stream
for stream in state["sensor_catalog"]["streams"]
if stream.get("semantic_channel_id") == "camera.preview.live"
]
assert len(cameras) == 2
assert all(camera["availability"] == "available" for camera in cameras)
assert all(camera["modality"] == "encoded-video" for camera in cameras)
assert all(camera["decode_status"] == "rtsp-h264-observed-browser-remux" for camera in cameras)
assert all(camera["activation"]["max_active"] == 1 for camera in cameras)
assert all(camera["delivery"] is None for camera in cameras)
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")