708 lines
25 KiB
Python
708 lines
25 KiB
Python
from __future__ import annotations
|
|
|
|
import asyncio
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
import pytest
|
|
from pydantic import SecretStr
|
|
|
|
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,
|
|
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_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(
|
|
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")
|