3331 lines
120 KiB
Python
3331 lines
120 KiB
Python
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import json
|
|
import logging
|
|
import threading
|
|
from collections.abc import AsyncIterator
|
|
from contextlib import asynccontextmanager
|
|
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,
|
|
ConnectionVerifyRequest,
|
|
ConnectRequest,
|
|
OpenApplicationControlSessionRequest,
|
|
OperatorPresenceRequest,
|
|
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",
|
|
verification="live-device-info",
|
|
)
|
|
QUICK_CONNECT_ATTESTATION = CompatibilityAttestationRequest(
|
|
firmware_version="3.0.2",
|
|
topology="device-ap",
|
|
verification="live-device-info",
|
|
)
|
|
DIRECT_CONNECT_ATTESTATION = CompatibilityAttestationRequest(
|
|
firmware_version="3.0.2",
|
|
topology="controller-hotspot",
|
|
verification="live-device-info",
|
|
)
|
|
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 | None, 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 | None,
|
|
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()
|
|
|
|
|
|
class FakeInteractiveControlSession:
|
|
def __init__(self) -> None:
|
|
self.state = "workspace-ready"
|
|
self.start_projects: list[str] = []
|
|
self.stop_calls = 0
|
|
|
|
def snapshot(self) -> dict[str, object]:
|
|
return {
|
|
"state": self.state,
|
|
"can_confirm_standby": False,
|
|
}
|
|
|
|
def open_project_prompt(self) -> dict[str, object]:
|
|
assert self.state == "workspace-ready"
|
|
self.state = "project-ready"
|
|
return self.snapshot()
|
|
|
|
def request_start(self, *, project_name: str, confirmation: object) -> dict[str, object]:
|
|
assert confirmation is not None
|
|
assert self.state == "project-ready"
|
|
self.start_projects.append(project_name)
|
|
self.state = "scanning"
|
|
return self.snapshot()
|
|
|
|
def request_stop(self, *, confirmation: object) -> dict[str, object]:
|
|
assert confirmation is not None
|
|
assert self.state == "scanning"
|
|
self.stop_calls += 1
|
|
self.state = "awaiting-standby-confirmation"
|
|
return self.snapshot()
|
|
|
|
def close_prestart(self) -> dict[str, object]:
|
|
self.state = "closed"
|
|
return self.snapshot()
|
|
|
|
def close(self) -> None:
|
|
self.state = "closed"
|
|
|
|
|
|
PHYSICAL_ACCEPTANCE = OperatorPresenceRequest(
|
|
operator_present=True,
|
|
owner_controlled_device=True,
|
|
lixelgo_closed=True,
|
|
battery_storage_confirmed=True,
|
|
expected_physical_state_confirmed=True,
|
|
)
|
|
|
|
|
|
def test_control_session_transport_metadata_does_not_leak_into_confirmation() -> None:
|
|
request = OpenApplicationControlSessionRequest(
|
|
operator_present=True,
|
|
owner_controlled_device=True,
|
|
lixelgo_closed=True,
|
|
battery_storage_confirmed=True,
|
|
expected_physical_state_confirmed=True,
|
|
timezone_name="Europe/Moscow",
|
|
)
|
|
|
|
assert request.confirmation() == PHYSICAL_ACCEPTANCE.confirmation()
|
|
|
|
|
|
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 _wifi_status_read(ipv4: str | None) -> dict[str, Any]:
|
|
return {
|
|
"schema_version": 1,
|
|
"profile_id": "xgrids-k1-fw3-wifi-v1",
|
|
"observed_at_utc": "2026-07-20T12:00:00Z",
|
|
"adapter": "CoreBluetooth",
|
|
"bleak_version": "test",
|
|
"device_macos_uuid": "test-ble-transport",
|
|
"device_name": "XGR-K1",
|
|
"service_uuid": "00007f00-0000-1000-8000-00805f9b34fb",
|
|
"status_characteristic_uuid": "00007f02-0000-1000-8000-00805f9b34fb",
|
|
"operation": "single_reviewed_wifi_status_read",
|
|
"write_performed": False,
|
|
"status": {
|
|
"value_length": 54,
|
|
"mode": "WIFI_CLIENT",
|
|
"ipv4": ipv4,
|
|
"status_code": 1,
|
|
"reserved": 0,
|
|
"trailer_hex": "",
|
|
},
|
|
}
|
|
|
|
|
|
def _set_scanned_devices(
|
|
service: XgridsK1CompatibilityService,
|
|
devices: list[dict[str, Any]],
|
|
) -> None:
|
|
service._devices = devices # noqa: SLF001
|
|
observed_monotonic = facade_module.time.monotonic()
|
|
service._ble_device_last_seen_monotonic = { # noqa: SLF001
|
|
str(item["device_id"]): observed_monotonic for item in devices
|
|
}
|
|
|
|
|
|
def _set_scanned_k1(
|
|
service: XgridsK1CompatibilityService,
|
|
*,
|
|
device_id: str = "test-ble-transport",
|
|
) -> None:
|
|
_set_scanned_devices(service, [
|
|
{
|
|
"device_id": device_id,
|
|
"name": "XGR-K1",
|
|
"rssi": -44,
|
|
"address": None,
|
|
"connectable": True,
|
|
"likely_k1": True,
|
|
}
|
|
])
|
|
|
|
|
|
def test_ble_discovery_lease_expiry_hides_and_rejects_candidate(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
_set_scanned_k1(service)
|
|
service._ble_device_last_seen_monotonic["test-ble-transport"] = ( # noqa: SLF001
|
|
facade_module.time.monotonic()
|
|
- facade_module.BLE_DISCOVERY_LEASE_TTL_SECONDS
|
|
- 0.001
|
|
)
|
|
|
|
async def forbidden_status_read(*_: object, **__: object) -> dict[str, Any]:
|
|
raise AssertionError("an expired candidate must not be rediscovered by an action")
|
|
|
|
async def forbidden_provision(*_: object, **__: object) -> dict[str, Any]:
|
|
raise AssertionError("an expired candidate must not reach the Wi-Fi write boundary")
|
|
|
|
monkeypatch.setattr(facade_module, "read_wifi_status_once", forbidden_status_read)
|
|
monkeypatch.setattr(facade_module, "provision_wifi_once", forbidden_provision)
|
|
|
|
stale_state = service.state()
|
|
assert stale_state["devices"] == []
|
|
assert "устарели" in stale_state["message"]
|
|
with pytest.raises(ValueError, match="найдите и выберите"):
|
|
service.verify_connection(
|
|
ConnectionVerifyRequest(
|
|
device_id="test-ble-transport",
|
|
compatibility_attestation=ATTESTATION,
|
|
)
|
|
)
|
|
with pytest.raises(ValueError, match="найдите и выберите"):
|
|
asyncio.run(
|
|
service.connect(
|
|
ConnectRequest(
|
|
device_id="test-ble-transport",
|
|
ssid="lab-network",
|
|
password=SecretStr(PRIMARY_TEST_CREDENTIAL),
|
|
compatibility_attestation=ATTESTATION,
|
|
)
|
|
)
|
|
)
|
|
|
|
|
|
def test_new_ble_scan_generation_invalidates_old_candidates_before_io_and_on_failure(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
_set_scanned_k1(service, device_id="old-scan-device")
|
|
|
|
async def scenario() -> None:
|
|
entered = asyncio.Event()
|
|
release = asyncio.Event()
|
|
|
|
async def failing_scan(_duration_seconds: float) -> dict[str, Any]:
|
|
entered.set()
|
|
await release.wait()
|
|
raise RuntimeError("synthetic BLE scan failure")
|
|
|
|
monkeypatch.setattr(facade_module, "scan", failing_scan)
|
|
scan_task = asyncio.create_task(service.scan_ble(6.0))
|
|
await asyncio.wait_for(entered.wait(), timeout=1.0)
|
|
assert service.state()["devices"] == []
|
|
release.set()
|
|
with pytest.raises(RuntimeError, match="synthetic BLE scan failure"):
|
|
await asyncio.wait_for(scan_task, timeout=1.0)
|
|
|
|
asyncio.run(scenario())
|
|
|
|
assert service.state()["devices"] == []
|
|
assert service._devices == [] # noqa: SLF001
|
|
assert service._ble_device_last_seen_monotonic == {} # noqa: SLF001
|
|
|
|
|
|
def test_older_ble_scan_cannot_replace_a_newer_generation(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
first_entered = asyncio.Event()
|
|
release_first = asyncio.Event()
|
|
call_count = 0
|
|
|
|
def scan_result(device_id: str) -> dict[str, Any]:
|
|
return {
|
|
"devices": [
|
|
{
|
|
"macos_uuid": device_id,
|
|
"name": "XGR-K1",
|
|
"local_name": "XGR-K1",
|
|
"rssi": -44,
|
|
"k1_name_candidate": True,
|
|
}
|
|
]
|
|
}
|
|
|
|
async def overlapping_scan(_duration_seconds: float) -> dict[str, Any]:
|
|
nonlocal call_count
|
|
call_count += 1
|
|
if call_count == 1:
|
|
first_entered.set()
|
|
await release_first.wait()
|
|
return scan_result("older-generation")
|
|
return scan_result("newer-generation")
|
|
|
|
async def scenario() -> dict[str, Any]:
|
|
monkeypatch.setattr(facade_module, "scan", overlapping_scan)
|
|
older_task = asyncio.create_task(service.scan_ble(6.0))
|
|
await asyncio.wait_for(first_entered.wait(), timeout=1.0)
|
|
newer_state = await service.scan_ble(6.0)
|
|
release_first.set()
|
|
await asyncio.wait_for(older_task, timeout=1.0)
|
|
return newer_state
|
|
|
|
newer_state = asyncio.run(scenario())
|
|
|
|
assert [item["device_id"] for item in newer_state["devices"]] == ["newer-generation"]
|
|
assert [item["device_id"] for item in service.state()["devices"]] == [
|
|
"newer-generation"
|
|
]
|
|
|
|
|
|
def test_connect_stops_before_ble_write_when_a_new_scan_replaces_its_generation(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
_set_scanned_k1(service)
|
|
preflight_entered = threading.Event()
|
|
release_preflight = threading.Event()
|
|
|
|
def delayed_preflight(*_: object, **__: object) -> dict[str, Any]:
|
|
preflight_entered.set()
|
|
if not release_preflight.wait(timeout=1.0):
|
|
raise RuntimeError("test preflight release timed out")
|
|
return {
|
|
"schema_version": 1,
|
|
"adapter": "macOS Keychain",
|
|
"available": True,
|
|
"profile_enrolled": False,
|
|
"credential_source": "exact-firmware-profile",
|
|
}
|
|
|
|
async def replacement_scan(_duration_seconds: float) -> dict[str, Any]:
|
|
return {
|
|
"devices": [
|
|
{
|
|
"macos_uuid": "replacement-device",
|
|
"name": "XGR-NEW",
|
|
"local_name": "XGR-NEW",
|
|
"rssi": -40,
|
|
"k1_name_candidate": True,
|
|
}
|
|
]
|
|
}
|
|
|
|
@asynccontextmanager
|
|
async def forbidden_activation(
|
|
*_args: object,
|
|
**_kwargs: object,
|
|
) -> AsyncIterator[dict[str, Any]]:
|
|
raise AssertionError("changed discovery generation must stop before BLE write")
|
|
yield {}
|
|
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"ensure_wifi_profile_from_credential_source",
|
|
delayed_preflight,
|
|
)
|
|
monkeypatch.setattr(facade_module, "scan", replacement_scan)
|
|
monkeypatch.setattr(facade_module, "device_ap_activation_session", forbidden_activation)
|
|
|
|
async def scenario() -> None:
|
|
connect_task = asyncio.create_task(
|
|
service.connect(
|
|
ConnectRequest(
|
|
device_id="test-ble-transport",
|
|
connection_mode="quick-connect",
|
|
compatibility_attestation=QUICK_CONNECT_ATTESTATION,
|
|
)
|
|
)
|
|
)
|
|
entered = await asyncio.wait_for(
|
|
asyncio.to_thread(preflight_entered.wait, 1.0),
|
|
timeout=1.5,
|
|
)
|
|
assert entered is True
|
|
replacement_state = await service.scan_ble(6.0)
|
|
assert [item["device_id"] for item in replacement_state["devices"]] == [
|
|
"replacement-device"
|
|
]
|
|
release_preflight.set()
|
|
with pytest.raises(ValueError, match="изменились или устарели"):
|
|
await asyncio.wait_for(connect_task, timeout=1.0)
|
|
|
|
try:
|
|
asyncio.run(scenario())
|
|
finally:
|
|
release_preflight.set()
|
|
|
|
|
|
def test_verify_connection_adopts_scanned_existing_lan_without_device_write(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
_set_scanned_k1(service)
|
|
status_reads: list[tuple[str, float, bool]] = []
|
|
|
|
async def fake_status_read(
|
|
device_id: str,
|
|
*,
|
|
timeout_seconds: float,
|
|
rediscover: bool,
|
|
) -> dict[str, Any]:
|
|
status_reads.append((device_id, timeout_seconds, rediscover))
|
|
return _wifi_status_read("10.255.254.77")
|
|
|
|
async def forbidden_provision(*_: object, **__: object) -> dict[str, Any]:
|
|
raise AssertionError("connection.verify must never call Wi-Fi provisioning")
|
|
|
|
monkeypatch.setattr(facade_module, "read_wifi_status_once", fake_status_read)
|
|
monkeypatch.setattr(facade_module, "provision_wifi_once", forbidden_provision)
|
|
monkeypatch.setattr(facade_module, "_target_is_local_ipv4", lambda _target: False)
|
|
monkeypatch.setattr(facade_module, "_host_route_class", lambda _target: "direct-or-routed")
|
|
monkeypatch.setattr(facade_module, "_control_endpoint_reachable", lambda _target: True)
|
|
|
|
state = service.verify_connection(
|
|
ConnectionVerifyRequest(
|
|
device_id="test-ble-transport",
|
|
compatibility_attestation=ATTESTATION,
|
|
)
|
|
)
|
|
|
|
assert status_reads == [("test-ble-transport", 20.0, True)]
|
|
assert state["selected_device_id"] == "test-ble-transport"
|
|
assert state["k1_ip"] == "10.255.254.77"
|
|
assert state["connection_mode"] == "bridge"
|
|
assert state["device_ref"]["transport_alias"] == "test-ble-transport"
|
|
assert state["device_session"]["connectivity"] == "connected"
|
|
assert state["compatibility"]["attestation"]["topology"] == "direct-lan"
|
|
assert state["connection_verification"] == {
|
|
"status": "adopted",
|
|
"lease_state": "reachable",
|
|
"lease_generation": 1,
|
|
"endpoint_validation": "ble-wifi-status-read+mqtt-tcp-connect",
|
|
"network_reachability": "reachable",
|
|
"host_route_class": "direct-or-routed",
|
|
"address_source": "ble-wifi-status-read",
|
|
"connection_origin": "external-existing-network",
|
|
"admission_source": "connection.verify",
|
|
"address_changed": True,
|
|
"previous_address_present": False,
|
|
"write_performed": False,
|
|
"observed_at": "2026-07-20T12:00:00Z",
|
|
}
|
|
|
|
|
|
def test_verify_connection_adoption_requires_current_scan_candidate(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
|
|
async def forbidden_status_read(*_: object, **__: object) -> dict[str, Any]:
|
|
raise AssertionError("an unscanned device must not be probed")
|
|
|
|
async def forbidden_provision(*_: object, **__: object) -> dict[str, Any]:
|
|
raise AssertionError("connection.verify must never call Wi-Fi provisioning")
|
|
|
|
monkeypatch.setattr(facade_module, "read_wifi_status_once", forbidden_status_read)
|
|
monkeypatch.setattr(facade_module, "provision_wifi_once", forbidden_provision)
|
|
|
|
with pytest.raises(ValueError, match="найдите и выберите"):
|
|
service.verify_connection(
|
|
ConnectionVerifyRequest(
|
|
device_id="not-in-current-scan",
|
|
compatibility_attestation=ATTESTATION,
|
|
)
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("ipv4", "is_local", "route_class", "endpoint_reachable", "message"),
|
|
[
|
|
(None, False, "direct-or-routed", True, "не сообщил актуальный DHCP-адрес"),
|
|
("192.168.56.1", False, "device-ap", True, "не сообщил актуальный DHCP-адрес"),
|
|
("10.255.254.77", True, "direct-or-routed", True, "этому компьютеру"),
|
|
("10.255.254.77", False, "tunnel", True, "прямой локальный маршрут"),
|
|
("10.255.254.77", False, "default-route", True, "прямой локальный маршрут"),
|
|
("10.255.254.77", False, "direct-or-routed", False, "1883 недоступен"),
|
|
],
|
|
)
|
|
def test_verify_connection_adoption_fails_closed_before_establishing_lease(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
ipv4: str | None,
|
|
is_local: bool,
|
|
route_class: str,
|
|
endpoint_reachable: bool,
|
|
message: str,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
_set_scanned_k1(service)
|
|
|
|
async def fake_status_read(*_: object, **__: object) -> dict[str, Any]:
|
|
return _wifi_status_read(ipv4)
|
|
|
|
async def forbidden_provision(*_: object, **__: object) -> dict[str, Any]:
|
|
raise AssertionError("connection.verify must never call Wi-Fi provisioning")
|
|
|
|
monkeypatch.setattr(facade_module, "read_wifi_status_once", fake_status_read)
|
|
monkeypatch.setattr(facade_module, "provision_wifi_once", forbidden_provision)
|
|
monkeypatch.setattr(facade_module, "_target_is_local_ipv4", lambda _target: is_local)
|
|
monkeypatch.setattr(facade_module, "_host_route_class", lambda _target: route_class)
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"_control_endpoint_reachable",
|
|
lambda _target: endpoint_reachable,
|
|
)
|
|
|
|
with pytest.raises(RuntimeError, match=message):
|
|
service.verify_connection(
|
|
ConnectionVerifyRequest(
|
|
device_id="test-ble-transport",
|
|
compatibility_attestation=ATTESTATION,
|
|
)
|
|
)
|
|
|
|
state = service.state()
|
|
assert state["selected_device_id"] is None
|
|
assert state["k1_ip"] is None
|
|
assert state["connection_mode"] is None
|
|
assert state["device_session"] is None
|
|
assert state["connection_verification"].get("write_performed") is not True
|
|
|
|
|
|
def test_connection_verify_request_requires_exact_bridge_attestation() -> None:
|
|
with pytest.raises(ValidationError, match="provided together"):
|
|
ConnectionVerifyRequest(device_id="test-ble-transport")
|
|
|
|
with pytest.raises(ValidationError, match="topology=direct-lan"):
|
|
ConnectionVerifyRequest(
|
|
device_id="test-ble-transport",
|
|
compatibility_attestation=QUICK_CONNECT_ATTESTATION,
|
|
)
|
|
|
|
|
|
def test_verify_connection_refreshes_dynamic_dhcp_address_and_rotates_session(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
service._selected_device_id = "test-ble-transport" # noqa: SLF001
|
|
service._connection_mode = "bridge" # noqa: SLF001
|
|
service._k1_ip = "10.255.254.54" # noqa: SLF001
|
|
service._device_id = "known-k1" # noqa: SLF001
|
|
service._device_session_id = "old-device-session" # noqa: SLF001
|
|
service._device_session_opened_at = "2026-07-20T10:00:00Z" # noqa: SLF001
|
|
service._device_calibration = {"status": "available"} # noqa: SLF001
|
|
|
|
async def fake_status_read(*_: object, **__: object) -> dict[str, Any]:
|
|
return _wifi_status_read("10.255.254.77")
|
|
|
|
monkeypatch.setattr(facade_module, "read_wifi_status_once", fake_status_read)
|
|
monkeypatch.setattr(facade_module, "_target_is_local_ipv4", lambda _target: False)
|
|
|
|
state = service.verify_connection()
|
|
|
|
assert state["k1_ip"] == "10.255.254.77"
|
|
assert state["device_session"]["device_session_id"] != "old-device-session"
|
|
assert state["connection_verification"] == {
|
|
"status": "live-address-observed",
|
|
"lease_state": "configured",
|
|
"lease_generation": 1,
|
|
"endpoint_validation": "ble-wifi-status-read",
|
|
"network_reachability": "not-probed",
|
|
"address_changed": True,
|
|
"previous_address_present": True,
|
|
"write_performed": False,
|
|
"observed_at": "2026-07-20T12:00:00Z",
|
|
}
|
|
assert state["device_calibration"]["status"] == "unavailable"
|
|
|
|
|
|
def test_implicit_acquisition_target_uses_current_ble_dhcp_address(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
service._selected_device_id = "test-ble-transport" # noqa: SLF001
|
|
service._connection_mode = "bridge" # noqa: SLF001
|
|
service._k1_ip = "10.255.254.54" # noqa: SLF001
|
|
|
|
async def fake_status_read(*_: object, **__: object) -> dict[str, Any]:
|
|
return _wifi_status_read("10.255.254.77")
|
|
|
|
monkeypatch.setattr(facade_module, "read_wifi_status_once", fake_status_read)
|
|
monkeypatch.setattr(facade_module, "_target_is_local_ipv4", lambda _target: False)
|
|
|
|
state = service.prepare_acquisition(
|
|
PrepareAcquisitionRequest(
|
|
project_name=PROJECT_NAME,
|
|
compatibility_attestation=ATTESTATION,
|
|
)
|
|
)
|
|
|
|
assert state["k1_ip"] == "10.255.254.77"
|
|
assert state["acquisition"]["target_host"] == "10.255.254.77"
|
|
|
|
|
|
def test_control_session_reuses_reachable_process_owned_connection_lease(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
service._selected_device_id = "test-ble-transport" # noqa: SLF001
|
|
service._connection_mode = "bridge" # noqa: SLF001
|
|
service._k1_ip = "10.255.254.54" # noqa: SLF001
|
|
service._device_id = "known-k1" # noqa: SLF001
|
|
service._device_session_id = "known-session" # noqa: SLF001
|
|
service._compatibility_attestation = {"profile": "exact"} # noqa: SLF001
|
|
opened_hosts: list[str] = []
|
|
|
|
class FakeOpenControlSession:
|
|
def snapshot(self) -> dict[str, object]:
|
|
return {"state": "idle", "can_confirm_standby": False}
|
|
|
|
def open(self, *, host: str, **_: object) -> dict[str, object]:
|
|
opened_hosts.append(host)
|
|
return self.snapshot()
|
|
|
|
async def forbidden_status_read(*_: object, **__: object) -> dict[str, Any]:
|
|
raise AssertionError("reachable connection lease must not reopen BLE")
|
|
|
|
service._application_control_session = FakeOpenControlSession() # type: ignore[assignment] # noqa: SLF001
|
|
monkeypatch.setattr(facade_module, "read_wifi_status_once", forbidden_status_read)
|
|
monkeypatch.setattr(facade_module, "_control_endpoint_reachable", lambda _target: True)
|
|
|
|
state = service.open_application_control_session(
|
|
OpenApplicationControlSessionRequest(
|
|
operator_present=True,
|
|
owner_controlled_device=True,
|
|
lixelgo_closed=True,
|
|
battery_storage_confirmed=True,
|
|
expected_physical_state_confirmed=True,
|
|
timezone_name="Europe/Moscow",
|
|
)
|
|
)
|
|
|
|
assert opened_hosts == ["10.255.254.54"]
|
|
assert state["k1_ip"] == "10.255.254.54"
|
|
assert state["connection_verification"]["lease_state"] == "reachable"
|
|
assert state["connection_verification"]["network_reachability"] == "reachable"
|
|
open_operation = next(
|
|
operation
|
|
for operation in state["operations"]
|
|
if operation["action"] == "application-control.session.open"
|
|
)
|
|
assert open_operation["status"] == "succeeded"
|
|
assert open_operation["result"] == {
|
|
"lease_generation": 0,
|
|
"connection_lease_reused": True,
|
|
"recovery_performed": False,
|
|
"address_changed": False,
|
|
"device_write_performed": False,
|
|
}
|
|
|
|
|
|
def test_reachable_connection_lease_supports_repeated_independent_control_sessions(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
service._selected_device_id = "test-ble-transport" # noqa: SLF001
|
|
service._connection_mode = "bridge" # noqa: SLF001
|
|
service._k1_ip = "10.255.254.54" # noqa: SLF001
|
|
service._device_id = "known-k1" # noqa: SLF001
|
|
service._device_session_id = "known-session" # noqa: SLF001
|
|
service._connection_lease_generation = 7 # noqa: SLF001
|
|
service._compatibility_attestation = {"profile": "exact"} # noqa: SLF001
|
|
opened_hosts: list[str] = []
|
|
|
|
class FakeCompletedControlSession:
|
|
def snapshot(self) -> dict[str, object]:
|
|
return {
|
|
"state": "completed",
|
|
"can_open": True,
|
|
"can_confirm_standby": False,
|
|
}
|
|
|
|
def open(self, *, host: str, **_: object) -> dict[str, object]:
|
|
opened_hosts.append(host)
|
|
return self.snapshot()
|
|
|
|
async def forbidden_status_read(*_: object, **__: object) -> dict[str, Any]:
|
|
raise AssertionError("repeated scans must not repeat BLE or Wi-Fi setup")
|
|
|
|
service._application_control_session = FakeCompletedControlSession() # type: ignore[assignment] # noqa: SLF001
|
|
monkeypatch.setattr(facade_module, "read_wifi_status_once", forbidden_status_read)
|
|
monkeypatch.setattr(facade_module, "_control_endpoint_reachable", lambda _target: True)
|
|
request = OpenApplicationControlSessionRequest(
|
|
operator_present=True,
|
|
owner_controlled_device=True,
|
|
lixelgo_closed=True,
|
|
battery_storage_confirmed=True,
|
|
expected_physical_state_confirmed=True,
|
|
timezone_name="Europe/Moscow",
|
|
)
|
|
|
|
service.open_application_control_session(request)
|
|
state = service.open_application_control_session(request)
|
|
|
|
assert opened_hosts == ["10.255.254.54", "10.255.254.54"]
|
|
assert state["device_session"]["device_session_id"] == "known-session"
|
|
assert state["connection_verification"]["lease_generation"] == 7
|
|
open_operations = [
|
|
operation
|
|
for operation in state["operations"]
|
|
if operation["action"] == "application-control.session.open"
|
|
]
|
|
assert len(open_operations) == 2
|
|
assert {operation["status"] for operation in open_operations} == {"succeeded"}
|
|
assert all(
|
|
operation["result"]["connection_lease_reused"] is True for operation in open_operations
|
|
)
|
|
|
|
|
|
def test_control_session_recovers_changed_bridge_address_without_wifi_write(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
service._selected_device_id = "test-ble-transport" # noqa: SLF001
|
|
service._connection_mode = "bridge" # noqa: SLF001
|
|
service._k1_ip = "10.255.254.54" # noqa: SLF001
|
|
service._device_id = "known-k1" # noqa: SLF001
|
|
service._device_session_id = "old-session" # noqa: SLF001
|
|
service._compatibility_attestation = {"profile": "exact"} # noqa: SLF001
|
|
opened_hosts: list[str] = []
|
|
status_calls: list[dict[str, object]] = []
|
|
|
|
class FakeOpenControlSession:
|
|
def snapshot(self) -> dict[str, object]:
|
|
return {"state": "idle", "can_confirm_standby": False}
|
|
|
|
def open(self, *, host: str, **_: object) -> dict[str, object]:
|
|
opened_hosts.append(host)
|
|
return self.snapshot()
|
|
|
|
async def fake_status_read(*_: object, **kwargs: object) -> dict[str, Any]:
|
|
status_calls.append(kwargs)
|
|
return _wifi_status_read("10.255.254.77")
|
|
|
|
service._application_control_session = FakeOpenControlSession() # type: ignore[assignment] # noqa: SLF001
|
|
monkeypatch.setattr(facade_module, "read_wifi_status_once", fake_status_read)
|
|
monkeypatch.setattr(facade_module, "_target_is_local_ipv4", lambda _target: False)
|
|
monkeypatch.setattr(facade_module, "_host_route_class", lambda _target: "direct-or-routed")
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"_control_endpoint_reachable",
|
|
lambda target: target == "10.255.254.77",
|
|
)
|
|
|
|
state = service.open_application_control_session(
|
|
OpenApplicationControlSessionRequest(
|
|
operator_present=True,
|
|
owner_controlled_device=True,
|
|
lixelgo_closed=True,
|
|
battery_storage_confirmed=True,
|
|
expected_physical_state_confirmed=True,
|
|
timezone_name="Europe/Moscow",
|
|
)
|
|
)
|
|
|
|
assert status_calls == [{"timeout_seconds": 20.0, "rediscover": True}]
|
|
assert opened_hosts == ["10.255.254.77"]
|
|
assert state["k1_ip"] == "10.255.254.77"
|
|
assert state["device_session"]["device_session_id"] != "old-session"
|
|
assert state["connection_verification"]["status"] == "recovered"
|
|
assert state["connection_verification"]["write_performed"] is False
|
|
open_operation = next(
|
|
operation
|
|
for operation in state["operations"]
|
|
if operation["action"] == "application-control.session.open"
|
|
)
|
|
assert open_operation["result"]["recovery_performed"] is True
|
|
assert open_operation["result"]["address_changed"] is True
|
|
assert open_operation["result"]["device_write_performed"] is False
|
|
|
|
|
|
def test_control_session_prestart_failure_is_journaled_and_marks_lease_offline(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
service._selected_device_id = "test-ble-transport" # noqa: SLF001
|
|
service._connection_mode = "bridge" # noqa: SLF001
|
|
service._k1_ip = "10.255.254.54" # noqa: SLF001
|
|
service._device_id = "known-k1" # noqa: SLF001
|
|
service._device_session_id = "known-session" # noqa: SLF001
|
|
service._compatibility_attestation = {"profile": "exact"} # noqa: SLF001
|
|
|
|
async def failed_status_read(*_: object, **__: object) -> dict[str, Any]:
|
|
raise TimeoutError("synthetic BLE recovery timeout")
|
|
|
|
monkeypatch.setattr(facade_module, "read_wifi_status_once", failed_status_read)
|
|
monkeypatch.setattr(facade_module, "_control_endpoint_reachable", lambda _target: False)
|
|
monkeypatch.setattr(facade_module, "_host_route_class", lambda _target: "direct-or-routed")
|
|
|
|
with pytest.raises(
|
|
facade_module.ConnectionLeaseUnavailable,
|
|
match="повторное чтение состояния",
|
|
):
|
|
service.open_application_control_session(
|
|
OpenApplicationControlSessionRequest(
|
|
operator_present=True,
|
|
owner_controlled_device=True,
|
|
lixelgo_closed=True,
|
|
battery_storage_confirmed=True,
|
|
expected_physical_state_confirmed=True,
|
|
timezone_name="Europe/Moscow",
|
|
)
|
|
)
|
|
|
|
state = service.state()
|
|
assert state["application_control_session"]["state"] == "idle"
|
|
assert state["device_session"]["connectivity"] == "offline"
|
|
assert state["connection_verification"]["lease_state"] == "disconnected"
|
|
open_operation = next(
|
|
operation
|
|
for operation in state["operations"]
|
|
if operation["action"] == "application-control.session.open"
|
|
)
|
|
assert open_operation["status"] == "failed"
|
|
assert open_operation["error"] == {
|
|
"category": "connection",
|
|
"code": "connection_lease_ble_recovery_failed",
|
|
"retryable": False,
|
|
"safe_to_retry": True,
|
|
"side_effect_status": "none",
|
|
}
|
|
|
|
|
|
def test_bridge_route_mismatch_stops_before_ble_recovery_and_vendor_commands(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
service._selected_device_id = "test-ble-transport" # noqa: SLF001
|
|
service._connection_mode = "bridge" # noqa: SLF001
|
|
service._k1_ip = "192.168.68.50" # noqa: SLF001
|
|
service._device_id = "known-k1" # noqa: SLF001
|
|
service._device_session_id = "known-session" # noqa: SLF001
|
|
service._compatibility_attestation = {"profile": "exact"} # noqa: SLF001
|
|
|
|
async def forbidden_status_read(*_: object, **__: object) -> dict[str, Any]:
|
|
raise AssertionError("host route mismatch must stop before BLE recovery")
|
|
|
|
monkeypatch.setattr(facade_module, "_control_endpoint_reachable", lambda _target: False)
|
|
monkeypatch.setattr(facade_module, "_host_route_class", lambda _target: "tunnel")
|
|
monkeypatch.setattr(facade_module, "read_wifi_status_once", forbidden_status_read)
|
|
|
|
with pytest.raises(
|
|
facade_module.ConnectionLeaseUnavailable,
|
|
match="другой локальной сети",
|
|
):
|
|
service.open_application_control_session(
|
|
OpenApplicationControlSessionRequest(
|
|
operator_present=True,
|
|
owner_controlled_device=True,
|
|
lixelgo_closed=True,
|
|
battery_storage_confirmed=True,
|
|
expected_physical_state_confirmed=True,
|
|
timezone_name="Europe/Moscow",
|
|
)
|
|
)
|
|
|
|
state = service.state()
|
|
assert state["connection_verification"]["reason_code"] == (
|
|
"connection_lease_host_route_mismatch"
|
|
)
|
|
assert state["connection_verification"]["endpoint_validation"] == "host-route"
|
|
assert state["connection_verification"]["host_route_class"] == "tunnel"
|
|
assert state["application_control_session"]["state"] == "idle"
|
|
|
|
|
|
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"]["mount_type"] == "handheld"
|
|
assert state["acquisition"]["gnss_mode"] == "none"
|
|
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=" K1 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_acquisition_is_unbounded_by_default_and_accepts_ten_hour_hint() -> None:
|
|
unbounded = PrepareAcquisitionRequest(
|
|
project_name=PROJECT_NAME,
|
|
host="192.168.1.20",
|
|
compatibility_attestation=ATTESTATION,
|
|
)
|
|
ten_hours = PrepareAcquisitionRequest(
|
|
project_name=PROJECT_NAME,
|
|
host="192.168.1.20",
|
|
duration_seconds=10 * 60 * 60,
|
|
compatibility_attestation=ATTESTATION,
|
|
)
|
|
|
|
assert unbounded.duration_seconds is None
|
|
assert ten_hours.duration_seconds == 36_000
|
|
|
|
|
|
def test_connection_modes_require_their_exact_topology_attestation() -> None:
|
|
assert (
|
|
ConnectRequest(
|
|
device_id="synthetic-device",
|
|
connection_mode="quick-connect",
|
|
compatibility_attestation=QUICK_CONNECT_ATTESTATION,
|
|
).connection_mode
|
|
== "quick-connect"
|
|
)
|
|
assert (
|
|
ConnectRequest(
|
|
device_id="synthetic-device",
|
|
ssid="synthetic-network",
|
|
password=SecretStr(PRIMARY_TEST_CREDENTIAL),
|
|
connection_mode="direct-connect",
|
|
compatibility_attestation=DIRECT_CONNECT_ATTESTATION,
|
|
).connection_mode
|
|
== "direct-connect"
|
|
)
|
|
with pytest.raises(ValidationError):
|
|
ConnectRequest(
|
|
device_id="synthetic-device",
|
|
connection_mode="quick-connect",
|
|
compatibility_attestation=ATTESTATION,
|
|
)
|
|
with pytest.raises(ValidationError, match="host Wi-Fi profile"):
|
|
ConnectRequest(
|
|
device_id="synthetic-device",
|
|
ssid="synthetic-network",
|
|
password=SecretStr(PRIMARY_TEST_CREDENTIAL),
|
|
connection_mode="quick-connect",
|
|
compatibility_attestation=QUICK_CONNECT_ATTESTATION,
|
|
)
|
|
with pytest.raises(ValidationError, match="32 UTF-8 bytes"):
|
|
ConnectRequest(
|
|
device_id="synthetic-device",
|
|
ssid="🛰️" * 9,
|
|
password=SecretStr(PRIMARY_TEST_CREDENTIAL),
|
|
compatibility_attestation=ATTESTATION,
|
|
)
|
|
with pytest.raises(ValidationError, match="64 UTF-8 bytes"):
|
|
ConnectRequest(
|
|
device_id="synthetic-device",
|
|
ssid="synthetic-network",
|
|
password=SecretStr("🔒" * 17),
|
|
compatibility_attestation=ATTESTATION,
|
|
)
|
|
|
|
|
|
def test_only_physically_accepted_mount_and_gnss_values_are_admitted() -> None:
|
|
with pytest.raises(ValidationError):
|
|
PrepareAcquisitionRequest(
|
|
project_name=PROJECT_NAME,
|
|
host="192.168.1.20",
|
|
mount_type="uav", # type: ignore[arg-type]
|
|
compatibility_attestation=ATTESTATION,
|
|
)
|
|
with pytest.raises(ValidationError):
|
|
PrepareAcquisitionRequest(
|
|
project_name=PROJECT_NAME,
|
|
host="192.168.1.20",
|
|
gnss_mode="rtk", # type: ignore[arg-type]
|
|
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",
|
|
"verification": "live-device-info",
|
|
"basis": "selected-profile-live-device-info-required",
|
|
"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_profile_selected_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_plugin_commanded_acquisition_keeps_start_and_stop_as_explicit_actions(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
control = FakeInteractiveControlSession()
|
|
service._application_control_session = control # type: ignore[assignment] # noqa: SLF001
|
|
service._k1_ip = "192.168.1.20" # noqa: SLF001
|
|
service._compatibility_attestation = {"profile": "exact"} # noqa: SLF001
|
|
|
|
prepared = service.prepare_acquisition(
|
|
PrepareAcquisitionRequest(
|
|
project_name="TEST001",
|
|
host="192.168.1.20",
|
|
compatibility_attestation=ATTESTATION,
|
|
)
|
|
)
|
|
acquisition_id = prepared["acquisition"]["acquisition_id"]
|
|
assert prepared["acquisition"]["control_mode"] == "plugin-commanded"
|
|
assert control.state == "project-ready"
|
|
assert runtime.start_calls == []
|
|
|
|
with pytest.raises(ValueError, match="подтверждения присутствия"):
|
|
service.start_acquisition(StartAcquisitionRequest(acquisition_id=acquisition_id))
|
|
assert control.start_projects == []
|
|
|
|
service.start_acquisition(
|
|
StartAcquisitionRequest(
|
|
acquisition_id=acquisition_id,
|
|
physical_acceptance=PHYSICAL_ACCEPTANCE,
|
|
)
|
|
)
|
|
assert control.start_projects == ["TEST001"]
|
|
runtime.mark_ready()
|
|
runtime.pcl_frames = 1
|
|
acquiring = service.state()
|
|
assert acquiring["acquisition"]["state"] == "acquiring"
|
|
|
|
stopping = service.stop_acquisition(
|
|
StopAcquisitionRequest(
|
|
acquisition_id=acquisition_id,
|
|
mode="graceful",
|
|
physical_acceptance=PHYSICAL_ACCEPTANCE,
|
|
)
|
|
)
|
|
assert control.stop_calls == 1
|
|
assert stopping["acquisition"]["state"] == "awaiting_external_stop"
|
|
assert stopping["last_operation"]["status"] == "running"
|
|
|
|
control.state = "completed"
|
|
completed = service.state()
|
|
assert control.stop_calls == 1
|
|
assert completed["acquisition"]["state"] == "completed"
|
|
assert completed["acquisition"]["result"]["device_state"] == "ready"
|
|
assert completed["last_operation"]["status"] == "succeeded"
|
|
assert runtime.stop_calls == 1
|
|
assert completed["live_perception_shadow"]["active"] is False
|
|
|
|
|
|
def test_next_scan_retires_stale_terminal_live_perception_ingress_before_start(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
service.live_perception_ingress.begin_session("stale-completed-session")
|
|
prepared = service.prepare_acquisition(
|
|
PrepareAcquisitionRequest(
|
|
project_name=PROJECT_NAME,
|
|
host="192.168.1.20",
|
|
compatibility_attestation=ATTESTATION,
|
|
)
|
|
)
|
|
|
|
state = service.start_acquisition(
|
|
StartAcquisitionRequest(acquisition_id=prepared["acquisition"]["acquisition_id"])
|
|
)
|
|
|
|
assert runtime.start_calls
|
|
assert state["acquisition"]["state"] == "starting"
|
|
assert state["live_perception_shadow"]["active"] is True
|
|
assert state["live_perception_shadow"]["session_id"] != "stale-completed-session"
|
|
|
|
|
|
def test_confirmed_scanning_activates_and_records_right_camera(
|
|
tmp_path: Path,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
control = FakeInteractiveControlSession()
|
|
service._application_control_session = control # type: ignore[assignment] # noqa: SLF001
|
|
service._k1_ip = "192.168.1.20" # noqa: SLF001
|
|
service._compatibility_attestation = {"profile": "exact"} # noqa: SLF001
|
|
|
|
prepared = service.prepare_acquisition(
|
|
PrepareAcquisitionRequest(
|
|
project_name="TEST001",
|
|
host="192.168.1.20",
|
|
compatibility_attestation=ATTESTATION,
|
|
)
|
|
)
|
|
acquisition_id = prepared["acquisition"]["acquisition_id"]
|
|
service.start_acquisition(
|
|
StartAcquisitionRequest(
|
|
acquisition_id=acquisition_id,
|
|
physical_acceptance=PHYSICAL_ACCEPTANCE,
|
|
)
|
|
)
|
|
out_dir = service._acquisition_out_dir # noqa: SLF001
|
|
assert out_dir is not None
|
|
out_dir.mkdir(parents=True)
|
|
events: list[tuple[str, object]] = []
|
|
camera_state: dict[str, object] = {
|
|
"phase": "idle",
|
|
"active_source_id": None,
|
|
"recording": {"active": False},
|
|
}
|
|
|
|
def select_camera(source_id: str, target: str) -> dict[str, object]:
|
|
events.append(("select", (source_id, target)))
|
|
camera_state["phase"] = "selected"
|
|
camera_state["active_source_id"] = source_id
|
|
return dict(camera_state)
|
|
|
|
def start_recording(session_dir: Path) -> dict[str, object]:
|
|
events.append(("record", session_dir))
|
|
camera_state["recording"] = {"active": True}
|
|
camera_state["phase"] = "connecting"
|
|
return dict(camera_state)
|
|
|
|
monkeypatch.setattr(service.camera_preview, "snapshot", lambda: dict(camera_state))
|
|
monkeypatch.setattr(service.camera_preview, "select", select_camera)
|
|
monkeypatch.setattr(service.camera_preview, "start_recording", start_recording)
|
|
|
|
service._activate_default_acquisition_camera() # noqa: SLF001
|
|
|
|
assert events == [
|
|
("select", ("sensor.camera.right", "192.168.1.20")),
|
|
("record", out_dir),
|
|
]
|
|
assert camera_state["active_source_id"] == "sensor.camera.right"
|
|
assert camera_state["recording"] == {"active": True}
|
|
assert runtime.source_mode == "live"
|
|
|
|
|
|
def test_device_standby_retires_sources_after_terminal_local_stop_failure(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
control = FakeInteractiveControlSession()
|
|
service._application_control_session = control # type: ignore[assignment] # noqa: SLF001
|
|
service._k1_ip = "192.168.1.20" # noqa: SLF001
|
|
service._compatibility_attestation = {"profile": "exact"} # noqa: SLF001
|
|
|
|
prepared = service.prepare_acquisition(
|
|
PrepareAcquisitionRequest(
|
|
project_name="TEST001",
|
|
host="192.168.1.20",
|
|
compatibility_attestation=ATTESTATION,
|
|
)
|
|
)
|
|
acquisition_id = prepared["acquisition"]["acquisition_id"]
|
|
service.start_acquisition(
|
|
StartAcquisitionRequest(
|
|
acquisition_id=acquisition_id,
|
|
physical_acceptance=PHYSICAL_ACCEPTANCE,
|
|
)
|
|
)
|
|
runtime.mark_ready()
|
|
runtime.pcl_frames = 1
|
|
service.state()
|
|
|
|
stopping = service.stop_acquisition(
|
|
StopAcquisitionRequest(
|
|
acquisition_id=acquisition_id,
|
|
mode="graceful",
|
|
physical_acceptance=PHYSICAL_ACCEPTANCE,
|
|
)
|
|
)
|
|
stop_operation = stopping["last_operation"]
|
|
with service._lock: # noqa: SLF001
|
|
assert service._acquisition is not None # noqa: SLF001
|
|
service._acquisition.transition( # noqa: SLF001
|
|
"failed",
|
|
message_code="acquisition.camera_failed",
|
|
result={"camera_failure_code": "camera-source-ended"},
|
|
)
|
|
service._operations.transition( # noqa: SLF001
|
|
stop_operation["operation_id"],
|
|
"failed",
|
|
stage_code="runtime-failed",
|
|
message_code="acquisition.stop.runtime_failed",
|
|
error={
|
|
"category": "stream",
|
|
"code": "runtime-failed",
|
|
"retryable": False,
|
|
"safe_to_retry": False,
|
|
"side_effect_status": "unknown",
|
|
},
|
|
)
|
|
|
|
control.state = "completed"
|
|
recovered = service.state()
|
|
|
|
assert control.stop_calls == 1
|
|
assert control.state == "completed"
|
|
assert recovered["acquisition"]["state"] == "failed"
|
|
assert recovered["application_control_session"]["state"] == "completed"
|
|
assert runtime.stop_calls == 1
|
|
|
|
|
|
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 | None,
|
|
project_name: str,
|
|
) -> None:
|
|
assert duration_seconds is None
|
|
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_selected_for_live_device_info_verification(
|
|
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-selected",
|
|
"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"] == (
|
|
"selected-profile-live-device-info-required"
|
|
)
|
|
assert attested["device_session"]["compatibility_profile_id"] == (
|
|
XGRIDS_K1_COMPATIBILITY_PROFILE_ID
|
|
)
|
|
|
|
|
|
def test_prepare_rejects_device_ap_without_completed_quick_connect(tmp_path: Path) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
|
|
with pytest.raises(ValueError, match="connection flow"):
|
|
service.prepare_acquisition(
|
|
PrepareAcquisitionRequest(
|
|
project_name=PROJECT_NAME,
|
|
host="192.168.56.1",
|
|
compatibility_attestation=QUICK_CONNECT_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"
|
|
assert failed["source_mode"] == "idle"
|
|
assert failed["phase"] != "error"
|
|
assert runtime.stop_calls == 1
|
|
|
|
|
|
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)
|
|
_set_scanned_devices(
|
|
service,
|
|
[{"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)
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"_host_route_class",
|
|
lambda _target: "direct-or-routed",
|
|
)
|
|
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 connected["connection_mode"] == "bridge"
|
|
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_bridge_network_change_retires_terminal_acquisition_and_receiver_error(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
_set_scanned_devices(service, [{"device_id": "k1-a", "name": "XGR-TEST-A"}])
|
|
prepared = service.prepare_acquisition(
|
|
PrepareAcquisitionRequest(
|
|
project_name=PROJECT_NAME,
|
|
host="192.168.1.20",
|
|
compatibility_attestation=ATTESTATION,
|
|
)
|
|
)
|
|
service.abort_acquisition(
|
|
AbortAcquisitionRequest(
|
|
acquisition_id=prepared["acquisition"]["acquisition_id"],
|
|
)
|
|
)
|
|
runtime.phase = "error"
|
|
runtime.source_mode = "live"
|
|
stop_calls_before_connect = runtime.stop_calls
|
|
|
|
async def fake_provision(*_: object, **__: object) -> dict[str, Any]:
|
|
return {
|
|
"started_at_utc": "2026-07-28T18:11:37Z",
|
|
"completed_at_utc": "2026-07-28T18:11:44Z",
|
|
"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)
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"_host_route_class",
|
|
lambda _target: "direct-or-routed",
|
|
)
|
|
connected = asyncio.run(
|
|
service.connect(
|
|
ConnectRequest(
|
|
device_id="k1-a",
|
|
ssid="lab-network",
|
|
password=SecretStr(PRIMARY_TEST_CREDENTIAL),
|
|
connection_mode="bridge",
|
|
compatibility_attestation=ATTESTATION,
|
|
)
|
|
)
|
|
)
|
|
|
|
assert connected["connection_mode"] == "bridge"
|
|
assert connected["k1_ip"] == "192.168.1.20"
|
|
assert connected["acquisition"] is None
|
|
assert connected["source_mode"] == "idle"
|
|
assert connected["phase"] == "connected"
|
|
assert runtime.stop_calls == stop_calls_before_connect + 1
|
|
|
|
|
|
def test_bridge_provisioning_reports_host_route_mismatch_without_hiding_device_success(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
_set_scanned_devices(service, [{"device_id": "k1-a", "name": "XGR-TEST-A"}])
|
|
|
|
async def fake_provision(*_: object, **__: object) -> dict[str, Any]:
|
|
return {
|
|
"started_at_utc": "2026-07-28T19:43:03Z",
|
|
"completed_at_utc": "2026-07-28T19:43:16Z",
|
|
"profile_id": "xgrids-k1-fw3-wifi-v1",
|
|
"outcome": "lan_address_observed",
|
|
"observations": [{"status": {"ipv4": "192.168.68.50"}}],
|
|
}
|
|
|
|
monkeypatch.setattr(facade_module, "provision_wifi_once", fake_provision)
|
|
monkeypatch.setattr(facade_module, "_host_route_class", lambda _target: "tunnel")
|
|
|
|
connected = asyncio.run(
|
|
service.connect(
|
|
ConnectRequest(
|
|
device_id="k1-a",
|
|
ssid="lab-router",
|
|
password=SecretStr(PRIMARY_TEST_CREDENTIAL),
|
|
connection_mode="bridge",
|
|
compatibility_attestation=ATTESTATION,
|
|
)
|
|
)
|
|
)
|
|
|
|
assert connected["k1_ip"] == "192.168.68.50"
|
|
assert connected["phase"] == "device_selected"
|
|
assert "компьютер подключён к другой сети" in connected["message"]
|
|
assert connected["device_session"]["connectivity"] == "offline"
|
|
assert connected["connection_verification"] == {
|
|
"status": "host-route-mismatch",
|
|
"lease_state": "disconnected",
|
|
"lease_generation": 1,
|
|
"endpoint_validation": "host-route",
|
|
"network_reachability": "unreachable",
|
|
"reason_code": "connection_lease_host_route_mismatch",
|
|
"host_route_class": "tunnel",
|
|
"write_performed": True,
|
|
"observed_at": connected["connection_verification"]["observed_at"],
|
|
}
|
|
operation = next(
|
|
item for item in connected["operations"] if item["action"] == "network.provision"
|
|
)
|
|
assert operation["status"] == "succeeded"
|
|
assert operation["result"]["host_route_ready"] is False
|
|
assert operation["result"]["host_route_class"] == "tunnel"
|
|
|
|
|
|
def test_quick_connect_activates_the_device_ap_then_associates_the_host(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
_set_scanned_devices(service, [{"device_id": "k1-a", "name": "XGR-TEST-A"}])
|
|
activation_calls: list[str] = []
|
|
association_calls: list[tuple[Path, str, str]] = []
|
|
ble_session_open = False
|
|
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"ensure_wifi_profile_from_credential_source",
|
|
lambda *_args, **_kwargs: {
|
|
"schema_version": 1,
|
|
"adapter": "macOS Keychain",
|
|
"available": True,
|
|
"profile_enrolled": True,
|
|
"credential_source": "exact-firmware-profile",
|
|
},
|
|
)
|
|
|
|
@asynccontextmanager
|
|
async def fake_activation_session(device_id: str, **_: object) -> AsyncIterator[dict[str, Any]]:
|
|
nonlocal ble_session_open
|
|
activation_calls.append(device_id)
|
|
ble_session_open = True
|
|
try:
|
|
yield {
|
|
"profile_id": "xgrids-k1-fw3-quick-connect-ap-v1",
|
|
"started_at_utc": "2026-07-19T15:00:00Z",
|
|
"completed_at_utc": "2026-07-19T15:00:01Z",
|
|
"outcome": "ap_ready_observed",
|
|
"ready_observed": True,
|
|
"write_performed": True,
|
|
"write_mode": "with_response",
|
|
}
|
|
finally:
|
|
ble_session_open = False
|
|
|
|
def fake_associate(
|
|
helper_path: Path,
|
|
profile_id: str,
|
|
expected_ssid: str,
|
|
**_: object,
|
|
) -> dict[str, Any]:
|
|
assert ble_session_open
|
|
association_calls.append((helper_path, profile_id, expected_ssid))
|
|
return {
|
|
"schema_version": 1,
|
|
"adapter": "CoreWLAN",
|
|
"outcome": "associated",
|
|
"already_associated": False,
|
|
"profile_enrolled": True,
|
|
"scan_attempt_count": 2,
|
|
"scan_elapsed_ms": 900,
|
|
"credential_source": "system-wifi-keychain",
|
|
}
|
|
|
|
async def forbidden_provisioning_write(*_: object, **__: object) -> dict[str, Any]:
|
|
raise AssertionError("Quick Connect must not send router credentials to the K1")
|
|
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"device_ap_activation_session",
|
|
fake_activation_session,
|
|
)
|
|
monkeypatch.setattr(facade_module, "associate_with_wifi_profile_once", fake_associate)
|
|
monkeypatch.setattr(facade_module, "provision_wifi_once", forbidden_provisioning_write)
|
|
monkeypatch.setattr(facade_module, "_target_is_local_ipv4", lambda _target: False)
|
|
|
|
state = asyncio.run(
|
|
service.connect(
|
|
ConnectRequest(
|
|
device_id="k1-a",
|
|
connection_mode="quick-connect",
|
|
compatibility_attestation=QUICK_CONNECT_ATTESTATION,
|
|
)
|
|
)
|
|
)
|
|
|
|
assert activation_calls == ["k1-a"]
|
|
assert not ble_session_open
|
|
assert len(association_calls) == 1
|
|
assert association_calls[0][0].name == "associate_wifi.swift"
|
|
assert association_calls[0][1] == facade_module.quick_connect_host_profile_id("XGR-TEST-A")
|
|
assert association_calls[0][2] == "XGR-TEST-A"
|
|
assert state["connection_mode"] == "quick-connect"
|
|
assert state["k1_ip"] == "192.168.56.1"
|
|
assert state["compatibility"]["attestation"]["topology"] == "device-ap"
|
|
quick_sessions = sorted(service.evidence_root.glob("*viewer_k1_ap_association*"))
|
|
assert len(quick_sessions) == 1
|
|
assert not (quick_sessions[0] / "provisioning.sensitive.json").exists()
|
|
redacted_manifest = (quick_sessions[0] / "manifest.redacted.json").read_text(encoding="utf-8")
|
|
assert PRIMARY_TEST_CREDENTIAL not in redacted_manifest
|
|
assert "host_wifi_profile_id" in redacted_manifest
|
|
assert '"host_wifi_profile_ready_before_device_write": true' in redacted_manifest
|
|
assert '"credentials_resolved_by_plugin": true' in redacted_manifest
|
|
assert "credential_provider_id" in redacted_manifest
|
|
assert "device_ap_activation_profile_id" in redacted_manifest
|
|
assert (quick_sessions[0] / "ap-activation.redacted.json").exists()
|
|
assert (
|
|
service._camera_target_for_session( # noqa: SLF001
|
|
state["device_session"]["device_session_id"]
|
|
)
|
|
== "192.168.56.1"
|
|
)
|
|
|
|
prepared = service.prepare_acquisition(
|
|
PrepareAcquisitionRequest(
|
|
project_name=PROJECT_NAME,
|
|
compatibility_attestation=QUICK_CONNECT_ATTESTATION,
|
|
)
|
|
)
|
|
assert prepared["acquisition"]["target_host"] == "192.168.56.1"
|
|
|
|
|
|
def test_direct_connect_reuses_the_reviewed_ble_provisioning_frame(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
_set_scanned_devices(service, [{"device_id": "k1-a", "name": "XGR-TEST-A"}])
|
|
provisioning_calls: list[tuple[str, str, str]] = []
|
|
|
|
async def fake_provision(
|
|
device_id: str,
|
|
ssid: str,
|
|
password: str,
|
|
**_: object,
|
|
) -> dict[str, Any]:
|
|
provisioning_calls.append((device_id, ssid, password))
|
|
return {
|
|
"started_at_utc": "2026-07-19T01:00:00Z",
|
|
"completed_at_utc": "2026-07-19T01:00:01Z",
|
|
"profile_id": "xgrids-k1-fw3-wifi-v1",
|
|
"outcome": "lan_address_observed",
|
|
"observations": [{"status": {"ipv4": "172.20.10.2"}}],
|
|
}
|
|
|
|
def forbidden_host_association(*_: object, **__: object) -> dict[str, Any]:
|
|
raise AssertionError("Direct Connect must not switch the host Wi-Fi network")
|
|
|
|
monkeypatch.setattr(facade_module, "provision_wifi_once", fake_provision)
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"associate_with_wifi_profile_once",
|
|
forbidden_host_association,
|
|
)
|
|
monkeypatch.setattr(facade_module, "_target_is_local_ipv4", lambda _target: False)
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"_host_route_class",
|
|
lambda _target: "direct-or-routed",
|
|
)
|
|
|
|
state = asyncio.run(
|
|
service.connect(
|
|
ConnectRequest(
|
|
device_id="k1-a",
|
|
ssid="controller-hotspot",
|
|
password=SecretStr(PRIMARY_TEST_CREDENTIAL),
|
|
connection_mode="direct-connect",
|
|
compatibility_attestation=DIRECT_CONNECT_ATTESTATION,
|
|
)
|
|
)
|
|
)
|
|
|
|
assert provisioning_calls == [("k1-a", "controller-hotspot", PRIMARY_TEST_CREDENTIAL)]
|
|
assert state["connection_mode"] == "direct-connect"
|
|
assert state["k1_ip"] == "172.20.10.2"
|
|
assert state["compatibility"]["attestation"]["topology"] == ("controller-hotspot")
|
|
|
|
|
|
def test_quick_connect_missing_credential_provider_stops_before_ap_write(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
_set_scanned_devices(service, [{"device_id": "k1-a", "name": "XGR-TEST-A"}])
|
|
service._selected_device_id = "previous-k1" # noqa: SLF001
|
|
service._k1_ip = "192.168.1.20" # noqa: SLF001
|
|
service._connection_mode = "bridge" # noqa: SLF001
|
|
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"ensure_wifi_profile_from_credential_source",
|
|
lambda *_args, **_kwargs: {
|
|
"schema_version": 1,
|
|
"adapter": "macOS Keychain",
|
|
"available": False,
|
|
"profile_enrolled": False,
|
|
"credential_source": None,
|
|
},
|
|
)
|
|
|
|
@asynccontextmanager
|
|
async def forbidden_activation(
|
|
*_args: object, **_kwargs: object
|
|
) -> AsyncIterator[dict[str, Any]]:
|
|
raise AssertionError("missing host credential must stop before the K1 AP write")
|
|
yield {}
|
|
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"device_ap_activation_session",
|
|
forbidden_activation,
|
|
)
|
|
|
|
with pytest.raises(RuntimeError, match="credential-source-unavailable"):
|
|
asyncio.run(
|
|
service.connect(
|
|
ConnectRequest(
|
|
device_id="k1-a",
|
|
connection_mode="quick-connect",
|
|
compatibility_attestation=QUICK_CONNECT_ATTESTATION,
|
|
)
|
|
)
|
|
)
|
|
|
|
state = service.state()
|
|
assert state["selected_device_id"] == "previous-k1"
|
|
assert state["k1_ip"] == "192.168.1.20"
|
|
assert state["connection_mode"] == "bridge"
|
|
assert not list(service.evidence_root.glob("*viewer_k1_ap_association*"))
|
|
operation = next(item for item in state["operations"] if item["action"] == "network.provision")
|
|
assert operation["status"] == "failed"
|
|
assert operation["error"]["side_effect_status"] == "none"
|
|
assert operation["error"]["safe_to_retry"] is True
|
|
|
|
|
|
def test_quick_connect_does_not_start_host_wifi_without_ap_ready_flag(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
_set_scanned_devices(service, [{"device_id": "k1-a", "name": "XGR-TEST-A"}])
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"ensure_wifi_profile_from_credential_source",
|
|
lambda *_args, **_kwargs: {
|
|
"schema_version": 1,
|
|
"adapter": "macOS Keychain",
|
|
"available": True,
|
|
"profile_enrolled": False,
|
|
"credential_source": "exact-firmware-profile",
|
|
},
|
|
)
|
|
|
|
@asynccontextmanager
|
|
async def not_ready_session(*_: object, **__: object) -> AsyncIterator[dict[str, Any]]:
|
|
yield {
|
|
"profile_id": "xgrids-k1-fw3-quick-connect-ap-v1",
|
|
"started_at_utc": "2026-07-19T15:00:00Z",
|
|
"completed_at_utc": "2026-07-19T15:00:15Z",
|
|
"outcome": "no_status_change_before_timeout",
|
|
"ready_observed": False,
|
|
"write_performed": True,
|
|
"write_mode": "with_response",
|
|
}
|
|
|
|
def forbidden_association(*_: object, **__: object) -> dict[str, Any]:
|
|
raise AssertionError("host Wi-Fi must wait for the canonical AP-ready flag")
|
|
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"device_ap_activation_session",
|
|
not_ready_session,
|
|
)
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"associate_with_wifi_profile_once",
|
|
forbidden_association,
|
|
)
|
|
|
|
with pytest.raises(RuntimeError, match="не подтвердил готовность"):
|
|
asyncio.run(
|
|
service.connect(
|
|
ConnectRequest(
|
|
device_id="k1-a",
|
|
connection_mode="quick-connect",
|
|
compatibility_attestation=QUICK_CONNECT_ATTESTATION,
|
|
)
|
|
)
|
|
)
|
|
|
|
quick_sessions = sorted(service.evidence_root.glob("*viewer_k1_ap_association*"))
|
|
assert len(quick_sessions) == 1
|
|
assert (quick_sessions[0] / "ap-activation.redacted.json").exists()
|
|
assert not (quick_sessions[0] / "manifest.redacted.json").exists()
|
|
|
|
|
|
def test_failed_connection_change_revokes_the_previous_route(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
_set_scanned_devices(service, [{"device_id": "k1-a", "name": "XGR-TEST-A"}])
|
|
service._selected_device_id = "previous-k1" # noqa: SLF001
|
|
service._k1_ip = "192.168.1.20" # noqa: SLF001
|
|
service._connection_mode = "bridge" # noqa: SLF001
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"ensure_wifi_profile_from_credential_source",
|
|
lambda *_args, **_kwargs: {
|
|
"schema_version": 1,
|
|
"adapter": "macOS Keychain",
|
|
"available": True,
|
|
"profile_enrolled": False,
|
|
"credential_source": "exact-firmware-profile",
|
|
},
|
|
)
|
|
|
|
@asynccontextmanager
|
|
async def fake_activation_session(*_: object, **__: object) -> AsyncIterator[dict[str, Any]]:
|
|
yield {
|
|
"profile_id": "xgrids-k1-fw3-quick-connect-ap-v1",
|
|
"started_at_utc": "2026-07-19T15:00:00Z",
|
|
"completed_at_utc": "2026-07-19T15:00:01Z",
|
|
"outcome": "ap_ready_observed",
|
|
"ready_observed": True,
|
|
"write_performed": True,
|
|
"write_mode": "with_response",
|
|
}
|
|
|
|
def failed_association(*_: object, **__: object) -> dict[str, Any]:
|
|
raise facade_module.HostWifiProfileError(
|
|
"network-not-found",
|
|
scan_attempt_count=4,
|
|
scan_elapsed_ms=15014,
|
|
)
|
|
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"device_ap_activation_session",
|
|
fake_activation_session,
|
|
)
|
|
monkeypatch.setattr(facade_module, "associate_with_wifi_profile_once", failed_association)
|
|
|
|
with pytest.raises(RuntimeError, match="network-not-found"):
|
|
asyncio.run(
|
|
service.connect(
|
|
ConnectRequest(
|
|
device_id="k1-a",
|
|
connection_mode="quick-connect",
|
|
compatibility_attestation=QUICK_CONNECT_ATTESTATION,
|
|
)
|
|
)
|
|
)
|
|
|
|
state = service.state()
|
|
assert state["selected_device_id"] is None
|
|
assert state["k1_ip"] is None
|
|
assert state["connection_mode"] is None
|
|
assert state["compatibility"]["attestation"] is None
|
|
quick_sessions = sorted(service.evidence_root.glob("*viewer_k1_ap_association*"))
|
|
failure_evidence = json.loads(
|
|
(quick_sessions[0] / "host-wifi-association.redacted.json").read_text(encoding="utf-8")
|
|
)
|
|
assert failure_evidence["reason_code"] == "network-not-found"
|
|
assert failure_evidence["scan_attempt_count"] == 4
|
|
assert failure_evidence["scan_elapsed_ms"] == 15014
|
|
|
|
|
|
def test_network_provisioning_rejects_an_ipv4_owned_by_the_local_host(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
caplog: pytest.LogCaptureFixture,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
_set_scanned_devices(service, [{"device_id": "k1-a"}])
|
|
|
|
async def fake_provision(*_: object, **__: object) -> dict[str, Any]:
|
|
return {
|
|
"started_at_utc": "2026-07-18T15:19:25Z",
|
|
"completed_at_utc": "2026-07-18T15:19:26Z",
|
|
"profile_id": "xgrids-k1-fw3-wifi-v1",
|
|
"outcome": "lan_address_observed",
|
|
"observations": [{"status": {"ipv4": "10.255.254.51"}}],
|
|
}
|
|
|
|
monkeypatch.setattr(facade_module, "provision_wifi_once", fake_provision)
|
|
monkeypatch.setattr(facade_module, "_target_is_local_ipv4", lambda _target: True)
|
|
|
|
with (
|
|
caplog.at_level(logging.ERROR, logger=facade_module.__name__),
|
|
pytest.raises(RuntimeError, match="уже принадлежит этому компьютеру"),
|
|
):
|
|
asyncio.run(
|
|
service.connect(
|
|
ConnectRequest(
|
|
device_id="k1-a",
|
|
ssid="lab-network",
|
|
password=SecretStr(PRIMARY_TEST_CREDENTIAL),
|
|
compatibility_attestation=ATTESTATION,
|
|
)
|
|
)
|
|
)
|
|
|
|
state = service.state()
|
|
assert state["selected_device_id"] is None
|
|
assert state["k1_ip"] is None
|
|
operation = next(item for item in state["operations"] if item["action"] == "network.provision")
|
|
assert operation["status"] == "failed"
|
|
assert operation["error"]["safe_to_retry"] is False
|
|
assert operation["error"]["side_effect_status"] == "unknown"
|
|
failure_log = next(
|
|
record
|
|
for record in caplog.records
|
|
if getattr(record, "event_code", None) == "k1_network_provision_failed"
|
|
)
|
|
assert failure_log.operation_stage == "ble-provisioning-write"
|
|
assert failure_log.connection_mode == "bridge"
|
|
assert failure_log.error_code == "RuntimeError"
|
|
assert failure_log.safe_to_retry is False
|
|
assert failure_log.side_effect_status == "unknown"
|
|
assert failure_log.network_change_attempted is True
|
|
assert PRIMARY_TEST_CREDENTIAL not in caplog.text
|
|
assert "lab-network" not in caplog.text
|
|
|
|
|
|
def test_provisioning_cannot_switch_device_during_active_acquisition(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
_set_scanned_devices(service, [{"device_id": "k1-a"}])
|
|
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")
|