fix(k1): stabilize repeated acquisition and live viewer recovery

This commit is contained in:
DCCONSTRUCTIONS
2026-07-29 01:55:12 +03:00
parent 37b8930527
commit 2a97cf28c0
28 changed files with 2776 additions and 286 deletions
+25
View File
@@ -136,6 +136,31 @@ def test_live_ingress_allows_only_one_worker_consumer() -> None:
ingress.open_consumer("worker-2")
def test_live_ingress_new_session_discards_queued_events_from_previous_session() -> None:
ingress = LivePerceptionIngress()
ingress.open_consumer("worker-1")
ingress.begin_session("session-1")
assert ingress.publish(
modality="lidar",
source_id="lixel/application/report/lio_pcl",
source_sequence=1,
captured_at_epoch_ns=1,
received_monotonic_ns=1,
payload=b"old-session",
)
ingress.end_session("session-1")
ingress.begin_session("session-2")
events = []
while (event := ingress.take_next("worker-1", timeout=0)) is not None:
events.append(event)
assert len(events) == 1
assert events[0].session_id == "session-2"
assert events[0].modality == "control"
assert events[0].payload == b'{"event":"session-start"}'
def test_live_result_round_trip_keeps_video_mask_boxes_and_shadow_authority() -> None:
mask = np.zeros((600, 800), dtype=np.uint8)
mask[100:120, 200:240] = 4
+40 -16
View File
@@ -1,6 +1,8 @@
from __future__ import annotations
import json
import logging
import socket
import struct
import threading
import time
@@ -20,6 +22,7 @@ from k1link.viewer.rerun_bridge import (
RerunSceneSettings,
_live_time_panel,
_point_colors,
_select_available_grpc_port,
)
@@ -62,6 +65,33 @@ class DisconnectFailureRecording(FakeRecording):
raise RuntimeError("synthetic disconnect failure")
def test_rerun_port_selection_skips_a_recording_still_held_by_a_viewer(
caplog: pytest.LogCaptureFixture,
) -> None:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as occupied:
occupied.bind(("0.0.0.0", 0))
occupied.listen()
preferred_port = int(occupied.getsockname()[1])
selected_port = _select_available_grpc_port(preferred_port, search_span=8)
recording = FakeRecording()
with caplog.at_level(
logging.INFO,
logger="k1link.device_plugins.xgrids_k1.viewer_receiver",
):
bridge = RerunBridge(
grpc_port=preferred_port,
recording_factory=lambda _: recording, # type: ignore[arg-type]
)
assert selected_port != preferred_port
assert preferred_port < selected_port < preferred_port + 8
assert caplog.records[-1].event_code == "rerun_grpc_port_rotated"
assert caplog.records[-1].preferred_port == preferred_port
assert caplog.records[-1].selected_port == selected_port
bridge.close()
def _message(
topic: str,
payload: bytes,
@@ -266,7 +296,7 @@ def test_palettes_are_deterministic_and_custom_color_is_exact() -> None:
assert custom_over_rgb.tolist() == [[16, 32, 48], [16, 32, 48]]
def test_runtime_reuses_one_bridge_across_sequential_sessions(tmp_path: Path) -> None:
def test_runtime_owns_fresh_bridge_for_each_sequential_session(tmp_path: Path) -> None:
capture = tmp_path / "mqtt.raw.k1mqtt"
point_topic = "RealtimePointcloud"
pose_topic = "RealtimePath"
@@ -300,10 +330,12 @@ def test_runtime_reuses_one_bridge_across_sequential_sessions(tmp_path: Path) ->
encoding="utf-8",
)
recording = FakeRecording()
recordings: list[FakeRecording] = []
created: list[RerunBridge] = []
def bridge_factory(**kwargs: object) -> RerunBridge:
recording = FakeRecording()
recordings.append(recording)
bridge = RerunBridge(
recording_factory=lambda _: recording, # type: ignore[arg-type]
**kwargs, # type: ignore[arg-type]
@@ -329,8 +361,8 @@ def test_runtime_reuses_one_bridge_across_sequential_sessions(tmp_path: Path) ->
assert snapshot["metrics"]["mqtt_to_publish_ms"] is None
runtime.stop(wait_seconds=5.0)
assert runtime.snapshot()["phase"] == "idle"
assert runtime.snapshot()["rerun_grpc_url"] == "rerun+http://127.0.0.1:9876/proxy"
assert recording.disconnected is False
assert runtime.snapshot()["rerun_grpc_url"] is None
assert recordings[0].disconnected is True
runtime.start_replay(capture, speed=0.0)
deadline = time.monotonic() + 5.0
@@ -340,13 +372,12 @@ def test_runtime_reuses_one_bridge_across_sequential_sessions(tmp_path: Path) ->
time.sleep(0.05)
runtime.stop(wait_seconds=5.0)
assert len(created) == 1
assert len(created) == 2
assert runtime.snapshot()["metrics"]["pcl_frames"] == 1
assert runtime.snapshot()["rerun_grpc_url"] == "rerun+http://127.0.0.1:9876/proxy"
assert recording.disconnected is False
assert runtime.snapshot()["rerun_grpc_url"] is None
assert all(recording.disconnected for recording in recordings)
runtime.close()
assert runtime.snapshot()["rerun_grpc_url"] is None
assert recording.disconnected is True
def test_runtime_reports_bridge_close_failure_instead_of_false_idle(tmp_path: Path) -> None:
@@ -385,18 +416,11 @@ def test_runtime_reports_bridge_close_failure_instead_of_false_idle(tmp_path: Pa
time.sleep(0.01)
snapshot = runtime.snapshot()
assert snapshot["phase"] == "idle"
assert snapshot["rerun_grpc_url"] == "rerun+http://127.0.0.1:9876/proxy"
assert recording.disconnected is False
with pytest.raises(RuntimeError, match="synthetic disconnect failure"):
runtime.close()
snapshot = runtime.snapshot()
assert snapshot["phase"] == "error"
assert "synthetic disconnect failure" in snapshot["message"]
assert snapshot["rerun_grpc_url"] is None
assert recording.disconnected is True
runtime.close()
def test_close_during_blocked_factory_closes_the_late_bridge(tmp_path: Path) -> None:
+142
View File
@@ -0,0 +1,142 @@
from __future__ import annotations
import json
import logging
import stat
from collections.abc import Callable
from pathlib import Path
from typing import Any
import pytest
from fastapi import APIRouter
from fastapi.routing import APIRoute
from pydantic import ValidationError
from k1link.web.runtime_diagnostics import (
SCANNER_LOGGER_NAME,
configure_scanner_diagnostics,
)
from k1link.web.viewer_diagnostics_api import (
LiveViewerDiagnosticEvent,
build_viewer_diagnostics_router,
)
def _endpoint(router: APIRouter, path: str, method: str) -> Callable[..., Any]:
for route in router.routes:
if (
isinstance(route, APIRoute)
and route.path == path
and route.methods is not None
and method in route.methods
):
return route.endpoint
raise AssertionError(f"{method} {path} route is missing")
def test_private_scanner_diagnostics_are_durable_structured_and_bounded(
tmp_path: Path,
) -> None:
target = configure_scanner_diagnostics(tmp_path / "logs")
logger = logging.getLogger(f"{SCANNER_LOGGER_NAME}.test")
logger.error(
"field control failure",
extra={
"event_code": "k1_application_control_session_failed",
"reason_code": "mqtt_network_loop_failed",
"mqtt_loop_result_code": 7,
"mqtt_loop_result_name": "The connection was lost.",
"mqtt_loop_phase": "post-publish-drain",
"automatic_retry": False,
"camera_source_id": "sensor.camera.right",
"evidence_session_id": "20260728T163450Z_viewer_live",
"activation_trigger": "application-control-scanning",
"network_change_admissible": True,
"network_change_reconciliation": (
"explicit-network-change-only-after-acknowledged-stop"
),
"lease_generation": 3,
"lease_state": "reachable",
"recovery_strategy": "existing-mqtt-endpoint",
"endpoint_reachable": True,
"address_changed": False,
"device_write_performed": False,
"preferred_port": 9876,
"selected_port": 9877,
"unapproved_secret_field": "must-not-be-written",
},
)
for handler in logging.getLogger(SCANNER_LOGGER_NAME).handlers:
handler.flush()
document = json.loads(target.read_text(encoding="utf-8").splitlines()[-1])
assert stat.S_IMODE(target.parent.stat().st_mode) == 0o700
assert stat.S_IMODE(target.stat().st_mode) == 0o600
assert document["event_code"] == "k1_application_control_session_failed"
assert document["reason_code"] == "mqtt_network_loop_failed"
assert document["mqtt_loop_result_code"] == 7
assert document["mqtt_loop_phase"] == "post-publish-drain"
assert document["automatic_retry"] is False
assert document["camera_source_id"] == "sensor.camera.right"
assert document["evidence_session_id"] == "20260728T163450Z_viewer_live"
assert document["activation_trigger"] == "application-control-scanning"
assert document["network_change_admissible"] is True
assert document["network_change_reconciliation"] == (
"explicit-network-change-only-after-acknowledged-stop"
)
assert document["lease_generation"] == 3
assert document["lease_state"] == "reachable"
assert document["recovery_strategy"] == "existing-mqtt-endpoint"
assert document["endpoint_reachable"] is True
assert document["address_changed"] is False
assert document["device_write_performed"] is False
assert document["preferred_port"] == 9876
assert document["selected_port"] == 9877
assert "unapproved_secret_field" not in document
parent = logging.getLogger(SCANNER_LOGGER_NAME)
for handler in list(parent.handlers):
if getattr(handler, "baseFilename", None) == str(target):
parent.removeHandler(handler)
handler.close()
def test_live_viewer_diagnostic_endpoint_accepts_only_bounded_events(
caplog: pytest.LogCaptureFixture,
) -> None:
router = build_viewer_diagnostics_router()
endpoint = _endpoint(router, "/api/v1/viewer/live-diagnostics", "POST")
event = LiveViewerDiagnosticEvent(
schema_version="missioncore.live-viewer-diagnostic/v1",
event_code="live_receiver_stalled",
failure_stage="receiver-stalled",
stream_id="acquisition-123",
backend_activity_sequence=8_572,
viewer_range_max_ns=231_000_000_000,
stalled_for_ms=5_500,
recovery_attempt=1,
)
with caplog.at_level(
logging.INFO,
logger="k1link.device_plugins.xgrids_k1.viewer_receiver",
):
response = endpoint(event)
assert response.status_code == 204
assert "event=live_receiver_stalled" in caplog.text
assert caplog.records[-1].failure_stage == "receiver-stalled"
with pytest.raises(ValidationError):
LiveViewerDiagnosticEvent.model_validate(
{
**event.model_dump(),
"source_url": "http://192.168.56.1/private",
}
)
fallback = LiveViewerDiagnosticEvent(
schema_version="missioncore.live-viewer-diagnostic/v1",
event_code="live_receiver_active_store_admitted",
stream_id="acquisition-123",
backend_activity_sequence=8_573,
)
assert fallback.failure_stage is None
+57
View File
@@ -128,3 +128,60 @@ def test_read_wifi_status_once_reads_only_and_returns_current_dhcp_address(
assert result["operation"] == "single_reviewed_wifi_status_read"
assert result["write_performed"] is False
assert result["status"]["ipv4"] == "10.255.254.77"
def test_read_wifi_status_recovery_rediscover_ignores_retained_handle(
monkeypatch: pytest.MonkeyPatch,
) -> None:
value = bytearray(54)
value[0] = 11
value[1:12] = b"WIFI_CLIENT"
value[33] = 4
value[34:38] = bytes((10, 255, 254, 77))
value[50] = 1
stale_handle = object()
recovered_handle = object()
characteristic = SimpleNamespace(
uuid=wifi_module.STATUS_CHARACTERISTIC_UUID,
service_uuid=wifi_module.SERVICE_UUID,
properties=["read"],
)
service = SimpleNamespace(uuid=wifi_module.SERVICE_UUID)
class FakeServices:
def get_service(self, uuid: str) -> object | None:
return service if uuid == wifi_module.SERVICE_UUID else None
def get_characteristic(self, uuid: str) -> object | None:
return characteristic if uuid == wifi_module.STATUS_CHARACTERISTIC_UUID else None
class FakeClient:
def __init__(self, device: object, **_kwargs: object) -> None:
assert device is recovered_handle
self.services = FakeServices()
self.name = "XGR-K1"
async def __aenter__(self) -> Any:
return self
async def __aexit__(self, *_args: object) -> None:
return None
async def read_gatt_char(self, _characteristic: object) -> bytes:
return bytes(value)
async def rediscover(*_args: object, **_kwargs: object) -> object:
return recovered_handle
monkeypatch.setattr(wifi_module, "discovered_device", lambda _uuid: stale_handle)
monkeypatch.setattr(wifi_module.BleakScanner, "find_device_by_address", rediscover)
monkeypatch.setattr(wifi_module, "BleakClient", FakeClient)
result = asyncio.run(
read_wifi_status_once(
"synthetic-corebluetooth-uuid",
rediscover=True,
)
)
assert result["status"]["ipv4"] == "10.255.254.77"
+485 -50
View File
@@ -247,6 +247,8 @@ def test_verify_connection_refreshes_dynamic_dhcp_address_and_rotates_session(
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,
@@ -283,7 +285,7 @@ def test_implicit_acquisition_target_uses_current_ble_dhcp_address(
assert state["acquisition"]["target_host"] == "10.255.254.77"
def test_control_session_opens_against_current_ble_dhcp_address(
def test_control_session_reuses_reachable_process_owned_connection_lease(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
@@ -291,6 +293,8 @@ def test_control_session_opens_against_current_ble_dhcp_address(
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] = []
@@ -302,12 +306,12 @@ def test_control_session_opens_against_current_ble_dhcp_address(
opened_hosts.append(host)
return self.snapshot()
async def fake_status_read(*_: object, **__: object) -> dict[str, Any]:
return _wifi_status_read("10.255.254.77")
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", fake_status_read)
monkeypatch.setattr(facade_module, "_target_is_local_ipv4", lambda _target: False)
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(
@@ -320,8 +324,241 @@ def test_control_session_opens_against_current_ble_dhcp_address(
)
)
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(
@@ -384,18 +621,24 @@ def test_acquisition_is_unbounded_by_default_and_accepts_ten_hour_hint() -> None
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"
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",
@@ -628,6 +871,91 @@ def test_plugin_commanded_acquisition_keeps_start_and_stop_as_explicit_actions(
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(
@@ -1611,6 +1939,9 @@ def test_runtime_error_terminalizes_pending_graceful_stop(tmp_path: Path) -> Non
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(
@@ -1970,6 +2301,11 @@ def test_network_provisioning_is_single_flight_and_secret_is_unwrapped_only_at_b
}
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(
@@ -2005,6 +2341,117 @@ def test_network_provisioning_is_single_flight_and_secret_is_unwrapped_only_at_b
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)
service._devices = [{"device_id": "k1-a", "name": "XGR-TEST-A"}] # noqa: SLF001
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)
service._devices = [{"device_id": "k1-a", "name": "XGR-TEST-A"}] # noqa: SLF001
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,
@@ -2028,9 +2475,7 @@ def test_quick_connect_activates_the_device_ap_then_associates_the_host(
)
@asynccontextmanager
async def fake_activation_session(
device_id: str, **_: object
) -> AsyncIterator[dict[str, Any]]:
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
@@ -2092,9 +2537,7 @@ def test_quick_connect_activates_the_device_ap_then_associates_the_host(
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][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"
@@ -2102,9 +2545,7 @@ def test_quick_connect_activates_the_device_ap_then_associates_the_host(
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"
)
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
@@ -2112,9 +2553,12 @@ def test_quick_connect_activates_the_device_ap_then_associates_the_host(
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"
assert (
service._camera_target_for_session( # noqa: SLF001
state["device_session"]["device_session_id"]
)
== "192.168.56.1"
)
prepared = service.prepare_acquisition(
PrepareAcquisitionRequest(
@@ -2158,6 +2602,11 @@ def test_direct_connect_reuses_the_reviewed_ble_provisioning_frame(
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(
@@ -2171,14 +2620,10 @@ def test_direct_connect_reuses_the_reviewed_ble_provisioning_frame(
)
)
assert provisioning_calls == [
("k1-a", "controller-hotspot", PRIMARY_TEST_CREDENTIAL)
]
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"
)
assert state["compatibility"]["attestation"]["topology"] == ("controller-hotspot")
def test_quick_connect_missing_credential_provider_stops_before_ap_write(
@@ -2232,9 +2677,7 @@ def test_quick_connect_missing_credential_provider_stops_before_ap_write(
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"
)
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
@@ -2259,9 +2702,7 @@ def test_quick_connect_does_not_start_host_wifi_without_ap_ready_flag(
)
@asynccontextmanager
async def not_ready_session(
*_: object, **__: object
) -> AsyncIterator[dict[str, Any]]:
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",
@@ -2325,9 +2766,7 @@ def test_failed_connection_change_revokes_the_previous_route(
)
@asynccontextmanager
async def fake_activation_session(
*_: object, **__: object
) -> AsyncIterator[dict[str, Any]]:
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",
@@ -2370,9 +2809,7 @@ def test_failed_connection_change_revokes_the_previous_route(
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"
)
(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
@@ -2413,9 +2850,7 @@ def test_network_provisioning_rejects_an_ipv4_owned_by_the_local_host(
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"
)
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"
+33
View File
@@ -778,3 +778,36 @@ def test_post_publish_timeout_poisoned_transport_never_retries() -> None:
required_response_operation_keys={"bootstrap:1:DeviceInfoRequest"},
)
assert len(fake.publish_calls) == 1
def test_network_loop_failure_keeps_exact_paho_result_and_phase() -> None:
class LoopFailureClient(FakeControlClient):
def loop(self, timeout: float) -> mqtt.MQTTErrorCode:
if self.publish_calls and not self.events:
return mqtt.MQTT_ERR_CONN_LOST
return super().loop(timeout)
fake = LoopFailureClient()
transport = ReviewedApplicationMqttTransport(
"192.168.1.20",
client_factory=lambda: cast(mqtt.Client, fake),
)
transport.open()
with pytest.raises(
ApplicationCommandOutcomeUnknown,
match=r"phase=post-publish-drain, result=7: The connection was lost",
):
transport.exchange_batch_once(
[_envelope()],
required_response_operation_keys={"bootstrap:1:DeviceInfoRequest"},
)
snapshot = transport.snapshot().as_dict()
assert snapshot["state"] == "poisoned"
assert snapshot["last_loop_result_code"] == int(mqtt.MQTT_ERR_CONN_LOST)
assert snapshot["last_loop_result_name"] == mqtt.error_string(
int(mqtt.MQTT_ERR_CONN_LOST)
)
assert snapshot["last_loop_phase"] == "post-publish-drain"
assert len(fake.publish_calls) == 1
+208
View File
@@ -170,6 +170,7 @@ def test_canonical_stages_require_operator_events_but_device_standby_does_not(
monkeypatch: pytest.MonkeyPatch,
) -> None:
FakeExecutor.records = []
scanning_observed = threading.Event()
monkeypatch.setattr(
session_module,
"PhysicalAcceptanceDialogueExecutor",
@@ -181,6 +182,7 @@ def test_canonical_stages_require_operator_events_but_device_standby_does_not(
loader,
transport_factory=lambda _host: transport, # type: ignore[arg-type]
epoch_seconds=lambda: 1_752_680_000,
scanning_observer=scanning_observed.set,
)
session.open(
@@ -202,6 +204,7 @@ def test_canonical_stages_require_operator_events_but_device_standby_does_not(
session.request_start(project_name="TEST001", confirmation=_confirmation())
_wait_phase(session, "scanning")
assert scanning_observed.wait(timeout=1.0)
assert FakeExecutor.records[-1] == "wait:stop"
session.request_stop(confirmation=_confirmation())
@@ -210,6 +213,7 @@ def test_canonical_stages_require_operator_events_but_device_standby_does_not(
assert completed["pending_operator_action"] is None
assert loader.calls == 1
assert completed["automatic_retry"] is False
assert completed["scanning_observer_errors"] == 0
assert completed["scripted_transitions"] is False
assert FakeExecutor.records == [
"connection:1-6",
@@ -562,6 +566,210 @@ def test_start_outcome_unknown_blocks_reopen_even_when_transport_is_closed(
assert failed["can_open"] is False
assert failed["failure"]["modeling_command_attempted"] is True # type: ignore[index]
assert failed["failure"]["safe_to_retry"] is False # type: ignore[index]
with pytest.raises(
session_module.ApplicationAcceptanceError,
match="cannot be retired",
):
session.retire_for_network_change()
def test_acknowledged_stop_disconnect_allows_only_explicit_network_change(
monkeypatch: pytest.MonkeyPatch,
) -> None:
@dataclass
class StopDisconnectTransportSnapshot:
state: str
def as_dict(self) -> dict[str, object]:
return {
"state": self.state,
"publish_attempts": 15,
"qos2_completions": 15,
"correlated_responses": 13,
"ignored_known_responses": 304,
"late_known_responses": 0,
"device_status_reports": 52,
"latest_device_session_state": "scan_stopping",
"latest_device_project_bound": True,
"latest_system_error_code": None,
"last_loop_result_code": 7,
"last_loop_result_name": "The connection was lost.",
"last_loop_phase": "maintain-open",
"automatic_retry": False,
"automatic_reconnect": False,
}
class StopDisconnectTransport(FakeTransport):
def snapshot(self) -> StopDisconnectTransportSnapshot:
return StopDisconnectTransportSnapshot(self.state)
class StopDisconnectExecutor(FakeExecutor):
def __init__(self, transport: StopDisconnectTransport) -> None:
super().__init__(transport)
self.stop_attempted = False
self.stop_complete = False
def execute_canonical_start(self, *_args: object, **_kwargs: object) -> object:
self.records.append("start:11-14")
return object()
def execute_canonical_stop(self, *_args: object, **_kwargs: object) -> object:
self.records.append("stop")
self.stop_attempted = True
self.stop_complete = True
return object()
def maintain_post_stop_until_standby(self) -> None:
self.records.append("wait:device-standby")
self.transport.state = "poisoned"
raise session_module.ApplicationCommandOutcomeUnknown(
"control MQTT network loop returned an error",
reason_code="mqtt_network_loop_failed",
)
def snapshot(self) -> dict[str, object]:
return {
"dialogue_stage": (
"stop-acknowledged" if self.stop_complete else "test-stage"
),
"start_attempted": True,
"start_complete": True,
"stop_attempted": self.stop_attempted,
"stop_complete": self.stop_complete,
"response_evidence": [],
"automatic_retry": False,
}
FakeExecutor.records = []
monkeypatch.setattr(
session_module,
"PhysicalAcceptanceDialogueExecutor",
StopDisconnectExecutor,
)
session = InteractiveApplicationControlSession(
FakeAuthorityLoader(),
transport_factory=lambda host: StopDisconnectTransport(host), # type: ignore[arg-type]
)
session.open(
host="192.168.1.20",
timezone_name="Europe/Moscow",
confirmation=_confirmation(),
)
_wait_phase(session, "connection-ready")
session.enter_workspace()
_wait_phase(session, "workspace-ready")
session.open_project_prompt()
_wait_phase(session, "project-ready")
session.request_start(project_name="TEST001", confirmation=_confirmation())
_wait_phase(session, "scanning")
session.request_stop(confirmation=_confirmation())
failed = _wait_phase(session, "failed")
failed_thread = session._thread # noqa: SLF001
assert failed_thread is not None
failed_thread.join(timeout=2.0)
assert not failed_thread.is_alive()
failed = session.snapshot()
assert failed["can_open"] is False
assert failed["outcome_unknown"] is True
failure = failed["failure"]
assert isinstance(failure, dict)
assert failure["safe_to_retry"] is False
assert failure["network_change_admissible"] is True
assert failure["network_change_reconciliation"] == {
"device_session_state": "scan_stopping",
"device_project_bound": True,
"system_error_code": None,
"stop_complete": True,
"standby_confirmed": False,
"decision": "explicit-network-change-only-after-acknowledged-stop",
"automatic_retry": False,
}
retired = session.retire_for_network_change()
assert retired["state"] == "idle"
assert retired["failure"] is None
assert retired["can_open"] is True
def test_prestart_loop_failure_allows_only_fresh_explicit_retry_after_ready_status(
monkeypatch: pytest.MonkeyPatch,
) -> None:
@dataclass
class ReconciledTransportSnapshot:
state: str
def as_dict(self) -> dict[str, object]:
return {
"state": self.state,
"publish_attempts": 4,
"qos2_completions": 4,
"correlated_responses": 3,
"ignored_known_responses": 0,
"late_known_responses": 0,
"device_status_reports": 1,
"latest_device_session_state": "ready",
"latest_device_project_bound": False,
"latest_system_error_code": None,
"last_loop_result_code": 7,
"last_loop_result_name": "The connection was lost.",
"last_loop_phase": "post-publish-drain",
"automatic_retry": False,
"automatic_reconnect": False,
}
class ReconciledTransport(FakeTransport):
def snapshot(self) -> ReconciledTransportSnapshot:
return ReconciledTransportSnapshot(self.state)
class PrestartLoopFailureExecutor(FakeExecutor):
def run_connection_stage(
self,
_orchestrator: object,
) -> LiveDeviceControlBinding:
raise session_module.ApplicationCommandOutcomeUnknown(
"control MQTT network loop returned an error",
reason_code="mqtt_network_loop_failed",
)
monkeypatch.setattr(
session_module,
"PhysicalAcceptanceDialogueExecutor",
PrestartLoopFailureExecutor,
)
session = InteractiveApplicationControlSession(
FakeAuthorityLoader(),
transport_factory=lambda host: ReconciledTransport(host), # type: ignore[arg-type]
)
session.open(
host="192.168.1.20",
timezone_name="Europe/Moscow",
confirmation=_confirmation(),
)
failed_thread = session._thread # noqa: SLF001
assert failed_thread is not None
failed_thread.join(timeout=2.0)
assert not failed_thread.is_alive()
failed = session.snapshot()
assert failed["state"] == "failed"
assert failed["outcome_unknown"] is True
assert failed["automatic_retry"] is False
assert failed["can_open"] is True
failure = failed["failure"]
assert isinstance(failure, dict)
assert failure["reason_code"] == "mqtt_network_loop_failed"
assert failure["modeling_command_attempted"] is False
assert failure["safe_to_retry"] is True
assert failure["status_reconciliation"] == {
"device_session_state": "ready",
"device_project_bound": False,
"system_error_code": None,
"decision": "safe-explicit-prestart-retry",
"automatic_retry": False,
}
def test_unavailable_transport_snapshot_after_publish_blocks_reopen(